Leaf

Duancf發表於2024-07-28

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,大致架構如下圖所示:

image

test_tag在第一臺Leaf機器上是11000的號段,當這個號段用完時,會去載入另一個長度為step=1000的號段,假設另外兩臺號段都沒有更新,這個時候第一臺機器新載入的號段就應該是30014000。同時資料庫對應的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方案。

image

Leaf-snowflake方案完全沿用snowflake方案的bit位設計,即是“1+41+10+12”的方式組裝ID號。對於workerID的分配,當服務叢集數量較小的情況下,完全可以手動配置。Leaf服務規模較大,動手配置成本太高。所以使用Zookeeper持久順序節點的特性自動對snowflake節點配置wokerID。Leaf-snowflake是按照下面幾個步驟啟動的:

image

  • 啟動Leaf-snowflake服務,連線Zookeeper,在leaf_forever父節點下檢查自己是否已經註冊過(是否有該順序子節點)。
  • 如果有註冊過直接取回自己的workerID(zk順序節點生成的int型別ID號),啟動服務。
  • 如果沒有註冊過,就在該父節點下面建立一個持久順序節點,建立成功後取回順序號當做自己的workerID號,啟動服務。

弱依賴ZooKeeper

除了每次會去ZK拿資料以外,也會在本機檔案系統上快取一個workerID檔案。當ZooKeeper出現問題,恰好機器出現問題需要重啟時,能保證服務能夠正常啟動。這樣做到了對三方元件的弱依賴。一定程度上提高了SLA。

解決時鐘問題

因為這種方案依賴時間,如果機器的時鐘發生了回撥,那麼就會有可能生成重複的ID號,需要解決時鐘回退的問題。

image

參見上圖整個啟動流程圖,

  • 服務啟動時首先檢查自己是否寫過ZooKeeper leaf_forever節點:

  • 若寫過,則用自身系統時間與leaf_forever/${self}節點記錄時間做比較,若小於leaf_forever/${self}時間則認為機器時間發生了大步長回撥,服務啟動失敗並報警。

  • 若未寫過,證明是新服務節點,直接建立持久節點leaf_forever/${self}並寫入自身系統時間,接下來綜合對比其餘Leaf節點的系統時間來判斷自身系統時間是否準確,具體做法是取leaf_temporary下的所有臨時節點(所有執行中的Leaf-snowflake節點)的服務IP:Port,然後透過RPC請求得到所有節點的系統時間,計算sum(time)/nodeSize。

  • 若abs( 系統時間-sum(time)/nodeSize ) < 閾值,認為當前系統時間準確,正常啟動服務,同時寫臨時節點leaf_temporary/${self} 維持租約。

  • 否則認為本機系統時間發生大步長偏移,啟動失敗並報警。

  • 每隔一段時間(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能壓測到近5萬/s,TP999 1ms,已經能夠滿足大部分的業務的需求。每天提供億數量級的呼叫量,作為公司內部公共的基礎技術設施,必須保證高SLA和高效能的服務,我們目前還僅僅達到了及格線,還有很多提高的空間。