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,可以結合業務場景來選取(對時延敏感的可以選用同步,對吞吐量敏感的可以選用異步)。
本文轉自架構師之路,作者:沈劍,轉自遊戲技術網:http://www.youxijishu.com/nd.jsp?id=127&_np=2_323
遊戲技術網公衆号,掃描加入讨論遊戲技術