原文:https://juejin.cn/post/6854573212018147336
前言
什么是RPC服務 RPC,是Remote Procedure Call的簡稱,翻譯成中文就是遠程過程調用。RPC就是允許程序調用另一個地址空間(通常是另一台機器上)的類方法或函數的一種服務。 它是一種架設在計算機網絡之上並隱藏底層網絡技術,可以像調用本地服務一樣調用遠端程序,在編碼代價不高的情況下提升吞吐的能力。
為什么要使用RPC服務 隨着計算機技術的快速發展,單台機器運行服務的方案已經不足以支撐越來越多的網絡請求負載,分布式方案開始興起,一個業務場景可以被拆分在多個機器上運行,每個機器分別只完成一個或幾個的業務模塊。為了能讓其他機器使用某台機器中的業務模塊方法,就有了RPC服務,它是基於一種專門實現遠程方法調用的協議上完成的服務。現如今很多主流語言都支持RPC服務,常用的有Java的Dubbo、Go的net/rpc & RPCX、谷歌的gRPC等。
關於gRPC 大部分RPC都是基於socket實現的,可以比http請求來的高效。gRPC是谷歌開發並開源的一款實現RPC服務的高性能框架,它是基於http2.0協議的,目前已經支持C、C++、Java、Node.js、Python、Ruby、Objective-C、PHP和C#等等語言。要將方法調用以及調用參數,響應參數等在兩個服務器之間進行傳輸,就需要將這些參數序列化,gRPC采用的是protocol buffer的語法(檢查proto),通過proto語法可以定義好要調用的方法、和參數以及響應格式,可以很方便地完成遠程方法調用,而且非常利於擴展和更新參數。

快速上手gRPC
使用gRPC實現遠程方法調用之前,我們需要了解protocol buffer語法,安裝支持protocol buffer語法編譯成.proto文件的工具,然后再完成gRPC的服務端(遠程方法提供者)和客戶端(調用者)的搭建和封裝。
了解protocol buffer
Protocol Buffer是Google的跨語言,跨平台,可擴展機制的,用於序列化結構化數據 - 對比XML,但更小,更快,更簡單的一種數據格式。您可以定義數據的結構化,例如方法的名字、參數和響應格式等,然后可以使用對應的語言工具生成的源代碼輕松地在各種數據流中使用各種語言編寫和讀取結構化數據。
語法使用
- 定義消息類型
package test;
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
復制代碼
上面的例子就是一個.proto文件,該文件的第一行指定包名,方便您在別的proto文件中import這個文件的定義,第二行是您正在使用proto3語法:如果您不這樣做,protobuf 編譯器將假定您正在使用proto2。這必須是文件的第一個非空的非注釋行,目前建議使用proto3語法。 SearchRequest是消息體的名字,指定了三個字段,分別指定了字段的類型和順序,順序必須從1開始,並且不可重復;
- 指定字段規則 消息字段可以是以下之一:
單數(默認):格式良好的消息可以包含該字段中的零個或一個(但不超過一個)。 repeated:此字段可以在格式良好的消息中重復任意次數(包括零)。將保留重復值的順序。例如:
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
repeated Body body = 4;
}
message Body {
int32 id = 1;
string number = 2;
}
復制代碼
上述例子其實就是定義了一個格式,用我們通常的json格式表示就是:
{
"query": str,
"page_number":int,
"result_per_page":int,
"body":[
{
"id":int,
"number":str
}
],
}
復制代碼
- 標量值類型 標量消息字段可以具有以下類型之一 - 該表顯示.proto文件中指定的類型,以及自動生成的類中的相應類型:
.proto Type | 備注 | Python Typ |
---|---|---|
double | float | |
float | float | |
int32 | 使用變長編碼,對於負值的效率很低,如果你的域有可能有負值,請使用sint64替代 | int |
uint32 | 使用變長編碼 | int/long |
uint64 | 使用變長編碼 | int/long |
sint32 | 使用變長編碼,這些編碼在負值時比int32高效的多 | int |
sint64 | 使用變長編碼,有符號的整型值。編碼時比通常的int64高效。 | int/long |
fixed32 | 總是4個字節,如果數值總是比總是比228大的話,這個類型會比uint32高效。 | int |
fixed64 | 總是8個字節,如果數值總是比總是比256大的話,這個類型會比uint64高效。 | int/long |
sfixed32 | 總是4個字節 | int |
sfixed64 | 總是8個字節 | int/long |
bool | 布爾值 | bool |
string | 一個字符串必須是UTF-8編碼或者7-bit ASCII編碼的文本。 | str/unicode |
bytes | 可能包含任意順序的字節數據。 | str |
- 默認值 解析消息時,如果編碼消息不包含特定的單數元素,則解析對象中的相應字段將設置為該字段的默認值。這些默認值是特定於類型的:
- 對於字符串,默認值為空字符串。
- 對於字節,默認值為空字節。
- 對於bools,默認值為false。
- 對於數字類型,默認值為零。
- 對於枚舉,默認值是第一個定義的枚舉值,該值必須為0。
- 重復字段的默認值為空(通常是相應語言的空列表)
- 枚舉類型
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
復制代碼
Corpus枚舉的第一個常量映射為零:每個枚舉定義必須包含一個映射到零的常量作為其第一個元素。這是因為:
- 必須有一個零值,以便我們可以使用0作為數字默認值。
- 零值必須是第一個元素,以便與proto2語義兼容,其中第一個枚舉值始終是默認值。
- 定義方法
service SearchService {
rpc Search(SearchRequest)returns(SearchResponse);
}
復制代碼
上面的語句就定義好了遠程調用的方法名Search,待編譯好對應語言的源代碼之后就可以使用遠程調用,例如在Python中初始化SearchService方法,則執行Search方法,就是采用SearchRequest的格式去調用遠程機器的方法,然后按定義好的SearchResponse格式返回調用結果。根據proto的語法定義,甚至可以實現跨平台,跨語言使用這種遠程調用。

使用工具生成對應語言的源代碼
根據實際工作需要,生成以下對應語言的自定義消息類型Java,Python,C ++,Go, Ruby, Objective-C,或C#的.proto文件,你需要運行protobuf 編譯器protoc上.proto。如果尚未安裝編譯器,請下載該軟件包並按照自述文件中的說明進行操作。 Protobuf 編譯器的調用如下:
protoc --proto_path = IMPORT_PATH --cpp_out = DST_DIR --java_out = DST_DIR --python_out = DST_DIR --go_out = DST_DIR --ruby_out = DST_DIR --objc_out = DST_DIR --csharp_out = DST_DIR path / to / file .proto
復制代碼
Python生成對應的源代碼
- 安裝Python的gRPC源碼包grpcio,用於執行gRPC的各種底層協議和請求響應方法
- 安裝Python基於gRPC的proto生成python源代碼的工具grpcio-tools
sudo python -m pip install grpcio
python -m pip install grpcio-tools
復制代碼
- 執行編譯生成python的proto序列化協議源代碼:
# 編譯 proto 文件
python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. test.proto
python -m grpc_tools.protoc: python 下的 protoc 編譯器通過 python 模塊(module) 實現, 所以說這一步非常省心
--python_out=. : 編譯生成處理 protobuf 相關的代碼的路徑, 這里生成到當前目錄
--grpc_python_out=. : 編譯生成處理 grpc 相關的代碼的路徑, 這里生成到當前目錄
-I. test.proto : proto 文件的路徑, 這里的 proto 文件在當前目錄
復制代碼
編譯后生成的源代碼:
- test_pb2.py: 用來和 protobuf 數據進行交互,這個就是根據proto文件定義好的數據結構類型生成的python化的數據結構文件
- test_pb2_grpc.py: 用來和 grpc 進行交互,這個就是定義了rpc方法的類,包含了類的請求參數和響應等等,可用python直接實例化調用
搭建Python gRPC服務
生成好了python可以直接實例化和調用的gRPC類,我們就可以開始搭建RPC的服務端(遠程調用提供者)和客戶端(調用者)了。
- 搭建服務端server.py
from concurrent import futures
import time
import grpc
import test_pb2
import test_pb2_grpc
# 實現 proto 文件中定義的 SearchService
class RequestRpc(test_pb2_grpc.SearchService):
# 實現 proto 文件中定義的 rpc 調用
def doRequest(self, request, context):
return test_pb2.Search(query = 'hello {msg}'.format(msg = request.name)) # return的數據是符合定義的SearchResponse格式
def serve():
# 啟動 rpc 服務,這里可定義最大接收和發送大小(單位M),默認只有4M
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10), options=[
('grpc.max_send_message_length', 100 * 1024 * 1024),
('grpc.max_receive_message_length', 100 * 1024 * 1024)])
test_pb2_grpc.add_SearchServiceServicer_to_server(RequestRpc(), server)
server.add_insecure_port('[::]:50051')
server.start()
try:
while True:
time.sleep(60*60*24) # one day in seconds
except KeyboardInterrupt:
server.stop(0)
if __name__ == '__main__':
serve()
復制代碼
- 搭建客戶端client.py
import grpc
import helloworld_pb2
import helloworld_pb2_grpc
def run():
# 連接 rpc 服務器
channel = grpc.insecure_channel('localhost:50051')
# 調用 rpc 服務
stub = test_pb2_grpc.SearchServiceStub(channel)
response = stub.doRequest(test_pb2.SearchRequest(query='henry'))
print("client received: ", response)
if __name__ == '__main__':
run()
復制代碼
最佳實踐
- 編寫proto文件的時候,注意定義好數據的格式,要多考慮可擴張性,例如可以定義api_version等用於區分版本,防止未來的版本有大的數據格式更新的時候可以兼容;
- 對於不可變類型,建議使用枚舉,例如請求一個字段type,取值是固定的時候,可以用枚舉類型;
- 對於服務端和客戶端的編寫,建議指定好最大接收和發送大小,避免出現數據溢出的異常;
- gRPC偶爾會出現斷線重連的情況,所以要增加異常處理機制,捕獲到由於重連時引發遠程調用失敗的問題,則可以執行重試(會在接下來的文章中詳細說明);
- gRPC可以采用SSL或TLS的協議,實現http2.0加密傳輸,提高系統的安全性(會在接下來的文章中詳細說明);
- 對於流量、並發較大的服務,可以通過微服務的一些應用或組件(如istio)等實現流量的熔斷、限流等等,提高穩定性。
gRPC的優勢
性能
gRPC消息使用一種有效的二進制消息格式protobuf進行序列化。Protobuf在服務器和客戶機上的序列化非常快。Protobuf序列化后的消息體積很小,能夠有效負載,在移動應用程序等有限帶寬場景中顯得很重要。
gRPC是為HTTP/2而設計的,它是HTTP的一個主要版本,與HTTP 1.x相比具有顯著的性能優勢:
- 二進制框架和壓縮。HTTP/2協議在發送和接收方面都很緊湊和高效。
- 通過單個TCP連接復用多個HTTP/2調用。多路復用消除了線頭阻塞。
代碼生成
所有gRPC框架都為代碼生成提供了一流的支持。gRPC開發的核心文件是*.proto文件 ,它定義了gRPC服務和消息的約定。根據這個文件,gRPC框架將生成服務基類,消息和完整的客戶端代碼。
通過在服務器和客戶端之間共享*.proto文件,可以從端到端生成消息和客戶端代碼。客戶端的代碼生成消除了客戶端和服務器上的重復消息,並為您創建了一個強類型的客戶端。無需編寫客戶端代碼,可在具有許多服務的應用程序中節省大量開發時間。
嚴格的規范
不存在具有JSON的HTTP API的正式規范。開發人員不需要討論URL,HTTP動詞和響應代碼的最佳格式。(想想,是用Post還是Get好?使用Get還是用Put好?一想到有選擇恐懼症的你是不是又開了糾結,然后浪費了大量的時間)
該gRPC規范是規定有關gRPC服務必須遵循的格式。gRPC消除了爭論並節省了開發人員的時間,因為gPRC在各個平台和實現之間是一致的。
流
HTTP/2為長期的實時通信流提供了基礎。gRPC通過HTTP/2為流媒體提供一流的支持。
gRPC服務支持所有流組合:
- 一元(沒有流媒體)
- 服務器到客戶端流
- 客戶端到服務器流
- 雙向流媒體 截至時間/超時和取消 gRPC允許客戶端指定他們願意等待RPC完成的時間。該期限被發送到服務端,服務端可以決定在超出了限期時采取什么行動。例如,服務器可能會在超時時取消正在進行的gRPC / HTTP /數據庫請求。
通過子gRPC調用截至時間和取消操作有助於實施資源使用限制。
推薦使用gRPC的場景
- 微服務 - gRPC設計為低延遲和高吞吐量通信。gRPC非常適用於效率至關重要的輕型微服務。 點對點實時通信 - gRPC對雙向流媒體提供出色的支持。gRPC服務可以實時推送消息而無需輪詢。 多語言混合開發環境 - gRPC工具支持所有流行的開發語言,使gRPC成為多語言開發環境的理想選擇。
- 網絡受限環境 - 使用Protobuf(一種輕量級消息格式)序列化gRPC消息。gRPC消息始終小於等效的JSON消息。