分散式系統中唯一 ID 的生成方法

發表於2017-02-05

本文主要介紹在一個分散式系統中, 怎麼樣生成全域性唯一的 ID

一, 問題描述

在分散式系統存在多個 Shard 的場景中, 同時在各個 Shard 插入資料時, 怎麼給這些資料生成全域性的 unique ID?

在單機系統中 (例如一個 MySQL 例項), unique ID 的生成是非常簡單的, 直接利用 MySQL 自帶的自增 ID 功能就可以實現.

但在一個存在多個 Shards 的分散式系統 (例如多個 MySQL 例項組成一個叢集, 在這個叢集中插入資料), 這個問題會變得複雜, 所生成的全域性的 unique ID 要滿足以下需求:

  1. 保證生成的 ID 全域性唯一
  2. 今後資料在多個 Shards 之間遷移不會受到 ID 生成方式的限制
  3. 生成的 ID 中最好能帶上時間資訊, 例如 ID 的前 k 位是 Timestamp, 這樣能夠直接通過對 ID 的前 k 位的排序來對資料按時間排序
  4. 生成的 ID 最好不大於 64 bits
  5. 生成 ID 的速度有要求. 例如, 在一個高吞吐量的場景中, 需要每秒生成幾萬個 ID (Twitter 最新的峰值到達了 143,199 Tweets/s, 也就是 10萬+/秒)
  6. 整個服務最好沒有單點

如果沒有上面這些限制, 問題會相對簡單, 例如:

  1. 直接利用 UUID.randomUUID() 介面來生成 unique ID (http://www.ietf.org/rfc/rfc4122.txt). 但這個方案生成的 ID 有 128 bits, 另外, 生成的 ID 中也沒有帶 Timestamp
  2. 利用一箇中心伺服器來統一生成 unique ID. 但這種方案可能存在單點問題; 另外, 要支援高吞吐率的系統, 這個方案還要做很多改進工作 (例如, 每次從中心伺服器批量獲取一批 IDs, 提升 ID 產生的吞吐率)
  3. Flickr 的做法 (http://code.flickr.net/2010/02/08/ticket-servers-distributed-unique-primary-keys-on-the-cheap/). 但他這個方案 ID 中沒有帶 Timestamp, 生成的 ID 不能按時間排序

在要滿足前面 6 點要求的場景中, 怎麼來生成全域性 unique ID 呢?

Twitter 的 Snowflake 是一種比較好的做法. 下面主要介紹 Twitter Snowflake, 以及它的變種

二, Twitter 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 生成過程:

  • 10 bits 的機器號, 在 ID 分配 Worker 啟動的時候, 從一個 Zookeeper 叢集獲取 (保證所有的 Worker 不會有重複的機器號)
  • 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

整個過程中, 只是在 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 的其他變種

Snowflake 有一些變種, 各個應用結合自己的實際場景對 Snowflake 做了一些改動. 這裡主要介紹 3 種.

1. Boundary flake

http://boundary.com/blog/2012/01/12/flake-a-decentralized-k-ordered-unique-id-generator-in-erlang/

變化:

  • 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

2. Simpleflake

http://engineering.custommade.com/simpleflake-distributed-id-generation-for-the-lazy/

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).

3. instagram 的做法

先簡單介紹一下 instagram 的分散式儲存方案:

  • 先把每個 Table 劃分為多個邏輯分片 (logic Shard), 邏輯分片的數量可以很大, 例如 2000 個邏輯分片
  • 然後制定一個規則, 規定每個邏輯分片被儲存到哪個資料庫例項上面; 資料庫例項不需要很多. 例如, 對有 2 個 PostgreSQL 例項的系統 (instagram 使用 PostgreSQL); 可以使用奇數邏輯分片存放到第一個資料庫例項, 偶數邏輯分片存放到第二個資料庫例項的規則
  • 每個 Table 指定一個欄位作為分片欄位 (例如, 對使用者表, 可以指定 uid 作為分片欄位)
  • 插入一個新的資料時, 先根據分片欄位的值, 決定資料被分配到哪個邏輯分片 (logic Shard)
  • 然後再根據 logic Shard 和 PostgreSQL 例項的對應關係, 確定這條資料應該被存放到哪臺 PostgreSQL 例項上

instagram unique ID 的組成:

  • 41 bits: Timestamp (毫秒)
  • 13 bits: 每個 logic Shard 的代號 (最大支援 8 x 1024 個 logic Shards)
  • 10 bits: sequence number; 每個 Shard 每毫秒最多可以生成 1024 個 ID

生成 unique ID 時, 41 bits 的 Timestamp 和 Snowflake 類似, 這裡就不細說了.

主要介紹一下 13 bits 的 logic Shard 代號 和 10 bits 的 sequence number 怎麼生成.

logic Shard 代號:

  • 假設插入一條新的使用者記錄, 插入時, 根據 uid 來判斷這條記錄應該被插入到哪個 logic Shard 中.
  • 假設當前要插入的記錄會被插入到第 1341 號 logic Shard 中 (假設當前的這個 Table 一共有 2000 個 logic Shard)
  • 新生成 ID 的 13 bits 段要填的就是 1341 這個數字

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 為單位做資料遷移的, 所以這種做法也不會影響到今後的資料遷移

相關文章