ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Chaos Monkey 随机杀 Pod 没发现任何问题,但真实故障还是发生了:混沌工程不是随机搞破坏

Chaos Monkey 随机杀 Pod 没发现任何问题,但真实故障还是发生了:混沌工程不是随机搞破坏 title: Chaos Monkey 随机杀 Pod 没发现任何问题但真实故障还是发生了混沌工程不是随机搞破坏description: 从 Netflix 的 Chaos Monkey 到生产级故障注入深入讲解混沌工程的设计原则、实验框架和 3 个 Java 故障注入实现。tags: [混沌工程, Chaos Engineering, 故障注入, Java, 微服务, resilience]2023 年 9 月我们在生产环境引入 Chaos Monkey每天随机杀掉 5% 的 Pod。跑了两个月所有服务都 表现良好——自动扩容、自动恢复、无告警。但 11 月的一次真实故障打了脸一个下游支付接口延迟从 50ms 涨到 8 秒我们的服务因为没有设置 read timeout连接池被占满级联雪崩。Chaos Monkey 从来没发现这个问题因为它只杀 Pod不模拟 慢节点。那次教训让我明白混沌工程不是随机搞破坏是有假设、有度量、有针对性的实验。混沌工程的 5 个原则不是 3 个也不是随便玩Netflix 提出的混沌工程有 5 个核心原则建立稳态假设系统正常时的关键指标是什么QPS、P99、错误率、业务成功率引入真实世界的变量网络延迟、CPU 满载、磁盘满、依赖服务故障在生产环境运行staging 和生产的差异往往就是故障的触发条件自动化持续运行人工执行的一次性实验没有统计意义最小化爆炸半径用金丝雀、流量镜像、分区隔离控制影响范围我们早期的问题在于只做了第 2 步随机杀 Pod忽略了第 1 步没有定义稳态和第 4 步没有自动化。实验 1模拟慢节点Latency Injection真实世界里节点很少 直接挂掉更多时候是 变慢。CPU steal time 高、GC 频繁、网络拥塞都会导致节点响应变慢但又不完全不可用。// 用 Byteman 在方法入口注入延迟 // 规则文件slow-method.btm RULE slow down order query CLASS com.example.OrderService METHOD queryOrder AT ENTRY IF true DO Thread.sleep(5000); // 注入 5 秒延迟 ENDRULEByteman 是 JBoss 开发的 Java 字节码注入工具不需要改代码通过 attach 到 JVM 进程动态生效。AT ENTRY表示在方法入口处注入Thread.sleep(5000)让每次调用延迟 5 秒。但这种硬编码的注入太粗暴。我们后来换成了Chaos Mesh JVMChaos# Chaos Mesh 的 JVMChaos 实验定义 apiVersion: chaos-mesh.org/v1alpha1 kind: JVMChaos metadata: name: slow-order-service spec: action: latency class: com.example.OrderService method: queryOrder latency: 5000 # 注入 5s 延迟 mode: all # 对所有匹配 Pod 生效 selector: namespaces: - production labelSelectors: app: order-serviceaction: latency是注入延迟mode: all表示对 selector 匹配的所有 Pod 生效。Chaos Mesh 是 PingCAP 开源的 Kubernetes 混沌工程平台支持网络、Pod、JVM、磁盘等多种故障注入。这个实验暴露了我们服务的一个严重问题HTTP 客户端没有设置 read timeout。RestTemplate默认是无限等待的下游 5 秒延迟直接导致我们线程池堆积。// 修复前默认 RestTemplate无超时 RestTemplate restTemplate new RestTemplate(); // 危险 // 修复后显式设置连接和读取超时 SimpleClientHttpRequestFactory factory new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(1000); // 连接超时 1s factory.setReadTimeout(3000); // 读取超时 3s RestTemplate restTemplate new RestTemplate(factory);setReadTimeout(3000)是关键超过 3 秒没收到响应就抛ResourceAccessException触发熔断降级而不是无限等待把线程池占满。实验 2模拟 CPU 满载有些故障只在 CPU 高负载时触发比如 Go 的 GC、Java 的 Safepoint。// Java CPU 满载注入用无限循环占满一个核心 public class CpuBurner { public static void burnCpu(int cores, int durationSeconds) { ExecutorService executor Executors.newFixedThreadPool(cores); AtomicBoolean running new AtomicBoolean(true); for (int i 0; i cores; i) { executor.submit(() - { while (running.get()) { // 数学计算占满 CPU避免被 JIT 优化掉 Math.random() * Math.random(); } }); } try { Thread.sleep(durationSeconds * 1000L); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } running.set(false); executor.shutdown(); } }Math.random() * Math.random()是刻意设计的 不可优化 计算JIT 编译器不能把这段代码优化掉因为它每次结果不同。AtomicBoolean控制停止信号保证实验结束后 CPU 恢复正常。我们在订单服务上做了这个实验占满 2 个核心容器 limit 4 核发现- QPS 从 3500 降到 1800- P99 从 45ms 涨到 320ms- 但错误率依然是 0%这看起来 还行但监控显示GC 次数从每分钟 2 次涨到 40 次。如果持续高负载GC 压力会导致更严重的停顿。这个实验促使我们把容器的 CPU limit 从 4 核调到 6 核并设置了 HPA水平自动扩缩容的 CPU 阈值从 70% 降到 60%。实验 3模拟网络分区网络分区是分布式系统最可怕的故障之一节点没挂但互相之间通不了信。# 用 tc iptables 模拟网络分区 # 把 Pod-1 和 Pod-2 之间的网络切断 # 在 Pod-1 上执行 iptables -A OUTPUT -d Pod-2-IP -j DROP iptables -A INPUT -s Pod-2-IP -j DROPiptables -A OUTPUT -d IP -j DROP把目标 IP 为 Pod-2 的出站包全部丢弃。这个实验在 Redis Cluster 上跑时发现了脑裂问题两个主节点互相认为对方挂了都把自己提升为主导致数据不一致。# Chaos Mesh 的网络分区实验 apiVersion: chaos-mesh.org/v1alpha1 kind: NetworkChaos metadata: name: redis-partition spec: action: partition mode: all selector: labelSelectors: app: redis-node-1 direction: both target: mode: all selector: labelSelectors: app: redis-node-2 duration: 5maction: partition是网络分区direction: both表示双向隔离Pod-1 不能发给 Pod-2Pod-2 也不能发给 Pod-1。duration: 5m实验持续 5 分钟。这个实验后我们给 Redis Cluster 加了min-replicas-to-write 1和cluster-node-timeout 5000的配置缩短故障检测时间降低脑裂窗口。混沌工程的落地框架不是工具越多越好我们的混沌工程实践分三层基础设施层Chaos Mesh 做 Pod/网络/磁盘级别的故障应用层Byteman 做 JVM 方法级注入业务层自定义脚本模拟业务异常如订单状态不一致、库存超卖// 业务层混沌模拟库存扣减异常 Component public class InventoryChaos { Autowired private InventoryService inventoryService; // 按 0.1% 概率注入库存不一致 public boolean deductWithChaos(Long skuId, int quantity) { if (Math.random() 0.001) { // 模拟扣减成功但数据库未提交 inventoryService.mockCommitFailure(skuId, quantity); return true; // 返回成功实际没扣 } return inventoryService.deduct(skuId, quantity); } }业务层混沌是最难做的因为它需要深入理解业务逻辑。但收益也是最大的——我们发现过一个订单状态机漏洞已支付 状态可以非法转移到 已取消这个漏洞在代码 review 时没发现但在混沌实验里被触发了。稳态度量没有度量实验就是瞎搞每次实验前我们必须定义 3 个稳态指标指标正常值实验阈值超出后动作P99 延迟 100ms 300ms自动停止实验错误率 0.1% 1%自动停止实验订单成功率 99.9% 99%自动停止实验// 实验自动停止的钩子 Component public class ChaosAbortPolicy { Autowired private MetricsClient metrics; Scheduled(fixedRate 5000) public void checkSteadyState() { double errorRate metrics.getErrorRate(order-service); if (errorRate 0.01) { chaosEngine.stopAllExperiments(); alertService.sendP1Alert(Chaos experiment breached error rate threshold); } } }Scheduled(fixedRate 5000)每 5 秒检查一次指标。chaosEngine.stopAllExperiments()立即停止所有正在运行的实验。这个自动保护机制是必须的——人工观察 Grafana 再手动停止可能已经晚了。总结混沌工程不是 搞破坏看谁先挂是在受控条件下验证系统韧性 。Chaos Monkey 随机杀 Pod 只是入门真正的价值在于模拟真实世界的复杂故障慢节点、CPU 满载、网络分区、业务异常。我的建议是1.不要从生产环境开始先在 staging 跑 1 个月修复明显问题后再上生产2.不要随机搞每个实验都要有假设如果下游延迟 5 秒我们的熔断会触发吗3.必须有自动停止机制没有稳态度量和自动停止的实验是自杀4.记录和复盘每个实验的结果都要写成文档作为架构改进的依据那次支付接口雪崩的事故后我们团队形成了一个规矩每次引入新的外部依赖必须先做一个 下游延迟 10 秒 的混沌实验。这个规矩在接下来的一年里提前发现了 3 个潜在的单点故障。思考题你的系统现在能容忍下游服务延迟 5 秒吗超时设置在哪里有没有 fallback如果你现在要在生产环境跑混沌实验你敢先从哪个服务开始它的爆炸半径怎么控制
返回列表