여러 서버가 조율 없이도 겹치지 않는 고유 ID를 만들어 내는 방식. 트위터가 공개한 구조다.
64비트 구성
┌─┬─────────────────────────┬────────────┬──────────────┐
│0│ 타임스탬프 41비트 │ 노드 10비트│ 시퀀스 12비트│
└─┴─────────────────────────┴────────────┴──────────────┘
↑
부호 비트(항상 0 — 음수가 되지 않게)
| 필드 | 비트 | 의미 |
|---|---|---|
| 타임스탬프 | 41 | 기준 시각부터의 밀리초. 2⁴¹ms ≈ 69년 |
| 노드 ID | 10 | 2¹⁰ = 1024대의 서버를 구분 |
| 시퀀스 | 12 | 같은 밀리초 안의 일련번호. 2¹² = 4096개 |
초당 생성 가능 수 = 1024대 × 4096개 × 1000ms = 약 41억 개/초
왜 이 구조인가
① 조율이 필요 없다 — 각 서버가 자기 노드 ID만 알면 혼자 만들 수 있다. 중앙 ID 서버에 물어볼 필요가 없어 네트워크 왕복도, 단일 장애점도 없다.
② 시간 순 정렬이 된다 — 타임스탬프가 맨 앞에 있으므로 숫자 크기 순 = 생성 시간 순이다. 이것이 결정적으로 중요하다.
- DB 인덱스에 순차적으로 삽입된다
- B+트리 오른쪽 끝에만 추가 → 페이지 분할 최소화
- UUID(랜덤)는 곳곳에 삽입되어 분할이 잦고 인덱스가 비대해진다
③ 크기가 작다 — 8바이트. UUID는 16바이트(문자열이면 36바이트). InnoDB에서는 모든 보조 인덱스가 PK를 복사하므로 이 차이가 크게 누적된다.
UUID·auto_increment와의 비교
| auto_increment | UUID v4 | Snowflake | |
|---|---|---|---|
| 크기 | 8B | 16B | 8B |
| 정렬 | ○ | ✗ | ○ |
| 분산 생성 | ✗ (DB 의존) | ○ | ○ |
| 예측 가능성 | 높음(보안 문제) | 없음 | 시각은 노출 |
| 삽입 성능 | 좋음 | 나쁨 | 좋음 |
auto_increment의 문제 — 샤딩하면 샤드마다 같은 번호가 생겨 충돌한다. 또 ID를 얻으려면 반드시 DB에 INSERT해야 해서, 미리 ID를 알 수 없다.
UUID v7은 앞부분에 타임스탬프를 넣어 정렬 가능하게 개선한 것으로, Snowflake와 같은 발상이다(다만 16바이트).
함정 ① — 시계 역행
- NTP 동기화로 시계가 뒤로 감
- 이미 발급한 것과 같은 타임스탬프
- 시퀀스도 겹치면 중복 ID
대응
- 마지막 발급 시각을 기억하고, 역행을 감지하면 대기하거나 예외
- 작은 역행(수 ms)은 기다리고, 큰 역행은 즉시 실패시켜 사람이 개입
- NTP를 slew 모드(천천히 보정)로 설정해 급격한 점프를 막는다
함정 ② — 노드 ID 할당
1024개를 어떻게 겹치지 않게 나눠 주느냐가 실제 운영 과제다.
- 정적 설정 — : 배포 시 지정. 간단하지만 실수하기 쉽다
- ZooKeeper/etcd : 기동 시 순번을 받아 옴. 확실하지만 의존성 추가
- 쿠버네티스 — : StatefulSet의 ordinal(pod-0, pod-1)을 사용 ← 깔끔
- IP 기반 — : 사설 IP 하위 비트를 사용. 대역이 겹치면 위험
같은 노드 ID를 가진 인스턴스가 둘이 되는 순간 중복이 발생하므로 가장 조심해야 할 부분이다.
함정 ③ — 정보 노출
ID를 보면 생성 시각을 알 수 있다. 순차적으로 조회하면 "이 서비스가 하루에 주문을 몇 건 받는지" 추정할 수도 있다. 외부에 노출하는 식별자는 별도의 불투명한 값을 쓰는 편이 안전하다.