リーダーを選ぶルールを自分で書く
한국어 원문으로 표시합니다.
목표
세 개의 프로세스가 서로 표를 주고받아 리더 하나를 뽑는 과정을 직접 구현합니다. 끝나면 임기(term), 투표, 과반, 그리고 타임아웃을 왜 무작위로 두는지를 손으로 겪은 상태가 됩니다.
왜 중요한가
합의 알고리즘이 어려운 이유는 수식이 아니라 경계입니다. 임기가 같을 때와 클 때, 아직 투표하지 않았을 때와 이미 했을 때, 표가 절반일 때와 절반을 넘었을 때 — 이 경계를 한 칸씩 잘못 잡으면 시스템은 평소에 멀쩡히 돌다가 네트워크가 흔들리는 날 두 개의 리더를 만들어 냅니다. 그래서 여기서는 배선(HTTP 서버·타이머·재전송)을 미리 드리고 판단하는 함수만 여러분이 씁니다. 판단이 곧 알고리즘이고, 나머지는 배관입니다.
마지막 단계는 Raft 논문이 무작위 타임아웃을 도입한 이유를 실측으로 보여 줍니다. 세 노드의 타이머를 똑같이 맞춰 두면 셋이 동시에 후보가 되어 각자 자기에게 한 표씩 주고, 아무도 과반에 닿지 못한 채 임기만 끝없이 올라갑니다.
단계
/root/raft/cluster.json에 세 노드의 id 와 포트를 적습니다./opt/lab/raft/node.py를/root/raft/node.py로 복사하고 하나를 띄워/status를 확인합니다./root/raft/raftrules.py에election_timeout_ms(node_id, fixed_ms)를 씁니다.- 같은 파일에
on_timeout(state)를 더합니다. - 같은 파일에
on_request_vote(state, req)를 더합니다. - 같은 파일에
on_append_entries(state, req)를 더합니다. - 같은 파일에
has_majority(votes, total)을 더하고, 세 노드를 띄워 리더가 뽑히는 것을 봅니다. - 세 노드를
--timeout-ms 900으로 띄우고 결과를/root/raft/split-vote.json에 적습니다.
참고
- 노드를 띄우는 법:
cd /root/raft && python3 node.py --id 1 --port 5001 --peers 2:5002,3:5003 & - 상태 보기:
curl -s http://127.0.0.1:5001/status - 모두 내리기:
pkill -f 'node.py --id' - 배선은 규칙 파일에 있는 함수만 부릅니다. 아직 안 쓴 함수 때문에 프로세스가 죽지는 않습니다.
- 흔한 실수 1: 낡은 임기의 요청을 받고 내 임기를 그쪽으로 내려 버리는 것. 임기는 올라가기만 합니다.
- 흔한 실수 2: 과반을
votes >= total / 2로 쓰는 것. 4노드에서 2표가 과반이 되어 리더가 둘 생깁니다.
세 노드의 자리표
/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.py 에 election_timeout_ms(node_id, fixed_ms) 를 쓰세요. fixed_ms 가 0 이 아니면 그 값을 그대로, 0 이면 800 부터 1500 까지의 무작위 값을 돌려줍니다.
random.randint(a, b) 는 양 끝을 포함합니다. 왜 무작위여야 하는지는 마지막 단계에서 직접 보게 됩니다. fixed_ms 를 그대로 돌려주는 길을 반드시 남겨 두세요 — 그 길이 없으면 마지막 단계의 실험이 성립하지 않습니다.
타임아웃이 나면 후보가 된다
raftrules.py 에 on_timeout(state) 를 더하세요. 임기를 1 올리고 state 를 candidate 로, voted_for 를 자기 id 로 바꾼 딕셔너리를 돌려줍니다.
state 는 {'id', 'term', 'state', 'voted_for', 'leader', 'log', 'commit'} 를 가진 딕셔너리입니다. 바꾸고 싶은 열쇠만 담아 돌려주면 배선이 반영합니다. 후보가 자기에게 투표하는 것이 Raft 의 첫 한 표입니다 — 이것을 빠뜨리면 아무도 과반에 닿지 못합니다.
한 임기에 한 표
raftrules.py 에 on_request_vote(state, req) 를 더하세요. term, voted_for, state, granted 를 담은 딕셔너리를 돌려줍니다.
세 가지를 순서대로 보세요. (1) req 의 임기가 내 임기보다 크면 내 임기를 올리고 물러나며 이번 임기의 투표 기록을 지웁니다. (2) 임기가 내 것보다 작으면 거절합니다 — 이때 내 임기를 내리면 안 됩니다. (3) 같은 임기에서는 아직 아무에게도 안 줬거나 같은 후보에게 줬을 때만 줍니다. 같은 후보의 재전송에 거절하면 패킷 한 번 잃었다고 선거가 멈춥니다.
심장박동을 받는 쪽
raftrules.py 에 on_append_entries(state, req) 를 더하세요. term, state, leader, voted_for, ok 를 담은 딕셔너리를 돌려줍니다.
리더는 할 말이 없어도 계속 말을 겁니다 — 그것이 '나 아직 살아 있다' 는 유일한 신호이기 때문입니다. req 의 임기가 내 임기보다 작으면 ok 를 False 로 돌려주고 내 임기는 그대로 둡니다. 그 밖에는 팔로워로 돌아가 리더를 기록하고 ok 를 True 로 합니다. 후보로 있다가 같은 임기의 리더를 보면 물러나야 합니다.
과반을 세고 리더가 된다
raftrules.py 에 has_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 로 적습니다.