原文: https://thenewstack.io/deleting-production-in-a-few-easy-steps-and-how-to-fix-it/
想象一下,您从梦中醒来,收到一条简单的消息,“我们丢失了一个集群”,但这根本不是梦。
InfluxDB Cloud 运行在云应用编排平台 Kubernetes 上,我们使用自动化的持续交付(CD) 系统将代码和配置更改部署到生产环境。在一个平常的工作日,工程团队交付 5~15 种不同的生产变更。
为了将这些代码和配置更改部署到 Kubernetes 集群,该团队使用了一个名为 ArgoCD 的工具。ArgoCD 读取 YAML 配置文件并使用 Kubernetes API 使集群与 YAML 配置中指定的代码保持一致。
ArgoCD 使用 Kubernetes 中的自定义资源(称为 Applications 和 AppProjects)将源基础设施作为代码存储库进行管理。ArgoCD 还管理这些存储库的文件路径以及特定 Kubernetes 集群和命名空间的部署目标。
因为我们维护多个集群,所以我们还使用 ArgoCD 进行自我监管并管理所有不同 ArgoCD 应用程序和 AppProjects 的定义。这是一种常见的开发方法,通常被称为app of apps
模式。
应用模式
我们使用一种称为 jsonnet 的语言来创建 YAML 配置的模板。CD 系统检测 jsonnet 中的更改,将 jsonnet 转换为 YAML,然后 Argo 应用更改。在我们事件发生时,单个应用程序的所有资源都保存在一个 YAML 文件中。
对象名称和目录结构遵循某些命名约定(app name)–(cluster name)
用于对象名称, env/(cluster name)/(app name)/yml
用于保留其定义的存储库。例如,cluster01 中的 app01 定义为 app01-cluster01,其定义保存在路径 env/cluster01/app01/yml 下。
我们对基础设施即代码进行代码审查,其中包括检查生成的 YAML 并确保它在应用更新之前按预期运行。
考验始于配置文件中的一行代码。团队中的某个人创建了一个 PR,将几个新对象添加到配置文件和渲染的 YAML 文件中。
在这种情况下,添加的对象之一是新的 ArgoCD 应用程序和 AppProject。由于自动化错误,对象的名称错误。它们本应命名为app02-cluster01,但改为命名为app01-cluster01。代码审查忽略了 app01 和 app02 之间的差异,因此,在渲染时,这两个资源最终都在一个 YAML 配置文件中。
ArgoCD 应用程序/项目名称冲突
当我们将 PR 与错误命名的对象合并时,ArgoCD 会读取整个生成的 YAML 文件并按照它们在文件中列出的顺序应用所有对象。结果,列出的最后一个对象“获胜”并被应用,这就是发生的事情。ArgoCD 用新实例替换了以前的实例 app1。问题在于,ArgoCD 删除的 app1 实例是 InfluxDB Cloud 的核心工作负载。
此外,新对象创建了我们不想在该集群上启用的额外工作负载。简而言之,当 ArgoCD 替换 app01 的实例时,该过程触发了整个生产环境的立即删除。
显然,这对我们的用户不利。当生产出现故障时,所有 API 端点(包括所有写入和读取)都返回 404 错误。在中断期间,没有人能够收集数据,任务无法运行,外部查询也不起作用。
我们立即着手解决问题,首先查看合并 PR 中的代码。这个问题很难被发现,因为它涉及项目和应用程序名称之间的 ArgoCD 冲突。
我们的第一个直觉是恢复更改以使事情恢复正常。不幸的是,这并不是有状态应用程序的工作方式。我们开始了恢复过程,但几乎立即停止了,因为恢复更改会导致 ArgoCD 创建我们应用程序的全新实例。这个新实例将没有原始实例所具有的关于用户、仪表板和任务的元数据。至关重要的是,新实例不会拥有最重要的东西~客户的数据。
在这一点上,值得一提的是,我们将 InfluxDB 云集群中的所有数据存储在使用 reclaimPolicy:Retain 的卷中。这意味着即使我们管理的 Kubernetes 资源,例如 StatefulSet、PersistentVolumeClaim (PVC) 被删除,底层的 PersistentVolume 和云中的卷也不会被删除。
我们在制定恢复计划时考虑到了这一关键细节。我们必须手动重新创建所有底层 Kubernetes 对象,例如 PVC。一旦新对象启动并运行,我们需要从备份系统中恢复任何丢失的数据,然后让 ArgoCD 重新创建我们应用程序的无状态部分。
InfluxDB Cloud 在其他微服务与之交互的系统的一些组件中保持状态,包括:
团队首先恢复 etcd 和元数据。这可能是恢复过程中最直接的任务,因为 etcd 存储的数据集相对较小,因此我们能够让 etcd 集群快速启动并运行。这对我们来说是一场轻松的胜利,让我们能够将所有注意力集中在更复杂的恢复任务上,比如 Kafka 和存储。
我们识别并重新创建了任何丢失的 Kubernetes 对象,这使卷(特别是持久卷对象)重新联机并将它们置于可用状态。一旦卷的问题得到解决,我们重新创建了 StatefulSet,以确保所有 pod 运行和集群同步。
下一步是恢复 Kafka,为此我们还必须让 Zookeeper 保持健康状态,它为 Kafka 集群保留元数据。Zookeeper 卷也在事件中被删除。好在我们使用 Velero 每小时备份一次 Zookeeper,Zookeeper 的数据不会经常变化。我们从最近的备份中成功恢复了 Zookeeper 卷,这足以让它启动并运行。
要恢复 Kafka,我们必须创建与 Kafka 的卷和状态相关的任何缺失对象,然后一次重新创建集群的 StatefulSet 一个 pod。我们决定禁用所有健康和就绪检查,以使 Kafka 集群处于健康状态。这是因为我们必须一次在 StatefulSet 中创建一个 Pod,而 Kafka 直到集群领导者启动后才准备好。暂时禁用检查允许我们创建所有必要的 pod,包括集群领导者,以便 Kafka 集群报告为健康。
因为 Kafka 和 etcd 是相互独立的,所以我们可以同时恢复两者。但是,我们希望确保有正确的程序,所以我们选择一次恢复一个。
一旦 Kafka 和 etcd 重新上线,我们可以重新启用部分 InfluxDB Cloud 以开始接受写入。因为我们使用 Kafka 作为我们的预写日志 (WAL),即使存储功能不正常,我们也可以接受对系统的写入并将它们添加到 WAL。一旦其他部分重新上线,InfluxDB Cloud 就会处理这些写入。
随着写入变得可用,我们开始担心我们的实例会被来自 Telegraf 和其他客户端的请求所淹没,这些请求写入在集群关闭时缓冲的数据。为了防止这种情况,我们调整了处理写入请求的组件的大小,增加了副本的数量并增加了内存请求和限制。这有助于我们处理临时的写入高峰并将所有数据摄取到 Kafka 中。
为了修复存储组件,我们重新创建了所有存储 pod。InfluxDB 还将所有时间序列数据备份到对象存储(例如,AWS S3、Azure Blob Store 和 Google Cloud Storage)。当 pod 出现时,他们从对象存储中下载数据副本,然后索引所有数据以实现高效读取。该过程完成后,每个存储 pod 都会联系 Kafka 并读取 WAL 中任何未处理的数据。
一旦创建存储 pod 和索引现有数据的过程开始,灾难恢复团队就能够专注于修复系统的其他部分。
我们更改了存储集群的一些设置,减少了某些服务的副本数量,以使重新上线的部分能够更快地启动。此时,我们重新启用了 ArgoCD,以便它可以创建仍然缺失的任何 Kubernetes 对象。
在初始部署和存储引擎功能齐全后,我们可以重新启用关键流程的功能,例如查询数据和查看仪表板。在此过程继续进行的同时,我们开始为所有资源重新创建适当数量的副本,并重新启用任何剩余的功能。
最后,所有组件都部署了预期数量的副本,并且一切都处于健康和就绪状态,团队启用了计划任务并进行了最终的 QA 检查,以确保一切正常运行。
总的来说,从 PR 合并到我们恢复全部功能的时间不到六个小时。
事件发生后,我们进行了适当的事后分析,以分析哪些方面进展顺利,以及我们可以为未来的事件改进哪些方面。
从好的方面来说,我们能够在不丢失任何数据的情况下恢复系统。在整个中断期间,任何重试向 InfluxDB 写入数据的工具都会继续这样做,最终,该数据被写入 InfluxDB 云产品。例如,我们的开源收集代理 Telegraf 默认执行重试。
最重要的问题是我们的监控和警报系统没有立即检测到这个问题。这就是为什么我们最初的反应是尝试回滚更改,而不是计划和执行经过深思熟虑的恢复过程。我们还缺少丢失部分或整个 InfluxDB Cloud 实例的运行手册。
作为此事件的结果,InfluxData 工程创建了专注于恢复状态的运行手册。如果发生类似情况,即如果 Kubernetes 对象(例如 Persistent Volume Claims)被删除,但底层磁盘和卷上的数据被保留,我们现在有详细说明如何继续。我们还确保所有环境中的所有卷都设置为保留数据,即使 PVC 对象被删除。
我们还改进了处理面向公众的事件的流程。我们的目标是尽可能少地发生事件,这将有助于我们解决未来可能面向公众的平台出现的任何问题。
在技术方面,我们意识到我们的系统应该阻止 PR 被合并,我们采取了多个步骤来解决这个问题。我们改变了 InfluxDB 存储生成的 YAML 文件的方式,转向每个文件一个对象的方法。例如 v1.Service-(namespace).etcd.yaml 用于 etcd 服务。将来,类似的 PR 将清楚地显示为对现有对象的覆盖,并且不会被误认为是添加新对象。
我们还改进了在生成 YAML 文件时检测重复项的工具。现在,系统会在提交更改以供审核之前警告每个人重复。此外,由于 Kubernetes 的工作方式,检测逻辑不仅仅关注文件名。例如,apiVersion 包括 group 名和 version,具有 apiVersion 的对象 networking.k8s.io/v1beta1 和 networking.k8s.io/v1 和相同的命名空间和名称应被视为相同的对象,尽管 apiVersion 字符串不同。
这一事件是我们配置 CD 的宝贵经验。ArgoCD 允许添加特定的注释来防止删除某些资源。向我们所有的有状态资源添加 Prune=false 注释可确保 ArgoCD 在出现配置错误问题时保持这些资源完好无损。我们还将注解添加到 ArgoCD 管理的 Namespace 对象中,否则,ArgoCD 仍可能删除它所在的 Namespace,导致所有对象的级联删除。
我们还为 ArgoCD 应用程序对象添加了 FailOnSharedResource=true 选项。这使得 ArgoCD 在尝试将任何更改应用到由另一个 ArgoCD 应用程序管理或以前由另一个 ArgoCD 应用程序管理的对象之前失败。这确保了类似的错误,或将 ArgoCD 指向错误的集群或命名空间,将阻止它对现有对象造成任何更改。
虽然这些都是我们已经想要做出的改变,但这一事件促使我们实施它们以改进我们所有的自动化和流程。希望对我们经验的深入了解将帮助您制定有效的灾难恢复计划。
- END -