RPC远程过程调用,二进制封装,提供高吞吐低延迟的高性能RPC框架
大石头 authored at 2022-08-10 13:26:19
10.10 KiB
NewLife.Remoting
# NewLife.Remoting RPC 性能测试报告 ## 性能概览 NewLife.Remoting 是基于 TCP 的高性能 RPC 框架,提供 ApiServer/ApiClient 全链路远程调用能力。本次测试表明:**服务端纯处理能力达 1284 万 RPC/s,TCP 端到端吞吐量达 26.3 万 RPC/s(100 连接),单次无参调用延迟仅 52 μs,具备优秀的高并发扩展性。** --- ## 测试环境 | 项目 | 值 | |------|-----| | OS | Windows 10 (10.0.19045) | | CPU | Intel Core i9-10900K @ 3.70GHz (10 核 20 线程) | | Runtime | .NET 10.0.3, X64 RyuJIT x86-64-v3 | | BenchmarkDotNet | v0.15.8 | | GC | Non-concurrent Workstation | | SIMD | AVX2 VectorSize=256 | --- ## 测试方法 ### BenchmarkDotNet 基准测试(RpcBenchmarks) - 对 ApiClient → ApiServer 全链路 TCP 调用进行精确微基准测试 - 每个并发级别创建独立客户端连接,调用服务端 `BenchController` - 测试场景:无参返回 Int32、String 回显(16/2000 字符)、多参数调用、IPacket 回显(16/2000 字节) - 并发级别:1、4、16、20(= CPU 线程数)、32 ### 服务端纯处理能力测试(Direct) - 绕过 TCP 网络栈,直接调用 `ApiServer.Process()` 测量服务端原始处理能力 - 20 线程并发,预热 3 秒 + 正式测试 10 秒 ### 网络吞吐量压力测试(Throughput) - 100 个 TCP 客户端连接持续并发调用,测量端到端吞吐量和平均延迟 - 预热 3 秒 + 正式测试 10 秒 --- ## 测试结果 ### 1. BenchmarkDotNet 基准测试 | 场景 | 并发数 | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | |------|--------|-----:|------:|-------:|-----:|-----:|----------:| | 无参返回Int32 | 1 | 52.39 μs | 201.184 μs | 11.028 μs | 0.2441 | - | 3.22 KB | | String出入参(16字符) | 1 | 53.60 μs | 203.216 μs | 11.139 μs | 0.4883 | - | 5.60 KB | | String出入参(2000字符) | 1 | 116.12 μs | 86.841 μs | 4.760 μs | 3.6621 | - | 34.91 KB | | 多基础类型参数 | 1 | 53.56 μs | 88.185 μs | 4.834 μs | 0.8545 | - | 8.92 KB | | IPacket出入参(16字节) | 1 | 56.20 μs | 234.207 μs | 12.838 μs | 0.2441 | - | 3.38 KB | | IPacket出入参(2000字节) | 1 | 62.59 μs | 296.472 μs | 16.251 μs | 0.4883 | - | 5.32 KB | | 无参返回Int32 | 4 | 126.83 μs | 316.883 μs | 17.369 μs | 0.9766 | - | 11.90 KB | | String出入参(16字符) | 4 | 109.51 μs | 510.389 μs | 27.976 μs | 1.9531 | - | 21.45 KB | | String出入参(2000字符) | 4 | 116.69 μs | 35.898 μs | 1.968 μs | 14.4043 | 1.4648 | 138.18 KB | | 多基础类型参数 | 4 | 122.94 μs | 758.092 μs | 41.554 μs | 3.1738 | - | 34.72 KB | | IPacket出入参(16字节) | 4 | 130.50 μs | 296.237 μs | 16.238 μs | 0.9766 | - | 12.57 KB | | IPacket出入参(2000字节) | 4 | 125.15 μs | 521.811 μs | 28.602 μs | 1.9531 | - | 20.32 KB | | 无参返回Int32 | 16 | 277.94 μs | 1,141.172 μs | 62.551 μs | 3.4180 | - | 46.63 KB | | String出入参(16字符) | 16 | 262.05 μs | 48.643 μs | 2.666 μs | 6.3477 | - | 84.83 KB | | String出入参(2000字符) | 16 | 401.01 μs | 1,455.394 μs | 79.775 μs | 50.7813 | 10.7422 | 551.75 KB | | 多基础类型参数 | 16 | 408.00 μs | 2,450.887 μs | 134.341 μs | 9.7656 | - | 137.90 KB | | IPacket出入参(16字节) | 16 | 247.13 μs | 341.186 μs | 18.702 μs | 3.4180 | - | 49.33 KB | | IPacket出入参(2000字节) | 16 | 280.44 μs | 937.254 μs | 51.374 μs | 5.8594 | 0.4883 | 80.33 KB | | 无参返回Int32 | 20 | 279.41 μs | 18.081 μs | 0.991 μs | 3.9063 | - | 58.22 KB | | String出入参(16字符) | 20 | 434.32 μs | 2,162.381 μs | 118.527 μs | 6.8359 | - | 105.96 KB | | String出入参(2000字符) | 20 | 423.50 μs | 18.243 μs | 1.000 μs | 61.5234 | 15.6250 | 689.62 KB | | 多基础类型参数 | 20 | 462.79 μs | 2,810.361 μs | 154.045 μs | 11.7188 | - | 172.30 KB | | IPacket出入参(16字节) | 20 | 297.97 μs | 1.147 μs | 0.063 μs | 3.9063 | - | 61.58 KB | | IPacket出入参(2000字节) | 20 | NA | NA | NA | NA | NA | NA | | 无参返回Int32 | 32 | 520.81 μs | 2,181.539 μs | 119.578 μs | 4.8828 | - | 92.95 KB | | String出入参(16字符) | 32 | 467.86 μs | 27.738 μs | 1.520 μs | 8.7891 | - | 169.32 KB | | String出入参(2000字符) | 32 | 852.96 μs | 4,170.590 μs | 228.604 μs | 76.1719 | 19.5313 | 1,103.16 KB | | 多基础类型参数 | 32 | 791.80 μs | 5,006.656 μs | 274.432 μs | 13.6719 | - | 275.47 KB | | IPacket出入参(16字节) | 32 | 499.77 μs | 1,410.822 μs | 77.332 μs | 4.8828 | - | 98.32 KB | | IPacket出入参(2000字节) | 32 | NA | NA | NA | NA | NA | NA | > **注意**:IPacket(2000字节) 在并发 20 和 32 时出现异常(NA),可能与高并发下大数据包的内存竞争有关。 ### 2. 服务端纯处理能力测试(绕过 TCP,20 线程) | 场景 | 总请求数 | 耗时 | 吞吐量 | 每请求分配 | GC Gen0 | |------|-------:|-----:|-------:|----------:|--------:| | NoArg_ReturnInt32 | 128,533,012 | 10.01 秒 | **12,844,229 RPC/s** | 0 B | 9,115 | | EchoPacket_16B | 96,516,183 | 10.01 秒 | **9,642,161 RPC/s** | 0 B | 6,999 | ### 3. 网络吞吐量压力测试(TCP 端到端,100 连接) | 场景 | 总请求数 | 耗时 | 吞吐量 | 平均延迟 | GC Gen0 | GC Gen1 | |------|-------:|-----:|-------:|--------:|--------:|--------:| | NoArg_ReturnInt32 | 2,638,607 | 10.03 秒 | **263,184 RPC/s** | 380.0 μs | 707 | 0 | | EchoPacket_16B | 2,338,682 | 10.00 秒 | **233,812 RPC/s** | 427.7 μs | 608 | 7 | --- ## 核心指标 ### 单次调用延迟(BenchmarkDotNet,单线程) | 场景 | 延迟 | 内存分配 | 等效 QPS | |------|-----:|--------:|---------:| | 无参返回Int32 | 52.39 μs | 3.22 KB | ~19,088 | | String回显(16字符) | 53.60 μs | 5.60 KB | ~18,657 | | String回显(2000字符) | 116.12 μs | 34.91 KB | ~8,612 | | 多基础类型参数 | 53.56 μs | 8.92 KB | ~18,671 | | IPacket回显(16字节) | 56.20 μs | 3.38 KB | ~17,794 | | IPacket回显(2000字节) | 62.59 μs | 5.32 KB | ~15,977 | ### 服务端纯处理峰值 | 指标 | 值 | |------|---:| | NoArg 峰值 | **1284 万 RPC/s** | | EchoPacket_16B 峰值 | **964 万 RPC/s** | | 每请求堆分配 | **0 B**(已完全池化) | ### TCP 端到端峰值(100 连接) | 指标 | 值 | |------|---:| | NoArg 峰值 | **26.3 万 RPC/s** | | EchoPacket_16B 峰值 | **23.4 万 RPC/s** | | NoArg 平均延迟 | **380 μs** | | EchoPacket_16B 平均延迟 | **428 μs** | --- ## 对比分析 ### 纵向分析:并发扩展性 以**无参返回Int32**为例,观察不同并发级别的性能变化: | 并发数 | Mean | 每连接延迟 | 等效总 QPS | 扩展效率 | |-------:|-----:|----------:|---------:|---------:| | 1 | 52.39 μs | 52.39 μs | 19,088 | 基准 | | 4 | 126.83 μs | 31.71 μs | 31,540 | 41.4% | | 16 | 277.94 μs | 17.37 μs | 57,566 | 18.9% | | 20 | 279.41 μs | 13.97 μs | 71,579 | 18.8% | | 32 | 520.81 μs | 16.28 μs | 61,449 | 10.1% | - **最优并发点为 20(= CPU 线程数)**,总吞吐量约 7.2 万 QPS,每连接延迟最低 13.97 μs - 并发 32 超过 CPU 线程数后,总延迟上升 86%,等效 QPS 反而下降,出现线程竞争 - 并发 1→4 扩展效率最高(41.4%),4→16 仍有显著提升,超过核心数后收益递减 ### 横向分析:不同场景对比(并发 = 1) | 场景 | 延迟 | vs 基准 | 内存 | vs 基准 | |------|-----:|--------:|-----:|--------:| | 无参返回Int32(基准) | 52.39 μs | - | 3.22 KB | - | | String回显(16字符) | 53.60 μs | +2.3% | 5.60 KB | +73.9% | | 多基础类型参数 | 53.56 μs | +2.2% | 8.92 KB | +177.0% | | IPacket回显(16字节) | 56.20 μs | +7.3% | 3.38 KB | +5.0% | | IPacket回显(2000字节) | 62.59 μs | +19.5% | 5.32 KB | +65.2% | | String回显(2000字符) | 116.12 μs | +121.6% | 34.91 KB | +984.5% | - **IPacket 是最高效的数据传输方式**:2000 字节 IPacket 仅比无参慢 19.5%,而 2000 字符 String 慢 121.6% - IPacket 2000 字节内存分配仅 5.32 KB,String 2000 字符高达 34.91 KB(6.6 倍差距) - 小数据量(≤16 字符/字节)时各场景延迟差异不大(2~7%),RPC 框架开销占主导 ### 处理瓶颈分析:纯处理 vs TCP 端到端 | 指标 | 纯处理(Direct) | TCP 端到端 | TCP 开销占比 | |------|----------------:|-----------:|------------:| | NoArg 吞吐量 | 12,844,229 RPC/s | 263,184 RPC/s | 97.95% | | EchoPacket_16B 吞吐量 | 9,642,161 RPC/s | 233,812 RPC/s | 97.58% | - TCP 网络栈开销占总延迟的 **97%~98%**,服务端处理本身极轻 - 纯处理场景已实现零堆分配(0 B/req),GC 压力来自消息编解码的临时对象 --- ## 性能瓶颈定位 1. **TCP 网络栈是主要瓶颈**:纯处理 1284 万 vs TCP 端到端 26.3 万,差距约 49 倍。TCP 的系统调用、缓冲区拷贝、异步回调是主要开销 2. **大 String 序列化开销显著**:2000 字符 String 的延迟是无参的 2.2 倍,内存分配是 10.8 倍,JSON 序列化/反序列化是主因 3. **IPacket 大数据包高并发不稳定**:2000 字节 IPacket 在并发 ≥20 时出错(NA),可能存在缓冲区竞争或 Dispose 时序问题 4. **超核心数并发收益递减**:并发 32 时性能反而低于并发 20,线程上下文切换和锁竞争成为瓶颈 --- ## 优化建议 | 优先级 | 建议 | 预期收益 | |:------:|------|---------| | P0 | **排查 IPacket(2000B) 高并发异常**:在并发 ≥20 时复现错误,检查 ArrayPacket/OwnerPacket 的线程安全和 Dispose 时序 | 消除高并发大包场景的不稳定性 | | P1 | **启用连接复用(Multiplex)**:当前代码已注释,恢复后可在单连接上并行处理多请求,减少连接管理开销 | TCP 吞吐量预计提升 30~50% | | P2 | **IO 优化**:考虑使用 `System.IO.Pipelines` 替代传统 Stream 模型,减少缓冲区拷贝 | 降低 TCP 层开销,缩小纯处理与端到端差距 | | P3 | **大 String 场景优化**:对于大文本传输,建议业务侧先转为 byte[] 或 IPacket,避免 JSON 序列化/反序列化的内存开销 | 大文本延迟降低约 50%,内存分配降低约 85% | | P4 | **池化消息对象**:在 BenchmarkDotNet 测试中观察到 Gen0 GC 随并发线性增长,可进一步池化 DefaultMessage 及其 Payload | 降低 GC 压力,改善 P99 延迟抖动 |