微服務(wù):服務(wù)間如何通信?
在微服務(wù)架構(gòu)中,會(huì)將一個(gè)完整的應(yīng)用程序拆分成一組服務(wù)。這些服務(wù)之間需要經(jīng)過協(xié)作,通過接口調(diào)用,才能組成一個(gè)完整的應(yīng)用。
不同的服務(wù)部署在不同的機(jī)器上,或者同一個(gè)機(jī)器的多個(gè)容器中,進(jìn)程間進(jìn)行通信就不可避免了,也變得非常重要。
按種類來分,進(jìn)程間的通信方式有很多種,比如遠(yuǎn)程過程調(diào)用的 RESTful API 和 gRPC 、基于消息機(jī)制的異步方式等。
- RESTful API :現(xiàn)在前后端分離比較流行,RESTful API 大家應(yīng)該都比較熟悉。REST 是一種使用 HTTP 協(xié)議的進(jìn)程間通信機(jī)制,一般使用 Json 來傳遞數(shù)據(jù);
- gRPC :是一個(gè)高性能、開源和通用的 RPC 框架,基于 ProtoBuf ( Protocol Buffers ) 序列化協(xié)議開發(fā),支持眾多開發(fā)語言。面向服務(wù)端和移動(dòng)端,基于 HTTP/2 設(shè)計(jì),帶來諸如雙向流、流控、頭部壓縮、單 TCP 連接上的多復(fù)用請求等特性;
- 異步消息:使用消息中間件來實(shí)現(xiàn),比如 RabbitMQ、Kafka 等。
按照交互方式來分,會(huì)有同步、異步。
- 同步:客戶端向服務(wù)端發(fā)起請求、等待服務(wù)端響應(yīng),等待的過程會(huì)造成阻塞;
- 異步:客戶端向服務(wù)端發(fā)起請求,服務(wù)端立即響應(yīng),不會(huì)造成阻塞,比如說消息隊(duì)列的發(fā)布、訂閱方式。
這里有幾個(gè)概念需要統(tǒng)一下語言:接口、客戶端、服務(wù)端
- 接口:如果使用的是消息機(jī)制,那么接口就是由消息通道、類型和消息格式組成的;如果是基于 HTTP ,則是由 URL、HTTP 動(dòng)詞和請求響應(yīng)的格式來組成;當(dāng)然,也可以是提供一組方法的類;
- 客戶端、服務(wù)端:說起客戶端,第一印象容易想到瀏覽器、移動(dòng) APP ,這里的客戶端是指在調(diào)用和被調(diào)用的調(diào)用方,服務(wù)端就是被調(diào)用方。而接口的定義是由服務(wù)端來進(jìn)行定義。
在前后端分離之前的單體應(yīng)用中,當(dāng)接口方法有變化時(shí),進(jìn)行代碼編譯就知道哪些地方需要調(diào)整,或者在 IDE 中進(jìn)行接口方法引用的查找,也能很容易處理。
在微服務(wù)中,不同的服務(wù)可能是不同的團(tuán)隊(duì)來進(jìn)行開發(fā),服務(wù)端接口的修改、滾動(dòng)發(fā)布等都需要有很好的兼容性和可用性。
一種方式是在接口中向下兼容,但時(shí)間越長代碼就會(huì)變得越復(fù)雜,比如:
- 添加一些控制性的參數(shù)。
- 接口方法新添加的參數(shù)需要給默認(rèn)值。
- 返回值可能也需要進(jìn)行特殊處理。
另一種方式就是不向下兼容,所有的客戶端需要進(jìn)行代碼的調(diào)整來適應(yīng)新的接口。在客戶端代碼還沒有完全調(diào)整完之前,新老接口需要共存,共存有兩種方式:
- 使用 URL 地址中添加版本號(hào),比如:/api/v1/xxx , /api/v2/xxx 。
- 在請求頭或消息體中添加版本號(hào),接口方法中根據(jù)版本號(hào)來進(jìn)行適配,調(diào)用對應(yīng)版本的邏輯然后返回給客戶端。
所以,一個(gè)設(shè)計(jì)良好的接口可以在暴露有用功能的同時(shí)隱藏實(shí)現(xiàn)細(xì)節(jié),對于細(xì)節(jié),可以進(jìn)行擴(kuò)展,修改,并不會(huì)影響到客戶端的調(diào)用,這就要求在接口設(shè)計(jì)之前,需要先進(jìn)行定義,經(jīng)過多輪評審后再進(jìn)行編碼實(shí)現(xiàn)。
好的設(shè)計(jì)自帶防腐層。
因?yàn)榭蛻舳撕头?wù)端是互相獨(dú)立的,服務(wù)端有時(shí)在特定時(shí)間內(nèi)無法完全響應(yīng)客戶端的請求,可能是自己本身的故障,也可能是超過了負(fù)載。這時(shí),客戶端請求就會(huì)被阻塞,無限地等待。
有幾種方式可以來解決這個(gè)問題:
- 設(shè)置超時(shí):在等待請求響應(yīng)時(shí),不要無限阻塞,設(shè)置一個(gè)超時(shí)時(shí)間,超過時(shí)間就返回。
- 根據(jù)負(fù)載能力,限制客戶端請求的數(shù)量,超過上限,后面的請求直接返回失敗。
- 對客戶端請求進(jìn)行監(jiān)控,如果失敗的比例超過閾值,就進(jìn)行熔斷,讓調(diào)用立即失敗。
現(xiàn)在有一些成熟的框架可以方便進(jìn)行熔斷的處理,比如:.NET 中的 Polly、Spring Cloud 中的 Sentinel、Hystrix 。
在傳統(tǒng)軟件中,經(jīng)常使用環(huán)境變量和配置文件來進(jìn)行靜態(tài)地址的配置,而部署在云端的分布式微服務(wù)程序中,地址是動(dòng)態(tài)的,那客戶端怎么能找到這些地址呢?這就需要用到服務(wù)發(fā)現(xiàn)。
服務(wù)發(fā)現(xiàn)就是客戶端不再依賴一個(gè)靜態(tài)的固定地址去尋找服務(wù)端,而是根據(jù)一個(gè)路由名稱在服務(wù)注冊表去尋找服務(wù)端地址,服務(wù)端部署后會(huì)將地址寫入服務(wù)注冊表。
在微服務(wù)框架中,也有相關(guān)的框架來實(shí)現(xiàn)服務(wù)發(fā)現(xiàn),比如:.NET 中的 consul 、Spring Cloud 中的 Eureka、Nacos 等。
對于實(shí)時(shí)性要求不高的場景,可以采用異步消息的方式來實(shí)現(xiàn)。比如刪除數(shù)據(jù)時(shí),需要?jiǎng)h除數(shù)據(jù)中對應(yīng)的附件信息、各種操作的日志記錄、流程流轉(zhuǎn)中需要發(fā)送消息通知等。
使用異步消息有下面幾個(gè)好處:
- 不需要知道是接收方的地址,只需要將消息發(fā)出去就行,發(fā)送方和接收方充分解耦。
- 消息的消費(fèi)者可以是一個(gè),也可以是多個(gè),當(dāng)處理速度不夠時(shí),可以橫向擴(kuò)展多個(gè)消費(fèi)者來進(jìn)行處理。
- 消息中間件在發(fā)送方和接收方中間起到一個(gè)緩沖的作用。
現(xiàn)在流行的開源中間件有 RabbitMQ、ActiveMQ、RokcetMQ、Kafka 等,選擇這些中間件時(shí)需要考慮:
- 支持的編程語言。
- 支持的消息標(biāo)準(zhǔn);。
- 是否支持持久化?
- 延遲是否在接受范圍之內(nèi)?
- 消息在處理時(shí)能否保持順序?
很多工作流引擎使用的是消息驅(qū)動(dòng)機(jī)制,流程在流轉(zhuǎn)過程中需要保證消息是順序處理的,否則流程數(shù)據(jù)可能出現(xiàn)錯(cuò)亂,如何在保證消息順序處理的情況下又能橫向進(jìn)行擴(kuò)展,這是一個(gè)挑戰(zhàn)。在 Kafka 中可以使用分片的方式進(jìn)行解決。
上面介紹的是服務(wù)間通信的一些常用方式,了解了基本邏輯,在具體實(shí)踐時(shí),無論是使用 .NET 技術(shù)棧還是 Java 技術(shù)棧來做微服務(wù),就都不是什么難事了。