一次 KubeCPUOvercommit 告警排查过程小记

问题现象#

在配置好 Prometheus Operator Chart 一段时间后,我们决定开启由 kubernetes-mixin 项目提供的 alert rules,并将 AlertManager 和 Opsgenie 集成。在其后的几天里,每到傍晚六点钟左右就会出现 KubeCPUOvercommit 的告警:

排查过程#

最初的猜测#

根据报警信息,我们第一反应打开了 Grafana,开始查看 Compute Resources / Cluster dashboard 的数据,其中一个 panel 为 CPU Requests Commmitment,此时的数据确实超过了 100%

但我们集群内安装了 Cluster Autoscaler,理论上不会出现 CPU Resource Requests 超量的情况,我们也在收到报警的时刻做了确认,当时并没有发现 unscheduled Pods。

难道是误报?我们打开了 panel 详情,提取了其中的表达式:

sum(kube_pod_container_resource_requests_cpu_cores{cluster="$cluster"})
/
sum(kube_node_status_allocatable_cpu_cores{cluster="$cluster"})

随后通过 Grafana Explore 面板查看 kube_pod_container_resource_requests_cpu_cores metric,发现它并不包含任何关于 Pod status 的 label,于是我们开始怀疑:是否是此 metric 包含了 failed 和 completed 的 Pod 导致的?

验证猜测#

我们手动删除了一部分 failed 和 completed Pods 后 alert closed,Grafana dashboard 上的显示也降到了 100% 以下。我们以为找到了问题的原因,正当我们修复了 panel 的表达式,准备调整 alert rule 的时候,发现该报警规则的表达式(来源)是:

sum(namespace:kube_pod_container_resource_requests_cpu_cores:sum)
  /
sum(kube_node_status_allocatable_cpu_cores)
  >
(count(kube_node_status_allocatable_cpu_cores)-1) / count(kube_node_status_allocatable_cpu_cores)

它的含义为:Pod所需要的CPU核心总数 / 可分配的核心总数 > (node数量 - 1) / node数量 则报警。

此处使用的是 namespace:kube_pod_container_resource_requests_cpu_cores:sum,并非 kube_pod_container_resource_requests_cpu_cores,因此之前的猜想错误。

再次 debug#

研究发现,namespace:kube_pod_container_resource_requests_cpu_cores:sum 是 Prometheus Operator Chart 自定义的 recording rule

Recording rule 主要用于预先计算那些使用率高或是计算量较大的表达式,并将其结果保存为一组新的时间序列,这样不仅能够提高报警规则的计算速度,同时可以达到复用复杂表达式的效果。

level:metric:operations 是官方推荐的 recording rules 命名协定

经过一番查找,该 recording rule 的表达式为(来源):

sum by (namespace) (
    sum by (namespace, pod) (
        max by (namespace, pod, container) (
            kube_pod_container_resource_requests_cpu_cores{job="kube-state-metrics"}
        ) * on(namespace, pod) group_left() max by (namespace, pod) (
          kube_pod_status_phase{phase=~"Pending|Running"} == 1
        )
    )
)

稍微有点长,我们简化一下,先看内层。

max by (namespace, pod, container) (
    kube_pod_container_resource_requests_cpu_cores{job="kube-state-metrics"}
)

* on(namespace, pod) group_left()

max by (namespace, pod) (
  kube_pod_status_phase{phase=~"Pending|Running"} == 1
)

on(namespace, pod) group_left() 是 PromQL 的语法之一,直译为 向量匹配。本例的含义为:依次查找运算符(*)左侧样本中 namespacePod 相匹配的右侧样本,若有匹配则参与运算并形成新的时间序列,若未找到则直接丢弃样本。

另外此处的条件 kube_pod_status_phase{phase=~"Pending|Running"} == 1,表示 Pod 的状态必须是 pending 或 running,否则样本会被丢弃。

最后,在外层再进行一些聚合运算,recording rule namespace:kube_pod_container_resource_requests_cpu_cores:sum 就计算完成了:

sum by (namespace) (
    sum by (namespace, pod) (
        ...
    )
)

经过排查,否定了我们的猜想,因为该时间序列已经排除了非 pending 或 running 状态的 Pod,它们的 resource requests 数值不会纳入到最终的样本中。

新的猜测和证实#

确定 namespace:kube_pod_container_resource_requests_cpu_cores:sum 没有问题后,我们仔细阅读告警的描述

Cluster has overcommitted CPU resource requests for Pods and cannot tolerate node failure.

恍然发现它表述的意思是:集群已承受超量的 Pod CPU 资源请求,无法容忍 node 失效(例如节点崩溃、drain/taint 造成的 eviction 等)。

这么说来就可以解释为何 CPU 核心数的利用率 要与 (node数量 - 1) / node数量 做比较了。可简单地理解为:目前集群中的工作负载已不容许出现 Node 数量减少 1,一旦有 node 失效,Pod 所需要的 CPU 资源就超出集群的资源总量了。

问题解释#

综上所述,该 alert 属于「预警」,并非「报警」。它的出现是为了提示我们,为了保证高可用性应该扩容集群了。而不是已经出现了 unscheduled Pod。

因为有 Cluster Autoscaler 的存在,扩容操作不需要我们手动执行,因此文章开头提到的「手动删除一部分 failed Pod 后恢复正常」实际上是一个「迷惑行为」,此时刚好碰上新的 node 启动完成而已。

解决方案#

为了能够保证集群的超高可用(至少可以容忍一台 node 失效),我们需要 overprovisioning。虽然 Cluster Autoscaler 并没有直接提供参数实现,但我们在 FAQ 发现了一个小技巧:How can I configure overprovisioning with Cluster Autoscaler

大致思路如下:

  1. 创建较低的 PriorityClass,例如名为 overprovisioning 值为 -1
  2. 创建 Deployment 作为「占位」,将 spec.template.spec.priorityClassName 设置为 overprovisioning,同时配置 containers 的 resource requests 为预留的资源数量。
  3. Cluster Autoscaler 将会 overprovisioned 一部分 node 以满足占位 Pods 的 resource requests。
  4. 因为占位 Pods 优先级较低,在其它优先级较高的 Pods 需要资源时,占位 Pods 将会被驱逐,将预留的资源提供给其它 Pods。
  5. 此时占位 Pods 无法被调度,Cluster Autoscaler 将会再次 overprovision 新的 node。

最终,在 kubernetes-mixin 项目中,修改告警规则的 ignoringOverprovisionedWorkloadSelector 配置。使用 Selectors 忽略 overprovisioned 的 Pods,使其不纳入工作负载的计算,即可让 Cluster Autoscaler 始终让我们的服务器保证多出当前的需求一台,来保证高可用性。

结语#

以上就是我们透过一次「误报」,排查后发现集群可用性存在的问题,并最终做出解决方案提高了可用性的经历。如果你有更好的实践,欢迎通过通过留言与我们交流。

本作品采用《CC 协议》,转载必须注明作者和本文链接
欢迎关注我们的微信公众号「RightCapital」
RightCapital
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。