物流订单系统99.99%可用性全链路容灾体系落地操作手册
手册前置说明
一、先给初级Java工程师讲透:为什么必须做这套体系?
1. 核心背景量化
你负责的是日均2300万单的物流订单系统,换算下来:
- 每秒平均订单量≈266单,大促峰值≈1.5万TPS
- 系统宕机1分钟,直接损失≈1.6万笔订单,对应百万级营收损失+资损风险+用户流失+品牌口碑影响
- 99.99%可用性 = 全年允许宕机时长≤52.56分钟;如果不做这套体系,单机房故障就可能导致单次宕机几小时,直接击穿SLA
2. 核心概念扫盲(必须先看懂,再操作)
| 术语 | 大白话解释 | 物流订单场景核心要求 |
|---|---|---|
| AZ(可用区) | 同一城市下,电力、网络物理隔离的机房,一个AZ故障不会影响其他AZ | 同城3个AZ,两两之间网络延迟≤2ms |
| 单元化 | 把系统拆成多个独立的「单元」,每个单元能完整处理一部分用户的全量下单请求,单元之间互不影响 | 按用户ID哈希分片,一个用户的所有请求都落在同一个单元,避免跨AZ调用 |
| RTO | 故障恢复时间,故障发生到系统恢复正常的时长 | 单AZ故障RTO≤30s,用户无感知 |
| RPO | 故障恢复后,数据丢失的最大时长 | RPO=0,零数据丢失,订单不丢单 |
| 核心链路 | 少了它就下不了单的流程,是必须100%保住的生命线 | 幂等校验→库存预占→订单创建→支付单创建 |
| 非核心链路 | 不影响下单主流程,只影响体验的辅助流程 | 商品详情查询、营销优惠计算、地址精细化校验、下单成功推送、发票开具等 |
| 熔断降级 | 熔断:依赖服务故障时,自动切断调用,避免拖垮自身;降级:故障时用兜底方案替代,不阻断主流程 | 非核心依赖故障时,自动降级,核心下单链路完全不受影响 |
| 混沌工程 | 主动注入故障,提前验证容灾方案是否有效,而不是等线上出故障才发现方案没用 | 每月主动注入故障,验证容灾能力 |
3. 手册整体框架
整套体系严格遵循「事前防控-事中自愈-事后闭环」的大厂黄金标准,3个环节环环相扣,缺一不可:
- 事前防控:提前把故障挡在门外,搭建不会轻易出故障的架构,提前验证所有预案
- 事中自愈:故障发生时,系统自动处理,无需人工干预,用户无感知
- 事后闭环:故障后彻底根因解决,避免重复踩坑,持续迭代优化
第一部分 事前防控:从根源降低故障概率,搭建容灾底座
模块1:同城3AZ单元化双活架构搭建
一、核心目标
实现单AZ故障时,用户无感知,RTO≤30s,RPO=0,核心下单链路零影响,彻底解决「单机房故障全量用户瘫痪」的行业痛点。
二、解决的问题 & 不做的致命隐患
| 解决的核心问题 | 不这么做的致命隐患 |
|---|---|
| 单AZ机房断电、光纤挖断、网络故障时,业务不中断 | 单机房故障直接导致全量用户无法下单,单次故障时长≥2小时,直接击穿99.99%可用性SLA |
| 按用户ID单元化路由,避免跨AZ网络调用,降低下单链路耗时 | 跨AZ调用导致链路耗时增加3-10倍,下单超时率飙升,大促时直接雪崩 |
| 数据多副本同步,实现RPO=0,零订单数据丢失 | 单机房数据库故障导致订单数据丢失,出现资损、用户投诉,合规风险 |
三、详细操作步骤(初级工程师可1:1复制)
步骤1:架构规划与环境准备
- 基础环境要求
- 同城3个独立AZ(AZ1/AZ2/AZ3),两两之间内网互通,网络延迟≤2ms
- 每个AZ都部署完整的服务集群、中间件、数据库,无单点依赖
- 核心原则:每个AZ都是一个独立的业务单元,能独立处理用户的全量下单请求
- 单元化分片规则设计
- 分片键:必须用
user_id(用户ID),因为物流订单的所有核心操作都是围绕用户展开的,一个用户的所有订单、库存、支付请求都落在同一个单元 - 分片算法:哈希取模,
单元编号 = user_id % 3,比如user_id=1001,1001%3=2,就固定路由到AZ2 - 为什么这么设计?
- 哈希取模规则简单,无状态,网关层就能完成路由,无需额外的路由服务
- 用户请求固定落在一个AZ,避免跨AZ调用,链路耗时最低
- 3个AZ流量均衡,每个AZ承载1/3的用户流量,单AZ故障时,流量可以均匀分散到另外2个AZ
- 分片键:必须用
步骤2:数据层双活部署(RPO=0的核心)
这是最关键的一步,订单系统的核心是数据,数据不丢是底线。
- MySQL数据库部署方案
-
架构:一主两从,半同步复制,跨AZ部署
- 主库:部署在AZ1,负责订单写入
- 从库1:部署在AZ2,实时同步主库数据,AZ1故障时自动切换为主库
- 从库2:部署在AZ3,实时同步主库数据,只读流量分担
-
核心配置(my.cnf),直接复制即可:
ini[mysqld] # 开启半同步复制,保证主库写入后,至少一个从库收到日志,才提交事务,实现RPO=0 plugin-load-add = semisync_master.so plugin-load-add = semisync_slave.so rpl_semi_sync_master_enabled = 1 rpl_semi_sync_slave_enabled = 1 # 主库等待从库确认的超时时间,超时后自动降级为异步,避免主库阻塞 rpl_semi_sync_master_timeout = 1000 # 开启GTID复制,故障切换时无数据丢失 gtid_mode = on enforce_gtid_consistency = 1 # binlog配置,保证数据可追溯、可恢复 log_bin = mysql-bin binlog_format = ROW expire_logs_days = 7 -
为什么这么做?
- 半同步复制:主库的每一笔订单写入,必须至少有一个从库收到binlog,才会提交事务,哪怕AZ1主库完全宕机,AZ2的从库有完整的数据,RPO=0,零订单丢失
- 跨AZ部署:3个副本在3个AZ,不会因为一个机房的电力/网络故障,导致所有数据副本丢失
- GTID复制:故障切换时,能精准找到同步位置,不会出现数据重复/丢失
-
- Redis缓存部署方案
- 架构:Redis Cluster 3主3从,跨AZ部署 ,每个主节点的从节点部署在不同的AZ
- 主节点1(AZ1)→ 从节点1(AZ2)
- 主节点2(AZ2)→ 从节点2(AZ3)
- 主节点3(AZ3)→ 从节点3(AZ1)
- 核心配置:开启持久化(RDB+AOF),主从自动切换,集群脑裂防护
- 为什么这么做?单AZ故障时,Redis主节点自动切换到其他AZ的从节点,缓存不中断,不会出现缓存雪崩导致数据库被打垮
- 架构:Redis Cluster 3主3从,跨AZ部署 ,每个主节点的从节点部署在不同的AZ
步骤3:网关层单元化路由配置(流量调度的核心)
所有用户请求先进入网关,网关根据user_id完成路由,把用户请求固定转发到对应的AZ单元。
-
技术选型:Spring Cloud Gateway(大厂主流,异步非阻塞,性能高)
-
核心路由代码&配置
-
第一步:编写自定义路由断言工厂,实现按user_id哈希路由
java// 自定义路由断言工厂,按user_id哈希分配AZ单元 @Component public class UserIdHashRoutePredicateFactory extends AbstractRoutePredicateFactory<UserIdHashRoutePredicateFactory.Config> { public UserIdHashRoutePredicateFactory() { super(Config.class); } // 3个AZ的服务地址,对应3个单元 private static final List<String> AZ_UNIT_LIST = Arrays.asList( "http://az1-logistics-gateway:8080", // AZ1单元地址 "http://az2-logistics-gateway:8080", // AZ2单元地址 "http://az3-logistics-gateway:8080" // AZ3单元地址 ); @Override public Predicate<ServerWebExchange> apply(Config config) { return exchange -> { // 1. 从请求头获取user_id(用户登录后,网关统一注入) String userIdStr = exchange.getRequest().getHeaders().getFirst("user-id"); if (StrUtil.isBlank(userIdStr)) { // 未登录用户,轮询分配到3个AZ,保证负载均衡 return true; } // 2. 按user_id哈希取模,计算目标AZ long userId = Long.parseLong(userIdStr); int azIndex = (int) (userId % AZ_UNIT_LIST.size()); String targetAz = AZ_UNIT_LIST.get(azIndex); // 3. 把目标AZ地址放入请求上下文,转发到对应单元 exchange.getAttributes().put("target_az", targetAz); return true; }; } // 配置类,可扩展 public static class Config { } } -
第二步:编写网关路由配置(application.yml),直接复制
yamlspring: cloud: gateway: routes: # 下单核心链路路由,按user_id单元化转发 - id: order-create-route uri: lb://logistics-order predicates: - Path=/order/create - name: UserIdHashRoute # 自定义的哈希路由断言 filters: - name: CircuitBreaker # 熔断配置,AZ故障时自动切流 args: name: azCircuitBreaker fallbackUri: forward:/fallback/az-switch # 其他订单相关路由,复用同样的路由规则 - id: order-other-route uri: lb://logistics-order predicates: - Path=/order/** - name: UserIdHashRoute
-
-
AZ故障自动切流兜底配置
-
编写Fallback控制器,当目标AZ健康度不达标时,自动把流量切换到其他健康AZ
java@RestController public class AzFallbackController { @Autowired private DiscoveryClient discoveryClient; // 3个AZ的服务名 private static final List<String> AZ_SERVICE_LIST = Arrays.asList( "logistics-order-az1", "logistics-order-az2", "logistics-order-az3" ); @PostMapping("/fallback/az-switch") public Mono<ServerResponse> azSwitchFallback(ServerWebExchange exchange) { // 1. 获取原请求的user_id String userIdStr = exchange.getRequest().getHeaders().getFirst("user-id"); // 2. 遍历3个AZ,找到健康的实例 for (String azService : AZ_SERVICE_LIST) { List<ServiceInstance> instances = discoveryClient.getInstances(azService); if (CollUtil.isNotEmpty(instances)) { // 3. 把请求转发到健康的AZ实例 URI targetUri = instances.get(0).getUri(); return ServerResponse.temporaryRedirect(targetUri).build(); } } // 极端情况:所有AZ都故障,返回兜底提示,不暴露系统细节 return ServerResponse.status(503).bodyValue("系统繁忙,请稍后重试"); } }
-
步骤4:服务层单元化适配
-
每个AZ的服务都注册到Nacos的对应命名空间,比如AZ1的服务注册到
nacos-namespace-az1,AZ2到nacos-namespace-az2,避免跨AZ服务调用 -
服务间RPC调用,必须优先调用同AZ的服务实例,配置如下(application.yml):
yamlspring: cloud: nacos: discovery: # 同AZ优先调用,避免跨AZ网络延迟 prefer-same-zone: true zone: az1 # 当前服务所在的AZ,每个AZ的服务修改对应值 -
为什么这么做?同AZ调用网络延迟≤1ms,跨AZ调用延迟≥3ms,下单链路有10+次RPC调用,累计能节省30+ms耗时,同时避免跨AZ网络故障影响调用。
四、验证方法(做完必须验证,确保方案有效)
- 路由正确性验证:用不同user_id的用户发起下单请求,查看网关日志,确认同一个user_id的请求始终转发到同一个AZ
- 单AZ故障模拟验证:手动停止AZ1的所有服务实例,查看网关是否在30s内把AZ1的用户流量自动切换到AZ2/AZ3,下单成功率保持100%
- 数据一致性验证:AZ1主库停止后,查看AZ2的从库是否自动切换为主库,订单数据无丢失,写入正常
模块2:全链路分级熔断降级体系搭建
一、核心目标
非核心依赖服务故障时,核心下单链路完全不受影响,极端场景下也能保住用户能正常下单,解决「非核心依赖故障拖垮整个下单链路」的雪崩问题。
二、解决的问题 & 不做的致命隐患
| 解决的核心问题 | 不这么做的致命隐患 |
|---|---|
| 营销、商品、地址等非核心服务故障时,不阻断下单流程 | 营销服务宕机,导致所有下单请求都超时,全量用户无法下单,核心链路被非核心依赖拖垮 |
| 依赖服务慢调用时,自动熔断,避免线程池被打满,出现级联雪崩 | 依赖服务响应变慢,订单服务的线程被大量占用,最终线程池打满,新的下单请求无法处理,整个服务瘫痪 |
| 大促期间流量突增时,自动流量控制,保护核心链路,拒绝非核心请求 | 大促流量峰值时,非核心请求占用大量资源,核心下单请求没有足够的资源处理,下单成功率暴跌 |
三、详细操作步骤(初级工程师可1:1复制)
步骤1:先划分核心/非核心链路,明确降级边界
这是前提,必须先搞清楚「什么必须保,什么可以丢」,物流订单系统的链路划分标准如下:
| 链路类型 | 包含的操作 | 降级原则 |
|---|---|---|
| 核心链路(必须100%保) | 幂等校验、库存预占、订单主表写入、支付单创建 | 绝对不能降级,必须保证强一致,所有资源优先保障 |
| 一级非核心链路(可轻度降级) | 收货地址校验、商品销售状态校验、运费计算 | 故障时用缓存兜底,不阻断下单,后续人工审核 |
| 二级非核心链路(可深度降级) | 营销优惠计算、优惠券核销、用户积分抵扣 | 故障时直接跳过,提示用户「优惠暂时无法使用,是否继续下单」,不阻断下单 |
| 三级非核心链路(可完全关闭) | 下单成功短信推送、APP推送、发票开具、订单同步到大数据 | 故障时直接关闭,异步重试,完全不影响同步下单链路 |
步骤2:技术选型与基础环境搭建
-
技术选型:Alibaba Sentinel(大厂主流,轻量易上手,无侵入,适配Spring Cloud,初级工程师友好)
-
环境搭建:参考之前的docker-compose.yml,一键启动Sentinel控制台,默认地址:http://127.0.0.1:8858,账号密码:sentinel/sentinel123456
-
项目引入依赖,所有业务服务都要引入:
xml<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
步骤3:3层降级策略落地(大厂标准方案)
我们针对每个非核心依赖,设计「缓存兜底→降级开关→流量控制」3层防护,一层失效,下一层补上,确保万无一失。
第一层:动态降级开关(最基础、最核心的兜底)
- 核心原理:在Nacos配置中心配置动态开关,无需重启服务,就能一键开启/关闭某个非核心功能,故障时直接跳过该功能
- 操作步骤:
-
在Nacos配置中心创建通用降级配置
logistics-common-degrade.yml,内容如下:yaml# 降级开关配置:true=开启降级,false=关闭降级 degrade: # 营销服务降级:故障时跳过优惠计算 marketing: false # 地址服务降级:故障时跳过配送范围校验 address: false # 商品服务降级:故障时用缓存商品信息,不实时查询 product: false # 推送服务降级:故障时关闭同步推送 push: true -
在订单服务中编写配置监听类,动态获取开关状态:
java@Component @RefreshScope // 配置动态刷新,无需重启服务 @ConfigurationProperties(prefix = "degrade") @Data public class DegradeSwitchConfig { private Boolean marketing = false; private Boolean address = false; private Boolean product = false; private Boolean push = true; } -
在业务代码中使用开关,示例(营销服务调用):
java@Autowired private DegradeSwitchConfig degradeSwitchConfig; @Autowired private MarketingFeignClient marketingFeignClient; // 优惠计算逻辑 public MarketingCalcDTO calcDiscount(OrderCreateDTO reqDTO, Long userId) { // 1. 判断是否开启了营销降级 if (Boolean.TRUE.equals(degradeSwitchConfig.getMarketing())) { // 降级兜底:直接返回零优惠,不调用营销服务,不阻断下单 log.warn("营销服务已降级,跳过优惠计算,userId:{}", userId); return new MarketingCalcDTO(BigDecimal.ZERO, null); } // 2. 未降级,正常调用营销服务 Result<MarketingCalcDTO> result = marketingFeignClient.calcDiscount(reqDTO, userId); if (!result.isSuccess()) { // 调用失败,临时降级兜底 log.error("营销服务调用失败,临时降级,userId:{}", userId); return new MarketingCalcDTO(BigDecimal.ZERO, null); } return result.getData(); } -
为什么这么做?
- 动态开关,无需重启服务,故障时10s内就能完成降级,比人工改代码重启快100倍
- 代码侵入性极低,初级工程师一看就懂,不会写错
- 双重兜底:开关手动降级 + 调用失败自动降级,双重保障
-
第二层:缓存兜底策略(降级后保证用户体验)
- 核心原理:非核心依赖的查询结果,提前缓存到Redis+本地缓存,依赖服务故障时,用缓存数据兜底,而不是直接跳过,保证用户体验基本不受影响
- 操作步骤(以地址服务为例):
-
地址服务查询接口,增加多级缓存:
java// 本地缓存,缓存热点地址数据,TTL 5分钟 private final LoadingCache<Long, AddressDTO> addressLocalCache = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.MINUTES) .maximumSize(10000) .build(addressId -> getAddressFromDB(addressId)); @Autowired private StringRedisTemplate redisTemplate; private static final String ADDRESS_CACHE_KEY = "address:info:"; // 地址查询核心接口 public AddressDTO getAddressById(Long addressId, Long userId) { // 1. 先查本地缓存,最快,无网络开销 try { return addressLocalCache.get(addressId); } catch (Exception e) { log.warn("本地缓存查询失败,addressId:{}", addressId); } // 2. 本地缓存没命中,查Redis分布式缓存 String addressJson = redisTemplate.opsForValue().get(ADDRESS_CACHE_KEY + addressId); if (StrUtil.isNotBlank(addressJson)) { AddressDTO addressDTO = JSONUtil.toBean(addressJson, AddressDTO.class); // 回写到本地缓存 addressLocalCache.put(addressId, addressDTO); return addressDTO; } // 3. 缓存都没命中,查数据库 AddressDTO addressDTO = getAddressFromDB(addressId); // 4. 回写到Redis和本地缓存,设置过期时间1小时 redisTemplate.opsForValue().set(ADDRESS_CACHE_KEY + addressId, JSONUtil.toJsonStr(addressDTO), 1, TimeUnit.HOURS); addressLocalCache.put(addressId, addressDTO); return addressDTO; } -
订单服务调用地址服务时,故障时用缓存兜底:
javapublic AddressDTO getAddressInfo(Long addressId, Long userId) { // 1. 先判断是否开启地址降级 if (Boolean.TRUE.equals(degradeSwitchConfig.getAddress())) { // 降级:直接从Redis缓存获取地址信息,不调用地址服务 String addressJson = redisTemplate.opsForValue().get(ADDRESS_CACHE_KEY + addressId); if (StrUtil.isNotBlank(addressJson)) { return JSONUtil.toBean(addressJson, AddressDTO.class); } } // 2. 正常调用地址服务 Result<AddressDTO> result = addressFeignClient.getAddressById(addressId, userId); if (!result.isSuccess()) { // 调用失败,用缓存兜底 String addressJson = redisTemplate.opsForValue().get(ADDRESS_CACHE_KEY + addressId); if (StrUtil.isNotBlank(addressJson)) { log.warn("地址服务调用失败,用缓存兜底,addressId:{}", addressId); return JSONUtil.toBean(addressJson, AddressDTO.class); } throw new BusinessException("地址信息查询失败"); } return result.getData(); } -
为什么这么做?
- 多级缓存,命中率≥99%,依赖服务故障时,用户几乎感知不到,体验比直接跳过好太多
- 本地缓存+Redis缓存,双重兜底,哪怕Redis也故障,本地缓存还能顶上去
-
第三层:流量控制与自动熔断(无需人工干预,系统自动处理)
- 核心原理:配置Sentinel规则,当依赖服务出现慢调用、异常比例过高时,自动熔断,调用兜底逻辑,无需人工操作,故障秒级响应
- 操作步骤:
-
在Sentinel控制台配置熔断规则,以营销服务为例:
规则类型 阈值配置 说明 慢调用熔断 最大RT=500ms,比例阈值=50%,熔断时长=10s,请求数阈值=10 10个请求里,有50%的请求耗时超过500ms,就触发熔断,10s内不调用营销服务,直接走兜底 异常比例熔断 异常比例=50%,熔断时长=30s,请求数阈值=10 10个请求里,有50%的请求异常,就触发熔断,30s内走兜底 异常数熔断 异常数=5,熔断时长=30s 1分钟内出现5次异常,就触发熔断 -
代码中配置熔断兜底,使用Sentinel的
@SentinelResource注解:java// 注解说明:value=资源名,blockHandler=限流兜底,fallback=熔断/异常兜底 @SentinelResource( value = "calcMarketingDiscount", blockHandler = "calcDiscountBlockHandler", fallback = "calcDiscountFallback" ) public MarketingCalcDTO calcDiscount(OrderCreateDTO reqDTO, Long userId) { // 正常业务逻辑,调用营销服务 Result<MarketingCalcDTO> result = marketingFeignClient.calcDiscount(reqDTO, userId); if (!result.isSuccess()) { throw new BusinessException("营销服务调用失败"); } return result.getData(); } // 限流兜底:触发流量控制时,调用这个方法 public MarketingCalcDTO calcDiscountBlockHandler(OrderCreateDTO reqDTO, Long userId, BlockException e) { log.warn("营销服务触发限流,兜底处理,userId:{}", userId); return new MarketingCalcDTO(BigDecimal.ZERO, null); } // 熔断/异常兜底:调用异常、触发熔断时,调用这个方法 public MarketingCalcDTO calcDiscountFallback(OrderCreateDTO reqDTO, Long userId, Throwable e) { log.error("营销服务调用异常,触发熔断兜底,userId:{}", userId, e); return new MarketingCalcDTO(BigDecimal.ZERO, null); } -
为什么这么做?
- 全自动,无需人工干预,故障发生的瞬间就触发熔断,比人工操作快太多
- 规则可动态调整,大促前可以收紧阈值,平峰期可以放宽,灵活适配业务场景
-
步骤4:全链路降级策略落地规范
所有非核心依赖,必须严格遵循以下规范,初级工程师直接套模板即可:
- 必须配置动态降级开关,支持一键降级
- 必须配置多级缓存兜底,故障时保证用户体验
- 必须配置Sentinel自动熔断规则,无需人工干预
- 降级兜底逻辑必须轻量,不能有新的远程调用,避免二次故障
- 降级必须打日志,记录降级时间、用户、原因,方便后续追溯
四、验证方法
- 开关降级验证:在Nacos把营销服务的降级开关改成true,发起下单请求,查看是否跳过营销服务调用,正常下单成功
- 熔断验证:手动停止营销服务,连续发起10次下单请求,查看Sentinel是否触发熔断,后续请求直接走兜底逻辑,下单成功
- 缓存兜底验证:停止地址服务,发起下单请求,查看是否用缓存的地址信息完成下单,无异常
模块3:混沌工程常态化演练体系搭建
一、核心目标
主动注入故障,提前验证你的容灾、降级方案是否真的有效,而不是等线上出故障才发现「方案写的很好,实际完全没用」,保障大促零故障。
二、解决的问题 & 不做的致命隐患
| 解决的核心问题 | 不这么做的致命隐患 |
|---|---|
| 提前发现容灾架构的漏洞,比如切流不生效、降级不触发、数据同步异常 | 线上出故障时,才发现容灾方案失效,故障时长从30s变成几小时,损失惨重 |
| 提升团队的故障应急能力,出故障时不慌,有成熟的预案 | 线上故障时,团队手忙脚乱,误操作导致故障扩大,比如删库、错误回滚 |
| 持续优化容灾体系,发现潜在的风险点 | 风险点持续积累,最终在大促期间集中爆发,造成不可挽回的损失 |
三、详细操作步骤(初级工程师可1:1复制)
步骤1:混沌工程工具选型与环境搭建
- 技术选型:ChaosBlade(阿里开源,国内大厂主流,命令简单,初级工程师友好,支持Java应用、Docker、K8s、数据库、缓存等全场景故障注入)
- 环境搭建:
- 下载ChaosBlade:
wget https://github.com/chaosblade-io/chaosblade/releases/download/v1.7.2/chaosblade-1.7.2-linux-amd64.tar.gz - 解压:
tar -zxvf chaosblade-1.7.2-linux-amd64.tar.gz - 进入目录:
cd chaosblade-1.7.2 - 启动blade工具:
./blade prepare jvm --process 订单服务的Java进程PID
- 下载ChaosBlade:
- 核心原则:先测试环境,后预发环境,最后生产环境低峰期小流量演练,绝对不能直接在生产环境全量注入故障
步骤2:确定演练场景与验收标准
针对物流订单系统,我们固定20+核心故障场景,覆盖95%以上的线上故障,初级工程师直接按这个列表演练即可:
| 演练场景分类 | 具体故障注入场景 | 验收标准(必须100%通过) |
|---|---|---|
| 基础设施故障 | 1. 单AZ所有服务实例宕机 | 30s内自动切流,下单成功率100%,RPO=0,用户无感知 |
| 2. 订单服务50%实例宕机 | 自动负载均衡,下单成功率100%,响应时间波动≤20% | |
| 3. 服务器CPU使用率100% | 自动弹性扩缩容,核心链路不受影响,下单成功率≥99.9% | |
| 中间件故障 | 4. Redis集群单节点宕机 | 自动主从切换,缓存命中率≥99%,下单链路无影响 |
| 5. Redis响应延迟增加到500ms | 自动降级,本地缓存兜底,下单成功率100% | |
| 6. MySQL主库宕机 | 30s内主从切换,数据零丢失,订单写入正常 | |
| 7. MySQL响应延迟增加到1s | 缓存兜底,慢查询自动熔断,核心下单不受影响 | |
| 8. RocketMQ集群单节点宕机 | 消息自动切换到其他节点,消息零丢失,异步流程正常 | |
| 依赖服务故障 | 9. 营销服务完全宕机 | 自动熔断降级,下单成功率100%,用户仅看不到优惠,不影响下单 |
| 10. 地址服务响应超时 | 缓存兜底,自动降级,下单成功率100% | |
| 11. 商品服务异常比例80% | 自动熔断,缓存兜底,下单成功率100% | |
| 12. 所有非核心服务同时宕机 | 核心下单链路完全正常,成功率100% | |
| 业务流量故障 | 13. 下单流量突增10倍 | 自动弹性扩容,下单成功率≥99.9%,系统不宕机 |
| 14. 热点SKU下单流量突增100倍 | 锁竞争优化生效,无超卖,下单成功率100% |
步骤3:标准化演练流程(大厂固定流程,必须严格遵守)
绝对不能上来就注入故障,必须按以下6步走,避免影响线上业务
- 第一步:预案准备(演练前3天完成)
- 明确演练场景、故障注入方式、回滚方案、验收标准
- 准备好应急联系人,所有相关团队(运维、DBA、中间件)同步到位
- 提前备份数据,配置好监控告警,实时观察系统状态
- 第二步:基线确认(演练前1小时完成)
- 确认演练前系统的核心指标:下单成功率100%、响应时间P99≤300ms、无异常告警
- 把基线数据记录下来,和演练后的指标做对比
- 第三步:小流量故障注入(低峰期执行,比如凌晨2-4点)
- 先注入小流量故障,比如先停止1台订单服务实例,而不是整个AZ的所有实例
- 观察系统的反应,监控指标是否符合预期,有没有出现异常
- 第四步:全量故障注入(小流量验证通过后执行)
- 按演练场景注入全量故障,比如停止整个AZ的服务实例
- 持续观察监控指标,记录故障发生后的系统表现,比如切流时长、下单成功率
- 第五步:故障恢复与回滚
- 演练完成后,立即恢复故障,比如重启停止的服务实例
- 观察系统是否恢复到演练前的基线状态,确认无残留影响
- 第六步:复盘与优化(演练后24小时内完成)
- 开复盘会,对比验收标准,哪些通过了,哪些没通过
- 用5Why分析法找到没通过的根因,制定优化方案,7天内完成落地
步骤4:常态化演练机制落地
- 日常演练:每月固定1次,注入2-3个核心场景,验证容灾方案的有效性
- 大促前演练:每次大促前2周,完成全量20+场景的全覆盖演练,所有场景必须100%通过,才能进入大促保障期
- 故障复盘后演练:线上发生故障后,把故障场景加入混沌演练列表,验证修复方案的有效性,避免重复发生
- 新人培训演练:新人入职后,必须完成混沌演练实操,熟悉故障场景和应急流程,提升团队整体应急能力
四、验证方法
- 所有演练场景,必须100%达到验收标准,才算通过
- 演练完成后,优化方案必须在7天内落地,再次演练验证通过
- 连续3次全量演练,所有场景100%通过,才算容灾体系完全落地
第二部分 事中自愈:故障发生时,系统自动处理,用户无感知
模块1:单AZ故障自动切流体系
一、核心目标
单AZ故障时,系统在30s内自动把故障AZ的流量切换到健康AZ,无需人工干预,用户无感知,RTO≤30s。
二、解决的问题 & 不做的隐患
| 解决的问题 | 不做的隐患 |
|---|---|
| 无需人工半夜起来处理故障,系统自动完成切流 | 凌晨机房故障,运维人员10分钟才响应,再花20分钟切流,故障时长30分钟以上,严重影响SLA |
| 切流过程平滑,无流量抖动,下单零失败 | 人工切流容易出现配置错误,导致流量雪崩,下单成功率暴跌 |
三、详细操作步骤
-
健康检查配置(自动切流的前提)
-
网关层健康检查:配置Nginx/Spring Cloud Gateway的健康检查,每隔5s探测一次AZ内的服务实例,连续3次探测失败,就把该实例标记为不健康,剔除转发列表
-
配置示例(Spring Cloud Gateway):
yamlspring: cloud: gateway: # 全局健康检查配置 globalcors: add-to-simple-url-handler-mapping: true loadbalancer: # 健康检查开启 health-check: enabled: true # 探测间隔5s interval: 5s # 连续3次失败,标记为不健康 failure-threshold: 3 # 连续2次成功,标记为健康 success-threshold: 2
-
-
自动切流触发阈值配置
- 当一个AZ内的核心服务(订单、库存)健康实例占比<80%时,自动触发全AZ切流,把该AZ的所有流量切换到其他2个健康AZ
- 实现方式:用Nacos的监听功能,实时监听服务实例的健康状态,当健康率低于阈值时,自动更新网关路由规则,切换流量
-
切流平滑过渡
- 切流时,不是一次性把所有流量切过去,而是10%→30%→50%→100%的灰度切流,避免流量突增把健康AZ打垮
- 切流过程中,持续监控健康AZ的CPU、内存、TPS,一旦出现异常,立即暂停切流
四、验证方法
手动停止AZ1的80%订单服务实例,查看网关是否在30s内触发自动切流,把AZ1的流量切换到AZ2/AZ3,下单成功率保持100%。
模块2:核心链路故障自动熔断自愈
一、核心目标
依赖服务出现故障时,系统自动熔断,调用兜底逻辑,避免级联雪崩,核心链路不受影响,无需人工干预。
二、详细操作步骤
-
全链路熔断规则统一配置
- 所有非核心依赖的Feign接口,都必须配置Sentinel熔断规则,统一阈值:慢调用RT≥500ms,比例≥50%,熔断时长10s;异常比例≥50%,熔断时长30s
- 所有熔断规则都在Nacos配置中心统一管理,动态更新,无需重启服务
-
线程池隔离
-
每个依赖服务的调用,都用独立的线程池,避免一个依赖服务故障,把整个订单服务的线程池打满
-
配置示例:
java// 营销服务独立线程池 private final ThreadPoolExecutor marketingThreadPool = new ThreadPoolExecutor( 5, 10, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100) ); // 用独立线程池调用营销服务,避免阻塞主线程 public CompletableFuture<MarketingCalcDTO> asyncCalcDiscount(OrderCreateDTO reqDTO, Long userId) { return CompletableFuture.supplyAsync(() -> calcDiscount(reqDTO, userId), marketingThreadPool); }
-
-
熔断自动恢复
- 熔断时长结束后,Sentinel会自动进入半开状态,放行少量请求,如果请求成功,就关闭熔断,恢复正常调用;如果失败,继续熔断,避免故障恢复不及时
模块3:资源瓶颈自动弹性扩缩容
一、核心目标
流量突增时,系统自动扩容服务实例;流量下降时,自动缩容,既保障峰值流量下的系统稳定,又节约资源成本。
二、详细操作步骤
-
K8s HPA弹性扩缩容配置
-
基于CPU利用率、内存使用率、TPS三个指标,配置自动扩缩容,示例yaml:
yamlapiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: logistics-order-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: logistics-order # 最小实例数,日常保障 minReplicas: 6 # 最大实例数,大促峰值保障 maxReplicas: 30 metrics: # 基于CPU利用率,阈值70% - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 # 基于内存使用率,阈值80% - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80 # 扩容冷却时间30s,快速扩容应对峰值 behavior: scaleUp: stabilizationWindowSeconds: 30 # 缩容冷却时间5分钟,避免频繁扩缩容 scaleDown: stabilizationWindowSeconds: 300
-
-
大促前预热扩容
- 大促前2小时,提前把实例数扩容到最大的80%,避免大促开始时,流量突增,扩容不及时导致系统被打垮
-
多维度扩容触发
- 除了CPU/内存,还可以基于MQ消息堆积量、接口响应时间、请求排队数等指标,触发扩容,全方位保障系统稳定
第三部分 事后闭环:故障后彻底解决,避免重复踩坑
模块1:故障全链路追踪与根因定位体系
一、核心目标
故障发生后,5分钟内定位根因,10分钟内恢复业务,找到根本原因,而不是只解决表面问题。
二、详细操作步骤
- 三位一体可观测体系搭建
- Metrics(指标):Prometheus+Grafana,实时监控核心指标,故障时第一时间发现异常点
- Trace(链路):SkyWalking,全链路追踪,TraceID透传所有系统,精准定位哪个环节出了问题
- Logging(日志):ELK,全量日志收集,所有日志都携带TraceID,通过TraceID就能查到该请求的所有日志
- 标准化故障排查步骤(初级工程师照着做就行)
- 第一步:看监控大盘,确认故障范围,是全量用户还是部分用户,是哪个环节出了问题
- 第二步:拿异常请求的TraceID,在SkyWalking看链路瀑布图,定位是哪个服务、哪个接口耗时高/异常
- 第三步:用TraceID在ELK查对应日志,找到具体的异常信息、报错堆栈
- 第四步:定位根因,制定恢复方案,先恢复业务,再深入分析
- 根因定位方法:5Why分析法
- 示例:
- 为什么下单失败?→ 库存预占超时
- 为什么库存预占超时?→ 库存服务数据库慢查询
- 为什么慢查询?→ 库存表的sku_id+warehouse_id联合索引失效
- 为什么索引失效?→ 代码里用了函数包裹sku_id字段,导致索引失效
- 为什么会出现这种代码?→ 团队没有SQL评审规范,上线前没有做SQL审核
- 根本原因:SQL评审规范缺失,而不是单纯的索引失效,只有解决根本原因,才能避免下次再出现
- 示例:
模块2:故障复盘与预案优化机制
一、核心目标
同一个故障,绝对不能发生第二次,形成「故障-复盘-优化-验证」的闭环。
二、详细操作步骤
-
标准化故障复盘报告模板(大厂通用,直接用)
模块 内容要求 故障概述 故障发生时间、恢复时间、故障时长、影响范围、影响用户量、资损金额 故障时间线 故障发生→发现→响应→恢复的完整时间线,每个节点的时间、操作人、操作内容 根因分析 用5Why分析法,找到根本原因,不是表面原因 临时恢复措施 故障发生时,做了什么操作恢复了业务 长期优化方案 针对根本原因,制定的优化方案,明确责任人、完成时间 预防措施 怎么避免同类故障再次发生,比如加入混沌演练、增加监控告警、完善规范 复盘总结 本次故障的经验教训,团队需要改进的地方 -
复盘流程规范
- P0级故障:24小时内必须开复盘会,7天内完成所有优化方案落地
- P1级故障:3个工作日内开复盘会,14天内完成优化落地
- 所有复盘报告,必须全团队同步,组织全员学习,避免其他团队踩同样的坑
-
预案优化
- 每次故障复盘后,必须更新故障应急预案,把本次故障的处理流程、恢复步骤加入预案
- 预案必须可落地,每一步都有明确的操作人、操作步骤、验证方法,初级工程师照着预案就能操作
模块3:容灾体系持续迭代优化
一、核心目标
容灾体系不是一成不变的,要跟着业务发展、流量增长、架构升级持续优化,永远能支撑业务需求。
二、详细操作步骤
- 季度架构评审:每季度组织一次容灾架构评审,看当前的架构是否能支撑未来6个月的业务增长、流量峰值,有没有需要优化的地方
- 新业务场景适配:新增业务场景(比如跨境物流、冷链物流),必须先评估容灾需求,补充对应的降级策略、演练场景,再上线
- 新技术落地:持续跟进业界主流的容灾技术,比如异地多活、Serverless弹性伸缩,持续优化架构,提升可用性
- 年度容灾大演练:每年组织一次全公司级的容灾大演练,模拟城市级故障,验证异地多活架构的有效性,把可用性从99.99%提升到99.999%
手册最终验收标准
做完以上所有操作,你的系统必须达到以下标准,才算真正实现了99.99%以上的可用性:
- 单AZ故障时,30s内自动切流,下单成功率100%,RPO=0,用户无感知
- 所有非核心依赖服务同时故障时,核心下单链路完全正常,成功率100%
- 每月混沌工程演练,20+核心场景100%通过验收
- 全年故障总时长≤52分钟,可用性SLA≥99.99%
- 线上故障发生后,5分钟内定位根因,10分钟内恢复业务,同类故障不重复发生