使用 Kubernetes 最易犯的 10 个错误

在我们多年使用 Kubernetes(以下简称为 K8s)的经历中,我们有机会近距离接触了相当多的集群(包括依托于 GCP、AWS 和 Azure 上的托管和非托管集群),并且看到一些错误在不断重复。但不必为此感到羞耻,因为我们也犯了很多这样的错误!

接下来,我会尽量解释那些我们经常犯的错误,并讨论如何修复它们。

资源 —— 请求(Request)和限制(Limit)的设置

这无疑是最值得关注的,因而列在了这个错误榜单的第一位。

设置 CPU request 的常见错误有两种 —— 「不设置」或「设置得很低」(这样我们可以在每个节点上容纳更多的 pod),这两种错误会导致节点被过量使用。在高需求的时候,节点的 CPU 就会被完全占用,我们的工作负载就只能得到“它所请求的很低的资源”,因此工作负载得到的 CPU 资源就很有限,这样就会导致应用程序延迟、超时等问题的增加。

不设置 CPU request(BestEffort,应尽量避免)时的配置如下:

    resources: {}

CPU request 设置得很低(应尽量避免)时的配置如下:

    resources:
      requests:
        cpu: "1m"

另一方面,设置一个 CPU limit 值可能会不必要地限制 pod 对 CPU 的使用,即使节点的 CPU 没有被充分利用时,这同样会导致延迟问题的增加。对于使用 Linux 内核中的 CPU CFS quota 来控制对 CPU 的使用,或关闭 CPU CFS quota 并用 CPU limit 设置来代替,这两种方式到底孰优孰劣?有一个公开的讨论,结论是 CPU limit 这个设置可能会导致更多的问题。

内存的过量使用会给你带来更多的麻烦。达到 CPU limit 时只会导致 CPU 使用受限,达到 Memory limit 将会导致 Pod 被杀死。你听说过 OOMkill 吗?是的,它正是我们要说的内存不足时自动杀死进程的机制。如果你想尽量减少这种情况的发生,那就不要过度使用内存,而应该使用“Guaranteed QoS”模式,将 Memory Request 值设为等于 Memory limit 值(就像下面的示例一样)。关于这个话题,你可以从 Henning Jacobs(Zalando)的这个演讲中获得更多信息[1]。

Burstable QoS 模式下的资源设置(很可能导致 OOMKill 机制更频繁地被触发):

resources:
      requests:
        memory: "128Mi"
        cpu: "500m"
      limits:
        memory: "256Mi"
        cpu: 2

Guaranteed QoS 模式的资源设置:

  resources:
      requests:
        memory: "128Mi"
        cpu: 2
      limits:
        memory: "128Mi"
        cpu: 2

那么在你设置 resource 时,有什么工具可以帮助你呢?

你可以借助 metrics-server 来检查 pod(和其中的容器)当前的 CPU 和内存的使用情况。很有可能它已经在服务器端运行了。要想看到使用情况,你只需简单地运行以下命令:

kubectl top pods
kubectl top pods --containers
kubectl top nodes

不过,这些命令只能显示当前的使用情况。这对于大致地了解资源使用情况是足够了,但最终你希望能够及时查看一些使用指标(比如:CPU 峰值、昨天上午的 CPU 使用情况等指标)。在这个方面,Prometheus、DataDog 和许多其他的监控系统可以帮助你。它们从 metrics-server 上获取这些指标信息并存储起来,然后你就可以对其进行查询和绘制图形。

VerticalPodAutoscaler[2] 工具可以帮助你自动化这一手动过程:包括及时查看 CPU/内存的使用情况,以及基于使用情况设置新的 request 和 limit 的值。

高效地利用你的计算资源并非易事。就像一直在玩俄罗斯方块。如果发现自己在平均利用率很低(例如〜10%)的情况下为计算支付了大量费用,那么你可能需要看看基于 AWS Fargate 或 Virtual Kubelet 之类的产品是否能解决你的问题,这些产品更多地利用了无服务器/按使用付费的计费模式,这对你来说可能更便宜。

存活(liveness)探针和就绪(readiness)探针的设置

默认情况下,系统不会设置检测一个 pod 状态的存活探针和就绪探针。K8s 的自愈能力有时可以让容器一直工作下去……

但是,如果容器出现了不可恢复的错误,你的服务将如何重新启动呢?负载均衡器(loadbalancer)如何判断某个特定的 Pod 是否可以开始处理流量?或者是否可以继续处理更多流量?

通常,人们不知道这两个探针(liveness 和 readiness)之间的区别。

  • 如果对一个 pod 的 liveness 探测失败,就会重启这个 pod。
  • 如果对一个 pod 的 readiness 探测失败,就会将这个 pod 和 K8s 服务断开连接(你可以使用 kubectl get endpoints 命令进行检查),并且在该探测再次成功之前,不再向其发送任何流量。

注意:这两种探测需要在整个 pod 的生命周期内都运行。这点很重要。

人们常常认为,readiness 探测仅在容器起动时运行,以告知 K8s 服务 pod 何时就绪,并且可以开始为流量提供服务。但这只是它的一个应用场景。

另外一个应用场景是:在一个 pod 的生命周期内,使用 readiness 探测可以让系统知道一个 pod 是否变得太“热”而无法处理过多的流量(或昂贵的计算),以便系统停止向它发送更多的流量而让它“冷静”下来。直到 readiness 再次探测成功,系统才开始继续向它发送更多流量。在这种情况下(当 pod 过热导致 readiness 探测失败时),如果让 liveness 探测失败就不是我们想要的。因为,我们为什么要重启一个运行良好,并且正在处理大量工作的 Pod 呢?

有时候,不配置任何一个探针比错误地配置探针要好。如上所述,如果将 liveness 探针配置成和 readiness 探针一样,那么你将遇到大麻烦。作为开始,建议你仅仅配置 readiness 探针,因为 liveness 探针很危险。

如果一个和其它 pod 有共享依赖项的 pod 被关闭,那么你必须保证针对这个 pod 的任何一个探针都不能失败,否则将导致所有 Pod 的级联失败。那你就是在搬起石头砸自己的脚了。

用于 HTTP 服务的负载均衡器

你的集群中可能有很多你想对外开放的 HTTP 服务。

如果你把 K8s 的外部访问方式设置为 LoadBalancer 类型,则其控制器(特定于供应商)将被配置来支持一个外部负载均衡器(不一定非得是支持七层交换的 L7 负载均衡器,更可能是仅仅支持四层交换的 L4 负载均衡器),并且这些资源(外部静态 IPv4 地址、计算硬件、带宽等等)可能会变得很昂贵,因为你会创建许多这样的服务。

在这种情况下,共享一个外部负载均衡器可能更有意义,并且你可以将 K8s 的外部访问方式设置为 NodePort 类型。或者,更好的方法是,部署一个像 nginx-ingress-controller(或者 traefik)之类的东西作为暴露给外部负载均衡器的单个 NodePort endpoint,并基于 K8s ingress resource 配置在集群中分配并路由流量。

集群内的其他需要会话的(微)服务可以通过 ClusterIP 服务和 K8s 自带的 DNS 服务发现(DNS Service Discovery)来实现会话。注意不要使用公共 DNS / IP,因为这可能会导致延迟和云使用成本的增加。

非 K8s 感知的集群自动伸缩

在集群中添加节点或者从集群中删除节点时,你不应该只是考虑一些简单的指标:如节点的 CPU 利用率。在调度 Pod 时,你需要根据很多调度约束条件(如 Pod 和节点的亲和性,污点和容忍,资源请求,QoS 等等)来进行决策。如果一个外部自动伸缩器不能理解这些约束,可能会带来很大的麻烦。

假设一个新的 pod 要被调度,但是所有可用的 CPU 都被占用了,而且这个 pod 现在处于挂起状态。当外部自动伸缩器看到当前使用的平均 CPU 情况(非常高),就不会向外扩展了(将这个 pod 添加作为节点)。也就是说这个 pod 不会被调度。

向内收缩(从集群中删除节点)总是更加困难。假设你有一个有状态的 pod(连接了持久化存储卷),由于持久化存储卷这种资源通常属于某个特定的可用区域,并且不能在该区域中复制,当你的自定义自动伸缩器删除一个带有此 pod 的节点时,调度器无法将其调度到另一个节点上,因为它受到持久化存储卷属于唯一可用区域的限制。所以这个 Pod 再次被卡在挂起状态。

K8s 社区正在广泛使用 cluster-autoscaler 集群自动伸缩器,它运行在你的集群中,并且已经和大多数主要的公共云提供商的 API 实现了集成,理解所有这些约束,可以帮助你在上述情况下实现自动向外扩展。它还能够帮助你确定在不影响设置的约束条件下方便地实现自动向内收缩,以节省你的计算成本。

不使用 IAM/RBAC 能力

不要将 IAM User 与机器和应用程序的永久 secret 一起使用,而应该使用角色(role)和服务账户(service account)生成临时 secret。

我们经常看到这样错误的用法:将访问权限和 secret key 硬编码到应用程序的配置中,当使用云 IAM 时,从来不轮换 secret。在合适的情况下,你应当使用 IAM 角色(role)和服务帐户(service account)而不是 IAM Users。

跳过 kube2am,直接用于服务帐户的 IAM 角色。就像 Štěpán Vraný 在这篇博客文章[3]中所说的那样。

apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/my-app-role
  name: my-serviceaccount
  namespace: default

上面的示例不是很难理解,不是吗?

另外,在不是必要时,千万不要将 admin 和 cluster-admin 的权限给予服务帐户或实例配置文件。这点做起来有点困难,尤其是在使用 K8s 的 RBAC 时,但是仍然值得努力。

POD 的自我反亲和性

运行某个部署的 pod 副本(比如说有 3 个 pod 副本),当节点下线时,你发现所有副本都随之同时下线了。呵呵?所有副本竟然都是在一个节点上运行的吗?K8s 难道不应该有魔法,可以自动提供高可用性吗?!

你不能指望 K8s 调度器自动为你的 pod 强加一个反亲和性设置。相反,你应该明确地定义它们(如下例)。

// omitted for brevity
      labels:
        app: zk
// omitted for brevity
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: "app"
                    operator: In
                    values:
                    - zk
              topologyKey: "kubernetes.io/hostname"

你需要像上面这样设置 pod 的反亲和性,它将确保 pod 被调度到不同的节点上(注意:这个设置是在调度时检查,而不是在执行时检查,因此需要设置成 requiredDuringSchedulingIgnoredDuringExecution 类型)。

我们这里讨论的是在不同节点(这里是 topologyKey: “kubernetes.io/hostname”)上的 pod 反亲和性(podAntiAffinity),而不是不同可用区域的 pod 反亲和性。如果你真的需要高可用性,你应该更加深入地探讨这个话题。

没有 poddisruptionbudget

你是在 K8s 上运行生产环境的工作负载。你的节点和集群必须不时地升级或停用。而 PodDisruptionBudget(pdb)就是一种被集群管理员和集群用户广泛用来确保服务质量的 API。

请确保你创建了 pdb,以避免由于节点停用过多而导致不必要的服务中断。

apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: zk-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: zookeeper

作为集群用户,你可以这样告诉集群管理员:“嘿,我这里有个 zookeeper 服务,不管你想做什么,我希望至少保证两个副本始终可用。”

关于这个话题,我在这篇博文中作了深入地讨论[4]。

共享集群中有更多租户或环境

K8s 命名空间不提供任何强隔离。

人们似乎期望,如果将非生产环境工作负载分离到一个命名空间,将生产环境工作负载分离到另一个命名空间,那么一个工作负载就永远不会影响另一个工作负载,这样就可以实现某种程度的公平性(基于资源请求和限制、配额、优先级类)和隔离性(基于亲和性、容忍度、污点(或节点选择器),以便“物理地”分离数据平面(data plane)中的工作负载,但是,实现这种程度地分离相当复杂。

如果你需要在同一集群中同时拥有这两种类型的工作负载,那么你将不得不承受这种复杂性。但是如果你不是非要这样做不可,并且拥有另一个集群对你来说相对便宜(比如在公共云中),那么最好将它们放在不同的集群中,以实现更高的隔离级别。

设置 externalTrafficPolicy 值为 Cluster

经常看到这种情况,所有的流量在集群内全部被路由到一个 NodePort 服务, K8s 服务默认将 externalTrafficPolicy:的值设为 Cluster(即 externalTrafficPolicy: Cluster)。这意味着集群内的每个节点都打开该 NodePort 服务,这样你就可以使用任何一个节点和所需的服务(一组 pods)通信。

通常,负责提供 NodePort 服务的实际 pod 只在这些节点的子集上运行。这意味着,如果你与一个没有运行 pod 的节点会话,它会将流量转发到另一个节点,从而导致额外的网络跳数和延迟的增加(如果节点位于不同的 AZ 或不同的数据中心,则延迟可能会变得非常高,并且给它带来额外的出口(egress)成本)。

将 K8s 服务的 externalTrafficPolicy 值设置为 Local (即 externalTrafficPolicy: Local)将不会导致在每个节点上打开该 NodePort 服务,它只会在 pod 实际运行的节点上打开。如果你使用一个能够对 endpoint 进行状态检查的外部负载平衡器(如 AWS ELB 所做的),它就仅仅将流量发送到它应该发送到的节点,从而改进延迟、降低计算开销和出口成本,并提高健全性。

很可能,你会将像 traefik 或 nginx-ingress-controller 之类的服务公开为 NodePort 类型(或者 LoadBalancer,它也使用 NodePort),以便路由 http ingress 流量,这个设置可以大大减少此类请求的延迟。

这里有篇很棒的博客文章[5],深入探讨了 externalTrafficPolicy 各种设置以及它们的优点和缺点。

有状态(pet)集群以及过高地控制平面的压力

你从按名字调用你的服务器 Anton, HAL9000 和 Colossus,到为你的 K8s 节点生成随机 id,现在你开始按名字来调用集群了?

你知道你是如何开始使用 K8s 进行概念验证的,你将集群命名为“testing”,并且在生产环境中仍然使用这个名字(testing)。这样命名导致了每个人都害怕碰它吗?(这个故事真实发生过)

Pet 集群(有状态的集群)并不容易管理,你可能需要考虑不时地删除你的集群,练习灾难恢复并管理好控制平面(control plane)。害怕操作控制平面不是个好现象。Etcd 组件挂起?好吧,你有个大麻烦。

另一方面,对控制平面操作太多也不好。随着时间的推移,控制平面会越来越慢,可能的原因是,你要么创建了太多对象而不轮换其状态(这在使用默认设置的 helm 时非常常见,它将导致在 configmaps/secrets 中不轮换其状态,最终在控制平面中有数千个对象),要么不断地从 kube-api 中拚凑并编辑了大量内容(用于自动伸缩、cicd、监控,事件、事件和控制器的日志等等)。

另外,请查看你的 K8s 托管服务提供商承诺的 SLA/SLO 以及服务质量保障。你的提供商可能会保证控制平面(或其子组件)的可用性,但不会保证让你满意的 p99 延迟。也就是说,你运行 kubectl get nodes 命令,只要该命令在 10 分钟执行完成并返回正确结果,你的服务提供商就仍然没有违反服务保证。

附送一个问题:使用 latest 标签

这是一个经典的问题。但是最近我发现自己不经常看到这种问题了,可能因为我们太多人都在这上面栽过跟头,所以我们不再使用:latest 这个标签了。

AWS ECR 现在支持标签不变性,这个很棒的特性绝对值得你去查看。

总结

不要指望一切都会自动进行,K8s 并非灵丹妙药。一个糟糕的应用程序始终是糟糕的应用程序,即使在 K8s 上也改变不了(实际上可能甚至更糟糕)。稍不小心,你可能就会面对诸如太多复杂性,过高的压力,控制平面缓慢,没有灾难恢复策略等麻烦。不要对开箱即用的多租户设计和高可用性期望过高。花点时间思考如何让你的云应用成为原生应用。

查看 Henning 收集的这些关于 K8s 使用的失败故事[6],可以对你在将来避开这些错误有所帮助。

如果你看到有人在 K8s 使用上犯了不同于本文中的错误,请在 Twitter 上告诉我们(@MarekBartik @MstrsObserver)!

[1] Optimizing Kubernetes Resource Requests/Limits for Cost-Efficiency and Latency – Highload++, https://www.slideshare.net/try_except_/optimizing-kubernetes-resource-requestslimits-for-costefficiency-and-latency-highload

[2] https://cloud.google.com/kubernetes-engine/docs/concepts/verticalpodautoscaler

[3] https://blog.pipetail.io/posts/2020-04-13-more-eks-tips/

[4] https://blog.marekbartik.com/posts/2018-06-29_kubernetes-in-production-poddisruptionbudget/

[5] https://www.asykim.com/blog/deep-dive-into-kubernetes-external-traffic-policies

[6] https://k8s.af/

发表评论

电子邮件地址不会被公开。 必填项已用*标注