博客

使用 GitOps 管理数千个 Edge Kubernetes 集群

F5 缩略图
F5
2019 年 12 月 18 日发布

在 Volterra,SRE 团队的工作是运营一个基于 SaaS 的全球边缘平台。 我们必须解决管理处于各种状态(即在线、离线、管理员关闭等)的大量应用集群的各种挑战,我们通过利用 Kubernetes 生态系统和使用 GitOps 的声明式拉动模型的工具来实现这一目标。

在这篇博客中,我们将描述:

使用 GitOps 有效地管理和监控大量基础设施(物理或云主机)和 K8s 集群

  1. 我们为解决 CI/CD 问题而构建的工具
  2. 对象编排和配置管理
  3. 跨基础设施和 K8s 集群的可观察性

我们将深入探讨大规模(3000 个边缘站点)的经验教训,我在最近于圣地亚哥举行的 Cloud Native Rejekts 演讲中也谈到了这一点。

TL;DR(摘要)

  1. 我们找不到一个简单且开箱即用的解决方案,可用于在公共云、本地或游牧位置部署和操作数千个(甚至数百万个)应用和基础设施集群
     
  2. 该解决方案需要为主机(物理或云)、Kubernetes 控制平面、应用工作负载以及各种服务的持续配置提供生命周期管理。 此外,该解决方案需要满足我们的 SRE 设计要求——声明式定义、不可变生命周期、gitops 以及无法直接访问集群。
     
  3. 在评估了各种开源项目(例如 Kubespray+Ansible(用于 Kubernetes 部署)或 Helm/Spinnaker(用于工作负载管理))之后,我们得出结论:如果不在每个边缘站点增加大量软件膨胀,这些解决方案都无法满足我们的上述要求。 因此,我们决定构建自己的基于 Golang 的软件守护进程,执行主机(物理或云)、Kubernetes 控制平面和应用工作负载的生命周期管理。
     
  4. 当我们将系统扩展到 3000 个集群甚至更多(在单个租户内)时,我们对公共云提供商、软件守护进程的可扩展性、操作工具和可观察性基础设施的所有假设都系统地崩溃了。 为了克服这些挑战,我们需要重新设计一些软件组件。

边缘的定义

  • 客户边缘 (CE) — 这些是云中的客户位置(如 AWS、Azure、GCP 或私有云)、本地位置(如工厂车间、石油/天然气设施等)或游牧位置(如汽车、机器人等)。 CE 由 Volterra SRE 团队管理,但客户也可以根据需要在他们选择的地点进行部署。
  • 区域边缘 (RE) — 这些是主要大都市市场的主机托管设施中的 Volterra 接入点 (PoP),与我们自己高度互联的私有主干网互连。 这些区域边缘站点还用于安全地连接客户边缘 (CE) 位置和/或向公共互联网公开应用服务。 RE 站点由 Volterra 基础设施运营 (Infra SRE) 团队全面管理和拥有。
管理01
图 1: 系统概述

上面的架构图(图 1)显示了我们的 RE 和 CE 之间的逻辑连接,其中每个 CE 都与最近的 RE 建立冗余(IPSec 或 SSL VPN)连接。

边缘管理的要求

大约两年前,当我们开始设计我们的平台时,我们的产品团队要求我们解决以下挑战:

  1. 系统可扩展性——我们的客户需要我们支持数千个(最终是数百万个)客户边缘站点,这与在云区域运行少量 Kubernetes 集群有很大不同。 例如,我们的一个客户拥有约 17,000 家便利店,另一个客户运营着 20,000 多个充电站。 这种规模意味着我们必须以与处理几个集群截然不同的方式构建我们的工具。
  2. 零接触部署——任何人都可以部署新站点,而无需对硬件、软件或 Kubernetes 有太多了解。 边缘站点需要像一个黑匣子一样运行,可以打开、呼叫总部并上网。
  3. 车队管理——简化数千个站点和工作负载的管理,无需单独处理。 在请求更改或升级时,任何站点都可能离线或不可用。 因此,网站上线时必须获取更新。
  4. 容错——即使任何组件出现故障,边缘站点也必须能够运行。 一切都必须进行远程管理,并在出现故障时提供恢复出厂设置或重建站点等功能。 我们不得不假设该站点没有物理访问权限。

设计原则(没有 Kubectl! 没有 Ansible! 没有包裹!)

考虑到我们运营高度分布式系统的要求和挑战,我们决定制定几个原则,让 SRE 团队遵循这些原则,以减少下游问题:

  1. 声明性定义——整个系统必须以声明性的方式描述,因为这使我们能够创建一个简单的抽象模型并对该模型进行验证。
     
  2. 不可变生命周期管理——过去,我们使用可变的 LCM 工具(例如 Ansible、Salt 或 Puppet)来安装大型私有云。 这一次,我们希望保持基础操作系统非常简单,并尝试将所有内容作为容器发送,而无需包管理或配置管理工具。
     
  3. GitOps——为管理 Kubernetes 集群提供了标准的操作模型。 它还可以帮助我们获得开箱即用的批准、审计和变更工作流程,而无需构建额外的工作流管理系统。 因此我们决定一切都必须通过 git 进行。
     
  4. 没有 kubectl — 这是最重要的原则之一,因为没有人被允许直接访问应用集群。 因此,我们删除了在单个边缘集群内运行 kubectl 或使用从中心位置(包括集中式 CD 系统)运行的脚本的能力。 采用推送方式的集中式 CD 系统适用于数十个集群,但肯定不适合无法保证 100% 网络可用性的数千个集群。
     
  5. 没有炒作的技术(或工具) ——我们过去的经验表明,许多流行的开源工具并没有达到他们的炒作水平。 虽然我们评估了用于交付基础设施、K8s 和应用工作负载的几个社区项目(例如 Helm、Spinnaker 和 Terraform),但我们最终只使用 Terraform 作为虚拟基础设施部分,并开发了自定义代码,我们将在本博客的以下部分中描述。

站点生命周期管理

作为边缘站点生命周期管理的一部分,我们必须解决如何配置主机操作系统、进行基本配置(例如用户管理、证书颁发机构、大页面等)、启动 K8s、部署工作负载以及管理正在进行的配置更改。

我们考虑但最终拒绝的选项之一是使用 KubeSpray+Ansible(用于管理操作系统和部署 K8s)和 Helm/Spinnaker(用于部署工作负载)。 我们拒绝这个的原因是,这将要求我们管理 2-3 个开源工具,然后进行重大修改以满足我们的要求,随着我们添加更多功能(如边缘集群的自动扩展、对安全 TPM 模块的支持、差异升级等),我们的要求会不断增长。

由于我们的目标是保持简单并尽量减少直接在操作系统中(Kubernetes 之外)运行的组件数量,因此我们决定编写一个名为 Volterra Platform Manager(VPM)的轻量级 Golang 守护程序。 这是操作系统中唯一的 systemd Docker 容器,它就像一把瑞士军刀,具有多种功能:

主机生命周期

VPM 负责管理主机操作系统的生命周期,包括安装、升级、修补、配置等。 需要配置的方面有很多(例如大页面分配、/etc/hosts 等)

  1. 操作系统升级管理——不幸的是,优势不仅仅在于 Kubernetes,我们还必须管理内核和操作系统的版本。 我们的优势基于 CoreOS(或根据客户需求的 CentOS),具有主动和被动分区。 当计划升级时,更新总是会下载到被动分区。 重新启动是更新的最后一步,其中主动分区和被动分区进行交换。 唯一敏感的部分是重启策略(对于多节点集群),因为所有节点不能同时重启。 我们实现了自己的 etcd 重启锁(在 VPM 中),其中集群中的节点会逐个重启。
  2. 用户对操作系统的访问权限管理——我们需要远程限制用户及其对 ssh 和控制台的访问。 VPM 执行所有操作,例如 ssh CA 轮换。
  3. 由于我们开发了自己的L3-L7 数据路径,因此需要我们根据硬件类型(或云虚拟机)在主机操作系统中配置 2M 或 1G 大页面

Kubernetes 生命周期

管理为 Kubernetes 清单提供生命周期。 我们决定不使用 Helm,而是使用 K8s client-go 库,我们将其集成到 VPM 中并使用了该库的几个功能:

  1. 乐观与悲观部署- 此功能允许我们对需要等待其恢复健康的应用进行分类。 它只是监视 K8s 清单ves.io/deploy:optimistic中的注释。

    乐观= 创造资源而不是等待状态。 它与kubernetes apply命令非常相似,您不知道实际的 pod 是否成功启动。

    悲观= 等待 Kubernetes 资源的状态。 例如,部署会等待所有 pod 准备就绪。 这类似于新的kubectl wait命令。

  2. 预更新操作,例如预拉取——有时不可能依赖 K8s 滚动更新,尤其是在网络数据平面发货时。 原因是旧的 pod 被销毁了,然后新的 pod 被拉出来了。 但是在数据平面的情况下,您会失去网络连接。 因此,无法拉取新的容器镜像,并且 pod 将永远不会启动。 带有图像列表的元数据注释 ves.io/prepull 在应用 K8s 清单之前触发拉取操作。
  3. 如果应用失败则重试和回滚。 当 K8s API 服务器出现间歇性中断时,这是一种非常常见的情况。

正在进行的配置

除了与 K8s 清单相关的配置之外,我们还需要通过其 API 配置各种 Volterra 服务。 一个例子是 IPsec/SSL VPN 配置——VPM 从我们的全局控制平面接收配置并在各个节点中对其进行编程。

恢复出厂设置

此功能允许我们远程将盒子重置为原始状态并重新执行整个安装和注册过程。 对于恢复需要控制台/物理访问的站点来说,这是一项非常关键的功能。

尽管 K8s 生命周期管理对于很多人来说似乎是一个很大的讨论话题,但对于我们的团队来说,它可能只占整体工作量的 40-50%。

零接触配置

在任何位置(云、本地或游牧边缘)对边缘站点进行零接触配置都是至关重要的功能,因为我们不能期望访问单个站点,也不希望配备那么多 Kubernetes 专家来安装和管理单个站点。 它只是无法扩展到数千。

下图(图 2)显示了 VPM 如何参与注册新站点的过程:

管理02
图 2 — 零接触配置流程
  1. 一旦通电,在 CE 上运行的 VPM(由绿色框表示)将向我们的全局控制平面(GC)提供注册令牌以创建新的注册。 注册令牌作为云虚拟机的 cloud-init 的一部分提供,可能是启动过程中人工输入的密钥,也可能是在边缘硬件的 TPM 中编程的密钥。
  2. GC 接收带有令牌的请求,这使它可以在租户(在令牌中编码)下创建新的注册。 客户操作员可以立即在地图上看到新的边缘站点,并通过输入名称和其他配置参数来批准它。
  3. GC 内的 VP-Controller 生成配置(例如,决定谁是 K8s 主机、从机等)和 etcd、K8s 和 VPM 的证书。
  4. VPM 开始引导站点,包括下载 docker 镜像、配置 hugepages、安装 K8s 集群以及启动 Volterra 控制平面服务。
  5. VPM 配置到最近的两个区域边缘站点的冗余隧道(IPSec/SSL VPN),用于跨站点和公共网络的数据流量和互连。

如您所见,整个过程是完全自动化的,用户不需要了解任何详细配置或执行任何手动步骤。 让整个设备进入在线状态并准备好为客户应用和请求提供服务大约需要 5 分钟。

基础设施软件升级

升级是我们必须解决的最复杂的问题之一。 让我们定义一下边缘站点正在升级的内容: 

  • 操作系统升级——涵盖内核和所有系统包。 任何操作过标准 Linux 操作系统发行版的人都知道跨小版本升级的痛苦(例如从 Ubuntu 16.04.x 升级到 16.04.y),以及跨主要版本升级的更大痛苦(例如从 Ubuntu 16.04 升级到 18.04)。 对于数千个站点的情况,升级必须是确定性的,并且不能在不同站点之间表现不同。 因此,我们选择了 CoreOS 和 CentOS Atomic,它们能够通过 2 个分区和路径上的只读文件系统进行 A/B 升级。 这使我们能够通过切换启动顺序分区立即恢复,并保持操作系统一致性,而无需维护操作系统包。 但是,我们不能再通过安装新的包来升级系统中的单个组件,例如 openssh 服务器。 对各个组件的更改必须作为新的不可变的操作系统版本发布。
     
  • 软件升级——包括作为 K8s 工作负载运行的 VPM、etcd、Kubernetes 和 Volterra 控制服务。 正如我已经提到的,我们的目标是让 K8s 内部的所有内容都作为 systemd 容器运行。 幸运的是,除了 3 个服务之外,我们能够将所有内容转换为 K8s 工作负载: VPM、etcd 和 kubelet。

有两种已知方法可用于向边缘站点提供更新: 

  1. 基于推送——推送方法通常由集中式 CD(持续交付)工具完成,例如 Spinnaker、Jenkins 或基于 Ansible 的 CM。 在这种情况下,中央工具需要能够访问目标站点或集群,并且必须能够执行该操作。
  2. 基于拉取— 基于拉取的方法独立获取升级信息,无需任何集中式交付机制。 它的扩展性更好,并且无需将所有站点的凭证存储在一个地方。

我们的升级目标是最大限度地提高简单性和可靠性——类似于标准手机升级。 此外,升级策略还必须满足其他考虑因素——升级环境可能仅限于站点运营商,或者设备可能由于连接问题等原因处于离线或不可用状态。 这些要求可以通过拉动方法更轻松地满足,因此我们决定采用它来满足我们的需求。

GitOps

此外,我们选择 GitOps,因为它可以更轻松地为我们的 SRE 团队提供管理 Kubernetes 集群、工作流和审计变更的标准操作模型。

为了解决数千个站点的扩展问题,我们提出了图 3 所示的 SRE 架构:

管理03
图 3 — GitOps 流程

首先,我想强调一下,我们使用 Git 不仅仅是为了存储状态或清单。 原因是我们的平台不仅要处理 K8s 清单,还要处理正在进行的 API 配置、K8s 版本等。 在我们的案例中,K8s 清单约占整个声明性配置的 60%。 为此,我们必须在其基础上提出自己的 DSL 抽象,并将其存储在 git 中。 此外,由于 git 不提供 API 或任何参数合并功能,我们必须为 SRE 开发额外的 Golang 守护程序: 配置 API、执行器和 VP 控制器。

让我们了解一下使用我们的 SaaS 平台在客户端发布新软件版本的工作流程: 

  1. 操作员决定发布新版本并针对 git 模型打开合并请求(MR)
     
  2. 一旦此 MR 被批准并合并,CI 就会触发操作将 git 模型配置加载到我们的 SRE Config-API 守护进程中。 该守护进程有几个用于参数合并、内部DNS配置等的API。
     
  3. Config-API 由 Executor 守护进程监视;在 git 更改加载后,它会立即开始使用注释中的版本呈现最终的 K8s 清单。 然后将这些清单上传到 Artifact 存储(类似 S3)的路径ce01-site// .yml 下
     
  4. 一旦新版本被渲染并上传到工件存储,执行器就会生成一个新状态,其中包含可供客户 API 使用的版本;这与手机中可用的新版本非常相似
     
  5. 客户(或运营商)可以安排将其站点更新到最新版本,并将此信息传递给 VP-Controller。 VP-Controller 是负责站点管理的守护进程,包括配置、退役或迁移到其他位置。 这已在零接触配置中部分解释过,并负责通过 mTLS API 更新边缘站点
     
  6. 图中的最后一步发生在边缘站点上 - 一旦 IPSec/SSL VPN 连接建立,VP-Controller 就会通知边缘的 VPM 下载新版本更新;但是,如果连接中断或出现间歇性问题,VPM 会每 5 分钟轮询一次更新
     
  7. 新的 K8s 清单和配置被获取并部署到 K8s 中。 使用上一节中介绍的悲观部署特性,VPM 会等待所有 Pod 准备就绪
     
  8. 作为最后一步,VPM 将升级状态发送回 VP 控制器,并将其作为状态推送到客户 API。

您可以在此处观看整个工作流程的演示:

从 3000 个边缘站点测试中得到的经验教训

在前面的部分中,我们描述了如何使用我们的工具来部署和管理边缘站点的生命周期。 为了验证我们的设计,我们决定构建一个拥有三千个客户边缘站点的大型环境(如图 4 所示)

管理04
图 4 — 3000 个客户边缘站点

我们使用 Terraform 在 AWS、Azure、Google 和我们自己的本地裸机云上配置了 3000 台虚拟机来模拟规模。 所有这些虚拟机都是独立的 CE(客户边缘站点),它们建立了到我们的区域边缘站点(又名 PoP)的冗余隧道。

下面的屏幕截图来自我们的 SRE 仪表板,显示了圆圈大小所代表位置的边缘数量。 在截屏时,我们有大约 2711 个健康的边缘站点和 356 个不健康的边缘站点。

管理05
图 5–3000 客户边缘站点部署

主要发现: 运营

作为扩展的一部分,我们确实发现了配置和操作方面的一些问题,需要我们对软件守护进程进行修改。 此外,我们在与云提供商时遇到了很多问题,导致需要开具多张支持单 — 例如,API 响应延迟、无法在单个区域获取超过 500 台虚拟机等。 

  1. 优化 VP-Controller — 最初,我们串行处理注册,每个处理大约需要两分钟,因为我们需要为 etcd、kubernetes 和 VPM 铸造各种证书。 我们通过使用更高熵的预生成密钥以及与大量工作者并行来优化这次。 这使得我们能够在不到 20 秒的时间内处理 100 个站点的注册。 我们仅需消耗 VP-Controller 上的一个 vCPU 和 2GB RAM 便能为所有 3000 个边缘站点提供服务。
     
  2. 优化 Docker 镜像交付——当我们开始扩展时,我们意识到边缘站点传输的数据量非常巨大。 每个边缘下载约 600MB(乘以 3000),因此总传输的数据为 1.8TB。 此外,我们在测试期间多次重建边缘站点,因此这个数字实际上会大得多。 因此,我们必须优化 Docker 镜像的大小,并使用预先拉取的 Docker 镜像构建云和 iso 镜像以减少下载。 虽然我们仍在使用公共云容器注册服务,但我们正在积极设计通过 RE(PoP)分发我们的容器注册中心并执行增量(二进制差异)升级。
     
  3. 优化全局控制数据库操作——我们所有的 Volterra 控制服务都基于使用 ETCD 作为数据库的 Golang 服务框架。 每个站点都表示为一个配置对象。 每个站点配置对象都有几个 StatusObjects,例如软件升级、硬件信息或 ipsec 状态。 这些 StatusObject 由各种平台组件生成,它们都在全局配置 API 中引用。当我们达到 3000 个站点时,我们必须在对象架构中进行某些优化。 例如,限制全局配置 API 接受的 StatusObjects 类型的数量,或者我们决定将它们移动到专用的 ETCD 实例以降低配置对象 DB 过载的风险。 这使我们能够为配置数据库提供更好的可用性和响应时间,并且还允许我们在发生故障时重建状态数据库。 另一个优化的例子是停止在所有租户中执行不必要的站点对象列表操作或引入二级索引以减少数据库的负载。

主要发现: 可观察性

当我们扩展系统时,分布式系统的可观察性带来了更大的挑战。

最初,对于指标,我们从 Prometheus 联合开始 - 全局控制中的中央 Prometheus 在区域边缘(RE)中联合 Promethei,它从其连接的 CE 中抓取其服务指标并联合指标。 顶层的 Prometheus 评估警报并作为进一步分析的指标源。 我们很快达到了这种方法的极限(大约 1000 个 CE),并试图将 CE 数量不断增长的影响降至最低。 我们开始为直方图和其他高基数指标生成预先计算的系列。 这为我们节省了一两天的时间,然后我们必须使用白名单来衡量指标。 最后,我们能够将每个 CE 站点的时间序列指标数量从大约 60,000 个减少到 2000 个。

最终,在继续扩展到 3000 个以上 CE 站点并在生产中运行多日之后,很明显这是不可扩展的,我们不得不重新考虑我们的监控基础设施。 我们决定放弃顶层 Prometheus(全局控制),并将每个 RE 中的 Prometheus 分成两个独立的实例。 一个负责抓取本地服务指标,另一个负责联合 CE 指标。 两者都会生成警报并将指标推送到 Cortex 中的中央存储。 Cortex 用于分析和可视化源,而不是核心监控警报流程的一部分。 我们测试了几种集中式指标解决方案,即 Thanos 和 M3db,发现 Cortex 最适合我们的需求。

管理06
图 6 — 指标收集架构

下面的截图(图 7)显示了在 3000 个端点时抓取 prometheus-cef 的内存消耗情况。 有趣的是它消耗了 29.7GB 的 RAM,但考虑到系统规模,这个数字实际上并没有那么多。 可以通过将抓取操作拆分为多个或将远程写入 Cortex 直接移动到边缘本身来进一步优化。

管理07
图 7 — RE 站点的资源利用率

下一个屏幕截图(图 8)显示了在这种规模下,Cortex 摄取器(最大 19GB RAM)和分发器需要多少内存和 CPU 资源。 Cortex 最大的优势是水平扩展,与必须垂直扩展的 Prometheus 相比,它使我们能够添加更多副本。

管理08
图 8 — 全局控制下的资源利用率

对于 CE 和 RE 中的日志记录基础设施,我们每个节点使用 Fluentbit 服务来收集服务和系统日志事件并将其转发到连接的 RE 中的 Fluentd。 Fluentd 将数据转发到 RE 中的 ElasticSearch。 Elastalert 评估来自 ElasticSearch 的数据,并设置规则来创建 Alertmanager 警报。 我们正在使用从 Elastalert 到 Alertmananger 的自定义集成来生成与 Prometheus 生成的相同标签的警报。

我们的监测历程中的关键点:

  • 使用新的 Prometheus 联合过滤器删除未使用的指标和标签

    - 最初,每个 CE 大约有 50,000 个时间序列,平均有 15 个标签

    - 我们将其优化到平均每 CE 2000 个

    指标名称的简单白名单和标签名称的黑名单

  • 从全局 Prometheus 联合迁移到 Cortex 集群

    - 集中式 Prometheus 抓取了所有 RE 和 CE 的 Prometheus

    - 公元 1000 年,管理指标数量已变得难以为继

    - 目前,我们在每个 RE 上都有 Prometheus(与连接的 CE 的 Promethei 联合),并将 RW 连接到 Cortex

  • Elasticsearch 集群和日志

    - 分散式日志架构

    - Fluentbit 作为每个节点上的收集器,将日志转发到 RE 中的 Fluentd(聚合器)

    - 每个 RE 中都部署了 ElasticSearch,使用远程集群搜索从单个 Kibana 实例查询日志

概括

我希望本博客能让您深入了解管理全球部署的数千个边缘站点和集群需要考虑的所有事项。 尽管我们已经能够满足并验证大多数初始设计要求,但我们仍有许多改进空间……