Ticketing이란?
Ticketing은 분산 락 하나만을 위해 설계된 전용 락 서버입니다. Rust로 작성한 서버 바이너리 하나와, 같은 바이너리 프로토콜을 바이트 단위로 동일하게 구현한 언어별 공식 클라이언트로 구성됩니다. 획득(acquire)과 반납(release) 두 동작, 키별 FIFO 대기열, 펜싱 토큰, 그리고 필요할 때 켜는 Raft 클러스터 분산 락에 필요한 것만 담고 나머지는 전부 뺐습니다.
저장소를 이용한 락과 무엇이 다른가
실무의 분산 락은 대부분 key-value 저장소를 이용합니다. Redis에 Lua 스크립트나 Redlock을 올리고, ZooKeeper나 etcd의 세션·리스 위에 락을 구현합니다. Redis는 뛰어난 저장소이지만, 전문 분산 락 시스템이 아닌 key-value 저장소를 이용해 분산 락을 구현한 것이라 구조적인 한계가 있습니다. 범용 프로토콜의 파싱 비용과 범용 자료구조의 오버헤드가 항상 따라옵니다.
Ticketing은 처음부터 분산 락 전용으로 설계되어, 모든 층이 락 기준으로 만들어져 있습니다.
DB transaction은 한 작업 내부의 atomicity를 보장하지만, 서로 다른 process가 같은 business resource에 동시에 진입하는 순서를 정하지는 않습니다. Ticketing은 같은 key의 진입을 먼저 직렬화하고, 획득한 실행자만 DB transaction을 시작하게 합니다. persistent DB에서는 그 transaction 안에서 Ticketing token을 검사하는 fencing 계약까지 지켜야 stale worker의 write를 최종 차단할 수 있습니다.
- 프로토콜 — 텍스트도, 범용 직렬화도 아닌 고정폭 바이너리 필드. 파싱 없이 오프셋으로 바로 읽고, 응답을 기다리지 않고 다음 요청을 보내는 파이프라이닝을 지원합니다.
- 서버 내부 — key별 FIFO 대기열과 lease deadline이 상태의 중심입니다. 서로 다른 key는 병렬로 처리하고, 전체 scan·불필요한 task·allocation·clone·reconnect를 줄이되 commit, token 검증, exact-token release와 자원 상한은 생략하지 않습니다.
- 운영 — 서버는 scratch 컨테이너 하나와 환경변수로 실행하며 local disk·volume·외부 상태 저장소를 쓰지 않습니다. 클러스터는 lease 안전성을 위해 인증된 clock-health source를 반드시 함께 운영합니다.
퍼포먼스
처리량 숫자 하나보다 single/3-voter/5-voter, plaintext/TLS, unique-key/hot-key를 나눠 throughput, p50/p99/p99.9, CPU, RSS, fairness와 release 지연을 함께 측정합니다. 안전성 구조를 끄는 성능 옵션은 두지 않습니다. 선택형 최적화는 처리량·CPU·allocation 중 하나가 10% 이상 좋아지고 p99·RSS· fairness가 5% 넘게 나빠지지 않을 때만 기본 구현으로 채택합니다.
왜 Ticketing인가
🚦 순서 무결성 — FIFO 대기열과 직접 인계
먼저 기다린 쪽이 먼저 받습니다. 보유자가 반납하면 대기열 맨 앞에게 재경쟁 없이 직접 인계 됩니다. 폴링과 재시도로 락을 다투는 방식에서 생기는 기아(starvation)도, 재시도 폭주도 없습니다.
🛟 클라이언트가 죽어도 잠기지 않습니다 — lease
락을 잡은 프로세스가 죽거나 네트워크가 끊기면, 획득할 때 정한 lease(1~250초) 시간이 지난 뒤 서버가 자동으로 회수해 다음 대기자에게 넘깁니다. 반납을 깜빡해도 시스템 전체가 멈추는 일은 없습니다.
🔑 잘못된 작업 순서 방지 — 펜싱 토큰
락만으로는 못 막는 사고가 하나 있습니다. lease가 만료된 줄 모르고 뒤늦게 깨어나 작업완료를 요청하는 경우입니다. Ticketing은 획득마다 단조 증가하는 펜싱 토큰을 함께 발급합니다. 보호 DB가 "더 낮은 token은 거부"를 실제 business write와 같은 transaction에서 지키면 이 마지막 구멍까지 막힙니다. fencing 없이 금융성 안전을 주장하지 않습니다. 자세한 원리는 동작 원리에서 설명합니다.
🗳️ Raft 기반 무정지 클러스터
서버 1대로도 충분히 동작하지만, 무정지가 필요하면 3대(또는 5대)로 클러스터를 구성합니다. 모든 락 상태 변경은 과반 노드의 합의(Raft)를 거친 뒤에만 확정되므로, 리더가 죽거나 네트워크가 분단되어도 같은 락이 두 곳에서 동시에 발급되는 일이 없습니다. 리더 장애 시 기본 설정에서 약 2.3~2.5초 안에 새 리더가 선출되고, 클라이언트는 새 요청을 새 리더로 보냅니다. 서버는 volume을 쓰지 않으므로 죽은 process를 같은 NodeId로 되살리지 않고 새 NodeId learner로 보충합니다. 3대는 동시에 1대, 5대는 동시에 2대의 process-state loss까지 순차 보충하며 서비스를 이어갑니다.
🔒 인증과 TLS 내장
모든 연결은 접속 직후 challenge-response 토큰 인증을 거치며, 토큰은 와이어에 평문으로 실리지 않습니다. 필요하면 TLS로 전 구간을 암호화할 수 있습니다.
🌐 주요 언어, 바이트 단위로 같은 프로토콜
Rust, Java/Kotlin, JavaScript/TypeScript, Python, C#, Go, Ruby, C/C++ — 공식 클라이언트가 같은 바이너리 프로토콜을 구현하고, 언어마다 그 생태계에 자연스러운 API(async 또는 블로킹)를 제공합니다. 어느 언어에서 획득한 락이든 다른 언어의 클라이언트와 같은 대기열에서 공정하게 줄을 섭니다. 설치 방법과 예제는 라이브러리에서 확인하세요.
API는 두 개뿐
락 전용 서버답게 표면적도 최소입니다.
- 지키고 싶은 자원에 키를 붙입니다. 예:
order-1234,account-77,daily-batch - 작업 전에 그 키를 획득(acquire) 합니다 — 누군가 쓰는 중이면 FIFO 대기열에서 차례를 기다립니다.
- 작업이 끝나면 반납(release) 합니다 — 다음 대기자가 재경쟁 없이 바로 이어받습니다.
이 두 동작에 lease(자동 회수), waitTimeout(0은 queue 없는 즉시 시도), 그리고 Ticket을 넘기기 위해 최소로 남아 있어야 할 작업 budget이 붙는 것이 API의 전부입니다. 무한 wait/lease와 250초 초과 작업은 지원하지 않습니다.
다음 읽을거리
- 락이 실제로 어떻게 배정되고 클러스터가 어떻게 버티는지 — 동작 원리
- 와이어에 오가는 바이트의 정확한 규격 — 프로토콜 (API)
- 서버 실행 명령 생성 — Ticketing 서버 배포