cloudpilot-ai

导航

全球最大分类广告商的Karpenter落地实践:减负运维、减少中断、每月省21万 (下)

原文链接:
https://medium.com/adevinta-tech-blog/the-karpenter-effect-redefining-our-kubernetes-operations-80c7ba90a599
编译:CloudPilot AI

在上一篇文章中,我们介绍了 Adevinta 迁移至 Karpenter 后如何利用这一开源工具为运维团队减负、增强应用稳定性以及实现成本优化(月省21万)。点击下方链接查看文章详情:

全球最大分类广告商的Karpenter实践:减负运维、减少中断、每月省21万(上)

本文将介绍 Adevinta 在迁移之路上踩过的坑以及他们是如何解决的。

以防没看过前文的你不了解本文的“主角”,以下是关于Adevinta的介绍:

Adevinta 是世界最大的在线分类广告商之一,其业务遍布全球9个国家及地区,每个月吸引超过 1.2 亿用户和 100 万家企业,2023 财年总营业额达 18 亿欧元。

面临的挑战

没有一段旅程是没有挑战的,Adevinta 的迁移之路也不例外。

正确配置 Pod Disruption Budget (PDB) 的重要性

尽管 Karpenter 通过遵循 Pod Disruption Budgets(PDBs)增强了可靠性,但这一优势的发挥依赖于客户正确设置 PDB。如果配置不当,可能会带来以下风险:

风险 1:未设置 PDB 时的中断增加

如果客户没有设置 PDB,Karpenter 在节点合并等操作期间可能会同时中断多个 Pod。由于 Karpenter 经常进行优化,这可能导致更多的服务中断。

风险 2:过于严格的 PDB 阻碍维护

相反,如果客户设置了过于严格的 PDB(例如,指定 maxUnavailable: 0),则会阻止任何自愿的 Pod 驱逐,包括必要的节点排空以进行维护。这会影响必要的操作,并降低集群健康状况。

缓解策略

为了平衡可靠性与操作灵活性,Adevinta 实施了以下策略:

设置合理的默认 PDB

Adevinta 为缺少 PDB 配置的应用提供默认 PDB 配置,确保在不限制过多的情况下,保护应用免受中断。这有助于在维护期间防止意外的服务影响,同时在必要时允许进行必要的节点中断。

使用 Kyverno 强制执行策略

通过 Kyverno,Adevinta 强制执行策略,避免配置那些可能妨碍集群维护的 PDB。

例如:

避免重复的 PDB Selector

禁止在命名空间中创建或更新与现有 PDB 选择器相同的 PDB。这样可以避免冲突,确保 PDB 的有效性。

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: forbid-duplicate-pdb-selectors
spec:
  validationFailureAction: Enforce
  background: false
  rules:
    - name: check-duplicate-pdb
      match:
        any:
          - resources:
              kinds:
                - PodDisruptionBudget
              operations:
                - CREATE
                - UPDATE
      context:
        - name: existingPDBs
          apiCall:
            urlPath: /apis/policy/v1/namespaces/{{request.namespace}}/poddisruptionbudgets
            jmesPath: "items[?to_string(@.spec.selector.matchLabels) == to_string(`{{ request.object.spec.selector.matchLabels }}`) && @.metadata.name != '{{ request.object.metadata.name }}']"
      validate:
        message: "A PodDisruptionBudget with the same selector already exists."
        deny:
          conditions:
            any:
              - key: "{{ existingPDBs | length(@) }}"
                operator: GreaterThan
                value: 0

确保合理的 maxUnavailable

Adevinta 强制要求,如果 PDB 指定了 maxUnavailable,则必须大于零。将其设置为零会阻止所有自愿的驱逐,从而妨碍维护任务的执行。

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: pdb-maxunavailable
  annotations:
    policies.kyverno.io/title: PodDisruptionBudget maxUnavailable Non-Zero
    policies.kyverno.io/subject: PodDisruptionBudget
    policies.kyverno.io/description: >-
      A PodDisruptionBudget which sets its maxUnavailable value to zero prevents
      all voluntary evictions including Node drains which may impact maintenance tasks.
      This policy enforces that if a PodDisruptionBudget specifies the maxUnavailable field
      it must be greater than zero.
spec:
  validationFailureAction: Enforce
  background: false
  rules:
    - name: pdb-maxunavailable
      match:
        any:
          - resources:
              kinds:
                - PodDisruptionBudget
      preconditions:
        any:
          - key: '{{ regex_match(''^[0-9]+$'', ''{{ request.object.spec.maxUnavailable || ''''}}'') }}'
            operator: Equals
            value: true
      validate:
        message: "The maxUnavailable field, when specified as an integer, must be greater than zero."
        pattern:
          spec:
            =(maxUnavailable): ">0"

限制 karpenter.sh/do-not-disrupt 注解的使用

Adevinta 阻止用户在没有充分理由的情况下,向工作负载添加 karpenter.sh/do-not-disrupt 注解,因为过度使用该注解可能会妨碍必要的维护工作。

通过实施这些措施,Adevinta 在确保应用可靠性的同时,保持了执行关键集群操作的能力。

过度配置和调度延迟

尽管 Karpenter 显著提高了资源利用率和成本效益,Adevinta 还是遇到了一些与调度延迟相关的挑战,特别是在快速扩展的阶段。Karpenter 对节点的优化和密集化(densification)意味着资源得到更高效的利用,但这也可能在某些情况下无意中导致扩展响应时间的延迟,影响客户体验。

调度延迟的挑战

在 Adevinta 高并发环境中,常常会遇到需要快速扩展 Pods 的突发情况。由于 Karpenter 对节点进行了密集优化,任何时刻可用的空闲资源相对较少。当突发需求出现时,可能没有立即足够的容量来调度新的 Pods,导致 Pods 进入待处理状态,直到 Karpenter 完成额外节点的配置。

此外,使用 startupTaints 进一步加剧了这一延迟。当 Karpenter 启动一个新节点时,会应用一个污点,防止 Pods 在该节点上调度,直到我们内部的 Kubernetes Operator 验证节点状态并移除污点。这个验证步骤对于确保节点健康和符合操作策略至关重要,但也因此增加了额外的延迟。

最终,Adevinta 观察到 Pod 调度时间的服务级别指标(SLI)有所增加,且客户投诉了由于延迟扩展而带来的性能影响。

实施低优先级 Pod 的超额预留策略

为了缓解调度延迟,Adevinta 采用了一种超额预留策略,与 codecentric 提出的 Cluster OverprovisionerKarpenter Overprovisioning Blueprint 中的方案类似。

以下是具体的实施方式:

1. 创建低优先级类

定义一个自定义的 Kubernetes PriorityClass,优先级低于所有标准工作负载。这一 PriorityClass 专用于调度占位 Pod,可以在需要时被更高优先级的 Pod 抢占。

2. 部署占位 Pod

使用低优先级类部署一组占位 Pod,这些 Pod 请求的资源极少,但足以保证节点的供应和就绪。它们可以缓冲工作负载,占用容量但不会消耗大量资源。

3. 为高优先级 Pod 提供即时调度能力

当新的高优先级 Pod 需要调度时,它们会抢占这些低优先级的占位 Pod。调度器会驱逐占位 Pod,从现有节点上释放资源,而无需等待新的节点启动,从而显著减少调度延迟。

4. 动态节点预留

Karpenter 持续监控集群状态,当检测到占位 Pod 被驱逐导致缓冲容量减少时,会自动创建新的节点,以维持超额预留状态,并将新的占位 Pod 调度到新节点上。

通过这种策略,Adevinta 实现了在突发负载时的快速响应,显著缩短了调度时间,同时确保资源利用的高效性与系统的稳定性。

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
 name: overprovisioning
value: -1
globalDefault: false
description: "Priority class for overprovisioning pods."
apiVersion: apps/v1
kind: Deployment
metadata:
 name: overprovisioning-placeholder
spec:
 replicas: 10
 selector:
   matchLabels:
     app: overprovisioning-placeholder
 template:
   metadata:
     labels:
       app: overprovisioning-placeholder
   spec:
     priorityClassName: overprovisioning
     containers:
     - name: pause
       image: k8s.gcr.io/pause:3.2
       resources:
         requests:
           cpu: X
           memory: X

未来改进方向

Adevinta 正在积极探索进一步优化当前策略的方式,包括以下两方面:

1. 超额预留容量的动态调整

计划引入自动化机制,根据历史使用模式和预测性扩容需求动态调整占位 Pod 的数量,从而进一步优化资源利用率。当前方案在集群规模较小时可能显得成本过高,因此动态调整将有助于降低小规模集群中的开销。

2. 社区协作

Adevinta 正积极关注 Karpenter 社区中的相关议题,讨论如何通过 Karpenter 原生支持超额预留功能。通过参与社区合作,Adevinta 希望推动这一功能的完善,为行业提供更高效的解决方案。

这些改进不仅有助于提升资源利用效率,还能增强系统在不同场景下的弹性和稳定性。

“最低价格优先”陷阱

尽管 Karpenter 的成本优化功能通常能带来显著效益,但在某些情况下并不完全理想。例如,Adevinta 发现 m5a 实例与 m6a 实例在价格上略有差异,但性能差距显著:

  • m5a.4xlarge(eu-west-1 区域):$0.7680/小时

  • m6a.4xlarge(eu-west-1 区域):$0.7704/小时

尽管价格仅相差 $0.0024/小时,m6a 实例的性能却明显优于 m5a。由于 Karpenter 默认选择最便宜的实例,这导致某些 CPU 密集型应用的性能未能达到最佳水平,反而因扩展更多副本而增加了整体成本。

某客户投诉称,自从迁移到 Karpenter 后,即使未进行任何代码更改,其 CPU 密集型应用的副本数量显著增加。这一现象表明,简单的“最低价格优先”策略在某些场景下可能适得其反。

同一应用程序的副本数量因基于 CPU 性能的自动扩展而激增

不仅仅是副本数量的激增成为问题,服务的延迟也显著增加,尽管代码并未进行任何更改。这表明,选择性能较低的实例不仅会导致资源消耗上升,还会对服务质量造成严重影响,尤其是在对延迟敏感的应用中。

即使不修改代码,延迟也会大幅增加

经过调查发现,由于 m5a 实例价格更低,它逐渐成为 Karpenter 的首选,取代了此前使用的 m6a 实例。这一自动化选择虽然节省了成本,却导致了性能的下降,进而影响了整体服务质量。

迁移后,实例类型开始从 m6a 转移到 m5a

为了解决这一问题,团队通过定义新的节点池并赋予更高权重(优先级),优先选择第六代实例,同时将第五代实例降级为备用选项。这种策略确保了性能和成本之间的平衡,有效提升了服务质量。

- key: karpenter.k8s.aws/instance-generation
 operator: Gt
 values:
 - "5"
weight: 15
- key: karpenter.k8s.aws/instance-generation
 operator: Gt
 values:
 - "4"
weight: 10

这一调整不仅确保了性能和成本的最佳平衡,还在第六代实例资源耗尽时,提供了备用节点池来承载工作负载。

此外,团队已与维护者展开讨论,探讨在 Karpenter 中原生支持类似场景解决方案的可行性。

v1 升级中的兼容性处理

从 Karpenter v0.37.x 升级到 v1 时,团队遇到了一些兼容性问题。升级过程中的切换和向后兼容性并不如预期顺畅,这导致了一些困惑。尽管这些问题最终都得以解决,但它们也提醒团队在采用新版本时必须保持谨慎,尤其是在使用 ArgoCD 管理 Karpenter 安装时,需要进行充分的测试和验证。

Karpenter 升级是社区的常驻问题。基于这一现状,CloudPilot AI 推出 Karpenter 自动升级功能,将原本需要一周的升级时间缩短至3小时,并且为客户解决兼容性问题。

展望未来

Karpenter 的使用之旅远未结束,团队正在积极探索以下领域:

将 Karpenter 控制器迁移出托管节点组

目前,Karpenter 控制器运行在专用于控制平面组件的托管节点组上。然而,团队正在考虑将其迁移至 AWS Fargate,这一做法已被部分行业团队实践。通过这一迁移,团队可以摆脱对托管节点组的依赖,从而进一步简化基础设施管理。

增强超额预置能力

团队密切关注 Karpenter 社区关于引入容量预留和超额预置功能的讨论。同时,也在投入精力优化现有的超额预置工具,使其更加灵活,并减少对硬编码配置的依赖,以适应多变的资源需求。

总 结

迁移到 Karpenter 对 Adevinta 来说是一次重大的改变。通过使用 Karpenter,Adevinta 不仅实现了更顺畅的集群升级,还在实例选择上获得了更大的灵活性,提升了工作负载隔离能力,实现了自动化的安全更新,并节省了大量成本。尽管仍然面临一些挑战,但整体收益远超这些问题。

对于需要大规模管理 Kubernetes 的团队来说,Karpenter 提供了许多实实在在的好处,可以简化运维,减少管理负担。

推荐阅读

全球最大分类广告商的Karpenter实践:减负运维、减少中断、每月省21万(上)

Karpenter v1.0.0对K8s弹性伸缩意味着什么?

Grafana如何利用Karpenter消除50%的云资源浪费?|落地案例

posted on 2024-12-04 10:13  CloudPilotAI  阅读(12)  评论(0编辑  收藏  举报