分散式系統設計的求生之路

發表於2017-03-07
WeTest導讀
分散式系統理念漸漸成為了後臺架構技術的重要選擇,本文介紹了作者在手遊領域對分散式系統進行的種種嘗試,並在嘗試中制定了對服務的定義、整體框架的構建以及服務內部拆分的流程。
前言

業務規模不斷擴大,對穩定性、擴充套件性的要求不斷提高,推動了後臺架構技術的不斷革新。面對日益複雜的需求,分散式系統的理念也逐漸深入到後臺開發者的骨髓。2013年,藉著手遊熱潮我對分散式系統開始嘗試。在近三年的摸爬滾打中,踩過不少坑,也從業界技術發展中吸取一些經驗,逐漸形成了目前的設計思路。這裡和大家分享點心得,不敢奢談有多大參考價值,權當拋磚引玉吧。

1. 失敗的首次嘗試
最初考慮使用分散式的出發點很簡單:解決端遊開發時單點結構導致容災、擴容困難的問題。一種樸素的想法就是將相同功能的程式作為一個整體對外提供服務。這裡簡要描述下基本框架:
11ClusterCenterServer
這種架構提供了三個基本元件:

Client API, 服務請求者API:

  • 從 Cluster Center Server 獲取服務提供者地址
  • 向Server叢集內所有例項註冊,註冊成功則認為可用
  • 通過負載均衡演算法,選擇一個Server例項通訊
  • 檢測Server叢集內各例項的執行狀態
Server API, 服務提供者API:
  • 向 Cluster Center Server 上報自己的狀態、訪問地址等
  • 接收 Client API 的註冊,並提供服務
  • 向已經註冊成功的Client定時彙報狀態
Cluster Center Server, 叢集中心程式:
  • 接收 Server Cluster 上報,確定服務叢集的結構,以及各例項的狀態
  • 接收 Client Cluster 的請求,返回可用服務叢集列表
這種架構具備了叢集的基本雛形,可以滿足容災擴容的基本需求,大家應該也發現不少問題,我這裡總結幾點:
1. 服務發現的蹩腳實現
Cluster Center Server 的實現是單點,出現故障時Client請求會異常;沒有提供監控機制,Client只能通過定時請求來獲取服務的最新狀況。
2. CS採用Request/Response的通訊方式不靈活
現實應用中,服務往往存在相互請求,一應一答遠遠不夠,全雙工 是必須要支援的。
3. 有瑕疵的保活機制
Server對Client定期單邊心跳,有兩個問題:不同Client對保活要求可能不同,有些5s,有些可能1s,如果心跳發起全部在Server,無法滿足差異化要求;服務端作為被動方,承擔監控請求者存活的責任不明智。
4. 架構設計的層次不清晰
對架構的層次、模組劃分沒有作出很好的規劃,比如通訊底層、服務發現、叢集探測與保活等等沒有清晰定義介面,導致相互耦合,替換、維護較為困難。
2. 看看外面的世界
上述問題,歸根結底還是眼界狹窄,自己悶頭造輪子沒跟上業界技術發展的步伐。近幾年微服務架構發展迅速,相比傳統面向服務架構不再過分強調企業服務匯流排,而是深入到單個業務系統內部的元件化。這裡我介紹下自己的調研結果。
2.1 服務協同
服務協同是分散式系統一個核心組成部分,概述為:多個程式節點作為整體對外提供服務,服務可以相互發現,服務關注者可以及時獲取被關注者的變化以完成協作。具體執行過程包括:服務註冊 和 服務發現。在實現上涉及以下方面:
  • 統一命名 對服務以及其中的節點,進行集中式、統一命名,便於相互區分和訪問。
  • 監控 確定服務的可用性和狀態,當服務狀態變化時,關注者要有途徑獲知。
  • 訪問策略 服務通常包含多個節點,以叢集形式存在,Client在每次請求時需要策略確定通訊節點,策略目標可能是多樣的,比如 負載均衡 ,穩定對映 等等。
  • 可用性 容災處理,動態擴容。
業界中較為成熟的實現如下表所示:
2.2 訊息中介軟體
亦稱訊息佇列,在分散式系統廣泛使用,在需要進行網路通訊的節點間建立通道,高效可靠地進行平臺無關的資料交流。架構上主要分為兩種:Broker-Based(代理),和 Brokerless(無代理)。前者需要部署一個訊息轉發的中間層,提供二次處理和可靠性保證。後者輕量級,直接在內嵌在通訊節點上。業界較為成熟的實現如下表所示:
2.3 通訊協議資料格式
服務間通訊,需要將資料結構/物件和傳輸過程中的二進位制流做相互轉化,一般稱為 序列化/反序列化 。不同程式語言或應用場景,對資料結構/物件的定義和實現是不同的。在選擇時需要考慮以下方面:
  • 通用性 是否支援跨平臺、跨語言;業界是否廣泛流行或者支援
  • 可讀性 文字流有天然優勢,純粹二進位制流如果沒有便捷視覺化工具,除錯將會異常痛苦
  • 效能 空間開銷——儲存空間的佔用;時間開銷——序列化/反序列化的快慢
  • 可擴充套件性 業務的不變之道就是——一直在變,必須具有處理新舊資料之間的相容性的能力
實現 序列化/反序列化 的元件一般包含:IDL(Interface Description Language), IDL Compiler, Stub/Skeleton。業界目前比較流行的序列化協議有:XML, JSON, ProtoBuf, Thrift, Avro等。關於這幾種協議的實現以及比較,可以參考文章 《序列化和反序列化》。這裡將原文中的選型結論摘錄給大家:
  • 允許高延遲比如100ms以上,內容變更頻繁,且複雜的業務,可以考慮基於XML的SOAP協議。
  • 基於Web browser的Ajax,以及Mobile app與服務端之間的通訊;對於效能要求不太高,或者以動態型別語言為主的場景,JSON可以考慮。
  • 對效能和簡潔性有極高要求的場景,Protobuf,Thrift,Avro都差不多。
  • 對於Terabyte級別資料持久化應用場景,Protobuf和Avro是首要選擇。持久化後的資料若儲存在Hadoop子專案裡,或以動態型別語言為主,Avro會是更好的選擇;非Hadoop專案,以靜態型別語言為主,首選Protobuf。
  • 不想造 RPC 的輪子,Thrift可以考慮。
  • 如果序列化之後需要支援不同的傳輸層協議,或者需要跨防火牆訪問的高效能場景,Protobuf可以優先考慮。
3. 重整旗鼓
調研周邊後,2015年開搞第二款手遊,吸取之前的教訓,這次設計的基本原則是:
  • 系統拆分、解耦,清晰定義系統間介面,隱藏系統內部實現
  • 大框架儘可能通用,子系統可在不同場景替換

下面首先對服務定義,然後介紹整體框架和服務內部拆分。

3.1 服務定義
舉個手遊的例子,看圖說話:
12ClusterCenterServer
  • 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則通過路徑描述父子關係。但不管怎麼都可以在概念層次使用路徑結構 。
上圖中,Service Instance 完整路徑可描述為:/AppID/Area/Platform/WorldID/GroupID/ClusterName/InstanceName。有以下特點:
  • 叢集路徑一定是其中各個例項的父路徑
  • 從功能完整性而言,叢集是服務的基本粒度
  • 相同功能的叢集在不同字首路徑下含義不同,服務目標也可以不同,比如:
    /Example/wechat/android/w_1/g_1/Lobby 和/Example/wechat/android/w_3/g_2/Lobby 功能上均表示大廳服務,但一個為大區1分組1服務,一個為大區3分組2服務

3.2 服務發現基本流程

13ClusterCenterServer
先抽象幾個基本操作,不同服務發現元件的API可能略有差異,但應該有對應功能:
  • Create 在服務發現元件中建立 Key 對應的 Service Node,指定全域性唯一的標記。
  • Delete 在服務發現元件中刪除 Key 對應的節點。
  • Set 設定 Key 對應的 Value, 安全訪問策略或者節點基礎屬性等。
  • Get 根據 Key 獲取對應節點的資料,如果是父節點可以獲取其子節點列表。
  • Watch 對節點設定監視器,當該節點自身,以及巢狀子節點資料發生變更時,服務發現元件將變更事件主動通知給監視者。
Service Instance 每次在啟動時,按照下面的流程處理:
  • 生成自己的 Service Path,注意這是服務例項的路徑。
  • 以 Service Path 為key,通過 Create 方法生成節點,Set 資料:對外開放的地址、安全訪問策略等。
  • 生成需要訪問的服務叢集的 Service Path,通過 Get 方法獲取叢集資料,如果找不到說明該服務不存在;如果可以找到分兩種情況:
  • 該路徑下沒有子節點。說明當前不存在可用的服務例項,對叢集路徑設定watcher,等待新的可用例項。
  • 該路徑下有子節點。那麼 Get 所有子節點列表,並進一步 Get 子節點訪問方式和其它資料。同時設定 watcher 到叢集路徑,檢測叢集是否存在變化,比如新增或減少例項等。
Service Instance 在關閉時,按照下面的流程處理:
  • 通過 Delete 方法刪除自己對應的節點。有些服務發現元件可以在例項生命週期結束時自行刪除,比如zookeeper的臨時節點。對於etcd的目錄,或者zookeeper的父路徑,如果非空,是無法刪除的。
根據上面的抽象可以定義 服務發現 的基本介面,介面的具體實現可以針對不同的元件開發不同的wrapper,但可以和業務解耦。

3.3 服務架構

所有的架構歸根結底還是需要具體到程式層次實現的。目前我們專案開發的分散式架構元件稱之為 DMS(Distributed Messaging System),以 DMS Library 的形式提供,整合該庫即可實現面向服務的分散式通訊。下面是 DMS 設計的總體結構:
14ClusterCenterServer
關於Serialize/DeSerialize, APP業務的選擇自由度較高,下面介紹其它Layer的具體實現:
3.3.1 Message Middleware
訊息中介軟體前面介紹有很多選擇。DMS 使用的是 ZeroMQ,出發點是:輕量級、效能強大、偏底層所以靈活而且可控性較高。由此帶來的成本是,高階應用場景需要做不少二次開發,而且長達80多頁的資料也需要不少時間。介紹ZeroMQ的文章太多,這裡不打算科普,所以直接給出設計方案。
通訊模式的選擇
ZeroMQ的Socket有多種型別,不同組合可以形成不同的通訊模式,列舉幾種常見的:
  • REQ/REP 一應一答,有請求必須等待回應
  • PUB/SUB 釋出訂閱
  • PUSH/PULL 流水線式處理,上游推資料,下游拉資料
  • DEALER/ROUTER 全雙工非同步通訊
15ClusterCenterServer
看到這裡,大家可能會覺得選擇PUB/SUBDEALER/ROUTER應該可以滿足絕大部分應用場景吧。實際上DMS只使用了一種socket型別,那就是ROUTER,通訊模式只有一種ROUTER/ROUTER。一種socket,一種通訊模式,聽起來很簡單,但真可以滿足要求嗎?
  • DEALER/ROUTER 是傳統非同步模式,一方connect,一方bind。前端如果要連線多個後端就得建立多個socket。在前面描述的叢集服務模式下,一個節點既會作為Client也會作為Server,會有多條入邊(被動接收連線)和出邊(主動發起連線)。這正好就是路由的概念,一個ROUTER socket可以建立多條通路,並對每條通路傳送或者接收訊息。
  • PUB/SUB 注重的是擴充套件性和規模,按照ZeroMQ作者的意思當每秒鐘需要向上千的節點廣播百萬條訊息時,你應該考慮使用 PUB/SUB 。好吧,可預見的將來業務規模恐怕還到達不到這種程度,現在先把簡單放在第一位吧。

3.3.2 DMS Protocol

訊息結構
DMS的協議實現叢集管理,訊息轉發等基本功能。ZeroMQ的訊息可以由 Frame 組成,一個Frame可以為空也可以是一段位元組流,一個完整的訊息可以包含多個Frame,稱為Multipart Message。基於這種特點,在DMS定義協議,可以將內容拆分為不同的基本單元,每個單元用一個Frame描述,通過單元組合表示不同的含義。這與傳統方式:一條協議就是一個結構體,不同單元組合需要定義為一個結構體的方式相比更加靈活。
下面來看看DMS Protocol的基本組成。首幀一定是對端ID。對端接收後也一定會獲取資訊傳送端的ID。第二幀包含DMS控制資訊。第三、第四幀等全部是業務自定義的傳輸資訊,僅對REQ-REP有效:
16ClusterCenterServer
PIDF有兩層含義:所在服務叢集的標記,自身的例項標記。這些標記與Service Discovery關於節點key的定義保持一致,有兩種形式 字串 與 整型,前者可讀方便理解,後者是前者的Hash,提高傳輸效率。使用虛擬碼來描述PIDF,大概是下面的樣子:
17ClusterCenterServer
PIDF中的 ClusterID 和 InstanceID 各種取值,會有不同的通訊行為:
在連線首次建立時,還需要將可讀的服務路徑傳輸給對端:
18ClusterCenterServer
協議命令字
DMS協議全部在每個訊息的第二幀即Control Frame中實現。命令字定義為:
通訊流程——建立連線
19ClusterCenterServer
通過 Service Discovery 找到server後不要立即連線,而是傳送探測包。原因有以下幾點:
  • 服務發現雖然可以反映節點是否存活,但一般有延遲,所以從服務發現獲取的節點僅僅是候選節點。
  • 網路底層機制差異較大,有些基於連線,比如raw socket,有些沒有連線,比如shared memory。最好在高層協議中解決連線是否成功。這就好比聲納,投石問路,有回應說明可以連線,沒有回應說明目前連線不可用。

通訊流程——業務訊息傳送

20ClusterCenterServer
  • 普通訊息 若 PIDF 表示對端例項和當前程式直接連線,那麼傳送訊息
  • 路由訊息 若 PIDF 表示對端例項和當前程式沒有直接連線,那麼可以通過直連的例項轉發。路由機制 後文會介紹
  • 廣播訊息 若 PIDF InstanceID為負數,則向指定叢集內所有例項廣播
路由 和 廣播 是可以混合使用的。上述過程 DMS 自動完成,業務不必參與,但可以截獲干預。

通訊流程——保活機制

建立連線後,請求者會持續按照自己的間隔向服務者傳送探測包。如果請求者連續若干次沒有收到服務者的PONG回包,則請求者認為與服務者的連線已經斷開。
如果服務者收到請求者的任何資料包,認為請求者存活,如果超出一定時間沒有收到(含PING),則認為請求者掉線。這個超時時間包含在READY協議中,由請求者告知服務者。

通訊流程——連線斷開

任何一方收到 DISCONNECT 後,即認為對方主動斷開連線,不要再主動向對方進行任何形式的通訊。

3.3.3 DMS Kernel

下面介紹 DMS Kernel 如何根據 DMS Protocol 實現相關邏輯,並如何與業務互動。
21ClusterCenterServer

SERVICE MANAGER

  • self 確定自身 服務路徑,實現服務註冊,以及與目標通訊鏈路的註冊,供路由表使用
  • targets 獲取並監控目標服務的資料以及執行狀態
  • ACL 訪問控制管理
  • 對服務發現層介面進行封裝,不同的 SERVICE DISCOVERY 功能可能有所不同

ROUTER MANAGER

22ClusterCenterServer
每個服務例項在主動成功連線對端服務後,通過 SERVICE MANAGER 將連線以邊的形式寫入到 SERVICE DISCOVERY 中,這樣就會以 鄰接邊 的形式生成一張完整的圖結構,也就是routing table。比如: Service 1 和 Service 2,Service 3,Service 4 均有連線,那麼將邊(1,2),(1,3),(1,4) 記錄下來。SERVICE DISCOVERY 關於路由鄰接連結串列的記錄可以使用公共的key,比如: /AppID/Area/Platform/routing_table 。然後所有的服務例項都可以更新、訪問該路徑以便獲得一致的路由表。基礎功能有兩個:
  • Updater 用於向路由表中新增邊,刪除邊,設定邊的屬性(比如權重),並對邊的變化進行監控
  • Calculator 根據鄰接邊形成的 圖結構 計算路由,出發點是當前例項,給定目標點判斷目標是否可達,如果可達確定路徑並傳輸給下一個節點轉發。預設選擇 Dijkstra 演算法,業務可以定製。

CONNECTION MANAGER

管理 Frontends 即前端請求進入的連線,和 Backends 即向後端主動發起的連線。Backends的目標來源於 Service Manager。
  • Sentinel 對前端發起的連線,通過 READY 協議,可以獲取該連線的失活標準,並通過前端主動包來判斷進入連線是否存活。如果失活,將該連線置為斷開狀態,不再向對應前端主動發包。
  • Prober 對後端服務進行連線建立和連線保活。
  • Dispatcher 訊息傳送時用於確定通訊對端例項。連線是基於例項的,但是業務一般都是面向服務叢集的,所以Dispathcer 需要實現一定的分配機制,將訊息轉發給 服務叢集中的某個 具體例項 。注意這裡僅只存在直接連線的單播。分配時應考慮 負載均衡 預設使用一致性雜湊演算法,業務完全可以根據具體應用場景自定義。

3.3.4 DMS Interface

23ClusterCenterServer
DMS API 是DMS對業務提供的服務介面,可以管理服務、通訊等基本功能;
DMS APP Interface 是DMS要求業務必須實現的介面比如:Dispatcher 的負載均衡策略,對端服務狀態變化通知,以及業務自定義 路由演算法 等等。

3.4 應用場景

下面羅列DMS三大類典型應用場景,其它場景應該可以通過這三個例子組合實現:
  • 無Broker通訊
24ClusterCenterServer
最基礎的通訊方式——兩個叢集之間的 Instance 全連線,適合服務數量不多、邏輯不復雜的簡單業務。
  • Broker通訊
25ClusterCenterServer
對於一個內部聚合的子系統,可能包含N個服務,這些服務之間相互存在較強的互動行為。如果使用無Broker模式可能有兩個問題:鏈路過多:通訊層的記憶體佔用較大;運維維護困難;服務沒有解耦,直接依賴於對端的存在;
這時Broker叢集可以承擔訊息中轉的作用,而且可以完成一些集中式邏輯處理。注意這裡Broker只是一個名字,通過 DMS Library 可以直接實現。
  • Broker級聯通訊
26ClusterCenterServer
多個子系統相互通訊,估計沒有設計者願意把內部細節完全暴露給對方,這時兩個Broker叢集就相當於門戶:首先可以實現內部子系統相互通訊,以及集中邏輯;其次,可以作為所處子系統的對外介面,遮蔽細節。這樣不同子系統只需通過各自的Broker叢集對外提供服務即可。
總結
本文主要介紹了 DMS 的幾個基礎結構:服務發現、訊息中介軟體以及通訊架構。基本思想是:框架分層、層級之間介面清晰定義,以便在不同場景下使用不同的具體實現進行替換。其中 zookeeper,ZeroMQ 只是舉例說明當前的一種實現方式,在不同場景下可以選擇不同元件,只要滿足介面即可。

相關文章