后端架构实战(六):服务间通信——RPC、REST 还是消息

写在前面

上一篇解决了"跨服务数据怎么一致",这一篇解决"跨服务怎么对话"。

服务间通信是个看似简单、实则踩坑最密集的话题。选错通信方式,会带来延迟、耦合、故障放大一连串问题。核心决策只有一个:同步,还是异步?

这篇会和我的中间件笔记呼应:《消息队列》系列讲了 RabbitMQ/Kafka 本身,《Nginx》系列讲了反向代理和负载均衡。这里讲的是"在架构层怎么选"。


一、同步 vs 异步:第一性抉择

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
同步通信(打电话):
  调用方发请求 → 等 → 拿到响应
  ✓ 直观、顺序清晰、易理解
  ✓ 能立即知道结果(成功/失败)
  ✗ 调用方被阻塞,吞吐受限
  ✗ 强耦合:被调方挂了,调用方也受影响
  ✗ 故障会沿调用链同步放大

异步通信(发邮件):
  调用方发消息 → 立即返回 → 被调方按自己节奏处理
  ✓ 解耦、高吞吐、削峰填谷
  ✓ 调用方不等被调方,故障不直接传导
  ✗ 不立即知道结果(要回调/查询)
  ✗ 一致性弱化(最终一致)
  ✗ 调试、追踪更复杂
1
2
3
4
5
6
选择原则:
  必须立即拿到结果 → 同步(查余额、读商品、用户登录)
  结果可以晚点、且能解耦 → 异步(发通知、记日志、算积分)

  反模式:把所有通信都做成同步
  → 一次请求串行调 5 个服务,延迟叠加、雪崩放大。

二、同步通信:REST vs RPC

2.1 REST

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
基于 HTTP,面向"资源"
  GET /orders/123        查
  POST /orders           建
  PUT /orders/123        改
  DELETE /orders/123     删

  ✓ 通用、标准化、人可读
  ✓ 跨语言、跨平台、防火墙友好
  ✓ 生态丰富(网关、监控、文档 Swagger)
  ✗ 基于文本(JSON),序列化开销大
  ✗ 语义偏向 CRUD,复杂业务动作要绕(POST /orders/123/cancel)
  ✗ 没有强类型契约(靠 OpenAPI 补)

  适合:对外 API、和前端/第三方对接、绝大多数业务场景

2.2 RPC(gRPC 为代表)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
基于 HTTP/2 + Protobuf,面向"动作/方法"
  stub.CancelOrder(request) → response

  ✓ 二进制传输,体积小、速度快(比 JSON 快数倍)
  ✓ 强类型契约(.proto 生成各语言客户端)
  ✓ 支持 HTTP/2 多路复用、流式(stream)
  ✗ 不可读(二进制),调试要工具
  ✗ 浏览器/前端不友好(要 gRPC-Web 网关)
  ✗ 强耦合:proto 一变客户端要重新生成

  适合:内部服务间高频调用、低延迟、大数据量、流式场景
1
2
3
4
5
6
选型经验:
  对外、前端、第三方          → REST
  内部服务间、低延迟、高吞吐   → gRPC
  大多数团队                  → REST 打天下,内部热点链路再换 gRPC

  不要一上来全 gRPC,proto 的耦合成本会被低估。

三、异步通信:消息队列

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
通过 Broker(消息中间件)解耦

  生产者 → 消息队列 → 消费者
  生产者不用知道谁消费、有几个、在哪

  ✓ 解耦(生产者消费者互不感知)
  ✓ 削峰(流量高峰积压在队列,消费者按能力处理)
  ✓ 异步、高吞吐
  ✓ 故障隔离(消费者挂了,消息留在队列)
  ✗ 一致性弱(最终一致)
  ✗ 增加架构复杂度(Broker 本身要高可用)

3.1 两大流派

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
RabbitMQ —— 传统消息队列(AMQP)
  特点:丰富的路由(Exchange/Queue/Binding)、可靠投递、消息确认
  模型:一条消息被消费后删除
  强项:业务消息路由、可靠投递、延迟队列
  弱项:吞吐和堆积能力一般

Kafka —— 分布式日志/流平台
  特点:高吞吐、可堆积海量消息、分区有序、支持流处理
  模型:消息是"日志",可重复消费、按 offset
  强项:日志、事件溯源、大数据管道、超高吞吐
  弱项:路由能力弱、不适合复杂业务路由

  详细对比见《消息队列(四):RabbitMQ vs Kafka 深度对比》。

3.2 两种消息语义

1
2
3
4
5
6
7
8
点对点(Queue):一条消息被一个消费者消费
  → 任务分发(一个订单只被一个处理者处理)

发布订阅(Topic):一条消息被所有订阅者消费
  → 事件广播(订单创建后,积分、通知、统计都各收一份)

  事件驱动架构(EDA)主要用 Pub/Sub:
  订单服务发"订单已创建",所有感兴趣的下游各自订阅。

四、消息可靠性的三个保证

用消息队列,必须想清楚这三件事,否则会丢消息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
1. 生产端不丢:确认机制 + 重试 + 本地落库
   → 关键消息用 Outbox 模式(上一篇讲过)

2. Broker 不丢:持久化 + 副本 + 确认
   → 消息持久化磁盘、集群多副本、生产者等 broker 确认

3. 消费端不丢:手动确认 + 幂等
   → 处理成功后再 ack(别自动 ack)
   → 处理失败重试,重试要幂等(消息会重复投递)

  三个环节任何一个漏了,消息就会丢。

五、服务间通信的反模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
反模式 1:同步链路过长
  A→B→C→D→E,五个服务串行同步调用
  → 延迟叠加、任意一环故障全链路崩
  → 能异步化的副作用(日志、通知、统计)坚决异步

反模式 2:循环依赖
  A 调 B,B 又调 A
  → 死锁可能、边界划分反了(回到第三篇:拆错了)

反模式 3:共享数据库当通信
  两个服务通过读写同一张表"传话"
  → 表结构耦合,等于没拆,且并发冲突
  → 服务通信要走 API/消息,不偷懒走数据库

反模式 4:消息不带版本
  事件 schema 一改,所有消费者崩
  → 事件契约要版本化、向后兼容

反模式 5:忽略超时和熔断
  同步调用不设超时 → 一个慢服务拖垮调用方线程池
  → 永远设超时 + 熔断(第四篇的分布式税)

六、网关:通信的统一入口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
对外不用让客户端直连每个服务,前面加一层 API 网关:

  客户端 → API 网关 → 各微服务

  网关职责:
    - 统一鉴权(token 校验集中在一处)
    - 限流、熔断
    - 路由(外部 REST 路由到内部 gRPC)
    - 协议转换(gRPC-Web、聚合多个服务)
    - 灰度、A/B

  常见:Kong / APISIX / Nginx(见我的 Nginx 系列)/ Ocelot(.NET)/ YARP(.NET)

  注意:网关是单点,必须高可用(多实例 + 负载均衡)。

七、一份选型速查

1
2
3
4
5
6
7
8
9
需求                          选择
──────────────────────────────────────────────
对外 API / 前端调用            REST
内部高频、低延迟调用           gRPC
必须立即拿结果                 同步(REST/gRPC)
可解耦、可削峰、能异步         消息队列
事件广播、多下游订阅           Kafka / RabbitMQ Pub-Sub
可靠事件投递                   Outbox + 消息队列
统一入口、鉴权、限流           API 网关

八、小结

  • 第一性抉择:同步(要立即结果)还是异步(可解耦、能晚点)
  • 同步:REST(通用、对外)vs gRPC(内部、低延迟、强类型)
  • 异步:消息队列解耦、削峰、故障隔离;RabbitMQ 重路由,Kafka 重吞吐/流
  • 可靠性三保证:生产端确认+Outbox、Broker 持久化+副本、消费端手动ack+幂等
  • 五大反模式:同步链路过长、循环依赖、共享库通信、消息不版本化、忽略超时熔断
  • 网关:统一入口,集中鉴权/限流/路由,必须高可用

下一篇讲把这些服务监控起来——可观测性,微服务的神经系统。