18 | 如何通过gRPC实现高效远程过程调用?

本文详细介绍了gRPC如何利用HTTP/2和ProtoBuf协议进行消息编码,包括gRPC的编码流程、流式RPC调用的处理,以及如何通过网络报文分析定位问题。还展示了gRPC在Python中的使用示例和流模式编码的原理。
摘要由CSDN通过智能技术生成

这一讲我们将以一个实战案例,基于前两讲提到的 HTTP/2 和 ProtoBuf 协议,看看 gRPC 如何将结构化消息编码为网络报文。

直接操作网络协议编程,容易让业务开发过程陷入复杂的网络处理细节。RPC 框架以编程语言中的本地函数调用形式,向应用开发者提供网络访问能力,这既封装了消息的编解码,也通过线程模型封装了多路复用,对业务开发很友好。

其中,Google 推出的 gRPC 是性能最好的 RPC 框架之一,它支持 Java、JavaScript、Python、GoLang、C++、Object-C、Android、Ruby 等多种编程语言,还支持安全验证等特性,得到了广泛的应用,比如微服务中的 Envoy、分布式机器学习中的 TensorFlow,甚至华为去年推出重构互联网的 New IP 技术,都使用了 gRPC 框架。

然而,网络上教你使用 gRPC 框架的教程很多,却很少去谈 gRPC 是如何编码消息的。这样,一旦在大型分布式系统中出现疑难杂症,需要通过网络报文去定位问题发生在哪个系统、主机、进程中时,你就会毫无头绪。即使我们掌握了 HTTP/2 和 Protobuf 协议,但若不清楚 gRPC 的编码规则,还是无法分析抓取到的 gRPC 报文。而且,gRPC 支持单向、双向的流式 RPC 调用,编程相对复杂一些,定位流式 RPC 调用引发的 bug 时,更需要我们掌握 gRPC 的编码原理。

这一讲,就将以 gRPC 官方提供的 example:data_transmisstion 为例,介绍 gRPC 的编码流程。在这一过程中,会顺带回顾 HTTP/2 和 Protobuf 协议,加深对它们的理解。虽然这个示例使用的是 Python 语言,但基于 gRPC 框架,你可以轻松地将它们转换为其他编程语言。

如何使用 gRPC 框架实现远程调用?

我们先来简单地看下 gRPC 框架到底是什么。RPC 的全称是 Remote Procedure Call,即远程过程调用,它通过本地函数调用,封装了跨网络、跨平台、跨语言的服务访问,大大简化了应用层编程。其中,函数的入参是请求,而函数的返回值则是响应。

gRPC 就是一种 RPC 框架,在你定义好消息格式后,针对你选择的编程语言,gRPC 为客户端生成发起 RPC 请求的 Stub 类,以及为服务器生成处理 RPC 请求的 Service 类(服务器只需要继承、实现类中处理请求的函数即可)。如下图所示,很明显,gRPC 主要服务于面向对象的编程语言。

gRPC 支持 QUIC、HTTP/1 等多种协议,但鉴于 HTTP/2 协议性能好,应用场景又广泛,因此 HTTP/2 是 gRPC 的默认传输协议。gRPC 也支持 JSON 编码格式,但在忽略编码细节的 RPC 调用中,高效的 Protobuf 才是最佳选择!因此,这一讲仅基于 HTTP/2 和 Protobuf,介绍 gRPC 的用法。

gRPC 可以简单地分为三层,包括底层的数据传输层,中间的框架层(框架层又包括 C 语言实现的核心功能,以及上层的编程语言框架),以及最上层由框架层自动生成的 Stub 和 Service 类,如下图所示:

接下来我们以官网上的data_transmisstion 为例,先看看如何使用 gRPC。

构建 Python 语言的 gRPC 环境很简单,你可以参考官网上的QuickStart

使用 gRPC 前,先要根据 Protobuf 语法,编写定义消息格式的 proto 文件。在这个例子中只有 1 种请求和 1 种响应,且它们很相似,各含有 1 个整型数字和 1 个字符串,如下所示:

package demo;

message Request {
    int64 client_id = 1;
    string request_data = 2;
} 

message Response {
    int64 server_id = 1;
    string response_data = 2;
}

请注意,这里的包名 demo 以及字段序号 1、2,都与后续的 gRPC 报文分析相关。

接着定义 service,所有的 RPC 方法都要放置在 service 中,这里将它取名为 GRPCDemo。GRPCDemo 中有 4 个方法,后面 3 个流式访问的例子我们呆会再谈,先来看简单的一元访问模式 SimpleMethod 方法,它定义了 1 个请求对应 1 个响应的访问形式。其中,SimpleMethod 的参数 Request 是请求,返回值 Response 是响应。注意,分析报文时会用到这里的类名 GRPCDemo 以及方法名 SimpleMethod。

service GRPCDemo {
    rpc SimpleMethod (Request) returns (Response);
}

用 grpc_tools 中的 protoc 命令,就可以针对刚刚定义的 service,生成含有 GRPCDemoStub 类和 GRPCDemoServicer 类的 demo_pb2_grpc.py 文件(实际上还包括完成 Protobuf 编解码的 demo_pb2.py),应用层将使用这两个类完成 RPC 访问。简化了官网上的 Python 客户端代码,如下所示:

with grpc.insecure_channel("localhost:23333") as channel:
    stub = demo_pb2_grpc.GRPCDemoStub(channel)
    request = demo_pb2.Request(client_id=1,
            request_data="called by Python client")
    response = stub.SimpleMethod(request)

示例中客户端与服务器都在同一台机器上,通过 23333 端口访问。客户端通过 Stub 对象的 SimpleMethod 方法完成了 RPC 访问。而服务器端的实现也很简单,只需要实现 GRPCDemoServicer 父类的 SimpleMethod 方法,返回 response 响应即可:

class DemoServer(demo_pb2_grpc.GRPCDemoServicer):
    def SimpleMethod(self, request, context):
        response = demo_pb2.Response(
            server_id=1,
            response_data="Python server SimpleMethod Ok!!!!")
        return response

可见,gRPC 的开发效率非常高!接下来我们分析这次 RPC 调用中,消息是怎样编码的。

gRPC 消息是如何编码的?

定位复杂的网络问题,都需要抓取、分析网络报文。如果你在 Windows 上抓取网络报文,可以使用 Wireshark 工具,如果在 Linux 上抓包可以使用 tcpdump 工具。当然,也可以从这里下载抓取好的网络报文,用 Wireshark 打开它。需要注意,23333 不是 HTTP 常用的 80 或者 443 端口,所以 Wireshark 默认不会把它解析为 HTTP/2 协议。你需要鼠标右键点击报文,选择“解码为”(Decode as),将 23333 端口的报文设置为 HTTP/2 解码器,如下图所示:

图中蓝色方框中,TCP 连接的建立过程请参见[第 9 讲]。我们重点看红色方框中的 gRPC 请求与响应,点开请求,可以看到下图中的信息:

先来分析蓝色方框中的 HTTP/2 头部。请求中有 2 个关键的 HTTP 头部,path 和 content-type,它们决定了 RPC 方法和具体的消息编码格式。path 的值为“/demo.GRPCDemo/SimpleMethod”,通过“/ 包名. 服务名 / 方法名”的形式确定了 RPC 方法。content-type 的值为“application/grpc”,确定消息编码使用 Protobuf 格式。如果你对其他头部的含义感兴趣,可以看下这个文档,注意这里使用了 ABNF 元数据定义语言。

HTTP/2 包体并不会直接存放 Protobuf 消息,而是先要添加 5 个字节的 Length-Prefixed Message 头部,其中用 4 个字节明确 Protobuf 消息的长度(1 个字节表示消息是否做过压缩),即上图中的桔色方框。为什么要多此一举呢?这是因为,gRPC 支持流式消息,即在 HTTP/2 的 1 条 Stream 中,通过 DATA 帧发送多个 gRPC 消息,而 Length-Prefixed Message 就可以将不同的消息分离开。关于流式消息,我们在介绍完一元模式后,再加以分析。

最后分析 Protobuf 消息,这里仅以 client_id 字段为例,对上一讲的内容做个回顾。在 proto 文件中 client_id 字段的序号为 1,因此首字节 00001000 中前 5 位表示序号为 1 的 client_id 字段,后 3 位表示字段的值类型是 varint 格式的数字,因此随后的字节 00000001 表示字段值为 1。序号为 2 的 request_data 字段请你结合上一讲的内容,试着做一下解析,看看字符串“called by Python client”是怎样编码的。

再来看服务器发回的响应,点开 Wireshark 中的响应报文后如下图所示:

其中 DATA 帧同样包括 Length-Prefixed Message 和 Protobuf,与 RPC 请求如出一辙,这里就不再赘述了,我们重点看下 HTTP/2 头部。你可能留意到,响应头部被拆成了 2 个部分,其中 grpc-status 和 grpc-message 是在 DATA 帧后发送的,这样就允许服务器在发送完消息后再给出错误码。关于 gRPC 的官方错误码以及 message 描述信息是如何取值的,你可以参考这个文档

这种将部分 HTTP 头部放在包体后发送的技术叫做 Trailer,RFC7230 文档对此有详细的介绍。其中,RPC 请求中的 TE: trailers 头部,就说明客户端支持 Trailer 头部。在 RPC 响应中,grpc-status 头部都会放在最后发送,因此它的帧 flags 的 EndStream 标志位为 1。

可以看到,gRPC 中的 HTTP 头部与普通的 HTTP 请求完全一致,因此,它兼容当下互联网中各种七层负载均衡,这使得 gRPC 可以轻松地跨越公网使用。

gRPC 流模式的协议编码

说完一元模式,我们再来看流模式 RPC 调用的编码方式。

所谓流模式,是指 RPC 通讯的一方可以在 1 次 RPC 调用中,持续不断地发送消息,这对订阅、推送等场景很有用。流模式共有 3 种类型,包括客户端流模式、服务器端流模式,以及两端双向流模式。在data_transmisstion 官方示例中,对这 3 种流模式都定义了 RPC 方法,如下所示:

service GRPCDemo {
    rpc ClientStreamingMethod (stream Request) returns Response);
    
    rpc ServerStreamingMethod (Request) returns (stream Response);

    rpc BidirectionalStreamingMethod (stream Request) returns (stream Response);
}

不同的编程语言处理流模式的代码很不一样,这里就不一一列举了,但通讯层的流模式消息编码是一样的,而且很简单。这是因为,HTTP/2 协议中每个 Stream 就是天然的 1 次 RPC 请求,每个 RPC 消息又已经通过 Length-Prefixed Message 头部确立了边界,这样,在 Stream 中连续地发送多个 DATA 帧,就可以实现流模式 RPC。画了一张示意图,可以对照它理解抓取到的流模式报文。

小结

这一讲介绍了 gRPC 怎样使用 HTTP/2 和 Protobuf 协议编码消息。

在定义好消息格式,以及 service 类中的 RPC 方法后,gRPC 框架可以为编程语言生成 Stub 和 Service 类,而类中的方法就封装了网络调用,其中方法的参数是请求,而方法的返回值则是响应。

发起 RPC 调用后,我们可以这么分析抓取到的网络报文。首先,分析应用层最外层的 HTTP/2 帧,根据 Stream ID 找出一次 RPC 调用。客户端 HTTP 头部的 path 字段指明了 service 和 RPC 方法名,而 content-type 则指明了消息的编码格式。服务器端的 HTTP 头部被分成 2 次发送,其中 DATA 帧发送完毕后,才会发送 grpc-status 头部,这样可以明确最终的错误码。

其次,分析包体时,可以通过 Stream 中 Length-Prefixed Message 头部,确认 DATA 帧中含有多少个消息,因此可以确定这是一元模式还是流式调用。在 Length-Prefixed Message 头部后,则是 Protobuf 消息,按照上一讲的内容进行分析即可。

思考题

gRPC 默认并不会压缩字符串,可以通过在获取 channel 对象时加入 grpc.default_compression_algorithm 参数的形式,要求 gRPC 压缩消息,此时 Length-Prefixed Message 中 1 个字节的压缩位将会由 0 变为 1。可以观察下执行压缩后的 gRPC 消息有何不同。

  • 15
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值