Grpc调用出现负时长DEADLINE_EXCEEDED异常的原因及解决方案
为什么会出现负数的deadline?
你遇到的核心问题是将带有固定deadline的gRPC Stub声明为了单例Spring Bean。
gRPC的withDeadlineAfter(duration, unit)方法是在调用该方法的时间点,计算出一个固定的截止时间点,并返回一个新的Stub实例——这个Stub的deadline是静态的,不会随后续调用动态更新。
因为Spring的@Bean默认是单例的,所以你的myAppSenderServiceBlockingStub和myAppCodeLoaderServiceBlockingStub会在应用启动时初始化,此时deadline就被固定为「启动时间 + 300000ms(5分钟)」。当超过5分钟后,任何后续调用都会发现当前时间已经晚于这个固定截止时间,错误信息里的负数-175.597476157s from now就是在告诉你:截止时间已经过去175秒了。
首次调用成功是因为此时还在5分钟的有效期内,后续调用超出这个窗口就会触发DEADLINE_EXCEEDED异常。
如何解决?
我们需要让每次gRPC调用都使用动态计算的deadline,而不是依赖单例Stub上的静态deadline。有两种常用方案:
方案1:移除单例Stub上的deadline,调用时动态设置
修改你的配置类,去掉Stub Bean定义中的withDeadlineAfter,只创建基础Stub:
@Bean public MyAppSenderServiceGrpc.MyAppSenderServiceBlockingStub myAppSenderServiceBlockingStub( TracingClientInterceptor tracingClientInterceptor, ManagedChannel managedChannel) { return MyAppSenderServiceGrpc .newBlockingStub(tracingClientInterceptor.intercept(managedChannel)); } @Bean public MyAppCodeLoaderServiceGrpc.MyAppCodeLoaderServiceBlockingStub myAppCodeLoaderServiceBlockingStub( TracingClientInterceptor tracingClientInterceptor, ManagedChannel managedChannel) { return MyAppCodeLoaderServiceGrpc .newBlockingStub(tracingClientInterceptor.intercept(managedChannel)); }
然后在调用Stub的业务代码中,每次调用前动态添加deadline:
// 注入基础Stub @Autowired private MyAppSenderServiceBlockingStub baseSenderStub; @Value("${grpc.client.deadline:300000}") private long grpcDeadline; // 业务方法中调用gRPC public void sendEvent(ContextMyAppEventGrpc event) { // 每次调用都创建一个带新deadline的Stub实例 baseSenderStub.withDeadlineAfter(grpcDeadline, TimeUnit.MILLISECONDS) .sendMessage(event, new StreamObserver<Empty>() { // 实现你的回调逻辑 @Override public void onNext(Empty empty) { // 处理响应 } @Override public void onError(Throwable throwable) { // 处理错误 } @Override public void onCompleted() { // 调用完成 } }); }
方案2:使用Supplier提供动态Stub(更优雅的配置层处理)
如果你希望在配置层封装deadline逻辑,可以用Supplier来每次返回一个带新deadline的Stub:
@Bean public Supplier<MyAppSenderServiceBlockingStub> myAppSenderServiceBlockingStubSupplier( TracingClientInterceptor tracingClientInterceptor, ManagedChannel managedChannel, @Value("${grpc.client.deadline:300000}") long deadline) { // 每次调用get()都会生成一个带新deadline的Stub return () -> MyAppSenderServiceGrpc .newBlockingStub(tracingClientInterceptor.intercept(managedChannel)) .withDeadlineAfter(deadline, TimeUnit.MILLISECONDS); }
在业务代码中通过Supplier获取Stub:
@Autowired private Supplier<MyAppSenderServiceBlockingStub> senderStubSupplier; public void sendEvent(ContextMyAppEventGrpc event, StreamObserver<Empty> responseObserver) { // 获取带最新deadline的Stub并调用 senderStubSupplier.get().sendMessage(event, responseObserver); }
额外注意点
- gRPC的Stub是不可变且线程安全的,所以每次调用
withDeadlineAfter生成新实例的开销极小,不用担心性能问题。 - 如果你需要不同的调用使用不同的deadline,方案1的灵活性更高,可以根据业务场景动态调整时长。
内容的提问来源于stack exchange,提问作者advortsov

