gRPC 与微服务通信

全面讲解 gRPC 在 .NET 微服务中的应用,覆盖 Protobuf 契约与代码生成、服务定义与实现、拦截器横切逻辑、流式 RPC、健康检查与网关接入,以及性能与安全调优实践。

1. gRPC 与 Protobuf 概览

一句话总结: gRPC 基于 HTTP/2,用 Protobuf 做二进制序列化,契约先行、多语言互通,是微服务间内部调用的高效选择。

gRPC 由 Google 开源,默认传输为 HTTP/2,序列化为 Protocol Buffers(.proto 文件定义)。相比 REST/JSON,它在性能(二进制、头部压缩、多路复用)与契约严格性(强类型 schema)上更胜一筹,特别适合服务间高频、低延迟的调用。

// 一个最小的 Proto 契约
syntax = "proto3";

package order;

service OrderService {
  rpc GetOrder (GetOrderRequest) returns (Order);
  rpc CreateOrder (CreateOrderRequest) returns (Order);
}

message GetOrderRequest {
  int32 id = 1;
}

message Order {
  int32 id = 1;
  string customer = 2;
  double total = 3;
  repeated OrderLine lines = 4;
}

message OrderLine {
  int32 product_id = 1;
  int32 quantity = 2;
}
特性gRPCREST/JSON
传输协议HTTP/2HTTP/1.1 为主
序列化Protobuf 二进制JSON 文本
契约.proto 强类型OpenAPI 文档
多路复用支持需连接池
适用服务间调用浏览器/开放 API

避坑: gRPC 的客户端需要专门的 stub,浏览器默认不支持 HTTP/2 裸 gRPC。面向浏览器的场景要用 gRPC-Web 或继续用 REST。契约一旦发布,字段编号(= 1)不能改动——那是二进制协议的兼容性根基。

2. Proto 契约与服务定义

一句话总结: .proto 是服务的唯一事实来源,message 字段编号、命名空间与 package 约定决定了跨语言的兼容性与代码组织。

定义 Proto 时,package 决定生成代码的命名空间,service 声明一组 RPC,message 声明数据结构。字段编号从 1 开始,删除字段要留编号占位(用 reserved),避免复用编号造成兼容性灾难。

syntax = "proto3";

package shop.orders.v1;   // 生成命名空间 Shop.Orders.V1

import "google/protobuf/timestamp.proto";

message Order {
  int32 id = 1;
  string customer = 2;
  double total = 3;
  google.protobuf.Timestamp created_at = 4;
  repeated OrderLine lines = 5;

  reserved 6;            // 已删除字段,编号不可复用
  reserved "legacy_flag";
}

service OrderApi {
  rpc ListOrders (ListOrdersRequest) returns (ListOrdersResponse);
  rpc GetOrder (GetOrderRequest) returns (Order);
  rpc WatchOrders (WatchOrdersRequest) returns (stream Order); // 服务端流
}
规则说明
字段编号1~2^29-1,生成后不可改
删除字段reserved 占位防复用
版本策略用 package 分层(v1/v2)
可选字段optional 显式表达缺失
枚举首个值必须为 0

避坑: 跨团队协作时,把 .proto 当契约评审对象:字段语义、命名、编号分配都要走评审。接口变更优先「加字段 + 后向兼容」,破坏性变更才升版本。别在 message 里塞业务逻辑依赖的魔法编号,一切以 schema 为准。

3. 代码生成与服务实现

一句话总结: .NET 的 Grpc.Tools 在编译期从 .proto 生成强类型客户端与服务器基类,开发者只实现基类方法即可。

在 .csproj 里引用 Grpc.AspNetCore 与 Grpc.Tools,把 .proto 标记为 Protobuf item,编译时自动生成代码。服务器继承生成的 OrderApiBase,客户端使用生成的 OrderApiClient。

<ItemGroup>
  <Protobuf Include="Protos\orders.proto" GrpcServices="Both" />
</ItemGroup>
// Program.cs —— 注册 gRPC 服务
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc();
var app = builder.Build();

app.MapGrpcService<OrderApiService>();

app.Run();

// 服务实现:继承生成的基类
public class OrderApiService(IOrderRepository repo) : OrderApi.OrderApiBase
{
    public override async Task<Order> GetOrder(GetOrderRequest request, ServerCallContext context)
    {
        var order = await repo.GetByIdAsync(request.Id, context.CancellationToken);
        return order is null
            ? throw new RpcException(new Status(StatusCode.NotFound, "order not found"))
            : MapToProto(order);
    }
}
// 客户端调用
using var channel = GrpcChannel.ForAddress("https://orders.internal:5001");
var client = new OrderApi.OrderApiClient(channel);

var reply = await client.GetOrderAsync(new GetOrderRequest { Id = 42 });
Console.WriteLine(reply.Customer);
产物说明
OrderApiBase服务器虚基类,override 实现
OrderApiClient强类型客户端 stub
GrpcChannel客户端连接抽象
ServerCallContext携带元数据、取消、死线

避坑: 服务器方法里的异常要映射为 RpcException + StatusCode,而不是抛普通 Exception——后者会成为 Unknown,客户端丢失业务语义。ServerCallContext.CancellationToken 必须传给一切底层调用,否则客户端取消后服务器还在跑。

4. 拦截器横切逻辑

一句话总结: 拦截器类似 ASP.NET Core 中间件,在请求前后统一处理认证、日志、指标与错误转换,避免在每个 RPC 里重复。

gRPC 拦截器分服务端与客户端两侧:服务端在方法执行前后介入,客户端在发请求前后介入。常用于:注入 token、记录耗时、统一异常转 RpcException、附加 traceId 元数据。

// 服务端拦截器:日志 + 耗时
public class LoggingInterceptor : Interceptor
{
    private readonly ILogger<LoggingInterceptor> _logger;

    public LoggingInterceptor(ILogger<LoggingInterceptor> logger) => _logger = logger;

    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
        TRequest request, ServerCallContext context,
        UnaryServerMethod<TRequest, TResponse> continuation)
    {
        var sw = Stopwatch.StartNew();
        try
        {
            return await continuation(request, context);
        }
        catch (RpcException) { throw; }
        catch (Exception ex)
        {
            throw new RpcException(new Status(StatusCode.Internal, ex.Message));
        }
        finally
        {
            sw.Stop();
            _logger.LogInformation("gRPC {Method} took {ElapsedMs}ms",
                context.Method, sw.ElapsedMilliseconds);
        }
    }
}

// 注册拦截器
builder.Services.AddGrpc(options =>
{
    options.Interceptors.Add<LoggingInterceptor>();
});
拦截器用途说明
认证从 Metadata 读取 token,校验后附加 Claims
日志方法名、耗时、状态
错误转换业务异常 → StatusCode
指标计数器 + 直方图
链路追踪注入/读取 traceId

避坑: 拦截器里别吞掉异常——至少要 throw,让业务语义传递下去。拦截器是横切逻辑的家,但不该承载业务规则。服务端拦截器拿不到 [Authorize] 特性那么细的声明式能力,认证逻辑要配合 gRPC 的认证中间件一起用。

5. 流式 RPC

一句话总结: gRPC 支持四种调用模式,流式通信让「大结果分批返回」与「双向持续对话」成为可能,适合实时推送与大数据传输。

四种模式:一元(Unary)、服务端流(Server streaming)、客户端流(Client streaming)、双向流(Bidirectional streaming)。服务端流适合批量导出,客户端流适合批量上传,双向流适合聊天、实时监控。

// 服务端流:边查边推
public override async Task WatchOrders(
    WatchOrdersRequest request,
    IServerStreamWriter<Order> responseStream,
    ServerCallContext context)
{
    await foreach (var order in _orderFeed.WatchAsync(context.CancellationToken))
    {
        await responseStream.WriteAsync(order); // 推送一条
    }
}

// 双向流:客户端不断发、服务器不断回
public override async Task Chat(
    IAsyncStreamReader<ChatMessage> requestStream,
    IServerStreamWriter<ChatReply> responseStream,
    ServerCallContext context)
{
    await foreach (var msg in requestStream.ReadAllAsync(context.CancellationToken))
    {
        var reply = new ChatReply { Text = $"echo: {msg.Text}" };
        await responseStream.WriteAsync(reply);
    }
}
模式客户端服务端场景
一元1 请求1 响应常规查询
服务端流1 请求N 响应导出、推送
客户端流N 请求1 响应批量上传
双向流N 请求N 响应聊天、实时

避坑: 流式响应要时刻关注背压:客户端处理慢时,WriteAsync 会阻塞或缓冲,内存随之增长。长连接要处理 CancellationToken,客户端断开后及时释放。不要为了「实时」滥用双向流——大部分场景服务端流就够。

6. 健康检查与网关接入

一句话总结: gRPC 健康检查让负载均衡与编排平台知道实例是否可服务,API 网关负责把外部 HTTP 请求翻译成内部 gRPC 调用。

服务注册到注册中心(Consul/Kubernetes)后,健康检查决定流量是否进入该实例。gRPC 有标准健康检查协议(grpc.health.v1.Health)。网关(如 Envoy、YARP)把外部 HTTP 请求映射为内部 gRPC,让浏览器客户端沿用熟悉的 REST 风格。

// 引入 Grpc.AspNetCore.HealthChecks
builder.Services.AddGrpcHealthChecks()
    .AddCheck("orders", () => HealthCheckResult.Healthy());

app.MapGrpcHealthChecksService(); // 暴露标准 /grpc.health.v1.Health

// YARP 反向代理配置示例(appsettings.json 片段)
// 把 /api/orders/** 转发到 gRPC 服务,做 HTTP/1.1 到 HTTP/2 的翻译
组件作用
gRPC Health Check标准健康协议,编排平台探活
Consul/K8s 服务发现注册实例、摘除故障实例
YARP/Envoy 网关HTTP ↔ gRPC 翻译、负载均衡
gRPC-Web让浏览器走 gRPC(需代理)

避坑: 健康检查要反映「真实可服务性」——不只是进程活着,还要依赖(DB、下游)可用。网关做协议翻译时注意超时与重试配置,别让网关成为故障放大器。浏览器场景优先 gRPC-Web,而不是强行暴露裸 gRPC。

7. 性能与安全调优

一句话总结: gRPC 的性能优势来自 HTTP/2 多路复用与 Protobuf 二进制,安全上要启用 TLS、控制消息大小并做限流与鉴权。

性能层面,GrpcChannel 复用 HTTP/2 连接,多个调用并发共享一条连接;服务器要合理设置 MaxReceiveMessageSize、MaxSendMessageSize 与并发限制。安全层面,生产必须 TLS(AddGrpc 默认要求),配合认证拦截器与授权策略。

builder.Services.AddGrpc(options =>
{
    options.MaxReceiveMessageSize = 4 * 1024 * 1024; // 4MB
    options.MaxSendMessageSize = 4 * 1024 * 1024;
    options.EnableDetailedErrors = builder.Environment.IsDevelopment();
    options.Interceptors.Add<AuthInterceptor>();
});

// 认证:把客户端证书或 token 转为 ClaimsPrincipal
public class AuthInterceptor : Interceptor
{
    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
        TRequest request, ServerCallContext context,
        UnaryServerMethod<TRequest, TResponse> continuation)
    {
        var token = context.RequestHeaders.GetValue("authorization")?
                            .Replace("Bearer ", "");
        if (!_validator.IsValid(token))
            throw new RpcException(new Status(StatusCode.Unauthenticated, "bad token"));
        return await continuation(request, context);
    }
}
优化项建议
消息大小按业务设置上限,防 OOM
连接复用用 DI 单例 GrpcChannel
并发服务端默认高并发,关注下游连接池
TLS生产强制 mTLS 或 TLS
鉴权拦截器 + 元数据 token

避坑: 不做限流与大小限制的 gRPC 服务容易被恶意大消息打爆内存。GrpcChannel 要在 DI 里注册为单例复用,别每次调用都新建。调试期开 EnableDetailedErrors,生产必须关闭,避免内部信息泄漏。

8. 总结

主题要点
契约.proto 是事实来源,字段编号不可改
服务实现继承生成基类,异常转 RpcException
拦截器认证、日志、错误转换集中治理
流式 RPC四类模式,关注背压与取消
健康检查编排平台探活,反映真实可服务性
网关HTTP ↔ gRPC 翻译,浏览器走 gRPC-Web
安全TLS 强制、限流、鉴权拦截器

gRPC 让微服务通信从「文档约定」走向「编译期契约」:.proto 定义了服务边界,代码生成消灭手写 DTO 与序列化样板,拦截器统一横切关注点,流式通信支撑实时场景。它不适合面向浏览器的开放 API,却是服务间通信的高性能选择。把契约当产品管理、把弹性与安全放进公共层,gRPC 就能成为微服务架构里可靠、高效、可观测的通信骨架。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「csharp」更多文章

  1. .NET 机器学习实战
  2. 内存剖析与 dump 分析
  3. 分布式事务与 Saga 编排