3. 微服务通信基础

微服务拆开之后,服务之间必须通信。通信方式选得对不对,直接决定系统的延迟、吞吐、可靠性和演进成本。本节先厘清两条主线:协议层是选 HTTP/REST 还是 gRPC,交互层是选同步请求还是异步消息;再落到 ASP.NET Core 中 HttpClient 与 IHttpClientFactory 的正确用法。

一、HTTP/REST 与 gRPC:不是替代,而是分工

HTTP/REST 以资源为中心,用 URL 定位资源、用 HTTP 动词表达操作、用 JSON 承载数据。它的最大优势是通用:浏览器、curl、Postman、任何语言都能直接调用,调试成本极低。代价是文本序列化体积大、每次请求都要携带完整头部、没有强类型契约,字段改名容易在运行时才暴露。

gRPC 以方法为中心,基于 HTTP/2 传输,用 Protocol Buffers 定义服务契约并二进制序列化。它天然支持多路复用、双向流、强类型代码生成,适合内部服务之间高频、低延迟、结构稳定的调用。代价是浏览器不能直接调用(需 gRPC-Web 代理)、二进制报文不便肉眼排查、契约变更需要重新生成代码。

选择时可以按下面的判断走:面向外部或前端的 API 用 REST;服务内部高频调用、需要流式传输或严格契约的用 gRPC;同一服务也可以同时暴露两套接口,对外 REST、对内 gRPC。

HTTP/REST 与 gRPC 通信特征对比 HTTP/REST 传输:HTTP/1.1 或 HTTP/2 载荷:JSON 文本,体积较大 契约:OpenAPI 文档,弱类型 调试:浏览器 / curl 直接可测 流式:需 SSE / WebSocket 补充 适用:对外 API、前端调用 典型场景:商品查询、订单提交 用户注册等低频、跨边界调用 注意:无强类型约束,字段变更 易在运行时才暴露;文本序列化 在高频调用下 CPU 与带宽开销偏高 gRPC 传输:HTTP/2,多路复用 载荷:Protobuf 二进制,体积小 契约:.proto 强类型,代码生成 调试:需 grpcurl / 专用工具 流式:一元 / 服务端 / 客户端 / 双向 适用:内部服务高频、低延迟调用 典型场景:库存扣减、价格计算 推荐引擎等内部密集交互 注意:浏览器不能直接调用,需 gRPC-Web 代理;契约变更需重新 生成客户端代码,版本管理要严格

二、同步通信与异步消息:可靠性与耦合度的取舍

同步通信指调用方发出请求后阻塞等待响应,REST 和 gRPC 的一元调用都属于这一类。它逻辑直观、结果即时,但调用链上任何一环变慢或宕机都会向上传导,形成级联故障。同步调用适合“必须立刻拿到结果才能继续”的场景,例如下单前校验库存。

异步消息指调用方把消息投递到消息中间件(RabbitMQ、Kafka、Azure Service Bus 等)后立即返回,由消费者按自己的节奏处理。它解耦了生产者和消费者,天然具备削峰填谷和失败重试能力,代价是最终一致性、消息幂等、顺序与重复消费都需要额外设计。异步适合“可以稍后处理”的场景,例如下单成功后发短信、更新积分、写审计日志。

工程上常见的是两者混用:核心链路用同步保证强一致,旁路动作走异步消息。判断标准很简单——调用方是否必须立刻得到结果;如果不需要,就应优先异步。

三、HttpClient 与 IHttpClientFactory 的正确用法

在 ASP.NET Core 中直接 new HttpClient() 是典型反模式:每次新建都会创建新的连接池,频繁调用会耗尽套接字(socket exhaustion);而长期持有单个静态实例又会导致 DNS 变更无法生效。IHttpClientFactory 正是为解决这两个矛盾而设计,它统一管理底层 HttpMessageHandler 的生命周期与连接池复用。

推荐做法是在 Program.cs 中注册具名客户端或类型化客户端,并配置基地址、超时和重试策略:

builder.Services.AddHttpClient("inventory", client =>
{
    client.BaseAddress = new Uri("https://inventory.internal/");
    client.Timeout = TimeSpan.FromSeconds(3);
    client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddStandardResilienceHandler();   // 内置重试、熔断、超时

// 类型化客户端:把 HTTP 细节封装在服务类里
builder.Services.AddHttpClient<IInventoryClient, InventoryClient>(client =>
{
    client.BaseAddress = new Uri("https://inventory.internal/");
    client.Timeout = TimeSpan.FromSeconds(3);
});

消费时通过构造函数注入 IHttpClientFactory,用 CreateClient("inventory") 获取实例。注意:HttpClient 实例应当短生命周期使用,用完即弃,底层 Handler 由工厂池化复用,不要把它存进字段长期持有。

public class OrderService
{
    private readonly IHttpClientFactory _factory;

    public OrderService(IHttpClientFactory factory) => _factory = factory;

    public async Task<StockResult> CheckStockAsync(string sku, int qty, CancellationToken ct)
    {
        var client = _factory.CreateClient("inventory");
        var resp = await client.GetAsync($"/api/stock/{sku}?qty={qty}", ct);
        resp.EnsureSuccessStatusCode();
        return await resp.Content.ReadFromJsonAsync<StockResult>(ct);
    }
}

几个容易踩的坑:一是超时不要依赖默认的 100 秒,内部调用通常应设为 1~3 秒并配合取消令牌;二是重试只对幂等的 GET 或带幂等键的请求开启,否则可能重复下单;三是不要在 AddHttpClient 里做耗时初始化,配置委托应保持轻量;四是跨服务传递 CancellationToken,让上游取消能及时释放下游资源。

选型速记:对外接口用 REST,内部高频调用用 gRPC;必须立刻拿结果用同步,可延后处理用异步消息;ASP.NET Core 中一律通过 IHttpClientFactory 获取 HttpClient,并显式设置超时与重试。

把这三层决策串起来看:协议决定报文长什么样,交互模式决定调用方是否等待,客户端工厂决定连接资源如何被安全复用。三者配合得当,微服务之间的通信才既高效又可控。