LabHub
学习 学习路径 课程

当时有两个领导者

亲手编写选举领导者的规则

在 LabHub 中继续学习

한국어 원문으로 표시합니다.

목표

세 개의 프로세스가 서로 표를 주고받아 리더 하나를 뽑는 과정을 직접 구현합니다. 끝나면 임기(term), 투표, 과반, 그리고 타임아웃을 왜 무작위로 두는지를 손으로 겪은 상태가 됩니다.

왜 중요한가

합의 알고리즘이 어려운 이유는 수식이 아니라 경계입니다. 임기가 같을 때와 클 때, 아직 투표하지 않았을 때와 이미 했을 때, 표가 절반일 때와 절반을 넘었을 때 — 이 경계를 한 칸씩 잘못 잡으면 시스템은 평소에 멀쩡히 돌다가 네트워크가 흔들리는 날 두 개의 리더를 만들어 냅니다. 그래서 여기서는 배선(HTTP 서버·타이머·재전송)을 미리 드리고 판단하는 함수만 여러분이 씁니다. 판단이 곧 알고리즘이고, 나머지는 배관입니다.

마지막 단계는 Raft 논문이 무작위 타임아웃을 도입한 이유를 실측으로 보여 줍니다. 세 노드의 타이머를 똑같이 맞춰 두면 셋이 동시에 후보가 되어 각자 자기에게 한 표씩 주고, 아무도 과반에 닿지 못한 채 임기만 끝없이 올라갑니다.

단계

  1. /root/raft/cluster.json 에 세 노드의 id 와 포트를 적습니다.
  2. /opt/lab/raft/node.py/root/raft/node.py 로 복사하고 하나를 띄워 /status 를 확인합니다.
  3. /root/raft/raftrules.pyelection_timeout_ms(node_id, fixed_ms) 를 씁니다.
  4. 같은 파일에 on_timeout(state) 를 더합니다.
  5. 같은 파일에 on_request_vote(state, req) 를 더합니다.
  6. 같은 파일에 on_append_entries(state, req) 를 더합니다.
  7. 같은 파일에 has_majority(votes, total) 을 더하고, 세 노드를 띄워 리더가 뽑히는 것을 봅니다.
  8. 세 노드를 --timeout-ms 900 으로 띄우고 결과를 /root/raft/split-vote.json 에 적습니다.

참고

세 노드의 자리표

/root/raft/cluster.json 에 id 1, 2, 3 과 포트 5001, 5002, 5003 을 적으세요.

노드는 컨테이너가 아니라 포트만 다른 프로세스입니다. 나중에 세 번 띄울 때 쓸 자리표를 먼저 적어 둡니다. nodes 라는 목록 아래에 id 와 port 를 가진 객체 세 개를 두세요.

배선을 제자리에 놓고 띄워 본다

/opt/lab/raft/node.py/root/raft/node.py 로 복사하고, 한 개를 띄워 /status 가 답하는지 확인하세요.

HTTP 서버와 타이머 같은 배선은 이미 만들어 두었습니다. 여러분이 채울 것은 '판단' 뿐입니다. python3 /root/raft/node.py --id 1 --port 5001 --peers '' 로 띄우고 다른 창에서 curl 127.0.0.1:5001/status 를 부르세요. 규칙 파일이 아직 없으니 아무 판단도 하지 않고 가만히 있는 것이 정상입니다.

선거 타임아웃

/root/raft/raftrules.pyelection_timeout_ms(node_id, fixed_ms) 를 쓰세요. fixed_ms 가 0 이 아니면 그 값을 그대로, 0 이면 800 부터 1500 까지의 무작위 값을 돌려줍니다.

random.randint(a, b) 는 양 끝을 포함합니다. 왜 무작위여야 하는지는 마지막 단계에서 직접 보게 됩니다. fixed_ms 를 그대로 돌려주는 길을 반드시 남겨 두세요 — 그 길이 없으면 마지막 단계의 실험이 성립하지 않습니다.

타임아웃이 나면 후보가 된다

raftrules.pyon_timeout(state) 를 더하세요. 임기를 1 올리고 state 를 candidate 로, voted_for 를 자기 id 로 바꾼 딕셔너리를 돌려줍니다.

state 는 {'id', 'term', 'state', 'voted_for', 'leader', 'log', 'commit'} 를 가진 딕셔너리입니다. 바꾸고 싶은 열쇠만 담아 돌려주면 배선이 반영합니다. 후보가 자기에게 투표하는 것이 Raft 의 첫 한 표입니다 — 이것을 빠뜨리면 아무도 과반에 닿지 못합니다.

한 임기에 한 표

raftrules.pyon_request_vote(state, req) 를 더하세요. term, voted_for, state, granted 를 담은 딕셔너리를 돌려줍니다.

세 가지를 순서대로 보세요. (1) req 의 임기가 내 임기보다 크면 내 임기를 올리고 물러나며 이번 임기의 투표 기록을 지웁니다. (2) 임기가 내 것보다 작으면 거절합니다 — 이때 내 임기를 내리면 안 됩니다. (3) 같은 임기에서는 아직 아무에게도 안 줬거나 같은 후보에게 줬을 때만 줍니다. 같은 후보의 재전송에 거절하면 패킷 한 번 잃었다고 선거가 멈춥니다.

심장박동을 받는 쪽

raftrules.pyon_append_entries(state, req) 를 더하세요. term, state, leader, voted_for, ok 를 담은 딕셔너리를 돌려줍니다.

리더는 할 말이 없어도 계속 말을 겁니다 — 그것이 '나 아직 살아 있다' 는 유일한 신호이기 때문입니다. req 의 임기가 내 임기보다 작으면 ok 를 False 로 돌려주고 내 임기는 그대로 둡니다. 그 밖에는 팔로워로 돌아가 리더를 기록하고 ok 를 True 로 합니다. 후보로 있다가 같은 임기의 리더를 보면 물러나야 합니다.

과반을 세고 리더가 된다

raftrules.pyhas_majority(votes, total) 을 더한 뒤, 세 노드를 5001, 5002, 5003 에 띄워 리더가 하나 뽑히는 것을 확인하세요.

과반은 절반 '이상' 이 아니라 절반 '초과' 입니다. 4노드에서 2표는 과반이 아닙니다 — 정수 나눗셈으로 쓰면 이 경계에서 틀리기 쉬우니 양변에 2를 곱해 비교하세요. 세 노드를 띄운 뒤 curl 127.0.0.1:5001/status 를 세 포트에 대고 state 와 term 과 leader 를 함께 보세요.

타이머가 같으면 아무도 이기지 못한다

세 노드를 모두 --timeout-ms 900 으로 띄워 9초 뒤의 상태를 보고, /root/raft/split-vote.json 에 fixed_timeout_ms, leader, max_term, states 를 적으세요.

--timeout-ms 를 주면 배선이 세 노드의 깨어나는 시각을 벽시계에 맞춥니다 — 타이머가 정말로 같을 때 무슨 일이 나는지 보기 위한 장치입니다. 셋이 같은 순간에 후보가 되면 각자 자기에게 한 표씩 주고, 아무도 과반에 닿지 못한 채 임기만 올라갑니다. leader 는 null 로 적습니다.