![6 intriguing system bugs in operating Kubernetes [and their solutions]](/wp-content/uploads/2019/03/bed059552ed86580939aa18fbdf1553e.jpg)
Over the years of operating Kubernetes in production, we have accumulated quite a few interesting stories about how bugs in various system components led to unpleasant or perplexing consequences that affected the operation of containers and pods. In this article, we have compiled some of the most frequent or intriguing ones. Even if you never encounter such situations, reading about these brief detective stories—especially from 'first-hand' sources—is always intriguing, isn't it?
Story 1. Supercronic and Docker Hanging
On one of the clusters, we periodically experienced a 'hanging' Docker, which hindered the normal functioning of the cluster. At the same time, the Docker logs revealed the following
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
… In this error, we are most interested in the message: pthread_create failed: No space left on device. A quick examination revealed that Docker cannot fork the process, which is why it periodically 'hung'.
The monitoring reflects the following picture:
![6 intriguing system bugs in operating Kubernetes [and their solutions]](/wp-content/uploads/2019/03/bd778052c87b338493bae54b26830ef3.jpg)
A similar situation is observed on other nodes:
![6 intriguing system bugs in operating Kubernetes [and their solutions]](/wp-content/uploads/2019/03/ef512532a95ca982e4342071115dbe9f.jpg)
![6 intriguing system bugs in operating Kubernetes [and their solutions]](/wp-content/uploads/2019/03/43c32ebca78755dde348ed5e7ac75c79.jpg)
On these same nodes, we see:
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]
root 17398 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl]
root 16852 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl]
root 9473 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl]
root 4664 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl]
root 30571 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl]
root 24113 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl]
root 16475 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl]
root 7176 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl]
root 1090 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl]It turned out that this behavior was a consequence of the pod working with (a Go utility that we use to run cron jobs in pods):
_ 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]
| _ [curl]
| _ [curl]
| _ [curl]
…The problem is as follows: when a task is started in supercronic, the process spawned by it cannot terminate correctly, turning into .
Note: To be precise, processes are spawned by cron tasks, but supercronic is not an init system and cannot 'adopt' the processes spawned by its children. When SIGHUP or SIGTERM signals are generated, they are not passed to the spawned processes, which means that the child processes do not terminate, remaining in a zombie state. More details on this can be found, for instance, in .
There are a couple of ways to solve the issues:
- As a temporary workaround — increase the number of PIDs in the system at a given time:
/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 - Or start tasks in supercronic not directly, but using the same , which can correctly terminate processes and not spawn zombies.
History 2. Zombies when removing cgroup
Kubelet started consuming a lot of CPU:
![6 intriguing system bugs in operating Kubernetes [and their solutions]](/wp-content/uploads/2019/03/6140058330faaa3785b089dcba857056.jpg)
Nobody likes this, so we armed ourselves and began to investigate the problem. The investigation results were as follows:
- Kubelet spends more than a third of its CPU time pulling memory data from all cgroups:
![6 intriguing system bugs in operating Kubernetes [and their solutions]](data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20600%20241%22%3E%3C%2Fsvg%3E)
- In the kernel developers mailing list, you can find . In short, it boils down to the fact that various tmpfs files and other similar things are not completely removed from the system when removing cgroup — so-called zombies. Sooner or later, they will be removed from the page cache; however, there is plenty of memory on the server, and the kernel doesn't see any point in spending time removing them. Therefore, they continue to accumulate. Why does this even happen? This is a server with cron jobs that constantly creates new jobs, bringing new pods with them. Thus, new cgroups are created for the containers within them, which are soon deleted.
- Why does cAdvisor in kubelet take so much time? This can easily be seen by simply executing
time cat /sys/fs/cgroup/memory/memory.stat. If the operation takes 0.01 seconds on a healthy machine, then on the problematic cron02 it takes 1.2 seconds. The reason is that cAdvisor, which reads data from sysfs very slowly, tries to account for the memory used in the zombie cgroups. - To forcefully remove zombies, we tried cleaning the caches as recommended in LKML:
sync; echo 3 > /proc/sys/vm/drop_caches, — but the kernel turned out to be more complicated and hung the machine.
What to do? The problem can be fixed (, for a description see ) by updating the Linux kernel to version 4.16.
Story 3. Systemd and its mount
Once again, kubelet consumes too many resources on some nodes, but this time — memory:
![6 intriguing system bugs in operating Kubernetes [and their solutions]](/wp-content/uploads/2019/03/044c4e23a772c61a6206b9b20aa67c1d.jpg)
It turned out that there is a problem in systemd used in Ubuntu 16.04, which arises when managing mounts created for connecting subPath from ConfigMaps or secrets. After the pod has finished its work, the systemd service and its auxiliary mount remain in the system. Over time, they accumulate in large numbers. There are even issues on this topic:
- ;
- .
… in the latest of which references a PR in systemd: (issue in systemd — ).
The problem is no longer present in Ubuntu 18.04, but if you want to continue using Ubuntu 16.04, our workaround on this topic may be useful to you.
So, we created the following 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… and it uses the following script:
#!/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… and it runs every 5 minutes using the previously mentioned supercronic. Its Dockerfile looks like this:
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"]Story 4. Concurrency in pod scheduling
It was noted that: if a pod is deployed on a node and its image takes a long time to download, another pod that "lands" on the same node simply does not start pulling the image of the new pod.Instead, it waits for the previous pod's image to finish pulling. As a result, a pod that has already been scheduled and whose image could be downloaded in just a minute gets stuck in the containerCreating.
In the events, there will be something like the following:
Normal Pulling 8m kubelet, ip-10-241-44-128.ap-northeast-1.compute.internal pulling image "registry.example.com/infra/openvpn/openvpn:master"This means that a single image from a slow registry can block the deployment on the node.
Unfortunately, there are not many solutions to this issue:
- Try to use your Docker Registry directly in the cluster or with the cluster (for example, GitLab Registry, Nexus, etc.);
- Utilize tools such as .
History 5. Node hanging due to lack of memory
During the operation of various applications, we also encountered a situation where a node became completely unavailable: it does not respond to SSH, all monitoring daemons drop off, and the logs show nothing (or almost nothing) abnormal.
I'll illustrate with images using an example of one node where MongoDB was functioning.
This is what atop looks like up to crashes:
![6 intriguing system bugs in operating Kubernetes [and their solutions]](/wp-content/uploads/2019/03/5de916d270a862cbcbb5ed23c31f698e.jpg)
And this is how it looks — after crashes:
![6 intriguing system bugs in operating Kubernetes [and their solutions]](/wp-content/uploads/2019/03/0f32bf1113204cf19f4639a297e40348.jpg)
Monitoring also shows a sharp spike when the node becomes unavailable:
![6 intriguing system bugs in operating Kubernetes [and their solutions]](/wp-content/uploads/2019/03/31e770cac5be32bb7f95cfbbc6b9f1ae.jpg)
Thus, the screenshots indicate that:
- The machine's RAM is nearly exhausted;
- There is a sudden spike in RAM usage, after which access to the entire machine is abruptly cut off;
- A large task arrives for Mongo that causes the database process to use more memory and actively read from the disk.
It turns out that when free memory runs out in Linux (when memory pressure occurs) and there is no swap, up to the arrival of the OOM killer may lead to an equilibrium between pushing pages into page cache and writing them back to disk. This is handled by kswapd, which bravely frees as many memory pages as possible for subsequent allocation.
Unfortunately, under high I/O load combined with a low amount of free memory, kswapd becomes the bottleneck of the entire system, because it is tied to all memory page allocations (page faults) in the system. This can last a very long time if processes do not want to use more memory and get stuck on the edge of the OOM-killer abyss.
The question arises: why does the OOM killer come so late? In its current iteration, the OOM killer is extremely foolish: it will only kill a process when an attempt to allocate a memory page fails, i.e., if the page fault fails. This does not happen for a long time because kswapd is bravely freeing memory pages by flushing the page cache (essentially all disk I/O in the system) back to disk. For more details, including the steps necessary to resolve such issues in the kernel, you can read .
This behavior with Linux kernel 4.6+.
History 6. Pods are stuck in Pending state
In some clusters where a substantial number of pods are operational, we began to notice that many of them remain 'stuck' in state Here are possible reasons (assuming that the scheduler is functioning normally):, even though the Docker containers are already running on nodes and can be manually accessed.
However, there's nothing wrong with of the PVC object: :
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 containerUpon investigation, we hypothesized that kubelet simply can't send all the information about the status of the pods to the API server in time, regarding liveness/readiness probes.
After examining the help, we found the following parameters:
--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)As you can see, the default values are quite small, and in 90% of cases they meet all needs... However, in our case, this was insufficient. Therefore, we set the following values:
--event-qps=30 --event-burst=40 --kube-api-burst=40 --kube-api-qps=30 --registry-qps=30 --registry-burst=40... and restarted the kubelets, after which we saw the following pattern on the API server access graphs:
![6 intriguing system bugs in operating Kubernetes [and their solutions]](/wp-content/uploads/2019/03/b2ae099729e55a686f6bec3012b96195.jpg)
... and yes, everything started to fly!
P.S.
I would like to express my deep gratitude to the many engineers in our company for their help in bug collection and article preparation, especially to my colleague from our R&D team, Andrey Klimentyev ().
P.P.S.
Also read in our blog:
- «».
- Kubernetes Tips & Tricks Cycle:
- «»;
- «»;
- «»;
- «».
Source: habr.com

![6 intriguing system bugs in operating Kubernetes [and their solutions]](/wp-content/uploads/2019/03/0d15d1de17cd6838fc1cad19615af218.jpg)