最大限度地从 Kubernetes 集群故障中吸取经验教训
本文最初发表于我的个人博客。
几个月以来,我们(NU.nl开发团队)一直在运行少量 Kubernetes 集群。我们看到了 Kubernetes 的潜力,它能够提升我们的生产力,并改进我们的 CI/CD 实践。目前,我们已在 Kubernetes 上运行部分日志记录和构建工具集,以及一些小型(内部)面向客户的工作负载。我们计划在积累了更多知识和信心后,将更多应用程序迁移到 Kubernetes 上。
最近,我们团队的一个集群出现了一些问题。虽然问题不至于严重到导致集群完全崩溃,但确实影响了一些内部使用工具和仪表盘的用户体验。
巧合的是,大约在同一时间,我参加了在慕尼黑举行的 DevOpsCon 2018 大会,大会开幕主题演讲“生存之道:从海底汲取故障管理模式”与此次事件非常契合。
本次演讲(由Twitter 工程经理Ronnie Chen主讲)重点探讨了如何提高 DevOps 团队在预防和处理故障方面的效率。其中一个主题是灾难通常是由一系列故障引发的,并引出了以下这段话:
事后分析如果只关注事件的根本原因,可能只涵盖导致事件发生的约 15% 的问题。
从这份事后分析模板列表中可以看出,很多模板都包含“根本原因”(复数)。然而,事件链很容易被忽略,尤其是在很多情况下,消除或修复根本原因就能解决问题。
那么,让我们看看导致此次事故的一系列故障是如何发生的,并最大限度地吸取我们的经验教训。
事件
我们的团队收到报告称,许多服务出现异常行为:偶尔出现错误页面、响应缓慢和超时。
我们尝试通过 Grafana 进行调查,发现 Grafana 和 Prometheus 都出现了类似的问题。从控制台检查集群后发现:
$: kubectl get nodes
NAME STATUS ROLES AGE VERSION
ip-10-150-34-78.eu-west-1.compute.internal Ready master 43d v1.10.6
ip-10-150-35-189.eu-west-1.compute.internal Ready node 2h v1.10.6
ip-10-150-36-156.eu-west-1.compute.internal Ready node 2h v1.10.6
ip-10-150-37-179.eu-west-1.compute.internal NotReady node 2h v1.10.6
ip-10-150-37-37.eu-west-1.compute.internal Ready master 43d v1.10.6
ip-10-150-38-190.eu-west-1.compute.internal Ready node 4h v1.10.6
ip-10-150-39-21.eu-west-1.compute.internal NotReady node 2h v1.10.6
ip-10-150-39-64.eu-west-1.compute.internal Ready master 43d v1.10.6
节点状况NotReady不佳。对各种节点(不仅仅是那些不健康的节点)的描述显示:
$: kubectl describe node ip-10-150-36-156.eu-west-1.compute.internal
<truncated>
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Starting 36m kubelet, ip-10-150-36-156.eu-west-1.compute.internal Starting kubelet.
Normal NodeHasSufficientDisk 36m (x2 over 36m) kubelet, ip-10-150-36-156.eu-west-1.compute.internal Node ip-10-150-36-156.eu-west-1.compute.internal status is now: NodeHasSufficientDisk
Normal NodeHasSufficientMemory 36m (x2 over 36m) kubelet, ip-10-150-36-156.eu-west-1.compute.internal Node ip-10-150-36-156.eu-west-1.compute.internal status is now: NodeHasSufficientMemory
Normal NodeHasNoDiskPressure 36m (x2 over 36m) kubelet, ip-10-150-36-156.eu-west-1.compute.internal Node ip-10-150-36-156.eu-west-1.compute.internal status is now: NodeHasNoDiskPressure
Normal NodeHasSufficientPID 36m kubelet, ip-10-150-36-156.eu-west-1.compute.internal Node ip-10-150-36-156.eu-west-1.compute.internal status is now: NodeHasSufficientPID
Normal NodeNotReady 36m kubelet, ip-10-150-36-156.eu-west-1.compute.internal Node ip-10-150-36-156.eu-west-1.compute.internal status is now: NodeNotReady
Warning SystemOOM 36m (x4 over 36m) kubelet, ip-10-150-36-156.eu-west-1.compute.internal System OOM encountered
Normal NodeAllocatableEnforced 36m kubelet, ip-10-150-36-156.eu-west-1.compute.internal Updated Node Allocatable limit across pods
Normal Starting 36m kube-proxy, ip-10-150-36-156.eu-west-1.compute.internal Starting kube-proxy.
Normal NodeReady 36m kubelet, ip-10-150-36-156.eu-west-1.compute.internal Node ip-10-150-36-156.eu-west-1.compute.internal status is now: NodeReady
看起来节点的操作系统在kubelet回收内存之前就终止了进程,正如Kubernetes 文档中所述。
我们集群中的节点属于自动扩缩容组。考虑到当时我们遇到了间歇性故障,并且无法访问 Grafana,我们决定NotReady逐个终止节点,以观察新节点是否能够保持稳定。结果并非如此,新节点虽然正确生成,但部分现有节点或新节点很快又进入了异常状态NotReady。
不过,这样做确实让 Prometheus 和 Grafana 被调度到一个保持稳定的节点上,所以至少我们有了更多的数据进行分析,根本原因也很快显现出来……
根本原因
我们Grafana配置中的一个仪表盘显示了集群范围内的总计数据,以及Pod内存和CPU使用率的图表。这很快就找到了问题的根源。
那些延伸到虚空的线路都是运行ElastAlert 的Pod 。我们使用 Elasticsearch 集群来存储日志,最近我们一直在尝试使用 ElastAlert 根据日志触发警报。在事件发生前不久引入的一个警报是,如果我们的Cloudfront-*索引在一定时间内没有收到新文档,就会触发该警报。由于该 CloudFront 分发的吞吐量高达每小时数百万个请求,这显然导致了内存使用量的急剧上升。事后看来,深入研究文档后,我们最好使用use_count_query`and/or` max_query_size。
一系列失败
所以,根本原因已经找到、调查并修复。事件结束了,对吧?但记住之前那句话,还有 85% 的经验教训需要总结,所以让我们深入探讨一下:
未触发任何警报
显然,我们正在着手处理告警问题,因为根本原因与 ElasticAlert 有关。一些需要处理的数据(目前)仅在 Elasticsearch 中可用,例如日志消息(关键字出现次数)或 Kubernetes 集群之外的系统。Prometheus 也有一个告警管理器,我们还需要进行配置。除了这两个数据源之外,我们还使用 New Relic 进行应用性能管理 (APM)。无论数据源是什么,或许需要进行整合,但至少首先要定义告警规则。
解决:
- 定义与资源使用情况相关的警报,例如 CPU、内存和磁盘空间。
- 继续研究能够有效整合多种信息源的预警策略。
Grafana仪表盘受集群问题影响
在 Kubernetes 集群中部署 Prometheus 和 Grafana 非常简单(安装一些 Helm Chart 后即可基本完成)。但是,如果您无法访问集群,那就如同盲人摸象。
解决:
- 考虑将指标导出到集群外部,并将 Grafana 也迁移到集群外部。不过,这样做并非没有缺点,robustperception.io 上的这篇文章对此有非常详细的解释。其优势在于,可以为多个集群提供一个统一的仪表盘入口。据我所知,Kublr也使用了类似的设置来监控多个集群。
- 集群外位置可以是 EC2,也可以是独立的 Kubernetes 集群。
未能充分利用我们的 ELK 技术栈。
我们运行着一个基于 EC2 的 ELK 技术栈,它会摄取大量的 CloudFront 日志。此外,它还会摄取来自 Kubernetes 集群的日志和指标,这些日志和指标由 Filebeat 和 Metricbeat 守护进程导出。因此,我们无法通过集群内部的 Grafana 访问的数据,实际上也存在于 ELK 技术栈中……只是要么没有被正确可视化,要么被忽略了。
这总体来说是一个比较棘手的问题:一方面,Elasticsearch 可能本来就需要用于集中式日志管理,而且它也能处理指标,因此可能是一个一站式解决方案。然而,大规模部署后,它的运维相当复杂,而且(在我看来)如果能提供更多示例仪表盘,对用户上手会有很大帮助。
另一方面,Prometheus 设置简单,似乎是 Kubernetes 生态系统中的默认技术,并且与 Grafana 提供的仪表板相结合,非常容易上手。
解决:
- 要么在 ELK 中可视化重要指标,要么提高 Prometheus/Grafana 的可用性。
- 改进指标策略。
ElastAlert pod 没有 CPU 和内存限制。
用于安装 ElastAlert 的 Helm chart允许指定资源请求和限制,但是这些没有默认值(这并不罕见),我们忽略了这一点。
为了强制执行资源限制配置,我们可以为我们的命名空间配置默认和限制内存请求。
解决:
- 通过 Helm 值指定资源限制。
- 配置命名空间,使其具有内存请求的默认值和限制。
影响面向客户的工作负载的运维服务
客户工作负载和监控/日志工具共享同一组资源存在放大效应的风险,最终可能导致所有资源被耗尽。流量增加会导致 CPU/内存压力增大,进而导致日志/指标数据量增加,进而进一步加剧 CPU/内存压力,如此循环往复。
我们原本就计划将所有日志记录、监控和 CI/CD 工具迁移到生产集群内的一个专用节点组。根据我们之前的经验,建立一个专用的“工具”集群也是一个可行的方案。
决议(已在计划之中):
- 将面向客户的工作负载与构建、日志记录和监控工作负载隔离。
团队内部对已部署的 ElastAlert 变更缺乏了解。
尽管新的警报功能通过了代码审查,但并非所有人都知道它已被合并并部署。更重要的是,由于它是由团队成员通过命令行安装的,因此没有直接的信息来源可以显示集群中哪些应用程序可能已被更新。
解决:
- 所有部署都通过自动化方式进行(例如 Jenkins 流水线)。
- 考虑采用GitOps方法部署新的应用程序版本:使用开发人员熟知的工具,记录“待定状态”和代码更改历史记录。
没有烟雾测试
如果我们使用流水线部署 ElastAlert 更新,就可以在部署后添加一个“冒烟测试”步骤。这可以检测内存使用过高,或者由于 Pod 超出配置的内存限制而导致的 Pod 重启。
解决:
- 通过包含冒烟测试步骤的流水线进行部署。
团队中只有部分成员了解 Kubernetes 的操作。
我们的团队(和大多数团队一样)由在不同领域拥有不同专业知识水平的成员组成。有些人拥有更丰富的云和DevOps经验,有些人是前端或Django专家等等。由于Kubernetes是一项相当新的技术,尤其对我们团队而言,其知识普及程度远不及预期。正如敏捷团队实践的所有技术一样:DevOps不应局限于单个(或部分)团队。幸运的是,经验丰富的团队成员可以随时协助那位基础设施经验不足的值班成员。
决议(已在计划之中):
- 确保与 Kubernetes 相关的工作(实际上是与云基础设施相关的工作)成为团队冲刺的一部分,并由所有团队成员参与,与更有经验的成员结对协作。
- 深入探讨特定主题的研讨会。
包起来
显而易见,解决 ElastAlert 问题本身只是冰山一角。从这起看似简单的事件中,我们学到了很多东西。本文列出的大多数要点,我们此前或多或少都已有所察觉,但本文强调了它们的重要性。
将这些经验转化为 Scrum(或看板)项目,将使我们能够有针对性地改进我们的平台和实践,并衡量我们的进展。
团队学习和进步需要一种允许“不追究责任的事后分析”的公司文化,而不仅仅关注“事件数量”或“解决时间”。最后,我想引用一段在DevOps大会上听到的话:
文章来源:https://dev.to/tbeijen/maximize-learnings-from-a-kubernetes-cluster-failure-3p53成功就是屡败屡战,而热情不减——温斯顿·丘吉尔
