業務規模不斷擴大,對穩定性、擴充套件性的要求不斷提高,推動了後臺架構技術的不斷革新。面對日益複雜的需求,分散式系統的理念也逐漸深入到後臺開發者的骨髓。2013年,藉著手遊熱潮我對分散式系統開始嘗試。在近三年的摸爬滾打中,踩過不少坑,也從業界技術發展中吸取一些經驗,逐漸形成了目前的設計思路。這裡和大家分享點心得,不敢奢談有多大參考價值,權當拋磚引玉吧。
![11ClusterCenterServer](https://i.iter01.com/images/0f8694fdbb8ab12238bcf03a243aaea52e9e67794cba7aedc9b8f53ec39ce205.png)
Client API, 服務請求者API:
- 從 Cluster Center Server 獲取服務提供者地址
- 向Server叢集內所有例項註冊,註冊成功則認為可用
- 通過負載均衡演算法,選擇一個Server例項通訊
- 檢測Server叢集內各例項的執行狀態
- 向 Cluster Center Server 上報自己的狀態、訪問地址等
- 接收 Client API 的註冊,並提供服務
- 向已經註冊成功的Client定時彙報狀態
- 接收 Server Cluster 上報,確定服務叢集的結構,以及各例項的狀態
- 接收 Client Cluster 的請求,返回可用服務叢集列表
Cluster Center Server 的實現是單點,出現故障時Client請求會異常;沒有提供監控機制,Client只能通過定時請求來獲取服務的最新狀況。
現實應用中,服務往往存在相互請求,一應一答遠遠不夠,全雙工 是必須要支援的。
Server對Client定期單邊心跳,有兩個問題:不同Client對保活要求可能不同,有些5s,有些可能1s,如果心跳發起全部在Server,無法滿足差異化要求;服務端作為被動方,承擔監控請求者存活的責任不明智。
對架構的層次、模組劃分沒有作出很好的規劃,比如通訊底層、服務發現、叢集探測與保活等等沒有清晰定義介面,導致相互耦合,替換、維護較為困難。
- 統一命名 對服務以及其中的節點,進行集中式、統一命名,便於相互區分和訪問。
- 監控 確定服務的可用性和狀態,當服務狀態變化時,關注者要有途徑獲知。
- 訪問策略 服務通常包含多個節點,以叢集形式存在,Client在每次請求時需要策略確定通訊節點,策略目標可能是多樣的,比如 負載均衡 ,穩定對映 等等。
- 可用性 容災處理,動態擴容。
![](https://i.iter01.com/images/9933535a2c16f74649e1e354e88d5e51c7c2eefc4993d16b0f80478de4b00fd2.png)
![](https://i.iter01.com/images/13de00b0919870ceb2ef9be9cf3424155c017314d0cbb83e5d510589524a5a63.png)
- 通用性 是否支援跨平臺、跨語言;業界是否廣泛流行或者支援
- 可讀性 文字流有天然優勢,純粹二進位制流如果沒有便捷視覺化工具,除錯將會異常痛苦
- 效能 空間開銷——儲存空間的佔用;時間開銷——序列化/反序列化的快慢
- 可擴充套件性 業務的不變之道就是——一直在變,必須具有處理新舊資料之間的相容性的能力
- 允許高延遲比如100ms以上,內容變更頻繁,且複雜的業務,可以考慮基於XML的SOAP協議。
- 基於Web browser的Ajax,以及Mobile app與服務端之間的通訊;對於效能要求不太高,或者以動態型別語言為主的場景,JSON可以考慮。
- 對效能和簡潔性有極高要求的場景,Protobuf,Thrift,Avro都差不多。
- 對於Terabyte級別資料持久化應用場景,Protobuf和Avro是首要選擇。持久化後的資料若儲存在Hadoop子專案裡,或以動態型別語言為主,Avro會是更好的選擇;非Hadoop專案,以靜態型別語言為主,首選Protobuf。
- 不想造 RPC 的輪子,Thrift可以考慮。
- 如果序列化之後需要支援不同的傳輸層協議,或者需要跨防火牆訪問的高效能場景,Protobuf可以優先考慮。
- 系統拆分、解耦,清晰定義系統間介面,隱藏系統內部實現
- 大框架儘可能通用,子系統可在不同場景替換
下面首先對服務定義,然後介紹整體框架和服務內部拆分。
![12ClusterCenterServer](https://i.iter01.com/images/37f78cfd29e062f05eab1662507c3584249a2ea67edb6134ae1de86913873b07.png)
- Service Cluster 服務叢集,由功能相同的例項組成,作為整體對外服務,是一個集合。比如 Lobby 提供大廳服務,Battle 提供戰鬥服務,Club 提供工會服務,Trade 提供交易服務。
- Service Instance 服務例項,提供某種服務功能的最細粒度,以程式形式存在。比如Club 叢集中有兩個例項 3.2.6.1 和 3.2.6.2 ,功能一致。
- Service Node 服務節點,是服務發現元件管理的基本單元,可以是叢集、例項、層次關係或者業務關心的含義。
- Service Key 服務節點的Key,全域性唯一的身份標記。key的設計需要能夠體現出層級關係,至少要能夠體現出 Cluster 和 Instance 的包含關係。etcd和zookeeper均支援key層次化的組織關係,類似檔案系統的樹形結構。etcd有mkdir直接建立目錄,zookeeper則通過路徑描述父子關係。但不管怎麼都可以在概念層次使用路徑結構 。
- 叢集路徑一定是其中各個例項的父路徑
- 從功能完整性而言,叢集是服務的基本粒度
- 相同功能的叢集在不同字首路徑下含義不同,服務目標也可以不同,比如:
/Example/wechat/android/w_1/g_1/Lobby 和/Example/wechat/android/w_3/g_2/Lobby 功能上均表示大廳服務,但一個為大區1分組1服務,一個為大區3分組2服務
3.2 服務發現基本流程
![13ClusterCenterServer](https://i.iter01.com/images/70deadb8bf3801a9c54fe30c309ee2bed13c357f0be3c01c3377648ba7afe97e.png)
- Create 在服務發現元件中建立 Key 對應的 Service Node,指定全域性唯一的標記。
- Delete 在服務發現元件中刪除 Key 對應的節點。
- Set 設定 Key 對應的 Value, 安全訪問策略或者節點基礎屬性等。
- Get 根據 Key 獲取對應節點的資料,如果是父節點可以獲取其子節點列表。
- Watch 對節點設定監視器,當該節點自身,以及巢狀子節點資料發生變更時,服務發現元件將變更事件主動通知給監視者。
- 生成自己的 Service Path,注意這是服務例項的路徑。
- 以 Service Path 為key,通過 Create 方法生成節點,Set 資料:對外開放的地址、安全訪問策略等。
- 生成需要訪問的服務叢集的 Service Path,通過 Get 方法獲取叢集資料,如果找不到說明該服務不存在;如果可以找到分兩種情況:
- 該路徑下沒有子節點。說明當前不存在可用的服務例項,對叢集路徑設定watcher,等待新的可用例項。
- 該路徑下有子節點。那麼 Get 所有子節點列表,並進一步 Get 子節點訪問方式和其它資料。同時設定 watcher 到叢集路徑,檢測叢集是否存在變化,比如新增或減少例項等。
- 通過 Delete 方法刪除自己對應的節點。有些服務發現元件可以在例項生命週期結束時自行刪除,比如zookeeper的臨時節點。對於etcd的目錄,或者zookeeper的父路徑,如果非空,是無法刪除的。
3.3 服務架構
![14ClusterCenterServer](https://i.iter01.com/images/7c6882ae514d0e6debca2356c18cdcdd2688e32f43445ffb21bd9a7fec90c809.png)
![](https://i.iter01.com/images/a6031183c5c6c30e6a2c491bed2bdc973f77bcf69b879c37fa3c5285ccb22cee.png)
- REQ/REP 一應一答,有請求必須等待回應
- PUB/SUB 釋出訂閱
- PUSH/PULL 流水線式處理,上游推資料,下游拉資料
- DEALER/ROUTER 全雙工非同步通訊
![15ClusterCenterServer](https://i.iter01.com/images/47cd61cd07c3af47cab4db5bb949d708344000b2cbbe10c2672919ce66271d3c.png)
- DEALER/ROUTER 是傳統非同步模式,一方connect,一方bind。前端如果要連線多個後端就得建立多個socket。在前面描述的叢集服務模式下,一個節點既會作為Client也會作為Server,會有多條入邊(被動接收連線)和出邊(主動發起連線)。這正好就是路由的概念,一個ROUTER socket可以建立多條通路,並對每條通路傳送或者接收訊息。
- PUB/SUB 注重的是擴充套件性和規模,按照ZeroMQ作者的意思當每秒鐘需要向上千的節點廣播百萬條訊息時,你應該考慮使用 PUB/SUB 。好吧,可預見的將來業務規模恐怕還到達不到這種程度,現在先把簡單放在第一位吧。
3.3.2 DMS Protocol
![16ClusterCenterServer](https://i.iter01.com/images/f0cb6a7c5e02c91016dba2a65924ecc07311a9e5a619469b07cade7c82352bea.png)
![17ClusterCenterServer](https://i.iter01.com/images/3fd10e312f6e69348b9a17d083542b5aa345a0d9186a964fe36f0a6d10d819ec.png)
![](https://i.iter01.com/images/f629bc447afae4e99dbecdd67420619e2def525f85e4ffc4be0943887f315458.png)
![18ClusterCenterServer](https://i.iter01.com/images/87b34ec6cfaa2162bd451cb90756ee3505c08487b2848c65fbb682dc939b2856.png)
![](https://i.iter01.com/images/3126bdd7a5a1c0cc94e423d61e28e3f788982b293c8eb7c15899dab1a354a746.png)
![19ClusterCenterServer](https://i.iter01.com/images/c60b8f52124e8ebba2db2c18b6c8e773eb2ce43f81db4f0bf3fa429101c53c64.png)
- 服務發現雖然可以反映節點是否存活,但一般有延遲,所以從服務發現獲取的節點僅僅是候選節點。
- 網路底層機制差異較大,有些基於連線,比如raw socket,有些沒有連線,比如shared memory。最好在高層協議中解決連線是否成功。這就好比聲納,投石問路,有回應說明可以連線,沒有回應說明目前連線不可用。
通訊流程——業務訊息傳送
![20ClusterCenterServer](https://i.iter01.com/images/9e222bc445c7c907124934366e847b926c8a876741f8a59fa83d82bff1d634cf.png)
- 普通訊息 若 PIDF 表示對端例項和當前程式直接連線,那麼傳送訊息
- 路由訊息 若 PIDF 表示對端例項和當前程式沒有直接連線,那麼可以通過直連的例項轉發。路由機制 後文會介紹
- 廣播訊息 若 PIDF InstanceID為負數,則向指定叢集內所有例項廣播
通訊流程——保活機制
如果服務者收到請求者的任何資料包,認為請求者存活,如果超出一定時間沒有收到(含PING),則認為請求者掉線。這個超時時間包含在READY協議中,由請求者告知服務者。
通訊流程——連線斷開
3.3.3 DMS Kernel
![21ClusterCenterServer](https://i.iter01.com/images/8940d8197ef76310c94c0fc80b81b71b51aedc82df0834477a85ceeaa6fdb2ee.png)
SERVICE MANAGER
- self 確定自身 服務路徑,實現服務註冊,以及與目標通訊鏈路的註冊,供路由表使用
- targets 獲取並監控目標服務的資料以及執行狀態
- ACL 訪問控制管理
- 對服務發現層介面進行封裝,不同的 SERVICE DISCOVERY 功能可能有所不同
ROUTER MANAGER
![22ClusterCenterServer](https://i.iter01.com/images/6ec02a395db435d9a50acc2f0e1f7058a546a2fd17c86cb505a8f65bcc72ec90.png)
- Updater 用於向路由表中新增邊,刪除邊,設定邊的屬性(比如權重),並對邊的變化進行監控
- Calculator 根據鄰接邊形成的 圖結構 計算路由,出發點是當前例項,給定目標點判斷目標是否可達,如果可達確定路徑並傳輸給下一個節點轉發。預設選擇 Dijkstra 演算法,業務可以定製。
CONNECTION MANAGER
- Sentinel 對前端發起的連線,通過 READY 協議,可以獲取該連線的失活標準,並通過前端主動包來判斷進入連線是否存活。如果失活,將該連線置為斷開狀態,不再向對應前端主動發包。
- Prober 對後端服務進行連線建立和連線保活。
- Dispatcher 訊息傳送時用於確定通訊對端例項。連線是基於例項的,但是業務一般都是面向服務叢集的,所以Dispathcer 需要實現一定的分配機制,將訊息轉發給 服務叢集中的某個 具體例項 。注意這裡僅只存在直接連線的單播。分配時應考慮 負載均衡 預設使用一致性雜湊演算法,業務完全可以根據具體應用場景自定義。
3.3.4 DMS Interface
![23ClusterCenterServer](https://i.iter01.com/images/f5ad39dc7de319f7051393e67a6fcdb348975385de9225a0b1e41de7de616e64.png)
3.4 應用場景
- 無Broker通訊
![24ClusterCenterServer](https://i.iter01.com/images/6daf4cda65634ec4b2e908ff498c4bc9c80e64e6d08a7900ac3641e92ef40624.png)
- Broker通訊
![25ClusterCenterServer](https://i.iter01.com/images/aed78569949e199f21d73b10beaec80e09d5bbeedb6035830cc04c2bd777d3d5.png)
這時Broker叢集可以承擔訊息中轉的作用,而且可以完成一些集中式邏輯處理。注意這裡Broker只是一個名字,通過 DMS Library 可以直接實現。
- Broker級聯通訊
![26ClusterCenterServer](https://i.iter01.com/images/76d50cfad8b56bbe54ecd260d346890679acc2aed4197b777c4d27dd61962e75.png)