本文基于对 Hidden Kubernetes Bad Practices Learned the Hard Way During Incidents 的阅读后,在尊重原文内容的基础上进行了本地化翻译与整理,在语义表达上相较于机翻,更加贴近中文母语者的阅读习惯,提升整体的可读性与理解成本。
在每个事件的原文翻译之后,都会补充我的个人注解,也算是对原作者没有提到的内容进行补充。所有个人注解均会明确标识,并独立于原文翻译部分呈现,不会穿插在原文内容之中,以避免对原意造成干扰。
原文一共 7 个故障,本文去掉了故障二的翻译解析,因为我没用过 ArgoCD...
Kube-proxy 默认负载均衡机制导致 HTTP/2 流量热点问题
原文翻译
本次事故发生在首次把 HTTP/2 流量引入 Kubernetes 时。我们发现 HTTP/2 流量在后端 Pod 之间的分布出现了异常负载。其表现为某个 Pod 持续出现 CPU 使用率和延迟都很高,但监控指标中同一服务下的其他 Pod 副本几乎完全处于空闲状态。
我们错误地认为 HTTP/2 流量在 kube-proxy 中的行为会和 HTTP/1.x 一样,但事实并非如此。kube-proxy 对这两种协议的处理方式并不一样。默认情况下,kube-proxy 在四层(TCP 层)进行负载均衡,它是通过 连接级别 而不是 请求级别 分发流量:
当客户端向 Kubernetes Service 建立一个 TCP 连接时,kube-proxy 会使用轮询算法选择一个后端 Pod。该 TCP 连接上的所有请求都会被转发到同一个 Pod 上。
对于 HTTP/1.x 客户端来说,这种方式通常没有问题,因为客户端一般会建立多个短连接(生命周期较短),流量因此可以较为均匀地分布。但对于单个连接多路复用处理所有请求的 HTTP/2 客户端来说,这种机制会导致所有请求都被转发到同一个 Pod,其余 Pod 大多处于空闲状态。最终该 Pod 上的 CPU 使用率和请求延迟显著上升成为性能瓶颈。
由于对 kube-proxy 连接处理机制理解有限,这个问题在最初阶只能通过调整应用层的 keep-alive 配置暂时缓解。但从长期来看,引入支持 HTTP/2 的服务网格,并通过七层负载均衡来解决才是最终方案。 关键结论:
默认情况下,通过 Service 的流量,是由 kube-proxy 在 TCP 连接级别进行负载均衡的,在与 HTTP/2 的多路复用机制结合使用时,容易导致流量分布不均。如果没有七层路由能力,单个 Pod 可能会成为瓶颈,而其他 Pod 却处于空闲状态,从而引发性能下降。
个人注解
在注解前需要先明确几点:
k8s svc 只是一个 转发规则,kube-proxy 才是将访问 svc 的流量转发到 Pod 的执行者
以上都发生在 TCP 层(L4)
这个问题的核心原因在于:
kube-proxy 默认是四层负载均衡,只在 TCP 连接建立(SYN)时选择一个 Pod
HTTP/2 一般使用单个连接多路复用处理所有请求
flowchart TB Client["Client
(HTTP/2 长连接)"] Service["k8s svc
(虚拟 IP)"] KubeProxy["kube-proxy
(L4 / TCP
连接建立时选择 Pod)"] PodA["od A
(导致高 CPU / 高延迟)"] PodB["od B
(几乎空闲)"] PodC["od C
(几乎空闲)"] Client -->|1 个 TCP 连接| Service Service --> KubeProxy KubeProxy -->|连接绑定| PodA KubeProxy -.-> PodB KubeProxy -.-> PodC所以他的正确解法应该是引入支持 HTTP/2 的七层负载均衡或服务网格:
这是我在资源管理中遇到过最困惑的事件之一。乍一看,集群状态完全正常,节点监控面板显示 CPU 仍然充足,也没有任何资源耗尽的告警。然而从用户体验来看,应用却频繁出现延迟飙升和请求超时。
最终定位到的根因是 Pod 级别配置了 CPU limit,这导致 CPU 被限流,从而拖慢了应用的执行。
原因在于:与内存不同,CPU 并不是一种可被消耗殆尽的资源,它是可压缩的,并且在每一个调度周期都会重新分配。每个调度周期(CFS 调度周期)中,CPU 时间都会被分配、回收并重新分发。
在配置了 CPU limit 的情况下,即使节点上仍然有空闲 CPU,但如果 Pod 达到了配置的 CPU limit 上限,也无法继续使用这些空闲的 CPU。
当容器触及 CPU limit 时:
内核会对容器进行限流(throttling)
进程会被暂停,直到下一个 CPU 调度周期
即使节点上存在空闲 CPU,容器也无法突破其限制进行突发使用
CPU 限流相关的指标是定位该问题根因的关键信号,尤其是 container_cpu_cfs_throttled_seconds_total 和 container_cpu_cfs_throttled_periods_total。将这些指标与延迟曲线进行关联分析,可以清晰地看到延迟峰值与 CPU 限流之间的高度一致性。
移除 CPU limit,仅保留 CPU request,就足以避免限流问题。这样既可以保证 Pod 按比例获得 CPU 资源,又能防止某些 Pod 过度抢占 CPU。 关键结论:
配置 CPU limit 可能会出现节点 CPU 空闲的情况下,让 Pod 遇到性能瓶颈,使得延迟问题难以诊断。在大多数场景下,合理配置 CPU request 就已经足够,既允许工作负载在需要时进行突发使用,又能避免 CPU 限制问题。
个人注解
需要注意的是,当 Pod 达到了配置的 mem limit 上限时,会触发 oomkill 导致 Pod 重启。但 CPU limit 的本质是:
一旦容器在某个 CFS 周期内用完了分配的 CPU 时间片,就会被内核直接暂停
这个暂停行为与节点是否还有空闲 CPU 无关
我的观点与作者结论一致,我认为 CPU limit 仅能作为隔离工具,而不是性能优化工具。除非你明确希望某些业务不希望占用过多 CPU。
关键业务未设置 PriorityClass
原文翻译
试想一下,核心业务 Pod 在集群中被驱逐,且在短时间内无法重新调度,而一些不那么重要的工作负载却仍在正常运行。
当你没有为关键部署设置 PriorityClass 时,在集群资源充足的情况下,一切看起来都很正常。但一旦集群遭遇资源压力(mem、cpu、disk),k8s 就会在节点中驱逐某些 Pod。
Kubelet 会基于服务质量(QoS:Quality Of Service)来驱逐 Pod。如果多个 Pod 属于同一种 QoS 类别(这种情况非常常见),那么其中任何一个 Pod 都可能被驱逐。
更糟糕的问题出现在后续的重新调度阶段。被驱逐的 Pod 可能会卡在 Pending 状态,并输出如下信息:
preemption: No victims found for incoming pod
复制代码
由于所有 Pod 拥有相同的优先级,抢占(preemption)机制无法生效,而此时所有节点又都被占满。 关键结论:
仅设置 resource requests 和 limits 并不足以在资源压力下保证存活。它们只能延缓 Pod 被驱逐,但并不能保证调度优先级。对于关键工作负载,必须始终定义 PriorityClass。
个人注解
简单来说 QoS 解决的是 "谁能活下来",而 PriorityClass 则是 "谁更重要"。
由于 Pod 卡在 Terminating 状态导致的 EKS IP 耗尽问题
原文翻译
想象一下,有 100 多个 Pod 卡在 Terminating 状态,而新的 Pod 因为 IP 耗尽而无法被调度。
在删除一个自定义 Operator 后,该 Operator 下所有带 finalizer 的 Pod 都卡在了 Terminating 状态。由于 EKS 会为 Pod 分配真实的 VPC IP 地址,这些正在终止的 Pod 仍然持续占用 IP,最终耗尽了子网中的 IP 资源,导致新的 Pod 无法被调度。
通过定位被 finalizer 阻塞的 Pod,并谨慎地移除这些 finalizer,使 Pod 得以真正删除并释放 IP,从而缓解了这次事故。当子网的 IP 容量恢复后,调度重新开始,集群也恢复了稳定。
批量移除 Pod 中 finalizer 的方法如下: