Leaf——美團點評分佈式ID生成系統

背景

在複雜分佈式系統中,每每須要對大量的數據和消息進行惟一標識。如在美團點評的金融、支付、餐飲、酒店、貓眼電影等產品的系統中,數據日漸增加,對數據分庫分表後須要有一個惟一ID來標識一條數據或消息,數據庫的自增ID顯然不能知足需求;特別一點的如訂單、騎手、優惠券也都須要有惟一ID作標識。此時一個可以生成全局惟一ID的系統是很是必要的。歸納下來,那業務系統對ID號的要求有哪些呢?html

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

上述123對應三類不一樣的場景,3和4需求仍是互斥的,沒法使用同一個方案知足。node

同時除了對ID號碼自身的要求,業務還對ID號生成系統的可用性要求極高,想象一下,若是ID生成系統癱瘓,整個美團點評支付、優惠券發券、騎手派單等關鍵動做都沒法執行,這就會帶來一場災難。mysql

由此總結下一個ID生成系統應該作到以下幾點:算法

  1. 平均延遲和TP999延遲都要儘量低;
  2. 可用性5個9;
  3. 高QPS。

常見方法介紹

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 Namespacesql

優勢:mongodb

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

缺點:數據庫

  • 不易於存儲:UUID太長,16字節128位,一般以36長度的字符串表示,不少場景不適用。
  • 信息不安全:基於MAC地址生成UUID的算法可能會形成MAC地址泄露,這個漏洞曾被用於尋找梅麗莎病毒的製做者位置。
  • ID做爲主鍵時在特定的環境會存在一些問題,好比作DB主鍵的場景下,UUID就很是不適用:緩存

    ① MySQL官方有明確的建議主鍵要儘可能越短越好[4],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的無序性可能會引發數據位置頻繁變更,嚴重影響性能。網絡

類snowflake方案

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

image

41-bit的時間能夠表示(1L<<41)/(1000L*3600*24*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_incrementauto_increment_offset來保證ID自增,每次業務使用下列SQL讀寫MySQL獲得ID號。

begin;
REPLACE INTO Tickets64 (stub) VALUES ('a');
SELECT LAST_INSERT_ID();
commit;

 

這種方案的優缺點以下:

優勢:

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

缺點:

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

對於MySQL性能問題,可用以下方案解決:在分佈式系統中咱們能夠多部署幾臺機器,每臺機器設置不一樣的初始值,且步長和機器數相等。好比有兩臺機器。設置步長step爲2,TicketServer1的初始值爲1(1,3,5,7,9,11...)、TicketServer2的初始值爲2(2,4,6,8,10...)。這是Flickr團隊在2010年撰文介紹的一種主鍵生成策略(Ticket Servers: Distributed Unique Primary Keys on the Cheap )。以下所示,爲了實現上述方案分別設置兩臺機器對應的參數,TicketServer1從1開始發號,TicketServer2從2開始發號,兩臺機器每次發號以後都遞增2。 

TicketServer1:
auto-increment-increment = 2
auto-increment-offset = 1

TicketServer2:
auto-increment-increment = 2
auto-increment-offset = 2

假設咱們要部署N臺機器,步長需設置爲N,每臺的初始值依次爲0,1,2...N-1那麼整個架構就變成了以下圖所示:

 

這種架構貌似可以知足性能的需求,但有如下幾個缺點:

  • 系統水平擴展比較困難,好比定義好了步長和機器臺數以後,若是要添加機器該怎麼作?假設如今只有一臺機器發號是1,2,3,4,5(步長是1),這個時候須要擴容機器一臺。能夠這樣作:把第二臺機器的初始值設置得比第一臺超過不少,好比14(假設在擴容時間以內第一臺不可能發到14),同時設置步長爲2,那麼這臺機器下發的號碼都是14之後的偶數。而後摘掉第一臺,把ID值保留爲奇數,好比7,而後修改第一臺的步長爲2。讓它符合咱們定義的號段標準,對於這個例子來講就是讓第一臺之後只能產生奇數。擴容方案看起來複雜嗎?貌似還好,如今想象一下若是咱們線上有100臺機器,這個時候要擴容該怎麼作?簡直是噩夢。因此係統水平擴展方案複雜難以實現。
  • ID沒有了單調遞增的特性,只能趨勢遞增,這個缺點對於通常業務需求不是很重要,能夠容忍。
  • 數據庫壓力仍是很大,每次獲取ID都得讀寫一次數據庫,只能靠堆機器來提升性能。

Leaf方案實現

Leaf這個名字是來自德國哲學家、數學家萊布尼茨的一句話:

There are no two identical leaves in the world

"世界上沒有兩片相同的樹葉"

綜合對比上述幾種方案,每種方案都不徹底符合咱們的要求。因此Leaf分別在上述第二種和第三種方案上作了相應的優化,實現了Leaf-segment和Leaf-snowflake方案。

Leaf-segment數據庫方案

第一種Leaf-segment方案,在使用數據庫的方案上,作了以下改變:

  • 原方案每次獲取ID都得讀寫一次數據庫,形成數據庫壓力大。改成利用proxy server批量獲取,每次獲取一個segment(step決定大小)號段的值。用完以後再去數據庫獲取新的號段,能夠大大的減輕數據庫的壓力。
  • 各個業務不一樣的發號需求用biz_tag字段來區分,每一個biz-tag的ID獲取相互隔離,互不影響。若是之後有性能需求須要對數據庫擴容,不須要上述描述的複雜的擴容操做,只須要對biz_tag分庫分表就行。

數據庫表設計以下:

 

+-------------+--------------+------+-----+-------------------+-----------------------------+ | Field | Type | Null | Key | Default | Extra | +-------------+--------------+------+-----+-------------------+-----------------------------+ | biz_tag | varchar(128) | NO | PRI | | | | max_id | bigint(20) | NO | | 1 | | | step | int(11) | NO | | NULL | | | desc | varchar(256) | YES | | NULL | | | update_time | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP | +-------------+--------------+------+-----+-------------------+-----------------------------+ 

重要字段說明:biz_tag用來區分業務,max_id表示該biz_tag目前所被分配的ID號段的最大值,step表示每次分配的號段長度。原來獲取ID每次都須要寫數據庫,如今只須要把step設置得足夠大,好比1000。那麼只有當1000個號被消耗完了以後纔會去從新讀寫一次數據庫。讀寫數據庫的頻率從1減少到了1/step,大體架構以下圖所示:

 

test_tag在第一臺Leaf機器上是1~1000的號段,當這個號段用完時,會去加載另外一個長度爲step=1000的號段,假設另外兩臺號段都沒有更新,這個時候第一臺機器新加載的號段就應該是3001~4000。同時數據庫對應的biz_tag這條數據的max_id會從3000被更新成4000,更新號段的SQL語句以下:

Begin
UPDATE table SET max_id=max_id+step WHERE biz_tag=xxx
SELECT tag, max_id, step FROM table WHERE biz_tag=xxx
Commit

這種模式有如下優缺點:

優勢:

  • Leaf服務能夠很方便的線性擴展,性能徹底可以支撐大多數業務場景。
  • ID號碼是趨勢遞增的8byte的64位數字,知足上述數據庫存儲的主鍵要求。
  • 容災性高:Leaf服務內部有號段緩存,即便DB宕機,短期內Leaf仍能正常對外提供服務。
  • 能夠自定義max_id的大小,很是方便業務從原有的ID方式上遷移過來。

缺點:

  • ID號碼不夠隨機,可以泄露發號數量的信息,不太安全。
  • TP999數據波動大,當號段使用完以後仍是會hang在更新數據庫的I/O上,tg999數據會出現偶爾的尖刺。
  • DB宕機會形成整個系統不可用。

雙buffer優化

對於第二個缺點,Leaf-segment作了一些優化,簡單的說就是:

Leaf 取號段的時機是在號段消耗完的時候進行的,也就意味着號段臨界點的ID下發時間取決於下一次從DB取回號段的時間,而且在這期間進來的請求也會由於DB號段沒有取回來,致使線程阻塞。若是請求DB的網絡和DB的性能穩定,這種狀況對系統的影響是不大的,可是假如取DB的時候網絡發生抖動,或者DB發生慢查詢就會致使整個系統的響應時間變慢。

爲此,咱們但願DB取號段的過程可以作到無阻塞,不須要在DB取號段的時候阻塞請求線程,即當號段消費到某個點時就異步的把下一個號段加載到內存中。而不須要等到號段用盡的時候纔去更新號段。這樣作就能夠很大程度上的下降系統的TP999指標。詳細實現以下圖所示:

採用雙buffer的方式,Leaf服務內部有兩個號段緩存區segment。當前號段已下發10%時,若是下一個號段未更新,則另啓一個更新線程去更新下一個號段。當前號段所有下發完後,若是下個號段準備好了則切換到下個號段爲當前segment接着下發,循環往復。

  • 每一個biz-tag都有消費速度監控,一般推薦segment長度設置爲服務高峯期發號QPS的600倍(10分鐘),這樣即便DB宕機,Leaf仍能持續發號10-20分鐘不受影響。

  • 每次請求來臨時都會判斷下個號段的狀態,從而更新此號段,因此偶爾的網絡抖動不會影響下個號段的更新。

Leaf高可用容災

對於第三點「DB可用性」問題,咱們目前採用一主兩從的方式,同時分機房部署,Master和Slave之間採用半同步方式[5]同步數據。同時使用公司Atlas數據庫中間件(已開源,更名爲DBProxy)作主從切換。固然這種方案在一些狀況會退化成異步模式,甚至在很是極端狀況下仍然會形成數據不一致的狀況,可是出現的機率很是小。若是你的系統要保證100%的數據強一致,能夠選擇使用「類Paxos算法」實現的強一致MySQL方案,如MySQL 5.7前段時間剛剛GA的MySQL Group Replication。可是運維成本和精力都會相應的增長,根據實際狀況選型便可。

 

同時Leaf服務分IDC部署,內部的服務化框架是「MTthrift RPC」。服務調用的時候,根據負載均衡算法會優先調用同機房的Leaf服務。在該IDC內Leaf服務不可用的時候纔會選擇其餘機房的Leaf服務。同時服務治理平臺OCTO還提供了針對服務的過載保護、一鍵截流、動態流量分配等對服務的保護措施。

Leaf-snowflake方案

Leaf-segment方案能夠生成趨勢遞增的ID,同時ID號是可計算的,不適用於訂單ID生成場景,好比競對在兩天中午12點分別下單,經過訂單id號相減就能大體計算出公司一天的訂單量,這個是不能忍受的。面對這一問題,咱們提供了 Leaf-snowflake方案。

 

Leaf-snowflake方案徹底沿用snowflake方案的bit位設計,便是「1+41+10+12」的方式組裝ID號。對於workerID的分配,當服務集羣數量較小的狀況下,徹底能夠手動配置。Leaf服務規模較大,動手配置成本過高。因此使用Zookeeper持久順序節點的特性自動對snowflake節點配置wokerID。Leaf-snowflake是按照下面幾個步驟啓動的:

  1. 啓動Leaf-snowflake服務,鏈接Zookeeper,在leaf_forever父節點下檢查本身是否已經註冊過(是否有該順序子節點)。
  2. 若是有註冊過直接取回本身的workerID(zk順序節點生成的int類型ID號),啓動服務。
  3. 若是沒有註冊過,就在該父節點下面建立一個持久順序節點,建立成功後取回順序號當作本身的workerID號,啓動服務。

弱依賴ZooKeeper

除了每次會去ZK拿數據之外,也會在本機文件系統上緩存一個workerID文件。當ZooKeeper出現問題,剛好機器出現問題須要重啓時,能保證服務可以正常啓動。這樣作到了對三方組件的弱依賴。必定程度上提升了SLA

解決時鐘問題

由於這種方案依賴時間,若是機器的時鐘發生了回撥,那麼就會有可能生成重複的ID號,須要解決時鐘回退的問題。

 

參見上圖整個啓動流程圖,服務啓動時首先檢查本身是否寫過ZooKeeper leaf_forever節點:

  1. 若寫過,則用自身系統時間與leaf_forever/${self}節點記錄時間作比較,若小於leaf_forever/${self}時間則認爲機器時間發生了大步長回撥,服務啓動失敗並報警。
  2. 若未寫過,證實是新服務節點,直接建立持久節點leaf_forever/${self}並寫入自身系統時間,接下來綜合對比其他Leaf節點的系統時間來判斷自身系統時間是否準確,具體作法是取leaf_temporary下的全部臨時節點(全部運行中的Leaf-snowflake節點)的服務IP:Port,而後經過RPC請求獲得全部節點的系統時間,計算sum(time)/nodeSize。
  3. 若abs( 系統時間-sum(time)/nodeSize ) < 閾值,認爲當前系統時間準確,正常啓動服務,同時寫臨時節點leaf_temporary/${self} 維持租約。
  4. 不然認爲本機系統時間發生大步長偏移,啓動失敗並報警。
  5. 每隔一段時間(3s)上報自身系統時間寫入leaf_forever/${self}。

因爲強依賴時鐘,對時間的要求比較敏感,在機器工做時NTP同步也會形成秒級別的回退,建議能夠直接關閉NTP同步。要麼在時鐘回撥的時候直接不提供服務直接返回ERROR_CODE,等時鐘追上便可。或者作一層重試,而後上報報警系統,更或者是發現有時鐘回撥以後自動摘除自己節點並報警,以下:

 

//發生了回撥,此刻時間小於上次發號時間
 if (timestamp < lastTimestamp) {

            long offset = lastTimestamp - timestamp;
            if (offset <= 5) {
                try {
                    //時間誤差大小小於5ms,則等待兩倍時間
                    wait(offset << 1);//wait
                    timestamp = timeGen();
                    if (timestamp < lastTimestamp) {
                       //仍是小於,拋異常並上報
                        throwClockBackwardsEx(timestamp);
                      }    
                } catch (InterruptedException e) {  
                   throw  e;
                }
            } else {
                //throw
                throwClockBackwardsEx(timestamp);
            }
        }
 //分配ID

從上線狀況來看,在2017年閏秒出現那一次出現過部分機器回撥,因爲Leaf-snowflake的策略保證,成功避免了對業務形成的影響。

Leaf現狀

Leaf在美團點評公司內部服務包含金融、支付交易、餐飲、外賣、酒店旅遊、貓眼電影等衆多業務線。目前Leaf的性能在4C8G的機器上QPS能壓測到近5w/s,TP999 1ms,已經可以知足大部分的業務的需求。天天提供億數量級的調用量,做爲公司內部公共的基礎技術設施,必須保證高SLA和高性能的服務,咱們目前還僅僅達到了及格線,還有不少提升的空間。

 

轉自:https://tech.meituan.com/MT_Leaf.html

相關文章
相關標籤/搜索