在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
相关产品推荐
相关产品推荐

