在本章中,你的任務是設計一個分散式系統的唯一 ID 產生器(unique ID generator)。

你的第一個想法可能是使用傳統資料庫中具有 auto_increment 屬性的主鍵。然而,auto_increment 在分散式環境中無法運作,因為單一資料庫伺服器不夠大,而要在多個資料庫之間以最小延遲產生唯一 ID 是有挑戰性的。

以下是一些唯一 ID 的範例:

Figure 1

Step 1 - Understand the problem and establish design scope#

提出釐清問題是處理任何系統設計面試問題的第一步。以下是應徵者與面試官互動的範例:

應徵者:唯一 ID 有什麼特性?

面試官:ID 必須是唯一且可排序的。

應徵者:對於每筆新紀錄,ID 是否每次加 1?

面試官:ID 隨時間遞增,但不一定每次只加 1。同一天傍晚建立的 ID 會比早上建立的更大。

應徵者:ID 是否只包含數值?

面試官:是的,沒錯。

應徵者:ID 長度的需求是什麼?

面試官:ID 應符合 64-bit。

應徵者:系統規模如何?

面試官:系統應能每秒產生 10,000 個 ID。

以上是一些你可以問面試官的範例問題。理解需求並釐清模糊之處很重要。對於這個面試問題,需求列出如下:

  • ID 必須是唯一的。
  • ID 只包含數值。
  • ID 符合 64-bit。
  • ID 按日期排序。
  • 能夠每秒產生超過 10,000 個唯一 ID。

Step 2 - Propose high-level design and get buy-in#

可以使用多種方法在分散式系統中產生唯一 ID。我們考慮的選項是:

  • 多主複製(Multi-master replication)
  • 通用唯一識別符(Universally unique identifier,UUID)
  • Ticket server
  • Twitter snowflake 方法

讓我們看看每個方法、它們如何運作,以及每個選項的優缺點。

多主複製#

如圖 2 所示,第一種方法是多主複製。

Figure 2

這種方法使用資料庫的 auto_increment 功能。我們不將下一個 ID 加 1,而是加 k,其中 k 是使用中的資料庫伺服器數量。如圖 2 所示,要產生的下一個 ID 等於同一台伺服器中前一個 ID 加 2。這解決了一些可擴展性問題,因為 ID 可以隨資料庫伺服器數量擴展。

然而,這個策略有一些主要缺點:

  • 難以跨多個資料中心擴展
  • 跨多台伺服器時,ID 不會隨時間遞增。
  • 當伺服器被新增或移除時擴展性不佳。

UUID#

UUID 是另一種獲得唯一 ID 的簡單方式。UUID 是一個 128-bit 的數字,用於識別電腦系統中的資訊。

UUID 發生衝突的機率非常低。引述自 Wikipedia:「以每秒產生 10 億個 UUID 約 100 年後,產生單一重複的機率才會達到 50%」[1]。

以下是 UUID 的範例:09c93e62-50b4-468d-bf8a-c07e1040bfb2。UUID 可以獨立產生,不需要伺服器之間的協調。圖 3 呈現 UUID 設計。

Figure 3

在這個設計中,每台 web 伺服器包含一個 ID 產生器,每台 web 伺服器負責獨立產生 ID。

優點

  • 產生 UUID 很簡單。伺服器之間不需要協調,因此不會有任何同步問題。
  • 系統易於擴展,因為每台 web 伺服器負責產生其所消耗的 ID。ID 產生器可以輕易地隨 web 伺服器擴展。

缺點

  • ID 長度為 128 bit,但我們的需求是 64 bit。
  • ID 不隨時間遞增。
  • ID 可能不是數值。

Ticket Server#

Ticket server 是另一種有趣的產生唯一 ID 的方式。Flicker 開發了 ticket server 來產生分散式主鍵 [2]。值得一提的是這個系統如何運作。

Figure 4

這個想法是在單一資料庫伺服器(Ticket Server)中使用集中式 auto_increment 功能。要進一步了解,請參考 flicker 的工程部落格文章 [2]。

優點

  • 數值 ID。
  • 易於實作,適用於中小規模應用。

缺點

  • 單點故障(Single point of failure)。單一 ticket server 意味著如果 ticket server 停機,所有依賴它的系統都會出問題。為了避免單點故障,我們可以設定多個 ticket server。然而,這會引入新的挑戰,例如資料同步。

單點故障是 ticket server 方案最大的風險。雖然可以透過設置多台 ticket server 來緩解,但會引入資料同步的新挑戰。

Twitter snowflake 方法#

上述方法給了我們一些關於不同 ID 產生系統如何運作的想法。然而,它們都不符合我們的特定需求;因此,我們需要另一種方法。Twitter 名為「snowflake」的唯一 ID 產生系統 [3] 啟發人心,並能滿足我們的需求。

分而治之是我們的好朋友。我們不直接產生 ID,而是將 ID 分為不同區段。

圖 5 顯示 64-bit ID 的版面配置。

Figure 5

每個區段的解釋如下。

  • Sign bit:1 bit。它將永遠為 0。這是保留供未來使用。它可能用於區分有號數與無號數。
  • Timestamp:41 bits。從 epoch 或自訂 epoch 起的毫秒數。我們使用 Twitter snowflake 預設的 epoch 1288834974657,等同於 2010 年 11 月 4 日 01:42:54 UTC。
  • Datacenter ID:5 bits,給我們 2 ^ 5 = 32 個資料中心。
  • Machine ID:5 bits,給我們每個資料中心 2 ^ 5 = 32 台機器。
  • Sequence number:12 bits。對於該機器/行程上產生的每個 ID,序號加 1。每毫秒重設為 0。

Step 3 - Design deep dive#

在高階設計中,我們討論了在分散式系統中設計唯一 ID 產生器的多種選項。我們決定採用基於 Twitter snowflake ID 產生器的方法。讓我們深入設計。為了喚起記憶,設計圖再次列出如下。

Figure 6

Datacenter ID 與 machine ID 在啟動時被選定,一旦系統運作通常就固定不變。對 datacenter ID 與 machine ID 的任何變更都需要仔細審查,因為這些值的意外變更可能導致 ID 衝突。

Timestamp 與序號在 ID 產生器運作時產生。

Timestamp#

最重要的 41 bits 構成 timestamp 區段。由於 timestamp 隨時間遞增,ID 可按時間排序。圖 7 顯示二進位表示如何轉換為 UTC 的範例。你也可以使用類似的方法將 UTC 轉換回二進位表示。

Figure 7

41 bits 可以表示的最大 timestamp 是

2 ^ 41 - 1 = 2199023255551 毫秒(ms),這給我們:~ 69 年 = 2199023255551 ms / 1000 / 365 days / 24 hours/ 3600 seconds

這意味著 ID 產生器將運作 69 年,而採用接近今天日期的自訂 epoch 時間可以延後溢位時間。69 年後,我們將需要新的 epoch 時間或採用其他技術來遷移 ID。

Sequence number#

序號為 12 bits,給我們 2 ^ 12 = 4096 種組合。除非在同一伺服器上於同一毫秒內產生多個 ID,否則此欄位為 0。理論上,一台機器每毫秒可支援最多 4096 個新 ID。

Step 4 - Wrap up#

在本章中,我們討論了設計唯一 ID 產生器的不同方法:多主複製、UUID、ticket server 與類 Twitter snowflake 唯一 ID 產生器。我們決定採用 snowflake,因為它支援我們所有的使用情境,並在分散式環境中具有可擴展性。

如果在面試結束時還有額外時間,以下是一些額外的討論點:

  • 時鐘同步。在我們的設計中,我們假設 ID 產生伺服器擁有相同的時鐘。當伺服器在多核心上運作時,這個假設可能不成立。在多機器場景中也存在相同的挑戰。時鐘同步的解決方案超出本課程範圍;然而,理解這個問題的存在很重要。Network Time Protocol 是這個問題最熱門的解決方案。對此有興趣的讀者,請參考參考資料 [4]。
  • 區段長度調整。例如,較少的序號但更多的 timestamp 位元對於低並行性與長期應用程式有效。
  • 高可用性。由於 ID 產生器是任務關鍵系統,它必須具備高可用性。

恭喜你看到這裡!現在給自己拍拍背,做得好!