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

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% 넘게 나빠지지 않을 때만 기본 구현으로 채택합니다.

가장 유명한 분산 락 시스템인 Redisson (Redis) 와 속도를 비교한 그래프입니다.
Ticketing 클라이언트Redis (Redisson RLock)
JavaScript
210.2 ms · 190,295 ops/s
210.2 ms
Rust
225.9 ms · 177,069 ops/s
225.9 ms
Go
228.4 ms · 175,131 ops/s
228.4 ms
C#
286.9 ms · 139,421 ops/s
286.9 ms
Kotlin
443.5 ms · 90,192 ops/s
443.5 ms
Java
457.8 ms · 87,374 ops/s
457.8 ms
C/C++
659.7 ms · 60,634 ops/s
659.7 ms
Python
739.4 ms · 54,098 ops/s
739.4 ms
Ruby
1,682.4 ms · 23,776 ops/s
1,682.4 ms
Redis기준
4,909.1 ms · 8,148 ops/s
4,909.1 ms
막대 길이는 전체 처리에 걸린 시간(ms)으로, 짧을수록 빠릅니다.
32 contended keys × 1,250 ops · concurrency 64 · mac mini m4 2024 basic (10 core), single local server, acknowledged release, 1s min-work budget

왜 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는 두 개뿐

락 전용 서버답게 표면적도 최소입니다.

  1. 지키고 싶은 자원에 키를 붙입니다. 예: order-1234, account-77, daily-batch
  2. 작업 전에 그 키를 획득(acquire) 합니다 — 누군가 쓰는 중이면 FIFO 대기열에서 차례를 기다립니다.
  3. 작업이 끝나면 반납(release) 합니다 — 다음 대기자가 재경쟁 없이 바로 이어받습니다.

이 두 동작에 lease(자동 회수), waitTimeout(0은 queue 없는 즉시 시도), 그리고 Ticket을 넘기기 위해 최소로 남아 있어야 할 작업 budget이 붙는 것이 API의 전부입니다. 무한 wait/lease와 250초 초과 작업은 지원하지 않습니다.

다음 읽을거리