最近半年我们团队在搞混沌工程从最开始觉得这东西很酷很极客到中间踩了无数坑差点把线上搞挂再到现在,慢慢找到感觉走上正轨经历了很多也学到了很多。

今天想分享一下我们做混沌工程的经历和踩坑过程从入门到差点放弃再到重新上路希望能给想做混沌工程的朋友一些参考和启发避免走我们走过的弯路也让大家对混沌工程有一个更真实的认识它不只是很酷的概念也不是随便搞故障,那么简单要做好其实很不容易。

一、什么是混沌工程

在说我们的经历之前,先简单介绍一下什么是混沌工程方便不太熟悉的朋友理解。

混沌工程(Chaos Engineering)简单说就是通过主动在系统中注入故障来测试系统的弹性和韧性看看系统在面对各种故障的时候,能不能正常工作会不会出问题它的核心思想是"故障不可避免与其等故障在最糟糕的时候,发生不如主动在可控的情况下制造故障发现系统的弱点提前修复"这样就能在真正的大故障发生之前,就把隐患找出来修掉提升系统的稳定性和可靠性。

混沌工程这个概念最早是Netflix提出来的他们在2011年左右开发了一个叫Chaos Monkey(混沌猴子)的工具会在工作时间随机杀掉生产环境的一些服务实例来测试系统的容错能力,因为他们的系统是大规模微服务架构部署在AWS上实例随时可能挂掉,所以他们需要确保系统能容忍单个实例的故障不会,因为一个实例挂了就整个服务不可用后来Netflix把混沌工程的实践总结成了一套方法论和工具开源了出来慢慢就传播开来成为了分布式系统稳定性保障的一个重要实践。

混沌工程的基本原则大概有这么几条:

  1. 建立稳定状态的假设:先定义系统正常工作的状态是什么样的,比如响应时间错误率吞吐量等指标在正常范围内作为基线。
  2. 多样化真实世界的事件:注入各种真实世界中可能发生的故障,比如实例挂掉网络延迟网络分区磁盘满CPU飙升依赖服务不可用等等。
  3. 在生产环境运行实验:混沌工程最好在生产环境做,因为,只有生产环境的流量和复杂度才能真实反映系统的状态测试环境做的效果有限,当然生产环境做要非常小心控制爆炸半径。
  4. 自动化持续运行:混沌工程实验应该自动化持续运行而不是,偶尔手动做一次这样才能持续发现问题防止系统退化。
  5. 最小化爆炸半径:做混沌工程实验的时候,要控制影响范围从小范围开始逐步扩大避免把整个系统搞挂影响用户。

这些原则说起来简单,但是真正做起来会遇到很多问题和挑战下面就说说我们团队做混沌工程的经历和踩坑。

二、入门:觉得很酷说干就干

我们团队负责公司的一个核心业务系统是微服务架构大概有二三十个服务部署在Kubernetes集群上之前,我们的系统出过几次故障都是,因为某个服务的实例挂了,或者某个依赖出问题导致整个链路不可用影响了用户每次出故障都很被动救火很辛苦后来我在技术大会上听了一个关于混沌工程的分享觉得这个思路很好主动注入故障发现问题提前修复而不是等故障发生了才救火回来后就跟团队分享了这个想法大家都觉得挺酷挺有意思的说干就干我们就开始了混沌工程的实践。

最开始我们的想法很简单也很天真觉得混沌工程不就是随机关掉几个服务实例嘛看看系统会不会出问题这有什么难的我们先在测试环境试了一下用kubectl命令随机关掉几个Pod看看系统的反应测试环境流量小也没什么真实用户,所以随便搞也不怕搞了几次发现大部分服务都能正常工作,因为Kubernetes会自动重启挂掉的Pod很快就恢复了,只有少数几个服务,因为没有配置好健康检查,或者启动慢会有短暂的不可用我们就把这些问题修了觉得混沌工程也,不过如此挺简单的嘛没什么难的。

然后我们就有点飘了觉得测试环境已经验证过了没问题就想在生产环境也试一下现在回想起来那时候真的是无知者无畏太鲁莽了生产环境和测试环境完全不是一回事流量规模复杂度都差很多测试环境没问题不代表生产环境没问题,但是那时候我们没想,那么多就直接在生产环境搞了第一次混沌工程实验结果就出事了。

三、第一次踩坑:差点把生产搞挂

我们第一次在生产环境做混沌工程实验选了一个看起来不,那么核心的服务是一个用户画像相关的服务觉得这个服务,即使出问题也不会太影响主流程,然后我们就用kubectl命令随机关掉了这个服务的一半Pod本来以为Kubernetes会自动重启很快恢复结果没想到出问题了。

这个服务启动比较慢,因为启动的时候,要从数据库加载大量的用户画像数据到内存缓存大概要两三分钟才能启动完成而我们关掉了一半Pod剩下的一半Pod本来负载就不低突然少了一半实例剩下的实例负载瞬间飙升CPU直接打满响应时间急剧上升,然后就开始超时而新的Pod启动又慢两三分钟才能起来这两三分钟里这个服务基本处于不可用状态而这个服务,虽然不是主流程的核心服务,但是主流程的几个服务会调用它获取用户画像做个性化推荐这些调用方没有配置好降级和超时设置调用超时后就一直等导致调用方的线程被占满,然后调用方也开始不可用,然后故障就像多米诺骨牌一样蔓延开来最后影响到了主流程的几个核心服务整个系统响应都变慢错误率飙升影响了用户。

当时我们都懵了没想到关掉一个看起来不核心的服务的一半Pod会导致这么大的影响赶紧把关掉的Pod恢复,然后等新的Pod启动起来大概过了五六分钟系统才慢慢恢复正常,虽然故障持续时间不算太长,但是影响了不少用户也触发了公司的故障告警和复盘流程我们团队被批评了一顿也做了故障复盘那时候真的有点灰心觉得混沌工程这东西太危险了差点把生产搞挂还被批评是不是不该搞这个差点就放弃了。

后来冷静下来复盘这次故障发现问题不是混沌工程本身的问题而是我们的做法太鲁莽了很多准备工作都没做好,比如:

  1. 没有控制爆炸半径:第一次在生产环境做就关掉了一半Pod太激进了应该从关掉一个Pod开始小步快跑。
  2. 没有充分了解系统的依赖关系:我们以为那个服务不核心,但是实际上,它被很多核心服务依赖,而且依赖方没有做好降级和超时故障会蔓延。
  3. 没有完善的监控和告警:做实验的时候,没有实时监控系统的关键指标等发现问题的时候,已经影响扩大了。
  4. 没有回滚和应急方案:做实验前没有准备好快速回滚和,应急的方案出问题了才手忙脚乱地恢复。
  5. 没有和相关团队沟通:做实验前没有和依赖这个服务的其他团队沟通他们都不知道我们在,做实验出问题了也不知道怎么回事。

这次故障给我们上了深刻的一课混沌工程不是随便搞故障它是一门严谨的工程实践需要充分的准备和谨慎的操作,否则,不仅发现不了问题还会制造故障造成损失那次之后,我们差点就放弃混沌工程了,但是后来想想问题不是混沌工程本身而是我们的做法不对,如果,因为一次失败就放弃那之前,的努力就白费了,而且混沌工程的理念确实是对的能帮我们提升系统稳定性,所以我们决定不放弃重新来这次要更严谨更规范地做。

四、重新上路:建立规范和流程

那次故障之后,我们没有急着再做实验而是先停下来认真学习混沌工程的方法论和最佳实践看了Netflix的混沌工程原则和相关的书籍文章也参考了其他公司的实践经验,然后结合我们自己的系统特点建立了一套混沌工程的规范和流程确保以后做实验的时候,安全可控不会再出类似的问题。

我们建立的规范和流程大概包括以下几个方面:

1. 实验前的准备

  • 明确实验目标和假设:每次实验前都要明确这次实验的目标是什么要验证什么假设,比如"关掉服务A的一个实例系统应该能正常工作错误率不超过0.1%"不能稀里糊涂地做实验。
  • 梳理系统依赖关系:实验前要梳理清楚目标服务的上下游依赖哪些服务依赖它它依赖哪些服务评估故障可能的影响范围和蔓延路径。
  • 确定稳定状态基线:实验前要确定系统正常状态的关键指标基线,比如QPS响应时间P99错误率CPU内存使用率等等作为实验中判断系统是否正常的依据。
  • 配置监控和告警:实验前要确保相关的监控和告警都配置好能实时看到系统的关键指标,并且设置实验的中止条件,比如错误率超过1%或者P99超过500ms就立即中止实验恢复。
  • 准备回滚和应急方案:实验前要准备好快速回滚和应急的方案,比如怎么快速恢复关掉的实例怎么快速扩容相关服务出问题了谁负责处理等等确保出问题能快速响应。
  • 和相关团队沟通:实验前要和所有可能受影响的团队沟通告诉他们我们什么时候要做什么实验可能有,什么影响让他们有心理准备也能协助观察系统状态。
  • 选择合适的实验时间:实验一般选在流量比较低的时段,比如工作日的上午,或者下午避开业务高峰和大促等特殊时期降低风险。

2. 实验中的执行

  • 从小范围开始:每次实验都从最小的爆炸半径开始,比如第一次只关掉一个实例观察没问题再逐步扩大到两个三个不能一上来就搞大的。
  • 实时监控:实验过程中要有人专门盯着监控大盘实时观察系统的关键指标一旦发现异常,或者达到中止条件立即中止实验恢复。
  • 控制实验时长:每次实验的持续时间不要太长一般几分钟到十几分钟够了观察到系统的反应就可以恢复了不要长时间注入故障增加风险。
  • 做好记录:实验过程中要记录好实验的时间操作系统的反应指标变化等等方便后续复盘和总结。

3. 实验后的复盘

  • 恢复系统:实验结束后要确保系统完全恢复到正常状态所有,注入的故障都清理掉实例都恢复正常指标都回到基线。
  • 分析实验结果:实验后要分析实验结果系统的表现是否符合我们的假设有没有发现什么问题,比如某个服务容错能力不够某个依赖没有降级某个监控没覆盖到等等。
  • 修复发现的问题:如果实验发现了问题要建工单跟进修复,并且跟踪修复进度确保问题真正被解决而不是发现了就,不管了。
  • 总结经验教训:每次实验后都要总结经验教训哪些地方做得好哪些地方需要改进不断优化我们的混沌工程流程和实践。
  • 分享成果:把实验的结果和发现的问题分享给团队和相关团队让大家都了解系统的弱点和,改进方向也让大家认识到混沌工程的价值。

建立了这套规范和流程之后,我们再做混沌工程实验就谨慎多了也安全多了没有再出过类似那次的大故障,而且通过混沌工程实验我们确实发现了很多系统的隐患和问题提前修复了提升了系统的稳定性下面说说我们通过混沌工程发现的一些典型问题。

五、通过混沌工程发现的典型问题

按照新的规范和流程我们做了一系列混沌工程实验从简单的实例故障到网络延迟网络分区再到依赖服务不可用等等发现了很多之前,没注意到的问题这里说几个典型的。

问题1:服务没有配置正确的健康检查实例挂了不能自动恢复

我们做的第一批实验是随机关掉各个服务的一个实例看看Kubernetes能不能自动恢复结果发现有几个服务的健康检查配置得不对有的没有配置就绪探针(Readiness Probe)导致实例还没启动完成就被加入了负载均衡接收流量,然后就报错有的存活探针(Liveness Probe)配置的超时时间太短实例稍微慢一点响应就被认为挂了被杀掉重启导致频繁重启这些问题之前,都没发现,因为平时实例不会随便挂,所以健康检查的问题暴露不出来通过混沌工程主动关掉实例才发现了这些问题,然后我们把所有服务的健康检查都重新梳理和配置了一遍确保实例挂了能被正确识别和自动恢复启动完成后才接收流量。

问题2:服务启动慢实例故障后恢复时间长

前面那次故障就是,因为服务启动慢导致的后来我们做实验的时候,发现不少服务都有启动慢的问题有的是,因为启动要加载大量数据到缓存有的是,因为启动要建立很多连接初始化很多资源有的是,因为代码里有一些慢的初始化逻辑启动慢就意味着实例挂了之后,恢复时间长在恢复期间剩下的实例负载会升高容易出问题后来我们对启动慢的服务做了优化,比如把全量加载改成懒加载,或者异步加载优化初始化逻辑减少不必要的初始化工作预热缓存等等把大部分服务的启动时间从几分钟降到了几十秒以内大大提升了故障恢复速度。

问题3:依赖服务调用没有配置超时和重试故障会蔓延

我们做依赖服务不可用的实验的时候,发现很多服务调用依赖服务的时候,没有配置合理的超时时间有的甚至没有超时会一直等下去也没有配置重试和降级逻辑这样一旦依赖服务不可用,或者响应慢调用方的线程就会被占满,然后调用方也开始不可用故障就蔓延开来这就是典型的故障蔓延和级联失败后来我们要求所有服务调用依赖服务都必须配置合理的超时时间和重试策略,并且要有降级逻辑依赖不可用的时候,能快速失败,或者返回默认值,或者走降级逻辑不能一直等把自己拖垮我们还引入了服务熔断和限流的组件进一步防止故障蔓延提升系统的容错能力。

问题4:数据库连接池配置不合理实例少了后连接不够

我们做关掉部分实例的实验的时候,发现有些服务在实例减少后会出现数据库连接不够的问题报错"cannot get connection from pool"排查后发现是这些服务的数据库连接池配置得不合理每个实例的连接数固定,但是总连接数是按实例数算的实例少了总连接数就少了而流量没有少剩下的实例需要处理更多请求需要更多数据库连接,但是连接池配置的最大连接数不够就出现连接不够的问题后来我们重新评估和配置了所有服务的数据库连接池考虑了实例故障减少的情况确保在部分实例故障的情况下剩下的实例也有足够的数据库连接能正常工作,同时也要控制总连接数不要超过数据库的最大连接数限制。

问题5:缓存没有做好降级缓存挂了数据库直接被打垮

我们做缓存服务(Redis)不可用的实验的时候,发现一个严重问题有几个服务严重依赖缓存缓存不可用的时候,没有降级逻辑所有请求都直接打到数据库数据库瞬间QPS飙升几十倍直接被打垮,然后整个服务都不可用这个问题之前,也没暴露出来,因为缓存服务一直很稳定没出过问题,所以大家都默认缓存是可靠的没考虑缓存不可用的情况通过混沌工程主动把缓存搞挂才发现了这个严重隐患后来我们对严重依赖缓存的服务都加了缓存降级逻辑缓存不可用的时候,要么走本地缓存要么限制访问数据库的频率要么直接返回降级结果不能让所有请求都直接打到数据库把数据库打垮,同时我们也做了缓存的高可用部署主从切换哨兵等等减少缓存本身出问题的概率。

问题6:监控有盲区出了问题不能及时发现

做混沌工程实验的过程中我们还发现我们的监控有不少盲区有些关键指标没有监控到,比如某个服务的队列堆积情况某个依赖的调用错误率数据库的慢查询数量等等做实验的时候,注入了故障,但是监控大盘上看不到相关指标的变化不能及时发现问题后来我们根据混沌工程实验中发现的监控盲区补充了很多监控指标和告警规则完善了我们的监控体系确保系统的各个关键部分都有监控覆盖出问题能及时发现和告警。

这些只是我们通过混沌工程发现的一部分问题,还有很多其他的小问题,比如配置错误日志不完善文档缺失等等都通过混沌工程实验暴露出来了,然后我们逐一修复了这些问题系统的稳定性和容错能力有了明显的提升后来我们系统再出故障的次数和影响范围都比以前小很多这就是混沌工程的价值它帮我们在故障真正发生之前,就发现了隐患提前修复避免了更大的损失。

六、做混沌工程的一些心得和建议

经过半年多的实践从最开始的鲁莽踩坑到后来建立规范稳步推进我们对混沌工程有了更深的认识和,体会这里分享一些心得和建议给想做混沌工程的朋友。

1. 混沌工程不是搞破坏是严谨的工程实践

很多人对混沌工程有误解觉得它就是随机关掉服务搞破坏很酷很极客其实不是混沌工程是一门严谨的工程实践它有自己的原则方法和流程需要认真对待谨慎操作不能随随便便搞,否则,不仅发现不了问题还会制造故障造成损失我们第一次踩坑就是,因为把混沌工程想得太简单太鲁莽了,所以做混沌工程之前,一定要充分学习和理解它的方法论和最佳实践建立完善的流程和规范再开始做不要一上来就在生产环境瞎搞。

2. 从小处着手控制爆炸半径

做混沌工程实验一定要从小处着手控制爆炸半径从最简单的实验开始从最小的影响范围开始,比如先在测试环境做没问题了再到预发环境再到生产环境生产环境也先从非核心服务的一个实例开始观察没问题再逐步扩大范围和实验复杂度不要一上来就搞核心服务搞大范围的故障那样风险太大很容易出问题小步快跑逐步推进才是安全的做法。

3. 完善的监控和应急方案是前提

做混沌工程实验之前,一定要确保有完善的监控和应急方案能实时看到系统的状态出问题能及时发现和恢复,如果监控都没做好连系统正常不正常都不知道那做混沌工程就是瞎搞很容易出问题,而且一定要有明确的实验中止条件和快速回滚方案一旦发现异常立即中止恢复把影响降到最低我们后来做实验都要求至少有两个人一起一个人操作一个人盯监控确保安全。

4. 混沌工程的价值在于发现问题不是证明系统没问题

很多人做混沌工程的目的是证明自己的系统很稳定没问题其实这是不对的混沌工程的真正价值在于发现系统的问题和隐患而不是证明系统没问题,如果做了很多实验都没发现问题那可能不是系统真的没问题而是你的实验不够深入不够全面没有覆盖到真正的弱点,所以做混沌工程要有期待发现问题的心态发现问题是好事说明混沌工程起作用了帮你在故障发生前找到了隐患不要怕发现问题也不要,因为发现问题就觉得系统很差,恰恰,相反能发现问题,并且修复才能让系统越来越稳定。

5. 混沌工程需要持续做不是一次性的

混沌工程不是一次性的活动做一次就完事了它需要持续做,因为系统是不断变化的代码在更新服务在增减架构在演进今天没问题不代表明天没问题新的代码可能引入新的问题架构的变化可能带来新的隐患,所以混沌工程需要持续做最好自动化定期运行持续发现问题持续改进这样才能保持系统的稳定性我们现在已经把一些基础的混沌工程实验自动化了定期在测试环境和生产环境运行持续验证系统的容错能力。

6. 争取团队和公司的支持很重要

混沌工程涉及到在生产环境注入故障有一定的风险,所以争取团队和公司的理解和支持非常重要,如果团队和公司不理解不支持觉得你是在搞破坏那混沌工程很难推进下去我们第一次出故障后就面临了很大的压力后来我们通过分享混沌工程的理念和价值展示我们通过混沌工程发现和修复的问题以及系统稳定性的提升慢慢争取到了团队和公司的理解和支持现在公司也很鼓励做混沌工程提升系统稳定性,所以做混沌工程要多沟通多分享让大家理解它的价值争取支持这样才能顺利推进。

七、写在最后

以上就是我们团队做混沌工程的经历和心得从最开始觉得很酷说干就干到第一次踩坑差点把生产搞挂差点放弃再到重新上路建立规范稳步推进通过混沌工程发现和修复了很多系统隐患提升了系统稳定性这半年多的经历,虽然有挫折有困难,但是收获也很大,不仅系统更稳定了我们团队对分布式系统的理解也更深了工程能力也提升了。

混沌工程是一个很有价值的工程实践特别是对于大规模分布式系统微服务架构来说它能帮我们主动发现系统的弱点和隐患提前修复避免在最糟糕的时候,出大故障,但是它也不是银弹不是随便搞就能见效的需要严谨的态度完善的流程和持续的投入才能真正发挥它的价值,如果你想做混沌工程希望我们的经历和心得能给你一些参考和启发避免走我们走过的弯路更顺利地推进混沌工程实践。

最后用一句话结束这篇文章:"混沌工程不是要搞垮系统而是要让系统在真正的故障面前更坚强从入门到放弃再到重新上路你会发现它值得你坚持。"

愿大家的系统都能稳定运行少出故障用户体验越来越好。