gRPC Java 重试后请求体变空,xDS 调用为何还可能返回 OK
gRPC Java 1.83.0、1.83.1 的常规 xDS 重试可能重用已读完的 InputStream,导致空请求。核对拦截器、正式修复与服务端业务结果。
同一个 gRPC 调用,第一次发送了非空请求,收到可重试错误后,下一次却发出空 protobuf 消息。更难发现的是,服务端若允许空消息,重试仍可能返回 OK,客户端于是把一次没有处理实际数据的调用当成成功。
gRPC Java 1.83.0、1.83.1 的常规 xds:/// 通道存在这条回归。新增的 RawMessageClientInterceptor 把方法描述符改为直接传递 InputStream,重试时再次使用已经消费完的流,不能重新获得最初的请求字节。不是所有重试、所有 gRPC 客户端都有这个问题。
对比每次尝试的消息,而不只看最终状态
先确认实际解析的 gRPC Java 模块、通道目标、拦截器和生效的重试策略。故障需要经过相关原始流序列化路径,并且实际发生了重试;没有第二次尝试时,不能用本题解释请求体缺失。
受控重现中,首次请求携带业务字段,服务端返回策略允许重试的状态,第二次尝试只剩一个长度为零的消息。未压缩的 gRPC 消息前缀为一个压缩标志字节加四个长度字节,空消息可能呈现:
00 00 00 00 00
这是零长度消息的前缀,不是说整个 HTTP/2 请求只包含这五个字节。生产排查优先记录尝试编号、消息长度、业务条数和关联 ID,不把认证元数据或业务正文倾倒到日志中。
若首次就为空、服务端主动删了字段,或请求根本没进入这条拦截链,应另查调用构造和协议。空 protobuf 也不一定是非法 protobuf:协议解码成功不能证明业务请求完整。
常规 xDS 通道采用包含门控修复的正式版本
gRPC Java 1.85.0 的发行说明明确包含修复:只有客户端或服务端 ext_proc 实验开关启用时,才加入该原始消息拦截器,避免普通 xDS 通道被它改变序列化行为。它是有正式发布证据的修复版本,不表示所有更早补丁都没有此项变化。
按工程的 BOM 或依赖管理一起更新相关 gRPC 模块,保留原有 xDS 资源、服务配置、认证和 deadline。不要只替换 grpc-xds,留下 core、stub 和 transport 处于未审查的混合版本。
这项修复有条件:应用若主动启用了 GRPC_EXPERIMENTAL_XDS_EXT_PROC_ON_CLIENT、GRPC_EXPERIMENTAL_XDS_EXT_PROC_ON_SERVER,或自行安装原始 InputStream 拦截器,不能直接套用“升级后普通 xDS 路径不再加入拦截器”的结论。要按实际功能核对该流能否被每次尝试重新序列化;随意删除必需的处理器也可能绕过业务过滤或鉴权逻辑。
对于暂时无法升级的故障服务,可以评估在相关方法上撤掉配置的重试策略,使请求以原始失败结束,避免第二次被空请求误判成功。这会降低对瞬时故障的恢复能力,需要明确业务取舍;透明重试与配置重试还有不同条件,撤一条策略不能证明所有重试都已消失。不要用无条件手工重发代替原策略。
失败重试要核对服务端实际处理量
验收保留原 xDS 入口,在隔离环境让首次尝试返回一个已经配置为可重试的状态。确认第二次尝试收到的请求条数、必要字段或受控内容摘要与第一次一致,最终业务结果也与输入对应。
同时覆盖不触发重试的正常调用、达到最大尝试次数后的失败,以及 deadline 到期后的取消。不要为了通过测试,把重试状态列表随意扩大或删除 deadline。
排查已发生的业务任务时,按请求关联记录检查处理数量。最终 OK 无法证明此前没有空请求误成功,也不能据此直接把全部任务重新执行;写入类调用要先按幂等键、去重记录和真实结果决定是否补偿。回退依赖前保存修复配置与验收结果,避免重新引入原来的空消息路径。


