前言
這是一本關於微服務架構設計方面的書,這是本人閱讀的學習筆記。首先對一些符號做些說明:
()為補充,一般是書本里的內容;
[]符號為筆者筆注;
微服務架構將應用程序構建為一組服務,這些服務必須經常協作才能處理各種外部請求。而服務的實例通常是在多台機器上運行的進程,所以它們必須使用進程間通信進行交互。
當前有多種進程間通信機制,比較流行的是REST(使用JSON)。選擇合適的進程間通信機制是一個重要的架構決策,它影響應用程序的可用性。
1. 微服務架構中的進程間通信概述
進程間通信技術有:基於同步請求/響應、異步的基於消息的通信機制等。
1.1 交互方式的兩個維度
- 第一個維度:
- 一對一:每個客戶端請求由一個服務實例來處理;
- 一對多:每個客戶端請求由多個服務實例來處理;
- 第二個維度:
- 同步模式:客戶端請求需要服務端實時響應,客戶端等待響應時可能導致堵塞;
- 異步模式:客戶端請求不會阻塞進程,服務端的響應可以是非實時的;
1.2 交互方式的類型
- 一對一交互:
- 請求/響應:一個客戶端向服務端發起請求,等待響應;客戶端期望服務端很快就會發送響應。在一個基於線程的應用中,等待過程可能造成線程阻塞。這樣的方式會導致服務的緊耦合;
- 異步請求/響應:客戶端發送請求到服務端,服務端異步響應請求。客戶端在等待時不會阻塞線程,因為服務端響應不會馬上返回;
- 單向通知:客戶端的請求發送到服務端,但是並不期望服務端做出任何響應;
- 一對多交互:
- 發布/訂閱方式:客戶端發布通知消息,被零個或多個感興趣的服務訂閱;
- 發布/異步響應方式:客戶端發布請求消息,然后等待從感興趣的服務發回的響應;
1.3 API的演化
- 語義化版本控制:用於指定如何使用版本號,並且以正確的方式遞增版本。其由3部分組成:
- MAJOR:對API進行不兼容的修改時;
- MINOR:對API進行向后兼容的增強時;
- PATCH:進行向后兼容的錯誤修復時;
- 規范:
MAJOR.MINOR.PATCH
;
- 進行次要並且向后兼容的改變:對ADP的附加修改更換或功能增強。其包括:
- 添加可選屬性;
- 向響應添加屬性;
- 添加新操作;
- 進行主要並且不向后兼容的版本:需要服務在一段時間內同時支持新舊版本的API時;
1.4 消息的格式
消息格式會影響進程間通信的效率、API的可用性和可演化新。使用跨語言的消息格式尤為重要;
- 基於文本的消息格式:
- 舉例:JSON、XML;
- 好處:可讀性高、自描述性,有良好的向后兼容性 [消息接收方只需挑選他們感興趣的值,忽略其他];
- 弊端:信息冗長,解析文本需要額外的性能效率開銷;
- 二進制消息格式:
- 舉例:Tars、Protocol Buffers、Avro;
- 好處:提供強類型定義的IDL(接口描述文件),用於定義消息;編譯器會根據這些格式生成序列化和反序列化代碼;
- 弊端:不得不采用API優先的方法進行服務設計
2. 基於同步遠程過程調用模式的通信
2.1 遠程過程調用RPI
指客戶端使用同步的遠程過程調用協議(如REST)來調用服務。
圖解:客戶端業務邏輯調用代理接口,這個接口由遠程過程調用代理適配器類實現。遠程過程調用代理向服務器發送請求,該請求由遠程過程調用服務器適配器類處理,該類通過接口調用服務的業務邏輯。然后它將恢復發送回遠程過程調用代理,該代理將結果返回給客戶端的業務邏輯。
- 代理接口:通常是封裝底層通信協議,如下面介紹的REST與gRPC。
2.2 REST通信協議的特點及優缺點
REST是一種(總是)使用HTTP協議的進程間通信機制。
特點:
- REST使用HTTP動詞來操作資源,使用URL引用這些資源;
- 資源通常使用XML文檔或JSON對象的形式,也可以使用其他格式(二進制等);
- REST的成熟模型有:有4個層次(P71);
- REST API:最流行的REST IDL是Open API規范,它是從Swagger開源項目發展而來的;
- REST API的挑戰:
- 在一個請求中獲取多個資源的挑戰:指如何在單個請求中檢索多個相關對象;
- 吧操作映射為HTTP動詞的挑戰:指一個HTTP動詞可能對應多種方法,如PUT請求更新訂單可能包括取消訂單、修改訂單等;
好處:
- 非常簡單,大家比較熟悉;
- 可以使用瀏覽器擴展(如Postman插件)或者curl之類的命令行測試HTTP API;
- 直接支持請求/響應方式的通信;
- HTTP對防火牆友好;
- 不需要中間代理,簡化系統架構;
弊端:
- 只支持請求/響應方式的通信;
- 可能導致可用性降低。由於客戶端和服務直接通信而沒有代理來緩沖消息,因此它們必須在REST API調用期間保持在線;
- 客戶端必須知道服務實例的位置(URL)。客戶端必須使用所謂的服務發現機制來定位服務實例;
- 在單個請求中獲取多個資源具有挑戰性;
- 有時很難將多個更新操作映射到HTTP動詞;
2.3 gRPC通信協議的特點及優缺點
gRPC是一個用於編寫跨語言客戶端和服務端的框架,是一種二進制協議。
特點:
- gRPC API由一個或多個服務和請求/響應消息定義組成;
- 服務定義類似Java接口,是強類型方法的集合;
- 使用Protocol Buffers作為消息格式,是一種高效且緊湊的二進制格式,是一種標記格式;
- 因此gRPC使API能夠在保持向后兼容的同時進行變更;
好處:
- 設計具有復雜更新操作的API非常簡單;
- 具有高效、緊湊的進程間通信機制,尤其是在交換大量消息時;
- 支持在遠程過程調用和消息傳遞過程中使用雙向流式消息方式;
- 實現了客戶端和用各種語言編寫的服務端之間的互操作性;
弊端:
- 與基於REST/JSON的API機制相比,JavaScript客戶端使用基於gRPC的API需要做更多的工作;
- 舊式防火牆可能不支持HTTP/2;
2.4 同步通信下的局部故障風險
客戶端和服務端是獨立的進程,服務端很可能無法在有限的時間內對客戶端的請求作出響應。
圖解:當Order Service無響應時,OrderServiceProxy將無限期地阻塞,等待響應。會消耗時間、浪費線程等資源。最終API Gateway將資源消耗,無法處理請求,整個API不可用。
解決方法是:
- 必須讓遠程過程調用代理(如OrderServiceProxy)有正確處理無響應服務的能力;
- 需要決定如何從失敗的遠程服務中恢復;
2.5 解決局部故障的思路與方法
- 開發可靠地遠程過程調用代理:使用Netflix描述的方法,可以包括以下機制的組合;
- 網絡超時:在等待針對請求的響應時,不要做成無限阻塞,而是設定一個超時,用來保證不會一直在無響應的請求上浪費資源;
- 限制客戶端向服務器發出請求的數量:把客戶端能夠向特定服務發起的請求設置一個上限,如果請求達到上限,就讓請求立刻失敗;
- 斷路器模式:監控客戶端發出請求的成功和失敗數量,如果失敗的比例超過一定的閾值,就啟動斷路器,讓后續調用立即失敗。如果大量請求都以失敗告終,說明被調服務不可用。經過一定時間后,客戶端繼續嘗試,如果調用成功,則移除斷路器;
- 從服務失效故障中恢復:
- 可以只是服務向其客戶端返回錯誤;
- 返回備用值(如默認值或緩存響應);
2.6 應用層服務發現模式
服務及其客戶直接與服務注冊表交互;
- 服務實例使用服務注冊表注冊其網絡位置。客戶端首先通過查詢服務注冊表獲取服務實例列表來調用服務,然后它向其中一個實例發送請求;
- 這種服務發現是以下兩種模式的組合:
- 自注冊模式:服務實例向服務注冊表注冊自己;
- 可以提供運行狀態檢查URL(“心跳”功能,服務注冊表定期調用該端點驗證服務實例是否正常且可用於處理請求);
- 客戶端發現模式:客戶端從服務注冊表檢索可用服務實例的列表,並在它們之間進行負載均衡;
- 為了提高性能,客戶端可能會緩存服務實例;
- 自注冊模式:服務實例向服務注冊表注冊自己;
- 業界有Netflix開發的Eureka組件,一個高可用的服務注冊表;Pivotal開發的SpringCloud
使相關組件使用非常簡單;
2.7 平台層服務發現模式
通過部署基礎設施來處理服務發現;
- 部署平台包括一個服務注冊表,用於跟蹤已部署服務的IP地址;
- 部署平台為每個服務提供DNS名稱、虛擬IP(VIP)地址和解析為VIP地址的DNS名稱;
- 這種服務發現是以下兩種模式的組合:
- 第三方注冊模式:由第三方負責(稱為注冊服務器)處理注冊,而不是服務本身先服務注冊表注冊自己;
- 服務端發現模式:客戶端向DNS名稱發出請求,對該DNS名稱的請求被解析到路由器,路由器查詢服務注冊表並對請求進行負載均衡;
- 業界有Docker與Kubernetes,都內置有服務注冊表與服務發現機制;
3. 基於異步消息模式的通信
使用消息機制時,服務之間的通信采用異步交換消息的方式完成。
基於消息機制的應用程序通常采用消息代理;另一種選擇是使用無代理架構。
3.1 關於消息
消息由消息頭部和消息主體組成;
- 消息頭部:
- 標題:名稱與值對;
- 消息ID:消息傳遞基礎唯一ID;
- 返回地址:指定發送回復的消息通道;
- 消息主體:以文本或二進制格式發送的數據;
- 文檔:包含數據的通用消息。接受者決定如何解釋它。對命令式消息的回復是文檔消息的一種應用場景;
- 命令:一條等同於RPC請求的消息。它指定要調用的操作及其參數;
- 事件:表示發送方這一端發生了重要的事件。事件通常是領域事件,表示領域對象的狀態更改;
3.2 關於消息通道
有以下兩種類型的消息通道:
- 點對點通道:
- 向正在從通道讀取的一個消費者傳遞消息;
- 如:命令式消息通常通過點對點通道發送;
- 發布 - 訂閱通道:
- 將一條消息發送給所有訂閱的接收方;
- 如:事件式消息通常通過發布 - 訂閱通道發送;
3.3 使用消息機制實現交互方式
介紹下面四種交互方式的消息機制:
- 實現單向通知:
- 客戶端將消息(通常是命令式消息)發送到服務所擁有的點對點通道;
- 服務訂閱該通道並處理該消息,但服務不會發回回復;
- 實現發布/訂閱:
- 客戶端將消息發布到由多個接收方讀取的發布/訂閱通道;
- 發布領域事件的服務擁有自己的發布/訂閱通道,通道名稱往往派生自領域類;
- 如:Order Service將Order事件發布到Order通道;Delivery Service將Delivery事件發布到Delivery通道;
- 實現發布/異步響應:
- 一種更高級的交互方式,將發布/訂閱與請求/響應這兩種方式的元素組合實現;
- 客戶端發布一條消息,在消息的頭部中指定回復通道。這個通道同時也是一個發布 - 訂閱通道;
- 消費者將包含相關性ID的回復消息寫入回復通道;
- 客戶端通過使用相關性ID來收集響應,以此將回復消息與請求進行匹配;
- 實現請求/響應和異步請求/響應:
- 客戶端發送請求,服務會發回回復;
- 客戶端必須告知服務發送回復消息的位置,並且必須將回復消息與請求匹配;
- 即:客戶端發送具有回復通道頭部的命令式消息。服務器將回復消息寫入回復通道,該回復消息包含與消息標識符具有相同的相關性ID。客戶端使用相關性ID將回復消息與請求匹配;
- 由於客戶端和服務端使用消息機制進行通信,因此交互本質上是異步的;
- 工作原理圖如下:
3.4 為基於消息機制的服務API創建API規范
服務的異步API規范必須制定消息通道的名稱、通過每個通道交換的消息類型及其格式。
- 服務的異步API包含供客戶端調用的操作和由服務對外發布的事件;
- (記錄異步操作)可以使用以下兩種不同交互方式之一調用服務的操作:
- 請求/異步響應式API:包括服務端命令消息通道、服務接受的命令式消息的具體類型和格式,以及服務發送的回復消息的類型和格式;
- 單向通知式API:包括服務的命令消息通道,以及服務接受的命令式消息的具體類型和格式;
- (記錄事件發布)服務還可以使用發布/訂閱的方式對外發布事件;
- 此API風格等規范包括事件通道以及服務發布到通道的事件式消息的類型和格式;
3.5 無代理消息的利弊
在無代理的架構中,服務可以直接交換信息。
好處:
- 允許更輕的網絡流量和更低的延遲,因為沒有中間代理過程;
- 消除了消息代理可能成為性能瓶頸或單點故障的可能性;
- 具有較低的操作復雜性,因為不需要設置和維護消息代理;
弊端:
- 服務需要了解彼此位置,因此必須使用服務發現機制;
- 降低可用性,因為在交換消息時,信息的接收方和發送方必須同時在線;
- 在實現例如確保消息能夠成功投遞這些復雜功能時的挑戰性更大;
舉例:
- ZeroMQ:一種流行的無代理消息技術;
3.6 基於代理消息的利弊
消息代理是所有消息的中介節點;發送方將消息寫入消息代理,消息代理將消息發送給接收方。
好處:
- 松耦合;
- 消息緩存:消息代理可以在消息被處理之前一直緩存消息;
- 靈活的通信:消息代理支持前面提到的所有交互方式;
- 明確的進程間通信
弊端:
- 潛在的性能瓶頸:解決方法 - 橫向擴展;
- 潛在的單點故障:解決辦法 - 大多數現代消息代理是高可用的;
- 額外的操作復雜性:消息系統必須是一個獨立安裝、配置和運維的系統組件;
舉例:
- 流行的開源消息代理:Apache ActiveMQ(JMS)、RabbitMQ(AMQP)、Apache Kafka;
- 基於雲的消息服務:AWS Kinesis、AWS SQS;
- 上述除了AWS SQS外都支持點對點和發布 - 訂閱通道;AWS SQS只支持點對點通道;
3.7 選擇消息代理需要考慮的因素
- 支持的編程語言;
- 支持的信息標准;
- 消息排序:消息代理是否能夠保留消息的排序;
- 投遞保證:消息代理提供怎樣的消息投遞保證;
- 持久性;
- 耐久性:如果接收方重新連接到消息代理,它是否會收到斷開連接時發送的消息;
- 可擴展性;
- 延遲;
- 競爭性(並發)接收方:消息代理是否支持競爭性接收方;
3.8 處理並發和消息順序
問題描述:在橫向擴展多個消息接收方的實例的情況下,消息的順序可能會錯位。
解決方法:使用分片消息通道擴展接收方;
圖解:
- 分片通道由兩個或多個分片組成,每個分片的行為類似於一個通道;
- 發送方在消息頭部指定分片鍵,通常是任意字符串或字節序列。消息代理使用分片鍵將消息分配給特定的分片;
- 如:通過計算分片鍵的散列來選擇分片;
- 消息代理將接收方的多個實例組合在一起,並將他們視為相同的邏輯接收方;
- 如:Apache Kafka使用術語消費者組;消息代理將每個分片分配給單個接收器;它在接收方啟動和關閉時重新分配分片;
3.9 處理重復消息
問題描述:客戶端、網絡或消息代理的故障可能導致消息被多次傳遞。
有以下兩種解決辦法:
- 編寫冪等消息處理器:
- 冪等操作特點:任意多次執行所產生的影響均與一次執行的影響相同;
- 跟蹤消息並丟棄重復消息:
- 將消息處理程序注冊進應用程序表(NoSQL)【第七章介紹】;
- 使用message id跟蹤消息並丟棄重復消息,如下圖:
3.10 事務性消息
- 使用數據庫表作為消息隊列:
- 事務性發件箱:通過將事件或消息保存在數據庫OUTBOX表中,將其作為數據庫事務是一部分發布;
- 事務性發件箱:通過將事件或消息保存在數據庫OUTBOX表中,將其作為數據庫事務是一部分發布;
- 通過輪詢模式發布事件:
- 輪詢發布數據:通過輪詢數據庫中的發件箱發布消息;
- 小規模下運行良好,弊端在於經常輪詢數據庫會造成較大開銷;
- 使用事務日志拖尾模式發布事件:
- 事務日志拖尾:通過拖尾數據日志發布對數據庫所做的修改;
- 一些行業案例:Debezium、Linkedln Databus、DynamoDB streams、Eventuate Tram;
- 下圖解:每次應用程序提交到數據庫的更新都對應着數據庫事務日志中的一個條目;事務日志挖掘器可以讀取事務日志,把每條跟消息有關的記錄發送給消息代理;
3.11 消息相關的類庫和框架
服務需要使用庫來發送和接收消息。
有兩種方法:
- 使用消息代理的客戶端庫,問題有:
- 客戶端庫將發布消息的業務邏輯耦合到消息代理API;
- 客戶端庫通常只提供發送和接收消息的基本機制,不支持更高級別的交互方式;
- 消息代理的客戶端庫通常非常底層,需要多行代碼才能發送/接收消息;
- 使用更高級別的庫或框架來隱藏底層細節,並直接支持更高級別的交互方式:
- 如Eventuate Tram框架;
4. 使用異步消息提高可用性
采用同步通信機制處理請求,會對系統的可用性帶來影響。因此,應盡可能選擇異步通信機制來處理服務之間的調用。
4.1 同步消息會降低可用性
4.2 消除同步交互的方法
-
使用異步交互模式:
- 下圖解:客戶的通過Order Service發送一個請求消息交換消息的方式創建訂單;這個服務隨即采用異步交換消息的方式跟其他服務通信完成訂單的創建;
- 缺點:很多情況下都要采用REST等同步通信協議API,不能替換為異步;
-
復制數據:
- 下圖解:Consumer Service和Restaurant Service在它們的數據發生變化時對外發布事件;Order Service訂閱這些事件,並據此更新自己的數據副本;
- 缺點:當數據量巨大時效率低下;
- 先返回響應,再完成處理:
- 下圖解:Order Service創建一個未檢驗(Pending)狀態的訂單,然后通過異步交互方式直接跟其他服務通信來完成驗證;
- 缺點:使客戶端更復雜。
5. 本章小結
- 微服務架構是一種分布式架構,因此進程間通信起着關鍵作用;
- 仔細管理服務API的演化至關重要。向后兼容的更改是最容易進行的,因為它們不會影響客戶端。如果對服務的API進行重大更改,通常需要同時支持舊版本和新版本,直到客戶端升級為止;
- 有許多進程間通信技術,每種技術都有不同的利弊。一個關鍵的設計決策是選擇同步遠程過程調用模式或異步消息模式。基於同步遠程過程調用的協議(如REST)是最容易使用的。但是,理想情況下,服務應使用異步消息進行通信,以提高可用性;
- 為了防止故障通過系統層層蔓延,使用同步協議服務的客戶端必須設計成能夠處理局部故障,這些故障是在被調用的服務停機或表現出高延遲時發生的。特別是,它必須在發出請求時使用超時,限制未完成請求的數量,並使用斷路器模式來避免調用失敗的服務;
- 使用同步協議的架構必須包含服務發現機制,以便客戶端確定服務實例的網絡位置。最簡單的方法是使用部署平台實現的服務發現機制:服務器端發現和第三方注冊模式。但另一種方法是在應用程序級別實現服務發現:客戶的發現和自注冊模式。它需要的工作量更大,但它確實可以處理服務在多個部署平台上運行的場景;
- 設計基於消息的架構的一種好方法是使用消息和通道模型,它抽象底層消息系統的細節。然后,你可以將該設計映射到特定的消息基礎結構,該基礎結構通常基於消息代理;
- 使用消息機制的一個關鍵挑戰是以原子化的方式同時完成數據庫更新和發布消息。一個好的解決方案是使用事務性發件箱模式,並首先將消息作為數據庫事務的一部分寫入數據庫。然后,一個單獨的進程使用輪詢發布者模式或事務日志拖尾模式從數據庫中檢索信息,並將其發布給消息代理。
最后
