第一章聊了【“爲什麼要進行服務化,服務化究竟解決什麼問題”】
第二章聊了【“微服務的服務粒度選型”】
第三章聊了【“爲什麼說要搞定微服務架構,先搞定RPC框架?”】
上一章聊了【“微服務架構之RPC-client序列化細節”】
通過上篇文章的介紹,知道了要實施微服務,首先要搞定RPC框架,RPC框架分爲客戶端部分與服務端部分。
RPC-client的部分又分爲:
(1)序列化反序列化的部分(上圖中的1、4)
(2)發送字節流與接收字節流的部分(上圖中的2、3)
前一篇文章討論了序列化與範序列化的細節,這一篇文章將討論發送字節流與接收字節流的部分。
客戶端調用又分爲同步調用與異步調用
同步調用的代碼片段爲:
Result = Add(Obj1, Obj2);// 得到Result之前處於阻塞狀態
異步調用的代碼片段爲:
Add(Obj1, Obj2, callback);// 調用後直接返回,不等結果
處理結果通過回調得到:
callback(Result){// 得到處理結果後會調用這個回調函數
…
}
這兩個調用方式,RPC-client裏,處理方式也不一樣,下文逐一敘述。
RPC-client同步調用
所謂同步調用,在得到結果之前,一直處於阻塞狀態,會一直佔用一個工作線程,上圖簡單的說明了一下組件、交互、流程步驟。
上圖中的左邊大框,就代表了調用方的一個工作線程。
左邊粉色中框,代表了RPC-client組件。
右邊橙色框,代表了RPC-server。
藍色兩個小框,代表了同步RPC-client兩個核心組件,序列化組件與連接池組件。
白色的流程小框,以及箭頭序號1-10,代表整個工作線程的串行執行步驟:
1)業務代碼發起RPC調用,Result=Add(Obj1,Obj2)
2)序列化組件,將對象調用序列化成二進制字節流,可理解爲一個待發送的包packet1
3)通過連接池組件拿到一個可用的連接connection
4)通過連接connection將包packet1發送給RPC-server
5)發送包在網絡傳輸,發給RPC-server
6)響應包在網絡傳輸,發回給RPC-client
7)通過連接connection從RPC-server收取響應包packet2
8)通過連接池組件,將conneciont放回連接池
9)序列化組件,將packet2範序列化爲Result對象返回給調用方
10)業務代碼獲取Result結果,工作線程繼續往下走
RPC框架需要支持負載均衡、故障轉移、發送超時,這些特性都是通過連接池組件去實現的。
連接池組件
典型連接池組件對外提供的接口爲:
int ConnectionPool::init(…);
Connection ConnectionPool::getConnection();
intConnectionPool::putConnection(Connection t);
【INIT】
和下游RPC-server(一般是一個集羣),建立N個tcp長連接,即所謂的連接“池”
【getConnection】
從連接“池”中拿一個連接,加鎖(置一個標誌位),返回給調用方
【putConnection】
將一個分配出去的連接放回連接“池”中,解鎖(也是置一個標誌位)
如何實現負載均衡?
回答:連接池中建立了與一個RPC-server集羣的連接,連接池在返回連接的時候,需要具備隨機性。
如何實現故障轉移?
回答:連接池中建立了與一個RPC-server集羣的連接,當連接池發現某一個機器的連接異常後,需要將這個機器的連接排除掉,返回正常的連接,在機器恢復後,再將連接加回來。
如何實現發送超時?
回答:因爲是同步阻塞調用,拿到一個連接後,使用帶超時的send/recv即可實現帶超時的發送和接收。
總的來說,同步的RPC-client的實現是相對比較容易的,序列化組件、連接池組件配合多工作線程數,就能夠實現。還有一個問題,就是【“工作線程數設置多少最爲合適?”】,這個問題在之前的文章中討論過,此處不再深究。
RPC-client異步回調
所謂異步回調,在得到結果之前,不會處於阻塞狀態,理論上任何時間都沒有任何線程處於阻塞狀態,因此異步回調的模型,理論上只需要很少的工作線程與服務連接就能夠達到很高的吞吐量。
上圖中左邊的框框,是少量工作線程(少數幾個就行了)進行調用與回調。
中間粉色的框框,代表了RPC-client組件。
右邊橙色框,代表了RPC-server。
藍色六個小框,代表了異步RPC-client六個核心組件:上下文管理器,超時管理器,序列化組件,下游收發隊列,下游收發線程,連接池組件。
白色的流程小框,以及箭頭序號1-17,代表整個工作線程的串行執行步驟:
1)業務代碼發起異步RPC調用,Add(Obj1,Obj2, callback)
2)上下文管理器,將請求,回調,上下文存儲起來
3)序列化組件,將對象調用序列化成二進制字節流,可理解爲一個待發送的包packet1
4)下游收發隊列,將報文放入“待發送隊列”,此時調用返回,不會阻塞工作線程
5)下游收發線程,將報文從“待發送隊列”中取出,通過連接池組件拿到一個可用的連接connection
6)通過連接connection將包packet1發送給RPC-server
7)發送包在網絡傳輸,發給RPC-server
8)響應包在網絡傳輸,發回給RPC-client
9)通過連接connection從RPC-server收取響應包packet2
10)下游收發線程,將報文放入“已接受隊列”,通過連接池組件,將conneciont放回連接池
11)下游收發隊列裏,報文被取出,此時回調將要開始,不會阻塞工作線程
12)序列化組件,將packet2範序列化爲Result對象
13)上下文管理器,將結果,回調,上下文取出
14)通過callback回調業務代碼,返回Result結果,工作線程繼續往下走
如果請求長時間不返回,處理流程是:
15)上下文管理器,請求長時間沒有返回
16)超時管理器拿到超時的上下文
17)通過timeout_cb回調業務代碼,工作線程繼續往下走
上下文管理器
爲什麼需要上下文管理器?
回答:由於請求包的發送,響應包的回調都是異步的,甚至不在同一個工作線程中完成,需要一個組件來記錄一個請求的上下文,把請求-響應-回調等一些信息匹配起來。
如何將請求-響應-回調這些信息匹配起來?
這是一個很有意思的問題,通過一條連接往下游服務發送了a,b,c三個請求包,異步的收到了x,y,z三個響應包:
(1)怎麼知道哪個請求包與哪個響應包對應?
(2)怎麼知道哪個響應包與哪個回調函數對應?
回答:這是通過【請求id】來實現請求-響應-回調的串聯的。
整個處理流程如上,通過請求id,上下文管理器來對應請求-響應-callback之間的映射關係:
1)生成請求id
2)生成請求上下文context,上下文中包含發送時間time,回調函數callback等信息
3)上下文管理器記錄req-id與上下文context的映射關係,
4)將req-id打在請求包裏發給RPC-server
5)RPC-server將req-id打在響應包裏返回
6)由響應包中的req-id,通過上下文管理器找到原來的上下文context
7)從上下文context中拿到回調函數callback
8)callback將Result帶回,推動業務的進一步執行
如何實現負載均衡,故障轉移?
回答:與同步的連接池思路相同。不同在於,同步連接池使用阻塞方式收發,需要與一個服務的一個ip建立多條連接,異步收發,一個服務的一個ip只需要建立少量的連接(例如,一條tcp連接)。
如何實現超時發送與接收?
回答:同步阻塞發送,可以直接使用帶超時的send/recv來實現,異步非阻塞的nio的網絡報文收發,如何實現超時接收呢?(由於連接不會一直等待回包,那如何知曉超時呢?)這時,超時管理器就上場啦。
超時管理器
超時管理器,用於實現請求回包超時回調處理。
每一個請求發送給下游RPC-server,會在上下文管理器中保存req-id與上下文的信息,上下文中保存了請求很多相關信息,例如req-id,回包回調,超時回調,發送時間等。
超時管理器啓動timer對上下文管理器中的context進行掃描,看上下文中請求發送時間是否過長,如果過長,就不再等待回包,直接超時回調,推動業務流程繼續往下走,並將上下文刪除掉。
如果超時回調執行後,正常的回包又到達,通過req-id在上下文管理器裏找不到上下文,就直接將請求丟棄(因爲已經超時處理過了)。
however,異步回調和同步回調相比,除了序列化組件和連接池組件,會多出上下文管理器,超時管理器,下游收發隊列,下游收發線程等組件,並且對調用方的調用習慣有影響(同步->回調)。異步回調能提高系統整體的吞吐量,具體使用哪種方式實現RPC-client,可以結合業務場景來選取(對時延敏感的可以選用同步,對吞吐量敏感的可以選用異步)。
末了,通過最近幾篇RPC框架細節的文章閱讀量來看,貌似大夥對細節不是特別感興趣,後續文章就不再延續這個系列啦。
==【完】==
【文章轉載自微信公衆號“架構師之路”】