上个月我们线上的Kubernetes集群出了一次严重的故障,起因是一个自定义Operator的bug,导致整个集群的资源被疯狂创建,最后把etcd撑爆了,集群完全不可用,故障持续了两个多小时,影响了很多业务。
这次故障非常惊心动魄,也让我们学到了很多教训。今天这篇文章,就来复盘一下这次K8s Operator故障,从故障发生、排查、定位、恢复,到事后的原因分析、改进措施,一步步详细记录,希望能给正在使用K8s和Operator的朋友一些警示和参考,避免踩同样的坑。
一、故障发生
先说说故障是怎么发生的。
那天是周三,下午两点多,正是业务高峰期,我们的监控系统突然开始报警,先是etcd的磁盘使用率报警,然后是API Server的响应时间报警,接着是各种业务服务的健康检查报警,告警信息像雪片一样飞来,整个监控系统都炸了。
我当时正在写代码,看到这么多报警,心里咯噔一下,知道出大事了。赶紧打开Kubernetes的Dashboard,发现根本打不开,页面一直转圈,加载不出来。然后用kubectl命令行操作,发现kubectl也卡住了,执行任何命令都没有响应,连kubectl get nodes都执行不了。
这时候我意识到,问题很严重,Kubernetes集群可能完全不可用了。赶紧叫上团队的几个同事,一起排查问题。
我们先登录到Kubernetes的Master节点,查看etcd的状态,发现etcd的磁盘已经满了,数据目录占用了100%的磁盘空间。etcd因为磁盘满了,无法写入数据,所以API Server也无法正常工作,整个集群就瘫痪了。
这时候我们还不知道是什么原因导致etcd磁盘满的,因为我们的etcd磁盘有500G,平时使用率只有20%左右,怎么会突然满了呢?
二、排查过程
我们先清理了etcd的一些快照和日志,释放了一点磁盘空间,让etcd能暂时运行起来,这样kubectl就能用了,方便我们排查问题。
kubectl恢复之后,我们执行kubectl get pods --all-namespaces,发现输出了大量的Pod,翻了好几页都翻不完,而且很多Pod的名字都是乱码一样的,看起来不正常。
然后我们执行kubectl get crd,发现有一个自定义资源定义(CRD),是我们自己开发的一个Operator管理的资源,这个资源的数量非常多,有几万个,而正常情况下,这个资源应该只有几十个。
我们又执行kubectl get <那个自定义资源> --all-namespaces,发现真的有几万个,而且还在不断增加,每秒都有新的资源被创建出来。
这时候我们意识到,问题出在那个自定义Operator上,它可能出了bug,在疯狂地创建自定义资源,导致etcd的数据量暴增,最后把磁盘撑满了。
我们赶紧先把那个Operator的Deployment缩容到0,停掉Operator,这样它就不会再创建新的资源了。然后我们开始清理那些多余的自定义资源,但是因为数量太多,有几万个,用kubectl delete删除非常慢,而且删除的时候API Server的负载很高,影响其他业务。
我们想了个办法,直接操作etcd,用etcdctl命令批量删除那些多余的资源的key,这样速度快很多,而且不经过API Server,不会影响其他业务。
清理了大概半个多小时,终于把那些多余的自定义资源都清理完了,etcd的数据量降下来了,磁盘使用率也恢复到了正常水平。然后我们重启了API Server和etcd,集群慢慢恢复了正常。
但是这时候,很多业务服务因为集群不可用,已经出现了问题,有的服务挂了,有的服务数据不一致,我们又花了一个多小时,逐个恢复业务服务,检查数据一致性,直到所有业务都恢复正常。
从故障发生到完全恢复,一共持续了两个多小时,影响了很多线上业务,造成了不小的损失。
三、原因分析
故障恢复之后,我们开始做详细的原因分析,想搞清楚到底是什么原因导致Operator疯狂创建资源。
我们查看了Operator的日志,发现Operator在处理一个自定义资源的时候,进入了一个死循环,不断地创建新的资源。具体的原因是这样的:
我们的Operator的逻辑是,监听自定义资源的变化,然后根据自定义资源的规格,创建对应的Deployment、Service等资源。如果创建成功,就更新自定义资源的状态,标记为已就绪。
但是有一个bug,就是在更新自定义资源状态的时候,如果更新失败(比如因为网络问题或者etcd暂时不可用),Operator没有正确处理这个错误,而是重新触发了一次调和(reconcile),在调和的过程中,又去创建资源,但是因为资源已经存在了,创建会失败,然后又更新状态,又失败,又触发调和,这样就形成了一个死循环。
更严重的是,在这个死循环中,Operator每次调和都会创建一个新的临时资源,用来做一些中间操作,但是因为死循环,这些临时资源创建了之后没有被清理,越积越多,最后就有了几万个,把etcd的磁盘撑满了。
这个bug其实很隐蔽,在正常情况下不会触发,因为更新状态一般不会失败。但是那天正好有一段时间,etcd的网络出现了一点抖动,导致更新状态失败,然后就触发了这个bug,进入了死循环,最后导致了整个集群的故障。
除了这个直接原因,我们还分析了一些深层原因:
- Operator的代码质量不够高:错误处理不完善,没有考虑到各种异常情况,比如更新状态失败的情况,导致死循环。
- 没有做资源数量的限制:对于自定义资源,没有做数量限制,也没有做配额,导致可以无限创建,最后把etcd撑爆。
- 监控不够完善:对于自定义资源的数量,没有做监控和告警,如果当时自定义资源数量异常增加的时候就告警,我们就能更早发现问题,不会等到etcd磁盘满了才发现。
- etcd的磁盘空间不够大:虽然500G看起来很大,但是对于异常情况来说,还是不够,而且没有做磁盘使用率的提前告警,等到满了才发现,已经晚了。
- 没有做混沌测试:对于Operator的各种异常情况,没有做充分的测试,比如网络抖动、etcd不可用等情况,导致这个bug没有在测试环境发现,带到了生产环境。
四、改进措施
分析完原因之后,我们制定了一系列的改进措施,避免类似的故障再次发生。
1. 修复Operator的bug
首先当然是修复那个导致死循环的bug,完善错误处理,在更新状态失败的时候,不要重新触发调和,而是加入重试队列,等一段时间之后再重试,而且要限制重试次数,避免无限重试。
同时,我们还审查了Operator的所有代码,检查其他地方有没有类似的错误处理问题,一并修复了。
2. 增加资源数量限制和配额
对于自定义资源,我们增加了数量限制,每个命名空间最多只能创建一定数量的自定义资源,超过了就拒绝创建。同时,我们还在Operator内部增加了保护机制,如果发现自定义资源的数量异常增加,就自动停止调和,发出告警,避免疯狂创建资源。
我们还给etcd增加了配额,限制每个资源类型的最大数量,避免某个资源类型把整个etcd撑爆。
3. 完善监控和告警
我们增加了很多监控指标,比如自定义资源的数量、Operator的调和次数、失败次数、etcd的磁盘使用率、API Server的响应时间等,并且设置了合理的告警阈值,在异常情况刚出现的时候就告警,让我们能尽早发现问题,尽早处理。
特别是etcd的磁盘使用率,我们设置了多级告警,使用率达到70%的时候就告警,80%的时候严重告警,90%的时候紧急告警,不会等到满了才发现。
4. 扩容etcd的磁盘
我们把etcd的磁盘从500G扩容到了1T,并且做了磁盘的动态扩容,即使磁盘使用率高了,也能快速扩容,避免磁盘满的情况。
同时,我们还优化了etcd的配置,增加了自动压缩和碎片整理,定期清理etcd的旧数据,减少磁盘占用。
5. 增加混沌测试和故障演练
我们引入了混沌测试,定期在测试环境模拟各种异常情况,比如网络抖动、etcd不可用、API Server超时、节点宕机等,测试Operator和整个集群在异常情况下的表现,发现问题及时修复。
我们还定期做故障演练,模拟各种故障场景,锻炼团队的应急响应能力,让大家在真正遇到故障的时候,能够快速、正确地处理,减少故障持续时间。
6. 完善Operator的开发规范
我们制定了Operator的开发规范,要求所有Operator的开发都必须遵循规范,包括错误处理、重试机制、资源清理、日志记录、监控指标等,从流程上保证Operator的质量。
同时,我们还加强了代码审查,所有Operator的代码都必须经过至少两个人的审查,才能合并到主分支,避免bug带到生产环境。
五、经验和教训
这次故障给了我们很大的教训,也让我们学到了很多东西,下面分享一些经验和教训。
1. Operator虽然强大,但是风险也很大
Operator是Kubernetes的一个强大的扩展机制,可以用来管理复杂的有状态应用,自动化很多运维操作。但是Operator的风险也很大,因为它有集群的操作权限,如果出了bug,可能会影响整个集群,就像我们这次一样,一个Operator的bug,导致整个集群不可用。
所以,在开发和使用Operator的时候,一定要非常谨慎,做好测试,做好错误处理,做好监控和告警,不能掉以轻心。
2. 错误处理非常重要
这次故障的直接原因,就是一个简单的错误处理问题,更新状态失败之后没有正确处理,导致死循环。很多时候,我们写代码的时候,只关注正常的逻辑,忽略了错误处理,觉得错误不会发生,但是在生产环境中,各种异常情况都可能发生,错误处理不完善,就可能导致严重的故障。
所以,写代码的时候,一定要重视错误处理,考虑各种异常情况,做好重试、降级、熔断等机制,不能想当然地觉得错误不会发生。
3. 监控和告警是生命线
这次故障,如果我们的监控更完善一点,在自定义资源数量异常增加的时候就告警,我们就能更早发现问题,可能几分钟就能解决,不会等到etcd磁盘满了,整个集群不可用了才发现,最后花了两个多小时才恢复。
监控和告警是系统的生命线,一定要做好,覆盖所有关键指标,设置合理的告警阈值,并且要确保告警能及时通知到相关人员,不能漏告,也不能告了没人管。
4. 容量规划要留足余量
我们的etcd磁盘有500G,平时使用率只有20%,我们觉得足够了,但是异常情况下,几万个资源就把它撑满了。所以,做容量规划的时候,一定要留足余量,考虑到异常情况,不能只按正常情况来规划。
而且,对于关键组件,比如etcd、数据库等,一定要做磁盘使用率的提前告警,在使用率达到70%、80%的时候就告警,及时处理,不要等到满了才发现。
5. 混沌测试很有必要
很多bug,在正常情况下不会触发,只有在异常情况下才会出现,比如网络抖动、依赖不可用等。如果只做正常的功能测试,这些bug很难发现,会被带到生产环境,最后导致故障。
混沌测试就是用来解决这个问题的,通过主动注入故障,模拟各种异常情况,测试系统在异常情况下的表现,发现问题及时修复。虽然混沌测试需要投入一定的成本,但是对于重要的系统来说,是非常有必要的,能帮你发现很多隐藏的bug,避免生产环境的故障。
六、写在最后
这次K8s Operator故障,是我们团队经历过的最严重的故障之一,非常惊心动魄,也给我们带来了很大的损失。但是,故障本身并不可怕,可怕的是不从故障中学习,不做改进,导致同样的故障再次发生。
通过这次故障的复盘,我们找到了系统中的很多问题,并且做了一系列的改进,现在我们的集群比以前更稳定了,Operator的质量也比以前更高了,团队的应急响应能力也得到了锻炼。
希望这篇复盘文章,能给正在使用K8s和Operator的朋友一些警示和参考,避免踩同样的坑。也欢迎大家在评论区分享自己遇到过的K8s故障和经验,一起交流学习。
最后,愿大家的线上系统都能稳定运行,永不宕机。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录