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

本文由騰訊WeTest團隊提供,轉載聯繫公眾號請申請許可權。更多資訊可直接戳鏈接查看:分散式系統設計的求生之路

微信號:TencentWeTest

作者:Simon,騰訊後台開發高級工程師

WeTest導讀

分散式系統理念漸漸成為了後台架構技術的重要選擇,本文介紹了作者在手游領域對分散式系統進行的種種嘗試,並在嘗試中制定了對服務的定義、整體框架的構建以及服務內部拆分的流程。

前言

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

1. 失敗的首次嘗試

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

這種架構提供了三個基本組件:

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 服務定義舉個手游的例子,看圖說話:

  • 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 服務發現基本流程

先抽象幾個基本操作,不同服務發現組件的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 設計的總體結構:

關於Serialize/DeSerialize, APP業務的選擇自由度較高,下面介紹其它Layer的具體實現:

3.3.1 Message Middleware消息中間件前面介紹有很多選擇。DMS 使用的是 ZeroMQ,出發點是:輕量級、性能強大、偏底層所以靈活而且可控性較高。由此帶來的成本是,高級應用場景需要做不少二次開發,而且長達80多頁的資料也需要不少時間。介紹ZeroMQ的文章太多,這裡不打算科普,所以直接給出設計方案。通信模式的選擇ZeroMQ的Socket有多種類型,不同組合可以形成不同的通信模式,列舉幾種常見的:

  • REQ/REP 一應一答,有請求必須等待回應

  • PUB/SUB 發布訂閱

  • PUSH/PULL 流水線式處理,上游推數據,下游拉數據

  • DEALER/ROUTER 全雙工非同步通信

看到這裡,大家可能會覺得選擇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有效:PIDF有兩層含義:所在服務集群的標記,自身的實例標記。這些標記與Service Discovery關於節點key的定義保持一致,有兩種形式 字元串 與 整型,前者可讀方便理解,後者是前者的Hash,提高傳輸效率。使用偽代碼來描述PIDF,大概是下面的樣子:PIDF中的 ClusterID 和 InstanceID 各種取值,會有不同的通信行為:在連接首次建立時,還需要將可讀的服務路徑傳輸給對端:

協議命令字

DMS協議全部在每個消息的第二幀即Control Frame中實現。命令字定義為:

通信流程——建立連接

通過 Service Discovery 找到server後不要立即連接,而是發送探測包。原因有以下幾點:

  • 服務發現雖然可以反映節點是否存活,但一般有延遲,所以從服務發現獲取的節點僅僅是候選節點。

  • 網路底層機制差異較大,有些基於連接,比如raw socket,有些沒有連接,比如shared memory。最好在高層協議中解決連接是否成功。這就好比聲納,投石問路,有回應說明可以連接,沒有回應說明目前連接不可用。

通信流程——業務消息發送

  • 普通消息 若 PIDF 表示對端實例和當前進程直接連接,那麼發送消息

  • 路由消息 若 PIDF 表示對端實例和當前進程沒有直接連接,那麼可以通過直連的實例轉發。路由機制 後文會介紹

  • 廣播消息 若 PIDF InstanceID為負數,則向指定集群內所有實例廣播

路由 和 廣播 是可以混合使用的。上述過程 DMS 自動完成,業務不必參與,但可以截獲干預。

通信流程——保活機制

建立連接後,請求者會持續按照自己的間隔向服務者發送探測包。如果請求者連續若干次沒有收到服務者的PONG回包,則請求者認為與服務者的連接已經斷開。

如果服務者收到請求者的任何數據包,認為請求者存活,如果超出一定時間沒有收到(含PING),則認為請求者掉線。這個超時時間包含在READY協議中,由請求者告知服務者。

通信流程——連接斷開

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

3.3.3 DMS Kernel

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

SERVICE MANAGER

  • self 確定自身 服務路徑,實現服務註冊,以及與目標通信鏈路的註冊,供路由表使用

  • targets 獲取並監控目標服務的數據以及運行狀態

  • ACL 訪問控制管理

  • 對服務發現層介面進行封裝,不同的 SERVICE DISCOVERY 功能可能有所不同

ROUTER MANAGER

每個服務實例在主動成功連接對端服務後,通過 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

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

3.4 應用場景

下面羅列DMS三大類典型應用場景,其它場景應該可以通過這三個例子組合實現:

  • 無Broker通信

最基礎的通信方式——兩個集群之間的 Instance 全連接,適合服務數量不多、邏輯不複雜的簡單業務。

  • Broker通信

對於一個內部聚合的子系統,可能包含N個服務,這些服務之間相互存在較強的交互行為。如果使用無Broker模式可能有兩個問題:鏈路過多:通信層的內存佔用較大;運維維護困難;服務沒有解耦,直接依賴於對端的存在;

這時Broker集群可以承擔消息中轉的作用,而且可以完成一些集中式邏輯處理。注意這裡Broker只是一個名字,通過 DMS Library 可以直接實現。

  • Broker級聯通信

多個子系統相互通信,估計沒有設計者願意把內部細節完全暴露給對方,這時兩個Broker集群就相當於門戶:首先可以實現內部子系統相互通信,以及集中邏輯;其次,可以作為所處子系統的對外介面,屏蔽細節。這樣不同子系統只需通過各自的Broker集群對外提供服務即可。總結本文主要介紹了 DMS 的幾個基礎結構:服務發現、消息中間件以及通信架構。基本思想是:框架分層、層級之間介面清晰定義,以便在不同場景下使用不同的具體實現進行替換。其中 zookeeper,ZeroMQ 只是舉例說明當前的一種實現方式,在不同場景下可以選擇不同組件,只要滿足介面即可。
推薦閱讀:

分散式系統測試那些事兒——錯誤注入
如何解決分散式系統的Logical Time問題?(一)

TAG:分布式系统 | 服务器架构 |