Ticketing 서버 배포
공식 이미지는 scratch로 끝나며 65532:65532 사용자가 실행합니다. 서버는 Raft vote, log, snapshot, lock, fencing token을 파일이나 volume에 저장하지 않습니다. TLS 인증서와 CA 파일만 read-only로 마운트할 수 있습니다.
먼저 선택할 모드
| 모드 | 용도 | 장애·fencing 범위 |
|---|---|---|
| single | 개발, 휘발 작업, 가장 낮은 지연 | process가 끝나면 lock과 token domain도 끝남. 재시작 가능한 persistent DB 보호에는 미지원 |
| 3 voter | 일반 production | 한 process의 unavailable/state loss 동안 quorum 유지와 새 NodeId 교체 지원 |
| 5 voter | 두 process 장애까지 필요한 production | 두 process의 unavailable/state loss까지 지원 |
클러스터에서 죽은 process를 같은 CLUSTER_NODE_ID로 다시 띄우면 안 됩니다. 새 process마다 과거에 쓰지 않은 incarnation과 leader가 발급한 새 NodeId를 사용하고 learner로 따라잡은 뒤 voter를 교체합니다. 3대 중 2대, 5대 중 3대의 휘발 state를 잃으면 기존 cluster_id와 fencing domain은 복구하지 않습니다.
single 실행
docker run --rm --read-only \
-p 5225:5225 \
-e CLIENT_TOKENS='replace-with-a-secret' \
sarolab/ticketingCLUSTER_* identity를 하나도 설정하지 않으면 single 모드입니다. 기본 client port는 5225이고 SERVER_PORT로 바꿀 수 있습니다. single token은 process lifetime 안에서만 증가하므로 금융성 또는 persistent DB 쓰기에는 3/5 voter 클러스터와 DB fencing을 사용합니다.
첫 클러스터 생성
세 노드 모두 다음 공통 조건을 만족해야 합니다.
CLUSTER_PEERS는 정확히 같은 3개 또는 5개 항목이며 각 항목은NodeId@incarnation@client-address@raft-address입니다.- NodeId, non-zero u128 incarnation, client address와 Raft address는 각각 중복되지 않습니다.
CLUSTER_TOKENS,CLUSTER_CONTROL_TOKENS,CLUSTER_CLOCK_AGENT_SECRET은 비어 있지 않고 서로 다른 secret입니다. clock secret은 32바이트 이상입니다.- production에는
TLS_CERT와TLS_KEY를 설정합니다. TLS를 끄려면 격리된 private network에서만CLUSTER_ALLOW_PLAINTEXT_PRIVATE=true를 명시합니다. - 인증된 clock-health agent가 먼저 떠 있어야 합니다. 단순히 서버 자신의 시각을 되돌려 주는 endpoint는 사용할 수 없습니다.
예를 들어 첫 번째 노드의 핵심 identity는 다음과 같습니다. 두 번째와 세 번째 노드는 자기 CLUSTER_NODE_ID, CLUSTER_INCARNATION, CLUSTER_SELF, CLUSTER_RAFT_SELF만 바꾸고 같은 peer 목록을 사용합니다.
CLUSTER_ID=payments-locks
CLUSTER_NODE_ID=1
CLUSTER_INCARNATION=1001
CLUSTER_SELF=ticketing-client-1.internal:5225
CLUSTER_RAFT_SELF=ticketing-raft-1.internal:6225
CLUSTER_PEERS=1@1001@ticketing-client-1.internal:5225@ticketing-raft-1.internal:6225,2@1002@ticketing-client-2.internal:5225@ticketing-raft-2.internal:6225,3@1003@ticketing-client-3.internal:5225@ticketing-raft-3.internal:6225
CLIENT_TOKENS=<client-secret>
CLUSTER_TOKENS=<raft-secret>
CLUSTER_CONTROL_TOKENS=<control-secret>
CLUSTER_BOOTSTRAP_CREDENTIAL=<one-shot-bootstrap-secret>
CLUSTER_CLOCK_AGENT_ENDPOINT=127.0.0.1:9123
CLUSTER_CLOCK_AGENT_SECRET=<at-least-32-byte-clock-secret>
CLUSTER_CLOCK_PAIRWISE_DELTA_MS=20
TLS_CERT=/run/ticketing/tls.crt
TLS_KEY=/run/ticketing/tls.key
TLS_CA=/run/ticketing/ca.crt모든 초기 process와 clock agent가 준비되면 같은 설정을 읽는 바이너리에서 한 번만 실행합니다.
각 process는 시작 직후 startup_admission=Pending이고 Vote/Append/Snapshot과 client 요청을 받지 않습니다. 설정된 나머지 모든 초기 peer의 status가 빈 membership, 미사용 bootstrap, Pending임을 직접 확인하면 Prepared barrier에 들어가며, 모든 process가 Prepared가 된 뒤에만 Open으로 전환합니다. peer가 접속되지 않는다고 건너뛰거나 자동 bootstrap하는 fallback은 없습니다. 과거 state 또는 이미 열린 admission을 관찰하면 해당 identity는 Rejected되고 process가 종료됩니다.
/ticketing control bootstrap ticketing-raft-1.internal:6225성공하면 bootstrap credential hash가 Raft state에 사용 완료로 기록됩니다. 살아 있는 초기 process를 환경변수 제거만을 위해 재시작하지 않습니다. 이후 replacement process에는 CLUSTER_BOOTSTRAP_CREDENTIAL을 넣지 않습니다.
무정지 교체
일반 container restart나 같은 이름의 StatefulSet 재생성은 지원하는 복구가 아닙니다. 전용 operator/controller가 다음 control-plane 순서를 실행해야 합니다.
ticketing control status <raft-address>에서 leader, healthy clock, uniform 3/5 voter,startup_admission=Open,active_replacement=None을 확인합니다.- 과거에 쓰지 않은 incarnation과 임시 Raft 주소로
prepare-replacement <raft-address> <old-node-id> <new-incarnation> <new-raft-address>를 실행합니다. - 명령이 출력한
CLUSTER_NODE_ID, identity, peer 목록과 one-shotCLUSTER_JOIN_*를 그대로 넣어 새 process를 시작합니다. 교체 process의 admission은 leader가 해당 Join을 승인한 뒤에만Open이 되며, 승인 전에는 Raft/client 요청을 처리하지 않습니다. - learner가 따라잡고 clock 검증을 통과하면 old voter는 client를 닫고, 필요하면 leadership을 넘긴 뒤 종료합니다. 남은 quorum이 uniform membership 교체를 commit합니다.
- status에서 새 voter와
active_replacement=None을 확인한 뒤에만 다음 노드를 교체합니다.
실패한 admission은 abort-replacement로 정리하되 그 NodeId와 incarnation은 재사용하지 않습니다. 명령과 gate의 정확한 절차는 무정지 새 NodeId learner 교체를 따릅니다.
서비스 무정지는 quorum이 새 요청을 계속 처리할 수 있다는 뜻입니다. leader 전환 순간 이미 일부 바이트가 송신된 acquire는 안전을 위해 Indeterminate가 될 수 있으며 자동 재전송하지 않습니다.
scratch 자원 기준
기본 profile은 최소 512MiB memory limit에서 시작하도록 보수적으로 낮춰져 있습니다. 연결 1024개, 키당 waiter 2048개, 전체 waiter 16384개, connection당 밀린 응답 256개가 기본 상한입니다. acquire가 포화되면 daemon이 OOM으로 죽기 전에 상관 가능한 B로 거부하고, release와 Raft 복구는 별도 예약 lane을 사용합니다. 전체 acquire·신규 key·전체 waiter는 hard limit의 90%에서 admission을 닫고 75% 미만에서 다시 열어 한계 부근의 진동을 막습니다. B의 상세 원인은 wire를 늘리지 않고 서버의 key_busy/per_key_limit/connection_limit/global_overload 카운터, 현재 사용량과 10초 제한 경고로 구분합니다.
최대 snapshot과 learner catch-up을 포함한 실제 peak RSS는 key 길이, 동시 connection, TLS와 네트워크 속도에 따라 달라집니다. production limit은 목표 부하에서 측정해 여유를 두고 확정하고, 단순히 상한을 크게 올려 OOM을 미루지 않습니다. 전체 기본값과 절대 ceiling은 설정 (환경변수)에 있습니다.
DB 쓰기 전 필수 조건
분산락은 유효 lease가 겹치지 않도록 순서를 정하지만, pause 뒤 lease를 잃은 오래된 worker의 실행을 중단시킬 수는 없습니다. persistent DB에서는 Ticket의 fencing token이 저장된 high-water보다 클 때만 허용하는 조건과 실제 business write를 반드시 같은 DB transaction 또는 하나의 조건부 write에서 수행합니다. fencing row만 먼저 갱신하고 별도 transaction에서 write하는 방식은 안전하지 않습니다.