IM消息ID技術專題(七):深度解密vivo的自研分布式ID服務(魯班) 僅登錄用戶可見
Posted on 2023-08-16 11:31 Jack Jiang 閱讀(119) 評論(0) 編輯 收藏本文由vivo互聯網技術An Peng分享,本文收錄時有內容修訂和重新排版。
1、引言
本文通過對分布式ID的3種應用場景、實現難點以及9種分布式ID的實現方式進行介紹,并對結合vivo業務場景特性下自研的魯班分布式ID服務從系統架構、ID生成規則與部分實現源碼進行分享,希望為本文的閱讀者在分布式ID的方案選型或技術自研提供參考。

- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》
- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK(備用地址點此)
(本文已同步發布于:http://www.52im.net/thread-4378-1-1.html)
2、專題目錄
本文是“IM消息ID技術專題”系列文章的第 7 篇,專題總目錄如下:
《IM消息ID技術專題(一):微信的海量IM聊天消息序列號生成實踐(算法原理篇)》
《IM消息ID技術專題(二):微信的海量IM聊天消息序列號生成實踐(容災方案篇)》
《IM消息ID技術專題(三):解密融云IM產品的聊天消息ID生成策略》
《IM消息ID技術專題(四):深度解密美團的分布式ID生成算法》
《IM消息ID技術專題(五):開源分布式ID生成器UidGenerator的技術實現》
《IM消息ID技術專題(六):深度解密滴滴的高性能ID生成器(Tinyid)》
《IM消息ID技術專題(七):深度解密vivo的自研分布式ID服務(魯班)》(* 本文)
vivo技術團隊分享的其它文章:
3、分布式ID的應用場景
3.1概述
隨著系統的業務場景復雜化、架構方案的優化演進,我們在克服問題的過程中,也總會延伸出新的技術訴求。分布式ID也是誕生于這樣的IT發展過程中。
在不同的關聯模塊內,我們需要一個全局唯一的ID來讓模塊既能并行地解耦運轉,也能輕松地進行整合處理。
以下,首先讓我們一起回顧這些典型的分布式ID場景。
3.2系統分庫分表
隨著系統的持續運作,常規的單庫單表在支撐更高規模的數量級時,無論是在性能或穩定性上都已經難以為繼,需要我們對目標邏輯數據表進行合理的物理拆分。
這些同一業務表數據的拆分,需要有一套完整的 ID生成方案來保證拆分后的各物理表中同一業務ID不相沖突,并能在后續的合并分析中可以方便快捷地計算。
以公司的營銷系統的訂單為例:當前不但以分銷與零售的目標組織區別來進行分庫存儲,來實現多租戶的數據隔離,并且會以訂單的業務屬性(訂貨單、退貨單、調拔單等等)來進一步分拆訂單數據。
具體是:
- 1)在訂單創建的時候,根據這些規則去構造全局唯一ID,創建訂單單據并保存在對應的數據庫中;
- 2)在通過訂單號查詢時,通過ID的規則,快速路由到對應的庫表中查詢;
- 3)在BI數倉的統計業務里,又需要匯總這些訂單數據進行報表分析。
3.3系統多活部署
無論是面對著全球化的各國數據合規訴求,還是針對容災高可用的架構設計,我們都會對同一套系統進行多活部署。
多活部署架構的各單元化服務,存儲的單據(如訂單/出入庫單/支付單等)均帶有部署區域屬性的ID結構去構成全局唯一ID。
創建單據并保存在對應單元的數據庫中,在前端根據單據號查詢的場景,通過ID的規則,可快速路由到對應的單元區域進行查詢。
對應多活部署架構的中心化服務,同步各單元的單據數據時,單據的ID是全局唯一,避免了匯聚數據時的ID沖突。
在公司的系統部署中,公共領域的 BPM 、待辦、營銷領域的系統都大范圍地實施多活部署。
3.4鏈路跟蹤技術
在微服務架構流行的大背景下,此類微服務的應用對比單體應用的調用鏈路會更長、更復雜,對問題的排查帶來了挑戰。
應對該場景的解決方案,是會在流量入口處產生全局唯一的TraceID,并在各微服務之間進行透傳,進行流量染色與關聯,后續通過該全局唯一的TraceID,可快速地查詢與關聯全鏈路的調用關系與狀態,快速定位根因問題。
在公司的各式各樣的監控系統、灰度管理平臺、跨進程鏈路日志中,都會伴隨著這么一個技術組件進行支撐服務。
4、分布式ID的核心難點
分布式ID的技術難點比較,這里我簡單總結了一下。
最核心的技術難點主要是:
1)唯一性: 保持生成的ID全局唯一,在任何情況下也不會出現重復的值(如防止時間回拔,時鐘周期問題);
2)高性能: ID的需求場景多,中心化生成組件后,需要高并發處理,以接近 0ms的響應大規模并發執行;
3)高可用: 作為ID的生產源頭,需要100%可用,當接入的業務系統多的時候,很難調整出各方都可接受的停機發布窗口,只能接受無損發布;
4)易接入: 作為邏輯上簡單的分布式ID要推廣使用,必須強調開箱即用,容易上手;
5)規律性: 不同業務場景生成的ID有其特征,例如有固定的前后綴,固定的位數,這些都需要配置化管理。
5、分布式ID的常見方案
常用系統設計中主要有下圖9種ID生成的方式:

以下是詳細表格說明之:

上面這些ID算法里,最知名是Twitter的雪花算法(Snowflake)。
Snowflake的核心思想就是:使用一個 64 bit 的 long 型的數字作為全局唯一 ID。
這 64 個 bit 中,其中 1 個 bit 是不用的,然后用其中的 41 bit 作為毫秒數,用 10 bit 作為工作機器 ID,12 bit 作為序列號。
SnowFlake的ID構成:

▲ 圖片引用自《深度解密美團的分布式ID生成算法 - 美團為什么不直接用Snowflake算法?》
SnowFlake的ID樣本:

▲ 圖片引用自《深度解密美團的分布式ID生成算法 - 美團為什么不直接用Snowflake算法?》
6、自研分布式ID服務(魯班)的方案
我們的系統跨越了公共、生產制造、營銷、供應鏈、財經等多個領域。
我們在分布式ID訴求下還有如下特點:
- 1)在業務場景上除了常規的Long類型ID,也需要支持“String類型”、“MixId類型”(后詳述)等多種類型的ID生成,每一種類型也需要支持不同的長度的ID;
- 2)在ID的構成規則上需要涵蓋如操作類型、區域、代理等業務屬性的標識;需要集中式的配置管理;
- 3)在一些特定的業務上,基于安全的考慮,還需要在尾部加上隨機數來保證ID不能被輕易猜測。
綜合參考了業界優秀的開源組件與常用方案均不能滿足,為了統一管理這類基礎技術組件的訴求,我們選擇基于公司業務場景自研一套分布式ID服務:魯班分布式ID服務。
7、分布式ID服務魯班的整體架構
魯班的整體架構如下圖所示:

魯班的架構說明:

8、魯班支持多種類型的ID規則
目前魯班分布式ID服務共提供"Long類型"、“String類型”、“MixId類型”等三種主要類型的ID,相關ID構成規則與說明如下。
8.1Long類型
1)構成規則:
靜態結構由以下三部分數據組成,組成部分共19位。如下所示。
* 固定部分(4位),由FixPart+ServerPart組成:
- 1)FixPart(4位):由大區zone 1位/代理 agent 1位/項目 project 1位/應用 app 1位,組成的4位數字編碼;
- 2)ServerPart(4位):用于定義產生全局ID的服務器標識位,服務節點部署時動態分配。
* 動態部分DynPart(13位): System.currentTimeMillis() - 固定配置時間的TimeMillis (可滿足使用100年)。
* 自增部分SelfIncreasePart(2位):用于在全局ID的客戶端SDK內部自增部分,由客戶端SDK控制,業務接入方無感知。共 2位組成。
2)降級機制:
主要自增部分在服務器獲取初始值后,由客戶端SDK維護,直到自增99后再次訪問服務端獲取下一輪新的ID以減少服務端交互頻率,提升性能,服務端獲取失敗后拋出異常,接入業務側需介入進行處理。
3)樣例說明:

8.2String類型
1)構成規則:
靜態結構由以下五部分數據組成,組成部分共25~27位。
* 固定部分操作位op+FixPart(9~11位):
- 1)操作位op(2~4位):2~4位由業務方傳入的業務類型標識字符;
- 2)FixPart(7位):業務接入時申請獲取,由大區zone 1位,代理 agent 2位,項目 project 2位,應用 app 2位組成。
* 服務器標識部分 ServerPart(1位): 用于定義產生全局ID的服務器標識位,服務節點部署時動態分配A~Z編碼。
* 動態部分DynPart(9位):System.currentTimeMillis() - 固定配置時間的TimeMillis ,再轉換為32進制字符串(可滿足使用100年)。
* 自增部分SelfIncreasePart(3位):用于在全局ID的客戶端SDK內部自增部分,由客戶端SDK控制,業務接入方無感知。
* 隨機部分secureRandomPart(3位):用于在全局ID的客戶端SDK的隨機部分,由SecureRandom隨機生成3位0-9,A-Z字母數字組合的安全隨機數,業務接入方無感知。
2)降級機制:
主要自增部分由客戶端SDK內部維護,一般情況下只使用001–999 共999個全局ID。也就是每向服務器請求一次,都在客戶端內可以自動維護999個唯一的全局ID。
特殊情況下在訪問服務器連接出問題的時候,可以使用帶字符的自增來做服務器降級處理,使用產生00A, 00B... 0A0, 0A1,0A2....ZZZ. 共有36 * 36 * 36 - 1000 (999純數字,000不用)= 45656個降級使用的全局ID。
3)樣例說明:

8.3MixId類型
1)構成規則:
靜態結構由以下三部分數據組成,組成部分共17位。
* 固定部分FixPart(4~6位):
- 1)操作位op(2~4位):2~4位由業務方傳入的業務類型標識字符;
- 2)FixPart(2位):業務接入時申請獲取由代理 agent 2位組成。
* 動態部分DynPart(6位): 生成ID的時間,年(2位)月(2位)日(2位)。
* 自增部分SelfIncreasePart(7位):用于在全局ID的客戶端SDK內部自增部分,由客戶端SDK控制,業務接入方無感知。
2)降級機制:
無,每次ID產生均需到服務端請求獲取,服務端獲取失敗后拋出異常,接入業務側需介入進行處理。
3)樣例說明:

9、魯班的業務自定義ID規則實現
魯班分布式ID服務內置“Long類型”,“String類型”,“MixId類型”等三種長度與規則固定的ID生成算法。除以上三種類型的ID生成算法外,業務側往往有自定義ID長度與規則的場景訴求。
在魯班分布式ID服務內置ID生成算法未能滿足業務場景時,為了能在該場景快速支持業務,魯班分布式ID服務提供了業務自定義接口并通過SPI機制在服務運行時動態加載,以實現業務自定義ID生成算法場景的支持。
相關能力的實現設計與接入流程如下。
1)ID的構成部分主要分FixPart、DynPart、SelfIncreasePart三個部分。
2)魯班分布式ID服務的客戶端SDK提供 LuBanGlobalIDClient的接口與getGlobalId(...)/setFixPart(...)/setDynPart(...)/setSelfIncreasePart(...)等四個接口方法。
3)業務側實現LuBanGlobalIDClient接口內的4個方法,通過SPI機制在業務側服務進行加載,并向外暴露出HTTP或DUBBO協議的接口。
4)用戶在魯班分布式ID服務管理后臺對自定義ID生成算法的類型名稱與服務地址信息進行配置,并關聯需要使用的AK接入信息。
5)業務側使用時調用客戶端SDK提供的LuBanGlobalIDClient的接口與getGlobalId方法,并傳入ID生成算法類型與IdRequest入參對象,魯班分布式ID服務接收請求后,動態識別與路由到對應ID生產算法的實現服務,并構建對象的ID返回給客戶端,完成整個ID生成與獲取的過程。
10、魯班保證ID生成不重復的方案
眾所周之,如何保證ID服務生成的ID不碰撞、不重復,是最基本的要求之一。
我們是這樣做的:

11、魯班的無狀態無損管理
服務部署的環境在虛擬機上,ip是固定,常規的做法是在配置表里配置ip與機器碼的綁定關系(這樣在服務擴縮容的時候就需要人為介入操作,存在一定的遺漏配置風險,也帶來了一定的運維成本)。
但在容器的部署場景,因為每次部署時IP均是動態變化的,以前通過配置表里ip與機器碼的映射關系的配置實現方式顯然不能滿足運行在容器場景的訴求,故在服務端設計了通過心跳上報實現機器碼動態分配的機制,實現服務端節點ip與機器碼動態分配、綁定的能力,達成部署自動化與無損發布的目的。
相關流程如下:

需要注意的是:
服務端節點可能因為異常,非正常地退出,對于該場景,這里就需要有一個解綁的過程,當前實現是通過公司平臺團隊的分布式定時任務服務,檢查持續5分鐘(可配置)沒有上報心跳的機器碼分配節點進行數據庫綁定信息清理的邏輯,重置相關機器碼的位置供后續注冊綁定使用。
12、魯班的使用方接入SDK的設計
SDK設計主要以"接入快捷,使用簡單"的原則進行設計。
1)接入時:
魯班分布式ID服務提供了spring-starter包,應用只需再pom文件依賴該starter,在啟動類里添加@EnableGlobalClient,并配置AK/SK等租戶參數即可完成接入。
同時魯班分布式ID服務提供Dubbo & Http的調用方式,通過在啟動注解配置accessType為HTTP/DUBBO來確定,SDK自動加載相關依賴。
2)使用時:
根據"Long"、"String"、"MixId"等三種id類型分別提供GlobalIdLongClient、GlobalIdStringClient、GlobalIdMixIDClient等三個客戶端對象,并封裝了統一的入參RequestDTO對象,業務系統使用時只需構建對應Id類型的RequestDTO對象(支持鏈式構建),并調用對應id類型的客戶端對象getGlobalID(GlobalBaseRequestDTO globalBaseRequestDTO)方法,即可完成ID的構建。
Long類型Id獲取代碼示例:
packagecom.vivo.it.demo.controller;
importcom.vivo.it.platform.luban.id.client.GlobalIdLongClient;
importcom.vivo.it.platform.luban.id.dto.GlobalLongIDRequestDTO;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.web.bind.annotation.RequestMapping;
@RequestMapping("/globalId")
publicclassGlobalIdDemoController {
@Autowired
privateGlobalIdLongClient globalIdLongClient;
@RequestMapping("/getLongId")
publicString getLongId() {
GlobalLongIDRequestDTO globalLongIDRequestDTO = GlobalLongIDRequestDTO.Builder()
.setAgent("1") //代理,接入申請時確定
.setZone("0") //大區,接入申請時確定
.setApp("8") //應用,接入申請時確定
.setProject("7") //項目,接入申請時確定
.setIdNumber(2); //當次返回的id數量,只對getGlobalIDQueue有效,對getGlobalID(...)無效
longlongId = globalIdLongClient.getGlobalID(globalLongIDRequestDTO);
returnString.valueOf(longId);
}
}
13、魯班的關鍵運行性能優化場景
13.1內存使用優化
在項目上線初時,經常發生FGC,導致服務停頓,獲取ID超時。
經過分析,魯班分布式ID服務的服務端主要為內存敏感的應用,當高并發請求時,過多對象進入老年代從而觸發FGC。
經過排查主要是JVM內存參數上線時是使用默認的,沒有經過優化配置,JVM初始化的內存較少,高并發請求時JVM頻繁觸發內存重分配,相關的對象也流程老年代導致最終頻繁發送FGC。
對于這個場景的優化思路主要是要相關內存對象在年輕代時就快速經過YGC回收,盡量少的對象進行老年代而引起FGC。
基于以上的思路主要做了以下的優化:
- 1)增大JVM初始化內存(-Xms,容器場景里為-XX:InitialRAMPercentage);
- 2)增大年輕代內存(-Xmn);
- 3)優化代碼,減少代碼里臨時對象的復制與創建。
13.2鎖顆粒度優化
客戶端SDK再自增值使用完或一定時間后會向服務端請求新的id生成,這個時候需要保證該次請求在多線程并發時是只請求一次。
當前設計是基于用戶申請ID的接入配置,組成為key,去獲取對應key的對象鎖,以減少同步代碼塊鎖的粒度,避免不同接入配置去在并發去遠程獲取新的id時,鎖粒度過大,造成線程的阻塞,從而提升在高并發場景下的性能。
14、應用現狀
當前魯班分布式ID服務日均ID生成量億級,平均RT在0~1ms內,單節點可支持 萬級QPS,已全面應用在公司IT內部營銷訂單、支付單據、庫存單據、履約單據、資產管理編碼等多個領域的業務場景。
15、未來規劃
在可用性方面,當前魯班分布式ID服務仍對Redis、Mysql等外部DB組件有一定的依賴(如應用接入配置信息、MixId類型自增部分ID計數器),規劃在該依賴極端宕機的場景下,魯班分布式ID服務仍能有一些降級策略,為業務提供可用的服務。
同時基于業務場景的訴求,支持標準形式的雪花算法等ID類型。
16、參考資料
[3] 深度解密美團的分布式ID生成算法
[4] 開源分布式ID生成器UidGenerator的技術實現
(本文已同步發布于:http://www.52im.net/thread-4378-1-1.html)
作者:Jack Jiang (點擊作者姓名進入Github)
出處:http://www.52im.net/space-uid-1.html
交流:歡迎加入即時通訊開發交流群 215891622
討論:http://www.52im.net/
Jack Jiang同時是【原創Java
Swing外觀工程BeautyEye】和【輕量級移動端即時通訊框架MobileIMSDK】的作者,可前往下載交流。
本博文
歡迎轉載,轉載請注明出處(也可前往 我的52im.net 找到我)。