分佈式系統唯一ID生成方案淺析

分佈式系統唯一ID生成方案淺析

在複雜分佈式系統中,往往需要對大量的數據和消息進行唯一標識。業務ID需要滿足的要求如下

  • 全局唯一性:不能出現重複的ID號,既然是唯一標識,這是最基本的要求。
  • 趨勢遞增:在MySQL InnoDB引擎中使用的是聚集索引,由於多數RDBMS使用B-tree的數據結構來存儲索引數據,在主鍵的選擇上面我們應該儘量使用有序的主鍵保證寫入性能。
  • 單調遞增:保證下一個ID一定大於上一個ID,例如事務版本號、IM增量消息、排序等特殊需求。
  • 信息安全:如果ID是連續的,惡意用戶的扒取工作就非常容易做了,直接按照順序下載指定URL即可;如果是訂單號就更危險了,競對可以直接知道我們一天的單量。所以在一些應用場景下,會需要ID無規則、不規則。

UUID

UUID(Universally Unique Identifier)的標準型式包含32個16進制數字,以連字號分爲五段,形式爲8-4-4-4-12的36個字符,示例:550e8400-e29b-41d4-a716-446655440000,到目前爲止業界一共有5種方式生成UUID,詳情見IETF發佈的UUID規範A Universally Unique IDentifier (UUID) URN Namespace

優點:

  • 性能非常高:本地生成,沒有網絡消耗。

缺點:

  • 不易於存儲:UUID太長,16字節128位,通常以36長度的字符串表示,很多場景不適用。

  • 信息不安全:基於MAC地址生成UUID的算法可能會造成MAC地址泄露,這個漏洞曾被用於尋找梅麗莎病毒的製作者位置。

UUID作爲主鍵時在特定的環境會存在一些問題,比如做DB主鍵的場景下,UUID就非常不適用:

  • MySQL官方有明確的建議主鍵要儘量越短越好,36個字符長度的UUID不符合要求。

All indexes other than the clustered index are known as secondary indexes. In InnoDB, each record in a secondary index contains the primary key columns for the row, as well as the columns specified for the secondary index. InnoDB uses this primary key value to search for the row in the clustered index. If the primary key is long, the secondary indexes use more space, so it is advantageous to have a short primary key.

  • 對MySQL索引不利:如果作爲數據庫主鍵,在InnoDB引擎下,UUID的無序性可能會引起數據位置頻繁變動,嚴重影響性能。
public static function v4() {
        return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',

            // 32 bits for "time_low"
            mt_rand(0, 0xffff), mt_rand(0, 0xffff),

            // 16 bits for "time_mid"
            mt_rand(0, 0xffff),

            // 16 bits for "time_hi_and_version",
            // four most significant bits holds version number 4
            mt_rand(0, 0x0fff) | 0x4000,

            // 16 bits, 8 bits for "clk_seq_hi_res",
            // 8 bits for "clk_seq_low",
            // two most significant bits holds zero and one for variant DCE1.1
            mt_rand(0, 0x3fff) | 0x8000,

            // 48 bits for "node"
            mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
        );
    }

類snowlack

這種方案大致來說是一種以劃分命名空間(UUID也算,由於比較常見,所以單獨分析)來生成ID的一種算法,這種方案把64-bit分別劃分成多段,分開來標示機器、時間等,比如在snowflake中的64-bit分別表示如下圖(圖片來自網絡)所示:

snowlack

41-bit的時間可以表示(1L<<41)/(1000L360024*365)=69年的時間,10-bit機器可以分別表示1024臺機器。如果我們對IDC劃分有需求,還可以將10-bit分5-bit給IDC,分5-bit給工作機器。這樣就可以表示32個IDC,每個IDC下可以有32臺機器,可以根據自身需求定義。12個自增序列號可以表示2^12個ID,理論上snowflake方案的QPS約爲409.6w/s,這種分配方式可以保證在任何一個IDC的任何一臺機器在任意毫秒內生成的ID都是不同的。

這種方式的優缺點是:

優點:

毫秒數在高位,自增序列在低位,整個ID都是趨勢遞增的。

不依賴數據庫等第三方系統,以服務的方式部署,穩定性更高,生成ID的性能也是非常高的。

可以根據自身業務特性分配bit位,非常靈活。

缺點:

強依賴機器時鐘,如果機器上時鐘回撥,會導致發號重複或者服務會處於不可用狀態。

應用舉例Mongdb objectID:

MongoDB官方文檔 ObjectID可以算作是和snowflake類似方法,通過“時間+機器碼+pid+inc”共12個字節,通過4+3+2+3的方式最終標識成一個24長度的十六進制字

數據庫生成

以MySQL舉例,利用給字段設置auto_increment_increment和auto_increment_offset來保證ID自增,每次業務使用下列SQL讀寫MySQL得到ID號。

這種方案的優缺點如下:

優點:

  • 非常簡單,利用現有數據庫系統的功能實現,成本小,有DBA專業維護。
    ID號單調自增,可以實現一些對ID有特殊要求的業務。

缺點:

  • 強依賴DB,當DB異常時整個系統不可用,屬於致命問題。配置主從複製可以儘可能的增加可用性,但是數據一致性在特殊情況下難以保證。主從切換時的不一致可能會導致重複發號。
  • ID發號性能瓶頸限制在單臺MySQL的讀寫性能。

微信 seqsvr

不考慮 seqsvr 的具體架構的話,它應該是一個巨大的 64 位數組,而我們每一個微信用戶,都在這個大數組裏獨佔一格 8bytes 的空間,這個格子就放着用戶已經分配出去的最後一個 sequence:cur_seq。每個用戶來申請 sequence 的時候,只需要將用戶的 cur_seq+=1,保存回數組,並返回給用戶。

圖 1. 小明申請了一個 sequence,返回 101

圖 1. 小明申請了一個 sequence,返回 101

預分配中間層:

任何一件看起來很簡單的事,在海量的訪問量下都會變得不簡單。前文提到,seqsvr 需要保證分配出去的 sequence 遞增(數據可靠),還需要滿足海量的訪問量(每天接近萬億級別的訪問)。滿足數據可靠的話,我們很容易想到把數據持久化到硬盤,但是按照目前每秒千萬級的訪問量(~10^7 QPS),基本沒有任何硬盤系統能扛住。

後臺架構設計很多時候是一門關於權衡的哲學,針對不同的場景去考慮能不能降低某方面的要求,以換取其它方面的提升。仔細考慮我們的需求,我們只要求遞增,並沒有要求連續,也就是說出現一大段跳躍是允許的(例如分配出的 sequence 序列:1,2,3,10,100,101)。於是我們實現了一個簡單優雅的策略:

  1. 內存中儲存最近一個分配出去的 sequence:cur_seq,以及分配上限:max_seq
  2. 分配 sequence 時,將 cur_seq++,同時與分配上限 max_seq 比較:如果 cur_seq > max_seq,將分配上限提升一個步長 max_seq += step,並持久化 max_seq
  3. 重啓時,讀出持久化的 max_seq,賦值給 cur_seq

圖 2. 小明、小紅、小白都各自申請了一個 sequence,但只有小白的 max_seq 增加了步長 100

圖 2. 小明、小紅、小白都各自申請了一個 sequence,但只有小白的 max_seq 增加了步長 100

這樣通過增加一個預分配 sequence 的中間層,在保證 sequence 不回退的前提下,大幅地提升了分配 sequence 的性能。實際應用中每次提升的步長爲 10000,那麼持久化的硬盤 IO 次數從之前~10^7 QPS 降低到~10^3 QPS,處於可接受範圍。在正常運作時分配出去的 sequence 是順序遞增的,只有在機器重啓後,第一次分配的 sequence 會產生一個比較大的跳躍,跳躍大小取決於步長大小。

分號段共享存儲:

請求帶來的硬盤 IO 問題解決了,可以支持服務平穩運行,但該模型還是存在一個問題:重啓時要讀取大量的 max_seq 數據加載到內存中。

我們可以簡單計算下,以目前 uid(用戶唯一 ID)上限 2^32 個、一個 max_seq 8bytes 的空間,數據大小一共爲 32GB,從硬盤加載需要不少時間。另一方面,出於數據可靠性的考慮,必然需要一個可靠存儲系統來保存 max_seq 數據,重啓時通過網絡從該可靠存儲系統加載數據。如果 max_seq 數據過大的話,會導致重啓時在數據傳輸花費大量時間,造成一段時間不可服務。

爲了解決這個問題,我們引入號段 Section 的概念,uid 相鄰的一段用戶屬於一個號段,而同個號段內的用戶共享一個 max_seq,這樣大幅減少了 max_seq 數據的大小,同時也降低了 IO 次數。

圖 3. 小明、小紅、小白屬於同個 Section,他們共用一個 max_seq。在每個人都申請一個 sequence 的時候,只有小白突破了 max_seq 上限,需要更新 max_seq 並持久化

圖 3. 小明、小紅、小白屬於同個 Section,他們共用一個 max_seq。在每個人都申請一個 sequence 的時候,只有小白突破了 max_seq 上限,需要更新 max_seq 並持久化

目前 seqsvr 一個 Section 包含 10 萬個 uid,max_seq 數據只有 300+KB,爲我們實現從可靠存儲系統讀取 max_seq 數據重啓打下基礎。

參考資料

文章:

各大公司的開源項目:

本文首發於:

本文作者: 荒古
本文鏈接: https://haxianhe.com/2019/10/12/分佈式系統唯一ID生成方案淺析/
版權聲明: 本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 3.0 許可協議。轉載請註明出處!

歡迎關注我的公衆號:荒古傳說

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章