當(dāng)線上事故來臨,這片雪花算法是無辜的??。?!
??近期引發(fā)了一場(chǎng)線上事故,盡然是因?yàn)橐粋€(gè)小小的雪花算法。說來也是歷史的原因,這里記錄下,一方面做些工作中的反思,同時(shí)大家應(yīng)注意自己項(xiàng)目中是否也存在類似的問題。
事故現(xiàn)場(chǎng)
事故發(fā)生在:2024-11-20:09:40,運(yùn)維小伙伴通過系統(tǒng)告警發(fā)現(xiàn)異常,陸續(xù)有各大業(yè)務(wù)群客戶反映系統(tǒng)異常。
圖片
緊急線上日志跟蹤,發(fā)現(xiàn)錯(cuò)誤:
圖片
異常關(guān)鍵字描述:
com.xxx.uid.exception.UidGenerateException: com.xxx.uid.exception.UidGenerateException: Timestamp bits is exhausted. Refusing UID generate. Now: 1732112168
問題排查
接到問題后,開發(fā)人員迅速到達(dá)救火現(xiàn)場(chǎng)。經(jīng)排查,原本項(xiàng)目中的唯一序列 基于雪花算法,依賴百度UidGenerator生成的自定義19位序列號(hào)。
跟蹤日志,異常發(fā)生處代碼如下:
/**
* Get current second
*/
private long getCurrentSecond() {
long currentSecond = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
if (currentSecond - epochSeconds > bitsAllocator.getMaxDeltaSeconds()) {
throw new UidGenerateException("Timestamp bits is exhausted. Refusing UID generate. Now: " + currentSecond);
}
return currentSecond;
}
顯然,異常顯示含義:UID的時(shí)間戳位超過最大限制。
追溯代碼,找到問題出處:
圖片
因?yàn)闀r(shí)間戳位設(shè)置過短導(dǎo)致的。
根因分析
UidGenerator原理
參考官網(wǎng)介紹:https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md
基于雪花算法實(shí)現(xiàn),初始設(shè)置
圖片
- sign(1bit) 固定1bit符號(hào)標(biāo)識(shí),即生成的UID為正數(shù)。
- delta seconds (28 bits) 當(dāng)前時(shí)間,相對(duì)于時(shí)間基點(diǎn)"2016-05-20"的增量值,單位:秒,最多可支持約8.7年
- worker id (22 bits) 機(jī)器id,最多可支持約420w次機(jī)器啟動(dòng)。內(nèi)置實(shí)現(xiàn)為在啟動(dòng)時(shí)由數(shù)據(jù)庫分配,默認(rèn)分配策略為用后即棄,后續(xù)可提供復(fù)用策略。
- sequence (13 bits) 每秒下的并發(fā)序列,13 bits可支持每秒8192個(gè)并發(fā)。
按照項(xiàng)目中代碼,時(shí)間戳(delta seconds)部分使用了28位存儲(chǔ)自2016年5月20日以來的秒數(shù)。我們可以計(jì)算最大支持的時(shí)間:
圖片
所以,按照提供的時(shí)間基點(diǎn)和算法設(shè)計(jì),雪花算法能夠支持到2024年11月20日左右。這個(gè)日期是理論上的最大值,也是和事故發(fā)生的時(shí)間恰好對(duì)上。
如果需要延長使用時(shí)間,可以考慮增加時(shí)間戳的位數(shù),例如增加到31位,這樣可以支持更長的時(shí)間范圍。
合理性分析
新的位分配方案合理性分析:
- 時(shí)間戳(timeBits = 31):
優(yōu)點(diǎn): 理論上可以支持到 (2^{31} - 1 = 2,147,483,647) 秒,大約為68.5年。這使得系統(tǒng)能夠覆蓋更長的時(shí)間跨度,從2016年5月20日開始,可以支持到大約2084年,方案可行。
- 工作節(jié)點(diǎn)ID(workerBits = 15):
優(yōu)點(diǎn): 15位可以支持 (2^{15} = 32,768) 個(gè)不同的工作節(jié)點(diǎn),這對(duì)于大多數(shù)分布式系統(tǒng)來說是足夠的,方案可行。
缺點(diǎn): 如果系統(tǒng)預(yù)期會(huì)有超過32,768個(gè)節(jié)點(diǎn),或者節(jié)點(diǎn)的生命周期非常短,可能需要考慮更多的位數(shù)。
序列號(hào)(seqBits = 17):
優(yōu)點(diǎn): 17位可以支持每秒大約 (2^{17} - 1 = 131,071) 個(gè)并發(fā)ID生成。這對(duì)于高并發(fā)系統(tǒng)來說是合理的,尤其是在需要在同一秒內(nèi)生成大量ID的場(chǎng)景中。
注意事項(xiàng):
需要確保系統(tǒng)在處理時(shí)間戳、工作節(jié)點(diǎn)ID和序列號(hào)時(shí)能夠正確地進(jìn)行位運(yùn)算。
- 擴(kuò)展性:
合理性: 這種分配方案為未來可能的擴(kuò)展提供了一定的靈活性,尤其是在時(shí)間戳和序列號(hào)方面。
注意事項(xiàng): 如果系統(tǒng)預(yù)期會(huì)有非常長的運(yùn)行時(shí)間或者非常高的并發(fā)需求,可能需要考慮進(jìn)一步增加時(shí)間戳或序列號(hào)的位數(shù)。
- 是否ID沖突:
時(shí)間戳: 31位時(shí)間戳提供了足夠的時(shí)間分辨率,以確保在大多數(shù)情況下,即使是在同一工作節(jié)點(diǎn)上,連續(xù)生成的ID也會(huì)因?yàn)闀r(shí)間戳的增加而不同。
工作節(jié)點(diǎn)ID: 15位工作節(jié)點(diǎn)ID允許系統(tǒng)區(qū)分不同的工作節(jié)點(diǎn),這有助于在分布式環(huán)境中避免ID沖突。
序列號(hào): 17位序列號(hào)在同一秒內(nèi)提供了高達(dá)131,071個(gè)不同的序列號(hào),這在高并發(fā)環(huán)境下可以減少同一節(jié)點(diǎn)同一時(shí)間生成相同ID的可能性。
時(shí)鐘同步: 所有節(jié)點(diǎn)需要保持時(shí)間同步,本次不涉及。
總之,按系統(tǒng)業(yè)務(wù)量和并發(fā)量,新的位分配方案是合理的。
實(shí)施措施
- 調(diào)整時(shí)間位數(shù),重新發(fā)布程序
/** Bits allocate */
protected int timeBits = 31; //28->31
protected int workerBits = 15;//22->15
protected int seqBits = 17;//13->17
即重新定義了位數(shù),對(duì)應(yīng)的新的位數(shù)如下:
圖片
- 分批進(jìn)行,核心公共程序優(yōu)先發(fā)布
- 整理服務(wù)發(fā)布列表,標(biāo)記受影響的服務(wù),是否已發(fā)布
- 驗(yàn)證版本,保證生產(chǎn)版本準(zhǔn)確性
- 驗(yàn)證基本流程,觀察異常情況,問題是否得到有效解決
- 故障匯報(bào)
復(fù)盤總結(jié)
- 當(dāng)線上事故來臨,沒有一片雪花算法是無辜的??。?!
- 開發(fā)人員應(yīng)掌握框架核心原理,能快速定位問題。
- 應(yīng)急措施前進(jìn)行合理性分析,避免引入新問題或者遺留問題
- 排雷全局引用問題,包括:程序、數(shù)據(jù)庫、業(yè)務(wù)方面等
- 歷史問題如何發(fā)現(xiàn)?
- 血的教訓(xùn),大家項(xiàng)目中類似問題及時(shí)排查
- 歡迎各位留言提供精妙的解決方案?。?!
參考資料
- 官網(wǎng)UidGenerator原理:https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md
- 百度UidGenerator源碼分析:https://juejin.cn/post/7026991586680668168
- 8種分布式ID生成方案匯總:https://mp.weixin.qq.com/s/3nG4-bIPBdiJk0ShE98APQ