前言
這是一本關於微服務架構設計方面的書,這是本人閱讀的學習筆記。首先對一些符號做些說明:
()為補充,一般是書本里的內容;
[]符號為筆者筆注;
微服務架構將應用程式構建為一組服務,這些服務必須經常協作才能處理各種外部請求。而服務的例項通常是在多臺機器上執行的程式,所以它們必須使用程式間通訊進行互動。
當前有多種程式間通訊機制,比較流行的是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)是最容易使用的。但是,理想情況下,服務應使用非同步訊息進行通訊,以提高可用性;
- 為了防止故障通過系統層層蔓延,使用同步協議服務的客戶端必須設計成能夠處理區域性故障,這些故障是在被呼叫的服務停機或表現出高延遲時發生的。特別是,它必須在發出請求時使用超時,限制未完成請求的數量,並使用斷路器模式來避免呼叫失敗的服務;
- 使用同步協議的架構必須包含服務發現機制,以便客戶端確定服務例項的網路位置。最簡單的方法是使用部署平臺實現的服務發現機制:伺服器端發現和第三方註冊模式。但另一種方法是在應用程式級別實現服務發現:客戶的發現和自注冊模式。它需要的工作量更大,但它確實可以處理服務在多個部署平臺上執行的場景;
- 設計基於訊息的架構的一種好方法是使用訊息和通道模型,它抽象底層訊息系統的細節。然後,你可以將該設計對映到特定的訊息基礎結構,該基礎結構通常基於訊息代理;
- 使用訊息機制的一個關鍵挑戰是以原子化的方式同時完成資料庫更新和釋出訊息。一個好的解決方案是使用事務性發件箱模式,並首先將訊息作為資料庫事務的一部分寫入資料庫。然後,一個單獨的程式使用輪詢釋出者模式或事務日誌拖尾模式從資料庫中檢索資訊,並將其釋出給訊息代理。