1. 程式人生 > >如何設計一款多場景分散式發號器(Vesta)

如何設計一款多場景分散式發號器(Vesta)

作者:李豔鵬,支付平臺架構師,專注線上和線下支付平臺的應用架構和技術架構的規劃與落地,負責交易、支付、渠道、賬務、計費、風控、對賬等系統的設計與實現,在移動支付、聚合支付、合規賬戶、掃碼支付、標記化支付等業務場景上有產品應用架構規劃的經驗。微信:robert_lyp; 簡書部落格:http://www.jianshu.com/u/581f548ef0ec; 開源專案:https://github.com/robertleepeak; 個人主頁:http://www.cloudate.net

責編:陳秋歌,尋求報道或者投稿請發郵件至chenqg#csdn.net。
瞭解更多前沿技術資訊,獲取深度技術文章推薦,請關注

CSDN研發頻道微博

《分散式服務化系統一致性的“最佳實幹”》一文中提出了保證系統最終一致性的定期校對模式,在定期校對模式中最常使用的方法是在每個系統間傳遞和儲存一個統一的唯一流水號(或稱為traceid),通過系統間兩兩核對或者第三方統一核對唯一流水號來保證各個系統之間步伐一致、沒有掉隊的行為,也就是系統間狀態一致,在網際網路的世界裡,產生唯一流水號的服務系統俗稱發號器。

Twitter的Snowflake是一個流行的開源的發號器的實現。Slowfake是由Scala語言實現的,並且文件簡單、釋出模式單一、缺少支援和維護,很難在現實的專案中直接使用。

為了能讓Java領域的小夥伴們在不同的環境下快速使用發號器服務,本文向大家推薦一款自主研發的多場景分散式發號器

Vesta,這是由Java語言編寫的,可以通過Jar包的形式嵌入到任何Java開發的專案中,也可以通過服務化或者REST服務釋出,釋出樣式靈活多樣,使用簡單、方便、高效。

Vesta是一款通用的唯一流水號產生器,它具有全域性唯一、粗略有序、可反解和可製造等特性,它支援三種釋出模式:嵌入釋出模式、中心伺服器釋出模式、REST釋出模式,根據業務的效能需求,它可以產生最大峰值型和最小粒度型兩種型別的ID,它的實現架構使其具有高效能,高可用和可伸縮等網際網路產品需要的質量屬性,是一款通用的高效能的發號器產品。

本文聚焦在筆者原創的多場景分散式發號器Vesta的設計、實現、效能評估等方面,同時介紹Vesta的釋出模式以及使用方式,並在最後給讀者介紹如何在你的專案中使用Vesta。

1. 如何思考和設計

1.1 當前遇到的問題

當前業務系統的ID使用資料庫的自增欄位,自增欄位完全依賴於資料庫,這在資料庫移植、擴容、洗資料、分庫分表等操作時帶來了很多麻煩。

在資料庫分庫分表時,有一種辦法是通過調整自增欄位或者資料庫sequence的步長來達到跨資料庫的ID的唯一性,但仍然是一種強依賴資料庫的解決方案,有諸多的限制,並且強依賴資料庫型別,我們並不推薦這種方法。

1.2 為什麼不用UUID

UUID雖然能夠保證ID的唯一性,但是,它無法滿足業務系統需要的很多其他特性,例如:時間粗略有序性、可反解和可製造型。另外,UUID產生的時候使用完全的時間資料,效能比較差,並且UUID比較長,佔用空間大,間接導致資料庫效能下降,更重要的是,UUID並不具有有序性,這導致B+樹索引在寫的時候會有過多的隨機寫操作(連續的ID會產生部分順序寫),另外寫的時候由於不能產生順序的append操作,需要進行insert操作,這會讀取整個B+樹節點到記憶體,然後插入這條記錄後寫整個節點回磁碟,這種操作在記錄佔用空間比較大的情況下,效能下降比較大,具體壓測報告請參考:Mysql效能壓測實踐報告

1.3 需求分析和整理

既然資料庫自增ID和UUID有諸多的限制,我們需要整理一下發號器的需求。

1. 全域性唯一

有些業務系統可以使用相對小範圍的唯一性,例如,如果使用者是唯一的,那麼同一使用者的訂單採用自增序列在使用者範圍內也是唯一的,但是如果這樣設計,訂單系統就會在邏輯上依賴使用者系統,因此,不如我們保證ID在系統範圍內的全域性唯一性更實用。

分散式系統保證全域性唯一的一個悲觀策略是使用鎖或者分散式鎖,但是,只要使用了鎖,就會大大的降低效能。

因此,我們決定利用時間的有序性,並且在時間的某個單元下采用自增序列,達到全域性的唯一性。

2. 粗略有序

上面討論了UUID的最大問題就是無序的,任何業務都希望生成的ID是有序的,但是,分散式系統中要做到完全有序,就涉及到資料的匯聚,當然要用到鎖或者布式鎖,考慮到效率,只能採用折中的方案,粗略有序,到底有多粗略,目前有兩種主流的方案,一種是秒級有序,一種是毫秒級有序,這裡又有一個權衡和取捨,我們決定支援兩種方式,通過配置來決定服務使用其中的一種方式。

3. 可反解

一個 ID 生成之後,ID本身帶有很多資訊量,線上排查的時候,我們通常首先看到的是ID,如果根據ID就能知道什麼時候產生的,從哪裡來的,這樣一個可反解的 ID 可以幫上很多忙。

如果ID 裡有了時間而且能反解,在儲存層面就會省下很多傳統的timestamp 一類的欄位所佔用的空間了,這也是一舉兩得的設計。

4. 可製造

一個系統即使再高可用也不會保證永遠不出問題,出了問題怎麼辦,手工處理,資料被汙染怎麼辦,洗資料,可是手工處理或者洗資料的時候,假如使用資料庫自增欄位,ID已經被後來的業務覆蓋了,怎麼恢復到系統出問題的時間視窗呢?

所以,我們使用的發號器一定要可複製,可恢復,可製造。

5. 高效能

不管哪個業務,訂單也好,商品也好,如果有新記錄插入,那一定是業務的核心功能,對效能的要求非常高,ID生成取決於網路IO和CPU的效能,CPU一般不是瓶頸,根據經驗,單臺機器TPS應該達到10000/s。

6. 高可用

首先,發號器必須是一個對等的叢集,一臺機器掛掉,請求必須能夠轉發到其他機器,另外,重試機制也是必不可少的。最後,如果遠端服務宕機,我們需要有本地的容錯方案,本地庫的依賴方式可以作為高可用的最後一道屏障。

7. 可伸縮

作為一個分散式系統,永遠都不能忽略的就是業務在不斷地增長,業務的絕對容量不是衡量一個系統的唯一標準,要知道業務是永遠增長的,所以,系統設計不但要考慮能承受的絕對容量,還必須考慮業務增長的速度,系統的水平伸縮是否能滿足業務的增長速度是衡量一個系統的另一個重要標準。

1.4 設計與實現

1. 釋出模式

根據最終的客戶使用方式,可分為嵌入釋出模式、中心伺服器釋出模式和REST釋出模式。

  1. 嵌入釋出模式:只適用於Java客戶端,提供一個本地的Jar包,Jar包是嵌入式的原生服務,需要提前配置本地機器ID(或者服務啟動時候Zookeeper動態分配唯一的ID,在第二版中實現),但是不依賴於中心伺服器。

  2. 中心伺服器釋出模式:只適用於Java客戶端,提供一個服務的客戶端Jar包,Java程式像呼叫本地API一樣來呼叫,但是依賴於中心的ID產生伺服器。

  3. REST釋出模式:中心伺服器通過Restful API匯出服務,供非Java語言客戶端使用。

釋出模式最後會記錄在生成的ID中。也參考下面資料結構段的釋出模式相關細節。

2. 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型別在配置時指定,需要重啟服務才能互相切換。

3. 資料結構

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年。

4. 併發

對於中心伺服器和REST釋出方式,ID生成的過程涉及到網路IO和CPU操作,ID的生成基本都是記憶體到快取記憶體的操作,沒有IO操作,網路IO是系統的瓶頸。

相對於CPU計算速度來說網路IO是瓶頸,因此,ID產生的服務使用多執行緒的方式,對於ID生成過程中的競爭點time和sequence,我們使用concurrent包的ReentrantLock進行互斥。

5. 機器ID的分配

我們將機器ID分為兩個區段,一個區段服務於中心伺服器釋出模式和REST釋出模式,另外一個區段服務於嵌入釋出模式。

0-923:嵌入釋出模式,預先配置,(或者由Zookeeper產生,第二版中實現),最多支援924臺內嵌伺服器。
924 – 1023:中心伺服器釋出模式和REST釋出模式,最多支援300臺,最大支援300*1萬=300萬/s的TPS。

如果嵌入式釋出模式和中心伺服器釋出模式以及REST釋出模式的使用量不符合這個比例,我們可以動態調整兩個區間的值來適應。

另外,各個垂直業務之間具有天生的隔離性,每個業務都可以使用最多1024臺伺服器。

6. 與Zookeeper整合

對於嵌入釋出模式,服務啟動需要連線Zookeeper叢集,Zookeeper分配一個0-923區間的一個ID,如果0-923區間的ID被用光,Zookeeper會分配一個大於923的ID,這種情況,拒絕啟動服務。

如果不想使用Zookeeper產生的唯一的機器ID,我們提供預設的預配的機器ID解決方案,每個使用統一發號器的服務需要預先配置一個預設的機器ID。

注:此功能在第二版中實現。

7. 時間同步

使用Linux的定時任務crontab,定時通過授時伺服器虛擬叢集(全球有3000多臺伺服器)來核准伺服器的時間。

ntpdate -u pool.ntp.orgpool.ntp.org

時間相關的影響以及思考:

  1. 調整時間是否會影響ID產生功能?

    1) 未重啟機器調慢時間,Vesta丟擲異常,拒絕產生ID。重啟機器調快時間,調整後正常產生ID,調整時段內沒有ID產生。

    2) 重啟機器調慢時間,Vesta將可能產生重複的時間,系統管理員需要保證不會發生這種情況。重啟機器調快時間,調整後正常產生ID,調整時段內沒有ID產生。

  2. 每4年一次同步潤秒會不會影響ID產生功能?

    1) 原子時鐘和電子時鐘每四年誤差為1秒,也就是說電子時鐘每4年會比原子時鐘慢1秒,所以,每隔四年,網路時鐘都會同步一次時間,但是本地機器Windows,Linux等不會自動同步時間,需要手工同步,或者使用ntpupdate向網路時鐘同步。

    2) 由於時鐘是調快1秒,調整後不影響ID產生,調整的1s內沒有ID產生。

8. 設計驗證

  1. 我們根據不同的資訊分段構建一個ID,使ID具有全域性唯一,可反解和可製造。

  2. 我們使用秒級別時間或者毫秒級別時間以及時間單元內部序列遞增的方法保證ID粗略有序。

  3. 對於中心伺服器釋出模式和REST釋出模式,我們使用多執行緒處理,為了減少多執行緒間競爭,我們對競爭點time和sequence使用ReentrantLock來進行互斥,由於ReentrantLock內部使用CAS,這比JVM的Synchronized關鍵字效能更好,在千兆網絡卡的前提下,至少可達到1萬/s以上的TPS。

  4. 由於我們支援中心伺服器釋出模式,嵌入式釋出模式和REST釋出模式,如果某種模式不可用,可以回退到其他釋出模式,如果Zookeeper不可用,可以會退到使用本地預配的機器ID。從而達到服務的最大可用。

  5. 由於ID的設計,我們最大支援1024臺伺服器,我們將伺服器機器號分為兩個區段,一個從0開始向上,一個從128開始向下,並且能夠動態調整分界線,滿足了可伸縮性。

2. 如何保證效能需求

一款軟體的釋出必須保證滿足效能需求,這通常需要在專案初期提出效能需求,在專案進行中做效能測試來驗證,請參考本文末尾的原始碼連線下載原始碼,檢視效能測試用例,本章節只討論效能需求和測試結果,以及改進點。

2.1 效能需求

最終的效能驗證要保證每臺伺服器的TPS達到1萬/s以上。

2.2 測試環境

筆記本,客戶端伺服器跑在同一臺機器
雙核2.4G I3 CPU, 4G記憶體

2.3 嵌入釋出模式壓測結果

設定:

**併發數:**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

2.4 中心伺服器釋出模式壓測結果

設定:

**併發數:**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

2.5 REST釋出模式(Netty實現)壓測結果

設定:

**併發數:**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

2.6 REST釋出模式(Spring Boot + Tomcat)壓測結果

設定:

**併發數:**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

2.7 效能測試總結

  1. 根據測試,Netty服務可到達11000的QPS,而Tomcat只能答道5000左右的QPS。
  2. 嵌入釋出模式,也就是JVM內部呼叫最快,沒秒可答道40萬以上。可見線上服務的瓶頸在網路IO以及網路IO的處理上。
  3. 使用Dubbo匯入匯出的中心伺服器釋出模式的QPS只有不到2000, 這比Tomcat提供的HTTP服務的QPS還要小,這個不符合常理,一方面需要檢視是否Dubbo RPC需要優化,包括執行緒池策略,序列化協議,通訊協議等,另外一方面REST使用apache ab測試,嵌入式釋出模式使用自己寫的客戶端測試,是否測試工具存在一定的差異。
  4. 測試過程中發現loopback虛擬網絡卡達到30+M的流量,沒有到達千兆網絡卡的極限,雙核心CPU佔用率已經接近200%,也就是CPU已經到達瓶頸。

參考上面總結第三條,中心伺服器的效能問題需要在後期版本跟進和優化。

3. 如何快速使用

Vesta多場景分散式發號器支援嵌入釋出模式、中心伺服器釋出模式、REST釋出模式,每種釋出 模式的API文件以及使用嚮導可參專案主頁的文件連線

3.1 安裝與啟動

1. 下載最新版本的REST釋出模式的釋出包

2. 解壓釋出包到任意目錄

  • 解壓:

    tar xzvf vesta-rest-netty-0.0.1-bin.tar.gz

3. 解壓後更改屬性檔案

  • 屬性檔案:

    vesta-rest-netty-0.0.1/conf/vesta-rest-netty.properties

  • 檔案內容:

    vesta.machine=1022
    vesta.genMethod=2
    vesta.type=0

    注意:

    1. 機器ID為1022, 如果你有多臺機器,遞減機器ID,同一服務中機器ID不能重複。
    2. genMethod為2表示使用嵌入釋出模式
    3. type為0, 表示最大峰值型,如果想要使用最小粒度型,則設定為1

4. REST釋出模式的預設埠為8088,你可以通過更改啟動檔案來更改埠號,這裡以10010為例

  • 啟動檔案:

    vesta-rest-netty/target/vesta-rest-netty-0.0.1/bin/server.sh

  • 檔案內容:

    port=10010

5. 修改啟動指令碼,並且賦予執行許可權

  • 進入目錄:

    cd vesta-rest-netty-0.0.1/bin

  • 執行命令:

    chmod 755 *

6. 啟動服務

  • 進入目錄:

    cd vesta-rest-netty-0.0.1/bin

  • 執行命令:

    ./start.sh

7. 如果看到如下訊息,服務啟動成功

  • 輸出:

    apppath: /home/robert/vesta/vesta-rest-netty-0.0.1
    Vesta Rest Netty Server is started.

3.2 測試Rest服務

1. 通過URL訪問產生一個ID

2. 把產生的ID進行反解

  • 結果:

    {“genMethod”:0,”machine”:1,”seq”:0,”time”:12235264,”type”:0,”version”:0}

    JSON字串顯示的是反解的ID的各個組成部分的數值。

3. 對產生的日期進行反解

4. 使用反解的資料偽造ID

4. 總結思考

發號器作為分散式服務化系統不可或缺的基礎設施之一,它在保證系統正確執行和高可用上發揮著不可替代的作用。而本文介紹了一款原創開源的多場景分散式發號器Vesta,並介紹了Vesta的設計、實現、以及使用方式,讀者在現實專案中可以直接使用它的任何釋出模式,既裝既用,讀者也可以借鑑其中的設計思路和思想,開發自己的分散式發號器,除了發號器本身,本文按照一款開源專案的生命週期構思文章結果,從設計、實現、驗證到使用嚮導,以及論述遺留的問題等,並提供了參考的開源實現,幫助讀者學習如何建立一款平臺類軟體的過程的思路,幫助讀者在技術的道路上發展越來越好。

《分散式服務化系統一致性的“最佳實幹”》一文中提到全域性的唯一流水ID可以把一個請求在分散式系統中流轉的路徑聚合,而呼叫鏈中的spanid可以把聚合的請求路徑通過樹形結構進行展示,讓技術支援人員輕鬆的發現系統出現的問題,能夠快速定位出現問題的服務節點,提高應急效率,下一篇《如何設計一款分散式服務化呼叫鏈追蹤》

5 瞭解更多