gRPC Java deadline 已过,线程却一直停在 waitAndDrain
gRPC Java 1.77.0 的重试计数竞态可能让 deadline 已触发的调用永不返回。1.80.0 已正式修正,需核真正加载的 core 及重试边界。
客户端已经收到 RST_STREAM,调用的 deadline 也已到期,业务线程却仍停在这里,没有收到预期的 DEADLINE_EXCEEDED:
io.grpc.stub.ClientCalls$ThreadlessExecutor.waitAndDrain
io.grpc.stub.ClientCalls.blockingUnaryCall
如果运行的是 gRPC Java 1.77.0,通道启用了重试,而且故障集中在重试与 deadline 取消交错时,要检查 RetriableStream 的终止计数。单凭线程处于 waitAndDrain 并不能确定异常:正常阻塞 RPC 在等待响应时也会停在那里。
取消定时器执行过,终止回调仍可能没有送达
受影响实现里,重试取消对象可以先注册,再发布其真正的 Future。若 deadline 在这段间隙取消调用,代码可能因为 Future 还是 null,跳过对应的在途子流计数递减。
结果是 deadline 状态已经被保存,响应 Future 却仍未完成。空的回调队列和已经完成的 deadline 定时任务,可以与一个仍在等待的业务线程同时存在。再加一个更短的同类 RPC deadline,不能修复这条终止回调路径。
排查时关联同一次调用的线程、运行版本、实际重试策略和终止状态。HTTP/2 的 RST_STREAM(errorCode=8) 是取消线索,不足以单独证明这个竞态;也不能从某个线程栈直接断言它持有数据库锁或已经阻塞全部服务。
采用包含计数修正的一致模块组合
gRPC Java 1.80.0 的正式发行已经修正这条竞态:是否消耗已排定重试的计数,不再依赖 Future 是否已经发布。1.85.1 的正式实现仍保留该处理。
按项目的 BOM 或既有依赖管理选择包含修正的受支持组合,确认最终运行的 grpc-core、stub、transport 以及 xDS 等模块没有继续混用旧版本。声明里更新一个模块,不足以证明运行时的 RetriableStream 已更换。
不要为了恢复调用而关闭 TLS、移除鉴权拦截器或把 deadline 无限放大。这些做法不会恢复丢失的终止计数。
暂时不能升级时,可以评估在受影响的调用范围内停用客户端配置重试,以减少这条交错路径。这会失去对瞬时错误的自动恢复能力,也不能据此保证所有通道的透明重试都消失。不要改成无条件手工重发,尤其是存在写入或其他非幂等操作的方法。
deadline 是整次调用的预算
把 DEADLINE_EXCEEDED 放进重试码列表,不会给每次尝试重新发一份完整 deadline。短超时配上激进重试,反而更容易让正常请求大量失败;修正依赖以后,仍应按调用总预算审查重试次数与退避。
在隔离环境分别覆盖正常响应、可重试失败、重试过程中 deadline 到期、主动取消和最终失败。每次调用都应以一个终态结束,等待线程和响应 Future 能释放;不要只看定时器日志里出现了“已取消”。
已卡住的旧调用也不会因修改依赖文件原地恢复。通过正常部署更新进程,保留原模块清单与故障证据;对此前结果未知的写请求,先按服务端记录和幂等键核实,不把超时或线程退出当作业务从未执行的证明。


