數據庫生成
以MySQL舉例,利用給字段設置auto_increment_increment和auto_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這個名字是來自德國哲學家、數學家萊布尼茨的一句話: >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-segment方案可以生成趨勢遞增的ID,同時ID號是可計算的,不適用於訂單ID生成場景,比如競對在兩天中午12點分別下單,通過訂單id號相減就能大致計算出公司一天的訂單量,這個是不能忍受的。
原文:https://tech.meituan.com/2017/04/21/mt-leaf.html
