忘掉 Snowflake,感覺一下性能高出 587 倍的全局惟一 ID 生成算法

今天咱們來拆解 Snowflake 算法,同時領略百度、美團、騰訊等大廠在全局惟一 ID 服務方面作的設計,接着根據具體需求設計一款全新的全局惟一 ID 生成算法。這還不夠,咱們會討論到全局惟一 ID 服務的分佈式 CAP 選擇與性能瓶頸。html

已經熟悉 Snowflake 的朋友能夠先去看大廠的設計和權衡。git

百度 UIDGenertor:https://github.com/baidu/uid-...github

美團 Leaf:https://tech.meituan.com/2017...算法

騰訊 Seqsvr: https://www.infoq.cn/article/...數據庫

全局惟一 ID 是分佈式系統和訂單類業務系統中重要的基礎設施。這裏引用美團的描述:編程

在複雜分佈式系統中,每每須要對大量的數據和消息進行惟一標識。如在美團點評的金融、支付、餐飲、酒店、貓眼電影等產品的系統中,數據日漸增加,對數據分庫分表後須要有一個惟一 ID 來標識一條數據或消息,數據庫的自增 ID 顯然不能知足需求;特別一點的如訂單、騎手、優惠券也都須要有惟一 ID 作標識。

這時候你可能會問:我仍是不懂,爲何必定要全局惟一 ID?數組

我再列舉一個場景,在 MySQL 分庫分表的條件下,MySQL 沒法作到依次、順序、交替地生成 ID,這時候要保證數據的順序,全局惟一 ID 就是一個很好的選擇。緩存

在爬蟲場景中,這條數據在進入數據庫以前會進行數據清洗、校驗、矯正、分析等多個流程,這期間有必定機率發生重試或設爲異常等操做,也就是說在進入數據庫以前它就須要有一個 ID 來標識它。安全

全局惟一 ID 應當具有什麼樣的屬性,纔可以知足上述的場景呢?

美團技術團隊列出的 4 點屬性我以爲很準確,它們是:性能優化

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

看上去第 3 點和第 4 點彷佛還存在些許衝突,這個後面再說。除了以上列舉的 ID 屬性外,基於這個生成算法構建的服務還須要買足高 QPS、高可用性和低延遲的幾個要求。

業內常見的 ID 生成方式有哪些?

你們在念書的時候確定都學過 UUIDGUID,它們生成的值看上去像這樣:

6F9619FF-8B86-D011-B42D-00C04FC964FF

因爲不是純數字組成,這就沒法知足趨勢遞增和單調遞增這兩個屬性,同時在寫入時也會下降寫入性能。上面提到了數據庫自增 ID 沒法知足入庫前使用和分佈式場景下的需求,遂排除。

有人提出了藉助 Redis 來實現,例如訂單號=日期+當日自增加號,自增加經過 INCR 實現。但這樣操做的話又沒法知足編號不可猜想需求。

這時候有人提出了 MongoDB 的 ObjectID,不要忘了它生成的 ID 是這樣的: 5b6b3171599d6215a8007se0,和 UUID 同樣沒法知足遞增屬性,且和 MySQL 同樣要入庫後才能生成。

難道就沒有能打的了嗎

大名鼎鼎的 Snowflake

Twitter 於 2010 年開源了內部團隊在用的一款全局惟一 ID 生成算法 Snowflake,翻譯過來叫作雪花算法。Snowflake 不借助數據庫,可直接由編程語言生成,它經過巧妙的位設計使得 ID 可以知足遞增屬性,且生成的 ID 並非依次連續的,可以知足上面提到的全局惟一 ID 的 4 個屬性。它連續生成的 3 個 ID 看起來像這樣:

563583455628754944
563583466173235200
563583552944996352

Snowflake 以 64 bit 來存儲組成 ID 的4 個部分:

一、最高位佔1 bit,值固定爲 0,以保證生成的 ID 爲正數;

二、中位佔 41 bit,值爲毫秒級時間戳;

三、中下位佔 10 bit,值爲工做機器的 ID,值的上限爲 1024;

四、末位佔 12 bit,值爲當前毫秒內生成的不一樣 ID,值的上限爲 4096;

Snowflake 的代碼實現網上有不少款,基本上各大語言都能找到實現參考。我以前在作實驗的時候在網上找到一份 Golang 的代碼實現:

代碼可在個人 Gist 查看和下載。

Snowflake 存在的問題

snowflake 不依賴數據庫,也不依賴內存存儲,隨時可生成 ID,這也是它如此受歡迎的緣由。但由於它在設計時經過時間戳來避免對內存和數據庫的依賴,因此它依賴於服務器的時間。上面咱們提到了 Snowflake 的 4 段結構,實際上影響 ID 大小的是較高位的值,因爲最高位固定爲 0,遂影響 ID 大小的是中位的值,也就是時間戳。

試想,服務器的時間發生了錯亂或者回撥,這就直接影響到生成的 ID,有很大機率生成重複的 ID必定會打破遞增屬性。這是一個致命缺點,你想一想,支付訂單和購買訂單的編號重複,這是多麼嚴重的問題!

另外,因爲它的中下位末位 bit 數限制,它每毫秒生成 ID 的上限嚴重受到限制。因爲中位是 41 bit 的毫秒級時間戳,因此從當前起始到 41 bit 耗盡,也只能堅持 70 年

再有,程序獲取操做系統時間會耗費較多時間,相比於隨機數和常數來講,性能相差太遠,這是制約它生成性能的最大因素

一線企業如何解決全局惟一 ID 問題

長話短說,咱們來看看百度、美團、騰訊(微信)是如何作的。

百度團隊開源了 UIDGenerator 算法.

它經過借用將來時間和雙 Buffer 來解決時間回撥與生成性能等問題,同時結合 MySQL 進行 ID 分配。這是一種基於 Snowflake 的優化操做,是一個好的選擇,你認爲這是否是優選呢?

美團團隊根據業務場景提出了基於號段思想的 Leaf-Segment 方案和基於 Snowflake 的 Leaf-Snowflake 方案.

出現兩種方案的緣由是 Leaf-Segment 並無知足安全屬性要求,容易被猜想,沒法用在對外開放的場景(如訂單)。Leaf-Snowflake 經過文件系統緩存下降了對 ZooKeeper 的依賴,同時經過對時間的比對和警報來應對 Snowflake 的時間回撥問題。這兩種都是一個好的選擇,你認爲這是否是優選呢?

微信團隊業務特殊,它有一個用 ID 來標記消息的順序的場景,用來確保咱們收到的消息就是有序的。在這裏不是全局惟一 ID,而是單個用戶全局惟一 ID,只須要保證這個用戶發送的消息的 ID 是遞增便可。

這個項目叫作 Seqsvr,它並無依賴時間,而是經過自增數和號段來解決生成問題的。這是一個好的選擇,你認爲這是否是優選呢?

性能高出 Snowflake 587 倍的算法是如何設計的?

在瞭解 Snowflake 的優缺點、閱讀了百度 UIDGenertor、美團 Leaf 和騰訊微信 Seqsvr 的設計後,我但願設計出一款可以知足全局惟一 ID 4 個屬性且性能更高、使用期限更長、不受單位時間限制、不依賴時間的全局惟一 ID 生成算法。

這看起來很簡單,但吸取所學知識、設計、實踐和性能優化佔用了我 4 個週末的時間。在我看來,這個算法的設計過程就像是液態的水轉換爲氣狀的霧同樣,遂我給這個算法取名爲薄霧(Mist)算法。接下來咱們來看看薄霧算法是如何設計和實現的。

位數是影響 ID 數值上限的主要因素,Snowflake 中下位和末位的 bit 數限制了單位時間內生成 ID 的上限,要解決這個兩個問題,就必須從新設計 ID 的組成。

拋開中位,咱們先看看中下位和末位的設計。中下位的 10 bit 的值實際上是機器編號,末位 12 bit 的值實際上是單位時間(同一毫秒)內生成的 ID 序列號,表達的是這毫秒生成的第 5 個或第 150 個 數值,同時兩者的組合使得 ID 的值變幻莫測,知足了安全屬性。實際上並不須要記錄機器編號,也能夠不用管它究竟是單位時間內生成的第幾個數值,安全屬性咱們能夠經過多組隨機數組合的方式實現,隨着數字的遞增和隨機數的變幻,經過 ID 猜順序的難度是很高的。

最高位固定是 0,不須要對它進行改動。咱們來看看相當重要的中位,Snowflake 的中位是毫秒級時間戳,既然不打算依賴時間,那麼確定也不會用時間戳,用什麼呢?我選擇自增數 1,2,3,4,5,...。中位決定了生成 ID 的上限和使用期限,若是沿用 41 bit,那麼上限跟用時間戳的上限相差無幾,通過計算後我選擇採用與 Snowflake 的不一樣的分段:

縮減中下位和末位的 bit 數,增長中位的 bit 數,這樣就能夠擁有更高的上限和使用年限,那上限和年限如今是多久呢?中位數值的上限計算公式爲 int64(1<<47 - 1),計算結果爲 140737488355327百萬億級的數值,假設天天消耗 10 億 ID,薄霧算法能用 385+ 年,幾輩子都用不完

中下位和末位都是 8 bit,數值上限是 255,即開閉區間是 [0, 255]。這兩段若是用隨機數進行填充,對應的組合方式有 256 * 256 種,且每次都會變化,猜想難度至關高。因爲不像 Snowflake 那樣須要計算末位的序列號,遂薄霧算法的代碼並不長,具體代碼可在個人 GitHub 倉庫找到:

聊聊性能問題,獲取時間戳是比較耗費性能的,不獲取時間戳速度固然快了,那 500+ 倍是如何得來的呢?以 Golang 爲例(我用 Golang 作過實驗),Golang 隨機數有三種生成方式:

  • 基於固定數值種子的隨機數;
  • 將會變換的時間戳做爲種子的隨機數;
  • 大數真隨機;

基於固定數值種子的隨機數每次生成的值都是同樣的,是僞隨機,不可用在此處。將時間戳做爲種子以生成隨機數是目前 Golang 開發者的主流作法,實測性能約爲 8800 ns/op。

大數真隨機知道的人比較少,實測性能 335ns/op,因而可知性能相差近 30 倍。大數真隨機也有必定的損耗,若是想要將性能提高到頂點,只須要將中下位和末位的隨機數換成常數便可,常數實測性能 15ns/op,是時間戳種子隨機數的 587 倍。

要注意的是,將常數放到中下位和末位的性能是很高,可是猜想難度也相應降低。

薄霧算法的依賴問題

薄霧算法爲了避開時間依賴,不得不依賴存儲,中位自增的數值只能在內存中存活,遂須要依賴存儲將自增數值存儲起來,避免由於宕機或程序異常形成重複 ID 的事故。

看起來是這樣,但它真的是依賴存儲嗎?

你想一想,這麼重要的服務一定要求高可用,不管你用 Twitter 仍是百度或者美團、騰訊微信的解決方案,在架構上必定都是高可用的,高可用必定須要存儲。在這樣的背景下,薄霧算法的依賴其實並非額外的依賴,而是能夠與架構徹底融合到一塊兒的設計。

薄霧算法和 Redis 的結合

既然提出了薄霧算法,怎麼能不提供真實可用的工程實踐呢?在編寫完薄霧算法以後,我就開始了工程實踐的工做,將薄霧算法與 KV 存儲結合到一塊兒,提供全局惟一 ID 生成服務。這裏我選擇了較爲熟悉的 Redis,Mist 與 Redis 的結合,我爲這個項目取的名字爲 Medis。

性能高並非編造出來的,咱們看看它 Jemeter 壓測參數和結果:

以上是 Medis README 中給出的性能測試截圖,在大基數條件下的性能約爲 2.5w/sec。這麼高的性能除了薄霧算法自己高性能以外,Medis 的設計也做出了很大貢獻:

  • 使用 Channel 做爲數據緩存,這個操做使得發號服務性能提高了 7 倍;
  • 採用預存預取的策略保證 Channel 在大多數狀況下都有值,從而可以迅速響應客戶端發來的請求;
  • Gorouting 去執行耗費時間的預存預取操做,不會影響對客戶端請求的響應;
  • 採用 Lrange Ltrim 組合從 Redis 中批量取值,這比循環單次讀取或者管道批量讀取的效率更高;
  • 寫入 Redis 時採用管道批量寫入,效率比循環單次寫入更高;
  • Seqence 值的計算在預存前進行,這樣就不會耽誤對客戶端請求的響應,雖然薄霧算法的性能是納秒級別,但併發高的時候也形成一些性能損耗,放在預存時計算顯然更香;
  • 得益於 Golang Echo 框架和 Golang 自己的高性能,整套流程下來我很滿意,若是要追求極致性能,我推薦你們試試 Rust;

Medis 服務啓動流程和接口訪問流程圖下所示:

感興趣的朋友能夠下載體驗一下,啓動 Medis 根目錄的 server.go 後,訪問 http://localhost:1558/sequence 便能拿到全局惟一 ID。

高可用架構和分佈式性能

分佈式 CAP (一致性、可用性、分區容錯性)已成定局,這類服務一般追求的是可用性架構(AP)。因爲設計中採用了預存預取,且要保持總體順序遞增,遂單機提供訪問是優選,即分佈式架構下的性能上限就是提供服務的那臺主機的單機性能。

你想要實現分佈式多機提供服務?

這樣的需求要改動 Medis 的邏輯,同時也須要改動各應用之間的組合關係。若是要實現分佈式多機同時提供服務,那麼就要廢棄 Redis 和 Channel 預存預取機制,接着放棄 Channel 而改用即時生成,這樣即可以同時使用多個 Server,但性能的瓶頸就轉移到了 KV 存儲(這裏是 Redis),性能等同於單機 Redis 的性能。你能夠採用 ETCD 或者 Zookeeper 來實現多 KV,但這不是又回到了 CAP 原點了嗎?

至於怎麼選擇,可根據實際業務場景和需求與架構進行討論,選擇一個適合的方案進行部署便可。

領略了 Mist 和 Medis 的風采後,相信你必定會有其餘巧妙的想法,歡迎在評論區留言,咱們一塊兒交流進步!

夜幕團隊成立於 2019 年,團隊包括崔慶才(靜覓)、周子淇(Loco)、陳祥安(CXA)、唐軼飛(大魚|BruceDone)、馮威(妄爲)、蔡晉(悅來客棧的老闆)、戴煌金(鹹魚)、張冶青(MarvinZ)、韋世東(Asyncins|奎因)和文安哲(sml2h3)。

涉獵的編程語言包括但不限於 Python、Rust、C++、Go,領域涵蓋爬蟲、深度學習、服務研發、逆向工程、軟件安全等。團隊非正亦非邪,只作認爲對的事情,請你們當心。

相關文章
相關標籤/搜索