【架构实战】系统稳定性保障:降级、兜底与容灾设计
一、那场让CEO亲自道歉的事故
2020年双11,零点峰值流量瞬间涌入。
凌晨00:05,用户疯狂投诉:
- App白屏
- 下单失败
- 支付超时
- 优惠券核销异常
所有系统同时雪崩。
事后排查:
- 支付服务调用银行接口超时,线程池被打满
- 雪崩蔓延到订单服务,订单服务线程池也满了
- 优惠券服务强依赖订单服务,跟着挂掉
- 最后所有服务都不可用
复盘会上 ,CEO对着技术团队说:"我们能不能不要再出现这种问题?"
我意识到:不是某个服务挂了,而是我们没有"隔离、限流、降级、兜底"的设计。
今天就分享我这几年在稳定性保障上的实战经验------如何让系统在大促、流量激增、依赖故障时,依然保持核心功能可用。
二、稳定性的核心思想:防御纵深
2.1 稳定性保障的金字塔
┌─────────────┐
│ 用户体验 │ ← 业务可降级
├─────────────┤
│ 业务连续性 │ ← 关键功能可用
├─────────────┤
│ 服务可用性 │ ← 故障隔离
├─────────────┤
│ 资源充足性 │ ← 容量规划
├─────────────┤
│ 代码健壮性 │ ← 单元测试
└─────────────┘
从下到上,越上层越脆弱,越下层越基础。
2.2 稳定性保障的三大策略
1. 预防:不让问题发生
- 容量规划
- 限流
- 监控
2. 检测:快速发现问题
- 多维监控
- 智能告警
- 链路追踪
3. 恢复:快速恢复服务
- 降级
- 兜底
- 容灾
核心思想 :任何环节都可能失败,架构要能"带病工作"。
2.3 故障的不可避免性
墨菲定律 :凡是可能出错的事,总有一天会出错。
接受现实:
- 服务器会宕机
- 网络会抖动
- 数据库会慢
- 第三方服务会挂
- 流量会激增
- 会有Bug
架构设计原则 :假设一切都会失败,做对应的容错设计。
三、降级:取舍的艺术
3.1 什么是降级
降级 = 在资源有限时,主动放弃非核心功能,保证核心功能可用。
降级不是放弃,是取舍。
降级场景:
- 流量激增(秒杀、大促)
- 依赖服务故障
- 系统资源不足
- 响应时间过长
3.2 降级策略分类
按降级粒度:
| 粒度 | 说明 | 示例 |
|---|---|---|
| 页面降级 | 整个页面降级 | 活动页降级为静态页 |
| 功能降级 | 某个功能不可用 | 评论区降级,只显示前10条 |
| 服务降级 | 某个服务不可用 | 推荐服务挂了,显示默认推荐 |
| 数据降级 | 返回兜底数据 | 实时数据降级为缓存数据 |
| 读降级 | 写不可用时,只读 | 评论写挂了,还能看评论 |
按降级方式:
1. 熔断降级
└── 服务调用失败率高时,自动熔断
2. 限流降级
└── 流量超过阈值时,拒绝部分请求
3. 超时降级
└── 调用超时后,返回降级结果
4. 失败降级
└── 调用异常后,返回降级结果
5. 人工降级
└── 运维/开发手动触发降级
3.3 降级设计模式
模式1:返回默认值
java
/**
* 库存查询降级 - 库存服务不可用时返回默认值
*/
@Service
public class InventoryServiceWrapper {
@Autowired
private InventoryService inventoryService;
@CircuitBreaker(name = "inventory", fallbackMethod = "getInventoryFallback")
public Inventory getInventory(String productId) {
return inventoryService.getInventory(productId);
}
/**
* 降级方法:返回默认库存(认为有货)
*/
public Inventory getInventoryFallback(String productId, Throwable t) {
log.warn("库存服务调用失败,返回默认值, productId={}, error={}",
productId, t.getMessage());
// 返回默认值:假设有货
return Inventory.builder()
.productId(productId)
.stock(999) // 默认认为有库存
.isFromCache(false)
.isDefault(true) // 标记是默认值
.build();
}
}
模式2:返回缓存数据
java
/**
* 推荐服务降级 - 推荐服务不可用时返回缓存的热门数据
*/
@Service
public class RecommendationServiceWrapper {
@Autowired
private RecommendationService recommendationService;
@Autowired
private RedisTemplate<String, List<Product>> redisTemplate;
@CircuitBreaker(name = "recommendation", fallbackMethod = "getRecommendationFallback")
public List<Product> getRecommendations(String userId) {
return recommendationService.getRecommendations(userId);
}
/**
* 降级方法:返回缓存的热门商品
*/
public List<Product> getRecommendationFallback(String userId, Throwable t) {
log.warn("推荐服务调用失败,返回热门商品, userId={}, error={}",
userId, t.getMessage());
// 从缓存读取热门商品
List<Product> hotProducts = redisTemplate.opsForValue()
.get("hot_products_top10");
if (hotProducts != null) {
return hotProducts;
}
// 缓存也没有,返回静态默认列表
return getDefaultProducts();
}
}
模式3:返回空数据
java
/**
* 评论服务降级 - 评论服务不可用时返回空列表
*/
@Service
public class CommentServiceWrapper {
@CircuitBreaker(name = "comment", fallbackMethod = "getCommentsFallback")
public List<Comment> getComments(String productId) {
return commentService.getComments(productId);
}
/**
* 降级方法:返回空列表
* 业务含义:暂时不显示评论(不显示"加载失败")
*/
public List<Comment> getCommentsFallback(String productId, Throwable t) {
log.warn("评论服务调用失败,返回空列表, productId={}", productId);
return Collections.emptyList();
}
}
3.4 降级开关设计
集中式降级开关:
java
/**
* 降级开关管理
*/
@Component
public class DegradeSwitch {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 检查功能是否降级
*/
public boolean isDegraded(String feature) {
String status = redisTemplate.opsForValue().get("degrade:" + feature);
return "true".equals(status);
}
/**
* 开启降级
*/
public void enableDegrade(String feature) {
redisTemplate.opsForValue().set("degrade:" + feature, "true", 1, TimeUnit.HOURS);
log.warn("降级开关已开启: feature={}", feature);
}
/**
* 关闭降级
*/
public void disableDegrade(String feature) {
redisTemplate.delete("degrade:" + feature);
log.info("降级开关已关闭: feature={}", feature);
}
}
/**
* 使用降级开关
*/
@Service
public class OrderService {
@Autowired
private DegradeSwitch degradeSwitch;
public Order createOrder(OrderRequest request) {
// 核心流程:创建订单
Order order = doCreateOrder(request);
// 非核心流程:发送短信通知(可降级)
if (!degradeSwitch.isDegraded("sms_notification")) {
smsService.sendNotification(order);
}
// 非核心流程:增加积分(可降级)
if (!degradeSwitch.isDegraded("points_service")) {
pointsService.addPoints(order);
}
return order;
}
}
降级开关平台:
【降级开关管理平台】
┌────────────────────────────────────────┐
│ 降级开关列表 │
├────────────────────────────────────────┤
│ 功能 状态 操作 │
│ 短信通知 [ON] 关闭 │
│ 邮件通知 [OFF] 开启 │
│ 积分服务 [OFF] 开启 │
│ 推荐服务 [OFF] 开启 │
│ 评论服务 [OFF] 开启 │
│ 优惠券服务 [OFF] 开启 │
└────────────────────────────────────────┘
[一键开启所有非核心服务降级] ← 大促时一键降级
[一键恢复所有服务] ← 故障恢复后一键恢复
3.5 自动降级 vs 手动降级
自动降级:
java
/**
* Sentinel配置:基于错误率自动降级
*/
@SentinelResource(
value = "createOrder",
blockHandler = "createOrderBlockHandler",
fallback = "createOrderFallback"
)
public Order createOrder(OrderRequest request) {
// 业务逻辑
}
/**
* 降级规则配置(通过Nacos动态下发)
*/
-DegradeRule
resource=createOrder
grade=ExceptionRatio // 基于异常比例
count=0.5 // 异常比例阈值50%
timeWindow=10 // 统计窗口10秒
minRequestAmount=20 // 最小请求数20
手动降级:
java
/**
* 紧急降级(API)
*/
@RestController
@RequestMapping("/admin/degrade")
public class DegradeAdminController {
@Autowired
private DegradeSwitch degradeSwitch;
/**
* 紧急开启全量降级
*/
@PostMapping("/emergency")
public ResponseEntity<String> emergencyDegrade() {
degradeSwitch.enableDegrade("sms_notification");
degradeSwitch.enableDegrade("email_notification");
degradeSwitch.enableDegrade("points_service");
degradeSwitch.enableDegrade("recommendation_service");
degradeSwitch.enableDegrade("coupon_service");
return ResponseEntity.ok("Emergency degrade enabled");
}
}
四、兜底:最后一道防线
4.1 兜底与降级的区别
降级:主动放弃非核心功能,让核心功能继续工作
兜底:当所有方案都失败时,返回"能用的"结果
降级是"有所取舍",兜底是"最后选择"。
4.2 兜底设计模式
模式1:缓存兜底
java
/**
* 实时数据查询 - 使用缓存兜底
*/
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
private RedisTemplate<String, Product> redisTemplate;
public Product getProduct(String productId) {
// 1. 优先从缓存获取
Product product = redisTemplate.opsForValue().get("product:" + productId);
if (product != null) {
return product;
}
// 2. 缓存未命中,从数据库获取
try {
product = productRepository.findById(productId);
if (product != null) {
redisTemplate.opsForValue().set(
"product:" + productId,
product,
1, TimeUnit.HOURS
);
}
return product;
} catch (Exception e) {
// 3. 数据库也挂了,从本地缓存兜底
log.error("数据库查询失败,使用本地缓存兜底, productId={}", productId, e);
return getLocalCachedProduct(productId);
}
}
private Product getLocalCachedProduct(String productId) {
// 本地Caffeine缓存(即使Redis挂也能用)
Product cached = localCache.getIfPresent(productId);
if (cached != null) {
return cached;
}
// 真的一点缓存都没有了,返回最小化兜底对象
return Product.builder()
.productId(productId)
.name("商品信息加载中")
.price(BigDecimal.ZERO)
.isDegraded(true) // 标记降级
.build();
}
}
模式2:默认数据兜底
java
/**
* 用户信息兜底 - 任何环节失败都返回基础信息
*/
@Service
public class UserServiceWrapper {
@CircuitBreaker(name = "user", fallbackMethod = "getUserFallback")
public User getUser(String userId) {
return userService.getUser(userId);
}
public User getUserFallback(String userId, Throwable t) {
log.error("用户服务完全不可用, userId={}", userId, t);
// 返回最基础的用户信息(让流程能继续)
return User.builder()
.userId(userId)
.username("用户" + userId.substring(0, 4)) // 默认名称
.level(UserLevel.NORMAL)
.isDegraded(true)
.build();
}
}
模式3:异步重试兜底
java
/**
* 重要操作的兜底:失败后异步重试
*/
@Service
public class PointsServiceWrapper {
@Autowired
private PointsService pointsService;
@Autowired
private RetryQueueService retryQueueService;
public void addPoints(String userId, int points) {
try {
pointsService.addPoints(userId, points);
} catch (Exception e) {
// 第一次失败,加入重试队列
log.warn("积分服务调用失败,加入重试队列, userId={}, points={}", userId, points, e);
retryQueueService.addRetryTask("addPoints",
new PointsTask(userId, points),
3 // 最多重试3次
);
}
}
}
4.3 兜底数据的"够用即可"原则
兜底数据不需要完美,需要"够用":
【用户中心兜底】
完整数据:头像、昵称、等级、积分、会员状态...
兜底数据:用户ID + 默认昵称 + 基础等级
【商品详情兜底】
完整数据:标题、描述、价格、库存、规格、图片...
兜底数据:商品ID + 默认标题 + 默认价格
【订单信息兜底】
完整数据:订单详情、状态、日志...
兜底数据:订单ID + 简化状态
核心原则 :业务能跑通,用户体验不至于崩溃。
五、容灾:最坏情况的应对
5.1 容灾等级
L0 - 单机房部署
└── 机房故障 = 全站不可用
L1 - 同城双活
└── 机房故障 = 切到另一个机房(RTO分钟级)
L2 - 异地灾备
└── 主城市故障 = 切换到异地(RTO小时级)
L3 - 异地多活
└── 多地同时提供服务(RTO秒级)
L4 - 同城双活 + 异地灾备
└── 综合方案,平衡成本和可用性
我们的选择 :L3 - 异地多活(电商业务)
5.2 限流:第一道防线
限流是最有效的稳定性保障手段。
java
/**
* Sentinel限流配置
*/
@RestController
public class OrderController {
/**
* 通用限流:每秒10000个请求
*/
@SentinelResource(
value = "createOrder",
blockHandler = "createOrderBlockHandler"
)
@PostMapping("/api/order/create")
public Order createOrder(@RequestBody OrderRequest request) {
return orderService.createOrder(request);
}
/**
* 限流处理:返回友好提示
*/
public Order createOrderBlockHandler(OrderRequest request, BlockException e) {
log.warn("订单创建被限流, userId={}", request.getUserId());
throw new BusinessException("系统繁忙,请稍后重试", 503);
}
}
多级限流:
【三级限流架构】
1. 接入层限流(CDN/Nginx)
└── IP级别、URL级别
└── 目的:过滤恶意流量
2. 应用层限流(Sentinel)
└── 接口级别、用户级别
└── 目的:保护核心服务
3. 资源层限流(数据库连接池、线程池)
└── 资源级别
└── 目的:防止资源耗尽
限流算法:
| 算法 | 特点 | 适用 |
|---|---|---|
| 计数器 | 简单、突发流量 | 通用场景 |
| 滑动窗口 | 平滑、精准 | 精确限流 |
| 漏桶 | 强制速率 | 流量整形 |
| 令牌桶 | 允许突发 | 大多数场景 |
5.3 熔断:服务隔离
熔断是"服务级别的限流"。
java
/**
* Resilience4j熔断配置
*/
@CircuitBreaker(name = "payment", fallbackMethod = "paymentFallback")
@Service
public class PaymentServiceWrapper {
@Autowired
private PaymentService paymentService;
public PaymentResult process(String userId, BigDecimal amount) {
return paymentService.process(userId, amount);
}
/**
* 熔断降级:返回支付排队结果
*/
public PaymentResult paymentFallback(String userId, BigDecimal amount, Throwable t) {
log.warn("支付服务熔断, userId={}, amount={}, error={}",
userId, amount, t.getMessage());
// 返回"支付排队中"的结果,让用户重试
return PaymentResult.builder()
.success(false)
.status(PaymentStatus.QUEUED)
.message("支付系统繁忙,请稍后重试")
.retryable(true)
.build();
}
}
// application.yml配置
resilience4j:
circuitbreaker:
instances:
payment:
failureRateThreshold: 50 # 失败率阈值50%
slowCallRateThreshold: 50 # 慢调用率阈值50%
slowCallDurationThreshold: 3s # 慢调用阈值3秒
slidingWindowSize: 100 # 滑动窗口100次
minimumNumberOfCalls: 20 # 最小调用次数20
waitDurationInOpenState: 30s # 熔断后等待30秒
permittedNumberOfCallsInHalfOpenState: 5 # 半开状态允许5次
5.4 隔离:故障不扩散
隔离模式:
【线程池隔离】
主业务线程池(200线程) ──> 核心业务
独立线程池1(50线程) ──> 支付服务
独立线程池2(50线程) ──> 库存服务
独立线程池3(50线程) ──> 通知服务
支付服务挂了 → 独立线程池1满 → 不影响核心业务
信号量隔离:
java
/**
* 信号量隔离
*/
@Bulkhead(name = "inventory", type = Bulkhead.Type.SEMAPHORE)
public Inventory getInventory(String productId) {
return inventoryService.getInventory(productId);
}
// 配置
resilience4j:
bulkhead:
instances:
inventory:
maxConcurrentCalls: 100 # 最大并发100
maxWaitDuration: 0 # 不等待
5.5 超时:避免雪崩
超时设置原则:
【超时设置原则】
下游依赖 推荐超时 说明
─────────────────────────
数据库 1-2秒 不能太长
Redis 100ms 通常很快
HTTP/RPC 1-3秒 根据业务
第三方API 3-5秒 银行等
超时要"前短后长":
- 网关层超时:5秒
- 服务A超时:4秒
- 服务B超时:3秒
- 数据库超时:2秒
避免:所有服务都设置30秒超时。
java
/**
* 合理的超时设置
*/
@FeignClient(name = "inventory-service")
public interface InventoryClient {
@GetMapping("/api/inventory/{productId}")
@RequestMapping(method = RequestMethod.GET,
consumes = MediaType.APPLICATION_JSON_VALUE)
InventoryDTO getInventory(@PathVariable("productId") String productId);
}
// application.yml
feign:
client:
config:
default:
connectTimeout: 1000 # 连接超时1秒
readTimeout: 2000 # 读取超时2秒
inventory-service:
connectTimeout: 500 # 库存服务连接超时500ms
readTimeout: 1000 # 库存服务读取超时1秒
六、灾备演练:真打才能发现问题
6.1 为什么要演练
"演练不是为了证明能恢复,而是为了发现恢复不了的问题"。
真实案例:
- 某银行灾备演练,发现"主备切换要4小时"(远超2小时RTO要求)
- 某电商演练,发现"回滚脚本里写错了一个IP"
不演练永远不知道问题。
6.2 演练类型
桌面演练(Tabletop):
- 团队讨论故障应对
- 检查流程完整性
- 成本低、风险低
功能演练:
- 模拟某个服务故障
- 验证降级、限流是否生效
- 在测试环境进行
真实演练:
- 切断某个服务、某个机房
- 验证整体可用性
- 需要业务方配合
6.3 我们的演练机制
月度:故障演练(功能演练)
- 模拟某个服务故障
- 验证降级、限流
- 30分钟内完成
季度:全链路演练
- 模拟机房级故障
- 验证容灾切换
- 业务低峰期进行
年度:真实灾备演练
- 真实切换到灾备环境
- 验证数据完整性
- 业务部门参与
演练结果:
- 2023年我们进行了8次演练
- 发现23个潜在问题
- 全部修复
- 2次真实故障时顺利恢复
6.4 混沌工程
Chaos Engineering :主动注入故障,发现系统弱点。
java
/**
* ChaosBlade故障注入示例
*/
@Component
public class ChaosTest {
/**
* 注入:支付服务延迟
*/
public void injectPaymentDelay(int seconds) {
// 在测试环境给支付服务注入延迟
// 观察订单服务是否正常降级
ChaosBlade.exec(
"dubbo delay",
"--time " + seconds * 1000,
"--service payment-service"
);
}
/**
* 注入:Redis连接失败
*/
public void injectRedisFailure() {
ChaosBlade.exec(
"redis full",
"--addr redis://127.0.0.1:6379"
);
}
}
故障注入清单:
| 故障类型 | 注入方式 | 验证目标 |
|---|---|---|
| 服务延迟 | 增加响应时间 | 熔断、超时、降级 |
| 服务异常 | 返回500 | 熔断、降级 |
| 服务不可用 | 停止服务 | 服务发现、降级 |
| 网络抖动 | 注入网络延迟 | 重试、缓存 |
| 数据库慢查询 | 慢SQL | 限流、降级 |
| 缓存失效 | 清除缓存 | 缓存重建 |
七、稳定性保障体系
7.1 完整体系
【稳定性保障体系】
事前:
├── 容量规划(QPS、存储、带宽)
├── 限流设计(接入、应用、资源)
├── 监控告警(指标、日志、链路)
└── 故障预案(Runbook)
事中:
├── 自动降级(熔断、限流)
├── 手动降级(降级开关)
├── 紧急扩容(弹性伸缩)
└── 故障通告
事后:
├── 故障复盘
├── 改进措施
├── 演练验证
└── 经验分享
7.2 故障处理流程
黄金30分钟原则:
0-5分钟:故障发现
└── 监控系统告警
5-10分钟:故障定位
└── 查看监控、日志、链路
10-15分钟:故障缓解
└── 降级、限流、回滚
15-30分钟:故障恢复
└── 修复或临时方案
30分钟后:复盘
└── 根因分析、改进措施
7.3 故障等级
| 等级 | 定义 | 响应时间 | 通知范围 |
|---|---|---|---|
| P0 | 全站不可用 | 5分钟 | CEO+CTO+全员 |
| P1 | 核心功能不可用 | 15分钟 | 技术总监+团队 |
| P2 | 部分功能受损 | 30分钟 | 团队Leader |
| P3 | 性能下降 | 1小时 | 团队内部 |
八、踩坑总结
8.1 坑1:降级开关没人用
症状:降级开关做了,但大促时没人敢开。
解决:
- 培训业务和运维
- 定期演练
- 明确授权(值班人可一键降级)
8.2 坑2:兜底数据返回null
症状:兜底逻辑返回null,导致业务NPE。
解决:
- 兜底数据必须可用
- 即使是不完整的对象
- 永远不要返回null
8.3 坑3:超时设置不合理
症状:超时设置30秒,依赖服务挂了导致线程池耗尽。
解决:
- 合理设置超时
- 超时要"短小精悍"
8.4 坑4:没有演练
症状:灾备方案做了一堆,从未演练。
解决:
- 制定演练计划
- 定期演练
- 演练结果纳入考核
8.5 坑5:故障复盘流于形式
症状:故障复盘变成"甩锅大会"。
解决:
- 对事不对人
- 重点是改进不是追责
- 改进措施有跟踪
九、实战案例:双11的稳定性保障
9.1 战前准备(提前1个月)
容量评估:
- 预估峰值QPS:30万
- 预估峰值订单:100万单/小时
- 服务器扩容:2倍常规配置
压测验证:
- 全链路压测3次
- 发现并修复30+个性能问题
- 验证降级、限流配置
应急预案:
- 30+个降级开关
- 紧急扩容脚本
- 故障处理Runbook
9.2 战中保障(大促期间)
值班机制:
- 7×24小时值班
- 核心人员驻场
- 实时监控大屏
自动降级:
- 接口响应时间>2秒 → 自动降级
- 服务错误率>10% → 自动熔断
- 流量超过阈值 → 限流
手动干预:
- 大促流量低于预期 → 关闭部分降级
- 出现意外故障 → 一键降级
9.3 战果
大促数据:
- 峰值QPS:35万(超预期)
- 系统稳定性:99.99%
- 重大故障:0
- 用户投诉:<100起
对比2020年:
- 2020年:凌晨雪崩,影响4小时
- 2023年:全程稳定,零重大故障
十、总结
稳定性保障是系统性工程,不是某个技术点。
关键要点:
- 预防+检测+恢复:三层防御
- 降级是取舍:核心功能优先,非核心可放弃
- 兜底是底线:所有失败都有兜底
- 容灾是兜底中的兜底:最坏情况有备份
- 演练是保障:不演练等于没做
- 故障是老师:每次故障都是学习机会
稳定性的哲学:
系统不会因为你的期望而稳定,只会因为你的设计而稳定。
核心原则:
- 假设一切都会失败(墨菲定律)
- 故障隔离,不扩散(隔离)
- 服务降级,不雪崩(降级)
- 最后兜底,不崩溃(兜底)
- 真打实练,不虚设(演练)
最后的话:
好的系统不是不故障,而是故障时能优雅降级、快速恢复。
稳定性不是"我努力让它不挂",而是"我知道它会挂,提前做好应对"。
这就是"稳定性"和"可靠性"的区别。
今日思考 :
你们系统在大促或流量激增时表现如何?有没有降级、兜底机制?欢迎分享你的稳定性经验!
作者:架构实战团队
日期:2026-07-23
标签:#系统稳定性 #降级 #兜底 #容灾 #高可用 #限流 #熔断