Python——gRPC详解及实战避坑方案(上)

共 7426字,需浏览 15分钟

 ·

2020-08-07 04:01

Python猫” ,一个值得加星标的公众号

作者:henry_czh(授权转载,勿二次转载)

来源:https://juejin.im/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,但更小,更快,更简单的一种数据格式。您可以定义数据的结构化,例如方法的名字、参数和响应格式等,然后可以使用对应的语言工具生成的源代码轻松地在各种数据流中使用各种语言编写和读取结构化数据。

语法使用

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开始,并且不可重复;

2、指定字段规则 消息字段可以是以下之一:

单数(默认):格式良好的消息可以包含该字段中的零个或一个(但不超过一个)。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
        }
    ],
}

3、标量值类型 标量消息字段可以具有以下类型之一 - 该表显示.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

4、默认值 解析消息时,如果编码消息不包含特定的单数元素,则解析对象中的相应字段将设置为该字段的默认值。这些默认值是特定于类型的:

  • 对于字符串,默认值为空字符串。
  • 对于字节,默认值为空字节。
  • 对于bools,默认值为false。
  • 对于数字类型,默认值为零。
  • 对于枚举,默认值是第一个定义的枚举值,该值必须为0。
  • 重复字段的默认值为空(通常是相应语言的空列表)

5、枚举类型

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语义兼容,其中第一个枚举值始终是默认值。

6、定义方法

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

3、执行编译生成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()

2、搭建客户端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. https://juejin.im/post/6844903687089831944
  2. https://doc.oschina.net/grpc?t=58008
  3. https://juejin.im/post/6844903794350751757
  4. https://www.jianshu.com/p/43fdfeb105ff

优质文章,推荐阅读:

Python 为什么会有个奇怪的“...”对象?

Python 为什么要有 pass 语句?

如何降低 Python 的内存消耗量?

Python 工匠:做一个精通规则的玩家

感谢创作者的好文
浏览 45
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报