几步轻松删除生产环境(以及如何修复它)
作者:Wojciech Kocjan / 用例,产品,开发者
2022年6月24日
导航到
本文最初发表在 The New Stack。
这是一个让开发者冷汗直流的噩梦。想象一下,你醒来收到团队发来的消息,只说“我们丢失了一个集群”,但这根本不是梦。
InfluxDB Cloud 在 Kubernetes(一个云应用编排平台)上运行。我们使用自动化的 持续交付(CD)系统将代码和配置更改部署到生产环境。在典型的工作日,工程团队会将5-15个不同的更改部署到生产环境中。
为了将这些代码和配置更改部署到 Kubernetes 集群,团队使用了一个名为 ArgoCD 的工具。ArgoCD 读取 YAML 配置文件,并使用 Kubernetes API 使集群与 YAML 配置中指定的代码保持一致。
ArgoCD 使用 Kubernetes 中的自定义资源(称为 Applications 和 AppProjects)来管理源基础设施代码存储库。ArgoCD 还管理这些存储库的文件路径以及特定 Kubernetes 集群和命名空间的目标部署位置。
因为我们维护多个集群,我们也使用 ArgoCD 来监控自身,并管理所有不同 ArgoCD Applications 和 AppProjects 的定义。这是一种常见的发展方法,通常被称为“应用的应用”模式。
我们使用一种名为 jsonnet 的语言来创建 YAML 配置的模板。CD 系统检测 jsonnet 中的更改,将其转换为 YAML,然后 Argo 应用这些更改。在我们的事件发生时,单个应用程序的所有资源都保存在一个 YAML 文件中。
对象名称和目录结构遵循某些命名约定(应用名称)-(集群名称)对于对象名称,以及 env/(集群名称)/(应用名称)/yml,用于存储其定义的存储库中的位置。例如,集群01中的 app01 被定义为 app01-cluster01,其定义保存在路径 env/cluster01/app01/yml 下。
我们对基础设施代码进行代码审查,这包括检查生成的 YAML,确保在应用更新之前它能按预期工作。
发生了什么
这次折磨始于配置文件中的一行代码。团队中的某个人创建了一个PR,将几个新的对象添加到配置文件和渲染的YAML文件中。
在这种情况下,添加的对象之一是新的ArgoCD应用程序和AppProject。由于自动化错误,对象的名称错误。它们的名称应该是 app02-cluster01,但它们实际上是 app01-cluster01。代码审查没有注意到app01和app02之间的区别,所以当渲染时,这两个资源都最终出现在一个YAML配置文件中。
当我们合并了包含错误名称对象的PR时,ArgoCD读取了整个生成的YAML文件,并按文件中列出的顺序应用了所有对象。结果是,最后列出的对象“胜出”并得到应用,这就是发生的事情。ArgoCD用新的实例替换了之前的app1。问题是ArgoCD删除的app1实例是InfluxDB Cloud的核心工作负载。
此外,新创建的对象在集群上创建了一个我们不想启用的工作负载。简而言之,当ArgoCD替换app01的实例时,这个过程触发了对整个生产环境的立即删除。
显然,这对我们的用户来说并不好。当生产出现问题时,所有API端点,包括所有写和读操作,都返回404错误。在故障期间,没有人能够收集数据,任务无法运行,外部查询也无法工作。
灾难恢复——规划和初步尝试
我们立即开始着手解决这个问题,首先审查合并的PR中的代码。由于涉及到ArgoCD项目中应用程序名称之间的冲突,问题很难发现。
我们的第一个直觉是撤销更改以恢复正常。不幸的是,这正是有状态应用程序的工作方式。我们开始撤销过程,但几乎立即停止,因为撤销更改会导致ArgoCD创建我们应用程序的一个全新实例。这个新实例将不会有原始实例中关于我们的用户、仪表板和任务的相关元数据。关键的是,新实例将没有最重要的东西——我们的客户数据。
在此阶段,值得一提的是,我们所有的数据都存储在具有reclaimPolicy: Retain的卷中,使用InfluxDB Cloud集群。这意味着即使我们管理的Kubernetes资源(如StatefulSet和/或PersistentVolumeClaim [PVC])被删除,底层的PersistentVolumes和云中的卷也不会被删除。
我们考虑到这个关键细节创建了恢复计划。我们必须手动重新创建所有底层的Kubernetes对象,如PVC。一旦新对象启动并运行,我们需要从备份系统中恢复任何缺失的数据,然后让ArgoCD重新创建我们应用程序的无状态部分。
灾难恢复——恢复状态和数据
InfluxDB Cloud在系统的几个组件中保持状态,这些组件与其他微服务交互,包括
- Etcd:用于元数据,它存在于一个专门的集群中,该集群与Kubernetes控制平面分开。
- Kafka和Zookeeper:用于写前日志(WALs)。
- 存储引擎:这包括PVC和用于持久性的对象存储。
团队首先恢复了etcd和我们的元数据。这可能是恢复过程中最直接的任务,因为etcd存储的数据集相对较小,所以我们能够快速启动etcd集群。这对我们来说是一个容易的胜利,使我们能够将所有注意力集中在更复杂的恢复任务上,如Kafka和存储。
我们识别并重新创建了任何缺失的Kubernetes对象,这使得卷(特别是持久卷对象)重新上线并处于可用状态。一旦解决了卷的问题,我们就重新创建了StatefulSet,确保所有Pod运行并保持集群同步。
下一步是恢复Kafka,为此我们还需要将Zookeeper恢复到健康状态,Zookeeper用于存储Kafka集群的元数据。Zookeeper卷也在事故中被删除了。幸运的是,我们使用Velero每小时备份Zookeeper,而Zookeeper的数据并不经常变化。我们成功从最近的备份中恢复了Zookeeper卷,这对于让它重新运行是足够的。
要恢复Kafka,我们必须创建与卷和Kafka状态相关的任何缺失对象,然后逐个重新创建集群的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云服务。例如,我们的开源收集代理Telegraf默认执行重试操作。
最显著的问题是我们的监控和警报系统没有立即检测到这个问题。这就是为什么我们的初始反应是尝试回滚更改,而不是计划并执行一个深思熟虑的恢复过程。我们也缺乏丢失部分或整个InfluxDB云实例的运行手册。
作为这次事件的后果,InfluxData工程团队创建了专注于恢复状态的运行手册。我们现在有了详细的操作指南,如果在类似情况下发生,即如果Kubernetes对象(如持久卷声明)被删除,但底层磁盘和卷上的数据得到保留,我们将如何操作。我们还确保了所有环境中的所有卷都设置为保留数据,即使PVC对象被删除。
我们还改进了处理面向公众事件的流程。我们希望尽可能少发生事件,这应该有助于我们应对未来可能面向公众的任何平台问题。
在技术方面,我们意识到我们的系统应该阻止PR合并,我们采取了多项措施来解决这个问题。我们改变了InfluxDB存储生成的YAML文件的方式,转向每个文件一个对象的方法。例如,etcd Service的v1.Service-(namespace).etcd.yaml。在未来,类似的PR将明确显示为覆盖现有对象,而不是错误地被认为是新对象的添加。
我们还改进了在生成YAML文件时检测重复的工具。现在,系统在提交更改以供审查之前会提醒所有人注意重复项。此外,由于Kubernetes的工作方式,检测逻辑不仅查看文件名。例如,apiVersion包括组名和版本——具有apiVersion networking.k8s.io/v1beta1和networking.k8s.io/v1以及相同命名空间的名称的对象应被视为同一对象,尽管apiVersion字符串不同。
这次事件是我们配置CD的一个宝贵教训。ArgoCD允许添加特定的注释来防止删除某些资源。将Prune=false注释添加到所有有状态资源确保ArgoCD在出现配置问题的情况下保留这些资源。我们还将其添加到由ArgoCD管理的命名空间对象,否则,ArgoCD将留下StatefulSet,但可能仍然删除其所在的命名空间,从而导致所有对象的级联删除。
我们还为ArgoCD应用程序对象添加了FailOnSharedResource=true选项。这使ArgoCD在尝试对由另一个ArgoCD应用程序管理或曾经管理的对象应用任何更改之前失败。这确保了类似错误,或指向错误的集群或命名空间,都不会导致它对现有对象造成任何更改。
最后一点
虽然这些改变都是我们原本想要进行的,但这次事件促使我们尽快实施,以提升我们的自动化和流程。希望我们对经验的深入探讨能帮助您制定有效的灾难恢复计划。