介绍
在 ClickHouse Cloud,我们喜欢 Kubernetes,我们在 Kubernetes 中运行我们的客户 ClickHouse 集群(服务器和守护程序)。我们使用 Elastic Kubernetes Service (EKS) 来托管 AWS 中的 ClickHouse 集群。来自不同 ClickHouse 集群的服务器 Pod 可以调度到同一个 EKS 节点上。我们使用 Kubernetes 命名空间 + Cilium 来进行隔离。随着 EKS 集群在新的区域中进行配置以支持我们的客户,我们的集群规模一直在显著增长,EC2 实例的基础设施成本也随之增加。
为了优化成本,我们分析了我们的 EKS 节点利用率。EC2 实例按小时计费,而不是按使用量计费,因此节点/集群利用率低下意味着我们浪费了钱。通过提高利用率并减少所需的 EC2 节点总数,可以降低成本。继续阅读了解我们如何改进 Pod 分配并节省数百万美元。
评估 EKS 节点利用率
为了尽可能高效地利用资源,我们进行了一项练习,以确定 Pod 的分配方式。我们分析了 EKS 集群节点中的 CPU/内存利用率。下面的屏幕截图来自 eks-node-viewer,这是一个可视化节点资源利用率的工具。
图片 1 显示了我们较大的 EKS 集群之一中的 CPU 利用率百分比。整个集群的 CPU 利用率约为 50%。此外,许多大型且昂贵的节点(如图片 2 所示)未得到充分利用。根据我们集群的 eks-node-viewer
结果,我们得出结论,服务器 Pod 在节点之间没有紧密地打包。两个本来可以调度到同一个节点上的 ClickHouse Pod 反而调度到了不同的节点上,导致我们需要更多数量的 EC2 节点来支持相同数量的 Pod,每个节点的资源利用率降低,集群成本增加。我们集群中很多节点的 CPU/内存资源大部分处于闲置状态,但我们仍在为这些节点付费。
根本原因分析
经过调查,我们确定了低利用率的根本原因。
- Kubernetes 默认调度程序使用 LeastAllocated 评分策略来对节点进行评分,当将挂起的 Pod 调度到可用节点时。LeastAllocated 评分策略偏向于具有更多可用资源的节点,这会导致 Pod 在集群节点上稀疏分布。
- 这种 LeastAllocated 评分策略使得节点缩容变得非常不可能。想象一下,在一个节点上终止了一个旧 Pod。然后,该节点将被调度程序优先考虑以调度新 Pod。因此,节点利用率很难降至集群自动伸缩器的阈值以下,无法进行回收。
同时,我们对新解决方案还有三个额外的要求和约束
- 提高整个集群的资源(CPU/内存)利用率(并减少 EC2 的规模和成本)。
- 不影响客户体验,例如增加 ClickHouse 实例配置时间。
- 将对客户正在运行的实例的干扰降至最低。
考虑到这些因素,我们探索了一些可能的解决方案。
调整集群自动伸缩器和过度配置(已放弃)
一种显而易见的方法是降低 集群自动伸缩器缩容阈值。但是,这意味着更多的客户 Pod 将频繁地被驱逐,这无法满足要求 #3(对客户查询的干扰)。
我们还简要考虑了通过调整资源请求和限制来过度配置节点。但是,过度配置资源会导致竞争。但是,无论是 CPU 竞争(查询被限制)还是内存竞争(可能会被 OOM 杀死)对于满足我们的客户体验都是不可接受的。
主动打包 Pod(已放弃)
我们还考虑添加注释 cluster-autoscaler.kubernetes.io/safe-to-evict 到 ClickHouse Pod。这允许 Kubernetes 集群自动伸缩器在节点利用率降至某个阈值以下时驱逐 Pod。但是,Pod 驱逐可能会给我们的客户带来干扰。例如,长时间运行的查询将被中断。
我们已经为 ClickHouse 服务器配置了 PodDisruptionBudget,以限制在一个 StatefulSet 中,任何时候只有一个 Pod 可用。但根据我们的经验,让集群自动伸缩器在随机时间终止一些 Pod 对于像 ClickHouse 这样的有状态工作负载来说仍然太具有破坏性了。
因此,我们决定不选择这种方法。
bin-packing 使用 MostAllocated 评分策略的 Pod
相反,我们决定将 kube-scheduler(默认 Kubernetes 调度程序)的评分策略从 LeastAllocated 更改为 MostAllocated,以更有效地打包我们的集群。该解决方案对我们的 Pod 实现了 bin-packing 范式。为什么这有帮助?
- 当需要创建一个新 Pod 时,调度程序现在会优先选择利用率更高的节点。这使得集群的整体利用率随着时间的推移而提高。
- 当旧 Pod 在节点上被终止时,该节点不太可能被考虑,因此更有可能达到较低的利用率阈值。最终,集群自动伸缩器可以从集群中移除该节点,从而降低总成本。
该策略是 Kubernetes 中默认启用的 NodeResourcesFit 插件的一部分。启用此设置后,在 Pod 调度阶段,Kubernetes 调度程序将执行以下操作
- 首先,它会识别集群中具有可用资源的节点,这些资源如 Pod 请求规范中所指定。这是“过滤阶段”。
- 接下来,它将按其组合资源利用率(CPU 和内存)的降序对过滤后的节点进行排序。这称为“评分阶段”。
- 为 Pod 选择的节点始终是能够容纳 Pod 且资源利用率最高的节点。
我们找到了一个满足所有需求的解决方案,因此决定研究如何在 EKS 中指定此调度策略。
EKS 设置支持 kube-scheduler 自定义
当我们开始研究如何在 EKS 中为 kube-scheduler 设置此策略时,我们发现 EKS 不支持通过 EKS 设置/配置自定义 kube-scheduler。这个功能已被许多用户提出请求,但目前 AWS 尚未表明会在近期添加支持。由于我们无法通过 EKS 设置来实现,因此选择在我们自己的 Kubernetes 集群中设置自定义调度器。
使用 most-allocated
评分策略的自定义调度器
为了在我们的集群中设置 Pod 的自定义调度器,我们主要遵循了Kubernetes 提供的便捷指南。Kubernetes 允许您创建自己的调度器二进制文件,尽管在我们的情况下并不需要,因为现有的 kube-scheduler 镜像(具有 most-allocated
评分策略)满足了我们的需求。为了在我们的集群中创建此调度器,我们采取了以下步骤:
- 将 kube-scheduler 镜像构建并部署到我们的容器注册表。我们没有找到托管此镜像的公共注册表。我们维护此镜像并不理想,但目前没有其他解决方案。
- 如指南中所述,创建调度器的部署。在 configMap 中,我们为评分策略提供了相关设置 - ‘MostAllocated’,以及在考虑分配时 CPU/内存的权重。configMap 的配置文件如下所示:
profiles:
- pluginConfig:
- args:
apiVersion: kubescheduler.config.k8s.io/v1beta3
kind: NodeResourcesFitArgs
scoringStrategy:
resources:
- name: cpu
weight: 1
- name: memory
weight: 1
type: MostAllocated
name: NodeResourcesFit
plugins:
score:
enabled:
- name: NodeResourcesFit
weight: 1
schedulerName: <schedulerName>
- 为了确保高可用性,我们选择在自定义调度器部署中定义三个 Pod,并启用领导选举,这样只有一个 Pod 会进行主动调度,而另外两个 Pod 处于待机状态。
- 我们将此调度器部署到我们的集群,然后通过在PodSpec中指定 schedulerName,将我们的 Pod 更新为使用此 most-allocated 调度器。
通过此设置,我们可以确保调度吞吐量与现有的 kube-scheduler 相似,同时还在自定义调度器设置中内置了冗余。我们还可以最大程度地减少中断,从而提高集群资源利用率。
系统实用程序工作负载
我们的 EKS 集群有一些负责实用程序工作负载的系统 Pod,例如 CoreDNS、ArgoCD、Cilium Cluster Mesh 等。有时,这些是低利用率节点上唯一剩余的 Pod。集群自动缩放器在驱逐其中一些 Pod 时遇到了问题,因为它们使用了本地存储。这些被占用的节点反过来无法缩减。
为了解决这个问题,我们对所有此类系统实用程序工作负载添加了safe-to-evict: true 注释。
为了更平滑地扩展而进行过渡配置
EKS 集群自动缩放器 Pod 本身也会阻止回收节点。对于自动缩放器,我们选择将其运行在一个小型节点上,而不是配置 safe-to-evict: true
。这提供了更好的稳定性。
在 ClickHouse Cloud 中,我们使用 EKS 集群自动缩放器推荐的过渡配置工作负载。我们创建具有类似资源需求但较低PriorityClass 的工作负载。此较低的优先级允许为 ClickHouse Pod 驱逐过渡配置 Pod。
我们注意到,当过渡配置 Pod 使用 default-scheduler
,而 ClickHouse Pod 使用自定义调度器时,抢占不起作用。因为每个调度器只抢占由自身调度的 Pod。在这种情况下,集群自动缩放器也会错误地认为不需要向上扩展。为了解决这个问题并使事情更加一致,我们让过渡配置 Pod 也使用自定义调度器。
测试和推出
为了确保自定义调度器不会出现性能下降,我们还进行了一些 Pod 调度压力测试。我们创建了一个作业,以便持续创建由自定义调度器调度的 Pod。然后,我们杀死了持有租约锁的自定义调度器 Pod。我们观察到其他调度器备用 Pod 迅速接管。Pod 调度没有受到很大影响,根据我们的测试,调度吞吐量也不是问题。
另一个潜在风险是 Pod 冷启动时间增加。现在,由于集群更加拥挤,理论上,新实例创建更有可能触发节点组扩展以调度挂起的 Pod。为了观察这一点,我们测量了 P90 和 P99 冷启动时间,以检查影响。我们验证了这种影响可以忽略不计。这可能是由于为容纳 ClickHouse 服务而进行的节点配置并不频繁,因此不会产生很大影响。
在最终推出时,我们仍然谨慎行事。
- 我们按区域逐个推出调度器更改,并从几个小型区域开始。
- 在单个区域内,我们首先只更新较小的 ClickHouse 实例,然后才逐渐将此调度器应用于集群中的所有实例。
- 一旦 Pod 规范使用 ‘schedulerName’ 字段更新为使用此自定义调度器,正在运行的 Clickhouse Pod 将使用新调度器重新调度。由于我们已经使用 Pod 中断预算以及正常关机(以避免中断正在运行的查询)来配置我们的有状态集,因此当调度器发生更改时,我们的客户实例不会遇到任何中断。经过一次重新调度后,正在运行的 Pod 将不再受调度器更改的影响,这满足了以前解决方案无法满足的要求 #3。
推出后的集群利用率
作为这些更改的直接结果,集群利用率上升到 70%。
我们注意到节点数量减少了大约 10%,其中最大的节省来自于清理了一些大型 24xl 节点。再加上大型节点的减少,我们实现了 20% 以上的 EC2 成本降低。
最后,我们还与我们的AWS 成本和使用情况报告进行了交叉引用,该报告证实了类似的节省金额。
结论
总之,我们通过将 Kubernetes 调度器评分策略更改为 MostAllocated
,显著降低了 EKS 基础设施成本。我们通过在 EKS 集群中设置自定义调度器(具有 MostAllocated
评分策略的 kube-scheduler 镜像)来实现这一点。这种方法很好地平衡了降低成本和维护客户工作负载的稳定性。我们还彻底地对一些可抢占的系统工作负载进行了注释,以确保节点可以及时回收。
对我们而言,此项目的成功体现在所实现的成本节省以及客户的可靠性和性能没有下降。
通过上述更改,我们能够将 EKS 集群资源利用率提高 20%-30%,并在 EKS 集群中的 EC2 实例上实现相应的成本节省。