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

2018-05-24 08:55:00
Joel Speed
转贴:
infoq
365

作为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: 5
livenessProbe:
  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提供的所有东西,让你的应用程序尽可能地冗余和可用。虽然我所讨论的所有内容并不适用于每一个应用程序,但我强烈建议你在开始的时候设置适当的请求和限制,以及设置适当的健康检查。

发表评论
贰 减 捌 =
评论通过审核后显示。
文章分类
联系我们
联系人: 王春生
Email: chunsheng@cnezsoft.com