Skip to content

Commit fc356f5

Browse files
committed
feat: 2025-05-27-distributed-system-uid.md
1 parent 69d3062 commit fc356f5

File tree

1 file changed

+321
-0
lines changed

1 file changed

+321
-0
lines changed
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
---
2+
title: 분산 시스템을 위한 UID 살펴보기 (UUID, MongoDB ObjectId, Twitter Snowflake)
3+
author: w8385
4+
categories: [ System Design ]
5+
tags: [ UUID, MongoDB ObjectId, Twitter Snowflake ]
6+
date: 2025-05-27 12:27:00 +0900
7+
mermaid: true
8+
math: true
9+
---
10+
11+
### 분산 시스템을 위한 UID
12+
13+
UID는 객체 집합에서 객체를 참조하기 위한 고유 식별자입니다. \
14+
DB에서 ID 하면 auto-increment 또는 UUID 등으로 고유성을 보장하는 방법이 먼저 떠오릅니다.
15+
16+
그런데 최근에 트위터 스노우플레이크 기법을 통해 UID를 정렬 키로 사용하는 아이디어를 배웠습니다. \
17+
신기하지 않나요?
18+
19+
스노우플레이크 뿐만 아니라 분산 시스템을 위한 여러 UID들을 살펴보려 합니다.
20+
21+
---
22+
23+
## UUID
24+
25+
RFC 4122[^RFC4122]에서 정의된 UUID는 16바이트 길이의 고유 식별자입니다. \
26+
현재는 3가지 버전이 추가된 RFC 9562[^RFC9562]를 따릅니다.
27+
28+
### 구조
29+
30+
UUID는 32개의 16진수 숫자와 4개의 하이픈(-), 총 36자리의 문자열로 표현됩니다.\
31+
5개의 그룹을 하이픈으로 구분하여 8-4-4-4-12 형식으로 나타냅니다.
32+
33+
이를테면 다음과 같습니다. \
34+
`a0e7b586-1bcf-4614-a327-d0ad591da49b`
35+
36+
세 번째 그룹의 첫 번째 숫자는 UUID의 버전(`ver`)을 나타내며, \
37+
네 번째 그룹의 첫 번째 숫자는 UUID의 변형(`var`)을 나타냅니다. (보통 8-9,A-B)
38+
39+
### 버전
40+
41+
- v1: Time-based UUID
42+
- 타임스탬프와 IEEE 802 MAC 주소를 사용하여 생성됩니다.
43+
- v2: DCE Security UUID
44+
- v3: Name-based UUID (MD5 해시)
45+
- 주어진 네임스페이스와 이름을 해싱하여 생성됩니다.
46+
- v4: Random UUID
47+
- `ver`, `var` 필드를 제외한 122비트가 랜덤 값으로 채워집니다.
48+
- v5: Name-based UUID (SHA-1 해시)
49+
- v3과 유사하지만 SHA-1 해시를 사용합니다.
50+
- v6: Time-based UUID
51+
- v1과 유사하지만, 시간순 정렬을 위해 타임스탬프 배치가 변환됩니다.
52+
- v7: Unix Epoch Time-based UUID
53+
- v1, v6이 100ns 단위로 타임스탬프를 표현하는 반면, v7은 Unix 타임스탬프를 ms 단위로 표현합니다.
54+
- 48비트(Unix Epoch) + 8비트(`ver`, `var`)를 제외한 74비트를 랜덤 값으로 채워집니다.
55+
- v1, v6보다 높은 엔트로피를 갖기 때문에 권장되는 방법입니다.
56+
- v8: Custom UUID
57+
- `ver`, `var` 필드를 제외한 122비트가 사용자 정의 값으로 채워집니다.
58+
59+
### 예시
60+
61+
Python uuid 모듈을 사용하여 임의로 두 번씩 생성한 UUID 예시입니다.
62+
63+
v1과 v6는 유사하지만, v6는 시간순 정렬에 유리합니다.
64+
65+
```
66+
v1: bfefe0a3-3a1b-11f0-a0c6-8c882b407a1c
67+
v1: 17038ccc-3a1c-11f0-a593-8c882b407a1c
68+
69+
v6: 1f03a1bb-fefe-60a3-a0c6-8c882b407a1c
70+
v6: 1f03a1c1-7038-6ccc-a593-8c882b407a1c
71+
```
72+
73+
`time_low`, `time_high` 자리만 스왑됨
74+
75+
```mermaid
76+
---
77+
title: "UUIDv1"
78+
---
79+
packet-beta
80+
0-31: "time_low"
81+
32-47: "time_mid"
82+
48-51: "ver"
83+
52-63: "time_high"
84+
64-65: "var"
85+
66-71: "clock_seq"
86+
72-127: "node"
87+
```
88+
89+
```mermaid
90+
---
91+
title: "UUIDv6"
92+
---
93+
packet-beta
94+
0-31: "time_high"
95+
32-47: "time_mid"
96+
48-51: "ver"
97+
52-63: "time_low"
98+
64-65: "var"
99+
66-71: "clock_seq"
100+
72-127: "node"
101+
```
102+
103+
v4는 랜덤입니다.
104+
105+
```
106+
v4: eb168b5e-8139-4ce4-b514-c6e19f802472
107+
v4: f9cf4ef0-afd7-43b2-8c85-7dfc919fdf54
108+
```
109+
110+
```mermaid
111+
---
112+
title: "UUIDv4"
113+
---
114+
packet-beta
115+
0-47: "random_a"
116+
48-51: "ver"
117+
52-63: "random_b"
118+
64-65: "var"
119+
66-127: "random_c"
120+
```
121+
122+
v7은 시간순 정렬에 최적화 되어 있습니다.
123+
124+
```
125+
v7: 01970c20-445e-7d81-baca-41b6f867d37b
126+
v7: 01970c20-445f-76e4-ba3e-d69ca79a9d42
127+
```
128+
129+
```mermaid
130+
---
131+
title: "UUIDv7"
132+
---
133+
packet-beta
134+
0-47: "unix_ts_ms"
135+
48-51: "ver"
136+
52-63: "random_a"
137+
64-65: "var"
138+
66-127: "random_b"
139+
```
140+
141+
### 특징
142+
143+
[//]: # (@formatter:off)
144+
> UUIDv4의 경우 122비트의 랜덤 값으로 생성되며,약 $5.3 \times 10^{36}$개의 고유한 UUID를 생성할 수 있습니다. \
145+
> 생일 문제에 기반하여 고유성을 확률적으로 보장하지만, 중복될 가능성은 일반적으로 0에 가깝습니다.[^UUID]
146+
> - $2^{36}$ -> 0.000'000'000'000'000'4
147+
> - $2^{41}$ -> 0.000'000'000'000'4
148+
> - $2^{46}$ -> 0.000'000'4
149+
{: .prompt-tip }
150+
151+
> UUIDv7은 48비트의 Unix 타임스탬프가 가장 앞에 배치되어 시간순 정렬에 최적화 되어 있습니다.
152+
{: .prompt-info }
153+
[//]: # (@formatter:on)
154+
155+
---
156+
157+
## MongoDB ObjectId
158+
159+
MongoDB 8.0 기준으로 정리합니다. \
160+
MongoDB는 12바이트 길이의 ObjectId를 사용하여 각 문서를 고유하게 식별합니다.
161+
162+
### 구조
163+
164+
ObjectId는 다음과 같은 구조로 구성됩니다:[^ObjectId]
165+
166+
```mermaid
167+
---
168+
title: "MongoDB ObjectId"
169+
---
170+
packet-beta
171+
0-31: "timestamp"
172+
32-71: "random value"
173+
72-83: "incrementing counter"
174+
```
175+
176+
- 4 bytes: 타임스탬프
177+
- Unix 타임스탬프를 초단위로 표현합니다.
178+
- 5 bytes: 랜덤
179+
- 프로세스당 한 번씩 생성되는 랜덤 값입니다.
180+
- 3 bytes: 카운터
181+
- 임의의 값으로 초기화되는 프로세스당 증분 카운터입니다.
182+
183+
### 예시
184+
185+
[@nestjs/mongoose](https://www.npmjs.com/package/@nestjs/mongoose)로 임의 간격으로 생성한 ObjectId 목록입니다.
186+
187+
```
188+
6833e66a1570af6cd0f956fe
189+
6833e6741570af6cd0f95700
190+
6833e6781570af6cd0f95702
191+
6833e6911570af6cd0f95705
192+
```
193+
194+
필드별로 분리하면 다음과 같습니다.
195+
196+
```
197+
6833e66a 1570af6cd0 f956fe
198+
6833e674 1570af6cd0 f95700
199+
6833e678 1570af6cd0 f95702
200+
6833e691 1570af6cd0 f95705
201+
```
202+
203+
타임스탬프는 단조적으로 증가하고 있습니다. \
204+
싱글프로세스 환경에서 테스트했기 때문에, 5바이트 랜덤 값은 동일합니다. \
205+
카운터 또한 증가하고 있지만, 오버플로우 또는 프로세스 재시작 시 초기화 될 것이므로 단조성이 보장되지 않습니다.
206+
207+
### 특징
208+
209+
[//]: # (@formatter:off)
210+
> MongoDB 클라이언트는 고유한 ObjectId를 가진 `_id` 필드를 추가해야 합니다.
211+
> - `ObjectId.getTimestamp()` 메서드를 사용하여 ObjectId 생성 시간을 알 수 있습니다.
212+
> - ObjectId는 생성 시간에 따라 대략적으로 정렬됩니다.
213+
{: .prompt-info }
214+
215+
> ObjectId 값은 시간이 지남에 따라 증가하지만 반드시 단조적이지는 않습니다.
216+
> - 동일한 초 내에 생성된 ObjectId 값은 순서가 보장되지 않습니다.
217+
> - 클라이언트에서 생성되며, 시스템 클럭이 다를 수 있습니다.
218+
{: .prompt-warning }
219+
[//]: # (@formatter:on)
220+
221+
## Twitter Snowflake
222+
223+
Twitter Snowflake는 Twitter에서 분산 시스템에서 고유한 ID를 생성하기 위해 개발한 알고리즘입니다. \
224+
Snowflake는 64비트 정수로 표현되는 고유 식별자를 생성합니다.
225+
226+
MySQL에서 Cassandra로 마이그레이션할 때 ID를 생성하는 방법으로 개발되었으며, \
227+
여러 데이터센터 및 서버에서 고유한 ID를 생성할 수 있도록 설계되었습니다.[^Snowflake]
228+
229+
### 구조
230+
231+
Snowflake ID는 다음과 같은 구조로 구성됩니다:
232+
233+
```mermaid
234+
---
235+
title: "Twitter Snowflake"
236+
---
237+
packet-beta
238+
0-0: "sign"
239+
1-41: "timestamp"
240+
42-46: "datacenter_id"
241+
47-51: "server_id"
242+
52-63: "sequence"
243+
```
244+
245+
- 1 bit: 부호 비트
246+
- 41 bits: 타임스탬프
247+
- Twitter Epoch 타임스탬프를 ms 기준으로 41비트로 표현합니다.
248+
- 5 bits: 데이터 센터 ID
249+
- 5 bits: 서버 ID
250+
- 12 bits: 일련번호
251+
- 각 서버에서 ID를 생성할 때마다 1씩 증가합니다.
252+
- 이 값은 1ms 경과할 때마다 초기화됩니다.
253+
254+
### 예시
255+
256+
Snowflake ID는 64비트 정수로 표현되며, 다음과 같은 예시로 나타낼 수 있습니다.
257+
[Snowflake ID Generator](https://www.onlineappzone.com/snowflake-id-generator/)에서 임의로 생성한 Snowflake ID입니다.
258+
259+
```
260+
7332957014902968320
261+
7332957014902968321
262+
7332957014902968322
263+
264+
7332957018988220416
265+
7332957018988220417
266+
7332957018988220418
267+
```
268+
269+
각 ID를 2진수로 표현하고 필드별로 구분하면 다음과 같습니다.
270+
271+
```
272+
0 11001011100001111100101100011011010101010 00000 00001 000000000000
273+
0 11001011100001111100101100011011010101010 00000 00001 000000000001
274+
0 11001011100001111100101100011011010101010 00000 00001 000000000010
275+
276+
0 11001011100001111100101100011101001111000 00000 00001 000000000000
277+
0 11001011100001111100101100011101001111000 00000 00001 000000000001
278+
0 11001011100001111100101100011101001111000 00000 00001 000000000010
279+
```
280+
281+
### 특징
282+
283+
[//]: # (@formatter:off)
284+
> 많은 비동기 작업으로 인해 순서대로 전달됨을 완벽히 보장할 수는 없지만, \
285+
> 합리적인 범위 내에서 정렬된 ID를 생성할 수 있습니다.
286+
> - 타임스탬프가 41비트로 표현되므로, 약 69년 동안 고유한 ID를 생성할 수 있습니다.
287+
> - 일련번호는 12비트로 표현되므로, 동일한 ms 내에서 최대 4096개의 ID를 생성할 수 있습니다.
288+
{: .prompt-info }
289+
290+
> Discord는 2015년을 Epoch로 설정하여 Snowflake ID를 생성합니다.[^Discord] \
291+
> Instagram은 타임스탬프에 41비트, 샤드 ID에 13비트, 일련번호에 10비트를 할당하여 사용합니다.[^Instagram]
292+
{: .prompt-tip }
293+
[//]: # (@formatter:on)
294+
295+
## 정리
296+
297+
UID를 생성하는 다양한 방법들을 살펴보았습니다. \
298+
ID의 고유성을 해치지 않으면서, 대략적으로 정렬가능하도록 설계된 방법들이 많았습니다. \
299+
이는 데이터의 양이 늘어남에 따라 분산화되는 동향으로부터 기인된 것으로 보입니다.
300+
301+
`auto-increment` 방식은 단일 노드에서만 성립하고, \
302+
티켓 서버에서 발행시 병목 현상 및 단일 장애점(SPOF)의 위험을 피할 수 없습니다.
303+
304+
ID로부터 시간순 정렬이 가능하다면 객체를 불러오지 않고도 정렬될 수 있으며, \
305+
ID의 길이는 시스템의 특성에 따라 최적화가 필요할 수 있습니다. Redis와 같은 시스템에서는 64비트가 적합할 수 있습니다. \
306+
각 서버에서 발행되면서도 충돌을 피할 수 있는 방법이 필요합니다.
307+
308+
시스템의 요구사항에 따라 총 ID 길이, 비트 레이아웃, 어휘 형식 및 인코딩, 타임스탬프 유형, 타임스탬프 형식, 타임스탬프 정확도, 노드 형식 및 구성 요소, 충돌 처리, 다중 타임스탬프 틱 생성 시퀀싱 등 서로
309+
다른 설계와 구현이 나타날 수 있다는 점이 흥미롭습니다.
310+
311+
## 참조
312+
313+
[//]: # (@formatter:off)
314+
[^RFC4122]: [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122) - A Universally Unique IDentifier (UUID) URN Namespace
315+
[^RFC9562]: [RFC 9562](https://datatracker.ietf.org/doc/html/rfc9562) - Universally Unique IDentifiers (UUIDs)
316+
[^UUID]: [UUID](https://www.h2database.com/html/advanced.html#uuid) - h2database.com/html/advanced.html#uuid
317+
[^ObjectId]: [MongoDB ObjectId](https://www.mongodb.com/docs/manual/reference/bson-types/#objectid) - MongoDB Documentation
318+
[^Snowflake]: [Twitter Snowflake](https://github.com/twitter-archive/snowflake/tree/b3f6a3c6ca8e1b6847baa6ff42bf72201e2c2231) - Snowflake GitHub Archive
319+
[^Discord]: [Discord Snowflake](https://discord.com/developers/docs/reference#snowflakes) - Discord Developer Documentation
320+
[^Instagram]: [Sharding & IDs at Instagram](https://instagram-engineering.com/sharding-ids-at-instagram-1cf5a71e5a5c) - Instagram Engineering Blog
321+
[//]: # (@formatter:on)

0 commit comments

Comments
 (0)