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