현재 테스트 중입니다: 완료되면 GitHub 코드를 오픈할 예정입니다.

무정지 새 NodeId learner 교체

핵심 원칙은 두 가지입니다. 종료된 NodeId는 절대 재사용하지 않고, 언제나 process-state 과반을 유지합니다. 3 voter는 한 번에 1대, 5 voter는 한 번에 2대의 unavailable/state loss까지 견딥니다. 하지만 계획된 교체는 구성과 관계없이 항상 한 노드씩 완료합니다.

Ticketing 서버는 local disk나 volume에 Raft state를 저장하지 않습니다. 따라서 꺼진 process를 같은 NodeId로 다시 시작하는 작업은 복구가 아닙니다. leader가 새 NodeId를 발급하고, 교체 process가 learner로 완전히 따라잡은 뒤 voter로 commit되는 절차만 지원합니다.

교체 전 준비

  • cluster control token, 피어 토큰, TLS·clock 설정을 새 process에 주입할 수 있어야 합니다.
  • 외부 start authority가 process 시작마다 발급한, 과거에 쓴 적 없는 non-zero u128 incarnation과 새 process만 쓸 temporary Raft 주소를 준비합니다. stale 환경변수를 재사용하는 자동 restart는 끕니다.
  • old/new process가 동시에 뜨는 surge 구간에서도 Raft 주소는 중복되면 안 됩니다. stable client 주소는 Service/VIP 슬롯으로 old process에서 new process로 인계합니다.
  • clock agent가 먼저 건전해야 합니다. learner와 현재 leader의 clock health와 pairwise Δ가 하나라도 다르면 승격하지 않습니다.

실제 절차

  1. 관리 토큰이 설정된 동일 바이너리로 현재 상태를 확인합니다.

    sh
    /ticketing control status <접속-가능한-raft-주>

    leader 존재, clock.state=Healthy, startup_admission=Open, active_replacement=None, 정확히 3개 또는 5개의 voter를 확인합니다.

  2. leader에 교체를 준비시킵니다.

    sh
    /ticketing control prepare-replacement \
      <접속-가능한-raft-주> <old-node-id> <new-incarnation> <new-raft-주> 60

    leader가 복제된 next_node_id에서 새 NodeId를 발급하고 one-shot join 자격을 만듭니다. 출력된 CLUSTER_NODE_ID, CLUSTER_INCARNATION, CLUSTER_SELF, CLUSTER_RAFT_SELF, CLUSTER_PEERS, CLUSTER_JOIN_*를 임의로 바꾸지 않습니다.

  3. 출력된 값과 기존 cluster/control 토큰, TLS, clock, 자원 설정으로 교체 process를 시작합니다. CLUSTER_BOOTSTRAP_CREDENTIAL은 넣지 않습니다. process는 먼저 Raft/control listener만 엽니다. startup_admission=Pending에서는 Join/control만 처리하고, leader가 one-shot Join을 승인한 뒤 Open이 되어 learner로 참여합니다. snapshot/log catch-up·clock health·membership barrier가 끝나기 전에는 client connection을 받지 않습니다.

  4. learner가 catch-up과 clock gate를 통과해 LearnerReady가 되면 old process가 client를 drain합니다. 교체 대상이 현재 leader면 다른 catch-up voter로 leadership을 이양한 뒤 Raft를 종료합니다. 제거 대상에게 마지막 uniform membership을 전달할 수 있다고 가정하지 않으며, 남은 quorum이 old voter 제거와 new voter 승격을 commit합니다.

  5. status를 반복해 startup_admission=Open, active_replacement=None, 새 NodeId가 voter, old NodeId가 제거됨, clock.state=Healthy를 모두 확인합니다. old process가 실제로 종료됐는지도 확인합니다.

  6. voter 수와 건전성이 완전히 복구된 뒤에만 다음 노드 교체를 시작합니다. 교체가 voter swap 전에 실패했다면 다음으로 중단하고 active_replacement=None이 될 때까지 확인합니다.

    sh
    /ticketing control abort-replacement \
      <접속-가능한-raft-주> <new-node-id>

    중단된 NodeId와 incarnation도 retired로 남으므로 다시 쓰지 않습니다.

이 순서를 지키면 새 voter가 membership에 commit될 때마다 장애 허용 수가 다시 채워집니다. 기존 connection의 일부 acquire는 leader 전환 순간 Indeterminate가 될 수 있습니다. 무정지는 개별 호출이 전부 투명하게 재전송된다는 뜻이 아니라 quorum 서비스가 끊기지 않고 새 호출을 계속 처리할 수 있다는 뜻입니다.

3대 중 2대 또는 5대 중 3대가 휘발 process state까지 잃으면 복구 범위 밖입니다. 남은 불완전 state로 같은 fencing domain을 자동 bootstrap하지 말고, 운영자가 새 cluster_id의 새 domain을 만들어야 합니다. 이는 이전 lock과 token 연속성을 복구하는 작업이 아닙니다.

흐름 (3 voter 기준)

리더
교체 노드
생존 voter
① 새 process 시작 — 새 NodeId learner admission
로그 복제·스냅숏으로 완전 catch-up
② old process client drain·리더십 이양·종료
③ 남은 quorum이 uniform membership 교체 commit
④ status 확인 뒤 다음 교체
합의 RPC (Raft)

learner catch-up과 membership commit을 확인하지 않고 old voter를 먼저 내리면 무정지 보장이 사라집니다. readiness는 단순한 TCP connect가 아니라 learner catch-up, committed uniform membership, local·remote clock health를 모두 확인해야 합니다.

Kubernetes에서 같은 pod 이름으로 자동 restart하는 StatefulSet을 안전한 복구로 간주하면 안 됩니다. stable client 주소는 Service/VIP 슬롯으로 유지하고 실제 process와 Raft 주소는 교체마다 새로 만듭니다. 전용 operator/controller가 위 control-plane 절차를 완료한 뒤 old pod을 제거해야 합니다.

최초 cluster의 시작도 peer 접속 실패를 건너뛰지 않습니다. 설정된 모든 초기 process가 서로의 빈 membership과 미사용 bootstrap을 확인해 Prepared barrier에 도달한 뒤에만 admission이 Open됩니다. 살아 있던 cluster의 state를 가진 peer나 이미 열린 identity가 보이면 stale start로 판정해 Rejected하고 종료합니다. 다만 무디스크 process 여러 대가 같은 오래된 환경으로 동시에 다시 뜨는 일을 로컬 state만으로 완전히 식별할 수는 없으므로, 매 시작마다 새 incarnation을 발급하고 자동 재시작을 막는 외부 start authority 계약은 여전히 필수입니다.