![6 bogues système intéressants lors de l'exploitation de Kubernetes [et leurs solutions]](/wp-content/uploads/2019/03/bed059552ed86580939aa18fbdf1553e.jpg)
Au fil des années d'exploitation de Kubernetes en production, nous avons accumulé de nombreuses histoires fascinantes sur la manière dont des bogues dans différents composants système ont entraîné des conséquences désagréables et/ou incompréhensibles, affectant le fonctionnement des conteneurs et des pods. Dans cet article, nous avons rassemblé certains des exemples les plus fréquents ou intéressants. Même si vous n'avez jamais eu la chance de faire face à de telles situations, lire sur de tels petits mystères — surtout « de première main » — est toujours captivant, n'est-ce pas ?...
Histoire 1. Supercronic et Docker bloqué
Sur l'un des clusters, nous recevions périodiquement un Docker « bloqué », ce qui compliquait le bon fonctionnement du cluster. Pendant ce temps, dans les journaux de Docker, nous avons observé ce qui suit
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
… Dans cette erreur, ce qui nous intéresse le plus est le message : pthread_create failed: No space left on device. Une brève étude a expliqué que Docker ne pouvait pas forker un processus, ce qui faisait qu'il se « bloquait » périodiquement.
Dans le monitoring, l'image qui en résulte est la suivante :
![6 bogues système intéressants lors de l'exploitation de Kubernetes [et leurs solutions]](/wp-content/uploads/2019/03/bd778052c87b338493bae54b26830ef3.jpg)
Une situation similaire est observée sur d'autres nœuds :
![6 bogues système intéressants lors de l'exploitation de Kubernetes [et leurs solutions]](/wp-content/uploads/2019/03/ef512532a95ca982e4342071115dbe9f.jpg)
![6 bogues système intéressants lors de l'exploitation de Kubernetes [et leurs solutions]](/wp-content/uploads/2019/03/43c32ebca78755dde348ed5e7ac75c79.jpg)
Sur ces mêmes nœuds, nous voyons :
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]Il s'est avéré que ce comportement résultait du fonctionnement du pod avec (un utilitaire en Go que nous utilisons pour exécuter des tâches cron dans les 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]
…Le problème est le suivant : lorsque la tâche est lancée dans supercronic, le processus qu'elle engendre ne peut pas se terminer correctement, se transformant en .
Remarque: Pour être plus précis, les processus sont engendrés par des tâches cron, mais supercronic n'est pas un système init et ne peut pas « adopter » les processus engendrés par ses enfants. En cas de signaux SIGHUP ou SIGTERM, ceux-ci ne sont pas transmis aux processus engendrés, ce qui fait que les processus fils ne se terminent pas, restant en état de zombie. Vous pouvez en apprendre davantage à ce sujet dans .
Il existe plusieurs façons de résoudre les problèmes :
- Comme solution temporaire, augmentez le nombre de PID dans le système à un moment donné :
/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 - Ou alors, faites exécuter les tâches dans supercronic non directement, mais à l'aide de , qui peut terminer correctement les processus et ne pas engendrer de zombies.
Histoire 2. « Zombies » lors de la suppression de cgroup
Kubelet a commencé à consommer une grande quantité de CPU :
![6 bogues système intéressants lors de l'exploitation de Kubernetes [et leurs solutions]](/wp-content/uploads/2019/03/6140058330faaa3785b089dcba857056.jpg)
Cela ne plaira à personne, donc nous nous sommes équipés de et avons commencé à enquêter sur le problème. Les résultats de l'enquête sont les suivants :
- Kubelet dépense plus d'un tiers du temps processeur à extraire des données de mémoire de tous les cgroup :
![6 bogues système intéressants lors de l'exploitation de Kubernetes [et leurs 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)
- Dans la mailing list des développeurs du noyau, vous pouvez trouver . En résumé, la question est que différents fichiers tmpfs et autres éléments similaires ne sont pas complètement supprimés du système lors de la suppression de cgroup — les soi-disant zombis. Tôt ou tard, ils seront supprimés du cache de la page, mais il y a beaucoup de mémoire sur le serveur, et le noyau ne voit pas l'intérêt de passer du temps à les supprimer. Par conséquent, ils continuent à s'accumuler. Pourquoi cela se produit-il ? C'est un serveur avec des tâches cron, qui crée en permanence de nouveaux jobs, et avec eux — de nouveaux pods. Ainsi, de nouveaux cgroups sont créés pour les conteneurs, qui sont vite supprimés.
- Pourquoi cAdvisor dans kubelet prend-il autant de temps ? C'est facile à voir en exécutant simplement
time cat /sys/fs/cgroup/memory/memory.stat. Si une opération sur une machine saine prend 0,01 seconde, sur la machine problématique cron02, cela prend 1,2 seconde. Tout vient du fait que cAdvisor lit très lentement les données de sysfs, essayant de prendre en compte la mémoire utilisée et les cgroups zombies. - Pour forcer la suppression des zombies, nous avons essayé de nettoyer les caches, comme recommandé dans LKML :
sync; echo 3 > /proc/sys/vm/drop_caches, — mais le noyau s'est révélé plus complexe et a bloqué la machine.
Que faire ? Le problème est résolu (, et la description se trouve dans ) en mettant à jour le noyau Linux vers la version 4.16.
Histoire 3. Systemd et son mount
Encore une fois, kubelet consomme trop de ressources sur certains nœuds, mais cette fois — déjà de la mémoire :
![6 bogues système intéressants lors de l'exploitation de Kubernetes [et leurs solutions]](/wp-content/uploads/2019/03/044c4e23a772c61a6206b9b20aa67c1d.jpg)
Il s'est avéré qu'il y a un problème avec systemd, utilisé dans Ubuntu 16.04, et cela se produit lors de la gestion des mounts, qui sont créés pour le montage subPath à partir de ConfigMaps ou de secrets. Après l'exécution du pod, le service systemd et son mount auxiliaire restent dans le système. Avec le temps, ils s'accumulent en grand nombre. À ce sujet, il y a même des problèmes :
- ;
- .
… dans laquelle on fait référence à un PR dans systemd : (un problème dans systemd — ).
Le problème n'existe plus dans Ubuntu 18.04, mais si vous souhaitez continuer à utiliser Ubuntu 16.04, notre solution de contournement à ce sujet pourrait vous être utile.
Ainsi, nous avons créé le DaemonSet suivant :
---
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… et il utilise le script suivant :
#!/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… et il s'exécute toutes les 5 minutes grâce à l'outil déjà mentionné supercronic. Son Dockerfile est le suivant :
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"]Histoire 4. Concurrence lors de la planification des pods
Il a été remarqué que : si un pod est déployé sur un nœud et que son image est téléchargée très lentement, un autre pod qui "atterrit" sur ce même nœud ne commence pas à tirer l'image du nouveau pod.Au lieu de cela, il attend que l'image de l'ancien pod soit téléchargée. En conséquence, le pod qui avait déjà été planifié et dont l'image aurait pu être téléchargée en une minute, restera longtemps en état de containerCreating.
Les événements seront à peu près les suivants :
Normal Pulling 8m kubelet, ip-10-241-44-128.ap-northeast-1.compute.internal pulling image "registry.example.com/infra/openvpn/openvpn:master"Il en résulte qu'un seul image provenant d'un registre lent peut bloquer le déploiement sur le nœud. Malheureusement, il n'y a pas beaucoup de solutions à cette situation :
Malheureusement, il n'y a pas beaucoup de solutions :
- Essayez d'utiliser votre Docker Registry directement dans le cluster ou directement avec le cluster (par exemple, GitLab Registry, Nexus, etc.);
- Profitez d'outils tels que .
Histoire 5. Blocage des nœuds en raison d'un manque de mémoire
Au cours de l'exploitation de diverses applications, nous avons également rencontré des situations où un nœud devenait complètement inaccessible : il ne répond pas au SSH, tous les démons de surveillance échouent, et dans les logs, il n'y a rien (ou presque rien) d'anormal.
Je vais illustrer cela avec des images en prenant l'exemple d'un nœud où MongoDB fonctionnait.
Voici à quoi ressemble atop à accidents :
![6 bogues système intéressants lors de l'exploitation de Kubernetes [et leurs solutions]](/wp-content/uploads/2019/03/5de916d270a862cbcbb5ed23c31f698e.jpg)
Et voici comment cela se présente — après accidents :
![6 bogues système intéressants lors de l'exploitation de Kubernetes [et leurs solutions]](/wp-content/uploads/2019/03/0f32bf1113204cf19f4639a297e40348.jpg)
La surveillance montre également une forte hausse, à laquelle le nœud devient inaccessible :
![6 bogues système intéressants lors de l'exploitation de Kubernetes [et leurs solutions]](/wp-content/uploads/2019/03/31e770cac5be32bb7f95cfbbc6b9f1ae.jpg)
Ainsi, d'après les captures d'écran, il est évident que :
- La mémoire vive de la machine est presque épuisée ;
- Une forte hausse de l'utilisation de la mémoire vive est observée, après quoi l'accès à toute la machine est brusquement coupé ;
- Une tâche importante arrive sur Mongo, ce qui oblige le processus de SGBD à utiliser plus de mémoire et à lire activement sur le disque.
Il s'avère que si la mémoire libre se termine sous Linux (pression mémoire) et qu'il n'y a pas de swap, à l'arrivée du OOM killer peut créer un équilibre entre l'envoi de pages dans le cache des pages et leur écriture à nouveau sur le disque. Cela est géré par kswapd, qui libère vaillamment autant de pages mémoire que possible pour une distribution ultérieure.
Malheureusement, avec une forte charge d'E/S combinée à un faible nombre de pages mémoire disponibles, kswapd devient le goulot d'étranglement de tout le système, car il est lié tout à l'allocation (page faults) des pages mémoire dans le système. Cela peut durer très longtemps si les processus ne veulent pas utiliser plus de mémoire et restent au bord du gouffre OOM-killer.
La question se pose : pourquoi le OOM killer intervient-il si tard ? Dans sa version actuelle, le OOM killer est extrêmement stupide : il ne terminera un processus que lorsque la tentative d'allocation d'une page mémoire échoue, c'est-à-dire si le page fault échoue. Cela ne se produit pas pendant longtemps, car kswapd libère vaillamment des pages mémoire en purgant le cache des pages (toute l'E/S disque dans le système, en fait) vers le disque. Pour des détails plus approfondis, avec une description des étapes nécessaires pour résoudre de tels problèmes dans le noyau, vous pouvez lire .
Ce comportement avec le noyau Linux 4.6+.
Histoire 6. Les pods restent bloqués dans l'état Pending
Dans certains clusters où de nombreux pods fonctionnent réellement, nous avons commencé à remarquer qu'une grande partie d'entre eux reste très longtemps « bloquée » dans l'état Pending, bien que les conteneurs Docker soient déjà démarrés sur les nœuds et qu'ils puissent être manipulés manuellement.
Cela dit, dans describe il n'y a rien de mauvais :
Type Raison Âge De Message
---- ------ ---- ---- -------
Normal Programmé 1m planificateur-par-défaut Assignation réussie de sphinx-0 à ss-dev-kub07
Normal AttachVolumeRéussie 1m contrôleur_attachdetach AttachVolume.Attach réussi pour le volume "pvc-6aaad34f-ad10-11e8-a44c-52540035a73b"
Normal MontéeVolumeRéussie 1m kubelet, ss-dev-kub07 MontéeVolume.SetUp réussie pour le volume "sphinx-config"
Normal MontéeVolumeRéussie 1m kubelet, ss-dev-kub07 MontéeVolume.SetUp réussie pour le volume "default-token-fzcsf"
Normal MontéeVolumeRéussie 49s (x2 sur 51s) kubelet, ss-dev-kub07 MontéeVolume.SetUp réussie pour le volume "pvc-6aaad34f-ad10-11e8-a44c-52540035a73b"
Normal Tiré 43s kubelet, ss-dev-kub07 L'image du conteneur "registry.example.com\/infra\/sphinx-exporter\/sphinx-indexer:v1" est déjà présente sur la machine
Normal Créé 43s kubelet, ss-dev-kub07 Conteneur créé
Normal Démarré 43s kubelet, ss-dev-kub07 Conteneur démarré
Normal Tiré 43s kubelet, ss-dev-kub07 L'image du conteneur "registry.example.com\/infra\/sphinx\/sphinx:v1" est déjà présente sur la machine
Normal Créé 42s kubelet, ss-dev-kub07 Conteneur créé
Normal Démarré 42s kubelet, ss-dev-kub07 Conteneur démarréEn fouillant, nous avons formulé l'hypothèse que le kubelet ne parvenait tout simplement pas à envoyer toutes les informations sur l'état des pods à l'API serveur, les probes de liveness/readiness.
Et après avoir étudié l'aide, nous avons trouvé les paramètres suivants :
--kube-api-qps - QPS à utiliser lors de la communication avec le serveur API Kubernetes (par défaut 5)
--kube-api-burst - Burst à utiliser lors de la communication avec le serveur API Kubernetes (par défaut 10)
--event-qps - Si > 0, limite le nombre de créations d'événements par seconde à cette valeur. Si 0, illimité. (par défaut 5)
--event-burst - Taille maximale des enregistrements d'événements ponctuels, permet temporairement aux enregistrements d'événements d'atteindre ce nombre, tout en ne dépassant pas event-qps. Utilisé uniquement si --event-qps > 0 (par défaut 10)
--registry-qps - Si > 0, limite le pull du registre QPS à cette valeur.
--registry-burst - Taille maximale des pulls ponctuels, permet temporairement aux pulls d'atteindre ce nombre, tout en ne dépassant pas registry-qps. Utilisé uniquement si --registry-qps > 0 (par défaut 10)Comme on peut le voir, les valeurs par défaut sont assez petites, et dans 90 % des cas, elles couvrent tous les besoins… Cependant, dans notre cas, cela n'était pas suffisant. Nous avons donc défini les valeurs suivantes :
--event-qps=30 --event-burst=40 --kube-api-burst=40 --kube-api-qps=30 --registry-qps=30 --registry-burst=40… et nous avons redémarré les kubelets, après quoi sur les graphiques d'accès à l'API serveur, nous avons vu la scène suivante :
![6 bogues système intéressants lors de l'exploitation de Kubernetes [et leurs solutions]](/wp-content/uploads/2019/03/b2ae099729e55a686f6bec3012b96195.jpg)
… et oui, tout a commencé à fonctionner correctement !
P.S.
Je tiens à exprimer ma grande gratitude aux nombreux ingénieurs de notre entreprise pour leur aide dans la collecte de bugs et la préparation de l'article, en particulier à mon collègue de notre équipe R&D, Andrei Klimentiev ().
P.P.S.
Lisez aussi dans notre blog :
- «».
- Cycle des astuces et conseils Kubernetes :
- «»;
- «»;
- «»;
- «».
Source : habr.com

![6 bogues système intéressants lors de l'exploitation de Kubernetes [et leurs solutions]](/wp-content/uploads/2019/03/0d15d1de17cd6838fc1cad19615af218.jpg)