微服務(wù)之服務(wù)間通信
單體應(yīng)用向微服務(wù)拆分進化后,要解決的第一個問題就是進程間的通信問題。具體的實現(xiàn)方式有很多種但是通訊類型無外乎同步和異步兩大類。
進程通信的本質(zhì)是交換消息,而所謂的消息通常就是業(yè)務(wù)處理所需要的數(shù)據(jù),通信過程中消息格式至關(guān)重要。消息格式的選擇會對進程間通信的效率產(chǎn)生直接影響,同時也對通信雙方的API接口規(guī)范的制定起到了決定性要求。另外在考慮消息類型時還要考慮到跨語言場景,大部分情況下一個完整的微服務(wù)系統(tǒng)都不會是只有一種語言構(gòu)建而成的,一般大廠都會橫跨Java、Python、Go、C等多種語言。
通信類型
同步
HTTP REST
RPC
好處 | 弊端 | |
REST | 它非常簡單,并且大家都很熟悉 | 可能會導(dǎo)致可用性降低。通信過程需要客戶端和服務(wù)端雙方同時在線,任何一段故障通信均將失敗。 |
可測試很高,可以通過瀏覽器或者curl命令來測試。 | 客戶端必須知道服務(wù)端的具體位置(URL) | |
直接支持請求/響應(yīng)方式的通信 | 只支持請求/響應(yīng)方式的通信 | |
HTTP對防火墻友好。對于一些路由網(wǎng)關(guān)類的負載配置,能夠高效靈活的支持其具體實現(xiàn)。 | 單個請求中獲取多個資源時,對設(shè)計具有挑戰(zhàn)性 | |
不需要中間代理,簡化了系統(tǒng)架構(gòu) | 有時很難講多個更新操作映射到HTTP動詞。 | |
RPC | 設(shè)計具有復(fù)雜更新操作的API非常簡單 | 對腳本類的語言支持不好 |
具有高效、緊湊的進程間通信機制,尤其是在交換大量消息時。 | 需要有配套的路由負載和安全策略(大部分RPC框架其實已經(jīng)具備了解決模塊) | |
支持在遠程調(diào)用和消息傳遞過程中使用雙向流失消息方式。 | ||
它實現(xiàn)了客戶端和用各種語言編寫服務(wù)端之間的互操作性。(跨語言) |
異步
RocketMQ
kafka
通過使用異步消息通信的方式能夠很好的實現(xiàn)微服務(wù)的解耦,異步消息傳遞通常有兩種模式,一種是:C端直接將消息發(fā)送給P端即可不需要得到恢復(fù),當需要P端恢復(fù)時由于是異步通信,C端也不會阻塞,P端處理完后再根據(jù)接收到的信息向C端回復(fù)處理結(jié)果。這種方式也叫無代理模式,比較小眾主要適用于一些異步通知場景。另外一種就是:使用消息代理,也就是我們常說的消息中間件MQ了,使用場景比較廣泛,行業(yè)內(nèi)有很多成熟方案。
消息主要由消息頭和消息體構(gòu)成,按照不同使用場景可以細分為文檔類型、命令類型和事件類型:
結(jié)構(gòu)
消息頭:消息頭中可以細分出標題,即名稱與值對的集合,用于描述正在發(fā)送的數(shù)據(jù)的源數(shù)據(jù)。除了消息發(fā)送者提供的名稱與值對之外,小系統(tǒng)還包含其他信息,例如發(fā)件人或消息傳遞基礎(chǔ)設(shè)施生成的唯一消息ID,以及可選的返回地址,該地址指定發(fā)送回復(fù)的消息通道。
消息體:消息正文是以文本或二進制格式發(fā)送的數(shù)據(jù)。
類型
文檔:僅包含數(shù)據(jù)的通用信息。接受者決定如何解釋它。對命令式消息的回復(fù)是文檔消息的一種使用場景。
命令:一條等同于RPC請求的消息。它指定要調(diào)用的操作及其參數(shù)。
事件:表示發(fā)送方這一端發(fā)生了重要的事件。事件通常是領(lǐng)域事件,表示領(lǐng)域?qū)ο蟮臓顟B(tài)更改。
使用消息代理的一個重要好處就是發(fā)送方不需要知道接收方的網(wǎng)絡(luò)位置,另一個好處是消息代理緩沖消息,知道接收方能夠處理它。
好處 | 壞處 |
松耦合 | 潛在性能瓶頸 |
自帶消息緩存機制 | 潛在單點故障 |
靈活通信 | 額外的操作復(fù)雜性 |
中間件選型:需要關(guān)注以下幾點
支持的編程語言(跨語言):選擇的中間件應(yīng)該支持盡可能多的編程語言。
支持的消息標準:中間件是否支持多種消息標準,比如AMQP和STOMP,還是只支持專用的消息標準。
消息排序:是否能夠保證消息的處理順序
投遞保障:是否有消息丟失保障機制
持久性:是否支持持久化到磁盤,并且能夠在服務(wù)崩潰時有較高的數(shù)據(jù)恢復(fù)處理機制
耐久性(偏移量):接收方重新連接到中間件時,是否會收到斷開連接時發(fā)送的消息。
可擴展性:擴展成本如何
延遲:端到端是否有較大延遲
競爭性接收方(并發(fā)):是否支持客戶端并發(fā)接收
每種中間件都有不同的側(cè)重點,通常消息傳遞的順序性、可擴展性、投遞保障是必不可少的參考標準。基于異步消息模式設(shè)計微服務(wù)架構(gòu)無疑是一種很好的方法,它抽象底層消息系統(tǒng)的細節(jié)。使用消息機制的一個關(guān)鍵挑戰(zhàn)是以原子化的方式同時完成數(shù)據(jù)庫更新和發(fā)布消息,即事物的控制。一個好的解決方案是使用事務(wù)性發(fā)件箱模式,并首先將消息作為數(shù)據(jù)庫事物的一部分寫入數(shù)據(jù)庫。然后,通過一個單獨的進程輪詢發(fā)布者模式或事物日志拖尾模式從數(shù)據(jù)庫中檢索消息,并將其發(fā)布給中間件。
通信格式
消息的格式可以分為兩大類:文本和二進制。文本類消息的可讀性較高、兼容性較好,適合業(yè)務(wù)系統(tǒng)之間傳遞消息,例如JSON、XML等;二進制類消息傳輸性能較高,可讀性較和兼容性較差,適合一些Paas層的組件使用。
可讀性格式 JSON、XML等
高效格式avro、protocol Buffers
使用基于文本格式消息的弊端主要是消息往往過度冗長,特別是XML。消息的每一次傳遞都必須反復(fù)包含除了值以外的屬性名稱,這樣會造成額外的開銷。另外一個弊端是解析文本引入的額外開銷,尤其是消息較大的時候。因此,在對效率和性能敏感的場景下,優(yōu)先選擇二進制格式消息。
服務(wù)發(fā)現(xiàn)
微服務(wù)的一大特點就是客戶端和服務(wù)端要有靈敏的服務(wù)發(fā)現(xiàn)機制。即,服務(wù)端要將自己在網(wǎng)絡(luò)中的位置暴漏給客戶端,由于微服務(wù)自帶自動擴展、故障和升級的動態(tài)改變特性,客戶端必須要動態(tài)、及時的感知到服務(wù)端地址變化。
我們可以進一步將服務(wù)發(fā)現(xiàn)抽象為服務(wù)注冊信息存儲數(shù)據(jù)庫,該庫包含一張服務(wù)實例網(wǎng)絡(luò)位置信息存儲記錄表。
實際服務(wù)發(fā)現(xiàn)的核心思路其實就一種,那就是注冊中心。至于怎么實現(xiàn)這個注冊中心就仁者見仁智者見智了,比如可以通過一張簡單的數(shù)據(jù)表來存儲,服務(wù)端向數(shù)據(jù)表更新地址信息,客戶端通過查詢數(shù)據(jù)表后再自行實現(xiàn)負載請求。亦或是直接通過成熟的中間件完成,例如SpringCloud默認的Eureka或dubbo默認的zookeeper等。還可以直通通過Nginx實現(xiàn),將負載部分下沉到組件,客戶端不需要自助實現(xiàn)負載等。

當然這只是服務(wù)發(fā)現(xiàn)最核心的實現(xiàn)原理,其具體實施還需要考慮多種元素,比如心跳探活、通訊協(xié)議、緩存設(shè)計、跨語言支撐,甚至有些中間件把服務(wù)發(fā)現(xiàn)只作為分布式服務(wù)通信中的一個模塊,與負載均衡、流量路由一并封裝實現(xiàn)。在微服務(wù)設(shè)計階段服務(wù)發(fā)現(xiàn)雖然很重要但是不能作為我們強依賴的環(huán)節(jié),這點需要特別注意。注冊信息可以在應(yīng)用服務(wù)啟動時初始化,也可以定期同步甚至是被動接收信息更新,但不可作為強依賴項,要解耦出來。
通信故障
在分布式系統(tǒng)中,當服務(wù)試圖向另一個服務(wù)發(fā)送同步請求時,永遠都面臨著局部故障的風險。通常有三大類異常需要考慮:
超時:超時最大的潛在風險在于,很大可能由于下游系統(tǒng)的問題,交易持續(xù)耗時耗盡上游系統(tǒng)資源造成雪崩。所以在等待針對請求的響應(yīng)時,一定不要做成無限阻塞,而是要設(shè)定一個超時閾值。
流量控制:流量控制包括限流和路由兩類
限流:服務(wù)端要具備限流能力,這要可以有效的保護自身和下游系統(tǒng)。通用的限流計算方法有漏斗、令牌、計數(shù)器、滑動窗口等方法,根據(jù)不同的業(yè)務(wù)需要可以選擇不同的限流計算方法。限流的實現(xiàn)方式也分很多種,可以做成單節(jié)點限流或分布式限流。如果是單節(jié)點限流可以通過一些第三方類庫實現(xiàn),比如guava的limit包;如果是分布式限流需要引入一個可以控制集中事務(wù)的組件,例如Redis、MySQL等。當然限流的維度也可以細分為接口級別、方法級別甚至是業(yè)務(wù)維度組合級別,需要根據(jù)具體的應(yīng)用場景決定。
路由:路由是在處理流量不均或者AB測試、紅藍發(fā)布等場景的有效解決方案,對于系統(tǒng)也可以起到一定的保護。具體實現(xiàn)方式可以通過引入組件(比如:Nginx)或根據(jù)自身場景自研,自研成本也不會太高。
熔斷:實際上就是一種快速失敗的手段,在一些大促場景通常被當做兜底方案使用。熔斷雖然會對業(yè)務(wù)有損但是它能保住一部分交易,不至于整個系統(tǒng)都無法對外提供服務(wù)。
以上三類異常場景均需要有配到的服務(wù)治理能力支撐才行,比如異常流量發(fā)現(xiàn)機制、自動限流、自動熔斷等操作機制。同時異常處理完成后還要具備自動恢復(fù)的能力。故障的處理通常是本著快速止血、定位問題(需要有一些故障現(xiàn)場的日志支撐才行)、自動恢復(fù)這三個自動運維的原則設(shè)計。
總結(jié)
設(shè)計系統(tǒng)時是要通過同步方式還是要通過異步方式來實現(xiàn)通信其實從不同的角度看會有不同的答案,此處我只給我我所經(jīng)歷的兩個項目經(jīng)驗。
第一個就是我們要做一個支持18W TPS的高并發(fā)系統(tǒng),這個系統(tǒng)最大的特定就是流量大,業(yè)務(wù)連續(xù)性要求高(金融行業(yè)業(yè)務(wù)連續(xù)性L5的要求,RTO<=2分鐘 & RPO≈0,系統(tǒng)可用性99.99%,計劃外全年系統(tǒng)故障不得超過5分鐘),同時業(yè)務(wù)場景相對簡單。針對以上特征我們最后采用的是RPC同步通信方式,因為這種方式有一點常被大家忽略的好處就是可以橫向擴容,對資源就可以提升并發(fā)量。我們在設(shè)計之初又特意避開所有強依賴項,整個核心交易鏈路理論上是可以無限擴容下去的。
另外一個項目也是一個金融跨境項目,但是這個項目對TPS要求較低,日常100峰值800即可。但這個項目業(yè)務(wù)上過于復(fù)雜,拆分后的微服務(wù)數(shù)量有50個左右,這還是沒有上線前大家根據(jù)業(yè)務(wù)功能完成的拆分項,后續(xù)肯定還會增加。由于拆分服務(wù)過多、業(yè)務(wù)復(fù)雜、TPS要求較低,針對這三個特點我們最后通過MQ異步實現(xiàn)通信。
歸納下就是MQ異步通信會成為我們系統(tǒng)吞吐量的瓶頸,而且還是強依賴的這種。根據(jù)不同的場景可以按照下面列出的指標作為選型參考:(不絕對只作為參考,很多時候都是需要衡量取舍的)
同步 | 異步 | |
吞吐量高要求系統(tǒng) | ? | |
業(yè)務(wù)復(fù)雜且多變 | ? | |
業(yè)務(wù)連續(xù)性高要求系統(tǒng) | ? | |
解耦性要求高的系統(tǒng) | ? |
