Python——gRPC詳解及實戰避坑方案(上)

前言

什麼是RPC服務 RPC,是Remote Procedure Call的簡稱,翻譯成中文就是遠程過程調用。RPC就是容許程序調用另外一個地址空間(一般是另外一臺機器上)的類方法或函數的一種服務。 它是一種架設在計算機網絡之上並隱藏底層網絡技術,能夠像調用本地服務同樣調用遠端程序,在編碼代價不高的狀況下提高吞吐的能力。java

爲何要使用RPC服務 隨着計算機技術的快速發展,單臺機器運行服務的方案已經不足以支撐愈來愈多的網絡請求負載,分佈式方案開始興起,一個業務場景能夠被拆分在多個機器上運行,每一個機器分別只完成一個或幾個的業務模塊。爲了能讓其餘機器使用某臺機器中的業務模塊方法,就有了RPC服務,它是基於一種專門實現遠程方法調用的協議上完成的服務。現現在不少主流語言都支持RPC服務,經常使用的有Java的Dubbo、Go的net/rpc & RPCX、谷歌的gRPC等。python

關於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的服務端(遠程方法提供者)和客戶端(調用者)的搭建和封裝。json

瞭解protocol buffer

Protocol Buffer是Google的跨語言,跨平臺,可擴展機制的,用於序列化結構化數據 - 對比XML,但更小,更快,更簡單的一種數據格式。您能夠定義數據的結構化,例如方法的名字、參數和響應格式等,而後可使用對應的語言工具生成的源代碼輕鬆地在各類數據流中使用各類語言編寫和讀取結構化數據。api

語法使用

  1. 定義消息類型
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開始,而且不可重複;安全

  1. 指定字段規則 消息字段能夠是如下之一:

單數(默認):格式良好的消息能夠包含該字段中的零個或一個(但不超過一個)。 repeated:此字段能夠在格式良好的消息中重複任意次數(包括零)。將保留重複值的順序。例如:ruby

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格式表示就是:bash

{
    "query": str,
    "page_number":int,
    "result_per_page":int,
    "body":[
        {
            "id":int,
            "number":str
        }
    ],
}
複製代碼
  1. 標量值類型 標量消息字段能夠具備如下類型之一 - 該表顯示.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
  1. 默認值 解析消息時,若是編碼消息不包含特定的單數元素,則解析對象中的相應字段將設置爲該字段的默認值。這些默認值是特定於類型的:
  • 對於字符串,默認值爲空字符串。
  • 對於字節,默認值爲空字節。
  • 對於bools,默認值爲false。
  • 對於數字類型,默認值爲零。
  • 對於枚舉,默認值是第一個定義的枚舉值,該值必須爲0。
  • 重複字段的默認值爲空(一般是相應語言的空列表)
  1. 枚舉類型
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語義兼容,其中第一個枚舉值始終是默認值。
  1. 定義方法
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生成對應的源代碼

  1. 安裝Python的gRPC源碼包grpcio,用於執行gRPC的各類底層協議和請求響應方法
  2. 安裝Python基於gRPC的proto生成python源代碼的工具grpcio-tools
sudo python -m pip install grpcio

python -m pip install grpcio-tools
複製代碼
  1. 執行編譯生成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的服務端(遠程調用提供者)和客戶端(調用者)了。

  1. 搭建服務端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()
複製代碼
  1. 搭建客戶端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()
複製代碼

最佳實踐

  1. 編寫proto文件的時候,注意定義好數據的格式,要多考慮可擴張性,例如能夠定義api_version等用於區分版本,防止將來的版本有大的數據格式更新的時候能夠兼容;
  2. 對於不可變類型,建議使用枚舉,例如請求一個字段type,取值是固定的時候,能夠用枚舉類型;
  3. 對於服務端和客戶端的編寫,建議指定好最大接收和發送大小,避免出現數據溢出的異常;
  4. gRPC偶爾會出現斷線重連的狀況,因此要增長異常處理機制,捕獲到因爲重連時引起遠程調用失敗的問題,則能夠執行重試(會在接下來的文章中詳細說明);
  5. gRPC能夠採用SSL或TLS的協議,實現http2.0加密傳輸,提升系統的安全性(會在接下來的文章中詳細說明);
  6. 對於流量、併發較大的服務,能夠經過微服務的一些應用或組件(如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消息。

參考文獻

  1. juejin.im/post/5bb597…
  2. doc.oschina.net/grpc?t=5800…
  3. juejin.im/post/5c8614…
  4. www.jianshu.com/p/43fdfeb10…
相關文章
相關標籤/搜索