CRD 와 오퍼레이터 · 조정이 두 번 일어났다 · 이론
리더가 둘이 되는 순간
한 줄 요약
리더 선출은 작은 오브젝트 하나(Lease)에 '지금 누가 리더인가' 를 적어 두고 주기적으로 갱신하는 약속이다.
만료 여부는 오브젝트에 적혀 있지 않고 각 인스턴스가 자기 시계로 계산하므로, 리더가 둘이 되는 구간이
원리적으로 존재한다.
왜 이 장치가 필요했나
오퍼레이터를 한 개만 띄우면 그 파드가 죽는 동안 아무도 조정하지 않는다. 그래서 둘 이상 띄운다. 그런데
조정 루프는 클러스터 상태를 바꾸는 코드다. 둘이 동시에 돌면 같은 자원을 두 번 만들려 하고, 서로의
결과를 지우고, 스케일 값을 번갈아 되돌린다.
가장 단순한 해법은 "하나만 일하게 한다" 이다. 여러 인스턴스가 떠 있되 그중 하나만 조정하고 나머지는
대기한다. 문제는 그 하나를 어떻게 정하느냐인데, 별도의 합의 시스템을 두면 그것 자체가 또 하나의 장애
지점이 된다. 쿠버네티스는 이미 있는 것을 썼다 — API 서버의 낙관적 동시성이다. 오브젝트 하나를
여럿이 동시에 갱신하려 하면 resourceVersion 이 맞는 쪽만 성공한다. 그 성질 위에 아주 얇은 약속을
얹은 것이 리더 선출이다.
어떻게 동작하나
약속의 그릇이 coordination.k8s.io/v1 의 Lease 다. 다섯 필드가 전부다.
| 필드 | 뜻 |
| --- | --- |
| holderIdentity | 지금 리더라고 주장하는 인스턴스의 이름 |
| leaseDurationSeconds | 갱신이 끊긴 뒤 이만큼 지나면 만료로 본다 |
| acquireTime | 지금 홀더가 임차를 잡은 시각 |
| renewTime | 홀더가 마지막으로 갱신한 시각 |
| leaseTransitions | 홀더가 바뀐 횟수 |
acquireTime 과 renewTime 은 보통의 Time 이 아니라 MicroTime 이라 소수점 이하 여섯 자리가 필요하다.
형식이 다르면 값이 아니라 파싱 단계에서 거절당한다.
리더가 하는 일은 renewTime 만 주기적으로 고쳐 쓰는 것이다. 홀더도 전환 횟수도 건드리지 않는다.
후보가 하는 일은 임차를 주기적으로 읽어 renewTime + leaseDurationSeconds 가 지금보다 과거인지 보는
것이다. 과거이면 만료로 보고 자기 이름으로 인수하면서 acquireTime 을 새로 적고 leaseTransitions 를
하나 올린다. 그래서 전환 횟수는 운영 지표다. 홀더가 계속 바뀌고 있다면 그 숫자가 계속 는다.
kube-controller-manager 의 세 플래그가 이 리듬을 정한다 — --leader-elect-lease-duration(기본 15초),--leader-elect-renew-deadline(기본 10초), --leader-elect-retry-period(기본 2초). 관계가 중요하다.
갱신 기한은 임차 길이보다 반드시 짧아야 한다. 리더는 갱신 기한 안에 갱신하지 못하면 스스로
리더 자리를 내려놓는데, 이 기한이 임차 길이보다 길면 "리더는 아직 자기가 리더라고 믿는데 다른 후보는
이미 만료로 보고 가져간" 구간이 생긴다. 재시도 주기는 그 기한 안에 여러 번 시도할 수 있게 훨씬 짧게 둔다.
권한은 놀랄 만큼 작다. coordination.k8s.io 그룹 leases 에 대한 get·create·update 셋이면 된다.
지울 일도 없고, 감시(watch)도 필요 없다 — 후보는 감시가 아니라 주기적인 조회로 만료를 확인한다.
현장에서 만나는 모습
첫째, 리더가 둘이 되는 구간은 없앨 수 없다. 네트워크가 잠깐 끊긴 리더는 자기가 아직 리더라고 믿고
조정을 계속하는데, 그 사이 다른 인스턴스가 임차를 인수해 조정을 시작한다. 이 구간을 없애려면 분산
시스템의 더 강한 보증이 필요하고, 쿠버네티스의 리더 선출은 그것을 약속하지 않는다. 그래서 **조정
루프 자체가 멱등해야 한다.** 리더 선출은 확률을 낮추는 장치이지 배타를 보장하는 장치가 아니다.
둘째, 시계 어긋남이 원인인 사고. 노드들의 시계가 몇 초 어긋나 있으면 만료 판정이 노드마다 다르게
나온다. 증상은 "리더가 계속 바뀐다" 이고, 화면에서 보이는 것은 leaseTransitions 가 분 단위로 오르는
것뿐이다. 조사할 때 임차 오브젝트만 보면 원인에 닿지 않는다 — 노드 시계와 API 서버 지연을 함께 봐야 한다.
셋째, 두 리더가 남기는 흔적. 두 조정 루프가 같은 자원을 만들려 하면 하나는 이름 충돌로 실패하고,
같은 필드를 서버사이드 적용으로 쓰려 하면 필드 관리자 충돌이 난다. 후자는 특히 유용하다 — API
서버가 "이 필드는 다른 관리자의 것" 이라고 이름까지 알려 주기 때문에, 충돌 메시지 하나로 누가 함께
쓰고 있었는지가 드러난다. 오퍼레이터를 서버사이드 적용으로 짜 두면 이 사고가 조용히 지나가지 않는다.
이 실습 환경의 한계
실습 파드에서는 진짜 컨트롤러 두 개를 띄워 경쟁시킬 수 없다. kwok 의 파드는 가짜라 컨테이너가 돌지
않기 때문이다. 그래서 갱신·만료·인수는 사람이 patch 로 흉내 내고, 시계 어긋남도 renewTime 을 과거로
적어 재현한다. 반대로 API 서버는 진짜라서 MicroTime 형식 거절, RBAC 판정, 서버사이드 적용의 필드
관리자 충돌은 모두 실제 동작 그대로 확인할 수 있다.
다음 실습에서 할 것
임차를 만들어 갱신해 보고, 만료된 임차와 살아 있는 임차를 판정하는 스크립트를 만든다. 인수를 patch 로
흉내 내 전환 횟수가 오르는 것을 확인하고, MicroTime 형식을 어겼을 때 갱신이 어떻게 막히는지 본다.
리더 선출에 필요한 최소 권한을 직접 맞춘 뒤, 두 인스턴스가 같은 자원을 쓰려 할 때 API 서버가 남기는
충돌을 받아 보고, 마지막에 운영 점검표로 묶는다.