在《分佈式服務化系統一致性的「最佳實幹」》一文中提出了保證系統最終一致性的按期校對模式,在按期校對模式中最常使用的方法是在每一個系統間傳遞和保存一個統一的惟一流水號(或稱爲traceid),經過系統間兩兩覈對或者第三方統一覈對惟一流水號來保證各個系統之間步伐一致、沒有掉隊的行爲,也就是系統間狀態一致,在互聯網的世界裏,產生惟一流水號的服務系統俗稱發號器。git
Twitter的Snowflake是一個流行的開源的發號器的實現。Slowfake是由Scala語言實現的,而且文檔簡單、發佈模式單1、缺乏支持和維護,很難在現實的項目中直接使用。github
爲了能讓Java領域的小夥伴們在不一樣的環境下快速使用發號器服務,本文向你們推薦一款自主研發的多場景分佈式發號器Vesta,這是由Java語言編寫的,能夠經過Jar包的形式嵌入到任何Java開發的項目中,也能夠經過服務化或者REST服務發佈,發佈樣式靈活多樣,使用簡單、方便、高效。sql
Vesta是一款通用的惟一流水號產生器,它具備全局惟1、粗略有序、可反解和可製造等特性,它支持三種發佈模式:嵌入發佈模式、中心服務器發佈模式、REST發佈模式,根據業務的性能需求,它能夠產生最大峯值型和最小粒度型兩種類型的ID,它的實現架構使其具備高性能,高可用和可伸縮等互聯網產品須要的質量屬性,是一款通用的高性能的發號器產品。數據庫
本文聚焦在筆者原創的多場景分佈式發號器Vesta的設計、實現、性能評估等方面,同時介紹Vesta的發佈模式以及使用方式,並在最後給讀者介紹如何在你的項目中使用Vesta。apache
當前業務系統的ID使用數據庫的自增字段,自增字段徹底依賴於數據庫,這在數據庫移植、擴容、洗數據、分庫分表等操做時帶來了不少麻煩。緩存
在數據庫分庫分表時,有一種辦法是經過調整自增字段或者數據庫sequence的步長來達到跨數據庫的ID的惟一性,但仍然是一種強依賴數據庫的解決方案,有諸多的限制,而且強依賴數據庫類型,咱們並不推薦這種方法。服務器
UUID雖然可以保證ID的惟一性,可是,它沒法知足業務系統須要的不少其餘特性,例如:時間粗略有序性、可反解和可製造型。另外,UUID產生的時候使用徹底的時間數據,性能比較差,而且UUID比較長,佔用空間大,間接致使數據庫性能降低,更重要的是,UUID並不具備有序性,這致使B+樹索引在寫的時候會有過多的隨機寫操做(連續的ID會產生部分順序寫),另外寫的時候因爲不能產生順序的append操做,須要進行insert操做,這會讀取整個B+樹節點到內存,而後插入這條記錄後寫整個節點回磁盤,這種操做在記錄佔用空間比較大的狀況下,性能降低比較大,具體壓測報告請參考:Mysql性能壓測實踐報告。網絡
既然數據庫自增ID和UUID有諸多的限制,咱們須要整理一下發號器的需求。數據結構
有些業務系統可使用相對小範圍的惟一性,例如,若是用戶是惟一的,那麼同一用戶的訂單採用自增序列在用戶範圍內也是惟一的,可是若是這樣設計,訂單系統就會在邏輯上依賴用戶系統,所以,不如咱們保證ID在系統範圍內的全局惟一性更實用。多線程
分佈式系統保證全局惟一的一個悲觀策略是使用鎖或者分佈式鎖,可是,只要使用了鎖,就會大大的下降性能。
所以,咱們決定利用時間的有序性,而且在時間的某個單元下采用自增序列,達到全局的惟一性。
上面討論了UUID的最大問題就是無序的,任何業務都但願生成的ID是有序的,可是,分佈式系統中要作到徹底有序,就涉及到數據的匯聚,固然要用到鎖或者布式鎖,考慮到效率,只能採用折中的方案,粗略有序,到底有多粗略,目前有兩種主流的方案,一種是秒級有序,一種是毫秒級有序,這裏又有一個權衡和取捨,咱們決定支持兩種方式,經過配置來決定服務使用其中的一種方式。
一個 ID 生成以後,ID自己帶有不少信息量,線上排查的時候,咱們一般首先看到的是ID,若是根據ID就能知道何時產生的,從哪裏來的,這樣一個可反解的 ID 能夠幫上不少忙。
若是ID 裏有了時間並且能反解,在存儲層面就會省下不少傳統的timestamp 一類的字段所佔用的空間了,這也是一箭雙鵰的設計。
一個系統即便再高可用也不會保證永遠不出問題,出了問題怎麼辦,手工處理,數據被污染怎麼辦,洗數據,但是手工處理或者洗數據的時候,假如使用數據庫自增字段,ID已經被後來的業務覆蓋了,怎麼恢復到系統出問題的時間窗口呢?
因此,咱們使用的發號器必定要可複製,可恢復,可製造。
無論哪一個業務,訂單也好,商品也好,若是有新記錄插入,那必定是業務的核心功能,對性能的要求很是高,ID生成取決於網絡IO和CPU的性能,CPU通常不是瓶頸,根據經驗,單臺機器TPS應該達到10000/s。
首先,發號器必須是一個對等的集羣,一臺機器掛掉,請求必須可以轉發到其餘機器,另外,重試機制也是必不可少的。最後,若是遠程服務宕機,咱們須要有本地的容錯方案,本地庫的依賴方式能夠做爲高可用的最後一道屏障。
做爲一個分佈式系統,永遠都不能忽略的就是業務在不斷地增加,業務的絕對容量不是衡量一個系統的惟一標準,要知道業務是永遠增加的,因此,系統設計不但要考慮能承受的絕對容量,還必須考慮業務增加的速度,系統的水平伸縮是否能知足業務的增加速度是衡量一個系統的另外一個重要標準。
根據最終的客戶使用方式,可分爲嵌入發佈模式、中心服務器發佈模式和REST發佈模式。
嵌入發佈模式:只適用於Java客戶端,提供一個本地的Jar包,Jar包是嵌入式的原生服務,須要提早配置本地機器ID(或者服務啓動時候Zookeeper動態分配惟一的ID,在第二版中實現),可是不依賴於中心服務器。
中心服務器發佈模式:只適用於Java客戶端,提供一個服務的客戶端Jar包,Java程序像調用本地API同樣來調用,可是依賴於中心的ID產生服務器。
REST發佈模式:中心服務器經過Restful API導出服務,供非Java語言客戶端使用。
發佈模式最後會記錄在生成的ID中。也參考下面數據結構段的發佈模式相關細節。
根據時間的位數和序列號的位數,可分爲最大峯值型和最小粒度型。
1) 最大峯值型:採用秒級有序,秒級時間佔用30位,序列號佔用20位。
字段 |
版本 |
類型 |
生成方式 |
秒級時間 |
序列號 |
機器ID |
---|---|---|---|---|---|---|
位數 | 63 | 62 | 60-61 | 40-59 | 10-39 | 0-9 |
2) 最小粒度型:採用毫秒級有序,毫秒級時間佔用40位,序列號佔用10位
字段 |
版本 |
類型 |
生成方式 |
毫秒級時間 |
序列號 |
機器ID |
---|---|---|---|---|---|---|
位數 | 63 | 62 | 60-61 | 20-59 | 10-19 | 0-9 |
最大峯值型可以承受更大的峯值壓力,可是粗略有序的粒度有點大,最小粒度型有較細緻的粒度,可是每一個毫秒能承受的理論峯值有限,爲1k,同一個毫秒若是有更多的請求產生,必須等到下一個毫秒再響應。
ID類型在配置時指定,須要重啓服務才能互相切換。
1) 機器ID
10位, 2^10=1024, 也就是最多支持1000+個服務器。中心發佈模式和REST發佈模式通常不會有太多數量的機器,按照設計每臺機器TPS 1萬/s,10臺服務器就能夠有10萬/s的TPS,基本能夠知足大部分的業務需求。
可是考慮到咱們在業務服務可使用內嵌發佈方式,對機器ID的需求量變得更大,這裏最多支持1024個服務器。
2) 序列號
最大峯值型
20位,理論上每秒內平都可產生2^20= 1048576個ID,百萬級別,若是系統的網絡IO和CPU足夠強大,可承受的峯值達到每毫秒百萬級別。
最小粒度型
10位,每毫秒內序列號總計2^10=1024個, 也就是每一個毫秒最多產生1000+個ID,理論上承受的峯值徹底不如咱們最大峯值方案。
3) 秒級時間/毫秒級時間
最大峯值型
30位,表示秒級時間,2^30/60/60/24/365=34,也就是可以使用30+年。
最小粒度型
40位,表示毫秒級時間,2^40/1000/60/60/24/365=34,一樣可使用30+年。
4) 生成方式
2位,用來區分三種發佈模式:嵌入發佈模式,中心服務器發佈模式,REST發佈模式。
00:嵌入發佈模式
01:中心服務器發佈模式
02:REST發佈模式
03:保留未用
5) ID類型
1位,用來區分兩種ID類型:最大峯值型和最小粒度型。
0:最大峯值型
1:最小粒度型
6) 版本
1位,用來作擴展位或者擴容時候的臨時方案。
0:默認值,以避免轉化爲整型再轉化回字符串被截斷
1:表示擴展或者擴容中
做爲30年後擴展使用,或者在30年後ID將近用光之時,擴展爲秒級時間或者毫秒級時間來掙得系統的移植時間窗口,其實只要擴展一位,徹底能夠再使用30年。
對於中心服務器和REST發佈方式,ID生成的過程涉及到網絡IO和CPU操做,ID的生成基本都是內存到高速緩存的操做,沒有IO操做,網絡IO是系統的瓶頸。
相對於CPU計算速度來講網絡IO是瓶頸,所以,ID產生的服務使用多線程的方式,對於ID生成過程當中的競爭點time和sequence,咱們使用concurrent包的ReentrantLock進行互斥。
咱們將機器ID分爲兩個區段,一個區段服務於中心服務器發佈模式和REST發佈模式,另一個區段服務於嵌入發佈模式。
0-923:嵌入發佈模式,預先配置,(或者由Zookeeper產生,第二版中實現),最多支持924臺內嵌服務器。
924 – 1023:中心服務器發佈模式和REST發佈模式,最多支持300臺,最大支持300*1萬=300萬/s的TPS。
若是嵌入式發佈模式和中心服務器發佈模式以及REST發佈模式的使用量不符合這個比例,咱們能夠動態調整兩個區間的值來適應。
另外,各個垂直業務之間具備天生的隔離性,每一個業務均可以使用最多1024臺服務器。
對於嵌入發佈模式,服務啓動須要鏈接Zookeeper集羣,Zookeeper分配一個0-923區間的一個ID,若是0-923區間的ID被用光,Zookeeper會分配一個大於923的ID,這種狀況,拒絕啓動服務。
若是不想使用Zookeeper產生的惟一的機器ID,咱們提供缺省的預配的機器ID解決方案,每一個使用統一發號器的服務須要預先配置一個默認的機器ID。
注:此功能在第二版中實現。
使用Linux的定時任務crontab,定時經過授時服務器虛擬集羣(全球有3000多臺服務器)來覈准服務器的時間。
ntpdate -u pool.ntp.orgpool.ntp.org
時間相關的影響以及思考:
調整時間是否會影響ID產生功能?
1) 未重啓機器調慢時間,Vesta拋出異常,拒絕產生ID。重啓機器調快時間,調整後正常產生ID,調整時段內沒有ID產生。
2) 重啓機器調慢時間,Vesta將可能產生重複的時間,系統管理員須要保證不會發生這種狀況。重啓機器調快時間,調整後正常產生ID,調整時段內沒有ID產生。
每4年一次同步潤秒會不會影響ID產生功能?
1) 原子時鐘和電子時鐘每四年偏差爲1秒,也就是說電子時鐘每4年會比原子時鐘慢1秒,因此,每隔四年,網絡時鐘都會同步一次時間,可是本地機器Windows,Linux等不會自動同步時間,須要手工同步,或者使用ntpupdate向網絡時鐘同步。
2) 因爲時鐘是調快1秒,調整後不影響ID產生,調整的1s內沒有ID產生。
咱們根據不一樣的信息分段構建一個ID,使ID具備全局惟一,可反解和可製造。
咱們使用秒級別時間或者毫秒級別時間以及時間單元內部序列遞增的方法保證ID粗略有序。
對於中心服務器發佈模式和REST發佈模式,咱們使用多線程處理,爲了減小多線程間競爭,咱們對競爭點time和sequence使用ReentrantLock來進行互斥,因爲ReentrantLock內部使用CAS,這比JVM的Synchronized關鍵字性能更好,在千兆網卡的前提下,至少可達到1萬/s以上的TPS。
因爲咱們支持中心服務器發佈模式,嵌入式發佈模式和REST發佈模式,若是某種模式不可用,能夠回退到其餘發佈模式,若是Zookeeper不可用,能夠會退到使用本地預配的機器ID。從而達到服務的最大可用。
因爲ID的設計,咱們最大支持1024臺服務器,咱們將服務器機器號分爲兩個區段,一個從0開始向上,一個從128開始向下,而且可以動態調整分界線,知足了可伸縮性。
一款軟件的發佈必須保證知足性能需求,這一般須要在項目初期提出性能需求,在項目進行中作性能測試來驗證,請參考本文末尾的源碼鏈接下載源代碼,查看性能測試用例,本章節只討論性能需求和測試結果,以及改進點。
最終的性能驗證要保證每臺服務器的TPS達到1萬/s以上。
筆記本,客戶端服務器跑在同一臺機器
雙核2.4G I3 CPU, 4G內存
設置:
**併發數:**100
測試結果:
測試 | 測試1 | 測試2 | 測試3 | 測試4 | 測試5 | 平均值/最大值 |
---|---|---|---|---|---|---|
QPS | 431000 | 445000 | 442000 | 434000 | 434000 | 437200 |
平均時間(us) | 161 | 160 | 168 | 143 | 157 | 157 |
最大響應時間(ms) | 339 | 304 | 378 | 303 | 299 | 378 |
設置:
**併發數:**100
測試結果:
測試 | 測試1 | 測試2 | 測試3 | 測試4 | 測試5 | 平均值/最大值 |
---|---|---|---|---|---|---|
QPS | 1737 | 1410 | 1474 | 1372 | 1474 | 1493 |
平均時間(us) | 55 | 67 | 66 | 68 | 65 | 64 |
最大響應時間(ms) | 785 | 952 | 532 | 1129 | 1036 | 1129 |
設置:
**併發數:**100
**Boss線程數:**1
**Workder線程數:**4
測試結果:
測試 | 測試1 | 測試2 | 測試3 | 測試4 | 測試5 | 平均值/最大值 |
---|---|---|---|---|---|---|
QPS | 11001 | 10611 | 9788 | 11251 | 10301 | 10590 |
平均時間(ms) | 11 | 11 | 11 | 10 | 10 | 11 |
最大響應時間(ms) | 25 | 21 | 23 | 21 | 21 | 25 |
設置:
**併發數:**100
**Boss線程數:**1
**Workder線程數:**2
Exececutor線程數:最小25最大200
測試結果:
測試 | 測試1 | 測試2 | 測試3 | 測試4 | 測試5 | 平均值/最大值 |
---|---|---|---|---|---|---|
QPS | 4994 | 5104 | 5223 | 5108 | 5100 | 5105 |
平均時間(ms) | 20 | 19 | 19 | 19 | 19 | 19 |
最大響應時間(ms) | 75 | 61 | 61 | 61 | 67 | 75 |
參考上面總結第三條,中心服務器的性能問題須要在後期版本跟進和優化。
Vesta多場景分佈式發號器支持嵌入發佈模式、中心服務器發佈模式、REST發佈模式,每種發佈 模式的API文檔以及使用嚮導可參項目主頁的文檔鏈接。
點擊下載:
若是你經過源代碼方式安裝Vesta的發佈包到你的Maven私服,你能夠直接從你的Maven私服下載此安裝包:
解壓:
tar xzvf vesta-rest-netty-0.0.1-bin.tar.gz
屬性文件:
vesta-rest-netty-0.0.1/conf/vesta-rest-netty.properties
文件內容:
vesta.machine=1022
vesta.genMethod=2
vesta.type=0
注意:
- 機器ID爲1022, 若是你有多臺機器,遞減機器ID,同一服務中機器ID不能重複。
- genMethod爲2表示使用嵌入發佈模式
- type爲0, 表示最大峯值型,若是想要使用最小粒度型,則設置爲1
啓動文件:
vesta-rest-netty/target/vesta-rest-netty-0.0.1/bin/server.sh
文件內容:
port=10010
進入目錄:
cd vesta-rest-netty-0.0.1/bin
執行命令:
chmod 755 *
進入目錄:
cd vesta-rest-netty-0.0.1/bin
執行命令:
./start.sh
輸出:
apppath: /home/robert/vesta/vesta-rest-netty-0.0.1
Vesta Rest Netty Server is started.
命令:
結果:
1138729511026688
命令:
結果:
{「genMethod」:0,」machine」:1,」seq」:0,」time」:12235264,」type」:0,」version」:0}
JSON字符串顯示的是反解的ID的各個組成部分的數值。
命令:
結果:
Fri May 22 14:41:04 CST 2015
命令:
結果:
1138729511026688
發號器做爲分佈式服務化系統不可或缺的基礎設施之一,它在保證系統正確運行和高可用上發揮着不可替代的做用。而本文介紹了一款原創開源的多場景分佈式發號器Vesta,並介紹了Vesta的設計、實現、以及使用方式,讀者在現實項目中能夠直接使用它的任何發佈模式,既裝既用,讀者也能夠借鑑其中的設計思路和思想,開發本身的分佈式發號器,除了發號器自己,本文按照一款開源項目的生命週期構思文章結果,從設計、實現、驗證到使用嚮導,以及論述遺留的問題等,並提供了參考的開源實現,幫助讀者學習如何建立一款平臺類軟件的過程的思路,幫助讀者在技術的道路上發展愈來愈好。
在《分佈式服務化系統一致性的「最佳實幹」》一文中提到全局的惟一流水ID能夠把一個請求在分佈式系統中流轉的路徑聚合,而調用鏈中的spanid能夠把聚合的請求路徑經過樹形結構進行展現,讓技術支持人員輕鬆的發現系統出現的問題,可以快速定位出現問題的服務節點,提升應急效率,下一篇《如何設計一款分佈式服務化調用鏈追蹤》