踩坑记:gRPC 异常响应

踩坑记:gRPC 异常响应_第1张图片

- 起 -

前些天接到一个 Oncall,来自 Lark 的胡同学反馈,用 gRPC 官方的 python 客户端请求 Kitex gRPC Server,有时收到的 response 为 None。

请求的代码大致如下:

stub = xx_grpc.XXXServiceStub(channel)
resp = stub.SomeMethod(req)
logger.info("resp = {}".format(resp))

如果请求失败,按 python 的尿性,这里应该 raise 一个 exception,返回个 None 算个啥?

踩坑记:gRPC 异常响应_第2张图片

胡同学补充说,从服务端的日志来看,请求是正常收到且处理了,并且通过增加两个环境变量再运行 client 端:

$ GRPC_TRACE=all GRPC_VERBOSITY=debug python3 test.py

可以看到 client 把 tcp 报文内容都 dump 出来了,虽然乱码很多,但是从一些文本中可以看到,确实收到了 server 的 response:

踩坑记:gRPC 异常响应_第3张图片

考虑到胡同学用的是 gRPC 官方的 client,我充分利用自己锻炼了多年的反思技能,先从 Kitex 查起。

踩坑记:gRPC 异常响应_第4张图片

- 承 -

既然这个问题能够稳定复现,那就说明可以稳定复现这个问题。

踩坑记:gRPC 异常响应_第5张图片

那么就先用 tcpdump 抓它个包:

$ tcpdump -i any tcp port 9954 -Ans 0 -w grpc.pcap

然后在 wireshark 里打开,结合 log 里的信息,通过 filter 找到有问题的这个请求:

踩坑记:gRPC 异常响应_第6张图片

踩坑记:gRPC 异常响应_第7张图片

人肉 decode 这个报文确实有点为难,好在可以在 wireshark 里指定报文的协议为 HTTP2,然后它顺手就把 gRPC 也给识别出来了:

踩坑记:gRPC 异常响应_第8张图片

但也没有完全识别出来,只是把这个 gRPC payload 识别成了「Line-based text data」。将这个的「text data」保存为 packet_bytes.bin,然后执行:

$ protoc --decode_raw < packet_bytes.bin

可以正常解码,说明返回的数据是符合 protobuf 规范的。

而对比其他正常响应的报文,Wireshark 可以直接按照 protobuf 的编码协议当场 decode 出来:

踩坑记:gRPC 异常响应_第9张图片

这说明在一些情况下, Kitex gRPC Server 确实返回了不正确的报文。

对比这两个请求的 Wireshark 解析结果,可以看到,在 gRPC Message 下面有两个 flag 不同:

flag 正常响应 异常响应
Frame Type 0 1
Compressed Flag 0 1

看起来非常接近真相了。

踩坑记:gRPC 异常响应_第10张图片

那么问题是哪一个呢?

- 转 -

根据官方文档 gRPC over HTTP2 ,其报文是由一系列「Length-Prefixed-Message」构成的;而每个「Length-Prefixed-Message」则由三个部分组成:

  • Compressed-Flag:0/1(1字节无符号整数)
  • Message-Length:消息长度(4字节无符号整数)
  • Message:二进制数据

异常响应的报文显然不符合规范 —— Compressed Flag 这个字节的值竟然是 0xAF ?

踩坑记:gRPC 异常响应_第11张图片

根据以前的经验,这种脏东西大概是这两种情况导致的:(1) 并发读写导致内存数据被写坏了,或 (2) 复用了未清理的buf。

侯捷大佬说过「源码面前,了无秘密」。但是他没说扒代码会这么辛苦。此处省略800字,我终于扒出来 Kitex 的 gRPC 编码逻辑,位于 pkg/remote/codec/protobuf/grpc.go:

踩坑记:gRPC 异常响应_第12张图片

在编码一个 Data Frame 的时候,会先从 mcache 获取一个 buf,在 [1, 5) 写入数据的长度,并在 [5, +∞) 写入 protobuf 编码后的数据。

至此破案。

- 合 -

既然问题定位到了,修复起来也就很简单了,只要将从 mcache 获得的 buf 第一个字节清零即可,相关 PR 已经合入 develop 分支,将会随着下一个版本的 Kitex 一起发布。

此外,python client 在遇到错误的报文时没有任何报错,也不是太对劲,于是我给他们提了个 issue

最后打个小广告,欢迎大家关注 CloudWeGo (微信公众号:CloudWeGo)—— CloudWeGo 是一套可快速构建企业级云原生微服务架构的中间件集合。 它包含许多组件:Golang RPC 框架 Kitex,HTTP 框架 Hertz,Rust RPC 框架 Volo,网络库 Netpoll,Go 语言 Thrift 编译器 Thriftgo 等等。 通过结合社区优秀的开源产品和生态,可以快速搭建一套完善的云原生微服务体系。

(完)

参考

你可能感兴趣的:(踩坑记:gRPC 异常响应)