引言:一场真实的雪崩事故
2018年某电商平台的双11大促,凌晨2点,一场噩梦开始了:
2:00 - 商品详情服务的MySQL数据库出现慢查询,响应时间从10ms飙升到5秒
2:02 - 商品服务的Tomcat线程池被耗尽(200个线程全部阻塞在数据库查询上)
2:03 - 订单服务调用商品服务超时,订单服务的线程池也被耗尽
2:04 - 用户服务调用订单服务超时,用户服务也挂了
2:05 - 整个系统瘫痪,首页无法访问,交易全部失败
损失:10分钟内损失订单超过5000万元
这就是微服务雪崩效应的真实案例。
一个数据库的慢查询,像多米诺骨牌一样,导致整个系统崩溃。
今天我们就来学习如何用Sentinel防止这种雪崩的发生。
场景描述:一个典型的调用链路
系统架构
我们有一个典型的微服务架构:
用户请求
↓
[用户服务 User-Service]
↓ 调用
[订单服务 Order-Service]
↓ 调用
[商品服务 Product-Service]
↓ 查询
[MySQL数据库]
调用关系:
- 用户服务 → 订单服务:查询用户的订单列表
- 订单服务 → 商品服务:查询订单中的商品详情
- 商品服务 → MySQL:查询商品信息
正常流程
- 用户访问"我的订单"页面
- 用户服务调用订单服务的
/order/list接口 - 订单服务返回订单ID列表
- 订单服务调用商品服务的
/product/detail接口,批量查询商品信息 - 商品服务查询MySQL数据库
- 层层返回,用户看到订单列表
正常情况下的响应时间:
- 商品服务查询数据库:10ms
- 订单服务调用商品服务:50ms
- 用户服务调用订单服务:100ms
- 用户看到页面:200ms
问题分析:雪崩是如何发生的?
故障起点:商品服务的数据库慢查询
某天凌晨,商品服务的MySQL数据库出现了慢查询:
-- 慢查询:全表扫描,耗时5秒
SELECT * FROM product WHERE category = 'phone' AND price > 5000 ORDER BY sales DESC;
直接影响:商品服务的响应时间从10ms飙升到5秒。
第一层传播:商品服务线程池耗尽
商品服务:
- Tomcat最大线程数:200
- 每个请求等待5秒
- QPS:100(假设)
结果:
- 5秒内到达的请求 = 100 * 5 = 500个
- 线程池只有200个,300个请求排队等待
- 线程池被耗尽!
症状:
- 商品服务的所有接口都变慢
- CPU使用率不高(大部分线程在等待数据库)
- 内存使用率不高
- 但响应时间极长,接近于不可用
第二层传播:订单服务被拖垮
订单服务调用商品服务,设置的超时时间是3秒:
// 订单服务调用商品服务
@GetMapping("/order/list")
public List<Order> getOrderList(Long userId) {
List<Order> orders = orderDao.findByUserId(userId);
// 批量查询商品信息(10个订单,每个订单1个商品)
for (Order order : orders) {
Product product = productClient.getProduct(order.getProductId()); // 超时3秒
order.setProduct(product);
}
return orders;
}
问题:
- 每个订单查询一次商品,10个订单 = 10次调用
- 每次调用超时3秒
- 10次调用 = 30秒
- 订单服务的线程被阻塞30秒!
结果:
- 订单服务的线程池也被耗尽
- 订单服务自身的其他接口也变慢
- 雪崩继续向上传播
第三层传播:用户服务崩溃
用户服务调用订单服务:
// 用户服务调用订单服务
@GetMapping("/user/orders")
public List<Order> getUserOrders(Long userId) {
return orderClient.getOrderList(userId); // 超时5秒
}
问题:
- 调用订单服务超时5秒
- 用户服务的线程也被阻塞
- 用户服务崩溃
雪崩路径图
MySQL慢查询(5秒)
↓
商品服务线程池耗尽
↓
订单服务调用超时(3秒 × 10次)
↓
订单服务线程池耗尽
↓
用户服务调用超时(5秒)
↓
用户服务线程池耗尽
↓
整个系统崩溃
关键问题:
- 同步阻塞:每次调用都是同步等待,线程被阻塞
- 无熔断机制:明知道下游服务已经很慢,还继续调用
- 资源耗尽:线程池被耗尽,无法处理新请求
- 连锁反应:故障沿着调用链向上传播
解决方案:多层熔断隔离
熔断策略设计
核心思想:在每一层服务中,对下游服务进行熔断保护。
[用户服务]
↓ 熔断保护
[订单服务]
↓ 熔断保护
[商品服务]
↓
[MySQL](故障点)
熔断规则:
订单服务对商品服务的熔断:
- 策略:慢调用比例
- 阈值:RT > 1秒算慢调用,慢调用比例 > 50%
- 熔断时长:10秒
- 降级方案:返回默认商品信息
用户服务对订单服务的熔断:
- 策略:慢调用比例
- 阈值:RT > 2秒算慢调用,慢调用比例 > 50%
- 熔断时长:15秒
- 降级方案:返回"订单服务繁忙"
完整代码实现
1. 商品服务(Product-Service)
商品服务本身不做熔断,但可以做限流保护,防止被压垮。
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/detail/{id}")
@SentinelResource(value = "getProductDetail")
public Product getProductDetail(@PathVariable Long id) {
// 查询数据库(可能很慢)
return productService.getProductById(id);
}
}
限流规则:
@Configuration
public class SentinelConfig {
@PostConstruct
public void initFlowRule() {
List<FlowRule> rules = new ArrayList<>();
// 限流:QPS不超过200
FlowRule rule = new FlowRule();
rule.setResource("getProductDetail");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(200);
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
}
2. 订单服务(Order-Service)
订单服务对商品服务进行熔断保护。
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private RedisTemplate<String, Product> redisTemplate;
/**
* 查询订单列表
*/
public List<OrderVO> getOrderList(Long userId) {
// 1. 查询订单
List<Order> orders = orderDao.findByUserId(userId);
// 2. 批量查询商品信息(带熔断保护)
List<OrderVO> orderVOList = new ArrayList<>();
for (Order order : orders) {
OrderVO vo = new OrderVO();
vo.setOrderId(order.getId());
vo.setProductId(order.getProductId());
// 调用商品服务(带熔断)
Product product = getProductWithCircuitBreaker(order.getProductId());
vo.setProduct(product);
orderVOList.add(vo);
}
return orderVOList;
}
/**
* 调用商品服务(带熔断保护)
*/
@SentinelResource(
value = "callProductService",
blockHandler = "productServiceBlockHandler",
fallback = "productServiceFallback"
)
public Product getProductWithCircuitBreaker(Long productId) {
// 调用商品服务
String url = "http://product-service/product/detail/" + productId;
return restTemplate.getForObject(url, Product.class);
}
/**
* 熔断降级处理:返回缓存数据
*/
public Product productServiceBlockHandler(Long productId, BlockException ex) {
System.out.println("商品服务熔断,返回缓存数据");
// 从Redis获取缓存
String key = "product:" + productId;
Product product = redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
// 缓存也没有,返回默认数据
Product defaultProduct = new Product();
defaultProduct.setId(productId);
defaultProduct.setName("商品信息暂时无法获取");
defaultProduct.setPrice(0.0);
return defaultProduct;
}
/**
* 异常降级处理
*/
public Product productServiceFallback(Long productId, Throwable ex) {
System.out.println("商品服务调用异常:" + ex.getMessage());
return productServiceBlockHandler(productId, null);
}
}
熔断规则配置:
@Configuration
public class OrderSentinelConfig {
@PostConstruct
public void initDegradeRule() {
List<DegradeRule> rules = new ArrayList<>();
// 对商品服务的熔断规则
DegradeRule rule = new DegradeRule();
rule.setResource("callProductService");
rule.setGrade(RuleConstant.DEGRADE_GRADE_RT); // 慢调用比例
rule.setCount(1000); // RT > 1秒算慢调用
rule.setSlowRatioThreshold(0.5); // 慢调用比例50%
rule.setMinRequestAmount(5);
rule.setStatIntervalMs(10000);
rule.setTimeWindow(10); // 熔断10秒
rules.add(rule);
DegradeRuleManager.loadRules(rules);
System.out.println("✅ 订单服务熔断规则已加载");
}
}
3. 用户服务(User-Service)
用户服务对订单服务进行熔断保护。
@Service
public class UserService {
@Autowired
private RestTemplate restTemplate;
/**
* 获取用户订单列表
*/
@SentinelResource(
value = "callOrderService",
blockHandler = "orderServiceBlockHandler",
fallback = "orderServiceFallback"
)
public List<OrderVO> getUserOrders(Long userId) {
// 调用订单服务
String url = "http://order-service/order/list?userId=" + userId;
return restTemplate.getForObject(url, List.class);
}
/**
* 订单服务熔断降级
*/
public List<OrderVO> orderServiceBlockHandler(Long userId, BlockException ex) {
System.out.println("订单服务熔断,返回友好提示");
// 返回空列表 + 提示信息
List<OrderVO> emptyList = new ArrayList<>();
OrderVO tip = new OrderVO();
tip.setMessage("订单服务繁忙,请稍后重试");
emptyList.add(tip);
return emptyList;
}
/**
* 订单服务调用异常
*/
public List<OrderVO> orderServiceFallback(Long userId, Throwable ex) {
System.out.println("订单服务调用异常:" + ex.getMessage());
return orderServiceBlockHandler(userId, null);
}
}
熔断规则配置:
@Configuration
public class UserSentinelConfig {
@PostConstruct
public void initDegradeRule() {
List<DegradeRule> rules = new ArrayList<>();
// 对订单服务的熔断规则
DegradeRule rule = new DegradeRule();
rule.setResource("callOrderService");
rule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
rule.setCount(2000); // RT > 2秒算慢调用
rule.setSlowRatioThreshold(0.5);
rule.setMinRequestAmount(5);
rule.setStatIntervalMs(10000);
rule.setTimeWindow(15); // 熔断15秒
rules.add(rule);
DegradeRuleManager.loadRules(rules);
System.out.println("✅ 用户服务熔断规则已加载");
}
}
故障注入测试
测试目标
验证当商品服务出现慢查询时,熔断机制能否有效防止雪崩。
测试步骤
1. 模拟商品服务慢查询
在商品服务中添加一个开关,可以模拟慢查询:
@RestController
@RequestMapping("/product")
public class ProductController {
// 故障注入开关
private volatile boolean slowQuery = false;
@GetMapping("/detail/{id}")
@SentinelResource(value = "getProductDetail")
public Product getProductDetail(@PathVariable Long id) throws InterruptedException {
// 模拟慢查询
if (slowQuery) {
Thread.sleep(5000); // 5秒
}
return productService.getProductById(id);
}
// 故障注入接口
@PostMapping("/fault/slow")
public String enableSlowQuery(@RequestParam boolean enable) {
this.slowQuery = enable;
return "慢查询已" + (enable ? "开启" : "关闭");
}
}
2. 压测工具
使用JMeter或wrk进行压测:
# 使用wrk压测
wrk -t10 -c100 -d60s http://localhost:8080/user/orders?userId=1
3. 开启故障注入
# 开启商品服务慢查询
curl -X POST "http://localhost:8001/product/fault/slow?enable=true"
4. 观察日志
商品服务日志:
⚠️ 慢查询:耗时5000ms
⚠️ 慢查询:耗时5000ms
⚠️ 慢查询:耗时5000ms
订单服务日志:
⚠️ 调用商品服务,耗时5100ms
⚠️ 调用商品服务,耗时5100ms
🔴 商品服务熔断,返回缓存数据 ← 熔断生效!
🔴 商品服务熔断,返回缓存数据
🔴 商品服务熔断,返回缓存数据
用户服务日志:
✅ 调用订单服务成功,耗时150ms ← 订单服务因为熔断了商品服务,自身正常
✅ 调用订单服务成功,耗时150ms
✅ 调用订单服务成功,耗时150ms
测试结果对比
| 场景 | 无熔断 | 有熔断 |
|---|---|---|
| 商品服务响应时间 | 5秒 | 5秒 |
| 订单服务响应时间 | 30秒(超时) | 150ms ✅ |
| 用户服务响应时间 | 超时 | 200ms ✅ |
| 用户体验 | 白屏/报错 | 正常(部分数据降级) ✅ |
| 系统稳定性 | 全链路崩溃 ❌ | 故障隔离 ✅ |
效果验证:雪崩被阻止了
验证点1:订单服务未被拖垮
虽然商品服务响应慢,但订单服务:
- 检测到商品服务响应慢(RT > 1秒)
- 慢调用比例超过50%
- 自动熔断,停止调用商品服务
- 返回缓存数据或默认值
- 订单服务自身保持正常响应时间
验证点2:用户服务未受影响
用户服务调用订单服务:
- 订单服务因为熔断了商品服务,响应时间正常(150ms)
- 用户服务不需要熔断
- 用户体验良好
验证点3:故障自动恢复
当商品服务的慢查询问题解决后:
- 熔断时长到期(10秒)
- 熔断器进入半开状态
- 探测请求成功
- 自动恢复正常调用
完整的防御体系
MySQL慢查询(5秒)
↓
商品服务响应慢(5秒)
↓
订单服务检测到慢调用
↓
🛡️ 熔断器开启 ← 雪崩被阻止!
↓
订单服务返回降级数据
↓
订单服务保持正常(150ms)
↓
用户服务调用订单服务正常
↓
用户体验良好
最佳实践总结
1. 多层防护原则
第一层:限流(保护自己)
第二层:熔断(保护自己不被依赖服务拖垮)
第三层:降级(提供有限服务)
第四层:超时(避免无限等待)
2. 熔断规则配置建议
| 层级 | 熔断阈值 | 熔断时长 | 理由 |
|---|---|---|---|
| 靠近底层 | 更严格(RT 1秒) | 更短(10秒) | 故障源头,快速隔离 |
| 靠近上层 | 更宽松(RT 2秒) | 更长(15秒) | 给下游更多恢复时间 |
3. 降级数据准备
- 优先级1:缓存(Redis、本地缓存)
- 优先级2:静态数据(配置、数据库)
- 优先级3:默认值/空对象
- 优先级4:友好提示
4. 监控与告警
关键指标:
- 熔断次数:
sentinel_degrade_total - 慢调用比例:
sentinel_slow_ratio - 降级返回率:
sentinel_fallback_ratio
告警规则:
# 熔断频繁告警
- alert: HighDegradeRate
expr: rate(sentinel_degrade_total[5m]) > 10
annotations:
summary: "服务熔断频繁"
5. 压测验证
必做的压测:
- 正常压测:验证服务承载能力
- 故障注入:模拟依赖服务故障
- 雪崩测试:验证熔断是否生效
- 恢复测试:验证熔断器自动恢复
总结
本文我们通过一个完整的微服务调用链路案例,学习了如何防止服务雪崩:
- 雪崩原理:故障沿着调用链向上传播,导致连锁反应
- 核心问题:同步阻塞、无熔断机制、资源耗尽
- 解决方案:多层熔断隔离,每一层对下游进行熔断保护
- 降级策略:返回缓存、默认值、友好提示
- 故障注入测试:模拟真实故障,验证熔断效果
- 最佳实践:多层防护、监控告警、压测验证
核心要点:
熔断降级不是"放弃",而是"断臂求生"。通过主动熔断故障服务,可以保护整个系统的稳定性,避免雪崩效应。
第三阶段完结:
恭喜你完成了Sentinel熔断降级篇的学习!我们系统地学习了:
- 熔断原理和状态机
- 三种熔断策略(慢调用比例、异常比例、异常数)
- 降级规则和自定义降级处理
- 半开状态和恢复机制
- 防止微服务雪崩的完整方案
下一阶段预告:
在第四阶段(进阶特性篇),我们将学习:
- 系统自适应保护:Load、CPU、RT全方位防护
- 热点参数限流:保护热点数据
- 黑白名单与授权规则
- 集群流控:跨实例的流量管理
- 网关流控:统一流量入口的防护
- 动态规则配置:从硬编码到配置中心
敬请期待!