在亚马逊 EKS 上从 100 个容器扩展到 10,000 个

这篇文章由 OLX Autos 的 Nikhil Sharma 和 Ravishen Jain 共同撰写

简介

在 OLX Autos,我们在本土内部开发者平台 (IDP) ORION 上并行运行 100 多个非生产(非生产)环境,用于不同的用例。ORION 在亚马逊 Elastic Kubernetes 服务( 亚马逊 EKS)上运行。 每个 Autos 环境都包含至少 100 个吊舱。这些环境中有许多是短暂的,是为演示或开发需求而创建的。我们将所有这些环境托管在一个 Amazon EKS 集群上,以提高资源利用率。我们使用 100% 的竞价型实例来托管这个 Amazon EKS 集群,以优化成本。在任何时候,ORION 都会在 Amazon EKS 集群上运行 10,000 多个容器,并随着任何环境的扩展或新环境的创建而增加更多容器。

在这篇文章中,我们将向您展示我们如何将适用于 ORION 的主机 Amazon EKS 集群从 100 个扩展到 10,000 多个容器。

关于 OLX Autos

OLX Aut os OLX集团 的一部分, 是一个买卖二手车的全球市场。该业务旨在为买家和卖家提供安全、便捷的一站式解决方案。OLX Autos通过结合线上和线下体验来革新二手车交易,为买家和卖家提供便利、安全和安心。自成立以来,OLX Autos已扩展到11个国家。

OLX Autos IDP — 猎户座

我们的技术团队已经建立了一个名为ROADSTER的平台,该平台上有多种业务流,例如汽车交易、融资和分类广告。考虑到汽车市场不断变化、竞争激烈的性质,该平台背后的关键原则是超快的速度、模块化、弹性以及对尖端技术的关注。Roadster 托管在亚马逊 EKS 上。

随着我们的扩展,新功能的快速交付仍然是一项关键挑战。降低工程师的复杂性并提高他们的效率是我们关注的主要目标之一。我们的工程师必须为不同的用例创建多个非产品 Roadster 环境。用例范围从展示实验性功能演示、运行自动化测试用例到在新市场推出Roadster不等。Roadster 环境需要多个微服务和数据库,它们在业务用例中相互交互。

创建和维护环境既消耗时间又消耗资源。我们通过构建内部开发者平台(IDP)(ORION)来应对这一挑战,该平台提供了一个中央平台来解决创建新的Roadster环境的问题。

解决方案概述

下图显示了 ORION 的架构:

ORION Architecture diagram showing AWS services like Amazon Route53, AWS Certificate Manager, Amzon EKS, Amazon RDS, Amazon SQS and Amazon SNS.

猎户座的目标是:

  • 通过提供服务、工具、文档和见解的无缝集成,降低复杂性并专注于卓越的开发者体验。
  • 让开发人员能够以最少的点击量创建 Roadster 平台,从而消除对其他团队的依赖。

ORION的关键功能之一是只需点击几下即可提供Roadster环境。

使用亚马逊 EKS for IDP

Roadster 已完全集装箱化,并且已经在亚马逊 EKS 上运行了多年。对于 ORION 来说,亚马逊 EKS 是一个显而易见的选择。ORION 支持创建新的敞篷跑车环境。使用 ORION,我们在单个 Amazon EKS 集群上支持 100 多个并发环境。

下图显示了亚马逊 EKS 集群上 ORION 控制的规模:

扩展挑战和解决方案概览

随着我们团队中采用ORION的增加,它带来了许多有趣的扩展挑战。

DNS 解析延迟高

经典 ndots 挑战赛

亚马逊 EKS 中的默认 ndot 值为 5。根据 DNS 标准,如果域名的点数 (.) 等于 ndot 值或末尾有点 (.),则该域名被视为完全限定域名 (FQDN)。在对不是 FQDN 的域名进行 DNS 查询时,core-dns 会遍历所有搜索路径,直到找到成功为止。

如果你从 pod 中查询 amazon.com,那么 core-dns 会查询:

[amazon.com.default.svc.cluster.local,

amazon.com.svc.cluster.local,

amazon.com.cluster.local,

亚马逊。]

按此顺序,直到成功。

我们从亚马逊 EKS 集群 上的一个节点在亚马逊上执行了 nslookup,结果出现了 7 个 DNS 问题(5 个虚假的 DNS 查询、1 个 ipv4、1ipv6 DNS 查询)。

以下屏幕截图显示了亚马逊上的 nslookup 命令的输出。grep 问题的数量显示 DNS 查询的数量,grep NXDOMAIN 的计数显示由于虚假 DNS 查询而导致的错误数量:

Diagram showing output of nslookup command on amazon.com

随着 OLX Autos 规模的扩大,来自服务间通信的网络呼叫增加了许多倍。这导致了大量 DNS 查询。ndot 值为 5 使问题恶化了 5 次,导致核心 dns 崩溃,从而频繁出现 DNS 解析错误。这 篇文章 解释了 ndots 对 Kubernetes 中 DNS 查询的影响。

由于 CPU 节流,响应时间长

最初,我们对 pod 设置 CPU 限制 。CPU 限制定义了容器可以使用多少 CPU 时间的硬性上限。设置 CPU 限制加剧了 ndot 问题。在获得成功之前,每个 DNS 查询都进行了大量失败的 DNS 查询。CPU 限制限制了容器在一个时间间隔内可以使用的 CPU 时间。当容器等待成功的 DNS 查询时,在给定周期内分配给容器的 CPU 时间可能会失效,容器不得不等待另一个 CPU 周期。由于这种情况经常发生,应用程序的性能会降低,延迟增加。 埃里克·库恩在这里详细 解释了CPU限制的影响。 我们将 CPU 限制更改回原为默认值。

通过在末尾添加一个点来解析

在 DNS 名称中添加一个点使其成为 FQDN,解析器无需遍历搜索路径。这减少了 DNS 调用的数量。

我们在 “亚马逊” 上执行了 nslookup。(即,在末尾添加一个点)。这将 DNS 问题从 7 个减少到 2 个,其中 NXDOMAIN 错误为 0。这减少了我们的core-dns服务器上的70%以上的查询,并提高了性能。

以下屏幕截图显示了上述更改后在 “amazon.com。” 上的 nslookup 命令的输出:

Diagram showing output of nslookup command on amazon.com.

我们正在调用的 http 调用端点是通过环境变量配置的。我们在环境变量中这些 HTTP 端点的末尾添加了一个点。

DNS 缓存可进一步改进

上述更改大大减少了 core-dns 服务器上的 DNS 查询;但是,随着 OLX Autos 的扩展,我们仍然运行了大量 DNS 查询,从而导致了更高的延迟。我们推出了由 K8s 社区 支持的 N odeLocal DnsCache 代理 ,这进一步提高了我们的应用程序性能。

下图显示了带有 DNS 缓存和不使用 DNS 缓存的应用程序响应时间:

Graph shows API latency dropped by 98 % with DNS cache.

IP 饥饿和 VPC CNI 插件

VPC CNI 插件作为守护程序集运行,负责将亚马逊 虚拟私有云 (亚马逊 VPC ) 中的可用 IP 地址分配给 pod。 默认情况下,VPC CNI 向工作节点分配一个额外的 ENI (弹性网络接口),并为该节点分配许多 IP。

IP 的数量取决于节点的实例类型。有关每个网络每种接口类型的 IP 地址的详细列表,请参阅此 文档

这意味着,对于每个类型为 c5.9xlarge 的节点,它会从 VPC 池中保留 58 个 IP(即每个 ENI 上有 29 个),即使该工作节点上没有运行 Pod。

默认配置导致我们的 VPC 中的 IP 不足,并导致无法向 Amazon EKS 集群添加更多节点。它还导致未能在VPC中启动新的亚马逊云科技资源,例如亚马逊 弹性计算云(Amazon EC2)

VPC CNI 插件的默认行为可以通过以下参数来配置,这些参数控制每个节点分配的 IP 数量:

  • MINIMUM_IP_TARGET 是在每个节点上分配的最少 IP 数。
  • WARM_IP_TARGET 每个节点 上未使用 IP 的最大数量。
  • WARM_ENI_TARGET 控制连接到实例的弹性网络接口 (ENI) 的数量。

WARM_IP_TARGET 配置可最大程度地减少未使用的 IP

最初,我们非常激进,配置了 WARM_IP_TARGET= 1,这将只保留一个 IP。

当我们在集群上执行以下命令时,输出显示了该节点上所有可用和分配的 IP:

kubectl exec -it <aws-node-pod-name> -c aws-node -- curl localhost:61679/v1/enis

下图显示了该命令的结果:

ORION Architecture diagram showing AWS services like Amazon Route53, AWS Certificate Manager, Amzon EKS, Amazon RDS, Amazon SQS and Amazon SNS.

该配置解决了 IP 不足的问题;但是,它导致了另一个问题。由于只保留了一个额外的 IP 地址(即 WARM_IP_TARGET=1 ),因此 每当调度多个 Pod 时,都需要分配多个 IP,这会触发 IP 分配的争用条件。Pod 必须等待 VPC CNI 插件为工作节点分配更多 IP。在此期间,我们看到了诸如— 无法为容器分配 IP 地址 以及 DataStore 没有可用的 IP 地址 之类的错误。

下图显示了 kubectl describe pod 命令中此类错误事件的快照:

Warning FailedCreatePodSandBox 59s kubelet
Failed to create pod sandbox: rpc error: code = Unknown desc failed to
set up sandbox container

"42c567cfd18ffe01a6fa5f38574a4120d486cd20058b20bf3646a3236f198b65" network
for pod "ca9efe75-2a23-4da7-9752-8d7541ed6e92-8zq5s": networkPlugin cni
failed to set up pod "ca9efe75-2a23-4da7-9752-8d7541ed6e92-8zq5s_staging"
network: add cmd: failed to assign an IP address to container

下图显示了工作节点上来自 /var/log/aws-routed-eni/ipamd.log 的日志:

("level":"debug","ts":"2022-02-02T05:28:21.576Z","caller":"rpc/rpc.pb.go:486",
"msg":"AddNetworkRequest: ClientVersion:\"v1.7.5\" K8S_POD_NAME:\"nginx-2\"
KBS_POD_NAMESPACE:\"default\"
KBS_POD_INFRA_CONTAINER_ID:\"42b261766a518973ad2bdfa33948e745da823f1e132cdca2b862dd4e27d4f567\"
ContainerID:\"42b261766a518973ad2bdfa33948e745da823f1e132cdca2b862dd4e27d4f567\"
IfName:\"eth0\" NetworkName:\"aws-cni\" Netns: \"/proc/20244/ns/net\" "}
{"level":"debug","ts":"2022-02-02T05:28:21.576Z","caller": "ipamd/rpc_handler.go:142",
"msg":"AssignIPv4Address: IP address pool stats: total: 3, assigned 3"}

{"level":"debug","ts":"2022-02-02T05:28:21.576Z","caller":"ipamd/rpc_handler.go:142",
"msg":"AssignPodIPv4Address: ENI eni-067be541f80752bfb does not have available addresses"}

{"level": "error", "ts":"2022-02-02T05:28:21.576Z","caller":"ipamd/rpc_handler.go:142",
"msg":"Datastore has no available IP addresses"}

用于优化 IP 预分配的 MINIMUM_IP_TARGET 配置

为了修复这个问题,我们将 MINIM UM_IP_TA RGET 设置为每个节点上 预期的 pod 数量。设置此数字时,应同时考虑您的工作负载以及集群中运行的多个守护程序集。设置 WARM_IP_TARGET= 1 和 M INIM UM_IP_TARGET= [预期的吊舱数量] 修复了 IP 匮乏问题

使用 100% Spot 时会遇到挑战

托管 ORION 的亚马逊 EKS 集群在 Spot 队列 上运行。我们决定 100% 在 Spot 实例上运行,因为 Orion 管理的是短暂的环境,我们希望提高成本效益。它带来了以下挑战:

现货不可用

我们必须确保在竞价型实例不可用的情况下,业务不会中断。尽管 ORION 适用于非产品用例,但我们承受不起中断的代价。我们使用了不同的 CPU、内存配置相似的实例类型(例如,m5.2xlarge、m5a.2xlarge、m5d.2xlarge、m5ad.2xlarge 和 m4.2xlarge)。除此之外,如果竞价容量不可用,我们还使用优先级扩展器和集群自动扩展器来回退按需容量。

我们使用了以下 Kubernetes 配置:

apiVersion: v1
kind: ConfigMap
metadata:
  name: cluster-autoscaler-priority-expander
  namespace: kube-system
data:
  priorities: |-
   5:
     - .*on-demand.*
   50:
     - .*spot.*

为了采用这种方法,我们为竞价和按需实例创建了节点组。我们决定采用命名惯例,即名称包含 Spot 的节点组具有竞价型实例,而名称包含按需的节点组具有按需实例。使用命名模式为每个节点组配置了优先级,如前面的示例所示。我们将 Spot 节点组设置为高度优先级。它确保集群自动扩缩器首先从 Spot 节点组启动节点。如果没有足够的竞价容量可用,则集群自动缩放器将回退到按需节点组。我们将集群自动扩缩器的超时时间(即最大节点配置时间)从默认的 15 分钟更改为 10 分钟。

我们计划探索 Karpenter 来处理集群自动扩展和竞价容量。Karpenter 是一款动态、高性能的集群自动扩展解决方案,适用于 re: Invent 2021 上推出的 Kubernetes 平台。你可以参考这 篇文章 了解 更多信息。

现场终止处理

我们正在使用自我管理的节点组。我们需要妥善处理亚马逊 EC2 的终止。我们使用 了 aws-node-termination-hander (NTH ) 来解决这个问题。我们使用在每台主机上作为守护程序运行的 NTH 实例元数据服务 (IMDS) 处理器来监控 /spot 或 / events (http://169.254.169.254/latest/meta-data/spot/termination-time) 等 IMDS 路径。 NTH 持续对 /sp ot 和/e vent s 端点进行轮询,以检查实例是否出现故障。每当检测到此类事件时,它都会安全地隔离节点,确保节点上没有安排新的工作负载。所有先前存在的豆荚也都被排干了。我们使用 Helm 安装了 NTH,并参考了 亚马逊云科技 节点终止处理程序 NTH 的设置和配置。

尽量减少容量浪费

我们使用的是多个节点组,每个节点组由提供大致相似容量的实例类型组成。我们创建了由 m5a.xlarge、m4.xlarge、m5.xlarge 和 m5d.xlarge 实例类型组成的 xlarge 节点组,因为每种类型都提供 4 个 vCPU 和 16Gib 内存。同样,我们为 9xLarge 实例类型创建了另一个 9xLarge。我们希望确保我们不会为较小的工作负载启动更大的实例。我们使用集群自动缩放 器的最小浪费扩展器配置,使集群自动扩缩器 能够选择浪费最少的节点组。

以下屏幕截图显示了集群自动缩放器的相关配置:

expander 的默认值为机 ,它将集群自动扩缩器配置为随机启动来自任何节点组的节点。除此之外,我们还使用了 Cloudability 监控所有资源的成本,NewRelic 监控了所有工作负载的使用情况。我们使用这两个数据点来调整每个应用程序的资源大小。

处理高优先级工作负载

在我们的设置中,托管 ORION 的 Amazon EKS 集群还托管了我们的 CI/CD 管道运行器 pod,用于处理部署任务。使用 ORION 启动环境意味着同时调度 100 个 Pod,这会触发集群自动扩缩器向我们的 Amazon EKS 集群添加更多节点。有时,我们在启动新环境时会遇到竞价型实例的实例容量不足。虽然删除现有环境将负责释放容量,但就我们而言,删除环境将通过 CI/CD 管道完成。CI/CD 管道将调度到同一个亚马逊 EKS 集群上,由于实例容量不足,该集群无法扩展。这导致团队陷入困境,无法向前移动。

我们通过使用 pod 优先级解决了这个问题。我们创建了具有非常 高优先级值的高优先级非抢占式 pod 优先级类,并将其分配给管道运行器 pod。这确保了管道 pod 始终可用,即使调度器必须驱逐一些其他容器来容纳它们。虽然我们通常使用浪费最少的容量扩展器,但如果实例容量不足,我们会改用优先级扩展器。

以下是用于创建 优先级非抢占式优先级类的 Kubernetes 配置:

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority-nonpreempting
value: 1000000
preemptionPolicy: Never
globalDefault: false
description: "This priority class will not cause other pods to be preempted."

以下是使用 高优先级非抢占优先级类 的 pod 配置

apiVersion: v1
kind: Pod
metadata:
  name: gitlab-runner
spec:
  containers:
  - name: gitlab-runner
    image: gitlab/gitlab-runner
  priorityClassName: high-priority-nonpreempting

结论

在这篇文章中,我们向您展示了我们如何将适用于 ORION 的主机 Amazon EKS 集群从 100 个扩展到 10,000 多个容器。ORION使OLX Autos工程师能够专注于构建业务就绪功能,这些功能对于OLX Autos向新国家和新业务的扩张至关重要。由于创建新环境更容易,工程师不必彼此共享环境,这提高了敏捷性并解锁了效率。Amazon EKS 帮助 OLX 高效地管理资源,使用即时优先的方法降低了基础设施成本。

以下是我们从经验中学到的重要经验:

  1. 通过使用 FQDN(以点结尾的域名)和 DNS 缓存,我们将应用程序延迟从 3000 毫秒的峰值降低到 50 毫秒以内(提高了约 98%)。
  2. VPC CNI 插件的默认行为经过优化,可提高容器创建过程中的效率,因此会屏蔽大量 IP 地址。我们优化了配置以防止 IP 短缺。
  3. 竞价型实例的折扣高达90%,非常适合开发环境。我们使用优先级扩展器来获得可靠的容量,使用最低成本的扩展器来最大限度地减少浪费,并使用 aws-node-termination-handle(NTH)来实现节点的平稳终止 。这些功能使得 Spot 开发成为一个引人注目的选择。
  4. 我们使用优先级类来自定义 pod 调度,并确保 Amazon EKS 不会耗尽高优先级工作负载的容量。

Headshot of Nikhil Sharma

Nikhil Sharma,OLX

Nikhil Sharma 是 OLX 的平台工程经理。在长达十多年的职业生涯中,他曾研究过多个领域的各种技术领域,涉及网络、DNS、TLS、容器、Kubernetes 和系统性能的问题无穷无尽。他是一位狂热的读者,热爱旅行,是一位诗人,也是一位健身爱好者。目前,他领导OLX的平台团队。

Headshot of Ravishen Jain

Ravishen Jain,OLX

Ravishen Jain 是 OLX 的平台工程师。他对容器化的迷恋激发了他在K8s原生技术方面的精湛技巧,他是一位经验丰富的全栈开发人员,也是设计和构建K8s原生可扩展应用程序领域的活跃开源贡献者。在他目前的职位上,他正在构建和管理 OLX Autos 内部开发者平台,即 ORION。