作者:Tj Blogumas
在多年使用 Kubernetes 的过程中,我们接触了相当多的 K8s 集群,同样也犯了许多错误。本文就介绍了那些最容易也最常犯的 10 个错误,并讨论了要如何解决。
资源请求和限制
这绝对是犯错榜单的第一名。设置 CPU 请求有两种常见错误:不设置或者设置的很低。虽然这样可以在每个节点上容纳更多的 Pod,但会导致节点的过度使用。在高需求时期,节点的 CPU 会被完全占用,工作负载获得的请求资源会受到 CPU 限制,从而导致应用程序延迟、超时等情况。
不设置 CPU 请求的配置:
resources: {}
CPU 请求设置很低的配置:
resources:
另一方面,即使节点的 CPU 没有充分利用,如果设置了不必要的 CPU 限制同样会限制 Pod,这也会导致延迟增加。
内存的过量使用一样会带来许多问题。达到 CPU 限制值会导致延迟,而达到内存限制值,Pod 会被直接杀死,这就像是 OOMkill,一个内存不足时会自动杀死进程的机制。如果不想发生这样的事情,就不要过度使用内存,而应该使用 Guaranteed QoS,设置内存请求值等于限制值。
Burstable QoS 下的资源设置:
resources:
Guaranteed QoS 下的资源设置:
resources:
在设置资源时,我们可以使用 metrics-server 查看容器当前 CPU 和内存的使用情况。如果它已经在服务器端运行,可以运行以下命令:
kubectl top pods
通过显示的当前使用情况,我们就可以大致了解资源情况了。如果想要及时查看情况指标,例如峰值,昨天早晨的 CPU 使用情况等,我们可以使用 Prometheus、DataDog 等。它们会从 metrics-server 中获取指标并进行存储,然后我们就可以查询或绘制图形。另外 ,VerticalPodAutoscaler 工具可以帮助我们自动化地查看 CPU、内存的使用情况,并根据情况重新设置新的请求值和限制。
liveness 和 readiness 探针的设置
默认情况下,系统不会设置 liveness 和 readiness 探针。K8s 强大的自愈能力有时候可以让容器一直工作下去。但如果出现不可恢复的错误时,服务要如何重新启动?负载均衡器如何判断 Pod 是否可以开始处理流量,是否可以继续处理更多流量?
很多人不知道 liveness 和 readiness 探针之间的区别:
- 如果对 Pod 的 liveness 探测失败,会重新启动该 Pod
- 如果对 Pod 的 readiness 探测失败,会将 Pod 和 Kubernetes 断开连接(可以使用 kubectl get endpoints 进行检查),并且在下次探测成功之前,都不会发送流量
注意,这两种探针要在 Pod 整个生命周期中运行。
很多人只知道 readiness 的一个应用场景:readiness 探针在容器启动时运行,以告知 K8s 服务 Pod 何时就绪,可以开始为流量提供服务。
它的另一个应用场景是告诉用户,在 Pod 的生命周期内,Pod 有没有因为太“热”而无法处理过多的流量,需要减少工作“冷静”一下。直到 readiness 探测成功时,我们再继续给 Pod 发送更多流量。在这种情况下, readiness 探测失败就会适得其反,因为我们不需要重新启动这个运行状况良好的 Pod。
有时候,不配置任何探针会比配置错误探针要好。就像上面说的,如果将 liveness 探针配置成和 readiness 探针一样,那么会导致很多问题。一开始建议仅配置 readiness 探针,因为 liveness 探针很危险。
如果有一个和其他 Pod 有共享依赖项的 Pod 被关闭,我们就要保证这个 Pod 的任何一个探针都不能失败,否则将导致所有 Pod 的级联失效,这就像是搬起石头砸自己的脚。
HTTP 服务的负载均衡器
我们可能在集群中有更多的 HTTP 服务,并对外开放。如果将 Kubernetes 服务设置为 type: LoadBalancer
,那么其控制器将提供并配置一个外部负载均衡器(不一定是 L7 负载均衡器,有可能是 L4 负载均衡器),并且这些资源(外部静态 IPv4 地址、计算硬件等)可能会变得很贵,因为创建了很多这样的服务。
在这种情况下,我们将外部访问方式设置为 type: NodePort
,并共享一个外部负载均衡器会更好。另外,还有一个其他不错的方法是部署一个类似 Nginx-ingress-controller(或 traefik)的东西作为暴露给外部负载均衡器的单个 NodePort endpoint,并根据 Kubernetes ingress resource 配置在集群中分配路由流量。
集群内的(微)服务可以通过 ClusterIP 服务和 DNS Service Discovery 进行通信。注意不要使用公共 DNS/IP,这会影响延迟并增加云成本。
无 K8s 感知的集群自动伸缩
在集群中添加节点或从集群中删除节点时,不应只考虑节点 CPU 使用率等简单指标。在调度 Pod 时,我们需要根据许多调度约束(例如 Pod 和节点的亲和力、taints、tolerations、资源请求、QoS 等)来决定。
假设有一个新的 Pod 要调度,但是所有可用的 CPU 都被占用,并且 Pod 处于 Pending 状态。外部自动伸缩器看到当前使用的 CPU 平均使用率非常高,就不会扩展(不会将 Pod 添加为节点),也就是说该 Pod 不会被调度。
扩展(从集群中删除节点)总是会更加困难。假设有一个有状态的 Pod 已连接持久卷,由于持久卷通常属于特定的可用性区域,并且不能在该区域中复制,因此自定义自动伸缩器删除带有该 Pod 的节点时,调度程序会无法将其调度到另一个节点上,因为它受到持久卷唯一可用性区域的限制,Pod 会再次卡在 Pending 状态。
K8s 社区广泛使用集群自动伸缩器,它运行在集群中,并与大多数主要公有云供应商的 API 集成,可以理解这些限制,并且在上述情况下进行扩展。另外,它还会确定是否可以在不影响设置的情况下正常扩展,以节省计算成本。
不使用 IAM、RBAC 的功能
不要将具有永久 secret 的 IAM User 用于机器和应用程序,而应该使用角色(role)和服务帐号(service account)生成的临时 secret。
我们经常看到这种情况:在应用程序配置中对访问权限和密钥进行硬编码,使用 Cloud IAM 时就永远不轮换 secret。我们应该在适当的地方使用 IAM Roles 和服务帐户代替 IAM User。
跳过 kube2iam,直接使用服务帐户的 IAM Roles。
apiVersion: v1
另外,在不是必要时,千万不要将 admin 和 cluster-admin 的权限给予服务帐户或实例配置文件。
Pod 亲和性
运行某个部署的 3 个 Pod 副本时,如果该节点下线,所有副本都将会随之下线。我们不能指望 Kubernetes 调度器对 Pod 强加亲和性设置,而要自己明确定义它们。
// omitted for brevity
这样可以确保将 Pod 调度在不同的节点上(仅在调度时间,而不是在执行时间进行检查,因此要设置 requiredDuringSchedulingIgnoredDuringExecution
)。
没有 PodDisruptionBudget
在 Kubernetes 上运行生产工作负载时,节点和集群必须不时地升级或停用。PodDisruptionBudget(PDB)是一种 API,为集群管理员和集群用户提供服务保证。
确保创建 PDB 以避免由于节点停用而造成不必要的服务中断。
apiVersion: policy/v1beta1
作为集群用户,我们就可以告诉集群管理员:“嘿,我这里有个 zookeeper 服务,无论要做什么,都至少要有 2 个副本始终可用。”
共享集群中太多租户或环境
Kubernetes 命名空间不提供任何强隔离。大家通常认为,如果将非生产工作负载分离到一个命名空间,然后再将生产工作负载分离到另一个命名空间,那么二者就永远不会互相影响,这样就可以实现某种程度的公平分配,比如资源的请求和限制、配额、优先级等,并实现隔离(比如 affinities、tolerations、taints 或 nodeselectors),进而“物理地”分离数据平面上的负载,但这种分离是相当复杂的。
如果需要在同一集群中同时使用两种类型的工作负载,我们就必须承担这种复杂性。如果不需要这样,并且再用一个集群成本更低时(例如在公有云中),那么最好将其放在另一个集群中以实现更高的隔离级别。
externalTrafficPolicy:Cluster
我们经常看到这种情况,所有流量都在集群内路由到 NodePort 服务上,该服务默认 externalTrafficPolicy: Cluster
,这意味着集群中的每个节点都打开了 NodePort ,这样可以使用任何一个与所需的服务(一组 Pod)进行通信。
通常,NodePort 服务为针对的 Pod 仅运行在那些节点的子集上。这意味着,如果与未运行 Pod 的节点通信,它会将流量转发到另一个节点,从而导致额外的网络跳转并增加延迟。
在 Kubernetes 服务上设置 externalTrafficPolicy: Local
后就不会在每个节点上打开 NodePort,只会在实际运行 Pod 的节点上打开。如果使用外部负载均衡器对其终端节点进行检查,它会仅将流量发送到应该去往的那些节点,可以改善延迟并减少计算开销和出口成本。
把集群当宠物,控制平面压力大
大家有没有这样的经历:给服务器取一些奇怪的名字;给节点随机生成 ID;亦或者因为一开始用 Kubernetes 做验证,所以给集群取名“testing”,结果到生产环境还在使用“testing”名字。
把集群当宠物可不是开玩笑的,我们需要不时地删除集群,演练灾难恢复,并管理控制平面。另一方面,过多地使用控制平面也不是一件好事。随着时间的流逝,控制平面变慢了,很可能就是我们创建了很多对象但没有轮换它们。
总结
不要指望一切都会自动解决,Kubernetes 并不是万能的。即使在 Kubernetes 上,糟糕的应用一样是糟糕的应用,一不小心,就可能会导致很多复杂问题。希望本文总结的十个常见错误,可以对你带来帮助。