现象:

观察到磁盘空间不足的节点有大量pod处于Evicted 0/1状态,但并未进行重新调度。

查看到我的集群中有如下事件:

failed to garbage collect required amount of images

原因描述:

当容器集群中的节点(宿主机)磁盘使用率达到85%之后,会触发自动的容器镜像回收策略,以便于释放足够的宿主机磁盘。该事件发生于当触发镜像回收策略之后,磁盘空间仍然不足以达到健康阈值(默认为80%)。通常该错误是由于宿主机磁盘被占用太多导致。当磁盘空间占用率持续增长(超过90%),会导致该节点上的所有容器被驱逐,也就是当前节点由于磁盘压力不再对外提供服务,直到磁盘空间释放。

磁盘空间不足是由于一些历史原因导致的。在我的K8S集群中采用EFK的日志架构去收集日志,但是由于历史原因(从虚拟机转型到K8S),日志文件通过hostpath挂载到主机目录,并且通过在K8S node上通过systemd启动flume logtail组件去抓取挂载到宿主机的日志。这就导致两个问题:

  1. pod不是固定于某个节点上的,而是会随机调度,这也就造成有可能多个pod调度到一个主机上,并打印日志到同一个文件中去。
  2. hostpath的主机目录中的日志文件会不断堆积,引发本文中遇到的问题。

pod驱逐但未被删除的原因可以参考以下链接:

Kubelet does not delete evicted pods · Issue #55051 · kubernetes/kubernetes · GitHub

这样设计作可能是为了方便定位问题原因,而且如果大面积的pod被驱逐导致重启,这将对apiserver和etcd造成不小的冲击压力。

值得一提的是,当statefulset进入failed状态会自动重启,而deployment和damonset就不会。这里不作深入讨论。

https://github.com/kubernetes/kubernetes/blob/52eea971c57580c6b1b74f0a12bf9cc6083a4d6b/pkg/controller/statefulset/stateful_set_control.go#L386-L393 

k8s监控容器磁盘的读写 k8s磁盘限制_日志文件

解决方法:

我们从多个纬度来看待和处理这个问题:

  1. 从日志系统角度看,更优雅的做法应该是通过logtail sidecar和业务容器共同挂载emptydir的方式去抓取日志,emptydir属于临时存储(ephemeral-storage),占用的是kubelet根目录下的空间或者内存空间,可以考虑在podtemlate下的ephemeral-storage limit为其加入最大存储用量来防止超出预期的用量。此外当pod重启时,这部分空间会自动回收,不会保留日志文件,也就不会有大文件堆积的担忧。
  2. kubelet驱逐条件默认磁盘可用空间在10%以下,我们会更倾向调整云监控磁盘告警阈值以提前告警,如果是部署在集群内的prometheus,必须在prometheus被驱逐前通过各种告警手段通知到运维。
  3. kube-controller-manager可以设置--terminated-pod-gc-threshold为1,这个参数的意义表示controller manager可以容忍的最大的被驱逐的pod数量,如果超出该值,则会触发pod gc,将这些pod删除,以达到pod被驱逐后自动重启的目的。
  4. kube-scheduler配置了--policy-config-file下的CheckNodeDiskPressure策略项,当pod在调度时将磁盘使用率作为调度的前提条件之一,如果某个Node的磁盘使用率已经超过了阈值,则该Node将被标记为不可调度的。这意味着,Scheduler将不会将新的Pod绑定到该Node上,直到磁盘使用率恢复到安全水平为止。这个配置默认是true。
  5. 引入自动化脚本来定时删除被驱逐的pod也是个不错的选择。
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: delete-failed-pods
spec:
  schedule: "*/30 * * * *"
  failedJobsHistoryLimit: 1
  successfulJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: kubectl-runner
            image: wernight/kubectl
            command: ["sh", "-c", "kubectl get pods --all-namespaces --field-selector 'status.phase==Failed' -o json | kubectl delete -f -"]
            restartPolicy: OnFailure