最近我们线上的Kubernetes集群,因为Helm包管理的一个问题,出了一次比较严重的故障,整个排查和恢复的过程,惊心动魄,也踩了不少坑,今天就来做一个故障复盘,聊聊这次故障的经过、原因、排查过程、恢复方法,以及我们从中总结的经验教训。
一、故障背景
先简单说说故障的背景。
我们的线上服务,是部署在Kubernetes集群上的,用的是Helm来管理应用的部署和升级。Helm是Kubernetes的包管理工具,类似于Linux的apt、yum,用来管理Kubernetes应用的安装、升级、回滚等,是云原生生态里很重要的一个工具。我们用的是Helm 2,Tiller部署在集群里,通过helm命令来管理应用。
我们的应用,有十几个微服务,每个微服务都是一个Helm Release,用不同的Chart来管理。每次发布,都是用helm upgrade命令,升级对应的Release,更新镜像版本,然后滚动更新Pod。这个流程,我们已经用了一年多了,一直很稳定,没有出过什么大问题。
这次故障,发生在一个周三的下午,我们正在做一个常规的版本发布,升级其中一个微服务的版本。发布的过程,和平时一样,先在测试环境验证,然后灰度发布到线上的一小部分实例,观察没问题,再全量发布。一切看起来都很正常,但是,就在全量发布之后不久,监控告警就响了,这个微服务的错误率飙升,响应时间变长,紧接着,其他的微服务也开始出现问题,整个集群的服务,都受到了影响。
当时我们正在开会,看到告警之后,马上意识到出问题了,立刻开始排查,整个排查和恢复的过程,持续了大概两个小时,才最终恢复,中间经历了很多波折,也踩了不少坑,下面就来详细说说。
二、故障经过
再详细说说故障的经过。
下午两点半,我们开始发布,先升级测试环境,验证没问题,然后灰度发布到线上的一个实例,观察了十分钟,指标正常,没有问题。然后,我们开始全量发布,用helm upgrade命令,升级这个微服务的Release,更新镜像版本。
helm upgrade命令执行之后,显示成功,Release更新成功,Pod开始滚动更新。我们观察了一下,新的Pod正常启动,健康检查通过,旧的Pod正常终止,一切看起来都很正常。
但是,大概过了五分钟,监控告警就响了,这个微服务的5xx错误率飙升,从0.1%升到了30%多,响应时间也从100ms升到了2s多。我们马上意识到,新的版本可能有问题,立刻决定回滚。
我们用helm rollback命令,回滚这个Release到上一个版本。helm rollback命令执行之后,显示成功,Release回滚成功,Pod开始滚动更新,回滚到旧的版本。我们以为回滚之后,服务就会恢复正常,但是,等了几分钟,错误率还是很高,没有下降的趋势,甚至还在继续升高。
这时候,我们开始觉得不对劲了,回滚之后,应该恢复到旧的版本,旧的版本是稳定的,不应该还有问题。我们马上检查Pod的状态,发现新的Pod(回滚后的旧版本),虽然启动了,健康检查也通过了,但是,很多请求还是打到了已经终止的旧Pod上,或者打到了不存在的Endpoint上,导致了大量的5xx错误。
我们进一步检查,发现Service的Endpoint列表,有问题,很多已经终止的Pod的IP,还在Endpoint列表里,而新启动的Pod的IP,却没有加进去。这就导致了,请求被转发到了已经不存在的Pod上,连接失败,返回5xx错误。
这时候,我们意识到,问题可能不只是这个微服务的版本问题,而是Kubernetes的Service和Endpoint出了问题。我们马上检查其他的微服务,发现其他的微服务,也出现了类似的问题,Endpoint列表不正常,很多Pod的IP不对,错误率也开始升高。整个集群的服务,都受到了影响。
这时候,故障升级了,从一个微服务的问题,变成了整个集群的问题。我们马上成立了临时的故障处理小组,分工合作,一部分人继续排查原因,一部分人想办法恢复服务,一部分人和业务方沟通,告知故障情况。
整个排查和恢复的过程,持续了大概两个小时,中间经历了很多波折,我们尝试了很多方法,走了很多弯路,最终才找到问题的根源,恢复了服务。下面就来说说我们的排查过程。
三、排查过程
再说说排查过程,这个过程,惊心动魄,也踩了不少坑。
第一步:怀疑是新版本的代码问题,回滚
最开始,我们以为是新版本的代码有问题,导致了错误率升高,所以第一时间回滚了版本。但是,回滚之后,问题没有解决,错误率还是很高,甚至还在升高。这时候,我们意识到,问题可能不只是代码的问题。
第二步:检查Pod和Service,发现Endpoint异常
回滚无效之后,我们开始检查Pod和Service的状态。我们用kubectl get pods命令,查看Pod的状态,发现Pod的状态都是Running,健康检查也通过了,看起来正常。但是,用kubectl get endpoints命令,查看Service的Endpoint列表,发现Endpoint列表有问题,很多已经终止的Pod的IP,还在列表里,而新启动的Pod的IP,却没有加进去。
这就解释了为什么错误率这么高,因为请求被转发到了已经不存在的Pod上,连接失败,返回5xx错误。
第三步:检查kube-proxy和kubelet,怀疑是节点问题
发现Endpoint异常之后,我们开始怀疑是kube-proxy或者kubelet的问题,因为Endpoint是由kube-controller-manager管理的,然后同步到各个节点的kube-proxy,kube-proxy再更新iptables或者IPVS的规则,转发请求。
我们检查了kube-proxy的日志,发现有一些错误,但是不是很明显,也没有大规模的错误。我们又检查了kubelet的日志,也没有发现明显的问题。我们重启了几个节点的kube-proxy和kubelet,但是问题没有解决,Endpoint还是不正常。
这时候,我们开始怀疑,是不是整个集群的控制平面出了问题,比如kube-apiserver、kube-controller-manager、etcd等。
第四步:检查控制平面,发现etcd有异常
我们开始检查控制平面的组件。先检查kube-apiserver,日志里有一些错误,但是都是一些常规的错误,不是很严重。然后检查kube-controller-manager,这时候,我们发现了问题,kube-controller-manager的日志里,有大量的错误,都是关于Endpoint更新的,提示更新Endpoint失败,因为资源版本冲突,或者etcd的连接超时。
我们马上检查etcd的状态,发现etcd的集群,有一个节点,状态不正常,leader选举有问题,导致etcd的写入很慢,甚至超时。这就解释了为什么kube-controller-manager更新Endpoint失败,因为etcd写入超时了。
但是,etcd为什么会出问题呢?我们检查etcd的日志,发现有大量的写入操作,都是关于ConfigMap的,写入的频率很高,数据量也很大,导致etcd的负载很高,响应变慢,甚至超时。
这些ConfigMap,是哪里来的呢?我们进一步检查,发现这些ConfigMap,都是Helm创建的,每个Helm Release,都会创建一个ConfigMap,用来存储Release的信息。而这次故障,就是因为我们在发布的时候,不小心创建了大量的ConfigMap,导致etcd的负载过高,影响了整个集群的控制平面。
第五步:定位到Helm的问题
我们进一步排查,发现这次发布的时候,我们用的helm upgrade命令,加了一个--force参数,这个参数,会强制重新创建所有的资源,包括Pod、Service、ConfigMap等。而且,因为我们的Chart里,有一个循环,不小心写错了,导致每次升级,都会创建大量的临时ConfigMap,而这些ConfigMap,没有被正确清理,越积越多。
这次发布,因为加了--force参数,加上Chart里的循环错误,一下子创建了几千个ConfigMap,每个ConfigMap都有几KB的数据,总共几十MB的数据,全部写入etcd,导致etcd的负载瞬间飙升,响应变慢,甚至超时。
etcd变慢之后,kube-controller-manager更新Endpoint、Service等资源的时候,就会超时,失败,导致Endpoint列表不能及时更新,旧的Pod的IP没有被移除,新的Pod的IP没有被加入,请求就被转发到了不存在的Pod上,导致了大量的5xx错误。
而且,因为etcd变慢,整个集群的控制平面都受到了影响,不只是这个微服务,其他的微服务,也出现了Endpoint更新不及时的问题,导致整个集群的服务都受到了影响。
第六步:清理ConfigMap,恢复etcd
找到问题的根源之后,我们马上开始处理。首先,清理那些多余的ConfigMap,我们用kubectl delete configmap命令,批量删除那些临时的、无用的ConfigMap。删除了几千个ConfigMap之后,etcd的负载慢慢降下来了,响应速度也恢复了正常。
然后,我们重启了kube-controller-manager,让它重新同步Endpoint和Service的状态。重启之后,Endpoint列表开始正常更新,旧的Pod的IP被移除,新的Pod的IP被加入,请求转发恢复正常。
大概过了十几分钟,所有的Service的Endpoint都恢复了正常,错误率开始下降,服务慢慢恢复正常。我们又观察了半个小时,确认所有的指标都恢复正常,没有再出现问题,才宣布故障恢复。
整个故障,从发生到恢复,持续了大概两个小时,影响了线上的服务,也给业务方造成了一定的损失,事后我们做了详细的复盘,总结了经验教训,也做了很多改进,避免类似的问题再次发生。
四、故障原因总结
总结一下这次故障的原因,主要有以下几个:
1. Helm使用不当,加了--force参数
这次发布,我们用了helm upgrade --force命令,这个参数,会强制重新创建所有的资源,包括Pod、Service、ConfigMap等,比普通的helm upgrade更激进,更容易出问题。平时我们发布,都不会加--force参数,这次因为之前有一次发布,资源更新失败,我们以为加--force能解决问题,就加了,结果没想到,引发了更大的问题。
2. Chart里的循环错误,创建了大量的ConfigMap
我们的Chart里,有一个模板,用了range循环,用来创建多个ConfigMap,但是循环的条件写错了,导致每次升级,都会创建大量的临时ConfigMap,而这些ConfigMap,没有被正确清理,越积越多。平时发布,因为不加--force,创建的ConfigMap不多,问题没有暴露出来,这次加了--force,一下子创建了几千个,问题就爆发了。
3. etcd的容量和性能没有做好监控和限制
我们的etcd集群,没有做好容量和性能的监控,也没有对ConfigMap等资源的数量做限制,导致一下子创建了几千个ConfigMap,写入了大量的数据,etcd的负载瞬间飙升,但是我们没有及时发现,直到影响了整个集群,才意识到问题。
4. 回滚没有考虑到集群层面的问题
故障发生之后,我们第一时间回滚了应用的版本,但是没有考虑到,问题已经不是应用层面的了,而是集群层面的,etcd和控制平面已经出了问题,回滚应用版本,解决不了问题,反而浪费了时间。这说明我们的故障处理流程,还有待完善,遇到问题,应该先全面排查,确定问题的层面,再采取对应的措施,而不是想当然地回滚。
5. 发布流程不够严谨,没有在测试环境充分验证
这次发布,我们虽然在测试环境验证了,但是测试环境的集群,规模比较小,数据量也比较小,没有暴露出Chart里的循环错误,也没有测试--force参数的影响。到了线上,集群规模大,数据量大,问题就爆发了。这说明我们的发布流程,还不够严谨,测试环境和线上环境的差异太大,很多问题在测试环境发现不了。
五、经验教训和改进措施
这次故障,给我们敲响了警钟,也让我们学到了很多,事后我们做了详细的复盘,总结了以下经验教训和改进措施:
1. 谨慎使用helm --force参数
以后发布,除非有特殊情况,并且经过充分评估,否则不要使用--force参数。普通的helm upgrade,已经能满足大部分的发布需求,--force参数太激进,容易出问题。如果确实需要用--force,一定要先在测试环境充分验证,确认没有问题,再用到线上。
2. 完善Chart的审核和测试
以后,Chart的修改,必须经过代码审核,并且在测试环境充分验证,包括各种边界情况,各种参数组合,确保没有问题,才能合并和发布。而且,要对Chart做单元测试和集成测试,用helm lint、helm template等工具,检查Chart的语法和逻辑,避免出现循环错误之类的问题。
3. 加强etcd的监控和容量规划
以后,要加强对etcd的监控,包括etcd的集群状态、leader选举、写入延迟、存储容量、对象数量等,设置合理的告警阈值,一旦出现异常,及时告警,及时处理。而且,要对etcd的容量做好规划,定期清理无用的资源,比如旧的ConfigMap、Secret、Event等,避免etcd的存储和负载过高。
4. 对Kubernetes资源做数量限制
以后,要对Kubernetes的资源,比如ConfigMap、Secret、Pod、Service等,做数量限制,用ResourceQuota或者LimitRange,限制每个命名空间的资源数量,避免因为某个应用的问题,创建大量的资源,影响整个集群。而且,要对etcd的存储大小做限制,超过阈值就拒绝写入,避免etcd被写满。
5. 完善故障处理流程
以后,遇到故障,要先全面排查,确定问题的层面和范围,再采取对应的措施,不要想当然地回滚或者重启。要建立完善的故障处理流程,包括故障分级、响应机制、排查步骤、恢复方法、沟通机制等,确保故障发生之后,能快速、有序地处理,减少损失。
而且,要定期做故障演练,模拟各种故障场景,锻炼团队的故障处理能力,让大家熟悉故障处理流程,遇到真实故障的时候,才能从容应对。
6. 优化发布流程,缩小测试环境和线上环境的差异
以后,要优化发布流程,尽量缩小测试环境和线上环境的差异,比如用相同的集群规模、相同的配置、相同的数据量,确保在测试环境能发现的问题,在线上也能发现。而且,要完善灰度发布的流程,灰度的范围要足够大,观察的时间要足够长,确保没有问题,再全量发布。
而且,要建立自动化的发布和回滚机制,用CI/CD工具,自动化地完成发布、验证、灰度、回滚等流程,减少人为操作的失误,提高发布的效率和安全性。
7. 加强团队的技术培训和知识分享
以后,要加强团队的技术培训和知识分享,定期组织Kubernetes、Helm、云原生等相关技术的培训和分享,让大家都熟悉这些技术的原理和最佳实践,避免因为不熟悉而犯错误。而且,要建立知识库,把故障复盘、最佳实践、常见问题等,都记录下来,供大家学习和参考,避免重复犯同样的错误。
六、写在最后
这次Helm包管理的故障,是一次惊心动魄的经历,也是一次宝贵的学习机会。虽然故障给我们造成了一定的损失,但是也让我们发现了很多问题,学到了很多东西,做了很多改进,让我们的系统更稳定,团队更成熟。
技术的道路上,故障是不可避免的,重要的是,我们要从故障中学习,总结经验教训,不断改进,不断进步,让我们的系统越来越稳定,越来越可靠。
Helm是一个很强大的工具,但是强大的工具,也需要正确地使用,不然很容易出问题。希望我们这次的故障复盘,能给大家一些参考和借鉴,让大家在使用Helm和Kubernetes的时候,少踩坑,少出问题。
最后,感谢团队的每一个人,在故障处理的过程中,大家齐心协力,分工合作,最终快速恢复了服务,也做了详细的复盘和改进。相信经过这次故障,我们的团队会更成熟,我们的系统会更稳定。
愿我们都能在故障中成长,在问题中进步,打造更稳定、更可靠的系统。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录