引言:从单体到微服务的演进之痛
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秒内达到10倍甚至100倍
- 难以预测:用户行为受心理因素影响(从众效应)
- 恢复困难:系统崩溃后,重启需要时间,流量会继续堆积
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分钟:S3响应变慢(从10ms → 5000ms)
- 第3分钟:依赖S3的EC2控制台开始超时
- 第5分钟:CloudWatch监控服务因无法写入S3而故障
- 第10分钟:整个AWS控制台瘫痪(无法监控、无法操作)
- 第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个服务 → 3个服务 → 10个服务
- 难以定位:表面看是订单服务崩溃,实际是支付服务的问题
- 恢复困难:需要从下游向上游逐个重启
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 |
| 学习成本 | 各自学习 | 统一框架 |
七、总结
微服务架构带来了灵活性,也带来了复杂性。三大稳定性挑战时刻威胁着系统:
- 流量洪峰:10倍流量,微服务放大效应
- 服务雪崩:一个故障,全局崩溃
- 资源耗尽:温水煮青蛙,防不胜防
传统方案(Nginx、线程池、连接池)在单体时代够用,但在微服务时代面临四大不足:
- 粒度太粗
- 策略单一
- 分布式支持不足
- 动态配置困难
Sentinel的价值:用统一的框架,解决微服务流量治理的全部问题。
下一篇预告:《Sentinel初相识:5分钟快速上手》
我们将动手写第一个Sentinel程序,通过最简单的Hello World,体验Sentinel的魅力。只需要不到20行代码,你就能实现一个完整的限流功能。
思考题
- 你的系统中,哪个服务最容易成为"雪崩"的起点?
- 如果你的系统遇到10倍流量,会首先耗尽哪种资源?(内存、线程、连接)
- 你们现在的流量治理方案是什么?有哪些痛点?
欢迎在评论区分享你的故障案例和踩坑经验!