Kubernetes 运行中的 6 个有趣的系统错误 [及其解决方案]

Kubernetes 运行中的 6 个有趣的系统错误 [及其解决方案]

在生产中使用 Kubernetes 的这些年里,我们积累了许多有趣的故事,说明各种系统组件中的错误如何导致令人不快和/或难以理解的后果,从而影响容器和 Pod 的运行。 在本文中,我们选择了一些最常见或最有趣的内容。 即使你从来没有足够幸运遇到这样的情况,阅读这样的短篇侦探故事 - 尤其是“第一手” - 总是很有趣,不是吗?

故事 1. Supercronic 和 Docker 挂起

在其中一个集群上,我们定期收到冻结的 Docker,这干扰了集群的正常运行。 同时,在Docker日志中观察到以下内容:

level=error msg="containerd: start init process" error="exit status 2: "runtime/cgo: pthread_create failed: No space left on device
SIGABRT: abort
PC=0x7f31b811a428 m=0

goroutine 0 [idle]:

goroutine 1 [running]:
runtime.systemstack_switch() /usr/local/go/src/runtime/asm_amd64.s:252 fp=0xc420026768 sp=0xc420026760
runtime.main() /usr/local/go/src/runtime/proc.go:127 +0x6c fp=0xc4200267c0 sp=0xc420026768
runtime.goexit() /usr/local/go/src/runtime/asm_amd64.s:2086 +0x1 fp=0xc4200267c8 sp=0xc4200267c0

goroutine 17 [syscall, locked to thread]:
runtime.goexit() /usr/local/go/src/runtime/asm_amd64.s:2086 +0x1

…

我们对该错误最感兴趣的是以下消息: pthread_create failed: No space left on device。 快速学习 文件资料 解释说 Docker 无法分叉进程,这就是它定期冻结的原因。

在监控中,对应的情况如下图:

Kubernetes 运行中的 6 个有趣的系统错误 [及其解决方案]

在其他节点上也观察到类似的情况:

Kubernetes 运行中的 6 个有趣的系统错误 [及其解决方案]

Kubernetes 运行中的 6 个有趣的系统错误 [及其解决方案]

在相同的节点上我们看到:

root@kube-node-1 ~ # ps auxfww | grep curl -c
19782
root@kube-node-1 ~ # ps auxfww | grep curl | head
root     16688  0.0  0.0      0     0 ?        Z    Feb06   0:00      |       _ [curl] <defunct>
root     17398  0.0  0.0      0     0 ?        Z    Feb06   0:00      |       _ [curl] <defunct>
root     16852  0.0  0.0      0     0 ?        Z    Feb06   0:00      |       _ [curl] <defunct>
root      9473  0.0  0.0      0     0 ?        Z    Feb06   0:00      |       _ [curl] <defunct>
root      4664  0.0  0.0      0     0 ?        Z    Feb06   0:00      |       _ [curl] <defunct>
root     30571  0.0  0.0      0     0 ?        Z    Feb06   0:00      |       _ [curl] <defunct>
root     24113  0.0  0.0      0     0 ?        Z    Feb06   0:00      |       _ [curl] <defunct>
root     16475  0.0  0.0      0     0 ?        Z    Feb06   0:00      |       _ [curl] <defunct>
root      7176  0.0  0.0      0     0 ?        Z    Feb06   0:00      |       _ [curl] <defunct>
root      1090  0.0  0.0      0     0 ?        Z    Feb06   0:00      |       _ [curl] <defunct>

事实证明,这种行为是 pod 与 超长期的 (我们用来在 pod 中运行 cron 作业的 Go 实用程序):

 _ docker-containerd-shim 833b60bb9ff4c669bb413b898a5fd142a57a21695e5dc42684235df907825567 /var/run/docker/libcontainerd/833b60bb9ff4c669bb413b898a5fd142a57a21695e5dc42684235df907825567 docker-runc
|   _ /usr/local/bin/supercronic -json /crontabs/cron
|       _ /usr/bin/newrelic-daemon --agent --pidfile /var/run/newrelic-daemon.pid --logfile /dev/stderr --port /run/newrelic.sock --tls --define utilization.detect_aws=true --define utilization.detect_azure=true --define utilization.detect_gcp=true --define utilization.detect_pcf=true --define utilization.detect_docker=true
|       |   _ /usr/bin/newrelic-daemon --agent --pidfile /var/run/newrelic-daemon.pid --logfile /dev/stderr --port /run/newrelic.sock --tls --define utilization.detect_aws=true --define utilization.detect_azure=true --define utilization.detect_gcp=true --define utilization.detect_pcf=true --define utilization.detect_docker=true -no-pidfile
|       _ [newrelic-daemon] <defunct>
|       _ [curl] <defunct>
|       _ [curl] <defunct>
|       _ [curl] <defunct>
…

问题是这样的:当一个任务在 supercronic 中运行时,它产生的进程 无法正确终止, 转变为 僵尸.

注意:更准确地说,进程是由 cron 任务生成的,但 supercronic 不是 init 系统,不能“采用”其子进程生成的进程。 当发出 SIGHUP 或 SIGTERM 信号时,它们不会传递给子进程,导致子进程不会终止并保持僵尸状态。 您可以阅读有关这一切的更多信息,例如, 这样的文章.

有几种方法可以解决问题:

  1. 作为临时解决方法 - 在单个时间点增加系统中 PID 的数量:
           /proc/sys/kernel/pid_max (since Linux 2.5.34)
                  This file specifies the value at which PIDs wrap around (i.e., the value in this file is one greater than the maximum PID).  PIDs greater than this  value  are  not  allo‐
                  cated;  thus, the value in this file also acts as a system-wide limit on the total number of processes and threads.  The default value for this file, 32768, results in the
                  same range of PIDs as on earlier kernels
  2. 或者不直接在 supercronic 中启动任务,而是使用相同的 TINI,它能够正确终止进程并且不会产生僵尸。

故事 2. 删除 cgroup 时出现“僵尸”

Kubelet 开始消耗大量 CPU:

Kubernetes 运行中的 6 个有趣的系统错误 [及其解决方案]

没有人会喜欢这样,所以我们武装自己 PERF 并开始处理这个问题。 调查结果如下:

  • Kubelet 花费了超过三分之一的 CPU 时间从所有 cgroup 中提取内存数据:

    Kubernetes 运行中的 6 个有趣的系统错误 [及其解决方案]

  • 在内核开发者的邮件列表中你可以找到 问题的讨论。 简而言之,要点可以归结为: 各种 tmpfs 文件和其他类似的东西没有从系统中完全删除 当删除一个cgroup时,所谓的 记忆库 僵尸。 它们迟早会从页面缓存中删除,但服务器上有大量内存,内核认为没有必要浪费时间删除它们。 这就是为什么它们不断堆积。 为什么会发生这种情况? 这是一个带有 cron 作业的服务器,它不断创建新作业以及新的 Pod。 因此,会为其中的容器创建新的 cgroup,但很快就会被删除。
  • 为什么 kubelet 中的 cAdvisor 浪费这么多时间? 通过最简单的执行就很容易看出这一点 time cat /sys/fs/cgroup/memory/memory.stat。 如果在健康的机器上该操作需要 0,01 秒,那么在有问题的 cron02 上则需要 1,2 秒。 问题是 cAdvisor 从 sysfs 读取数据的速度非常慢,它试图考虑僵尸 cgroup 中使用的内存。
  • 为了强制删除僵尸,我们尝试按照 LKML 中的建议清除缓存: sync; echo 3 > /proc/sys/vm/drop_caches, - 但结果发现内核更复杂,导致车子崩溃。

该怎么办? 问题正在修复(犯罪,有关说明,请参阅 发布消息)将 Linux 内核更新到版本 4.16。

历史 3. Systemd 及其挂载

同样,kubelet 在某些节点上消耗了太多资源,但这一次它消耗了太多内存:

Kubernetes 运行中的 6 个有趣的系统错误 [及其解决方案]

事实证明,Ubuntu 16.04 中使用的 systemd 存在问题,并且在管理为连接创建的挂载时出现问题 subPath 来自 ConfigMap 或机密。 Pod 完成其工作后 systemd 服务及其服务挂载仍然存在 在系统中。 随着时间的推移,它们会积累大量。 甚至还有关于这个主题的问题:

  1. 第5916章;
  2. Kubernetes #57345.

...最后一个指的是 systemd 中的 PR: #7811 (systemd 中的问题 - #7798).

该问题在 Ubuntu 18.04 中不再存在,但如果您想继续使用 Ubuntu 16.04,您可能会发现我们关于此主题的解决方法很有用。

所以我们做了如下的DaemonSet:

---
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  labels:
    app: systemd-slices-cleaner
  name: systemd-slices-cleaner
  namespace: kube-system
spec:
  updateStrategy:
    type: RollingUpdate
  selector:
    matchLabels:
      app: systemd-slices-cleaner
  template:
    metadata:
      labels:
        app: systemd-slices-cleaner
    spec:
      containers:
      - command:
        - /usr/local/bin/supercronic
        - -json
        - /app/crontab
        Image: private-registry.org/systemd-slices-cleaner/systemd-slices-cleaner:v0.1.0
        imagePullPolicy: Always
        name: systemd-slices-cleaner
        resources: {}
        securityContext:
          privileged: true
        volumeMounts:
        - name: systemd
          mountPath: /run/systemd/private
        - name: docker
          mountPath: /run/docker.sock
        - name: systemd-etc
          mountPath: /etc/systemd
        - name: systemd-run
          mountPath: /run/systemd/system/
        - name: lsb-release
          mountPath: /etc/lsb-release-host
      imagePullSecrets:
      - name: antiopa-registry
      priorityClassName: cluster-low
      tolerations:
      - operator: Exists
      volumes:
      - name: systemd
        hostPath:
          path: /run/systemd/private
      - name: docker
        hostPath:
          path: /run/docker.sock
      - name: systemd-etc
        hostPath:
          path: /etc/systemd
      - name: systemd-run
        hostPath:
          path: /run/systemd/system/
      - name: lsb-release
        hostPath:
          path: /etc/lsb-release

...它使用以下脚本:

#!/bin/bash

# we will work only on xenial
hostrelease="/etc/lsb-release-host"
test -f ${hostrelease} && grep xenial ${hostrelease} > /dev/null || exit 0

# sleeping max 30 minutes to dispense load on kube-nodes
sleep $((RANDOM % 1800))

stoppedCount=0
# counting actual subpath units in systemd
countBefore=$(systemctl list-units | grep subpath | grep "run-" | wc -l)
# let's go check each unit
for unit in $(systemctl list-units | grep subpath | grep "run-" | awk '{print $1}'); do
  # finding description file for unit (to find out docker container, who born this unit)
  DropFile=$(systemctl status ${unit} | grep Drop | awk -F': ' '{print $2}')
  # reading uuid for docker container from description file
  DockerContainerId=$(cat ${DropFile}/50-Description.conf | awk '{print $5}' | cut -d/ -f6)
  # checking container status (running or not)
  checkFlag=$(docker ps | grep -c ${DockerContainerId})
  # if container not running, we will stop unit
  if [[ ${checkFlag} -eq 0 ]]; then
    echo "Stopping unit ${unit}"
    # stoping unit in action
    systemctl stop $unit
    # just counter for logs
    ((stoppedCount++))
    # logging current progress
    echo "Stopped ${stoppedCount} systemd units out of ${countBefore}"
  fi
done

...并且它使用前面提到的 supercronic 每 5 分钟运行一次。 它的 Dockerfile 如下所示:

FROM ubuntu:16.04
COPY rootfs /
WORKDIR /app
RUN apt-get update && 
    apt-get upgrade -y && 
    apt-get install -y gnupg curl apt-transport-https software-properties-common wget
RUN add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu xenial stable" && 
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - && 
    apt-get update && 
    apt-get install -y docker-ce=17.03.0*
RUN wget https://github.com/aptible/supercronic/releases/download/v0.1.6/supercronic-linux-amd64 -O 
    /usr/local/bin/supercronic && chmod +x /usr/local/bin/supercronic
ENTRYPOINT ["/bin/bash", "-c", "/usr/local/bin/supercronic -json /app/crontab"]

故事 4. 调度 Pod 时的竞争力

人们注意到:如果我们将一个 pod 放置在一个节点上,并且它的镜像被抽出很长时间,那么“击中”同一节点的另一个 pod 就会简单地 不开始拉取新 Pod 的镜像。 相反,它会等待直到拉取前一个 pod 的映像。 结果,一个已经被调度的 pod,其镜像在一分钟内就可以下载完毕,最终会处于以下状态: containerCreating.

事件看起来像这样:

Normal  Pulling    8m    kubelet, ip-10-241-44-128.ap-northeast-1.compute.internal  pulling image "registry.example.com/infra/openvpn/openvpn:master"

事实证明, 来自慢速注册表的单个映像可能会阻止部署 每个节点。

不幸的是,摆脱这种情况的方法并不多:

  1. 尝试直接在集群中或直接与集群一起使用您的 DockerRegistry(例如,GitLabRegistry、Nexus 等);
  2. 使用诸如 .

故事 5. 节点因内存不足而挂起

在各种应用程序的运行过程中,我们也遇到过节点完全无法访问的情况:SSH没有响应,所有监控守护进程都掉了,然后日志中没有任何(或几乎没有)异常。

我将以 MongoDB 运行的一个节点为例,通过图片告诉您。

这就是上面的样子 事故:

Kubernetes 运行中的 6 个有趣的系统错误 [及其解决方案]

就像这样—— 事故:

Kubernetes 运行中的 6 个有趣的系统错误 [及其解决方案]

在监控中,也有一个急剧的跳跃,此时节点不再可用:

Kubernetes 运行中的 6 个有趣的系统错误 [及其解决方案]

因此,从截图中可以清楚地看出:

  1. 机器上的RAM已接近尾声;
  2. RAM 消耗急剧增加,然后突然禁用对整个计算机的访问;
  3. 一个大任务到达 Mongo,这迫使 DBMS 进程使用更多内存并主动从磁盘读取。

事实证明,如果 Linux 耗尽了可用内存(内存压力开始出现)并且没有交换区,那么 当 OOM 杀手到来时,将页面放入页面缓存和将其写回磁盘之间可能会出现平衡行为。 这是由 kswapd 完成的,它勇敢地释放尽可能多的内存页以供后续分配。

不幸的是,由于 I/O 负载较大且可用内存较少, kswapd成为整个系统的瓶颈,因为他们与它联系在一起 所有 系统中内存页面的分配(页面错误)。 如果进程不想再使用内存,而是固定在 OOM 杀手深渊的边缘,这种情况可能会持续很长时间。

自然的问题是:为什么 OOM 杀手来得这么晚? 在当前的迭代中,OOM 杀手非常愚蠢:只有当尝试分配内存页面失败时(即,它才会终止进程)。 如果页面错误失败。 这种情况在很长一段时间内都不会发生,因为 kswapd 勇敢地释放内存页面,将页面缓存(实际上是系统中的整个磁盘 I/O)转储回磁盘。 更详细地,您可以阅读消除内核中此类问题所需步骤的描述 这里.

这种行为 应该改进 Linux 内核 4.6+。

故事 6.Pod 陷入 Pending 状态

在某些集群中,确实有很多 pod 正在运行,我们开始注意到大多数 pod 都处于“挂起”状态很长时间 Pending,尽管 Docker 容器本身已经在节点上运行并且可以手动使用。

有了这个 describe 没有任何错误:

  Type    Reason                  Age                From                     Message
  ----    ------                  ----               ----                     -------
  Normal  Scheduled               1m                 default-scheduler        Successfully assigned sphinx-0 to ss-dev-kub07
  Normal  SuccessfulAttachVolume  1m                 attachdetach-controller  AttachVolume.Attach succeeded for volume "pvc-6aaad34f-ad10-11e8-a44c-52540035a73b"
  Normal  SuccessfulMountVolume   1m                 kubelet, ss-dev-kub07    MountVolume.SetUp succeeded for volume "sphinx-config"
  Normal  SuccessfulMountVolume   1m                 kubelet, ss-dev-kub07    MountVolume.SetUp succeeded for volume "default-token-fzcsf"
  Normal  SuccessfulMountVolume   49s (x2 over 51s)  kubelet, ss-dev-kub07    MountVolume.SetUp succeeded for volume "pvc-6aaad34f-ad10-11e8-a44c-52540035a73b"
  Normal  Pulled                  43s                kubelet, ss-dev-kub07    Container image "registry.example.com/infra/sphinx-exporter/sphinx-indexer:v1" already present on machine
  Normal  Created                 43s                kubelet, ss-dev-kub07    Created container
  Normal  Started                 43s                kubelet, ss-dev-kub07    Started container
  Normal  Pulled                  43s                kubelet, ss-dev-kub07    Container image "registry.example.com/infra/sphinx/sphinx:v1" already present on machine
  Normal  Created                 42s                kubelet, ss-dev-kub07    Created container
  Normal  Started                 42s                kubelet, ss-dev-kub07    Started container

经过一番挖掘,我们假设 kubelet 根本没有时间将有关 pod 状态和活跃/就绪测试的所有信息发送到 API 服务器。

并且研究了help后,我们发现了以下参数:

--kube-api-qps - QPS to use while talking with kubernetes apiserver (default 5)
--kube-api-burst  - Burst to use while talking with kubernetes apiserver (default 10) 
--event-qps - If > 0, limit event creations per second to this value. If 0, unlimited. (default 5)
--event-burst - Maximum size of a bursty event records, temporarily allows event records to burst to this number, while still not exceeding event-qps. Only used if --event-qps > 0 (default 10) 
--registry-qps - If > 0, limit registry pull QPS to this value.
--registry-burst - Maximum size of bursty pulls, temporarily allows pulls to burst to this number, while still not exceeding registry-qps. Only used if --registry-qps > 0 (default 10)

如你所见 默认值相当小,并且 90% 的情况满足了所有需求...但是,在我们的案例中这还不够。 因此,我们设置以下值:

--event-qps=30 --event-burst=40 --kube-api-burst=40 --kube-api-qps=30 --registry-qps=30 --registry-burst=40

...并重新启动 kubelet,之后我们在 API 服务器的调用图中看到了下图:

Kubernetes 运行中的 6 个有趣的系统错误 [及其解决方案]

...是的,一切都开始飞翔!

PS

感谢我们公司的众多工程师,特别是我们研发团队的同事 Andrey Klimentyev(Andrey Klimentyev)在收集 bug 和准备本文方面提供的帮助。祖扎斯).

聚苯硫醚

另请阅读我们的博客:

来源: habr.com

添加评论