上个月,我们的Kubernetes集群出了一次大故障,整个线上服务停了将近两个小时。

那是一个周五的晚上,我正准备下班,监控系统突然发出告警:核心服务的错误率飙升,大量请求超时。我登录集群一看,发现多个节点状态异常,Pod不断重启,整个集群几乎瘫痪。

那两个小时,是我运维生涯中最紧张的两个小时。最终,我们找到了根因,恢复了服务,但也付出了不小的代价。这篇文章,我想做一次完整的故障复盘,分享故障的经过、根因、解决过程,以及从中总结的经验教训。

故障背景

先说说我们的集群情况。

我们的生产集群是Kubernetes 1.32版本,跑在云上,有20个工作节点,运行着大概200个Pod,承载着公司的核心业务。集群是我们自己维护的,用kubeadm部署,etcd是独立部署的三节点集群。

故障发生前一周,我们刚做了一次集群升级,从1.31升到了1.32。升级过程很顺利,升级之后也跑了一周,没发现什么问题。我们还做了一些常规的维护,更新了几个组件的版本。

现在回想起来,故障的种子在升级的时候就埋下了,只是当时没有爆发。

故障发生

故障发生在周五晚上七点半。

当时我刚收拾好东西准备走,手机突然收到告警消息:API网关的5xx错误率超过了10%。我心里一紧,赶紧打开电脑,登录监控系统。

监控显示,多个核心服务的错误率都在飙升,响应时间也从正常的几百毫秒变成了几秒甚至超时。我立刻登录Kubernetes集群,查看节点状态。

kubectl get nodes

结果让我倒吸一口凉气:20个节点中,有8个节点状态是NotReady,剩下的节点虽然是Ready,但负载很高。Pod的状态也很乱,很多Pod在CrashLoopBackOff,不断重启。

我第一反应是:是不是节点出问题了?我登录到一个NotReady的节点上,查看kubelet的日志。

journalctl -u kubelet -f

日志里有大量的错误,都是和API Server通信超时。kubelet无法连接到API Server,所以节点状态变成了NotReady。

那问题是不是出在API Server?我查看API Server的状态,发现API Server的Pod也在不断重启。etcd集群的状态也不正常,有一个节点掉出了集群。

这时候我意识到,这不是单个节点的问题,而是整个控制平面出了问题。

排查过程

我立刻叫了另外两个运维同事,一起排查。我们分了工:一个人看控制平面,一个人看节点和网络,一个人看业务日志。

控制平面排查

我负责控制平面。我先看了API Server的日志,发现大量的请求超时和etcd连接错误。API Server无法正常读写etcd,导致整个集群的状态无法更新。

然后我看etcd的状态。etcd是三节点集群,正常情况下应该有三个成员。但当时只有两个成员在运行,第三个成员掉了。而且,剩下的两个成员之间的同步也有问题,日志里有大量的raft选举超时。

我尝试用etcdctl查看集群状态:

etcdctl endpoint health
etcdctl member list

结果显示,两个健康的节点可以通信,但数据不同步,而且集群的leader频繁切换。这说明etcd集群已经脑裂了,无法正常提供服务。

etcd是Kubernetes的数据库,所有集群状态都存在etcd里。etcd出问题,整个集群就瘫痪了。

节点和网络排查

另一个同事在排查节点和网络。他发现,NotReady的节点都是同一批,都是上周升级的时候重启过的节点。这些节点的网络配置有问题,kube-proxy的规则没有正确生成,导致Pod之间的通信不通。

而且,这些节点上的CoreDNS Pod也出了问题,DNS解析失败,导致服务发现失效。很多业务Pod因为连不上依赖的服务而崩溃重启。

业务影响

第三个同事在看业务影响。他发现,核心的订单服务、支付服务、用户服务都受到了影响。用户无法下单、无法支付、无法登录。客服已经收到了大量用户投诉,运营团队也在群里催。

时间一分一秒过去,故障已经持续了四十分钟,服务还没有恢复的迹象。我的手心全是汗。

根因定位

我们三个人把排查到的信息汇总到一起,开始分析根因。

线索一:故障发生在集群升级一周之后,但出问题的节点都是升级时重启过的节点。

线索二:etcd集群有一个节点掉了,剩下的节点之间同步有问题。

线索三:部分节点的网络配置有问题,kube-proxy规则异常。

线索四:API Server因为etcd不可用而不断重启。

我们开始查升级时的变更记录。升级的时候,我们按照官方文档,先升级了控制平面节点,然后升级了工作节点。在升级工作节点的时候,我们用了kubectl drain驱逐节点上的Pod,然后升级kubelet和kube-proxy,最后重启节点。

问题出在kube-proxy的升级。Kubernetes 1.32对kube-proxy做了一些改动,默认的IPVS模式配置有变化。我们升级的时候,直接用了旧的配置文件,没有按照新版本的要求更新。结果,重启之后,kube-proxy没有正确生成iptables/IPVS规则,导致Service的流量转发异常。

但这还不是最致命的。最致命的是,我们的etcd集群中,有一个节点也是在升级的时候重启的。重启之后,这个etcd节点的数据目录权限出了问题(我们升级时改了etcd的运行用户,但没有递归修改数据目录的权限),导致etcd进程无法读写数据,启动失败。

一个etcd节点挂了,按理说三节点集群还能正常工作,因为还有两个节点,能形成多数派。但问题是,剩下的两个etcd节点,其中一个的网络也因为kube-proxy的问题而不通。两个节点之间无法通信,就无法形成多数派,etcd集群就脑裂了。

etcd脑裂,API Server就无法正常工作。API Server不正常,kubelet就无法上报节点状态,节点变成NotReady。节点NotReady,控制器就会驱逐上面的Pod,调度到其他节点。但其他节点的网络也有问题,Pod启动了也无法正常通信。于是,整个集群就瘫痪了。

根因找到了:升级时kube-proxy配置没有更新,导致部分节点网络异常;同时etcd节点数据目录权限问题,导致一个etcd节点挂掉;两个问题叠加,导致etcd集群脑裂,整个Kubernetes集群瘫痪。

恢复过程

找到根因之后,我们开始恢复。

第一步,修复etcd集群。我们先把那个挂掉的etcd节点的数据目录权限修复,然后重启etcd。但因为集群已经脑裂了,重启之后它也无法加入集群。我们不得不做了etcd的灾难恢复:从最近的快照恢复数据,然后重新组建集群。

幸好我们有定期的etcd快照备份,最近的一次备份是故障前两小时的。我们用快照恢复了etcd数据,然后重新启动三节点集群。etcd恢复之后,API Server也慢慢恢复正常。

第二步,修复kube-proxy配置。我们按照Kubernetes 1.32的官方文档,更新了所有节点的kube-proxy配置,然后重启kube-proxy。重启之后,网络规则重新生成,节点之间的通信恢复正常。

第三步,恢复节点状态。网络恢复之后,NotReady的节点陆续变成了Ready。我们手动重启了一些异常的Pod,让它们重新调度。CoreDNS也恢复正常,服务发现恢复。

第四步,验证业务。集群恢复之后,我们开始验证各个业务服务的状态。订单、支付、用户服务陆续恢复正常,错误率下降到正常水平。我们又做了一轮功能测试,确认核心功能都正常。

从故障发生到完全恢复,一共用了一小时五十分钟。虽然没有造成数据丢失,但对用户体验和公司声誉造成了不小的影响。

经验教训

这次故障,给了我们深刻的教训。我总结了几点:

第一,升级前必须仔细阅读版本变更说明。Kubernetes的每个版本都有一些不兼容的变更,尤其是kube-proxy、kubelet、CNI这些核心组件。升级之前,一定要仔细阅读CHANGELOG,了解哪些配置变了,哪些行为变了。不能想当然地认为旧配置在新版本上一定能用。

第二,升级要有完整的测试和回滚方案。我们这次升级,只在测试环境做了简单的验证,没有模拟生产环境的复杂场景。而且,没有制定详细的回滚方案,出了问题之后手忙脚乱。以后的升级,必须在预发布环境充分测试,制定详细的回滚预案,并且在业务低峰期操作。

第三,etcd是Kubernetes的命根子。etcd的稳定性直接决定了集群的稳定性。etcd一定要部署成高可用集群,定期备份,并且定期做恢复演练。etcd的权限、磁盘、网络,都要重点监控。etcd出问题,就是大问题。

第四,监控要覆盖控制平面。我们以前的监控主要关注业务指标和节点资源,对控制平面(API Server、etcd、scheduler、controller-manager)的监控不够。这次故障,其实早期就有一些征兆(比如etcd同步延迟、API Server响应变慢),但因为监控不到位,没有及时发现。以后,控制平面的每个组件都要有详细的监控和告警。

第五,变更要分批、灰度。我们这次升级,一次性升级了所有节点。如果分批升级,先升级一小部分,观察没问题再升级剩下的,故障的影响范围就会小很多。以后任何变更,都要分批、灰度,不要一次性全量操作。

第六,故障演练很重要。如果我们之前做过etcd灾难恢复的演练,这次恢复的时候就不会那么紧张,速度也会更快。以后要定期做故障演练,包括etcd恢复、节点故障、网络分区等场景,确保团队在真正出问题的时候能从容应对。

第七,文档和Runbook要完善。故障发生的时候,大家都很紧张,这时候如果有详细的Runbook(操作手册),就能按照步骤来,减少出错。我们以前的文档不够完善,很多操作靠记忆。以后,每个组件的常见故障和处理步骤,都要写成Runbook,定期更新。

改进措施

故障之后,我们做了一系列改进:

第一,完善了升级流程。制定了详细的升级Checklist,包括升级前的检查、升级中的步骤、升级后的验证、回滚方案。每次升级都要严格按照Checklist执行。

第二,加强了控制平面的监控。给API Server、etcd、scheduler、controller-manager都加了详细的监控指标和告警规则。etcd的同步延迟、leader切换、磁盘使用,都有实时监控。

第三,建立了etcd备份和恢复机制。etcd每小时自动备份一次,备份文件存到对象存储。每月做一次恢复演练,确保备份可用。

第四,变更管理更加严格。任何生产环境的变更,都要提交变更申请,说明变更内容、影响范围、回滚方案,经过审批之后才能执行。重大变更必须在业务低峰期操作,并且有至少两个人在场。

第五,定期做故障演练。每季度做一次全链路的故障演练,模拟各种故障场景,检验团队的应急响应能力。

第六,完善了文档和Runbook。把每个组件的架构、常见问题、处理步骤,都写成了详细的文档,放在团队的知识库⾥。新员工入职的时候,也要学习这些文档。

写在最后

这次K8s故障,是我运维生涯中最惊心动魄的一次经历。两个小时的时间,感觉像过了一整天。故障恢复之后,我瘫在椅子上,半天缓不过来。

但故障也是最好的老师。通过这次复盘,我们找到了系统的薄弱环节,改进了流程和工具,团队的应急能力也提升了。以后再遇到类似的问题,我们应该能更快地发现、更快地恢复。

做运维,就是这样。你永远不知道下一个故障什么时候来,你能做的,就是做好准备,完善监控,规范流程,定期演练。这样,当故障来临的时候,才能从容应对。

希望我的这次复盘经历,能给做K8s运维的朋友一些参考。如果你也有类似的故障经历或者更好的经验,欢迎在评论区交流。