서버 하나에만 데이터를 보관하면 그 서버가 멈췄을 때 요청을 처리할 수 없다. 다른 서버가 대신 처리할 수 있도록 데이터를 복제해 두더라도, 어느 서버의 데이터를 기준으로 삼을지 정해야 한다.
한 서버에는 변경 사항이 반영됐지만 다른 서버에는 아직 전달되지 않은 경우에 요청을 처리하던 서버가 멈춘다면, 역할을 넘겨받은 서버는 어디까지 처리됐는지 어떻게 알 수 있을까. Raft 논문은 여러 서버가 처리할 명령과 그 순서를 정하고, 장애가 발생해도 확정된 기록을 보존하는 방법을 설명한다.
논문의 출발점
왜 이해하기 쉬운 알고리즘이 필요했을까
In Search of an Understandable Consensus Algorithm은 Diego Ongaro와 John Ousterhout가 2014년에 발표한 논문이다. Paxos는 여러 서버가 하나의 결정에 합의하도록 하는 기존 알고리즘이다. 저자들은 Paxos의 동작 원리를 이해하기 어렵고, 실제 시스템을 구현하려면 논문에서 충분히 설명하지 않은 세부 동작을 개발자가 보완해야 한다는 점에 주목했다. Raft의 설계 목표는 다음 세 가지로 정리할 수 있다.
- 동작 원리와 그 이유를 이해하기 쉬워야 한다. 각 규칙이 어떻게 동작하는지, 그리고 왜 필요한지 알 수 있어야 한다.
- 실제 시스템으로 구현하기 쉬워야 한다. 리더 선출, 로그 복제, 장애 후 복구가 어떻게 연결되는지 구체적으로 설명해, 구현에 필요한 동작을 개발자가 각자 추론해야 하는 부담을 줄여야 한다.
- 이해하기 쉽게 설계하더라도 합의의 정확성과 일반적인 상황에서의 처리 효율은 유지해야 한다.
이를 위해 논문은 합의를 리더 선출, 로그 복제, 안전성으로 나누어 설명한다. 리더는 요청의 순서를 정하고 복제를 이끄는 서버를 말한다. 먼저 리더를 선출하고, 처리할 명령을 로그에 기록해 다른 서버에 복제한다. 이 과정에서 리더가 바뀌더라도 이미 확정된 기록은 보존돼야 한다.
… Raft separates the key elements of consensus, such as leader election, log replication, and safety, …
Raft는 리더 선출, 로그 복제, 안전성 같은 합의의 핵심 요소를 분리한다.
Paxos는 하나의 값을 결정하는 합의에서 출발한다. Multi-Paxos는 로그의 각 위치에 어떤 명령을 넣을지 반복해서 합의하는 방식이다. 따라서 로그 복제를 비교할 때는 Multi-Paxos와 Raft를 함께 보는 것이 적절하다. Multi-Paxos에서도 리더가 명령의 순서를 정하고 제안을 주도한다.
In normal operation, a single server is elected to be the leader, …
정상 동작 중에는 서버 하나가 리더로 선출된다.
표를 좌우로 밀어 비교할 수 있습니다.
| 구분 | Paxos / Multi-Paxos | Raft |
|---|---|---|
| 출발점 | 하나의 값을 결정하는 합의에서 출발해, 로그의 각 위치에 어떤 명령을 넣을지 반복해서 합의한다. | 처음부터 순서가 있는 로그 전체를 복제하는 문제로 구성한다. |
| 리더의 역할 | Multi-Paxos에서도 리더가 명령의 순서를 정하고 제안을 주도한다. | 리더가 명령의 순서를 정하고 로그를 복제하며, 확정된 위치를 다른 서버에 알린다. |
| 리더가 바뀔 때 | 새 리더가 과반수로부터 이전에 수락한 제안들을 확인해 이어갈 값을 결정한다. | 투표 조건으로 후보를 제한해, 선출된 리더가 이미 확정된 로그를 가지고 있도록 한다. |
저자들은 실제로 Raft가 이해하기 쉬운지 확인하기 위해, 학생들에게 Raft와 Paxos를 가르친 뒤 문제를 풀게 해 이해도를 비교했다. 논문에서는 이 비교와 함께 알고리즘의 정확성과 성능도 별도로 평가한다.
다만 규칙을 나누어 읽는 것만으로는 각각의 규칙이 왜 필요한지 알기 어렵다. 요청이 서로 다른 순서로 도착하거나, 일부 서버에만 기록이 남은 채 리더가 멈추는 상황을 고려해야 그 이유를 이해하기 쉽다.
같은 명령, 같은 순서
같은 요청도 처리 순서에 따라 결과가 달라진다
쇼핑몰에서 한 상품의 재고 수량을 두 서버에 복제해 저장하는 경우를 생각해 보자. 처음에는 두 서버 모두 그 상품의 재고를 20개로 저장하고 있다. 두 서버에 저장된 값은 동일한 상품의 재고를 가리키므로, 실제 재고 수량은 20개다.
여기에 재고 수량을 10개로 변경하는 요청과, 상품 3개가 판매돼 재고를 차감하는 요청이 들어왔다. 두 요청을 어떤 순서로 처리하느냐에 따라 최종 수량이 달라진다.
표를 좌우로 밀어 비교할 수 있습니다.
| 처리 순서 | 저장된 재고 수량 |
|---|---|
| 10개로 변경 → 3개 차감 | 7개 |
| 3개 차감 → 10개로 변경 | 10개 |
처리 순서에 따른 차이를 보여주기 위해 만든 예시다.
이 예시에서 데이터는 상품의 재고 수량이고, 명령은 ‘재고를 10개로 변경’하거나 ‘재고에서 3개를 차감’하는 동작이다. 로그는 이 명령들을 순서대로 기록한 것이다. 두 서버가 같은 재고 수량을 얻으려면 명령뿐 아니라 그 순서도 같아야 한다. 상태 머신은 로그를 읽고 명령을 하나씩 실행해, 실제 재고 수량을 바꾸는 로직을 담당한다.
Each state machine takes as input commands from its log.
각 상태 머신은 자신의 로그에 있는 명령을 입력으로 받는다.
물론 같은 상태에서 같은 명령을 실행하면 결과도 같아야 한다. 즉 결정적(deterministic)으로 동작해야 한다. 여기서 상태는 현재 저장된 재고 수량이다. ‘무작위 수량을 차감’하라는 명령을 받아 각 서버가 따로 수량을 고른다면, 같은 순서로 실행해도 최종 재고는 달라질 수 있다. 대신 수량을 먼저 정해 ‘3개 차감’이라는 명령으로 기록하면 각 서버가 동일하게 3개를 차감할 수 있다.
Raft는 복제할 명령과 그 순서를 맞추고, 상태 머신은 그 명령을 결정적으로 실행한다. 한 서버가 두 번째 명령까지 적용하는 동안 다른 서버는 첫 번째 명령까지만 적용했을 수 있다. 뒤처진 서버도 같은 출발 상태에서 같은 명령들을 순서대로 적용하면 최종적으로 같은 재고 수량에 도달한다.
§5.2 · 리더 선출
리더가 멈추면 누가 선거를 시작할까
이제 A·B·C 세 서버에 같은 재고 정보를 복제하는 경우를 살펴보자. A가 리더라면 요청의 순서를 정하고 로그를 B와 C에 전달한다. 팔로워는 B와 C처럼 리더의 로그를 받아 복제하는 서버를 말한다. 새 요청이 없을 때도 A는 주기적으로 heartbeat를 보내 자신이 리더로 동작하고 있음을 알린다.
팔로워는 현재 임기에 맞는 리더의 heartbeat나 로그 복제 요청을 받으면 선거 대기 시간을 다시 센다. 후보에게 투표한 경우에도 대기 시간을 다시 센다. 이후 리더의 요청을 받거나 후보에게 투표하는 일 없이 대기 시간이 끝나면 스스로 후보가 되어 선거를 시작한다. 각 서버는 정해진 범위 안에서 대기 시간을 무작위로 고르므로, 리더와의 통신이 끊겨도 후보가 되는 시점은 서로 다를 수 있다. 이 경우 타이밍에 따라 특정 한 서버가 먼저 후보가 될 수도 있고, 복수의 서버가 거의 동시에 후보가 될 수도 있다.
A가 멈춘 뒤 B의 대기 시간이 먼저 끝난 경우, B는 자신의 임기(term) 번호를 하나 높이고, 후보로 전환해 자신에게 투표한다. 그런 다음 A와 C에게 같은 임기의 투표를 요청한다. 임기는 선거 차수를 구분하는 번호다. 후보가 된 B는 자기 표 한 개를 가지고 다른 서버의 표를 모으기 시작한다.
아직 후보가 되지 않은 C가 요청을 받으면 후보의 임기와 로그를 확인한다. B의 선거 임기가 C의 현재 임기보다 낮지 않고, B의 로그가 C보다 뒤처지지 않았으며, C가 그 임기에 다른 후보에게 투표하지 않았다면 B에게 표를 준다. C가 B에게 투표하면 대기 시간을 처음부터 다시 세므로, 자신의 선거를 시작하기까지 다시 기다리게 된다. B는 자기 표와 C의 표를 합쳐 두 표를 얻고 리더가 된다. 과반수는 현재 응답하는 서버가 아니라 구성원으로 정해진 전체 서버 수를 기준으로 한다. 따라서 A가 멈췄어도 세 서버 중 두 표가 필요하다.
If election timeout elapses without receiving AppendEntries RPC from current leader or granting vote to candidate: convert to candidate
현재 리더의 AppendEntries RPC를 받거나 후보에게 투표하는 일 없이 선거 대기 시간이 끝나면 후보로 전환한다.
B와 C의 대기 시간이 거의 동시에 끝나 둘 다 같은 임기의 후보가 될 수도 있다. 이때는 각각 자신에게 투표한다. 각 서버는 한 임기에 최대 한 후보에게만 투표하고 그 선택을 번복하지 않으므로, 서로의 로그가 같아도 상대에게 표를 줄 수 없다. A가 멈춘 상태라면 둘 다 한 표에 머문다. 다시 대기 시간이 지나면 후보는 임기를 높여 새 선거를 시작하며, 이때도 대기 시간을 무작위로 골라 같은 상황이 반복될 가능성을 줄인다.
이미 후보가 된 C라도 더 높은 임기의 요청을 받으면 새 임기를 받아들이고 팔로워로 돌아간다. 이전 임기에 자신에게 투표했더라도, 새 임기에서는 다른 후보에게 투표할 수 있다. 다만 임기가 높다는 이유만으로 표를 주는 것은 아니다. 요청한 후보의 로그가 자신보다 뒤처지지 않았는지도 확인해야 한다.
§5.3–5.4 · 복제와 확정
뒤처진 서버가 새 리더가 되어도 괜찮을까
투표할 때 후보의 로그를 비교하는 이유는, 리더가 바뀌더라도 이미 확정된 기록을 보존해야 하기 때문이다. A가 리더로 동작하는 동안 주문 처리 서비스가 A에게 ‘재고에서 3개 차감’을 요청한 경우를 생각해 보자. 여기서 주문 처리 서비스는 쇼핑몰의 주문을 받아 재고 변경을 요청하는 클라이언트다. 이후 차감 예시에서는 ‘10개로 변경’ 명령이 이미 적용돼, 재고가 10개인 상태를 가정한다. A가 ‘3개 차감’ 명령을 현재 임기의 로그에 기록하는 과정은 다음과 같다.
- A가 자신의 로그에 ‘재고에서 3개 차감’ 명령을 저장하고 B와 C에게 복제를 요청한다.
- B가 명령을 로그에 저장한 뒤 A에게 저장을 마쳤다고 응답한다. 이때 C에는 아직 명령이 전달되지 않은 것으로 가정한다.
- A는 자신과 B, 즉 세 서버 중 두 서버에 현재 임기의 명령이 저장됐음을 확인하고 이를 확정한다.
이 기록들은 서버가 재시작하더라도 남아 있어야 한다. 따라서 서버는 관련 요청에 응답하기 전에 변경된 로그와 현재 임기, 투표한 대상을 영구 저장 장치에 기록해야 한다. 투표 기록도 함께 보존해야 재시작 후 같은 임기에 다른 후보에게 다시 투표하는 일을 막을 수 있다.
Updated on stable storage before responding to RPCs
RPC에 응답하기 전에 영구 저장 장치에 변경 사항을 기록한다.
B는 로그 저장을 마쳤다고 A에게 응답한다. 이 시점에는 아직 재고를 차감하지 않았으며, A로부터 확정 사실을 전달받지도 않았다. A는 B의 저장 응답을 받아 과반수에 기록됐음을 확인한 뒤 명령을 확정한다. 확정(commit)은 명령을 실제 데이터에 적용해도 되며, 이후 리더가 바뀌어도 버리지 않는다는 결정이다.
A는 확정된 명령을 순서대로 실행해 실제 재고 수량을 바꾸고, 클라이언트에게 결과를 응답한다. B에는 후속 로그 복제 요청이나 heartbeat로 어디까지 확정됐는지 알린다. B도 이 정보를 받으면 해당 명령을 자신의 재고 데이터에 적용한다. 로그 저장, 확정 사실의 전달, 실제 데이터에 적용하는 시점은 서로 다를 수 있다.
표를 좌우로 밀어 비교할 수 있습니다.
| 단계 | 재고 예시에서의 의미 |
|---|---|
| 저장 | ‘3개 차감’ 명령을 로그에 남긴다. 아직 재고 수량을 바꾼 것은 아니다. |
| 확정 | 해당 명령을 실제 데이터에 적용해도 되며, 이후 리더가 바뀌어도 보존해야 한다고 결정한다. |
| 적용 | 확정된 명령을 순서대로 실행해 실제 재고에서 3개를 차감한다. |
그런데 A가 명령을 확정한 직후, B에게 확정 사실을 알리기 전에 멈춘 경우를 생각해 보자. B에는 명령이 남아 있지만 C에는 없다. 이때 C를 새 리더로 삼아 C의 로그만 기준으로 처리한다면 이미 확정한 차감 명령이 빠지게 된다. 이를 막기 위해 투표하는 서버는 후보의 로그가 자신의 로그보다 뒤처졌으면 표를 주지 않는다. B는 확정 사실을 아직 몰라도 로그 자체는 가지고 있으므로 C에게 투표하지 않는다. A가 멈춘 상태에서 C는 자기 표만으로 과반수를 얻을 수 없다.
여기서는 서버의 현재 임기와 로그 항목에 기록된 임기를 구분해야 한다. 서버가 현재 진행 중인 선거와 리더를 구분하기 위해 관리하는 임기 번호가 있고, 각 로그 항목에 그 항목이 만들어진 때의 임기 번호가 남아 있다. 예를 들어 B가 7번 임기의 선거를 시작해도, B의 마지막 로그가 5번 임기에 만들어졌다면 그 항목에는 여전히 5가 기록돼 있다. 즉 선거를 시작하면서 서버의 현재 임기를 높이더라도, 이미 저장된 로그 항목의 임기는 그대로 유지된다.
로그의 최신 여부를 비교할 때는 각 서버의 마지막 로그 항목에 기록된 임기 번호를 먼저 비교한다. 그 번호가 같으면 마지막 항목의 순번(index)을 비교한다. 이 순번은 해당 항목이 로그에서 몇 번째에 있는지를 나타낸다. 마지막 로그의 임기가 6인 C에게, 마지막 로그의 임기가 5인 B가 투표를 요청하면 C는 거절한다. B가 새로 시작한 선거 임기가 7이어도 로그의 비교 결과는 달라지지 않는다.
그렇다고 전체 서버 중 로그가 가장 앞선 서버만 리더가 되는 것은 아니다. 예를 들어 마지막 로그의 임기가 모두 같고, A에는 100번까지, B와 C에는 99번까지 저장된 경우를 생각해 보자. B가 후보가 되어 자신과 C의 표를 얻으면 A의 표 없이도 리더가 될 수 있다. A에만 있는 100번 항목은 과반수에 복제되지 않은 미확정 기록이다. 따라서 로그를 가장 많이 가진 서버가 반드시 선출되는 것은 아니지만, 선출된 서버는 이미 확정된 기록을 모두 가지고 있어야 한다.
Raft는 선거·복제·확정 규칙을 통해 새 리더가 이미 확정된 기록을 모두 가지고 있도록 보장한다. 확정된 기록은 과반수의 서버에 저장돼 있고, 다음 리더도 과반수의 표를 받아야 한다. 따라서 그 기록을 저장한 서버들의 집합과 새 리더에게 투표한 서버들의 집합은 적어도 한 서버에서 겹친다. 앞의 세 서버 예시에서 A·B에 명령이 저장돼 확정됐고, 새 리더가 B·C의 표를 받는다면 두 집합에는 모두 B가 포함된다. 따라서 새 리더가 되려면 그 기록을 가진 B의 투표 조건도 만족해야 한다.
다만 투표하는 서버가 후보의 로그를 전부 받아 자신이 가진 기록이 하나도 빠짐없이 들어 있는지 검사하는 것은 아니다. 마지막 로그의 임기와 순번으로 비교하므로, 후보의 마지막 로그 임기가 더 높으면 미확정 기록이 일부 달라도 표를 줄 수 있다. 따라서 확정된 기록이 보존되는 이유를 이해하려면, 투표 조건과 함께 복제·확정 규칙도 살펴봐야 한다. Raft는 리더가 자신의 로그를 기준으로 복제하고, 같은 임기·순번의 항목이 일치하면 그 앞의 로그도 일치하도록 검사한다. 이 조건에 더해 현재 임기의 항목을 과반수에 복제해야 확정할 수 있도록 제한한다. 이 규칙들이 함께 적용돼야 확정된 기록이 다음 리더에게도 보존된다. 다음 절에서는 논문의 §5.4.2 예시를 통해 이 확정 조건이 필요한 이유를 살펴본다.
… the voter denies its vote if its own log is more up-to-date than that of the candidate.
투표하는 서버는 자신의 로그가 후보의 로그보다 최신이면 표를 주지 않는다.
§5.4.2 · 이전 임기의 로그
과반수에 저장됐는데도 확정할 수 없는 경우
앞의 차감 예시는 A가 자신의 현재 임기에 만든 명령을 과반수에 복제한 경우였다. 하지만 새 리더가 이전 임기에 만들어진 미확정 항목을 이어받아 복제하는 경우도 있다. 이때는 과반수에 저장됐다는 사실만으로 확정하지 않는다. 원논문의 그림 8에 나온 리더 교체 과정을 A·B·C·D·E 다섯 서버로 살펴보자. 과반수는 세 서버이며, 처음에는 모두 1번 로그까지 같은 기록을 가지고 있다고 가정한다.
표를 좌우로 밀어 비교할 수 있습니다.
| 단계 | 리더와 로그의 변화 |
|---|---|
| 2번 임기 | 리더 A가 2번 위치에 명령 X를 만들고 A·B에만 저장한 뒤 멈춘다. 다섯 서버 중 두 서버에만 저장됐으므로 X는 미확정이다. |
| 3번 임기 | E가 C·D·E의 표로 리더가 된다. 같은 2번 위치에 명령 Y를 만들지만 E에만 저장한 뒤 멈춘다. Y도 미확정이다. |
| 4번 임기 | 복구된 A가 A·B·C의 표로 리더가 된다. 기존 X를 C에도 복제해 A·B·C 세 서버가 X를 갖게 된다. X에 기록된 임기는 여전히 2다. |
이 시점의 리더는 4번 임기의 A이며, A·B·C에는 2번 임기에 만든 X가 있다. E에는 3번 임기에 만든 Y가 남아 있다. X와 Y는 같은 2번 위치에 들어갈 서로 다른 명령이므로 두 기록을 모두 유지할 수는 없다. 이후 A가 멈추는 경우와, 멈추기 전에 현재 임기의 명령을 추가로 복제하는 경우를 나누어 살펴보자.
먼저 A가 X만 세 서버에 저장한 상태에서 멈추고, E가 돌아와 5번 임기의 선거를 시작하는 경우를 생각해 보자. E의 마지막 로그는 3번 임기에 만든 Y이고, C의 마지막 로그는 2번 임기에 만든 X다. C는 E의 로그를 더 최신으로 판단하므로 E에게 투표할 수 있다. E는 C·D·E의 표로 리더가 되어, 2번 위치의 미확정 X를 자신의 Y로 교체할 수 있다. 즉 X가 과반수에 저장돼 있더라도, X가 없는 E가 리더로 선출될 수 있다.
그렇다고 E가 리더가 된 순간 Y가 확정되는 것은 아니다. Y도 3번 임기에 만든 기록이므로, 5번 임기의 리더 E에게는 이전 임기의 미확정 기록이다. E가 현재 임기인 5번 임기에 새 명령 W를 만들어 Y 뒤에 추가하고 과반수에 복제해 확정하면, 그 앞의 Y도 함께 확정된다. 이 경우 최종적으로 이어지는 기록은 Y → W이며, 충돌하는 X는 로그를 맞추는 과정에서 제거된다.
이번에는 A가 멈추기 전에 현재 임기의 명령을 추가로 복제하는 경우를 살펴보자. A가 4번 임기에 새 명령 Z를 만들어 X 다음인 3번 위치에 추가하고, A·B·C에 복제한다. 세 서버의 마지막 로그 임기는 이제 4다. 마지막 로그 임기가 3인 E는 이들에게 표를 받을 수 없고, 남은 D와 자신의 표만으로는 과반수를 얻지 못한다. A는 자신의 현재 임기인 4에서 만든 Z를 확정하고, 그 앞의 X도 함께 확정할 수 있다. Z를 복제할 때 앞선 로그의 일치도 확인하므로, Z를 저장한 서버에는 그 앞의 X도 같은 순서로 남는다. 이 경우 최종적으로 이어지는 기록은 X → Z이며, E의 미확정 Y는 E가 다시 합류해 로그를 맞추는 과정에서 제거된다.
표를 좌우로 밀어 비교할 수 있습니다.
| 진행 | 확정되는 기록 | 제거되는 미확정 기록 |
|---|---|---|
| A가 X만 복제하고 멈춘 뒤, E가 리더가 되어 현재 임기의 W까지 확정 | Y → W | X |
| A가 현재 임기의 Z까지 과반수에 복제하고 확정 | X → Z | Y |
첫 번째 경우에서 X를 ‘세 서버에 있으니 확정됐다’고 판단해 실행했다면, 이후 X가 Y로 교체될 때 이미 처리한 결과를 보존하지 못한다. 그래서 Raft는 그 시점의 X를 미확정으로 남겨 두고 적용하지 않는다. 두 번째 경우처럼 현재 임기의 Z까지 확정하면 앞선 X도 함께 확정돼 적용할 수 있다. 따라서 새 리더는 이전 임기의 미확정 기록을 과반수에 복제했다는 이유만으로 확정해서는 안 된다. 현재 임기의 항목을 과반수에 복제해 확정하면 앞선 기록도 함께 확정되며, 이후 리더가 바뀌더라도 보존된다.
Raft never commits log entries from previous terms by counting replicas.
Raft는 복제본 수를 세는 것만으로 이전 임기의 로그 항목을 확정하지 않는다.
§6–7 · 구성 변경과 로그 압축
서버 구성이 바뀌거나 로그가 계속 쌓인다면
앞선 예시에서는 합의에 참여하는 서버 구성이 바뀌지 않는다고 가정했다. 서버를 추가하거나 제거하면 누가 투표에 참여하는지도 달라진다. 여기서 서버 구성이란 합의에 참여하는 서버들의 목록을 말한다. 이 목록을 한 번에 변경하더라도 모든 서버가 변경 사항을 동시에 전달받지는 못한다. 일부는 이전 목록으로, 다른 일부는 새 목록으로 과반수를 계산하면 서로 다른 결정을 내릴 수 있다.
논문의 joint consensus는 구성 변경 과정에서 이전 구성과 새 구성의 동의를 모두 요구하는 방식이다. 예를 들어 A·B·C에 D와 E를 추가한다면, 이 단계에서는 이전 구성 A·B·C 중 둘 이상과 새 구성 A·B·C·D·E 중 셋 이상의 동의가 모두 필요하다. A·D·E가 동의해도 이전 구성에서는 A 하나뿐이므로 충분하지 않다. 두 구성의 과반수를 함께 요구하는 단계를 거친 뒤 새 구성으로 전환한다.
The joint consensus combines both the old and new configurations:
joint consensus는 이전 구성과 새 구성을 함께 사용한다.
요청을 처리할수록 로그가 쌓이는 문제도 있다. 스냅샷은 특정 시점까지 명령을 적용한 결과와, 마지막으로 적용한 로그의 위치를 함께 저장한 것이다. 재고 예시에서 1번 명령이 ‘10개로 변경’, 2번 명령이 ‘3개 차감’이고 둘 다 확정해 적용했다면, 스냅샷에는 결과인 재고 7개와 2번까지 적용했다는 위치를 함께 남긴다.
복구할 때는 이 스냅샷을 읽어 재고 수량을 7개로 복원한 뒤, 3번부터의 확정된 명령을 순서대로 적용한다. 2번 명령을 다시 실행하면 이미 반영한 차감을 반복하므로, 중복 실행을 방지해야 한다. 따라서 결과뿐 아니라 적용 위치도 필요한 것이다.
스냅샷으로 보존한 구간의 로그는 정리할 수 있다. 이렇게 하면 모든 변경 기록을 계속 보관하거나, 복구할 때 처음부터 전부 재생하는 부담을 줄일 수 있다. 복제가 늦은 서버에는 보통 빠진 로그를 보내지만, 필요한 구간이 이미 정리돼 없다면 그 로그를 보낼 수는 없다. 이때 리더가 스냅샷을 보내면 뒤처진 서버는 해당 시점의 상태를 복원한 뒤, 그 이후의 로그를 이어서 받아 적용한다.
§8 · 클라이언트 처리
처리는 끝났는데 응답을 받지 못했다면
재고 변경을 요청한 주문 처리 서비스에서는 어떤 문제가 생길 수 있을까. 리더 A가 ‘3개 차감’ 명령을 확정해 실제 재고에 적용한 뒤, 성공 응답을 보내기 전에 멈춘 경우를 생각해 보자. 주문 처리 서비스가 확인할 수 있는 것은 응답을 받지 못했다는 사실뿐이다. 요청이 처리되지 않은 것인지, 처리는 끝났는데 응답만 받지 못한 것인지 구분할 수 없다.
이 상태에서 새 리더에게 ‘3개 차감’을 다시 요청하는 경우, 새 리더가 이를 별개의 요청으로 처리하면 이미 차감한 재고에서 다시 3개를 차감하게 된다. 같은 요청이 서로 다른 로그 항목으로 기록되면, 각 항목을 순서대로 복제하고 실행하더라도 같은 작업을 두 번 처리하게 된다. 명령의 순서를 맞추는 것과 같은 요청이 다시 왔는지 알아보는 것은 별도로 처리해야 한다.
이를 방지하기 위해 논문에서는 클라이언트가 요청마다 고유한 번호를 붙이고, 상태 머신이 각 클라이언트의 마지막 처리 번호와 결과를 보관하는 방식을 설명한다. 예를 들어 주문 처리 서비스가 42번 요청으로 차감을 보냈다면, 재시도할 때도 같은 42번을 보낸다. 상태 머신이 이미 처리한 42번 요청을 확인하면 재고를 다시 차감하지 않고 보관한 결과를 돌려준다. 이 처리 기록도 복제되는 상태의 일부로 유지해야 새 리더가 같은 요청을 알아볼 수 있다.
However, as described so far Raft can execute a command multiple times:
하지만 지금까지 설명한 방식만으로는 Raft가 명령 하나를 여러 번 실행할 수 있다.
재고를 바꾸지 않고 조회만 하더라도 문제가 생길 수 있다. A와 나머지 두 서버 사이의 통신이 끊겨 B가 새 리더로 선출된 경우를 생각해 보자. B와 C에서는 재고 변경이 계속되지만, A는 새 리더가 생겼다는 사실을 모른 채 예전 재고 수량을 응답할 수 있다. 따라서 조회만 하는 경우에도 해당 서버의 데이터가 최신 상태인지 확인해야 한다.
이런 오래된 응답을 막으면서 읽기 요청마다 로그를 추가하지 않으려면, 리더는 세 가지를 확인해야 한다. 어디까지 확정됐는지, 이번 읽기를 처리하는 시점에도 자신이 리더인지, 그리고 읽을 데이터에 확정된 명령이 반영됐는지다.
먼저 새 리더가 로그를 가지고 있더라도, 어디까지 확정됐는지는 모를 수 있다는 점을 고려해야 한다. 앞의 예시에서 B는 ‘3개 차감’을 저장했지만 A의 확정 통지를 받지 못했다. 이를 100번 로그라고 가정해 보자. B는 리더가 된 뒤 자신의 임기로 101번 항목을 만들고 과반수에 복제해 확정한다. 그러면 앞서 설명한 규칙에 따라 100번까지도 함께 확정된다. 새 요청이 없다면 재고를 바꾸지 않는 빈 명령(no-op)을 101번에 넣을 수 있다. 빈 명령 자체는 재고를 바꾸지 않지만, 앞의 기록까지 확정할 수 있게 한다.
한 번 리더로 선출됐다는 사실만으로 이후의 모든 읽기에 응답할 수 있는 것은 아니다. B도 나중에 다른 서버들과 통신이 끊긴 채 자신이 여전히 리더라고 생각할 수 있다. 따라서 읽기 요청을 받은 뒤 과반수와 heartbeat를 주고받아, 자신의 임기로 리더 역할을 계속 수행할 수 있는지 확인한다. 이를 확인하지 못했다면 자신이 가진 값이 최신 상태라고 판단해 응답해서는 안 된다.
마지막으로 확정된 명령이 실제 데이터에 적용됐는지도 확인해야 한다. 앞에서 살펴본 것처럼 로그의 저장, 확정, 적용은 서로 다른 단계다. B의 로그에서 ‘3개 차감’이 확정됐더라도, 상태 머신이 아직 이 명령을 실행하지 않았다면 실제 재고 수량은 차감 전인 10개로 남아 있다. 이때 확정 여부만 확인하고 조회에 응답하면, 이미 확정된 차감이 반영되지 않은 값을 전달하게 된다. 따라서 B는 이번 조회에 필요한 확정 위치까지 명령을 순서대로 적용한 뒤, 차감이 반영된 7개를 읽어 응답해야 한다.
과반수와 요청 처리
서버가 살아 있어도 처리를 멈춰야 할 때
세 서버 중 하나가 멈췄더라도 나머지 둘이 원활히 통신할 수 있다면 요청 처리를 이어갈 수 있다. 하지만 동작하는 서버가 하나뿐이라면 새 요청을 확정할 수 없다. 전체 구성의 과반수에 도달하지 못하기 때문이다.
서버 세 대가 모두 동작하더라도 네트워크가 두 대와 한 대로 나뉘면 마찬가지다. 양쪽에서 서로 다른 변경을 확정한다면 다시 연결된 뒤 기록이 충돌한다. Raft에서는 과반수와 통신할 수 없는 쪽이 새 요청을 확정하지 못한다. 새 요청 처리가 멈추더라도 이미 확정된 기록은 보존된다.
앞의 재고 예시에서도 차감한 결과는 리더가 바뀐 뒤에 남아 있어야 하고, 응답을 받지 못해 재시도하더라도 같은 요청으로 다시 차감해서는 안 된다. 이를 위해 Raft의 합의 규칙으로 확정된 기록을 보존하고, 요청 번호로 중복 처리를 막는다. 따라서 여러 서버가 같은 결과를 유지하려면, 로그의 복제와 확정뿐 아니라 클라이언트의 재시도 처리까지 함께 고려해야 한다.