第一次混沌演练把生产打挂 23 分钟:稳态定错、半径失控,4 个换来的规矩
title: 第一次混沌演练把生产打挂 23 分钟稳态定错、半径失控4 个换来的规矩tags: [混沌工程, 故障注入, ChaosBlade, Chaos Mesh, 稳定性]category: 后端那天的演练计划写得很漂亮给订单服务调用库存服务的链路注入 500ms 延迟观察熔断是否按预期触发预计影响面 5% 流量持续 3 分钟。实际结果是注入开始后第 40 秒订单服务的 Tomcat 线程池打满第 70 秒网关开始返回 504第 110 秒连不在演练范围内的用户中心也开始超时。我们在第 3 分钟按计划停止注入但系统没有自己恢复——数据库连接池里堆积的等待线程、MQ 里积压的重试消息、Redis 里被写坏的降级标记三样东西互相拖着直到 23 分钟后手动清理才彻底恢复。复盘会上有人说这不就是演练该发现的问题吗。话是没错但代价是真实的那 23 分钟里下单成功率从 99.6% 掉到 71%客诉 380 多条。混沌工程的价值在于用可控的代价换取认知一旦代价失控它就变成了一次自制的故障。这篇写的是我们后来把演练流程重建了一遍的过程以及四条现在写进规范里的硬规矩。教训一稳态指标定成了服务是否存活而不是业务是否正常原始方案里我们定义的稳态是订单服务 HTTP 健康检查返回 200。这个指标在整个事故过程中一直是绿的——因为健康检查端点不查数据库、不调下游它只证明进程还活着。真正该看的是业务指标。我们后来把稳态假设改成了四条全部要有明确的数值和采样窗口下单接口成功率 ≥ 99.0%1 分钟滚动窗口下单接口 P99 ≤ 800ms1 分钟滚动窗口支付回调处理延迟 ≤ 5 秒队列积压量 200订单表写入 TPS 偏离基线不超过 ±30%这四条里任何一条连续 30 秒不满足演练必须立即中止。这个自动熔断是代码写死的不依赖值班同学的判断——事故当天我们其实在第 70 秒就有人喊停了但从决策到真正执行停止注入的命令中间过了 100 多秒。/** * 演练守护器独立进程运行与被注入的服务不在同一台机器上。 * 这一点很重要——如果守护器和被测服务同生共死故障一来它自己也挂了 * 就没人执行中止动作了。我们第一版就是这么写的。 */ Component public class ChaosGuardian { private final MetricsQueryClient metrics; private final ChaosController chaos; // 连续违反次数计数单次抖动不算连续 3 次每次 10 秒采样才中止 private final MapString, AtomicInteger violationCount new ConcurrentHashMap(); Scheduled(fixedRate 10_000) public void check() { ChaosExperiment running chaos.getRunningExperiment(); if (running null) { violationCount.clear(); return; } for (SteadyStateHypothesis h : running.getHypotheses()) { double actual metrics.query(h.getPromql(), Duration.ofMinutes(1)); boolean ok h.getComparator().test(actual, h.getThreshold()); AtomicInteger cnt violationCount.computeIfAbsent( h.getName(), k - new AtomicInteger()); if (ok) { cnt.set(0); continue; } int times cnt.incrementAndGet(); log.warn(steady state violated: {} actual{} threshold{} times{}, h.getName(), actual, h.getThreshold(), times); if (times 3) { // 中止动作必须幂等且尽最大努力 // 先停注入再触发回滚剧本最后才通知人 chaos.abort(running.getId(), steady state violated: h.getName()); rollbackPlaybook.execute(running); alerting.pageOnCall(running, h, actual); return; } } } }metrics.query走的是 Prometheus 的 range queryPromQL 长这样sum(rate(http_server_requests_seconds_count{uri/api/order/create,status~2..}[1m])) / sum(rate(http_server_requests_seconds_count{uri/api/order/create}[1m]))有个细节这个查询要排除演练自己造的流量。我们用一个特殊的 headerX-Chaos-Traffic: 1标记演练流量在指标上打 label 区分否则演练流量本身会污染稳态判断。教训二爆炸半径控制在流量比例上是不够的方案里写的是影响 5% 流量实现方式是在注入规则里加一个 percent5。听起来很安全实际上完全没起到隔离作用。原因是资源是共享的。订单服务那 8 个实例每个实例的 Tomcat 线程池是 200数据库连接池是 50。5% 的请求被延迟 500ms意味着这 5% 的请求会长时间占住线程和连接。当 QPS 是 3000 时5% 就是 150 QPS每个请求多占 500ms稳态下就是 75 个线程被长期占用。这 75 个线程是从 200 里扣的剩下的 125 个要扛 95% 的正常流量——线程池打满只是时间问题。真正有效的爆炸半径控制是资源维度的隔离不是流量比例。我们后来改了三处第一演练只在专门的实例组上做。用 K8s 的 label 把 8 个实例分成chaos-target1 个和normal7 个注入规则只匹配chaos-target网关按 header 把演练流量路由过去。# Chaos Mesh 的 NetworkChaos只作用于打了 chaos-target 标签的 Pod apiVersion: chaos-mesh.org/v1alpha1 kind: NetworkChaos metadata: name: order-to-inventory-delay namespace: prod spec: action: delay mode: all selector: namespaces: [prod] labelSelectors: app: order-service chaos-group: chaos-target # 关键只选中被隔离出来的那 1 个实例 direction: to target: mode: all selector: namespaces: [prod] labelSelectors: app: inventory-service delay: latency: 500ms correlation: 0 jitter: 50ms duration: 3m第二给演练实例单独收紧线程池和超时。演练实例的 Tomcat 线程池调到 50Feign 调用超时从 3 秒改到 1 秒。这样即使被打满影响也被限制在这一个实例内网关的健康检查会把它摘掉。第三注入前先做预演。在预发环境跑一遍完全相同的实验把稳态指标的基线和阈值校准好再上生产。这一步我们最初跳过了理由是预发流量太小不真实。现在的做法是承认预发不真实但它的目的不是验证系统韧性而是验证演练脚本本身没写错——事故当天我们的回滚脚本里有个 kubectl 命名空间参数写错了执行时报错这本来在预发就能发现。教训三只注入故障没准备恢复剧本停止注入不等于系统恢复。事故当天注入停了之后还有三样东西在自我维持Tomcat 线程池里的堆积请求。这些请求已经进来了还在等下游返回。虽然下游延迟消失了但队列里排了 4000 多个请求按正常处理速度也要几分钟才能消化而这期间新请求还在进来。MQ 的重试消息。订单创建失败后会发一条补偿消息RocketMQ 默认 16 次重试间隔从 1 秒递增到 2 小时。事故期间积压了 11 万条重试消息恢复后这些消息集体重投又把库存服务打了一波。Redis 里的降级标记。我们的降级开关是连续失败 N 次就打开TTL 10 分钟。注入停止后这个标记还有 8 分钟才过期期间所有下单都走降级逻辑跳过库存校验直接下单导致超卖 47 单。后来我们要求每个实验必须配套一份回滚剧本且剧本本身要在预发验证过public class RollbackPlaybook { public void execute(ChaosExperiment exp) { // 顺序很重要先切流再清状态最后放开 ListRollbackStep steps List.of( // 1. 把演练实例从网关摘掉停止新流量进入 new RollbackStep(drain-target, () - gatewayClient.removeUpstream(exp.getTargetInstances()), Duration.ofSeconds(10)), // 2. 主动清空降级标记不等 TTL 自然过期。 // 这一步必须在放开流量之前做否则新进来的请求还会走降级路径 new RollbackStep(clear-degrade-flag, () - redisTemplate.delete(exp.getDegradeFlagKeys()), Duration.ofSeconds(5)), // 3. 暂停重试队列的消费避免积压消息在恢复瞬间集中冲击。 // RocketMQ 用 suspendKafka 用 pause之后按 200 TPS 限速慢慢放 new RollbackStep(throttle-retry-queue, () - mqAdmin.suspendConsumer(exp.getRetryTopic()), Duration.ofSeconds(5)), // 4. 等线程池排空最长等 60 秒。等不到就强制重启实例—— // 这个决定我们纠结过但堆积请求靠自然消化有时要 10 分钟以上 new RollbackStep(wait-thread-pool-drain, () - waitUntilIdle(exp.getTargetInstances(), Duration.ofSeconds(60)), Duration.ofSeconds(65)), // 5. 重新挂回网关逐步放量10% → 30% → 100%每档观察 30 秒 new RollbackStep(restore-traffic, () - gatewayClient.rampUp(exp.getTargetInstances(), List.of(10, 30, 100), Duration.ofSeconds(30)), Duration.ofMinutes(2)) ); for (RollbackStep step : steps) { try { step.runWithTimeout(); log.info(rollback step done: {}, step.getName()); } catch (Exception e) { // 单步失败不中断整个剧本记录并继续。 // 恢复过程中卡在某一步不动比继续往下走危险得多 log.error(rollback step failed: {}, continue anyway, step.getName(), e); alerting.notify(rollback step failed: step.getName()); } } } }第 4 步等不到就重启这个决定团队里争论了很久。反对意见是重启会丢失正在处理的请求支持意见是那些请求本来也已经超时了。我们最后的折中是等待 60 秒超时后先看堆积量趋势如果在下降就继续等如果持平或上升就重启。这个判断逻辑写在waitUntilIdle里不靠人工。教训四应用层的故障注入比基础设施层更可控Chaos Mesh 这类工具在网络层做注入tc netem 加延迟、iptables 丢包优点是不侵入代码缺点是粒度太粗——它只能按 Pod、按端口、按 IP 段来切没法做到只让查询库存的这个方法慢其他调用正常。对于精细化的场景我们在应用层加了一个注入切面。它平时完全无开销开关关闭时直接返回只在演练期间由配置中心下发规则激活。Aspect Component public class ChaosInjectionAspect { // volatile 整体替换避免读写锁开销。配置中心变更时整个 Map 换掉 private volatile MapString, InjectionRule rules Collections.emptyMap(); Around(annotation(com.example.chaos.ChaosPoint)) public Object inject(ProceedingJoinPoint pjp) throws Throwable { // 快路径没有任何规则时直接放行这个判断的开销约 2ns MapString, InjectionRule snapshot rules; if (snapshot.isEmpty()) { return pjp.proceed(); } String point pjp.getSignature().toShortString(); InjectionRule rule snapshot.get(point); if (rule null || !rule.hit()) { // hit() 内部做百分比采样 return pjp.proceed(); } // 只对带演练标记的流量注入防止误伤真实用户。 // 这个检查绝对不能少——我们第二次演练时忘了加 // 结果 3% 的真实用户请求也被注入了延迟 if (!ChaosContext.isChaosTraffic()) { return pjp.proceed(); } switch (rule.getType()) { case DELAY: // 用 parkNanos 而不是 Thread.sleep前者不吃中断状态 LockSupport.parkNanos(rule.getDelayMillis() * 1_000_000L); return pjp.proceed(); case EXCEPTION: // 抛的异常类型要和真实故障一致 // 抛 RuntimeException 测不出针对 TimeoutException 写的 catch 分支 throw rule.newException(); case EMPTY_RESULT: // 返回空结果用来测试上游对查不到数据的处理 return rule.emptyValueFor(((MethodSignature) pjp.getSignature()).getReturnType()); default: return pjp.proceed(); } } }ChaosContext.isChaosTraffic()读的是从网关透传下来的 header通过 ThreadLocal 存储在异步调用时用 TransmittableThreadLocal 传递。这一层保护是第二次演练之后加的——那次我们在规则里只写了 percent3没加流量标记判断结果 3% 的真实用户请求被注入了 500ms 延迟虽然没造成事故但性质上已经是拿用户做实验了。三类注入工具的适用范围工具 / 层次注入能力粒度侵入性适合场景Chaos MeshK8s网络延迟/丢包/分区、Pod kill、IO 故障、时钟偏移Pod / 容器无侵入需 K8s 权限验证基础设施韧性、多副本切换ChaosBlade上述 JVM 层方法延迟、抛异常、CPU/内存打满进程 / 方法需挂 agent单机资源故障、JVM 方法级注入应用层切面方法延迟、返回空、抛指定异常、返回脏数据方法 参数条件需改代码埋点精细业务分支验证依赖侧 Mock如 WireMock下游返回慢、返回错误码、返回畸形报文单个 HTTP 依赖需改路由第三方接口异常验证第三行的返回脏数据是我最推荐但最少人做的一类。系统对下游超时通常都有处理但对下游返回了格式正确、内容错误的数据往往毫无防备。我们注入过一次库存服务返回负数库存结果订单服务算出了负数金额一路写进了数据库。这个 bug 在正常测试和网络层混沌演练里都不会暴露。我的几个判断没有完善监控之前不要做混沌工程。这不是保守是逻辑问题混沌工程的产出是发现系统的未知弱点而发现依赖于观测。监控不到位时做演练最可能的结果是把系统搞挂了但不知道为什么收益为负。判断标准很简单——如果你不能在 1 分钟内说出现在下单成功率是多少就先别演练。不建议一上来就在生产环境做。业界推崇生产环境才有真实性这话对但有前提你的演练流程本身要先成熟。我们的顺序是预发跑 5 次验证脚本生产影子流量跑 3 次验证观测最后才对真实流量的隔离实例做。跳过前面两步代价就是开头那 23 分钟。演练的目标不是证明系统很稳而是找到它在哪不稳。如果连续几次演练都平安无事第一反应应该是怀疑注入的强度不够或者稳态指标定得太松而不是庆祝。我们现在的规矩是连续 3 次演练零发现就要提升故障强度或换新的故障场景。复盘几个数字事故注入 500ms 延迟40 秒线程池打满110 秒波及无关服务恢复耗时 23 分钟下单成功率最低 71%客诉 380 条超卖 47 单。改造后的第一次生产演练同样场景影响面控制在 1 个实例下单成功率最低 99.2%守护器在第 34 秒自动中止当时阈值定得偏紧恢复耗时 41 秒。应用层注入切面的性能开销规则为空时 JMH 测出 1.8ns/次调用规则激活但未命中时 34ns/次可以常驻生产。半年内累计演练 41 次发现有效缺陷 19 个其中 6 个是 P1 级会导致资损或大面积不可用包括那个下游返回负库存的问题。演练平均耗时从最初的 2 小时含人工准备和恢复压到 22 分钟主要收益来自回滚剧本自动化。留个问题有个场景我们至今没找到好办法怎么演练数据库主从延迟 30 秒用 Chaos Mesh 给从库注入网络延迟会导致复制直接中断而不是延迟用pt-slave-delay那类工具又只能整体延迟不能按业务隔离。而这个场景在真实生产里出现过至少四次每次都是读写分离的读请求读到旧数据业务表现千奇百怪。你们是怎么覆盖这类数据一致性延迟故障的

相关新闻

2024年杭州集团网站建设指南:从战略规划到技术落地的全解析

2024年杭州集团网站建设指南:从战略规划到技术落地的全解析

说实话,每次听到“杭州集团网站建设”这个短语,我脑海里浮现的画面总是伴随着钱塘江畔的潮湿空气和写字楼里彻夜不眠的灯光。杭州,这座被誉为“数字之都”的城市,不仅有着西湖的温婉,更有着互联网浪潮的汹涌澎湃。在这里,做网站不仅仅是放几个图片、写几段文字那么简单,…

2026/10/21 1:38:34 阅读更多 →
给 Linux 加新硬盘的挂载命令

给 Linux 加新硬盘的挂载命令

挂载后加入磁盘 sdb 并读取全部分区内容 第一步:确认磁盘与分区信息 # 查看所有块设备 lsblk# 查看 sdb 详细分区表 sudo fdisk -l /dev/sdb# 或查看文件系统类型 sudo blkid /dev/sdb*第二步:创建挂载点并挂载 # 创建挂载目录 sudo mkdir -p /mnt/sd…

2026/10/20 13:04:22 阅读更多 →
如何用深度学习象棋AI助手VinXiangQi提升你的棋艺水平?三大技术支柱解析

如何用深度学习象棋AI助手VinXiangQi提升你的棋艺水平?三大技术支柱解析

如何用深度学习象棋AI助手VinXiangQi提升你的棋艺水平?三大技术支柱解析 【免费下载链接】VinXiangQi Xiangqi syncing tool based on Yolov5 / 基于Yolov5的中国象棋连线工具 项目地址: https://gitcode.com/gh_mirrors/vi/VinXiangQi 你是否曾在对弈中感到…

2026/10/17 5:54:27 阅读更多 →

最新新闻

VC++ MFC围棋程序实战:双人对战与核心逻辑实现

VC++ MFC围棋程序实战:双人对战与核心逻辑实现

简介:这是一份基于VC/MFC编写的围棋对弈程序源码,面向有一定C基础、希望深入理解Windows图形界面与游戏逻辑的开发者,尤其适合想通过完整小项目巩固MFC框架的学习者。压缩包共24个文件,约36KB,包含7个h头文件与6个cpp源…

2026/10/21 2:43:59 阅读更多 →
VC++ MFC双人对决围棋程序实战:从落子提子到胜负计算

VC++ MFC双人对决围棋程序实战:从落子提子到胜负计算

简介:这是一份基于VC与MFC框架编写的双人对决围棋程序源码,面向正在学习Windows游戏编程、希望理解MFC文档视图架构与图形绘制流程的开发者。程序支持双人轮流落子,并可根据15、17寸液晶屏选择合适窗口尺寸,落子响应迅速&#xff…

2026/10/21 2:43:59 阅读更多 →
arping 命令详解:局域网 MAC 地址探测与 IP 冲突排查实战

arping 命令详解:局域网 MAC 地址探测与 IP 冲突排查实战

简介:这是一份面向网络管理员、运维工程师及网络编程学习者的 arping 工具源码资源包,聚焦以太网环境下基于 ARP 协议探测主机在线状态并获取其 MAC 地址这一典型场景。资源共 19 个文件,以 cpp 与 h 源文件为核心,涵盖 ARP 报文构…

2026/10/21 2:43:59 阅读更多 →
基于协同过滤的电影推荐系统:Django+MySQL毕业设计实战拆解

基于协同过滤的电影推荐系统:Django+MySQL毕业设计实战拆解

简介:这套基于协同过滤的电影推荐系统毕业设计源码,面向计算机相关专业学生与推荐系统初学者,完整实现了基于用户与基于物品两种推荐算法,涵盖相似度计算、评分预测与 Top-N 推荐等完整流程。项目基于 Django 2.2.1、Python 3.7 与…

2026/10/21 2:43:59 阅读更多 →
2011款iMac装Win10:绕过BootCamp蓝屏,手动更新显卡驱动实战

2011款iMac装Win10:绕过BootCamp蓝屏,手动更新显卡驱动实战

简介:这份资源面向仍在使用 iMac 27 2011 Mid 一体机、并在其上安装 Windows 10 的用户,尤其是遇到播放视频时偶发蓝屏、苦于 Boot Camp 自带驱动版本过旧的人群。资源提供的是从 AMD 官网获取的较新显卡驱动,安装后驱动日期显示为 2015/8/19…

2026/10/21 2:43:59 阅读更多 →
纯Java数据包处理实战:抓包解析过滤统计全链路

纯Java数据包处理实战:抓包解析过滤统计全链路

简介:这份资源面向具备一定Java与网络编程基础、希望研究高性能数据包处理的开发者,是一套基于Intel数据平面开发套件(DPDK)的Java快速数据包处理框架。它最初作为毕业设计项目开发,包含DPDKJava主框架及报告后重构的D…

2026/10/21 2:42:59 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事:AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里,按宣传口径,本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”,而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起:用户到底在追求什么“封号重开”这四个字,放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流,背后是大量的时间和精力成本。正常情况下,没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”,而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时,就意识到:单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →