跳到主要内容
预计阅读 20 分钟

分布式网络架构 — 从单体到微服务的网络演进

当系统规模从”一台服务器搞定一切”演变为”数百个服务协同工作”时,网络不再只是传输数据的管道——它成了整个架构的骨骼和血脉。本章将从生产环境的真实挑战出发,带你掌握微服务通信、服务发现、API 网关以及网络故障排查的核心技能。

📋 开篇自测:你已经知道多少?

  1. 微服务之间的通信方式有哪些?同步通信和异步通信各适用于什么场景?
  2. 服务发现的两种模式(客户端发现 vs 服务端发现)有什么区别?
  3. API 网关在微服务架构中扮演什么角色?它与反向代理有什么区别?

一、从单体到微服务:网络视角的演进

1.1 拆分带来的网络挑战

单体架构下,模块之间是本地函数调用——零网络开销。拆分为微服务后,原来的函数调用全部变成了网络调用:

单体:  用户模块 --函数调用(纳秒)--> 订单模块 (同一进程)

微服务:
                     +----------+
                     | API 网关  |
                     +----+-----+
                          |
          +---------------+---------------+
          |               |               |
     +----+----+    +-----+-----+   +-----+-----+
     | 用户服务 |    |  订单服务  |   |  支付服务  |
     | :8001   |    |  :8002    |   |  :8003    |
     +----+----+    +-----+-----+   +-----+-----+

  函数调用变网络调用后的新问题:
  - 延迟: 纳秒 --> 毫秒 (差距百万倍)
  - 不可靠: 丢包、超时、连接中断
  - 服务发现: 怎么知道目标服务的地址?
  - 负载均衡: 多个实例, 请求发给谁?

二、微服务通信模式

2.1 同步通信 vs 异步通信

同步通信 (请求-响应):
  服务A --[请求]--> 服务B --[响应]--> 服务A
  协议: HTTP/REST, gRPC
  场景: 需要立即得到结果 (查询用户, 验证权限)

异步通信 (消息驱动):
  服务A --[消息]--> 消息队列 --[消费]--> 服务B
  协议: AMQP (RabbitMQ), Kafka
  场景: 不需要即时结果 (发邮件, 生成报表)

常见组合: 外部流量 --[REST]--> API网关 --[gRPC]--> 内部微服务

2.2 消息队列的两种模型

点对点 (Queue):
  生产者 --[消息]--> [ 队列 ] --[消息]--> 消费者
  一条消息只被一个消费者处理 (任务分发)

发布-订阅 (Topic):
  生产者 --[消息]--> [ Topic ]
                        |
                +-------+-------+
           消费者组A        消费者组B
           (订单服务)       (通知服务)
  一条消息可被多个消费者组独立消费 (事件广播)

2.3 实际场景:电商下单流程

客户端 --POST /orders(REST)--> API网关 --gRPC--> 订单服务
                                                   |
  +--[gRPC同步]--> 用户服务: 验证身份               |
  +--[gRPC同步]--> 库存服务: 锁定库存               |
  +--[gRPC同步]--> 支付服务: 发起支付               |
  +--[异步消息]--> 消息队列                         |
                     |                              |
              +------+------+                       |
              |             |                       v
         物流服务       通知服务              返回订单结果
         创建配送单     发送短信

设计原则: 需要即时结果 --> 同步 | 不影响主流程 --> 异步

🤔 想一想 如果支付服务成功了但发送消息到队列失败了,会出现什么问题?分布式事务要如何保证数据一致性?


三、服务发现

3.1 为什么需要服务发现

容器化环境中,服务实例随时动态增减,IP 地址不断变化。硬编码地址的方式彻底失效了:

硬编码:  order_url: http://192.168.1.10:8002  (容器重启IP就变)

服务发现:
  启动时 --> 自动向注册中心注册地址
  调用时 --> 从注册中心查询可用实例
  宕机时 --> 自动从注册中心移除

3.2 两种发现模式

客户端发现 (Client-Side):

  服务A --1.查询--> 注册中心(Consul/Nacos/etcd) --2.返回[B1,B2,B3]-->
  服务A --3.选择B2(客户端负载均衡)--> 服务B2

  优点: 客户端可实现智能负载均衡
  缺点: 每种语言都要实现发现逻辑
  常见注册中心: Consul, etcd, Nacos(国内广泛使用), ZooKeeper

服务端发现 (Server-Side):

  服务A --1.请求--> 负载均衡器/DNS --2.转发--> 服务B2
                         |
                   查询注册中心

  代表: Kubernetes Service + kube-proxy
  优点: 客户端无感, 语言无关
  缺点: 多一跳网络开销

3.3 Kubernetes 中的服务发现

Kubernetes 集群内:

  Pod(用户服务)
    | 访问 http://order-service:8080/api/orders
    v
  CoreDNS  --> 解析为 ClusterIP 10.96.0.100
    v
  kube-proxy (iptables/IPVS) --> 负载均衡到后端Pod
    v
  +--------+  +--------+  +--------+
  | 订单Pod |  | 订单Pod |  | 订单Pod |
  +--------+  +--------+  +--------+

  只需用服务名即可访问, 无需关心具体IP和端口

四、API 网关

4.1 为什么需要 API 网关

没有网关 (客户端直连每个服务):
  APP --> user.api.com:8001, order.api.com:8002, pay.api.com:8003
  问题: 地址管理混乱, 认证/限流/日志每个服务重复实现

有网关 (统一入口):
  APP --> api.example.com --> API网关 --> 路由到具体服务
  网关统一处理: 认证、限流、路由、协议转换、日志

4.2 核心功能一览

+------------------------------------------------------------+
|                       API 网关                              |
+------------------------------------------------------------+
|  1. 请求路由:  /api/users/** --> 用户服务                    |
|  2. 认证鉴权:  验证JWT --> 提取用户信息 --> 注入请求头        |
|  3. 限流熔断:  令牌桶限流 + 下游错误率>50%自动熔断           |
|  4. 协议转换:  外部REST --> 内部gRPC                        |
|  5. 请求聚合:  一个请求 --> 并发调多个服务 --> 合并响应       |
|  6. 可观测性:  请求日志、链路追踪ID注入、指标采集            |
+------------------------------------------------------------+

4.3 请求聚合(BFF 模式)

不聚合 (客户端串行3次请求):
  APP --> 用户服务(50ms) --> 订单服务(80ms) --> 推荐服务(120ms)
  总耗时: 250ms, 请求次数: 3

聚合 (网关并发调用):
  APP --1次请求--> 网关 --并发--> 用户(50ms) + 订单(80ms) + 推荐(120ms)
                       <-- 合并响应 --
  总耗时: 120ms, 请求次数: 1

4.4 限流策略

令牌桶: 桶容量100, 每秒补10个. 有令牌放行, 无令牌拒绝(429)
        允许突发流量(桶满可瞬间处理100请求)

滑动窗口: 统计最近60秒请求数, 超过阈值拒绝
          比固定窗口更平滑

多维度限流:
  - 全局: 总QPS <= 10000
  - 用户: 每用户每分钟 <= 100
  - IP:   每IP每秒 <= 50
  - 接口: /api/search 每秒 <= 200

五、网络容错与弹性设计

5.1 超时、重试、熔断、降级

这四个机制是分布式系统可靠性的四大支柱:

1. 超时 -- 快速失败, 不无限等待
   连接超时: 1-3s | 读取超时: 按接口特性 | 总超时: 含重试
   传递原则: 客户端(3s) > 网关(2.5s) > 服务A(2s) > 服务B(1.5s)

2. 重试 -- 应对瞬时故障
   只对可重试错误重试(超时/503), 不对业务错误重试(400/401)
   退避: 指数退避+抖动 (1s -> 2.3s -> 4.7s)
   重试预算: 重试请求不超过总请求的10%, 防止重试风暴

3. 熔断 -- 主动断开故障链路
   +------+  失败率>50%  +------+  冷却30s  +--------+
   | 关闭  | ----------> | 打开  | -------> | 半开    |
   | 正常  |             | 快速  |          | 探测    |
   | 转发  | <---------- | 失败  | <------- | 少量    |
   +------+  探测成功    +------+  探测失败  | 请求    |
                                            +--------+

4. 降级 -- 返回"够用"的结果
   推荐服务不可用 --> 返回热销Top10(缓存)
   Level1: 上次缓存 | Level2: 静态默认 | Level3: 隐藏模块

六、网络故障排查实战

6.1 分层排查法

问题: "服务A调用服务B超时"

第1步(网络层):  ping <IP>             --> 不通? 防火墙/路由
第2步(传输层):  nc -zv <IP> <端口>    --> 不通? 服务未启动
第3步(DNS):     dig <服务名> +short   --> 失败? DNS配置
第4步(应用层):  curl -v <url>/health  --> 5xx? 查服务日志
第5步(链路):    查看TraceID瀑布图     --> 定位最慢Span

6.2 常用排查命令

+------------------+------------------------------------------+
| 排查目标          | 命令                                     |
+------------------+------------------------------------------+
| 连通性            | ping <host>                              |
| 路由路径          | traceroute <host>                        |
| 端口监听          | ss -tlnp | grep <port>                   |
| TCP连接状态       | ss -tnp | grep <host>                    |
| DNS解析           | dig <domain> +short                      |
| HTTP调试          | curl -v <url>                            |
| 抓包分析          | tcpdump -i any host <ip> and port <port> |
| 连接数统计        | ss -s                                    |
+------------------+------------------------------------------+

6.3 实战案例:排查间歇性超时

现象: 订单服务调用用户服务, 约5%请求超时(恰好3s)

排查:
  1. 用户服务监控: CPU 30%, 内存 60% --> 资源正常
     但 P99延迟 2800ms --> 接近3s超时阈值!

  2. 慢请求日志: 超时请求都涉及"查用户详情+权限"

  3. 链路追踪:
     订单服务 --> 用户服务(2900ms)
                    +--> MySQL(50ms)
                    +--> 权限服务(2800ms)  <-- 瓶颈!

  4. 权限服务: 新版本有一个未加索引的查询
     数据量大时 10ms --> 3000ms

  解决: 紧急回滚 | 短期加索引 | 长期加缓存(TTL=5min)
  教训: 上线前做性能回归, 关键链路要有缓存兜底

6.4 分布式追踪

客户端 --X-Trace-ID:abc123--> API网关[5ms]
                                |
                            订单服务[150ms]
                              |-- 用户服务[30ms]
                              |-- 库存服务[45ms]
                              |-- 支付服务[200ms]
                                    |-- 银行网关[180ms]

瀑布图:
|-- 网关 (5ms) --------------------------------|
   |-- 订单 (150ms) ---------------------------|
      |-- 用户 (30ms) --|
      |-- 库存 (45ms) ----|
      |-- 支付 (200ms) ----------------------|
            |-- 银行 (180ms) --------------|

工具: Jaeger, Zipkin, SkyWalking, OpenTelemetry

七、章节小结

  1. 微服务通信分为同步(REST、gRPC)和异步(消息队列)两大类,选择取决于是否需要即时结果。
  2. 服务发现解决了”如何找到目标服务”的问题,Kubernetes 内置了基于 DNS 的服务发现。
  3. API 网关是微服务的统一入口,承担路由、认证、限流、协议转换等横切关注点。
  4. 容错四件套(超时、重试、熔断、降级)是分布式系统可靠性的基石。
  5. 故障排查遵循分层法,配合分布式追踪定位瓶颈。

思考题

  1. 在微服务架构中,如果服务 A 同步调用服务 B,服务 B 又同步调用服务 C,形成了长调用链。当服务 C 出现高延迟时,你会采取哪些措施防止级联故障?

  2. 一个电商系统在大促期间出现大量订单支付超时。你作为值班工程师,会按什么步骤排查?


📝 结尾自测:检验你的收获

  1. 同步通信和异步通信分别适合什么场景?各举两个例子。
  2. 客户端发现和服务端发现的核心区别是什么?各适合什么规模的系统?
  3. 熔断器的三个状态是什么?它们之间如何转换?
  4. API 网关的请求聚合有什么优势?什么场景下不适合使用?
  5. 排查微服务间超时问题时,你会按什么顺序逐层排查?

下一章预告:恭喜你走到了这里!下一章是本课程的结束篇,我们将回顾整个课程的知识脉络,用一张知识图谱把所有章节串联起来,并为你规划从网络原理到架构思维的进阶路线。

购买课程解锁全部内容

网络通信第一课:10 章掌握计算机网络

¥29.90