用自己写的 TCP 抓包代理看一次 gRPC Unary 调用的 HTTP/2 帧:请求是 HEADERS 加 DATA,响应是 HEADERS、DATA 和带 grpc-status 的 trailers;消息是 5 字节前缀加 protobuf;首次调用请求头 82 字节,第二次因为 HPACK 只有 15 字节。
抓包看 gRPC 的消息帧:消息边界由 5 字节前缀决定,和 HTTP/2 DATA 帧边界无关(20000 字节切成 16384 + 3628,1 MiB 切成 65 帧);gzip 压缩是逐条消息的,标志位为 1、长度是压缩后的;超过 4 MiB 的消息服务端读到长度就拒绝,线上只传了一小部分。
实测 gRPC 的 17 个状态码:线上传的是数字;普通 error 一律变成 Unknown;连接失败默认立刻返回 Unavailable(0.4 ms),加 WaitForReady 才会等;网关返回 429/502/503/504 在客户端都是 Unavailable,500 却是 Unknown。
为什么 grpc-status 放在 trailers:HTTP 状态 200 在第一个 HEADERS 就发出去了,流中途出错只能放在最后;一条消息都没发就失败是 trailers-only 的单个 HEADERS;grpc-message 做百分号编码;-bin 键用 base64;连接中途断了得到 Unavailable;缺少 trailers 得到 Internal。
实测 gRPC 的 deadline:线上是最多 8 位数字加单位字母的相对时长(999978u);A→B→C 逐跳扣减(999、898、797 ms);中间一层不传 ctx,客户端超时后下游还会把活干完;已过期的调用不发任何帧;服务端不监听 ctx 就看不到取消;传输延迟不会被扣掉。
实测 gRPC 取消:cancel 是一个 RST_STREAM(CANCEL),deadline 到期两边各发一个;handler 不监听 ctx 就会继续跑完;流式 Send 在取消后失败并带 Canceled;取消与响应赛跑可能成功也可能 Canceled;调用完成后 cancel() 不发帧;取消沿调用链几乎同时传到下游。
实测 gRPC metadata:键统一小写;普通键只能放可打印 ASCII,-bin 键可放任意字节(线上无填充 base64);user-agent、content-type、grpc-timeout 等库占用的头被静默丢弃;同键多值是多行;头总大小 16 MiB 附近断开;响应头必须在第一条消息之前设置。
服务端流的流控实测:快生产者遇到慢消费者,Send 从 0.4 ms 变成 31 ms 跟着消费者走;不读数据时默认只能先发 195584 字节;调大接收窗口可以多发,窗口选项会关闭动态窗口;被阻塞的 Send 在取消后返回 Canceled;handler 返回后已发的消息仍会送达。
客户端流和双向流的结束语义:CloseSend 在线上是长度 0 的 DATA 帧加 END_STREAM;半关闭后服务端还能继续发;服务端提前结束时客户端 Send 一律返回 io.EOF,状态要靠 Recv 取;只 Send 不 Recv 会在 5 条(1280 KiB)后死锁;忘了 CloseSend 服务端一直等。
实测 HTTP/2 流控窗口:吞吐大致等于窗口除以往返时间——RTT 20 ms 时 64 KiB 窗口只有 2.5 MiB/s、4 MiB 窗口 119.5;默认的动态窗口靠 PING 测量 BDP 逐步长大;4 个流共享连接时连接窗口 64 KiB 把总吞吐压到 9.1 MiB/s(4 MiB 时 402.7);小消息不受影响。
实测 gRPC 客户端连接管理:NewClient 惰性建连;pick_first 只连一个地址、round_robin 连全部;默认退避 1 秒起乘 1.6;断开后进入 IDLE 等下一次调用;GracefulStop 发两个 GOAWAY;WithIdleTimeout 让空闲连接回到 IDLE。
实测 gRPC keepalive:客户端 Time 有 10 秒下限;服务端默认策略下客户端 PING 到第三个就被 GOAWAY(too_many_pings);对端变成黑洞时有 keepalive 12 秒发现、没有的要等到 deadline;服务端也能探活;PermitWithoutStream 默认 false 时空闲连接不探测。