Spring WebFlux集成gRPC客户端异步写法合规性与性能疑问
项目背景
我们团队的项目是一款接收请求后转发至业务处理机的产品,采用Spring Boot WebFlux作为服务框架,框架内使用gRPC作为客户端向业务处理机器发送请求。
整体调用链路如下:
用户 <-> WebFlux <-> gRPC <-> 实际执行业务逻辑的机器
选型时我们主要看中WebFlux和gRPC的异步非阻塞特性,其中gRPC采用流式调用模式。
现有实现代码
写法1:基于CompletableFuture桥接
@PostMapping("xxxx") public Mono<String> mac() { final CompletableFuture<String> future = new CompletableFuture<>(); final StreamObserver<MacMessage> request = gRPCService.getStub.mac(new StreamObserver<>() { String mac; @Override public void onNext(MacMessage value) { mac = Base64.encode(value.getMac().toByteArray()); } @Override public void onError(Throwable t) { future.completeExceptionally(t); } @Override public void onCompleted() { future.complete(mac); } }); request.onNext(MacMessage.newBuilder().setXxxx(...).build()); request.onCompleted(); return Mono.fromFuture(future); }
写法2:基于Mono.create直接桥接
@PostMapping("xxxx") public Mono<String> mac() { return Mono.create(monoSink -> { final StreamObserver<MacMessage> request = gRPCService.getStub.mac(new StreamObserver<>() { String mac; @Override public void onNext(MacMessage value) { mac = Base64.encode(value.getMac().toByteArray()); } @Override public void onError(Throwable t) { monoSink.error(t); } @Override public void onCompleted() { monoSink.success(mac); } }); request.onNext(MacMessage.newBuilder().setXxxx(...).build()); request.onCompleted(); }); }
问题解答
问题1:两种写法是否满足异步非阻塞要求?如何正确实现?
这两种写法核心逻辑已经满足异步非阻塞要求,没有出现阻塞Netty事件循环线程的问题,但存在几个细节漏洞,修复后才是生产可用的正确实现:
- 先修正基础编码笔误:原代码存在
pulbic拼写错误、COmpletableFuture大小写错误、getMac漏写方法括号、接口路径使用中文全角引号等问题,这类问题和异步逻辑无关但会直接导致编译失败。 - 补充取消信号处理:两种写法都没有处理订阅者取消订阅的场景(比如客户端提前断开连接),会导致gRPC请求在后台空跑浪费资源。
Mono.create写法可以通过sink的取消回调,在触发时调用request.onError(Status.CANCELLED.asRuntimeException())终止gRPC流;CompletableFuture写法需要给future增加取消监听器,触发时同步终止gRPC流。 - 明确Stub调度规则:如果使用默认
newStub创建的异步Stub,所有StreamObserver回调默认运行在gRPC自身的事件循环线程上,回调逻辑绝对不能加阻塞操作,否则会卡死gRPC事件循环;如果回调存在必要阻塞逻辑,要提前通过publishOn切换到对应弹性调度器。 - 做空值防御:如果gRPC流异常中断导致
onNext从未触发,onCompleted执行时mac变量会是null,直接传给future.complete或者monoSink.success会抛出空指针,Reactor规范不允许Mono发出null值,需要提前判空,无有效值时返回对应业务错误。
问题2:为什么onNext里直接打印的性能远高于返回响应给前端的性能,且两种桥接写法性能接近?
性能差异本质是两种场景的链路长度和实际执行的工作量完全不在一个量级,和桥接用CompletableFuture还是Mono.create几乎没有关系:
- 直接在onNext里打印的场景,gRPC收到响应后只需要做一次Base64编码、一次控制台输出就结束了整个流程,完全不涉及WebFlux响应编码、HTTP协议序列化、网络IO回传、TCP流控、背压协调这些重逻辑,链路极短自然性能高。而且多数日志框架的控制台输出是异步刷盘,实际开销远低于直观感受。
- 两种桥接写法的最终目标都是把结果封装成Mono交还给WebFlux框架,后续要走完全相同的处理流程:把字符串序列化成HTTP响应体、按照对应HTTP版本组装响应帧、写回TCP连接、等待TCP层发送确认、处理连接流控和背压,这些步骤的开销比gRPC本地接收响应、桥接Reactor的开销大两个数量级以上,桥接层CompletableFuture和MonoSink的性能差异在整个链路里占比不到1%,所以测试时两种写法性能几乎一致。
- 补充说明:用控制台打印场景做性能基准测试的结果完全没有参考价值,高并发下日志打印本身会成为性能瓶颈,且该场景省略了真实业务中占比最高的序列化、网络回传开销,测出来的数值不能代表真实服务性能。
内容的提问来源于stack exchange,提问作者TJwoods
相关产品推荐
相关产品推荐

