在 Kubernetes 上运行可伸缩工作负载的六个技巧

阅读数:1226 2018 年 5 月 23 日

话题:DevOpsKubernetes

关键要点

  • Kubernetes 为帮助实现应用程序扩展和容错提供了许多工具。
  • 为 pod 设置资源请求很重要。
  • 使用亲和性将你的应用程序传播到节点上和可用区域中。
  • 增加 pod 中断预算,允许集群管理员在不破坏应用程序的情况下维护集群。
  • Kubernetes 的 pod 自动伸缩意味着,随着需求的增加,应用程序应该随时可用。

作为 Pusher 的基础设施工程师,我每天都会用到 Kubernetes。虽然我没怎么开发在 Kubernetes 上运行的应用程序,但已经为许多应用程序做过部署配置。我已经了解了 Kubernetes 对工作负载的期望,以及如何尽可能让它们保持容错。

本文主要是关于如何确保 Kubernetes 知道在部署过程中都发生了什么:譬如调度在哪里最好,知道什么时候可以开始处理请求,并确保工作负载在尽可能多的节点上传播。此外,我还将讨论 pod 中断预算和水平 pod 自动伸缩,我发现这两点经常被忽略。

我在 Pusher 的基础设施团队工作。我喜欢将我的团队工作描述为“为 SaaS 提供 PaaS”。我们是一个三个人的团队,主要工作是使用 Kubernetes 搭建一个平台来帮助我们的开发人员构建、测试和托管新产品。

在最近的一个项目中,我通过将我们的 Kubernetes 工作进程转移到 spot 实例来降低 EC2 实例的成本。也许你不熟悉spot 实例,其实它们与其他 AWS EC2 实例一样,不同的是你可以用较低的价格获得一个附加条款:如果实例即将失效,你将收到一个持续 2 分钟的警报。

这个项目的一个要求是确保我们的 Kubernetes 集群能够在这 2 分钟内容忍丢失节点,这也扩展到我的团队所管理的监控、警报和集群组件(kube-dns、度量指标)。这个项目的结果是一个可以容忍节点失效的集群,可惜的是,这并没有扩展到我们的产品工作负载中。

在将我们的集群转移到 spot 实例之后不久,一位工程师找到我,试图将他的应用程序的任何潜在的宕机时间降到最低:

有什么方法可以用来阻止 [我的 pod] 在 spot 实例上调度吗?

在我看来,这是错误的做法。工程师应该利用 Kubernetes 和它所提供的工具来确保应用程序具有可伸缩性和容错能力,而不是试图避免节点故障。

Kubernetes 如何帮到我的? 

Kubernetes 有很多特性,可以帮助应用程序开发人员确保他们的应用程序可以以一种高可用的、可伸缩和容错的方式进行部署。在将一个水平可伸缩的应用程序部署到 Kubernetes 时,你需要确保已经配置了以下内容:

  • 资源请求和限制
  • 节点和 pod、亲和性和反亲和性
  • 健康检查
  • 部署策略
  • pod 中断预算
  • pod 自动水平伸缩器

在接下来的部分,我将详细描述以上所提到的每个概念是如何使得 Kubernetes 工作负载同时具备可伸缩性和容错性的。



资源请求和限制 

资源请求和限制告诉 Kubernetes 你的应用程序预计使用多少 CPU 和内存。为部署设置请求和限制是你可以做的最重要的事情之一。如果没有设置请求,Kubernetes 调度器不能保证工作负载均匀地分布在节点上,这可能导致集群不平衡,一些节点过度使用,一些节点利用率不足。

适当的请求和限制使得自动伸缩器可以估计容量,并确保集群随着需求的变化而扩展(或缩小)。

spec:

      containers:        - name: example          resources:            requests:              cpu: 100m              memory: 64Mi            limits:              cpu: 200m              memory: 128Mi

每个容器上的请求和限制是以你的 pod 为基础设置的,但是调度程序会考虑所有容器的请求。例如,一个带有 3 个容器的 pod,每个容器请求 0.1 核 CPU 和 64MB 内存(如上面的 spec 所示),将总共请求 0.3 核 CPU 和 192MB 内存。因此,如果你正在运行带有多个容器的 pod,请注意对 pod 的总请求,总的请求越高,那么调度限制(找到一个具有可用资源的节点来匹配 pod 的总请求)就会更严格。

在 Kubernetes 中,CPU 以核数的小数数位来计量。如果一个 pod 请求 0.3 核 cpu,它将被限制使用 1 核处理器的 30%。内存则以 MB 为单位。请求应该代表一个合理的预测(很多情况下都只能靠预测),预计容器在正常运行期间可能用到的资源。

负载测试可以为你获得请求的初始值,例如,在服务前面部署 Nginx 作为摄入控制器。假设你预计在正常负载下每秒有 3 万个请求,并且最初有 3 个 Nginx 副本。然后,对单个容器进行负载测试,每秒 1 万个请求,并记录资源使用情况,以此为你的每个 pod 请求提供一个合理的起点。

另一方面,限制非常严格。如果一个容器达到 CPU 限制,将被节流,如果一个容器达到内存限制,就会被关闭。

因此,限制阈值应该设置得比请求数高,并且应该设置为只在特殊情况下才会达到的值。例如,如果有内存泄漏,你可能想要有意地清除某些东西,以阻止它破坏整个集群。

不过,这个环节一定要小心。在集群中的单个节点开始耗尽内存或 CPU 的情况下,一个带有容器的 pod 可能会在到达容器极限之前被清除。在缺少内存的情况下,第一个被清除的 pod 将会是容器请求超过配额的那个。

在 Kubernetes 集群内的所有 pod 上都有合适的请求,调度程序几乎总是可以保证工作负载拥有正常运行所需的资源。

在 Kubernetes 集群内的所有容器中设置适当的限制,确保没有一个单独的 pod 在一开始就占用所有的资源,也不会影响其他工作负载的运行。也许更重要的是,由于内存消耗有限,没有一个单独的 pod 能够将占用整个节点。

节点与 pod、亲和性与反亲和性

在试图找到最佳节点来分配你的 pod 时,亲和性与反亲和性是调度程序可以使用的另一种提示。亲和性与反亲和性将允许你基于一组规则或条件扩展或限制工作负载。

目前,有两种类型的亲和性 / 反亲和性:必选亲和性和首选亲和性。如果亲和性规则无法满足任何节点,则必选亲和性将阻止调度一个 pod。另一方面,即使没有发现与关联规则相匹配的节点,首选亲和性仍然可以调度 pod。

这两种类型的亲和性的结合,使得你可以根据具体要求来规划你的 pod,例如: 

如果可能的话,在一个可用区域(没有其他的带有 app=nginx-ingress-controller 标签的 pod)运行我的 pod。 

或 

只在有 GPU 的节点上运行我的 pod。

下面是一个反亲和性的例子。下面的 pod 反亲和性确保了带有标签 app=nginx-ingress-controller 的 pod 可在不同的可用区域中调度。在一个场景中,一个集群有三个不同区域的节点,你想要运行所有 pod 中的 4 个,这个规则将会阻止调度第 4 个 pod。

spec:

  affinity:    podAntiAffinity:      requiredDuringSchedulingIgnoredDuringExecution:      - labelSelector:          matchExpressions:          - key: app            operator: In            values:            - nginx-ingress-controller        topologyKey: failure-domain.beta.kubernetes.io/zone

如果我们将 requiredDuringSchedulingIgnoredDuringExecution 一行改为 preferredDuringSchedulingIgnoredDuringExecution,那么就是在告诉调度程序将 pod 扩展到可用区域,直到没有可用区域为止。有了首选规则,当第 4 或第 5 个 pod 被调度时,它们也将开始在可用区域之间进行平衡。

亲和性 spec 中的 topologyKey 字段以节点标签为基础。你可以使用标签来确保只在具有特定存储类型的节点上或者具有 GPU 的节点上进行调度。你还可以选择在同一类型的节点甚至是带有特定主机名的节点上调度。

如果你想深入了解调度程序如何管理跨节点的分配,请查看我同事 Alexandru Topliceanu 的这篇文章

健康检查 

Kubernetes 的健康检查有两种方式:准备就绪和活跃度探测。准备就绪探测告诉 Kubernetes,pod 已经准备好开始接收请求(通常是 HTTP)。活跃度探测告诉 Kubernetes,这个 pod 仍然如预期那样运行。

HTTP 准备就绪 / 活跃度探测与传统的负载平衡器健康检查非常相似。它们的配置通常是指定一个路径和端口,但也可以定义超时、成功 / 失败阈值和初始延迟。探测传回的响应状态码在 200 至 399 之间。

readinessProbe:

  httpGet:    path: /healthz    port: 10254    scheme: HTTP  initialDelaySeconds: 10  timeoutSeconds: 5livenessProbe:  httpGet:    path: /healthz    port: 10254    scheme: HTTP  initialDelaySeconds: 10  timeoutSeconds: 5

Kubernetes 使用活跃度探测来确定容器是否健康。如果一个活跃度探测在 pod 运行时检测失败,Kubernetes 将按照重启策略重新启动该 pod。如果可能的话,每个 pod 都应该有一个活跃度探测,这样 Kubernetes 就可以判断应用程序是否按预期运行。

准备就绪探测是针对那些预计服务请求的容器,通常会有一个服务在它们之前接收请求。在某些情况下,活跃度探测和准备就绪探测起到同样的作用。但是,在容器可能启动并且必须处理一些数据或在服务请求之前进行一些计算的情况下,准备就绪探测会告诉 Kubernetes,容器已准备好在服务中注册并接收来自外部的请求。

虽然这两个探测通常都是 HTTP 回调,但 Kubernetes 也支持 TCP 和 Exec 回调。TCP 探测检查容器内是否打开了一个 socket,Exec 探测在容器内(预计有一个 0 退出码)执行一个命令:

livenessProbe:

      exec:

        command:

        - cat

        - /tmp/healthy

如果这些都配置正确,有助于确保总能找到一个能够处理请求的容器。在进行自动伸缩和执行滚动更新时,探测也被用于其他核心的 Kubernetes 功能上。在本文的其余部分中,如果我提到一个准备就绪的 pod,就表示它已经通过准备就绪探测。

部署策略 

当你想要更新它们的配置时,部署策略决定了 Kubernetes 将如何替换运行中的 pod(例如,改变镜像标签)。目前有两种策略:重新创建和滚动更新。

“重新创建”策略将中止所有部署的 pod,然后再创建一个新的。这听起来可能很危险,而且在大多数情况下都是如此。该策略的预期用途是防止两个不同版本并行运行。实际上,这意味着它一般被用于对数据库或应用程序上,运行副本必须在新实例启动之前关闭。

另一方面,“滚动更新”策略同时运行旧的和新的配置。它有两个配置选项:maxUnavailable 和 maxSurge。它们定义了 Kubernetes 可以移除多少个 pod,以及 Kubernetes 在开始滚动更新时可以添加多少额外的 pod。

它们都可以被设定为绝对数字或百分比,默认值是 25%。当 maxUnavailable 被设置为一个百分比并且滚动更新正在进行中,Kubernetes 将计算(向下舍去)它可以终止多少个副本以允许新的副本出现。当 maxSurge 被设置成百分比时,Kubernetes 将计算(向上舍入)在更新过程中它可以添加多少额外的副本。

例如,在部署 6 个副本时:有一个更新镜像标签和滚动更新策略默认配置,Kubernetes 将终止 1 个实例 (6 个实例 0.25 = 1.5 个实例,向下舍去 = 1),然后引入一个新的副本集,有 3 个新实例 (6 个实例 0.25 = 1.5 个实例,向上舍入 =2,加 1 个实例来弥补 1 个终止实例 = 3 个实例),此时总共运行 8 个副本。一旦新的 pod 就绪,它将从旧的副本集中终止另外 2 个实例,以便将部署恢复到所需的副本数,然后重复这个过程,直到部署完成为止。

使用这种方法,在部署失败的情况下(这意味着在新的副本集上出现了一个检测活跃度或准备就绪的探针),滚动更新将停止,并且你的运行工作负载仍然保留在旧的副本集上,如果滚动更新已经删除了一些旧的实例,那么保留在旧副本集上的工作负载可能会稍微小一些。

你可以根据具体需求来配置 maxSurge 和 maxUnavailable,如果你想要的话,可以设置为零 (虽然它们不能同时为零),这样的话,运行的副本数不会超过你的预期,也不会少于你的预期。

pod 中断预算

Kubernetes 集群的中断几乎是不可避免的。需要 VM 连续运行 2 年的时代已经过去了,今天的 VM 更像是牲畜,打声招呼就消失了。

Kubernetes 中的节点可能会因为各种原因发生丢失,这些是我们已经见过的:

  • AWS 上的 spot 实例被取消。
  • 基础设施团队想要应用一个新的配置,于是更换集群中的节点。
  • 一个自动计数器发现你的节点没有得到充分利用,并将它删除。

pod 中断预算是为了确保你的部署总是有最少准备就绪的 pod。pod 中断预算允许你在部署中指定可用的 minAvailable 或 maxUnavailable 副本数。这些值可以是所需副本数的百分比,也可以是绝对数字。

通过为你的部署配置一个 pod 中断预算,Kubernetes 就可以拒绝自愿中断。自愿中断是由 Eviction API 引起的(例如,集群管理员清除集群节点)。

如果把 maxUnavailable 设为 1,第一次就可以将第一个 pod 清除出去。然后,当这个 pod 被重新调度并准备就绪时,所有清除其他 pod 的请求都将失败。使用Eviction API的应用程序,如kubectl drain,预计将在成功或应用程序超时之前重试尝试清除,因此管理员和开发人员可以在不影响彼此的情况下实现他们的目标。

虽然 pod 中断预算不能保护你不受非自愿中断的影响,但它可以确保你在发生这些事件期间不会进一步降低容量,在新副本被调度和准备就绪之前停止清除节点或移动 pod。

pod 自动水平伸缩 

最后,我想要说一下pod 自动水平伸缩。Kubernetes 控制面板最棒的一点是,它能够根据资源的利用率来扩展应用程序。当你的 pod 变得更忙时,它可以自动带出新的副本来分担负载。

控制器定期查看由metrics-server(或 Kubernetes 1.8 版本和更早版本的heapster)提供的度量指标,并根据这些 pod 的负载,按需扩展或缩小部署。

在配置 pod 自动水平伸缩时,可以根据 CPU 和内存使用情况进行伸缩。但是,通过使用定制的度量服务器,你可以扩展可用的度量指标,甚至可以根据每秒对服务的请求来扩展规模。

一旦你选择了要扩展的度量指标(默认或自定义),就可以为资源或 targetValue 定义你的 targetAverageUtilization 。这是你想要的状态。

资源的值以部署的请求集合为基础。如果你将 CPU 的 targetAverageUtilization 设置为 70%,那么自动伸缩器将尝试保持整个 pod 的平均 CPU 利用率,达到其所请求 CPU 值的 70%。此外,你必须设置预计部署的副本范围:预计部署需要的最小副本和最大副本数量。

总结

通过应用上面讨论的配置选项,你可以利用 Kubernetes 提供的所有东西,让你的应用程序尽可能地冗余和可用。虽然我所讨论的所有内容并不适用于每一个应用程序,但我强烈建议你在开始的时候设置适当的请求和限制,以及设置适当的健康检查。

关于作者

Joel Speed 是一名 DevOps 工程师,在过去的一年里一直在用 Kubernetes。他在软件开发领域工作了 3 年多,目前正在帮助推进他们的内部 Kubernetes 平台。最近,他一直专注于用 Kubernetes 改进自动伸缩、弹性、认证和授权的项目,以及为Pusher的工程团队打造一个 ChatOps 机器人 Marvin。在学习的过程中,他深入参与了 Warwick 学生电影节,对他们的基础设施和定期放映的电影都进行了容器化。

查看英文原文Six Tips for Running Scalable Workloads on Kubernetes

感谢无明对本文的审校。