多数派收到才算提交
한국어 원문으로 표시합니다.
목표
리더가 받은 값이 어떻게 세 노드로 퍼지고, 어느 순간 '확정' 되는지를 규칙으로 씁니다. 끝나면 로그 일치성, 덮어쓰기, 커밋 조건, 그리고 뒤처진 노드가 따라잡는 과정을 손으로 겪은 상태가 됩니다.
왜 중요한가
복제는 '값을 여러 곳에 보낸다' 가 아닙니다. 보낸 것과 확정한 것을 구분하는 일입니다. 리더가 값을 받아 자기 로그에 적었다는 사실은 아무것도 보장하지 않습니다 — 그 리더가 다음 순간 죽으면 그 값은 없던 일이 될 수 있습니다. 과반이 같은 자리에 같은 항목을 갖고 있어야 비로소 '어떤 미래의 리더도 이 자리를 바꿀 수 없다' 가 성립하고, 그때서야 클라이언트에게 성공이라고 말할 수 있습니다.
커밋 조건에는 사람들이 거의 항상 빠뜨리는 단서가 하나 더 붙습니다 — 리더는 자기 임기의 항목이 과반에 닿았을 때만 커밋 번호를 올립니다. 앞 임기의 항목이 과반에 닿았다는 이유로 커밋하면, 그 뒤에 뽑힌 다른 리더가 같은 자리를 덮어써서 이미 확정했다고 말한 값이 사라질 수 있습니다.
단계
node.py와rules-for-replication.py를 제자리에 놓고 리더를 뽑아/root/raft/leader.json에 적습니다.raftrules.py에up_to_date를 더하고on_request_vote가 그 조건을 함께 보게 합니다./root/raft/raftlog.py에append_entry(log, term, value)를 씁니다.- 같은 파일에
log_ok(log, prev_index, prev_term)을 더합니다. - 같은 파일에
apply_entries(log, prev_index, entries)를 더합니다. - 같은 파일에
commit_index(counts, total, log, term)을 더합니다. - 값 세 개를 써서 세 노드가 모두 커밋하는 것을 보고
/root/raft/replicated.json에 적습니다. - 팔로워 하나를 죽였다 되살려 따라잡는 것을 보고
/root/raft/catchup.json에 적습니다.
참고
- 이 실습은 60분 안팎이 걸립니다. 세션 시간이 모자라면 화면에서 연장하세요. 실습 파드에는
볼륨이 없어서 세션이 끝나면
/root의 작업물이 사라집니다 — 남기고 싶은 코드는 따로 복사해 두세요. - 값 쓰기:
curl -s -XPOST -d '{"value":"x"}' 127.0.0.1:5001/client - 로그 보기:
curl -s 127.0.0.1:5001/log - 상태 보기:
curl -s 127.0.0.1:5001/status(commit 과 log_len 을 함께 보세요) - 흔한 실수 1: 항목이 없는 심장박동에도 로그를 자르는 것. 어긋날 때만 자릅니다.
- 흔한 실수 2: 넘겨받은 로그 목록을 그 자리에서 고치는 것. 새 목록을 만들어 돌려주세요.
앞 실습의 결과를 다시 세운다
/opt/lab/raft/node.py 와 /opt/lab/raft/rules-for-replication.py 를 /root/raft/node.py 와 /root/raft/raftrules.py 로 놓고, 세 노드를 띄워 뽑힌 리더를 /root/raft/leader.json 에 적으세요.
실습은 매번 새 파드에서 시작하므로 앞 실습에서 만든 것이 남아 있지 않습니다. 선거 규칙은 앞 실습의 답을 그대로 드립니다. leader.json 에는 leader 와 term 두 열쇠를 적으세요.
뒤처진 후보에게는 표를 주지 않는다
raftrules.py 에 up_to_date(log, last_index, last_term) 를 더하고, on_request_vote 가 표를 주기 전에 그 조건도 함께 보게 고치세요.
로그가 생기면 선거에 조건이 하나 붙습니다 — 후보의 로그가 내 것만큼은 최신이어야 표를 줍니다. 비교는 길이가 먼저가 아닙니다. 마지막 항목의 임기를 먼저 보고, 그것이 같을 때만 길이로 가릅니다. 이 조건이 없으면 뒤처진 노드가 리더가 되어 이미 커밋된 항목을 덮어쓸 수 있습니다.
클라이언트 값이 로그에 붙는다
/root/raft/raftlog.py 에 append_entry(log, term, value) 를 쓰세요. term 과 value 를 가진 항목을 끝에 붙인 새 목록을 돌려줍니다.
항목에는 값만이 아니라 그때의 임기가 함께 붙습니다. 그 임기가 나중에 두 로그를 비교하는 유일한 근거가 됩니다. 넘겨받은 목록을 그 자리에서 고치지 말고 새 목록을 만들어 돌려주세요 — 배선이 원본을 따로 들고 있습니다.
로그 일치성
raftlog.py 에 log_ok(log, prev_index, prev_term) 을 더하세요. prev_index 자리의 임기가 prev_term 과 같은지 판정합니다.
Raft 는 로그 전체를 비교하지 않습니다. 한 자리만 맞으면 그 앞은 전부 같다고 봅니다 — 그 성질을 로그 일치성(Log Matching)이라고 합니다. prev_index 는 1부터 세는 자리 번호이고, 0 은 '앞자리가 없다' 는 뜻입니다. 내 로그가 prev_index 보다 짧으면 비교할 것 자체가 없습니다.
어긋나는 자리부터 잘라 낸다
raftlog.py 에 apply_entries(log, prev_index, entries) 를 더하세요. 어긋나는 자리를 만나면 그 자리부터 잘라 내고 새 항목을 붙인 목록을 돌려줍니다.
핵심은 '언제 자르지 않는가' 입니다. 같은 임기의 같은 항목이 다시 온 것뿐이라면 뒤를 건드리면 안 됩니다 — 재전송이 잦은 환경에서 매번 자르면 팔로워의 로그가 계속 짧아졌다 길어집니다. 항목이 하나도 없는 심장박동도 마찬가지입니다.
커밋 조건
raftlog.py 에 commit_index(counts, total, log, term) 을 더하세요. counts 는 각 서버가 가진 항목 수이고, 과반이 가졌으면서 그 자리가 현재 임기의 항목인 가장 높은 번호를 돌려줍니다.
'과반이 복제했으면 커밋' 만으로는 부족합니다. 앞 임기의 항목이 과반에 닿았다는 이유로 커밋하면, 그 뒤에 뽑힌 리더가 그 자리를 다른 항목으로 덮어쓸 수 있습니다 — 논문 그림 8 의 사고입니다. 그래서 리더는 자기 임기의 항목이 과반에 닿을 때만 커밋 번호를 올리고, 그때 앞의 것들이 함께 따라옵니다.
세 노드에 값을 쓴다
세 노드를 띄우고 리더에게 값 세 개를 써서 세 노드 모두 commit 이 3 이 되는 것을 확인한 뒤, /root/raft/replicated.json 에 values, commit, log_len 을 적으세요.
값은 리더에게만 씁니다: curl -s -XPOST -d '{"value":"x"}' 127.0.0.1:5001/client. 팔로워에게 보내면 거절하면서 리더가 누구인지 알려 줍니다. 쓴 직후에는 commit 이 아직 오르지 않은 것이 정상입니다 — 팔로워의 응답이 돌아오고 다음 심장박동이 나가야 반영됩니다.
되살아난 노드가 따라잡는다
팔로워 하나를 죽였다 되살려 빈 로그에서 세 칸을 따라잡는 것을 확인하고, /root/raft/catchup.json 에 node, log_len, commit 을 적으세요.
이 습작은 로그를 디스크에 남기지 않습니다. 그래서 되살아난 노드는 아무것도 모르는 상태로 돌아오고, 리더가 보내는 자리 번호를 한 칸씩 뒤로 물리며 맞는 지점을 찾아 거기서부터 채워 줍니다. 죽이는 법은 pkill -f 'node.py --id 2 ' 처럼 번호까지 지정하면 됩니다. 되살린 뒤 log_len 이 올라가는 것을 눈으로 보세요.