You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在JUnit测试中运行gRPC服务端与客户端偶现死锁问题排查

排查gRPC-Spring-Boot-Starter流式调用挂起问题的思路

我之前碰到过类似的gRPC阻塞式流式调用挂起的情况,结合你的场景——用@GrpcClient注入的stub调用forEachRemaining()时挂起,但手动创建的stub正常——给你几个具体的排查方向:

1. 检查Spring gRPC客户端的线程池配置

Spring管理的gRPC客户端默认会使用框架提供的线程池,而你手动创建的stub用的是gRPC默认的线程池。如果Spring配置的线程池出现资源耗尽或线程阻塞,很可能触发ClientCalls#waitAndDrain的死锁。

  • 尝试为目标客户端单独配置线程池,在application.yml里添加:
    grpc:
      client:
        stocks:
          executor:
            core-size: 10
            max-size: 20
            queue-capacity: 50
    
  • 测试时可以通过开启io.grpc的DEBUG日志,查看线程池的使用情况,是否有线程被长时间占用。

2. 抓取线程栈分析死锁场景

ClientCalls#waitAndDrain的死锁通常是调用线程与gRPC事件循环线程相互等待导致的。你可以在测试挂起时用jstack <pid>命令抓取线程栈,重点看:

  • 是否有线程阻塞在io.grpc.stub.ClientCalls.waitAndDrain()方法,等待的条件对象是什么?
  • 是否有其他线程持有某个锁(比如Spring上下文锁、线程池锁),同时在等待该线程释放资源?
    典型的死锁会看到两个线程互相持有对方需要的锁,通过线程栈能快速定位冲突点。

3. 调整测试服务的启动方式

你当前在@BeforeAll里手动启动测试服务线程,可能和Spring测试上下文的线程模型冲突。试试让Spring来管理测试服务的生命周期:

@SpringBootTest
class TestGrpc {

    @GrpcClient("stocks")
    private StockStaticDataRequestServiceBlockingStub stub;

    @TestConfiguration
    static class TestServerConfig {
        @Bean(destroyMethod = "shutdown")
        public Server testGrpcServer() throws IOException {
            Server server = ServerBuilder.forPort(9091)
                    .addService(new StockStaticDataRequestTestService())
                    .build();
            server.start();
            return server;
        }
    }

    // ... 测试方法不变
}

这样Spring会在测试上下文启动时初始化服务,避免手动线程带来的线程安全问题。

4. 排查Spring gRPC客户端的包装逻辑

@GrpcClient注入的stub会被Spring框架包装,可能添加了拦截器、重试、负载均衡等额外逻辑,这些组件可能导致流式调用的阻塞:

  • 先简化客户端配置,禁用所有额外功能:
    grpc:
      client:
        stocks:
          address: 'static://localhost:9091'
          negotiationType: plaintext
          enableKeepAlive: false
          interceptors: []
          retry:
            enabled: false
          loadbalancingPolicy: 'round_robin' # 和手动创建的保持一致
    
  • 如果简化后问题消失,再逐个恢复配置,排查是哪个组件导致的挂起。

5. 验证版本兼容性

检查你的技术栈版本是否匹配:gRPC-Spring-Boot-Starter 2.13.1官方推荐的gRPC版本是多少?比如该版本的starter可能兼容gRPC 1.45.x而不是1.44.0。

  • 尝试升级gRPC到starter推荐的版本,或者降级starter到匹配gRPC 1.44.0的版本,看是否能解决问题。

6. 调试流式响应的传递过程

  • 在测试服务的getManyStockStatics方法里添加日志,确认onNext和onCompleted是否被正确调用,且没有抛出异常:
    @Override
    public void getManyStockStatics(StockStaticManyDataRequest request, StreamObserver<Security> responseObserver) {
        LOG.info("Received request: {}", request);
        responseObserver.onNext(Security.newBuilder().setSecurity("TEST-MANY").build());
        LOG.info("Sent first response");
        responseObserver.onNext(Security.newBuilder().setSecurity("TEST-MORE").build());
        LOG.info("Sent second response");
        responseObserver.onCompleted();
        LOG.info("Completed stream");
    }
    
  • 把forEachRemaining改成手动遍历迭代器,看是否同样挂起:
    @Test
    void testClient() {
        StockStaticManyDataRequest request = StockStaticManyDataRequest.newBuilder()
                .addAllTickerSymbols(List.of("AAPL"))
                .build();
        Iterator<Security> iterator = stub.getManyStockStatics(request);
        while (iterator.hasNext()) {
            Security security = iterator.next();
            LOG.info("security={}", security);
        }
    }
    

如果手动遍历正常,可能是lambda表达式捕获了某些导致线程阻塞的资源(比如Spring上下文的Bean)。

内容的提问来源于stack exchange,提问作者mats

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 13:27:42