OB君:在單機環境下,程式之間的通訊主要通過IPC來實現。但是在不同的機器上,RPC就成為了進行通訊的經典方式。本文由OceanBase的90後技術專家符風向大家娓娓道來RPC框架背後的What、Why和How,並對目前主流的C++ RPC框架進行對比分析。
本文作者:符風
現任螞蟻金服OceanBase團隊技術專家,2012年畢業後加入Oceanbase團隊,主要負責OceanBase基礎庫的建設工作。
早期的計算機程式都是單機程式,一個程式只會在一臺機器上跑。如果一臺機器上的A程式和B程式,它們之間需要通訊交流,就只能通過IPC(Inter-Process Communication)的方式。典型的就有管道、訊號量、共享記憶體等方式。但是如果這兩個程式分別執行在不同的機器上,那麼使用IPC就不夠了,還需要把網路通訊這個功能加進來。其中,RPC就是在不同機器上進行通訊的經典方式。
什麼是RPC?
說白了,RPC就是一種網路上程式之間的溝通方式。稍微正式一點的定義我們可以參考維基百科上查詢到的結果:
RPC(Remote Procedure Call)或者叫做遠端過程呼叫是一個計算機通訊協議。該協議允許執行於一臺計算機的程式呼叫另一臺計算機的程式,而程式設計師無需額外地為這個互動作用程式設計。
—— 維基百科
從維基百科的介紹中,我們可以總結出以下幾個關鍵字:通訊協議,跨計算機呼叫,易程式設計。
- RPC必須要有自己的通訊協議,協議包括序列化協議和網路協議,典型的有JSON、XML等等
- 因為要跨計算機呼叫,所以RPC必須要有自己的網路傳輸層,典型的有TCP/IP等等
- RPC都是封裝成函式呼叫的方式,使用RPC時就像呼叫一個函式那樣就可以了
為什麼用RPC?
網路上程式進行通訊的方式有很多,比如像我們瀏覽網頁使用的HTTP協議,為什麼不是直接發個HTTP請求進行通訊呢?使用RPC進行封裝的通訊方式有什麼優勢呢?最大的好處就是易用性。
如果沒有RPC的封裝,一臺計算機上的程式要訪問另外一臺機器上程式的內容需要怎麼做呢?
1. 以一種對方能夠理解的協議構建請求包
2. 將這個請求包設法傳送到對方機器的程式上
3. 對方機器根據協議理解請求包的內容
4. 對方機器處理請求,並以客戶機能夠理解的協議構造回覆包
5. 想辦法把這個回覆包傳送到請求包來源的遠端機器上的具體程式,並設法把請求包和回覆包關聯起來
我們可以看到,這是一個比較複雜並且煩人的工作,單單是如果把訊息從一臺機器傳送到另外一臺機器就涉及很多問題:比如怎麼進行傳輸、如何通知程式等等。何況還要處理網路上的各種異常情況:超時、斷連結、網路抖動等。
如果使用RPC進行通訊需要幾個步驟的?最簡單的情況就是直接呼叫相應函式就可以了,由RPC框架把上面的這五部操作都完成了。
List employees;
RPC.getEmployeeList(dep_id, employees);
複製程式碼
是不是感覺這個世界都變得清爽了?
典型的RPC框架介於傳輸層和應用中間,它會幫助處理:
- 可靠性
比如傳輸層遇到的錯誤。 - 平臺無關性
比如Windows平臺是否可以和Linux平臺進行通訊?64位的系統是否可以和32位系統進行通訊? - 服務發現和路由選擇
RPC呼叫實際上是對某個服務的呼叫,那麼RPC框架需要解決具體呼叫需要落到哪臺機器的哪個程式上。 - 訊息分發
一般一個程式上會提供多種RPC呼叫,RPC框架需要提供區別不同型別RPC訊息並轉到相應處理函式上。 - 安全性
RPC框架的設計
RPC框架的核心部分有以下幾個:
- RPC介面
- 物件序列化和反序列化
- 傳輸協議和傳輸層
- RPC訊息分發
RPC介面
RPC的作用就是讓使用者呼叫遠端請求就像呼叫本地函式,所以不管是本地客戶端還是遠端服務端需要使用一套統一的介面,然後兩邊分別實現自己的邏輯。
舉個例子:
比如在設計一個獲取部門員工列表的RPC請求,介面可能如下設計,傳入一個部門ID,返回部門成員的列表:
void getEmployeeList(const DepartmentID &id, EmployeeList &list);
複製程式碼
然後在客戶端部分根據介面可以這樣實現:
// clientvoid getEmployeeList(const DepartmentID &id, EmployeeList &list){
auto result = Transport.send(GET_EMPLOYEE_LIST, encode(id));
list.decode(result.buffer());}
複製程式碼
服務端部分同樣根據介面實現具體邏輯:
// servervoid getEmployeeList(const DepartmentID &id, EmployeeList &list){
auto dep = department_map[id];
list = dep.get_employees();}
複製程式碼
介面的作用就是:告訴客戶端你只能這樣呼叫這個RPC,同時告訴服務端客戶端那邊只會這樣呼叫。
上面三段程式碼中,具體RPC實現者只需要關心介面和服務端的實現邏輯。所以一般RPC框架會提供友好的封裝,最簡單的形式就是隻需要實現一個函式,函式的簽名就是介面,實現就是服務端的程式碼,客戶端的程式碼則由框架自動填充。
物件序列化和反序列化
物件序列化的方式有很多種,比如比較通用的JSON、Protocol Buffers等,也可以按照自己的需求自己定製序列化和反序列化方式。
OceanBase就是設計了自己的物件序列化方式,以滿足使用過程中對高效能、少資源佔用和易用性各方面的權衡。它相比protobuf可以擁有更高的序列化效能和更小的空間佔用,並且和OceanBase的資料結構深度結合,不用像protobuf那樣使用自己的DSL語言定義資料型別。
傳輸協議和傳輸層
RPC要實現網路上的兩臺計算機間的通訊,必須依賴具體的網路傳輸層才能完成。
傳輸層的選擇也有很多,用得比較多的比如TCP、UDP這類的,也有為了追求效能選擇RDMA/RoCE/DPDK,或者像gRPC那樣選擇更上層的HTTP2。
傳輸層的選擇需要考慮幾個因素:
- 物理限制,有些協議需要在特定的硬體環境中才能執行
- 傳輸特性,比如TCP協議對可靠性有一定保證,但是需要使用者自己處理黏包問題
- 效能、安全性、是否好除錯等因素也可以進行參考
OceanBase的RPC框架使用的傳輸層主要是基於TCP協議的封裝。我們沒有選擇UDP是為了簡化傳輸層的邏輯,TCP相比UDP而言擁有更完善的控制能力,框架不需要再為拆包組包這些邏輯編寫額外的程式碼。並且,我們為TCP封裝了連結複用的功能,避免因為網路延遲等原因需要建立過多的TCP連線。當然我們目前正在探索更多的協議加入至傳輸層,以滿足不同場景下的傳輸需求。
RPC訊息分發
當服務端收到一個RPC訊息後,需要根據RPC型別和相應規則分發請求到相應的處理函式上。常見的做法有:
- 暴力switch/if else,由編譯器做這方面的優化
- 實現查詢表,以RPC型別(常見為列舉型別)作為下標定位到處理函式
- hashmap
其中,使用hashmap的方式最為靈活。前兩種方案的實現都有一定的侷限性。
OceanBase早期用的是if else分支判斷:如果RPC請求包是該型別的,就選擇這個處理函式,否則選擇另外的處理函式。原先這樣做的是因為簡單易實現,並且當時請求的種類也很少,效率上並沒有太大差別。
後來隨著請求種類的增加,使用分支判斷風格分發請求在效能上的劣勢就慢慢凸顯出來了,我們就改用了使用RPC Code作為索引到陣列中去查詢對應的處理函式。使用查詢表最大的問題是如果RPC Code跨度很大,就需要一個非常大的陣列來儲存這個隱射關係,它會使用更多的記憶體以及記憶體訪問的區域性性變差。Hashmap是更通用一些的方式,後續OceanBase會考慮遷移到這個方案上。
主流C++ RPC框架對比
gRPC
gRPC是一個高效能、通用的開源RPC框架,其由Google主要面向移動應用開發並基於HTTP/2協議標準而設計,基於ProtoBuf(Protocol Buffers)序列化協議開發。它支援眾多開發語言,當然也包含了C++了。
gRPC最大的優勢是因為IDL基於ProtoBuf的緣故所以非常簡單易用,生成的程式碼也比較小巧易懂,文件非常健全。缺點則是傳輸層繫結了HTTP/2,序列化層繫結了ProtoBuf,兩者都不支援動態定製。HTTP/2對於分散式資料庫場景下的對效能有極致追求的場景不太友好。
Thrift
Thrift是一種介面描述語言和二進位制通訊協議,它被用來定義和建立跨語言的服務。它被當作一個遠端過程呼叫(RPC)框架來使用,是由Facebook為“大規模跨語言服務開發”而開發的,目前由Apache基金會管理。
Thrift最大的優勢是靈活性非常高,每一層都可以讓使用者做出不同的選擇從而構建出合適自己場景的RPC需求。各層基本都可以使用者自己進行定製,比如使用者自定義的協議、傳輸方式和路由分發規則等。這個通過官網的模組圖就可以看出來:
PhxRPC
PhxRPC是微信後臺團隊推出的一個非常簡潔小巧的RPC框架,編譯生成的庫檔案非常小巧。根據官網的介紹,它使用ProtoBuf作為IDL,並且只支援ProtoBuf。使用半同步半非同步模式,也就是說有專門的IO執行緒處理epoll。支援ucontext和過載保護。缺點是功能簡單,文件也比較少。
brpc
brpc是百度開源的RPC框架。它是一套比較完整的RPC框架,和Thrift一個量級。brpc使用ProtoBuf作為IDL,也可以使用Thrift工具生成程式碼整合至brpc後和Thrift服務進行通訊。brpc也是一套非常靈活的框架,支援各層的自定義控制;並且從一些網站的介紹和對比測試來看,它的效能也很有競爭力。
總結
RPC是實現網路間不同計算進行通訊的手段,它很好的遮蔽了網路層的細節,讓使用者跨計算機訪問就像呼叫本地函式那樣方便。但是它並不適用於所有的通訊場景,比如做大資料傳輸時可能直接使用傳輸層的API更加方便直觀,再比如它在處理服務端訊息推送的時候會比較麻煩。
實現RPC框架也需要權衡很多因素,比如安全性和通訊效率的平衡、異常處理機制的完善程度等等。
無論如何,一個好用的RPC框架在絕大多數的分散式系統中都扮演著非常重要的角色。
符風邀請你加入OceanBase技術交流群
想跟本文作者 符風 深入交流嗎?
想認識螞蟻金服OceanBase的一線技術專家嗎?
掃描下方二維碼聯絡螞蟻金服加群小助手,快速加入OceanBase技術交流群!