为何我的基准测试中gRPC性能不及REST?求排查原因
疑问:gRPC基准测试性能不如REST,是配置问题还是结果合理?
近期开始使用gRPC,一直听闻它比REST性能更优,因此搭建了基准测试项目验证性能差距,但多次测试后均发现REST性能略优于gRPC。由于在基准测试和gRPC方面经验不足,怀疑是测试配置存在问题,想知道:测试配置哪里出错了?还是该结果本身合理?
本次针对BenchmarkWithSamePayload测试用例。
最新测试结果
// * Summary * BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3155/23H2/2023Update/SunValley3) AMD Ryzen 5 5600X, 1 CPU, 12 logical and 6 physical cores .NET SDK 8.0.201 [Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2 DefaultJob : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2 | Method | Mean | Error | StdDev | |-------------- |---------:|--------:|--------:| | BenchmarkGrpc | 147.2 us | 0.71 us | 0.59 us | | BenchmarkRest | 111.6 us | 0.68 us | 0.90 us | // * Hints * Outliers BenchmarkWithSamePayload.BenchmarkGrpc: Default -> 2 outliers were removed, 3 outliers were detected (145.56 us, 150.79 us, 152.36 us) BenchmarkWithSamePayload.BenchmarkRest: Default -> 8 outliers were removed (118.18 us..126.86 us) // * Legends * Mean : Arithmetic mean of all measurements Error : Half of 99.9% confidence interval StdDev : Standard deviation of all measurements 1 us : 1 Microsecond (0.000001 sec)
相关代码
gRPC部分
Protos定义
rpc SayHello (HelloRequest) returns (HelloReply); message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
gRPC客户端代码
public class Sender { private GrpcChannel _grpcChannel; private Greeter.GreeterClient _greeter; private HelloRequest _defaultRequest; public Sender() { _grpcChannel = GrpcChannel.ForAddress("http://localhost:5264"); _greeter = new Greeter.GreeterClient(_grpcChannel); _defaultRequest = new HelloRequest() { Name = "default" }; } public async Task<string> PostDefault() { var reply = await _greeter.SayHelloAsync(_defaultRequest); return reply.Message; } }
gRPC服务端代码
Program.cs
using GrpcService.Services; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddGrpc(); var app = builder.Build(); // Configure the HTTP request pipeline. app.MapGrpcService<Service>(); app.Run();
Service实现
public class Service : Greeter.GreeterBase { public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { return Task.FromResult(new HelloReply { Message = "Hello " + request.Name }); } }
REST部分
消息定义
HelloRequest
public class HelloRequest { public string Name { get; set; } }
HelloResponse
public class HelloResponse { public string Message { get; set; } }
REST客户端代码
public class Sender { private HttpClient _httpClient; private HelloRequest _defaultRequest; public Sender() { _httpClient = new HttpClient(); _defaultRequest = new HelloRequest() { Name = "default" }; } public async Task<string> PostDefault() { var content = JsonContent.Create(_defaultRequest); var response = await _httpClient.PostAsync("http://localhost:5082/greet", content); var responseString = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize<HelloResponse>(responseString)!.Message; } }
REST服务端代码
var builder = WebApplication.CreateBuilder(args); // Add services to the container. var app = builder.Build(); app.MapPost("/greet", (HelloRequest request) => { return new HelloResponse() { Message = "Hello " + request.Name }; }); app.Run();
Benchmark部分
Program.cs
var summary = BenchmarkDotNet.Running.BenchmarkRunner.Run<BenchmarkWithSamePayload>();
BenchmarkWithSamePayload测试类
public class BenchmarkWithSamePayload : BenchmarkBase { [Benchmark] public async Task<string> BenchmarkGrpc() { return await _gRpcSender.PostDefault(); } [Benchmark] public async Task<string> BenchmarkRest() { return await _restSender.PostDefault(); } }
BenchmarkBase基类
namespace BenchmarkRunner { using gRpcSender = GrpcClient.Sender; using RestSender = RestClient.Sender; public abstract class BenchmarkBase { protected gRpcSender _gRpcSender; protected RestSender _restSender; [GlobalSetup] public void Setup() { _gRpcSender = new gRpcSender(); _restSender = new RestSender(); } } }
BenchmarkConfig配置类(因杀毒软件要求)
public class BenchmarkConfig : ManualConfig { public BenchmarkConfig() { AddJob(Job.MediumRun.WithToolchain(InProcessNoEmitToolchain.Instance)); } }
分析与解答
一、当前测试结果的合理性
你的测试结果在小payload场景下REST略优是合理的,原因如下:
gRPC的性能优势核心在于大payload传输、高并发多路复用、流式场景,而当前测试仅传递简短字符串:
- 小数据量下,Protobuf与JSON的序列化/反序列化性能差异微乎其微
- gRPC基于HTTP/2,握手、头部压缩等额外逻辑会产生少量固定开销,反而比HTTP/1.1的REST单次请求耗时略高
二、测试配置的潜在优化点
进程内测试的干扰
使用InProcessNoEmitToolchain会让基准测试与服务同进程运行,容易引发线程池、端口等资源竞争,建议将gRPC和REST服务作为独立进程启动后再执行测试(若杀毒软件允许)。连接复用的优势未体现
当前测试仅测单次请求,gRPC的长连接、HTTP/2多路复用优势无法发挥,建议增加并发测试或多次请求的吞吐量测试(测每秒处理请求数)。缺少预热逻辑
尽管BenchmarkDotNet会自动预热,但网络请求类测试建议在GlobalSetup中手动调用几次接口,让连接池、序列化器完成初始化,避免首次请求的冷启动开销影响结果。
三、优化测试的方向
- 增加大payload测试:构造包含嵌套结构、大量字段的请求,对比Protobuf与JSON的序列化性能差异
- 测试吞吐量:用
[Benchmark(Baseline = true)]标记基准,关注每秒请求数(Ops/s),gRPC在高并发下的优势会更明显 - 测试流式场景:验证gRPC的客户端/服务端双向流式能力,这是REST不具备的核心优势
- 优化gRPC配置:调整
GrpcChannelOptions的连接参数、重试策略,最大化通道复用效率
内容的提问来源于stack exchange,提问作者Henrique
相关产品推荐
相关产品推荐

