深入理解美團(tuán) Leaf 發(fā)號(hào)器開源方案
大家好,我是樹哥。
之前我們有聊過「如何設(shè)計(jì)一個(gè)分布式 ID 發(fā)號(hào)器」,其中有講過 4 種解決方案,分別是:
- UUID
- 類雪花算法
- 數(shù)據(jù)庫自增主鍵
- Redis 原子自增
美團(tuán)以第 2、3 種解決方案為基礎(chǔ),開發(fā)出了分布式 ID 生成方案 Leaf,并將其開源。我們可以在 GitHub 上獲取到該項(xiàng)目的源碼,以及相關(guān)的文檔說明,項(xiàng)目地址:Meituan-Dianping/Leaf: Distributed ID Generate Service。
今天我們就來學(xué)習(xí)一下 Leaf 的設(shè)計(jì)思路,看看大廠是如何設(shè)計(jì)大型中間件的,這有利于進(jìn)一步提升我們自己的系統(tǒng)設(shè)計(jì)能力。
數(shù)據(jù)庫自增主鍵
在「如何設(shè)計(jì)一個(gè)分布式 ID 發(fā)號(hào)器?」文章里,我們說到可以基于數(shù)據(jù)庫自增主鍵設(shè)計(jì)發(fā)號(hào)器。但我們也提到其存在如下兩個(gè)問題:
- 只能依賴堆機(jī)器提高性能。當(dāng)請(qǐng)求再次增多時(shí),我們只能無限堆機(jī)器,這貌似是一種物理防御一樣。
- 水平擴(kuò)展困難。當(dāng)我們需要增加一臺(tái)機(jī)器時(shí),其處理過程非常麻煩。首先,我們需要先把新增的服務(wù)器部署好,設(shè)置新的步長(zhǎng),起始值要設(shè)置一個(gè)不可能達(dá)到的值。當(dāng)把新增的服務(wù)器部署好之后,再一臺(tái)臺(tái)處理舊的服務(wù)器,這個(gè)過程真的非常痛苦,可以說是人肉運(yùn)維了。
簡(jiǎn)單地說,就是基于數(shù)據(jù)庫主鍵自增的方式,其發(fā)號(hào)效率受限于單臺(tái)物理機(jī)器。在低 QPS 的情況下還可以支持,但在高 QPS 的時(shí)候就支持不了了。即使可以通過設(shè)計(jì)步長(zhǎng)的方式來堆機(jī)器,但是其運(yùn)維成本也非常高。
在這樣的業(yè)務(wù)背景下,美團(tuán)開源的 Leaf 做了進(jìn)一步優(yōu)化,在數(shù)據(jù)庫與業(yè)務(wù)服務(wù)之間加入了中間層。之前業(yè)務(wù)系統(tǒng)直接去請(qǐng)求數(shù)據(jù)庫,現(xiàn)在業(yè)務(wù)系統(tǒng)不直接請(qǐng)求數(shù)據(jù)庫,而是去請(qǐng)求 Leaf 中間層,之后由 Leaf 中間層去數(shù)據(jù)庫取數(shù)據(jù)。
Leaf 中間層每次去數(shù)據(jù)庫獲取 ID 的時(shí)候,一次性獲取一批 ID 號(hào)碼段,而不是只獲取一個(gè) ID。 整個(gè)服務(wù)的具體處理流程如下所示。
Leaf Segment 處理流程 - 圖片來自美團(tuán)技術(shù)團(tuán)隊(duì)博客
如上圖所示,綠色的業(yè)務(wù)系統(tǒng)請(qǐng)求 Leaf 發(fā)號(hào)服務(wù)的時(shí)候,帶上業(yè)務(wù)編碼標(biāo)記。隨后 Leaf 發(fā)號(hào)服務(wù)根據(jù)業(yè)務(wù)編號(hào)類型返回下一個(gè)唯一 ID。Leaf 發(fā)號(hào)服務(wù)每次向數(shù)據(jù)庫獲取 1000 個(gè) ID,數(shù)據(jù)庫往表中插入一條數(shù)據(jù),表示這 1000 個(gè) ID 分給了某個(gè)業(yè)務(wù)。
通過這種方式,原本業(yè)務(wù)系統(tǒng)獲取 1000 個(gè) ID 需要進(jìn)行 1000 次數(shù)據(jù)請(qǐng)求,現(xiàn)在可能只需要 1 次就夠了,極大地提高了運(yùn)行效率。 此外,業(yè)務(wù)系統(tǒng)獲取 ID 的時(shí)候都是從內(nèi)存獲取,不需要請(qǐng)求第三方,極大的提高了響應(yīng)速度。
上述方式雖然解決了數(shù)據(jù)庫層的壓力問題,但也存在一些問題,例如:在 ID 號(hào)碼段發(fā)完時(shí),這時(shí)候需要去進(jìn)行數(shù)據(jù)庫請(qǐng)求,這次請(qǐng)求的耗時(shí)就會(huì)突增,系統(tǒng)監(jiān)控上就會(huì)出現(xiàn)耗時(shí)尖刺。
為了解決這個(gè)問題,美團(tuán) Leaf 采用了「雙 Buffer + 預(yù)加載」的策略,即在內(nèi)存中維護(hù)兩個(gè) ID 段,并在上一個(gè) ID 段使用達(dá)到 10% 的時(shí)候去預(yù)加載。這其實(shí)有點(diǎn)像我們 App 上加載瀑布流的預(yù)加載,思路是一樣的。其設(shè)計(jì)思路如下圖所示。
雙 Buffer + 預(yù)加載 - 圖片來自美團(tuán)技術(shù)團(tuán)隊(duì)博客
通過預(yù)加載的方式,Leaf 解決了尖刺的問題,并且提供了一定程度的數(shù)據(jù)庫宕機(jī)高可用。 如果數(shù)據(jù)庫宕機(jī),Leaf 服務(wù)還可以提供一段時(shí)間的服務(wù),只要其在短時(shí)間內(nèi)可以恢復(fù)。
按照官方博客種的說法,這套方案再上線半年之后又遇到了問題 —— 發(fā)號(hào) ID 段是固定的,但流量不是固定的,如果流量增加 10 倍,就會(huì)發(fā)現(xiàn)維持的時(shí)間很短,這樣仍然有可能導(dǎo)致數(shù)據(jù)庫壓力較大。
為了解決這個(gè)問題,其采用了動(dòng)態(tài)號(hào)碼段的思路,即:根據(jù)上次號(hào)碼段的長(zhǎng)度及分發(fā)耗時(shí),計(jì)算出下次應(yīng)發(fā)的號(hào)碼段長(zhǎng)度。對(duì)于號(hào)碼長(zhǎng)度的計(jì)算規(guī)則如下:
- T < 15min,nextStep = step * 2
- 15min < T < 30min,nextStep = step
- T > 30min,nextStep = step / 2
簡(jiǎn)單地說,如果耗時(shí)低于 15 分鐘,那么下次應(yīng)發(fā)的號(hào)碼段長(zhǎng)度變?yōu)樵械?2 倍。如果在 15 - 30 分鐘之間,那么就保持應(yīng)發(fā)號(hào)碼段長(zhǎng)度不變。如果耗時(shí)大于 30 分鐘, 那么下次應(yīng)發(fā)號(hào)碼段長(zhǎng)度減半。
這種設(shè)計(jì)思路,其實(shí)有點(diǎn)像 Hotspot 虛擬機(jī)獲取鎖時(shí)的自適應(yīng)自旋,或許我們可以稱它為:自適應(yīng)長(zhǎng)度。
即使 Leaf 做了如此多的優(yōu)化,但在更惡劣的環(huán)境下,仍然可能發(fā)生系統(tǒng)不可以的情況,例如:
- 數(shù)據(jù)庫主庫突然宕機(jī),短時(shí)間內(nèi)無法恢復(fù),此時(shí)系統(tǒng)不可用。
- 業(yè)務(wù)流量突增幾十倍,即使有批量分發(fā),但數(shù)據(jù)庫單機(jī)無法支撐如此高的寫請(qǐng)求。
- 等等。
在上面這些場(chǎng)景下,系統(tǒng)仍然無法保證高可用。究其根本,這是因?yàn)?Leaf 對(duì)數(shù)據(jù)庫是強(qiáng)依賴的,因此需要從數(shù)據(jù)庫層面去做高可用保障。
按照 Leaf 官方博客的思路,Leaf 目前使用了半同步的方式同步數(shù)據(jù),并且實(shí)現(xiàn)了主從切換。 這就解決了主庫宕機(jī)的問題,進(jìn)一步提升了高可用程度。
MySQL 半同步有點(diǎn)類似于 Kafka 的副本同步,需要有一個(gè) slave 收到 binlog 之后,才能提交事務(wù),從而保證了一定程度上的一致性,降低了號(hào)碼段重發(fā)的風(fēng)險(xiǎn)。
但由于半同步方式,并無法保證數(shù)據(jù)強(qiáng)一致性,因此極端情況下還是可能會(huì)有號(hào)碼段重發(fā)的風(fēng)險(xiǎn),只是較低罷了。如果需要保證完全的強(qiáng)一致性,那么需要考慮使用 MySQL 的 Group Replication 特性。但由于美團(tuán)內(nèi)部的數(shù)據(jù)庫強(qiáng)一致性特性還在迭代中,因此 Leaf 也未實(shí)現(xiàn)數(shù)據(jù)強(qiáng)一致性。
類雪花算法
在「如何設(shè)計(jì)一個(gè)分布式 ID 發(fā)號(hào)器?」文章里,我們說到雪花算法是一個(gè)非常好的分布式 ID 生成算法。但其存在一個(gè)缺陷 —— 存在時(shí)鐘回?fù)艿膯栴}。美團(tuán)開源的 Leaf 也實(shí)現(xiàn)了基于雪花算法的分布式 ID 生成功能,并且解決了時(shí)鐘回?fù)艿膯栴}。
美團(tuán) Leaf 引入了 zookeeper 來解決時(shí)鐘回?fù)軉栴},其大致思路為:每個(gè) Leaf 運(yùn)行時(shí)定時(shí)向 zk 上報(bào)時(shí)間戳。每次 Leaf 服務(wù)啟動(dòng)時(shí),先校驗(yàn)本機(jī)時(shí)間與上次發(fā) ID 的時(shí)間,再校驗(yàn)與 zk 上所有節(jié)點(diǎn)的平均時(shí)間戳。如果任何一個(gè)階段有異常,那么就啟動(dòng)失敗報(bào)警。
這個(gè)解決方案還是比較好理解的,就是對(duì)比上次發(fā) ID 的時(shí)間,還有其他機(jī)器的平均時(shí)間。除了時(shí)間回?fù)艿膯栴},當(dāng)機(jī)器數(shù)量變多的時(shí)候,雪花算法中的 workerId 也不是很好維護(hù)。
因此,Leaf 也用 zookeeper 作為中間件,以每個(gè)服務(wù)器的 IP + Port 作為 key 去注冊(cè)一個(gè)節(jié)點(diǎn),獲取一個(gè) int 類型的節(jié)點(diǎn)作為 workerId。
總結(jié)
美團(tuán)開源的 Leaf 提供了兩種 ID 生成方式:
- 號(hào)碼段模式。基于數(shù)據(jù)庫自增組件,ID 從低位趨勢(shì)增長(zhǎng),能夠忍受 MySQL 短時(shí)間不可用。
- 類雪花算法模式。基于雪花算法,解決了時(shí)間回?fù)芤约昂A繖C(jī)器的 workId 維護(hù)問題。
對(duì)于號(hào)碼段模式而言,其在傳統(tǒng)的自增 ID 基礎(chǔ)上,增加了 Proxy 模式,提出號(hào)碼段模式。接著,又采用「雙 Buffer + 預(yù)加載」的方式解決尖刺的問題。再之,為了解決流量暴增的問題,采用了自適應(yīng)號(hào)碼段長(zhǎng)度的優(yōu)化思路。最后,在數(shù)據(jù)庫高可用上,使用 MySQL 半同步復(fù)制 + 主從切換,從一定程度上保障了高可用。
對(duì)于類雪花算法模式而言,其引入了 zookeeper 作為海量機(jī)器的 workerId 生成方法。其次,還通過「本地存儲(chǔ)時(shí)間戳 + 定時(shí)上報(bào)時(shí)間戳」的方式,解決了時(shí)間戳的問題。
好了,這就是今天分享的全部?jī)?nèi)容了。