引言:从单体到微服务的演进之痛

2015年,Netflix宣布他们的微服务架构已经包含超过1000个微服务,每天处理数十亿次API调用。这标志着微服务架构从理论走向了大规模生产实践。

但微服务不是银弹。在享受它带来的灵活性、可扩展性的同时,我们也必须面对新的挑战。

上一篇我们讲了流量控制的本质,这篇我们深入微服务场景,看看现代分布式系统面临的三大稳定性威胁:流量洪峰、服务雪崩、资源耗尽。这三个问题如果不妥善处理,任何一个都足以让整个系统在几分钟内瘫痪。


一、挑战1:流量洪峰——当10倍流量来袭

1.1 真实案例:2019年双11零点的惊险时刻

2019年双11,阿里云技术团队事后复盘了一个惊险瞬间:

零点前1秒

  • 系统QPS:50万/秒
  • 服务器CPU:60%
  • 数据库连接:5000个

零点后1秒

  • 系统QPS:680万/秒(13.6倍)
  • 服务器CPU:95%(濒临极限)
  • 数据库连接:10000个(已达上限)

零点后2秒

  • 部分慢查询开始出现
  • RT从50ms上升到200ms
  • 用户开始疯狂刷新(雪上加霜)

如果没有流量控制和弹性扩容,这个洪峰足以在10秒内压垮整个系统。

1.2 流量洪峰的特征

流量洪峰不是均匀的,而是呈现尖刺特征

QPS
 |
 |                    ╱╲
 |                   ╱  ╲
 |                  ╱    ╲
 |─────────────────╱      ╲────────────
 |                                       时间
 └─────────────────────────────────────>
    平时     活动开始    高峰    回落

关键问题

  1. 短时极高:可能在1秒内达到10倍甚至100倍
  2. 难以预测:用户行为受心理因素影响(从众效应)
  3. 恢复困难:系统崩溃后,重启需要时间,流量会继续堆积

1.3 微服务架构下的放大效应

单体应用时代,1个用户请求 = 1次数据库查询。

微服务时代:

1个用户请求
  → 网关(1次调用)
    → 订单服务(1次调用)
      → 用户服务(查询用户信息)
      → 商品服务(查询商品信息)
      → 库存服务(检查库存)
      → 优惠券服务(计算优惠)
      → 积分服务(计算积分)
      ↓
    5次下游调用 × 3次数据库查询 = 15次数据库操作

放大效应

  • 用户QPS = 10万
  • 实际下游调用 = 50万(5倍放大)
  • 实际数据库查询 = 150万(15倍放大)

这意味着:网关层的流量洪峰,在下游会被多次放大

1.4 传统方案的局限

方案1:Nginx限流

limit_req_zone $binary_remote_addr zone=one:10m rate=100r/s;

局限性

  • ❌ 只能在网关层限流,无法保护下游服务
  • ❌ 按IP限流,无法区分正常用户和爬虫
  • ❌ 限流粒度粗,无法针对不同接口

方案2:增加服务器

局限性

  • ❌ 成本高昂(双11后闲置)
  • ❌ 扩容有延迟(启动、预热需要时间)
  • ❌ 数据库等有状态服务难以扩容

方案3:缓存

局限性

  • ❌ 缓存击穿、缓存雪崩依然存在
  • ❌ 热点数据的突变(某个商品突然火了)
  • ❌ 写操作无法缓存

二、挑战2:服务雪崩——一颗老鼠屎坏了一锅粥

2.1 真实案例:亚马逊AWS的多米诺骨牌

2017年2月28日,AWS S3服务发生了一次著名的大规模故障:

故障起因:运维人员误删了几个S3服务器(人为失误)

连锁反应

  1. 第1分钟:S3响应变慢(从10ms → 5000ms)
  2. 第3分钟:依赖S3的EC2控制台开始超时
  3. 第5分钟:CloudWatch监控服务因无法写入S3而故障
  4. 第10分钟:整个AWS控制台瘫痪(无法监控、无法操作)
  5. 第30分钟:大量第三方网站受影响(Medium、Slack、Trello…)

影响范围:半个互联网

故障时长:4小时

根本原因没有熔断机制,故障在服务间传播

2.2 服务雪崩的传播机制

让我们用一个电商系统的例子,详细分析雪崩是如何发生的:

正常情况

用户 → 订单服务 → 支付服务
         (RT=50ms)   (RT=100ms)
  • 订单服务:1000个线程,每个请求50ms
  • 支付服务:正常响应,100ms

吞吐量计算

  • 订单服务:1000线程 × (1000ms / 50ms) = 20000 QPS

支付服务故障

假设支付服务因为数据库慢查询,响应时间从100ms变成5000ms:

第1秒

  • 订单服务调用支付服务,等待5000ms
  • 1000个线程在1秒内只能处理:1000 / 5 = 200个请求
  • 吞吐量骤降:20000 QPS → 200 QPS下降99%

第2秒

  • 新请求继续进来(用户不知道系统慢了)
  • 1000个线程全部被占用,等待支付服务响应
  • 线程池耗尽,新请求被拒绝

第3秒

  • 用户看到错误,开始刷新(重试风暴)
  • 请求堆积在订单服务
  • 内存溢出,订单服务开始宕机

第4秒

  • 网关调用订单服务超时
  • 网关的线程池也开始耗尽
  • 整个系统瘫痪

2.3 雪崩的三个阶段

阶段1:故障点出现
  支付服务慢(数据库锁等待)
       ↓
阶段2:调用方受影响
  订单服务线程耗尽
       ↓
阶段3:连锁崩溃
  网关、其他服务全部瘫痪

关键特征

  1. 指数级传播:1个服务 → 3个服务 → 10个服务
  2. 难以定位:表面看是订单服务崩溃,实际是支付服务的问题
  3. 恢复困难:需要从下游向上游逐个重启

2.4 传统方案的局限

方案1:增加超时时间

restTemplate.setReadTimeout(30000); // 30秒超时

局限性

  • ❌ 治标不治本,只是延缓崩溃时间
  • ❌ 30秒 × 1000线程 = 大量资源被占用
  • ❌ 用户体验更差(等待30秒才返回错误)

方案2:降低超时时间

restTemplate.setReadTimeout(1000); // 1秒超时

局限性

  • ❌ 误杀正常慢请求(网络抖动、大查询)
  • ❌ 无法自动恢复(一直超时)
  • ❌ 没有"试探"机制

方案3:健康检查 + 手动摘除

局限性

  • ❌ 检查周期长(通常10秒以上)
  • ❌ 依赖人工介入
  • ❌ 无法处理部分故障(70%请求正常,30%超时)

三、挑战3:资源耗尽——温水煮青蛙

3.1 真实案例:一个慢SQL引发的内存溢出

某电商公司凌晨2点收到告警:订单服务宕机。

故障排查

查看日志:

java.lang.OutOfMemoryError: Java heap space

查看堆dump:

80% 的内存被 ArrayList<Order> 占用
每个 ArrayList 包含100万条订单记录

根本原因

-- 有人写了一个没有limit的查询
SELECT * FROM orders WHERE status = 'PENDING';
-- 返回了100万条记录,每条1KB,总共1GB数据

为什么白天没问题,凌晨出现

  • 白天:待处理订单10万条,查询返回100MB(可接受)
  • 凌晨:运营批量导入100万条测试数据
  • 触发了这个查询,单次查询占用1GB内存
  • 10个并发请求 = 10GB内存,JVM堆只有8GB

3.2 资源耗尽的三大类型

类型1:内存耗尽

常见场景

  • 大对象(查询大量数据)
  • 内存泄漏(对象没有释放)
  • 缓存膨胀(没有淘汰策略)

后果

内存使用 → 95% → 开始Full GC
    ↓
Full GC频繁 → 每次停顿5秒
    ↓
请求堆积 → 内存继续上涨
    ↓
OOM崩溃

类型2:线程耗尽

常见场景

  • 下游服务慢,线程等待
  • 死锁
  • 阻塞IO(没有用NIO)

后果

线程池配置:最大500线程
    ↓
慢请求:每个占用线程10秒
    ↓
50 QPS × 10秒 = 500线程(耗尽)
    ↓
新请求被拒绝

类型3:连接耗尽

常见场景

  • 数据库连接池
  • Redis连接池
  • HTTP连接池

后果

// HikariCP配置:最大100个连接
config.setMaximumPoolSize(100);

// 慢SQL:每次查询5秒
SELECT * FROM big_table WHERE ...

// 20 QPS × 5秒 = 100个连接(耗尽)
// 新请求等待或超时

3.3 资源耗尽的"温水煮青蛙"特征

不像流量洪峰(突然爆发)和服务雪崩(快速传播),资源耗尽往往是缓慢发生的:

时间线:
00:00 - 系统正常,内存60%
01:00 - 慢查询出现,内存65%
02:00 - 慢查询增多,内存70%
03:00 - 内存75%,开始频繁GC
04:00 - 内存85%,响应变慢
05:00 - 内存95%,Full GC频繁
06:00 - OOM崩溃

危险之处

  • 初期没有告警(内存60%→70%是正常波动)
  • 问题逐渐积累(慢SQL越来越多)
  • 崩溃前夜才被发现(已经来不及)

3.4 传统方案的局限

方案1:增加资源

局限性

  • ❌ 成本高(内存、CPU翻倍)
  • ❌ 无法解决根本问题(慢SQL还在)
  • ❌ 资源总是有限的

方案2:监控 + 告警

局限性

  • ❌ 告警阈值难以设置(多少才算异常?)
  • ❌ 依赖人工响应(半夜谁起来处理?)
  • ❌ 无法自动保护

方案3:限制并发数

Semaphore semaphore = new Semaphore(100);
// 最多100个线程同时执行

局限性

  • ❌ 需要手动为每个接口设置
  • ❌ 无法区分快请求和慢请求
  • ❌ 无法自适应调整

四、传统方案为什么不够用

总结前面的分析,传统方案的共同问题:

4.1 粒度太粗

层面传统方案不足
网关层Nginx限流无法保护下游服务
应用层线程池无法区分不同接口
数据层连接池无法识别慢查询

需要:细粒度到接口级别、方法级别的保护。

4.2 策略单一

传统方案只有"限流",没有:

  • 熔断:自动切断故障服务
  • 降级:自动返回默认值
  • 自适应:根据系统负载调整

4.3 分布式支持不足

场景传统方案问题
单机限流Guava RateLimiter多实例时总QPS不准确
集群限流Redis计数器延迟高、复杂度高
链路追踪无法识别调用来源

4.4 动态配置困难

传统方案的配置是静态的:

  • 修改限流阈值 → 重启服务
  • 调整线程池大小 → 重启服务
  • 增加熔断规则 → 修改代码、发版

需要:运行时动态修改,立即生效。


五、微服务流量治理的新需求

综合三大挑战,我们可以梳理出微服务流量治理的核心需求:

5.1 多层防护

┌─────────────────────────────────┐
│ 1. 网关层:总入口流量控制      │
├─────────────────────────────────┤
│ 2. 服务层:每个服务独立保护    │
├─────────────────────────────────┤
│ 3. 方法层:热点方法重点防护    │
├─────────────────────────────────┤
│ 4. 资源层:数据库、缓存保护    │
└─────────────────────────────────┘

5.2 多种手段

手段应对场景效果
限流流量洪峰拒绝超出部分
熔断服务雪崩快速失败,防止传播
降级服务不可用返回默认值
隔离资源耗尽线程池隔离
自适应负载波动根据CPU、RT动态调整

5.3 实时可观测

需要看到

  • 实时QPS、RT、错误率
  • 每个接口的流量分布
  • 熔断、限流事件
  • 依赖关系拓扑图

需要能做

  • 实时调整限流阈值
  • 手动触发熔断/降级
  • 查看历史监控数据

5.4 框架集成

开箱即用

  • Spring Boot
  • Spring Cloud
  • Dubbo
  • Gateway

无侵入性

  • 注解方式
  • 切面方式
  • 过滤器方式

六、Sentinel:阿里的答案

面对这些挑战,阿里巴巴在2018年开源了Sentinel,这是他们内部流量治理体系的对外版本。

6.1 Sentinel的核心能力

┌─────────────────────────────────────┐
│           流量控制                   │
│   QPS限流、并发线程数、热点参数    │
├─────────────────────────────────────┤
│           熔断降级                   │
│   慢调用比例、异常比例、异常数      │
├─────────────────────────────────────┤
│           系统保护                   │
│   CPU、Load、RT、线程数、入口QPS    │
├─────────────────────────────────────┤
│           集群流控                   │
│   分布式限流、Token Server          │
└─────────────────────────────────────┘

6.2 Sentinel的三大优势

优势1:轻量级

  • 核心库无依赖
  • 内存占用小(<100MB)
  • 性能损耗低(<1ms)

优势2:实时性

  • 秒级监控数据
  • 规则实时生效
  • 毫秒级响应

优势3:生态完善

  • 官方Dashboard
  • 多种规则持久化方案
  • 主流框架适配器

6.3 Sentinel vs 传统方案

维度传统方案Sentinel
限流粒度IP、URL接口、方法、参数
熔断能力慢调用、异常比例
动态配置需重启实时生效
集群流控自己实现内置支持
监控面板Dashboard
学习成本各自学习统一框架

七、总结

微服务架构带来了灵活性,也带来了复杂性。三大稳定性挑战时刻威胁着系统:

  1. 流量洪峰:10倍流量,微服务放大效应
  2. 服务雪崩:一个故障,全局崩溃
  3. 资源耗尽:温水煮青蛙,防不胜防

传统方案(Nginx、线程池、连接池)在单体时代够用,但在微服务时代面临四大不足:

  • 粒度太粗
  • 策略单一
  • 分布式支持不足
  • 动态配置困难

Sentinel的价值:用统一的框架,解决微服务流量治理的全部问题。

下一篇预告:《Sentinel初相识:5分钟快速上手》

我们将动手写第一个Sentinel程序,通过最简单的Hello World,体验Sentinel的魅力。只需要不到20行代码,你就能实现一个完整的限流功能。


思考题

  1. 你的系统中,哪个服务最容易成为"雪崩"的起点?
  2. 如果你的系统遇到10倍流量,会首先耗尽哪种资源?(内存、线程、连接)
  3. 你们现在的流量治理方案是什么?有哪些痛点?

欢迎在评论区分享你的故障案例和踩坑经验!