在分佈式系統存在多個 Shard 的場景中, 同時在各個 Shard 插入數據時, 怎麼給這些數據生成全局的 unique ID? 在單機系統中 (例如一個 MySQL 實例), unique ID 的生成是很是簡單的, 直接利用 MySQL 自帶的自增 ID 功能就能夠實現.html
但在一個存在多個 Shards 的分佈式系統 (例如多個 MySQL 實例組成一個集羣, 在這個集羣中插入數據), 這個問題會變得複雜, 所生成的全局的 unique ID 要知足如下需求:git
- 惟一性,保證生成的 ID 全局惟一
- 從此數據在多個 Shards 之間遷移不會受到 ID 生成方式的限制
- 有序性,生成的 ID 中最好能帶上時間信息, 例如 ID 的前 k 位是 Timestamp, 這樣可以直接經過對 ID 的前 k 位的排序來對數據按時間排序
- 生成的 ID 最好不大於 64 bits
- 可用性,生成 ID 的速度有要求. 例如, 在一個高吞吐量的場景中, 須要每秒生成幾萬個 ID (Twitter 最新的峯值到達了 143,199 Tweets/s, 也就是 10萬+/秒)
- 整個服務最好沒有單點
在要知足前面 6 點要求的場景中, 怎麼來生成全局 unique ID 呢?github
數據庫自增ID
數據庫單表,使用 auto increment
來生成惟一全局遞增ID。mongodb
優點是無需額外附加操做,定長增加,單表結構中惟一性,劣勢是高併發下性能不佳,生產的上限是數據庫服務器單機的上限,水平擴展困難,分佈式數據庫下,沒法保證惟一性。數據庫
UUID
若是沒有上面這些限制, 問題會相對簡單, 例如: 直接利用 UUID.randomUUID() 接口來生成 unique ID (http://www.ietf.org/rfc/rfc4122.txt). 但這個方案生成的 ID 有 128 bits, 另外, 生成的 ID 中也沒有帶 Timestamp 通常編程語言中自帶 UUID 實現, Java 中 UUID.randomUUID().toString()
產生的ID 不依賴數據庫實現。編程
優點是,本地生成ID,無需遠程調用,全局惟一,水平擴展能力好。劣勢是,ID 有 128 bits 長,佔空間大,生成字符串類型,索引效率低,生成的 ID 中沒有帶 Timestamp 沒法保證時間遞增。服務器
Flickr 全局主鍵
Flickr 的作法1 是使用 MySQL 的自增ID, 和 replace into 語法。但他這個方案 ID 中沒有帶 Timestamp, 生成的 ID 不能按時間排序併發
建立64位自增ID,首先建立表負載均衡
CREATE TABLE `Tickets64` ( `id` bigint(20) unsigned NOT NULL auto_increment, `stub` char(1) NOT NULL default '', PRIMARY KEY (`id`), UNIQUE KEY `stub` (`stub`) ) ENGINE=MyISAM
SELECT * from Tickets64
假設表中有一行dom
+-------------------+------+ | id | stub | +-------------------+------+ | 72157623227190423 | a | +-------------------+------+
那麼若是須要產生一個新的全局 64 bits 的ID,只要執行 SQL:
REPLACE INTO Tickets64 (stub) VALUES ('a'); SELECT LAST_INSERT_ID();
SQL 返回的ID就是要產生的全局惟一ID。使用 REPLACE INTO
代替 INSERT INTO
的好處是避免錶行數太多。 stub 要設爲惟一索引。
Flickr 內部運行兩臺 ticket servers,經過兩臺機器作主備和負載均衡。
TicketServer1: auto-increment-increment = 2 auto-increment-offset = 1 TicketServer2: auto-increment-increment = 2 auto-increment-offset = 2
Twitter Snowflake
Twitter 利用 Zookeeper 實現一個全局的 ID 生成服務 Snowflake: https://github.com/twitter/snowflake
Snowflake 生成的 unique ID 的組成 (由高位到低位):
- 41 bits: Timestamp 毫秒級
- 10 bits: 節點 ID datacenter ID 5 bits + worker ID 5 bits
- 12 bits: sequence number
一共 63 bits ,其中最高位是 0
unique ID 生成過程:
-
41 bits 的 Timestamp: 每次要生成一個新 ID 的時候, 都會獲取一下當前的 Timestamp, 而後分兩種狀況生成 sequence number:
- 若是當前的 Timestamp 和前一個已生成 ID 的 Timestamp 相同 (在同一毫秒中), 就用前一個 ID 的 sequence number + 1 做爲新的 sequence number (12 bits); 若是本毫秒內的全部 ID 用完, 等到下一毫秒繼續 (**這個等待過程當中, 不能分配出新的 ID**) - 若是當前的 Timestamp 比前一個 ID 的 Timestamp 大, 隨機生成一個初始 sequence number (12 bits) 做爲本毫秒內的第一個 sequence number
-
10 bits 的機器號, 在 ID 分配 Worker 啓動的時候, 從一個 Zookeeper 集羣獲取 (保證全部的 Worker 不會有重複的機器號)
整個過程當中, 只有在 Worker 啓動的時候會對外部有依賴 (須要從 Zookeeper 獲取 Worker 號), 以後就能夠獨立工做了, 作到了去中心化.
異常狀況討論:
在獲取當前 Timestamp 時, 若是獲取到的時間戳比前一個已生成 ID 的 Timestamp 還要小怎麼辦? Snowflake 的作法是繼續獲取當前機器的時間, 直到獲取到更大的 Timestamp 才能繼續工做 (在這個等待過程當中, 不能分配出新的 ID)
從這個異常狀況能夠看出, 若是 Snowflake 所運行的那些機器時鐘有大的誤差時, 整個 Snowflake 系統不能正常工做 (誤差得越多, 分配新 ID 時等待的時間越久)
從 Snowflake 的官方文檔 (https://github.com/twitter/snowflake/#system-clock-dependency) 中也能夠看到, 它明確要求 「You should use NTP to keep your system clock accurate」. 並且最好把 NTP 配置成不會向後調整的模式. 也就是說, NTP 糾正時間時, 不會向後回撥機器時鐘.
下面是 Snowflake 的其餘變種, Instagram 產生 ID 的方法也借鑑 Snowflake
Boundary flake
代碼地址:https://github.com/boundary/flake
變化:
ID 長度擴展到 128 bits:
- 最高 64 bits 時間戳;
- 而後是 48 bits 的 Worker 號 (和 Mac 地址同樣長);
- 最後是 16 bits 的 Seq Number
因爲它用 48 bits 做爲 Worker ID, 和 Mac 地址的長度同樣, 這樣啓動時不須要和 Zookeeper 通信獲取 Worker ID. 作到了徹底的去中心化
基於 Erlang ,這樣作的目的是用更多的 bits 實現更小的衝突機率,這樣就支持更多的 Worker 同時工做。同時, 每毫秒能分配出更多的 ID。
Simpleflake
源代碼:https://github.com/SawdustSoftware/simpleflake
Simpleflake 的思路是取消 Worker 號, 保留 41 bits 的 Timestamp, 同時把 sequence number 擴展到 22 bits;
Simpleflake 的特色:
- sequence number 徹底靠隨機產生 (這樣也致使了生成的 ID 可能出現重複)
- 沒有 Worker 號, 也就不須要和 Zookeeper 通信, 實現了徹底去中心化
- Timestamp 保持和 Snowflake 一致, 從此能夠無縫升級到 Snowflake
Simpleflake 的問題就是 sequence number 徹底隨機生成, 會致使生成的 ID 重複的可能. 這個生成 ID 重複的機率隨着每秒生成的 ID 數的增加而增加.
因此, Simpleflake 的限制就是每秒生成的 ID 不能太多 (最好小於 100次/秒, 若是大於 100次/秒的場景, Simpleflake 就不適用了, 建議切換回 Snowflake).
Instagram 的作法
Instagram 參考 Flickr 的方案,結合 Twitter 的經驗,利用 PostgreSQL 數據庫的特性,實現了一個更加簡單可靠的 ID 生成服務。 Instagram 的分佈式存儲方案: 把每一個 Table 劃分爲多個邏輯分片 (logic Shard), 邏輯分片的數量能夠很大, 例如 2000 個邏輯分片。而後制定一個規則, 規定每一個邏輯分片被存儲到哪一個數據庫實例上面; 數據庫實例不須要不少. 例如, 對有 2 個 PostgreSQL 實例的系統 (instagram 使用 PostgreSQL); 可使用奇數邏輯分片存放到第一個數據庫實例, 偶數邏輯分片存放到第二個數據庫實例的規則
每一個 Table 指定一個字段做爲分片字段 (例如, 對用戶表, 能夠指定 uid 做爲分片字段)
插入一個新的數據時, 先根據分片字段的值, 決定數據被分配到哪一個邏輯分片 (logic Shard) 而後再根據 logic Shard 和 PostgreSQL 實例的對應關係, 肯定這條數據應該被存放到哪臺 PostgreSQL 實例上
Instagram 在設計ID時考慮了以下因素:
- 生成的IDs 須要按照時間排序,好比查詢一組照片時就不須要額外獲取照片更多的信息來進行排序
- IDs 64bits 索引,或者存儲在 Redis 中
- The system should introduce as few new ‘moving parts’ as possible — a large part of how we’ve been able to scale Instagram with very few engineers is by choosing simple, easy-to-understand solutions that we trust.
Instagram unique ID 的組成:
- 41 bits 表示 Timestamp (毫秒), 能自定義起始時間 epoch
- 13 bits 表示 每一個 logic Shard 的代號 (最大支持 8 x 1024 個 logic Shards)
- 10 bits 表示 sequence number; 每一個 Shard 每毫秒最多能夠生成 1024 個 ID
假設2011年9月9號下午 5 點鐘, epoch 開始於 2011 年 1 月1 號,那麼已經有 1387263000 毫秒通過,那麼前 41 bits 是
id = 1387263000 <<(64-41)
接下來13位由分片ID決定,假設按照 user ID 來分片,有 2000 個邏輯分片,若是用戶的ID 是 31341 , 那麼分片 ID 是 31341%2000 -> 1341
,因此接下來的13位是:
id |= 1341 <<(63-41-13)
最後,每一個表自增來填補剩下的 bits,假設已經爲表生成了 5000 個 IDs,那麼下一個值是 5001,而後取模 1024
id |= (5001 % 1024)
sequence number 利用 PostgreSQL 每一個 Table 上的 auto-increment sequence 來生成。若是當前表上已經有 5000 條記錄, 那麼這個表的下一個 auto-increment sequence 就是 5001 (直接調用 PL/PGSQL 提供的方法能夠獲取到) 而後把 這個 5001 對 1024 取模就獲得了 10 bits 的 sequence number。
instagram 這個方案的優點在於:
利用 logic Shard 號來替換 Snowflake 使用的 Worker 號, 就不須要到中心節點獲取 Worker 號了. 作到了徹底去中心化
另一個附帶的好處就是, 能夠經過 ID 直接知道這條記錄被存放在哪一個 logic Shard 上。同時, 從此作數據遷移的時候, 也是按 logic Shard 爲單位作數據遷移的, 因此這種作法也不會影響到從此的數據遷移
MongoDB ObjectID
MongoDB 的 ObjectID2 採用 12 個字節的長度,將時間戳編碼在內。
- 其中前四個字節時間戳,從標準紀元開始,單位秒,時間戳和後5個字節保證了秒級別的惟一性,保證插入順序以時間排序。
- 接着前四個字節時間戳的後面三個字節爲機器號,這三個字節爲所在主機惟一標識,通常爲機器名散列值。
- 接着兩個字節爲PID標識,同一臺機器中可能運行多個Mongo實例,用PID來保證不衝突
- 後三個字節爲遞增序號,自增計數器,來確保同一秒內產生的 ObjectID 不出現衝突,容許 256 的三次方 16777216 條記錄。
reference
- https://engineering.instagram.com/sharding-ids-at-instagram-1cf5a71e5a5c
- http://darktea.github.io/notes/2013/12/08/Unique-ID
- http://www.cnblogs.com/flyingeagle/articles/7124233.html
-
http://code.flickr.net/2010/02/08/ticket-servers-distributed-unique-primary-keys-on-the-cheap/ ↩
-
https://docs.mongodb.com/manual/reference/method/ObjectId/#objectid ↩