LabHub
배우기 러닝패스 코스

리더가 둘이었다 · 리더가 둘이었다 · 이론

리더가 둘이었다

LabHub 에서 이어서 보기

한 줄 요약

네트워크가 갈라지면 같은 순간에 리더가 둘이 될 수 있다. Raft 가 막는 것은 그 상태가 아니라
같은 임기에 리더가 둘인 상태이고, 소수 쪽 리더는 아무것도 확정하지 못한 채 자기 기록이
버려지는 것을 나중에 알게 된다.

왜 이게 필요했나

분산 시스템의 사고는 대개 장애 자체가 아니라 불완전한 정보에서 온다. 떼어진 리더는
자기가 떼어졌다는 것을 모른다. 그가 아는 것은 '요즘 응답이 안 온다' 뿐이고, 그것은
상대가 죽었을 때와 구별되지 않는다. 그런데 Raft 의 리더에게는 스스로 물러나는 장치가
없다. 더 큰 임기를 보기 전까지는 자기가 리더라고 계속 믿는다.

그래서 그는 계속 리더처럼 행동한다. 클라이언트의 값을 받고, 자기 로그에 적고, 성공을
돌려준다. 같은 순간 반대편에서는 과반이 모여 더 높은 임기의 새 리더를 뽑는다. 이때
리더는 정말로 둘이다. 이것을 막을 방법이 없다는 것이 이 알고리즘의 출발점이고,
대신 막는 것은 두 리더가 같은 자리에 서로 다른 값을 확정하는 일이다.

어떻게 동작하나

소수 쪽 리더는 과반의 확인을 받을 수 없다. 자기 로그에 항목을 적을 수는 있어도 커밋
번호는 올라가지 않는다. 그래서 그 항목은 '기록되었지만 확정되지 않은' 상태로 남는다.

분할 중                        임기  로그                  커밋  옛 리더 (혼자)                 1   alpha, ghost           1  새 리더 + 팔로워 (둘)          2   alpha, real            2

두 항목이 같은 자리 번호 2번을 두고 다툰다는 것이 핵심이다. 한 자리에는 하나만
남을 수 있고, 어느 쪽이 남을지는 이미 정해져 있다 — 과반의 확인을 받은 쪽이다.

회복은 조용히 일어난다. 연결이 돌아오면 옛 리더는 임기 2의 심장박동을 보고 자기 임기를
2로 올리며 팔로워로 물러난다. 그다음 새 리더가 "네 로그의 1번 자리가 임기 1이면 그 뒤에
real 을 붙여라" 라고 말하고, 옛 리더의 2번 자리는 임기가 어긋나므로 잘려 나간다.
ghost 는 사라진다. 아무 경고도 나지 않는다.

여기서 선거 제한이 왜 필요한지가 분명해진다. 만약 회복 직후 옛 리더가 먼저 타임아웃이
나서 후보가 되고, 로그 길이만으로 표를 받을 수 있다면, 이미 커밋된 real 을 ghost 로
덮어쓸 수 있다. 마지막 항목의 임기를 먼저 보는 규칙이 그 길을 막는다.

왜 이런 설계를 받아들이는지도 함께 볼 값어치가 있다. 소수 쪽이 쓰기를 받지 못하게
막는 것은 가용성을 버리는 선택이다. 분할된 양쪽이 모두 쓰기를 받게 하면 서비스는
계속 돌지만, 회복할 때 두 갈래의 기록을 사람이 합쳐야 한다. Raft 를 쓰는 시스템은
그 반대를 고른다 — 소수 쪽은 멈추고, 대신 회복이 자동으로 끝난다. 메타데이터처럼
'틀린 값보다 없는 값이 나은' 자리에서는 이쪽이 옳다.

그래서 이 코스의 제목이 뜻하는 바도 '버그를 발견했다' 가 아니다. 리더가 둘인 순간은
정상 동작이고, 그 순간에도 확정된 값은 하나뿐이라는 것이 이 알고리즘의 약속이다.
사고가 나는 자리는 알고리즘 안이 아니라 그 바깥 — 소수 쪽 리더가 돌려준 성공 응답을
클라이언트가 확정으로 읽는 지점이다.

현장에서 만나는 모습

운영에서 이 사건은 "쓰기가 성공했다는데 값이 없다" 는 형태로 온다. 클라이언트는 소수 쪽
리더에게서 성공을 받았고, 몇 초 뒤 조회하면 그 값이 없다. 로그에는 오류가 없다 —
아무것도 실패하지 않았기 때문이다.

실제 구현들은 이 창을 좁히려고 애쓴다. 리더가 심장박동에 대한 과반의 응답을 일정 시간
받지 못하면 스스로 물러나는 방식(리스 기반)이 대표적이다. etcd 문서는 일시적인 분할로
정족수를 잃으면 네트워크가 돌아올 때 자동으로 안전하게 재개되지만, 영구적인 정족수
상실은 치명적이라고 적는다([etcd FAQ](https://etcd.io/docs/v3.5/faq/)). 같은 문서에
3노드의 과반은 2이고 견딜 수 있는 고장은 1대, 5노드는 각각 3과 2라는 표가 있다.

짝수 대를 쓰지 말라는 조언도 여기서 나온다. 4노드의 과반은 3이라 견딜 수 있는 고장이
1대로 3노드와 같은데, 고장 날 수 있는 기계만 하나 늘어난다. etcd 문서는 클러스터를
일곱 대 이하로 두라고 권하고, Google Chubby 가 다섯 대를 제안한다는 점을 함께 적는다.

또 하나 기억할 것은 '분할' 이 케이블이 끊기는 일만 뜻하지 않는다는 점이다. 방화벽 규칙
하나, 보안 그룹 변경 하나, 한쪽 방향만 막히는 비대칭 경로, 길게 멈춘 가비지 컬렉션 —
모두 같은 모양으로 나타난다. 노드는 살아 있고 프로세스도 정상인데 메시지만 닿지 않는다.
한쪽만 막히는 경우가 특히 고약하다 — 심장박동은 나가는데 응답이 돌아오지 않으면,
리더는 자기가 말하고 있다고 믿고 팔로워는 아무 말도 듣지 못한 채 선거를 연다.
그래서 이 실습도 선을 끊지 않는다. 메시지를 거부하는 것으로 충분하다.

다음 실습에서 할 것

세 노드 중 리더를 나머지에서 떼어 냈다가 되돌린다. 파드에 방화벽을 걸 권한이 없으므로
선을 끊는 대신 서로의 메시지를 거부하는 스위치를 직접 만든다. 두 리더가 동시에 존재하는
순간을 기록하고, 양쪽에 값을 하나씩 쓴 뒤, 회복했을 때 어느 쪽이 사라지는지를 눈으로
확인한다.