为何gRPC-Java的UDS性能不如DNS?性能测试问题求助
我们开展了UDS与DNS(解析到localhost的TCP)的gRPC性能对比测试,按网络资料所述,UDS因无需多层网络协议应该更快,但实际测试发现UDS最多与TCP性能持平,多数情况下表现更差,以下为测试用例及结果。
环境
- jdk: jdk-17.0.10
- grpc: 1.61.1
- 测试机器: centos 7.9、64核CPU、251 GB内存
代码细节
UDS服务端核心代码
String UDS_FILE="/tmp/grpc_uds.socket"; server = NettyServerBuilder.forAddress(new DomainSocketAddress(UDS_FILE)) .bossEventLoopGroup(new EpollEventLoopGroup()) .workerEventLoopGroup(new EpollEventLoopGroup()) .channelType(EpollServerDomainSocketChannel.class) .maxInboundMessageSize(4*1024*1024) .addService(BrSpringApplication.getBean(GreeterServiceImple.class)) .build().start(); blockUtilShutDown();
UDS客户端核心代码
String socketPath="/tmp/grpc_uds.socket"; channel = NettyChannelBuilder.forAddress(new DomainSocketAddress(socketPath)) .eventLoopGroup(new EpollEventLoopGroup()) .channelType(EpollDomainSocketChannel.class) .maxInboundMessageSize(4 * 1024 * 1024) .usePlaintext().build(); GreeterGrpc.GreeterBlockingStub blockStub = GreeterGrpc.newBlockingStub(channel); HelloRequest request = HelloRequest.newBuilder().setName(paramJson.toJSONString()).build(); HelloReply response = getBlockStub().sayHello(request);
GreeterServiceImpl.java
@Override public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) { long startTime = System.currentTimeMillis(); logger.warn("request:{}", request.getName()); try { if (Context.current().isCancelled()) { logger.warn("deadline cancalled,deadline:{}", Context.current().getDeadline()); responseObserver.onError(new Exception("Cancelled by client")); return; } JSONObject msgRequest= JSONObject.parseObject(request.getName()); int num=msgRequest.getIntValue("factor"); String msg=msgRequest.getString("message"); StringBuilder messageBack = new StringBuilder("rep-"); for (int i = 0; i < num; i++) { messageBack.append(msg).append("|"); } // logger.warn("response:{}", messageBack); HelloReply response = HelloReply.newBuilder().setMessage(messageBack + ",ms:" + (System.currentTimeMillis() - startTime)).build(); responseObserver.onNext(response); responseObserver.onCompleted(); } catch (BrException e) { logger.error("BrException,code:{},message:{}", e.getCode(), e.getMessage()); HelloReply response = HelloReply.newBuilder().setMessage(e.getMessage()).build(); responseObserver.onNext(response); responseObserver.onCompleted(); } catch (Exception ex) { logger.error("sys error", ex); HelloReply response=HelloReply.newBuilder().setMessage(ex.getMessage()).build(); responseObserver.onNext(response); responseObserver.onCompleted(); } }
DNS(TCP)服务端核心代码
String port="80"; server = NettyServerBuilder.forPort(port) .maxInboundMessageSize(4*1024*1024) .addService(BrSpringApplication.getBean(GreeterServiceImple.class)) .build().start(); blockUtilShutDown();
DNS(TCP)客户端核心代码
String ip="localhost:80" channel = ManagedChannelBuilder.forTarget(ip).usePlaintext().build(); GreeterGrpc.GreeterBlockingStub blockStub = GreeterGrpc.newBlockingStub(channel); HelloRequest request = HelloRequest.newBuilder().setName(paramJson.toJSONString()).build(); HelloReply response=getBlockStub().sayHello(request);
测试结果
| 客户端消息大小 | 响应消息大小 | 并发数 | 总请求数 | 类型 | 测试次数 | 总耗时(ms) | QPS |
|---|---|---|---|---|---|---|---|
| 12B | 60B | 50 | 5000000 | dns | 1 | 127359ms | 5535.79 |
| 12B | 60B | 50 | 5000000 | dns | 2 | 126051ms | 5593.23 |
| 12B | 60B | 50 | 5000000 | dns | 3 | 128828ms | 5472.67 |
| 12B | 60B | 50 | 5000000 | uds | 1 | 133212ms | 5292.56 |
| 12B | 60B | 50 | 5000000 | uds | 2 | 128315ms | 5494.55 |
| 12B | 60B | 50 | 5000000 | uds | 3 | 127177ms | 5543.71 |
| 12B | 120B | 50 | 5000000 | dns | 1 | 125919ms | 5599.10 |
| 12B | 120B | 50 | 5000000 | dns | 2 | 122034ms | 5777.35 |
| 12B | 120B | 50 | 5000000 | dns | 3 | 120439ms | 5853.86 |
| 12B | 120B | 50 | 5000000 | uds | 1 | 123538ms | 5707.01 |
| 12B | 120B | 50 | 5000000 | uds | 2 | 126988ms | 5551.96 |
| 12B | 120B | 50 | 5000000 | uds | 3 | 126450ms | 5575.58 |
| 12B | 6kb | 50 | 5000000 | dns | 1 | 123176ms | 5723.78 |
| 12B | 6kb | 50 | 5000000 | dns | 2 | 127242ms | 5540.88 |
| 12B | 6kb | 50 | 5000000 | dns | 3 | 121556ms | 5800.07 |
| 12B | 6kb | 50 | 5000000 | uds | 1 | 131939ms | 5343.63 |
| 12B | 6kb | 50 | 5000000 | uds | 2 | 133399ms | 5285.14 |
| 12B | 6kb | 50 | 5000000 | uds | 3 | 130091ms | 5419.53 |
| 5kb | 5kb | 50 | 5000000 | dns | 1 | 133404ms | 5284.94 |
| 5kb | 5kb | 50 | 5000000 | dns | 2 | 130532ms | 5401.23 |
| 5kb | 5kb | 50 | 5000000 | dns | 3 | 130145ms | 5417.29 |
| 5kb | 5kb | 50 | 5000000 | uds | 1 | 140809ms | 5007.01 |
| 5kb | 5kb | 50 | 5000000 | uds | 2 | 140685ms | 5011.43 |
| 5kb | 5kb | 50 | 5000000 | uds | 3 | 140992ms | 5000.52 |
注:原表格中"thead"列应为并发数,已修正。
原因分析
本地TCP的内核级优化
当TCP连接指向localhost时,Linux内核会通过loopback接口处理,完全绕过物理网卡和网络协议栈的下层(如ARP、以太网帧),仅保留TCP/IP的核心逻辑,性能损耗极低。而UDS虽然省去了IP层封装,但内核对TCP的优化(如TCP小数据包合并、缓冲区调优、连接复用机制)经过数十年打磨,成熟度更高,在高并发场景下可能反超UDS。gRPC配置的不对称性
你的UDS服务端和客户端显式创建了EpollEventLoopGroup,但未指定线程数,默认线程数为CPU核心数。而TCP版本使用的是gRPC默认的EventLoopGroup,其线程模型可能更适配本地TCP场景。此外,UDS的Netty通道实现(EpollDomainSocketChannel)在gRPC中的优化程度可能不如TCP通道,存在额外的上下文切换或数据拷贝开销。业务逻辑开销掩盖传输层差异
测试中的服务端包含JSON解析、字符串循环拼接等CPU密集型操作,这些开销占总请求耗时的比例较高,会稀释UDS和TCP在传输层的性能差异。如果将业务逻辑替换为无计算的echo服务,传输层的性能差距会更明显。UDS的文件系统无影响
你将UDS路径设置到SSD后性能无变化,是因为/tmp目录默认挂载为tmpfs(内存文件系统),UDS socket的元数据和数据都在内存中,与磁盘介质无关,因此调整磁盘类型不会带来性能提升。连接复用与初始化开销
测试中使用的是阻塞Stub,若未启用连接复用(gRPC默认启用,但需确认配置),UDS的连接初始化开销可能略高于TCP。不过你的测试总请求数达500万,连接复用应该已经生效,这部分影响较小。
优化建议
- 统一EventLoopGroup配置:为TCP版本也显式配置
EpollEventLoopGroup,确保两者线程模型一致,排除配置差异影响。 - 简化业务逻辑:使用纯echo服务重新测试,聚焦传输层性能差异。
- 调整UDS缓冲区大小:通过Netty的
option(ChannelOption.SO_SNDBUF, size)和option(ChannelOption.SO_RCVBUF, size)调整UDS的收发缓冲区,匹配TCP的默认配置。 - 升级gRPC版本:新版本可能对UDS实现有性能优化,尝试升级到最新稳定版后再测试。
内容的提问来源于stack exchange,提问作者Chao Zhang

