几个简单的步骤删除生产环境(以及如何修复)

导航至

本文最初发表于 The New Stack

这是那种让开发者惊出一身冷汗的噩梦。想象一下,早上醒来收到团队的消息,上面只写着:“我们丢失了一个集群”,但这根本不是梦。

InfluxDB Cloud 在 Kubernetes 上运行,Kubernetes 是一个云应用程序编排平台。我们使用自动化的 持续交付 (CD)系统来部署代码和配置更改到生产环境。在一个典型的工作日,工程团队向生产环境交付 5-15 个不同的更改。

为了将这些代码和配置更改部署到 Kubernetes 集群,团队使用了一个名为 ArgoCD 的工具。ArgoCD 读取 YAML 配置文件,并使用 Kubernetes API 使集群与 YAML 配置中指定的代码保持一致。

ArgoCD 在 Kubernetes 中使用自定义资源(称为 Applications 和 AppProjects)来管理源代码基础设施即代码仓库。ArgoCD 还管理这些仓库的文件路径以及特定 Kubernetes 集群和命名空间的部署目的地。

因为我们维护多个集群,所以我们也使用 ArgoCD 来管理自身,并管理所有不同 ArgoCD Applications 和 AppProjects 的定义。这是一种常见的开发方法,通常被称为“应用的应用”模式。

app-of-apps-pattern-OG

我们使用一种名为 jsonnet 的语言来创建 YAML 配置的模板。CD 系统检测 jsonnet 中的更改,将 jsonnet 转换为 YAML,然后 Argo 应用这些更改。在我们的事件发生时,单个应用程序的所有资源都保存在一个 YAML 文件中。

对象名称和目录结构遵循某些命名约定 (应用名称)(集群名称) 用于对象名称,env/(集群名称)/(应用名称)/yml 用于仓库中其定义的保存位置。例如,cluster01 中的 app01 定义为 app01-cluster01,其定义保存在路径 env/cluster01/app01/yml 下。

我们对基础设施即代码执行代码审查,其中包括检查生成的 YAML 并确保它在应用更新之前能够按预期运行。

发生了什么

这场磨难始于配置文件中的一行代码。团队中的某人在配置文件中创建了一个 PR,该 PR 向配置文件和渲染的 YAML 文件添加了几个新对象。

在本例中,添加的对象之一是新的 ArgoCD Application 和 AppProject。由于自动化中的错误,对象的名称不正确。它们应该被命名为 app02-cluster01,但实际上被命名为 app01-cluster01。代码审查忽略了 app01 和 app02 之间的差异,因此,在渲染时,这两个资源最终都出现在一个 YAML 配置文件中。

ArgoCD-AppProject

当我们合并包含错误命名对象的 PR 时,ArgoCD 读取了整个生成的 YAML 文件,并按照文件中列出的顺序应用了所有对象。结果,列出的最后一个对象“获胜”并被应用,这就是发生的情况。ArgoCD 用新的实例替换了之前的实例 app1。问题是 ArgoCD 删除的 app1 实例是 InfluxDB Cloud 的核心工作负载。

此外,新对象还创建了一个我们不希望在该集群上启用的额外工作负载。简而言之,当 ArgoCD 替换 app01 的实例时,该过程触发了整个生产环境的立即删除。

显然,这对我们的用户不利。当生产环境宕机时,所有 API 端点,包括所有写入和读取,都返回 404 错误。在停机期间,没有人能够收集数据,任务运行失败,外部查询无法工作。

灾难恢复 — 计划和初步尝试

我们立即着手解决问题,首先审查合并的 PR 中的代码。问题很难发现,因为它涉及到项目和应用程序名称之间的 ArgoCD 冲突。

我们的第一直觉是回滚更改,使一切恢复正常。不幸的是,这与有状态应用程序的工作方式不太一样。我们开始了回滚过程,但几乎立即停止了,因为回滚更改将导致 ArgoCD 创建应用程序的全新实例。这个新实例将不具有关于我们的用户、仪表板和原始实例拥有的任务的元数据。至关重要的是,新实例将不具有最重要的东西 — 我们客户的数据。

此时,值得一提的是,我们将 InfluxDB Cloud 集群中的所有数据存储在使用 reclaimPolicy: Retain 的卷中。这意味着即使我们管理的 Kubernetes 资源(如 StatefulSet 和/或 PersistentVolumeClaim (PVC))被删除,底层的 PersistentVolumes 和云中的卷也 不会 被删除。

我们考虑到了这个关键细节,制定了我们的恢复计划。我们必须手动重新创建所有底层的 Kubernetes 对象,例如 PVC。一旦新的对象启动并运行,我们需要从备份系统中恢复任何丢失的数据,然后让 ArgoCD 重新创建我们应用程序的无状态部分。

灾难恢复 — 恢复状态和数据

InfluxDB Cloud 将状态保存在系统的一些组件中,其他微服务会与之交互,包括

  • Etcd:用于元数据,它存在于与 Kubernetes 控制平面分离的专用集群上。
  • Kafka 和 Zookeeper:用于预写日志(WAL)。
  • 存储引擎:这包括 PVC 和用于持久性的对象存储。

团队首先恢复了 etcd 和我们的元数据。这可能是恢复过程中最简单的任务,因为 etcd 存储的数据集相对较小,因此我们能够快速启动并运行 etcd 集群。这对我们来说是一个轻松的胜利,使我们能够将所有注意力集中在更复杂的恢复任务上,例如 Kafka 和存储。

我们识别并重新创建了任何丢失的 Kubernetes 对象,这使卷(特别是 Persistent Volume 对象)重新上线并使其处于可用状态。一旦卷的问题得到解决,我们重新创建了 StatefulSet,这确保了所有 Pod 运行并在集群中同步。

下一步是恢复 Kafka,为此,我们还必须使 Zookeeper(它保存 Kafka 集群的元数据)处于健康状态。Zookeeper 卷也在事件中被删除。幸运的是,我们使用 Velero 每小时备份 Zookeeper,并且 Zookeeper 的数据不会经常更改。我们成功地从最近的备份中恢复了 Zookeeper 卷,这足以使其启动并运行。

为了恢复 Kafka,我们必须创建任何与 Kafka 的卷和状态相关的丢失对象,然后一次一个 Pod 地重新创建集群的 StatefulSet。我们决定禁用所有健康和就绪检查,以使 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 Cloud 产品。例如,Telegraf,我们的开源收集代理,默认执行重试。

最严重的问题是我们的监控和警报系统没有立即检测到这个问题。这就是为什么我们最初的反应是尝试回滚更改,而不是计划和执行深思熟虑的恢复过程。我们还缺少用于丢失部分或整个 InfluxDB Cloud 实例的运行手册。

作为此事件的结果,InfluxData 工程团队创建了专注于恢复状态的运行手册。我们现在有关于如何在类似情况发生时如何进行的详细说明,即,如果 Kubernetes 对象(如 Persistent Volume Claims)被删除,但底层磁盘和卷上的数据被保留。我们还确保我们所有环境中的所有卷都设置为保留数据,即使 PVC 对象被删除也是如此。

我们还改进了处理面向公众事件的流程。我们的目标是尽可能少地发生事件,这应该有助于我们解决未来可能出现的任何面向公众的平台问题。

在技术方面,我们意识到我们的系统应该阻止 PR 被合并,我们采取了多项措施来解决这个问题。我们更改了 InfluxDB 存储生成的 YAML 文件的方式,转向每个对象一个文件的方法。例如,etcd 服务的 v1.Service-(命名空间).etcd.yaml。将来,类似的 PR 将清楚地显示为对现有对象的覆盖,而不会被误认为是添加新对象。

我们还改进了我们的工具,以在生成 YAML 文件时检测重复项。现在,系统会在提交更改以供审查之前警告所有人存在重复项。此外,由于 Kubernetes 的工作方式,检测逻辑不仅仅查看文件名。例如,apiVersion 包括组名称和版本 — 具有 apiVersion networking.k8s.io/v1beta1 和 networking.k8s.io/v1 以及相同命名空间和名称的对象应被视为相同对象,尽管 apiVersion 字符串不同。

此事件是配置 CD 的宝贵教训。ArgoCD 允许添加特定的注释,以防止删除某些资源。向我们所有有状态资源添加 Prune=false 注释可确保 ArgoCD 在配置错误问题发生时保持这些资源完整。我们还将注释添加到 ArgoCD 管理的 Namespace 对象,否则,ArgoCD 将保留 StatefulSet,但仍可能删除它所在的 Namespace,从而导致所有对象的级联删除。

我们还为 ArgoCD Application 对象添加了 FailOnSharedResource=true 选项。这使 ArgoCD 在尝试对由另一个 ArgoCD 应用程序管理或先前管理的对象应用任何更改之前失败。这确保了类似的错误,或将 ArgoCD 指向错误的集群或命名空间,将阻止它对现有对象进行任何更改。

最后说明

虽然这些都是我们已经想要进行的更改,但该事件促使我们实施它们以改进我们所有的自动化和流程。希望对我们经验的深入探讨将帮助您制定有效的灾难恢复计划。