10 Most Common Mistakes Using Kubernetes

翻译自 10 most common mistakes using kubernetes

resources - requests and limits

这个绝对值得一开始就讲。

CPU 限制通常都是 not set(没设置)或者 set very low(设置很低)(因此可以一次在一个节点放很多 pod),节点因此就会容易被过度使用。在需求旺盛的情况下,节点的 CPU 会被充分利用。我们的工作负载仅会获得“要求的算力”,会得到 CPU 使用限制,一般会导致应用程序延迟和超时增加,等等。

越多越好的策略(请别这么做)

    resources: {}

限制特别低的策略(请别这么做)

    resources:
      requests:
        cpu: "1m"

另一方面,设置 CPU 限制会无必要的限制 pod 的使用,即使节点的 CPU 并没有被充分利用,这也会导致应用的延迟增加。围绕 Linux 内核中的 CPU CFS 限额和基于设置的 CPU 限额并关闭 CFS 的限额有一些公开讨论。CPU 限制导致的问题比解决的问题更多。可以参考后面的链接查看更多信息。

内存过度使用会导致更多问题。达到 CPU 限制会导致限流,达到内存限制会导致 pod 被杀掉。见过 OOMkill 么?对的,我们说的就是这个。想要减少他的出现么?不要超额使用内存,使用有保障的 QoS ,像下面例子一样设置内存 request 等于 limit。可以看看 Henning Jacobs 的讲稿查看更多。

允许超量的策略(可能会出现更多的 OOMKill)

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

有保障的策略

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

那么在设置资源限制的时候,有什么参考呢?

通过 mertics-server 你可以查看当前各 pod(以及他们的容器) 的 cpu 和内存使用情况。如果你已经运行他们了,执行下面的命令即可。

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

然而这些只是当前的用量。这些可以用来产生一些比较粗糙的限额的想法,但是你最终还是想要看到历史的使用情况(回答类似的问题:cpu 使用的峰值是多少,昨天的用量是多少,等等)。你可以使用 Prometheus, DataDog 以及其他的一些工具收集这些数据。这些工具收集和保存之后,你就可以查询和画图了。

VerticalPodAutoscaler 可以帮助你自动处理这些事情,通过监控 cpu 内存历史使用情况来调整新的 request 和 limit 策略。

有效利用节点性能并不容易。就像玩俄罗斯方块一样。如果你发现在节点利用率比较低的情况下(例如 ~ 10%)账单比较高,那你可能可以看看基于 AWS Fargate 或者 Virtual Kubelet 的产品,这些基于用量付费的的产品可能更便宜。

liveness and readiness probes

默认情况下是没有 liveness 和 readiness 检测的。很多时候也没人管这个..

但是如果出现了不可恢复的错误的时候,你的服务如何重启?负载均衡器如何知道特定的 pod 已经可以开始处理流量了?以及还可以处理更多流量?

很多时候人们不知道这两个的区别

  • Liveness 检查在失效的时候会重启 pod
  • Readiness 检查在失效的时候会从 service 里面移除这个 pod(你可以通过 kubectl get endpoionts 查看),在这个检查恢复之前是不会有流量发送给这个 pod 的。

并且,两个都是在整个 pod 的生命周期里面持续检查的。这很重要。

人们有时会认为 readiness 检查只在 pod 启动的时候指示这个 pod 已经 Ready 并且可以接受请求了。但那个只是他的一个应用场景而已。

另外一个场景是指示在 pod 生命周期里面这个 pod 是不是接受了太多的请求了(或者一些比较高昂的计算),因而可以停止发送更多请求过去,让这个 pod 冷却下,当 readiness 恢复的时候再继续发送请求。这种情况下,如果 liveness 检查也失败可能适得其反。为啥要重启一个健康的并且处理很多请求的 pod 呢?

很多情况下两个的不设置好于设置错了。就像前面说的,如果 liveness 检查和 readiness 检查一样,你可能会遇到大问题。你可能需要从只设置 readiness 开始,因为 liveness 检查比较危险

不要因为依赖的服务 down 了就把其中任何一个设置为失效,这可能会导致所有 pod 集联失效。你等于是搬起石头砸自己脚

LoadBalancer for every http service

当你有很多 http 服务的时候你会需要把他们暴露给外界。

如果你暴露 kubernetes 服务为 type: LoadBalancer 类型,他的控制器(不同供应商不一样)会给你分配一个外部的 LB(一般不需要 L7 的,大都是 L4),当你创建比较多的这些(固定的 ipv4 地址,计算能力,按秒付费。。。)服务的时候可能会比较贵。

这种情况下,使用 type: NodePort 暴露你的服务,然后共享同一个外部负载均衡器会比较合理。或者更好一点的是,部署一个 nginx-ingress-controller(或者 traefik)作为外部的负载均衡器的入口,然后把所有流量都通过 kubernetes 的 ingress 资源来路由分配。

其他集群内部的(微)服务之间可以通过 CluterIP 服务类型来获得开箱即用的 dns 发现功能。小心不用使用公网的 DNS/IP,这可能导致延迟增加和费用增加。

non-kubernetes-aware cluster autoscaling

当你给集群增加或者减少节点的时候,你不需要考虑一些简单的指标,例如这些节点的 cpu 使用率。当编排 pod 的时候,你会使用很多的编排限制,例如 pod 或者节点的 affinities, taints 和 tolerations, resource requests, QoS 等等。外部的自动伸缩机制一般不理解这些限制,可能会导致问题。

想象一下有一个新的 pod 需要编排,但是所有的 CPU 都已经被请求(request)了,pod 会卡在 pending 状态。外部的自动伸缩机制会看到当前平均使用的 CPU(不是请求的)而不扩张(不增加新的节点)。pod 还是不会被编排。

缩减(减少集群里面的节点)通常比较难。想象一下你有一个有状态的 pod(有使用持久化的 volume),通常持久化的 volume 资源会属于某个特定的可用区(availability zone)而不能在整个 region 扩展。你的自定义伸缩机制移除这个 pod 运行的节点之后,pod 不能在一个不是这个可用区的节点上面编排。pod 还是会卡在 pending 状态。

cluster-autoscaler 正在被社区广泛使用,它运行在你的集群里面,集成了大部分主要的公有云的 API,理解这些限制,所以可以在上面的例子里面合理的扩展。它也知道是不是可以安静的缩减节点而不影响我们设置的任何限制而省钱。

Not using the power of IAM/RBAC

不要给程序使用使用永久密钥的 IAM 用户,应该使用零时的 role 或者 service account。

经常会看到,把 access key 和 secret key 硬编码到程序的配置里面,从来也不会轮转他们。使用 IAM 角色或者 service accounts 来代替。

跳过 kube2iam,像这篇文章描述的一样直接在 service account 使用 IAM 角色。

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

只需要一条 annotation,没那么难,是吧?

不是必须要的时候,不要给 service account 或者 instalce profile admin 和 cluster-admin 权限。这会稍微麻烦 一点,特别是基于 k8s RBAC,但是还是值得的。

self anti-affinities for pods

在一个 deployment 里面让 pod 运行 3 个复制,节点挂掉的时候,所有的复制都在这个节点。嗯?所有复制都在一个节点?kubernetes 难道不应该提供 HA 吗?

你不能指望 kubernetes scheduler 会给你的 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 会分布在不同的节点(这只会在编排阶段检查,而不是运行时,基于 requiredDuringSchedulingIgnoredDuringExecution )。

我们说的是 pod 基于节点名称的反亲和逻辑 - topologyKey: "kubernetes.io/hostname" ,而不是基于可用区。如果你真的需要 HA,那深入了解下这个话题吧。

no poddisruptionbudget

在 kubernetes 上面运行生产环境的工作负载。随着时间流逝,你的节点和集群会需要升级,或者下架机器。PodDisruptionBudget(PDB)是一个位于集群管理员和集群用户间的某种服务保证的 API 。

确保创建了 pdb 来避免在下线节点的时候造成不必要的服务故障。

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

集群用户可以使用这个来告诉集群管理员:嘿,我在这有一个 zookeeper 集群,不管你打算做啥,给我保留 2 个可用复制。

这有个更加深入的帖子

more tenants or envs in shared cluster

kubernetes 的命名空间不提供很强的隔离。

人们总觉得使用命名空间分开非生产环境和生产环境后,一个环境不会影响另一个环境。一定意义上是可能的 - resource requests and limits, quotas, priorityClasses - and isolation - affinities, tolerations, taints (or nodeselectors) 想要物理上隔绝会让事情变得很麻烦。

你如果想要两种类型的工作在一个集群里面,那就必须忍受这种复杂性。如果你不想这么复杂,那么多建一个集群是一个比较廉价的选择,把他们放到不同的集群里面可以得到很好的隔离。

externalTrafficPolicy: Cluster

经常会看到这个,NodePort 类型的服务默认情况下 externalTrafficPolicy: Cluster 所有的流量都会在集群内路由。这意味着,集群内每个节点都会开放这个 NodePort,因此你可以访问任何一个节点来访问到你的服务。

通常一个 NodePort 服务只运行在这些节点的子集上。这意味着,当访问其中某个并不运行这个服务的节点的时候,就需要转发这个请求到别的节点,这会导致多余的网络跳转,而增加延迟(如果这些节点在不同的 AZ/DC,延迟可能还会挺高)。

设置 externalTrafficPolicy: Local 后就不会在 pod 不在的节点上面暴露那些端口了,只会在真实运行这些 pod 节点上开放。如果你有一个外部的 LB 会检查他们的 endpoints(就像 ELB 那样),会只发送请求给这些节点。减少延迟,多余的计算,流量账单。

你可能会有一个 traefik 或者 nginx-ingress-controller 使用 NodePort (或者 LB,也使用 NodePort)来处理你的入口 http 流量和路由,那这个设置会极大的减少类似请求的延迟。

深入讨论这个的帖子

pet clusters + stressing the control plane too much

你会给服务器基于 Anton, HAL9000 and Colossus 起一些随机的名字,那么给集群起一个名字呢?

一开始验证 kubernetes 功能的时候,会给集群起名叫做 "testing",并且持续在生产环境使用这个名字,没人敢改?

Pet 集群可不好玩,你可能会需要删除你的集群,实践下灾难恢复,关心下你的控制节点。不敢动控制节点可不是个好现象。Etcd 死了?你遇到大问题了。

另一方面,经常改动也不一定好。一段时间后控制节点会变慢,可能会是因为你创建太多对象了(使用基于默认配置的 helm 会创建很多 configmaps/secrets,你可能会有上千个对象),或者你也可能经常通过修改 kube-api(为了 autoscaling, cicd, monitoring, logs from events, controllers, etc) 产生一些垃圾。

以及,看看你的 kubernetes 提供商提供的 "SLA/SLO" 保证。有的可能会保证控制节点的可用性,但是不保证对于你发送的请求的 p99 延迟。换句话说,你可能会需要等 10 分钟才能得到 kubectl get nodes 的结果,而不违反他们的服务保证。

bonus: using latest tag

这个比较经典。现在不常见了,可能因为我们有太多人被这个搞死了,而开始使用固定的版本号而不是 :latest 了。

ECR 现在提供了标签不可变的功能,值得一试。

Summary

别指望啥都会自动处理好,kubernetes 不是银弹。一个垃圾程序在 kubernetes 上面可能也还是垃圾程序(实际上还有可能更垃圾)。如果你不小心一点,事情可能会变得很复杂,压力很大,缓慢的控制节点,没有 DR 策略。别期待开箱即用的多租户功能和高可用。花点时间让你的程序变成 cloud native。