写在前面
上一篇解决了"跨服务数据怎么一致",这一篇解决"跨服务怎么对话"。
服务间通信是个看似简单、实则踩坑最密集的话题。选错通信方式,会带来延迟、耦合、故障放大一连串问题。核心决策只有一个:同步,还是异步?
这篇会和我的中间件笔记呼应:《消息队列》系列讲了 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+幂等
- 五大反模式:同步链路过长、循环依赖、共享库通信、消息不版本化、忽略超时熔断
- 网关:统一入口,集中鉴权/限流/路由,必须高可用
下一篇讲把这些服务监控起来——可观测性,微服务的神经系统。