Categories
程式開發

使用Kubernetes最常见的10个错误


使用Kubernetes最常见的10个错误 1

使用kubernetes这么多年以来,我们见过的集群不计其数(包括托管的和非托管的,GCP、AWS和Azure上的都有),还见识了很多经常重复出现的错误。其中大部分错误我们自己也犯过,这没什么丢人的!

本文会给大家展示一些我们经常遇到的问题,并谈谈修复它们的方法。

1.资源:请求和限制

这无疑是最值得关注的,也是这个榜单上的第一名。

人们经常不设置CPU请求或将CPU请求设置得过低(这样我们就可以在每个节点上容纳很多Pod),结果节点就会过量使用(overcommited)。在需求较高时,节点的CPU全负荷运行,而我们的负载只能得到“它所请求的”数据,使CPU节流(throttled),从而导致应用程序延迟和超时等指标增加。

BestEffort(不要这样做):

 resources: {}

very low cpu(不要这样做):

resources:
      requests:
        cpu: "1m"

另一方面,启用CPU限制可能会在节点的CPU没有充分利用的情况下,对Pod进行不必要地节流,这也会导致延迟增加。人们也讨论过关于Linux内核中的CPU CFS配额,和因为设置了CPU限制并关闭CFS配额而导致的CPU节流问题。CPU限制造成的问题可能会比它能解决的问题还多。想了解更多信息,请查看下面的链接。

内存过量使用会给我们带来更多麻烦。达到CPU限制将导致节流,达到内存限制会导致Pod被杀。见过OOMkill(因内存不足而被杀死)吗?我们要说的就是这个意思。想要尽量减少这类状况?那就不要过量使用内存,并使用Guaranteed QoS(Quality of Service)将内存请求设置为与限制相等,就像下面的例子那样。了解更多信息,请参考Henning Jacobs(Zalando)的演讲

Burstable(容易带来更多OOMkilled):

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

Guaranteed:

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

那么我们设置资源时有什么诀窍呢?

我们可以使用metrics-server查看Pod(以及其中的容器)的当前CPU和内存使用情况。你可能已经启用它了。只需运行以下命令即可:

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

不过,这些只会显示当前的使用情况。要大致了解这些数据的话这就够用了,但我们到头来是希望能及时看到这些使用量指标(以回答诸如:昨天上午CPU使用量的峰值等问题)。为此我们可以使用Prometheus和DataDog等工具。它们只是从metrics-server接收度量数据并存储下来,然后我们就能查询和绘制这些数据了。

VerticalPodAutoscaler可以帮助我们自动化这一手动过程——及时查看cpu/内存的使用情况,并基于这些数据再设置新的请求和限制。

有效利用计算资源不是一件容易的事情,就像不停地玩俄罗斯方块。如果我们发现自己花了大笔钱购买计算资源,可是平均利用率却很低(比如大约10%),那么我们可能就需要AWS Fargate或基于Virtual Kubelet的产品。它们主要使用无服务器/按使用量付费的的计费模式,这对我们来说可能会更省钱。

2.liveness和readiness探针

默认情况下,Kubernetes不会指定任何liveness和readiness探针。有时它会一直保持这种状态……

但如果出现不可恢复的错误,我们的服务将如何重新启动呢?负载均衡器如何知道特定的Pod可以开始处理流量,或能处理更多流量呢?

人们通常不知道这两者间的区别。

  • 如果探针失败,liveness探针将重新启动Pod
  • Readiness探针失败时,会断开故障Pod与Kubernetes服务的连接(我们可以用kubectl get endpoints检查这一点),并且直到该探针恢复正常之前,不会向该Pod发送任何流量。

它们两个都运行在整个Pod生命周期中。这一点是很重要的。

人们通常认为,readiness探针只在开始时运行,以判断Pod何时Ready并可以开始处理流量。但这只是它的一个用例而已。

它的另一个用例是在一个Pod的生命周期中判断它是否因过热而无法处理太多流量(或一项昂贵的计算),这样我们就不会让它做更多工作,而是让它冷却下来;等到readiness探针成功,我们会再给它发送更多流量。在这种情况下(当readiness探针失败时),如果liveness探针也失败就会非常影响效率了。我们为什么要重新启动一个健康的、正在做大量工作的Pod呢?

有时候,不指定任何探针都比指定一个错误的探针要好。如上所述,如果liveness探针等于readiness探针,我们将遇到很大的麻烦。我们一开始可能只会指定readiness探针,因为liveness探针太危险了。

如果你的任何共享依赖项出现故障,就不要让任何一个探针失败,否则它将导致所有Pod的级联故障。我们这是搬起石头砸自己的脚

3.在所有HTTP服务上启用负载均衡器

我们的集群中可能有很多HTTP服务,并且我们希望将这些服务对外界公开。

如果我们将Kubernetes服务以type: LoadBalancer的形式公开,那么它的控制器(取决于供应商)将提供并协调一个外部负载均衡器(不一定是L7的,更可能是L4 lb);当我们创建很多这种资源时,它们可能会变得很昂贵(外部静态ipv4地址、计算、按秒计费……)。

在这种情况下,共享同一个外部负载均衡器可能会更好些,这时我们将服务以type: NodePort的形式公开。或者更好的方法是,部署nginx-ingress-controller(或traefik)之类的东西,作为公开给这个外部负载均衡器的单个NodePort端点,并基于Kubernetes ingress资源在集群中路由流量。

其他相互通信的集群内(微)服务可以通过ClusterIP服务和开箱即用的DNS服务发现来通信。注意不要使用它们的公共DNS/IP,因为这可能会影响它们的延迟和云成本。

4.无Kubernetes感知的集群自动缩放

在集群中添加节点或删除节点时,不应该考虑一些简单的度量指标,比如这些节点的CPU利用率。在调度Pod时,我们需要根据许多调度约束来进行决策,比如Pod和节点的亲密关系(affinities)、污点(taints)和容忍(tolerations)、资源请求(resource requests)、QoS等。让一个不了解这些约束的外部自动缩放器(autoscaler)来处理缩放可能会招来麻烦。

假设有一个新的Pod要被调度,但是所有可用的CPU都被请求了,并且Pod卡在了Pending状态。可是外部自动缩放器会查看当前的平均CPU使用率(不是请求数量),然后决定不扩容(不添加新的节点)。结果Pod也不会被调度。

缩容(从集群中删除节点)总是更难一些。假设我们有一个有状态的Pod(连接了持久卷),由于持久卷(persistent volumes)通常是属于特定可用区域的资源,并且没有在该区域中复制,我们自定义的自动缩放器会删除一个带有此Pod的节点,而调度器无法将其调度到另一个节点上,因为这个Pod只能待在持久磁盘所在的那个可用区域里。Pod将再次陷入Pending状态。

社区正在广泛使用cluster-autoscaler ,它运行在集群中,能与大多数主要的公共云供应商API集成;它可以理解所有这些约束,并能在上述情况下扩容。它还能搞清楚是否可以在不影响我们设置的任何约束的前提下优雅地缩容,从而节省我们的计算成本。

5.不要使用IAM/RBAC的能力

不要使用IAM Users永久存储机器和应用程序的秘钥,而要使用角色和服务帐户生成的临时秘钥。

我们经常看到这种情况,那就是在应用程序配置中硬编码访问(access )和密钥(secret),并在使用Cloud IAM时从来不轮换密钥。我们应该尽量使用IAM角色和服务帐户来代替Users。

请跳过kube2iam,直接按照Štěpán Vraný在这篇博文中介绍的那样,使用服务账户的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。没那么难做吧。

另外,当服务帐户或实例配置文件不需要admincluster-admin权限时,也不要给它们这些权限。这有点困难,尤其是在k8s RBAC中,但仍然值得一试。

6.Pod的self anti-affinities

某个部署有3个Pod副本正在运行,然后节点关闭了,所有的副本也都随之关闭。岂有此理?所有副本都在一个节点上运行?Kubernetes难道不应该很厉害,并提供高可用性的吗?!

我们不能指望Kubernetes调度程序为我们的Pod强制使用anti-affinites。我们必须显式地定义它们。

// 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 )。

我们讨论的是不同节点名称上( topologyKey: "kubernetes.io/hostname" )的podAntiAffinity,而不是不同可用区域的podAntiAffinity。如果你确实需要很好的可用性水平,可以在这个主题上再深入做些研究。

7.无PodDisruptionBudget

我们在Kubernetes上运行生产负载。我们的节点和集群必须不时升级或停用。PodDisruptionBudget(pdb)是一种用于在集群管理员和集群用户之间提供服务保证的API。

请确保创建了pdb ,以避免由于节点耗尽而造成不必要的服务中断。

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

作为一个集群用户,我们可以告诉集群管理员:“嘿,我这里有个zookeeper服务,无论如何我都希望至少有2个副本是始终可用的”。

我在这篇博客文章中更深入地讨论了这个话题。

8.共享集群中有不止一个租户或环境

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

人们似乎期望,如果将非生产负载放到一个命名空间,然后将生产负载放到生产命名空间,那么这些负载之间就永远不会相互影响了。我们可以在某种程度上公平分配(比如资源的请求和限制、配额、优先级)并实现隔离(比如affinities、tolerations、taints 或nodeselectors),进而“物理地”分离数据平面上的负载,但这种分离是相当复杂的。

如果我们需要在同一个集群中同时拥有这两种类型的负载,那么就必须要承担这种复杂性。如果我们用不着局限在一个集群里,而且再加一个集群的成本更低时(比如在公共云上),那么应该将它们放在不同的集群中以获得更强的隔离级别。

9.externalTrafficPolicy: Cluster

经常看到这种情况,所有流量都在集群内路由到一个NodePort服务上,该服务默认使用 externalTrafficPolicy: Cluster 。这意味着在集群中的每个节点上都打开了NodePort,这样我们可以任选一个来与所需的服务(一组Pod)通信。

使用Kubernetes最常见的10个错误 2

通常情况下,NodePort服务所针对的那些Pod实际上只运行在这些节点的一个子集上。这意味着,如果我与一个没有运行Pod的节点通信,它将会把流量转发给另一个节点,从而导致额外的网络跳转并增加延迟(如果节点位于不同的AZs或数据中心,那么延迟可能会很高,并且会带来额外的出口成本)。

在Kubernetes服务上设置externalTrafficPolicy: Local,就不会在每个节点上都打开NodePort,只会在实际运行Pod的节点上开启它。如果我们使用一个外部负载均衡器来检查它端点的运行状况(就像AWS ELB所做的那样),它就会只将流量发送到应该接收流量的节点上,这样就能改善延迟、减少计算开销、降低出口成本并提升健全性。

我们可能会有像traefik或nginx-ingress-controller之类的东西,被公开成NodePort(或使用NodePort的负载均衡器)来处理入口HTTP流量路由,而这种设置可以极大地减少此类请求的延迟。

这里有一篇很棒的博客文章,更深入地讨论了externalTrafficPolicy和它们的权衡取舍

10.把集群当宠物+控制平面压力过大

你有没有过这样的经历:给服务器取Anton、HAL9000或Colossus之类的名字(都是带梗的名称,译注),或者给节点随机生成id,却给集群取个有含义的名称?

还可能是这样的经历:一开始用Kubernetes做概念验证,给集群取名”testing”,结果到了生产环境还没给它改名,结果谁都不敢碰它?(真实的故事)

把集群当宠物可不是开玩笑的,我们可能需要不时删除集群,演练灾难恢复并管理我们的控制平面。害怕触碰控制平面不是个好兆头。Etcd挂掉了?好嘞,我们遇到大麻烦。

反过来说,控制平面也不要用过头了。也许随着时间的流逝,控制平面变慢了。这很可能是因为我们创建了很多对象而没有轮换它们(使用helm时常见的情况,它的默认设置不会轮换configmaps/secrets的状态,结果我们在控制平面中会有数千个对象),或者是因为我们不断从kube-api(用于自动伸缩、CI/CD、监视、事件日志、控制器等)中删除和编辑了大量内容。

另外,请检查托管Kubernetes提供的“SLAs”/SLOs和保证。供应商可能会保证控制平面(或其子组件)的可用性,但不能保证发送给它的请求的p99延迟水平。换句话说,就算我们kubectl get nodes后用了10分钟才得到正确结果,也没有违反服务保证。

附赠一条:使用latest标签

这一条是很经典的。我觉得最近它没那么常见了,因为大家被坑的次数太多,所以再也不用 :latest ,开始加上版本号了。这下清静了!

ECR有一个标签不变性的强大功能,绝对值得一试。

总结

别指望所有问题都能自动解决——Kubernetes不是银弹。即使是在Kubernetes上,一个糟糕的应用程序还会是一个糟糕的应用程序(实际上,甚至还可能更糟糕)。如果我们不够小心,最后就会遇到一系列问题:太过复杂、压力过大、控制平面变慢、没有灾难恢复策略。不要指望多租户和高可用性是开箱即用的。请花点时间让我们的应用程序云原生化。

英文原文:

10 most common mistakes using kubernetes