gRPC调用性能差主因是Channel未复用、HTTP/2未配对、proto未对齐;须全局单例复用GrpcChannel,Windows明文HTTP/2需显式开启开关,csharp_namespace与proto路径须全项目统一。

gRPC 调用性能差,90% 不是业务逻辑慢,而是 Channel 没复用、HTTP/2 没配对、proto 生成没对齐。
GrpcChannel 必须全局单例复用,别在每次调用里 new
每次 GrpcChannel.ForAddress() 都会新建 TCP 连接 + TLS 握手 + HTTP/2 协商,开销远超业务本身。压测时看到大量 Status(StatusCode=Unavailable, Detail="Connection reset"),基本就是这个原因。
- 正确做法:在
Program.cs或 DI 容器中注册单例GrpcChannel,所有服务共用一个实例 - 若用
Grpc.Net.ClientFactory,它默认帮你做了复用,但必须通过 DI 获取客户端(IServiceProvider.GetService<Greeter.GreeterClient>()),不能手动 new - 非 DI 场景下,自己用
Lazy<GrpcChannel>或static readonly缓存,避免锁竞争 - 注意:Channel 是线程安全的,但
Client实例不是必须每次 new —— 它只是轻量包装,不带状态
HTTP/2 明文支持必须显式开启(尤其开发环境)
Windows 上用 http://localhost:5001 调用 gRPC,默认会被降级到 HTTP/1.1,双向流直接失败,错误提示却是模糊的 Status(StatusCode=Internal, Detail="Error starting gRPC call.")。
- 客户端启动前加开关:
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true) - 服务端 Kestrel 必须明确启用 HTTP/2:
listenOptions.Protocols = HttpProtocols.Http2,且开发环境可配UseHttps()或UseHttpServer()(仅限测试) - Linux/macOS 默认支持明文 HTTP/2;Windows 10 1607+ 才支持,旧系统必须走 HTTPS
- 别信“Kestrel 自动协商”——它只在 TLS 启用且 ALPN 可用时才生效
csharp_namespace 和 proto 文件路径必须全项目统一
服务端和客户端各自拷贝一份 link.proto,只改了字段编号或 csharp_namespace,编译能过,运行时报 Cannot convert from 'X' to 'Y' 或 Type not found,根源是命名空间分裂。
-
csharp_namespace值必须是合法 C# 命名空间(如LinkService),不能含空格、横线、点号以外字符 - 所有项目(服务端、客户端、共享 Protos 类库)必须引用同一份
.proto文件,用<Protobuf Include="..\Shared\link.proto" GrpcServices="Both"/>而非复制粘贴 - 字段编号(如
string Name = 1;)一旦发布就不能改,否则序列化字节错位,无任何明确异常,只有空响应或乱码 - 若用
Grpc.AspNetCore,服务端项目必须设GrpcServices="Server"或"Both",客户端用"Client",否则生成代码缺失
双向流卡死?根本不是 await 顺序问题,是流生命周期没对齐
写成「先 await foreach 收完所有请求,再 WriteAsync 发响应」,表面看逻辑通,实际破坏了流式语义:服务端发一半,客户端已关流,连接静默中断。
- 服务端必须并发驱动双流:
Task.Run(() => ReadLoop())和Task.Run(() => WriteLoop())同时跑,共用同一个CancellationToken - 客户端调用
RequestStream.WriteAsync()后,别急着await RequestStream.CompleteAsync(),除非你真要关闭请求流 - 务必捕获
OperationCanceledException:网络抖动、对方断连、超时都会抛这个,不处理会导致整个 call 崩溃 - 高频小消息场景下,
IAsyncEnumerable<T>比Task<T>更吃 CPU——每个yield return都触发一次 HTTP/2 DATA 帧封装,帧头开销可能超过数据本身
真正卡住的地方,往往藏在 csharp_namespace 的拼写、GrpcServices 的配置值、或者那行被注释掉的 AppContext.SetSwitch 里。



















