c语言进程间通信架构,构建微服务之:微服务架构中的进程间通信
這是使用微服務架構構建應用系列的第三篇文章。第一篇文章介紹了微服務架構模式并討論了使用微服務的優勢和劣勢 ;第二篇文章介紹了應用的客戶端如何通過API網關作為中介實現服務間的通信;在這篇文章中我們將看一看同一系統間的服務如何通信;第四篇文章主要介紹服務發現的問題。
介紹
在傳統單體應用中,模塊間使用編程語言級別的方法或功能彼此調用。然而微服務架構應用本質上是運行在多臺機器上的分布式系統,每個服務都是一個進程!因此,下圖為我們展示,微服務必須使用進程間通信(IPC)的機制實現交互:
Paste_Image.png
稍后,我們將看具體的 IPC 技術實現,但首先讓我們探討不同方案設計中的問題。
交互風格
當我們為服務選擇一種IPC機制的時候,我們首先要考慮服務間如何交互,技術上存在多種 client?service 交互風格:它們可以按照兩大維度分類:第一維度是服務間交互是一對一還是一對多;
一對一:每個客戶端請求只會被一個服務實例處理。
一對多:每個請求將會被多個服務實例處理
第二個維度是交互是同步模式還是異步模式:
同步:客戶端期望來自服務端的及時響應,甚至可能阻塞并等待。
異步:客戶端等待響應時不會阻塞,對異步來講,及時響應并不是必須的。
下列表格展示了兩種方式的不同
一對一
一對多
同步
請求/響應
—
異步
通知
發布/訂閱
異步
請求/異步響應
發布/異步響應
有下面幾種一對一的交互方式:
請求/響應模式: 客戶端向服務端發送請求并等待響應,并期望服務端可以及時的返回響應。在一個基于線程的應用中,發出請求的線程可能在等待時阻塞線程的執行。
通知(也就是單向請求):客戶端往服務端發送請求,但并不等待響應返回
請求/異步響應:客戶端往一個異步返回響應的服務發送請求。客戶端等待式并不會阻塞線程,因為設計時就假設請求不會立即返回(js回調)
有下面幾種一對多的交互方式:
發布/訂閱模式:客戶端發布一個通知消息,消息將會被0或多個感興趣的服務消費
發布/異步響應:客戶端發布一個請求消息,并在一定時間內等待消費消息的服務響應。
每個服務通常會使用多種交互風格的組合:對一些服務來講,簡單的IPC機制可能已經足夠了,但另外一些服務可能需要幾種IPC機制的組合。下圖展示了在taxi-hailing應用中,當用戶請求行程時,服務是如何交互的:
Paste_Image.png
這個服務使用了通知、請求/響應、發布/訂閱風格的組合。比如,乘客使用智能手機向行程管理服務發送一個接送需求的通知,行程管理服務將使用請求/響應模式調用乘客服務來驗證乘客賬號是否為活動狀態,然后行程管理服務創建行程并使用發布/訂閱方式來通知諸如分發器(用來定位空閑司機)等服務。
我們已經討論了交互風格,那么再來看下如何定義API。
定義API
服務API是服務與客戶之間的契約。拋開選擇哪種IPC機制的選擇,使用一些接口定義語言interface definition language (IDL)準確定義服務API是很重要的!.當然,最好考慮使用API優先的方式來定義服務,通過先寫接口定義語言來開始開發,并與客戶端開發者(服務消費者)一起review你的設計,先對API定義進行迭代,再去實現這些服務。這樣做設計的話將會使你構建更加符合客戶需求的服務!
后續文章你將會發現,服務定義和你選擇哪種IPC機制息息相關,如果你是要消息機制,API就由消息頻道和消息類型組成;如果你使用http,API就是由URLs以及request/response格式組成。稍后我們將會討論更多關于接口定義語言的細節。
API進化
服務API將會不可避免的隨著時間進化,在傳統單體應用中,我們可以很直接的去修改服務并更新所有服務的調用者(refactor)。但是在基于微服務架構的應用中,哪怕服務API的其他消費者都是在一個應用中,去更新所有服務也是相當困難的。你通常不能強制讓所有的客戶端升級來保持和服務端升級維持步調一致,而且,你還可能會增量部署新服務使得新老服務同時運行,尋找一種處理此種情況的策略是很重要的。
你是如何根據更改的大小來處理服務API的變化的呢?一些變化很小,通常可以與之前版本做到向后兼容,比如,你為請求或相應添加了一個屬性;對此,設計服務時考慮服務和客戶消費者的魯棒性原則是很有必要的:使用就版本服務API的客戶端可以在新版本服務API下正常工作,服務端為客戶端缺失的屬性提供默認值,客戶端自動忽略額外添加的響應屬性。最后強調,注意使用IPC機制和定義消息格式使你的API可以簡單方便的進化!
當然,有時候我們不得不對API做一些較大的,不再兼容的變化,而我們這時候又不可能強制每個客戶端升級,因此我們的服務就要繼續支持運行一段時間的老版本API。如果使用http,我們可以在URL里嵌入服務版本,每個服務實例可能同時處理多個版本的服務,當然,你也可以選擇為每個服務版本部署單獨的服務實例。
處理局部故障
就像前面關于API網關文章提到的那樣:在分布式系統中總會有無時無刻的局部故障的風險。由于客戶端和服務在不同的進程中,服務可能由于掛掉或者維護原因而不能及時響應客戶端的請求,或者服務由于過載原因導致響應緩慢。
比如,讓我們考慮之前文章提到的Product details場景,假設推薦服務沒有響應了,一個簡單的客戶端實現可能無期限的等待服務響應并阻塞,這樣不僅導致糟糕的用戶體驗,在很多應用中還會消耗比如線程這樣寶貴的資源,最終就像下圖展示的那樣,運行時將會用盡所有線程使得服務不再響應任何請求:
Paste_Image.png
為解決此類問題,設計上處理局部故障是很有必要的。
Netflix給出了一些處理局部故障比較好的方法:
網絡超時:等待響應時不要一直阻塞,而是使用超時,超時能夠保證資源不會一直被占用
限制未完成請求的數量:針對一個請求某服務的客戶端,需要設置其未處理請求數量的上限,一旦超過限制就不再處理任何請求,這樣就做到快速失敗。
斷路器模式:跟蹤成功和失敗請求的數量,如果比率超過了設置的閥值,打開斷路器使得后續請求快速失敗。如果大量請求失敗,就建議服務為不可以狀態并決絕處理新請求,過一段時間之后,客戶端可以再次重試,一旦成功,關閉斷路器。
提供fallback機制:請求失敗時提供fallback,比如返回緩存值或者為失敗的推薦服務返回默認空集合作為默認值。
Netflix Hystrix是一個實現了這些模式的開源工具包,如果你使用JVM那么一定要考慮使用它!如果你的服務不是運行在JVM中,那也要考慮有等效的實現來處理此類問題。
IPC 技術
我們有不同的IPC技術可供選擇:服務可以使用基于請求/響應的同步通信模式,比如基于Http的REST或者Thrift,當然,也可以使用異步基于消息的通信模式,比如AMQP、STOMP。這些通信模式有不同的消息格式,服務可以使用基于文本格式、方便閱讀的JSON 或者 XML格式,也可以使用效率更高的二進制格式(比如Avro或Protocol Buffers)。稍后我們將討論同步IPC機制,現在我們先討論下異步的IPC機制:
異步,基于消息的通信
使用消息時,進程間通過異步交換消息來通信。一個客戶端通過發送消息的方式請求服務,如果期望服務有響應,也是服務通過向客戶端發送另外的消息來實現。由于通信是異步的,客戶端不會為了響應等待并阻塞,相反的,客戶端編程時就是以服務不會立即返回響應來處理的。
一條消息包含消息頭(元數據和發送者)和消息體,消息通過頻道進行交換,任意數量的消費者都可以往頻道中發消息,任意數量的消費者也可以消費頻道中的消息。有point?to?point和publish?subscribe兩種頻道:point?to?point模式下,頻道的消息只會被交付到某一個消費者,這種模式用于前面提到的一對一的交互;publish?subscribe 模式下,頻道的消息將會交付到所有感興趣的消費者,使用于前面提到的一對多交互風格。
下圖展示了taxi-hailing 應用可能是一publish-subscribe模式:
Paste_Image.png
行程管理服務通過向publish-subscribe頻道寫入trip create消息的方式通知比如分發器這樣感興趣的服務,分發器查找空閑司機并通過向publish-subscribe頻道寫入Driver Proposed消息通知其他服務。
有多種消息系統供我們選擇,當然我們盡量選擇一個支持多種編程語言的來使用。一些消息系統支持標準的協議比如 AMQP和STOMP,另一些消息系統有專有但是文檔化的協議,大量的開源消息系統可供我們挑選,包括RabbitMQ、Apache Kafka、Apache ActiveMQ和NSQ。統一的來看,他們都支持某種形式的消息和頻道,都致力于高可靠,高性能和高擴展性,但是每個消息中介在實現細節上還是有很大的不同:
使用消息系統有很多優點:
客戶端與服務端解耦: 客戶端只需要向合適的頻道發送消息就實現簡單的請求,客戶端完全感知不到服務實例的存在,因此不需要再去使用一套服務發現機制去決定服務實例的位置。
緩存消息:在同步的請求/響應協議,比如HTTP下,客戶端和服務端在交互的階段必須保證雙方都可用,然而,消息中介會把消息寫入隊列直到消息被消費者處理位置,這意味著,盡管 在訂單履行系統響應緩慢甚至不可用情況下,在線商城仍然可以接受來自客戶的訂單,只需要先把訂單消息簡單的入隊即可。
靈活的客戶-服務端交換風格,消息支持前面提到的所有交互風格。
顯示的進程間通信:基于 RPC的通信機制試圖使調用遠程服務等同于調用本地服務。然而,由于物理定律和局部故障的可能性,事實上他們相當不同。消息使這些差異非常明顯,因此開發人員不被虛假的安全感所迷惑。
當然消息系統也有缺點:
額外的運維復雜度:消息系統畢竟也是額外的系統組件,也要求安裝、配置、運維等操作,有必要保證消息系統的高可用,否則會影響整個系統的穩定性。
實現請求/響應交互的復雜度:要實現請求/響應的交互風格還是要做些額外工作的:每條請求消息必要要包含回復頻道的標志符以及關聯標志符 ,服務回寫包含關聯ID的消息到回復頻道,客戶端使用關聯ID去匹配請求對應的響應。當然,如果使用直接支持請求/響應的基于IPC機制的方式,將會特別簡單。
我們已經討論了基于消息的IPC,再看檢驗下基于請求/響應的IPC吧:
同步,基于請求/響應的IPC
當使用同步,基于請求/響應的IPC機制的時候,客戶端向服務端發送請求,服務端處理請求并返回響應,很多客戶端,發出請求的線程會在等待響應過程中阻塞,另外有一些客戶端也會使用異步、事件驅動的代碼,比如封裝好的Futures 或 Rx Observables。然而,和使用消息不一樣,客戶端假設請求會立即返回。有幾種方案供我們選擇,比較流行就是REST和 Thrift,我們先看下REST:
REST
限制使用REST風格暴露API很流行,REST基本就是使用HTTP的IPC 機制,REST的關鍵理念是資源,也就是通常代表諸如用戶或產品的某個或一組業務對象,REST使用HTTP verbs維護URL指向的資源,比如 GET返回某資源的表示,可能是XML也可能是JSON對象, POST會創建新資源,PUT更新資源··· 引用自Roy Fielding,提出REST的大牛:
“REST provides a set of architectural constraints that, when applied as a whole, emphasizes scalability of component interactions, generality of interfaces, independent deployment of components, and intermediary components to reduce interaction latency, enforce security, and encapsulate legacy systems.”
—Fielding, Architectural Styles and the Design of Network-based Software Architectures
下圖展示了**taxi-hailing **應用使用REST的場景:
Paste_Image.png
乘客向行程服務/trips發送POST請求,行程服務通過向乘客管理服務發送GET請求獲取乘客信息,在驗證完乘客授權之后,創建行程,行程服務創建行程后返回201響應給手機.
很多用了HTTP暴露服務API的開發就說自己是REST,其實按照 Fielding 在blog post描述的規定,他們根本不是REST。 Leonard Richardson (no relation)定義了非常有用的 maturity model for REST組成了下面幾個級別:
Level 0 :客戶端使用HTTP POST調用服務固定的URL,每次請求指定動作和參數
Level 1:支持資源的概念,請求通過POST,并且要制定要做的動作和參數
Level 2:充分使用 HTTP verbs 執行動作GET獲取資源 POST創建資源PUT更新資源,還是要請求參數和請求體,還可以指定請求的參數,使得服務充分使用web基礎架構的功能,比如緩存請求等
Level 3: API定義按照HATEOAS (Hypertext As The Engine Of Application State) 原則。基本的定義就是GET請求返回表示資源的body中包含一些對資源允許動作的鏈接。比如,客戶端可以使用get訂單返回的訂單body中的一個超鏈接取消一個訂單。HATEOAS 優點之一是:客戶端不用在代碼中硬編碼URL了,另外,由于返回的body中包含允許對資源所作動作的超鏈接,客戶端就不需要再猜測當前資源狀態下他可以做哪些操作了。
使用基于HTTP的協議的優點有:
HTTP 簡單而且大家都熟悉
可以用瀏覽器測試,配合比如Postman插件更佳,命令行curl也很方便(假設使用json或其他數據格式)
直接就支持請求/響應風格的通信
HTTP很友好
無需中介,簡化架構
使用HTTP的缺點:
HTTP只支持請求/響應風格的交互,你可以使用HTTP請求向服務器發送通知,但是服務器一定要返回HTTP響應。
客戶端和服務端沒有消息buffer機制,交互都是直接的,這就要求交換消息的時候雙方必須同時運行。
客戶端必須知道每個服務實例的地址,比如URL,正如前面的API網關文章描述的那樣,在現代流行的應用架構中,這已經不再是一個問題,我們可以使用服務發現機制來定位服務實例。
開發者論壇最近又重新發掘了RESTful API風格接口定義語言的價值,我們可以選擇使用RAML或者Swagger等工具, Swagger允許定義請求響應的消息格式,RAML則要求你使用額外的諸如JSON Schema這樣的定義.IDL除了描述API,通常還會提供根據接口定義生產客戶端Stub或服務端骨架的工具。
Thrift
Apache Thrift是REST的一個很有意思的替代品,它是一個實現跨語言客戶端與服務端RPC通信的框架。Thrift提供C語言風格的接口定義語言來定義API,你可以通過編譯生成客戶端Stub和服務端的骨架,編譯器可以為 C++、Java、Python、PHP、Ruby、Erlang、Node.js等不同語言生產代碼。
一個Thrift接口包含一個或多個服務,一個服務定義可以類比java的接口:都是一組強類型方法的集合。Thrift方法可以返回值也可以被定義為單向通信,如果方法需要返回值就需要實現請求/響應風格的交互,客戶端等待響應的時候可能會拋出異常;單向通信就是我們前面講到的通知風格的交互,服務端不需要返回響應。
Thrift支持不同的消息格式:JSON、binary以及compact binary。 Binary相對JSON更加高效,因為解碼速度更快,compact binary比JSON空間利用率高,見名知意嘛,JSON則對人和瀏覽器更加的友好 ;Thrift也支持不同的通信協議選擇:原生TCP或者HTTP,原生TCP相比HTTP肯定更加高效,但是HTTP對防火墻、人以及瀏覽器更加的友好。
消息格式
既然我們已經討論了HTTP和Thrift,現在再來探討下消息格式的問題吧:如果你需要消息系統或者REST風格交互,你就必須選擇消息格式。其他類似Thrift的IPC機制可能只支持一小部分的消息格式,甚至只會支持一種!在某些情況下,使用一種支持跨語言的消息格式非常重要,哪怕你現在只有一種語言實現微服務,誰又能保證你以后不會使用新的語言呢?
主要有文本和二進制兩種格式:文本格式包括JSON和XML等,文本格式不僅僅方便閱讀,而且是自描述的,JOSN中對象屬性是采用一組鍵值對的組合來表示的;同樣,XML的屬性是采用命名元素和值來表示的,這樣允許消費者只挑選感興趣的消息摒棄其他消息,因而這種方式也可以方便的做到向后兼容。
XML文檔的結構是由XML schema來指定的,隨著時間的流逝,開發者論壇逐步意識到JSON也需要類似的機制:一種選擇是使用JSON Schema,要么單獨使用,要么作為類似Swagger這種IDL的一部分使用。
文本格式消息的缺點是非常的冗長,尤其是XML格式:由于消息是自描述的,每條消息除了值之外還包含屬性的名稱,另一個缺點就是解析文本開銷略大,這時候可以考慮下二進制格式。
二進制格式也有多種選擇:如果使用Thrift,你可以選擇Thrift binary,如果選擇其他的消息格式,比較流行的還有Protocol Buffers和Apache Avro,兩種格式都提供了IDL來定義消息的結構。區別是,Protocol Buffers使用標記字段,而Avro 消費者則需要了解Schema才能解析消息,因此使用Protocol Buffers時,API進化比Avro更容易。這篇 文章是一個對Thrift、 Protocol Buffers以及 Avro非常好的比較。
總結
微服務需要使用進程間通信的機制進行交互,當設計你的服務如何通信的時候,需要考慮多個問題:服務如何交互、如何為服務定義API、如何處理API進化、如何處理局部故障。有兩種微服務可以使用的IPC機制:異步消息和同步的請求/響應。該系列的下一篇文章,將會講解微服務架構中的服務發現問題。
總結
以上是生活随笔為你收集整理的c语言进程间通信架构,构建微服务之:微服务架构中的进程间通信的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 怎么手动安装python 官方whl包、
- 下一篇: android tab 点击,TabLa