6 bogues système intéressants lors de l'exploitation de Kubernetes [et leurs solutions]

6 bogues système intéressants lors de l'exploitation de Kubernetes [et leurs solutions]

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 documentation 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]

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]

6 bogues système intéressants lors de l'exploitation de Kubernetes [et leurs solutions]

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 supercronic (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 zombie.

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 un tel article.

Il existe plusieurs façons de résoudre les problèmes :

  1. 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
  2. Ou alors, faites exécuter les tâches dans supercronic non directement, mais à l'aide de tini, 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]

Cela ne plaira à personne, donc nous nous sommes équipés de perf 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]

  • Dans la mailing list des développeurs du noyau, vous pouvez trouver une discussion sur le problème. 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 memcg 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 (commit, et la description se trouve dans le message de release) 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]

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 :

  1. kops #5916;
  2. kubernetes #57345.

… dans laquelle on fait référence à un PR dans systemd : #7811 (un problème dans systemd — #7798).

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 :

  1. Essayez d'utiliser votre Docker Registry directement dans le cluster ou directement avec le cluster (par exemple, GitLab Registry, Nexus, etc.);
  2. Profitez d'outils tels que kraken.

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]

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]

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]

Ainsi, d'après les captures d'écran, il est évident que :

  1. La mémoire vive de la machine est presque épuisée ;
  2. 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é ;
  3. 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 ici.

Ce comportement devrait s'améliorer 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]

… 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 (zuzzas).

P.P.S.

Lisez aussi dans notre blog :

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster