6 bogues système amusants dans le fonctionnement de Kubernetes [et leur solution]

6 bogues système amusants dans le fonctionnement de Kubernetes [et leur solution]

Au fil des années d'utilisation de Kubernetes en production, nous avons accumulé de nombreuses histoires intéressantes sur la façon dont des bugs dans divers composants du 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 sélectionné quelques-uns des plus courants ou des plus intéressants. Même si l’on n’a jamais la chance de rencontrer de telles situations, lire des romans policiers aussi courts – surtout « de première main » – est toujours intéressant, n’est-ce pas ?

Histoire 1. Supercronic et Docker suspendus

Sur l'un des clusters, nous recevions périodiquement un Docker gelé, ce qui interférait avec le fonctionnement normal du cluster. Dans le même temps, les éléments suivants ont été observés dans les journaux 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

…

Ce qui nous intéresse le plus dans cette erreur est le message : pthread_create failed: No space left on device. Étude rapide documentation a expliqué que Docker ne pouvait pas lancer un processus, c'est pourquoi il se bloquait périodiquement.

En monitoring, l’image suivante correspond à ce qui se passe :

6 bogues système amusants dans le fonctionnement de Kubernetes [et leur solution]

Une situation similaire est observée sur d’autres nœuds :

6 bogues système amusants dans le fonctionnement de Kubernetes [et leur solution]

6 bogues système amusants dans le fonctionnement de Kubernetes [et leur solution]

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

Il s'est avéré que ce comportement est une conséquence du fait que le pod fonctionne avec supercronique (un utilitaire 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] <defunct>
|       _ [curl] <defunct>
|       _ [curl] <defunct>
|       _ [curl] <defunct>
…

Le problème est le suivant : lorsqu'une tâche est exécutée en supercronique, le processus engendré par celle-ci ne peut pas se terminer correctement, se transformer en zombi.

Noter: Pour être plus précis, les processus sont générés par les tâches cron, mais supercronic n'est pas un système d'initialisation et ne peut pas « adopter » les processus générés par ses enfants. Lorsque les signaux SIGHUP ou SIGTERM sont émis, ils ne sont pas transmis aux processus enfants, ce qui fait que les processus enfants ne se terminent pas et restent dans le statut zombie. Vous pouvez en savoir plus sur tout cela, par exemple, dans un tel article.

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

  1. Comme solution de contournement 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 lancez des tâches dans supercronic pas directement, mais en utilisant le même tini, qui est capable de terminer correctement les processus et de ne pas générer de zombies.

Histoire 2. « Zombies » lors de la suppression d'un groupe de contrôle

Kubelet a commencé à consommer beaucoup de CPU :

6 bogues système amusants dans le fonctionnement de Kubernetes [et leur solution]

Personne n'aimera ça, alors nous nous sommes armés Perf et j'ai commencé à régler le problème. Les résultats de l'enquête ont été les suivants :

  • Kubelet passe plus d'un tiers de son temps CPU à extraire les données mémoire de tous les groupes de contrôle :

    6 bogues système amusants dans le fonctionnement de Kubernetes [et leur solution]

  • Dans la liste de diffusion des développeurs du noyau, vous pouvez trouver discussion du problème. Bref, le problème se résume à ceci : divers fichiers tmpfs et autres éléments similaires ne sont pas complètement supprimés du système lors de la suppression d'un groupe de contrôle, ce qu'on appelle memcg zombie. Tôt ou tard, elles seront supprimées du cache des pages, mais il y a beaucoup de mémoire sur le serveur et le noyau ne voit pas l'intérêt de perdre du temps à les supprimer. C'est pourquoi ils continuent de s'accumuler. Pourquoi est-ce que cela se produit ? Il s'agit d'un serveur avec des tâches cron qui crée constamment de nouvelles tâches, et avec elles de nouveaux pods. Ainsi, de nouveaux groupes de contrôle sont créés pour les conteneurs qu'ils contiennent, qui sont rapidement supprimés.
  • Pourquoi cAdvisor dans Kubelet perd-il autant de temps ? C'est facile à voir avec l'exécution la plus simple time cat /sys/fs/cgroup/memory/memory.stat. Si sur une machine saine, l'opération prend 0,01 seconde, alors sur le cron02 problématique, cela prend 1,2 seconde. Le fait est que cAdvisor, qui lit très lentement les données de sysfs, essaie de prendre en compte la mémoire utilisée dans les groupes de contrôle zombies.
  • Pour supprimer de force les zombies, nous avons essayé de vider les caches comme recommandé dans LKML : sync; echo 3 > /proc/sys/vm/drop_caches, - mais le noyau s'est avéré plus compliqué et a fait planter la voiture.

Ce qu'il faut faire? Le problème est en cours de résolution (commettre, et pour une description, voir message de libération) mettant à jour le noyau Linux vers la version 4.16.

Historique 3. Systemd et son montage

Encore une fois, le kubelet consomme trop de ressources sur certains nœuds, mais cette fois il consomme trop de mémoire :

6 bogues système amusants dans le fonctionnement de Kubernetes [et leur solution]

Il s'est avéré qu'il y avait un problème dans systemd utilisé dans Ubuntu 16.04, et cela se produit lors de la gestion des montages créés pour la connexion. subPath à partir de ConfigMap ou de secrets. Une fois que le pod a terminé son travail le service systemd et son service mount restent dans le système. Au fil du temps, un grand nombre d'entre eux s'accumulent. Il y a même des problèmes sur ce sujet :

  1. #5916;
  2. Kubernetes #57345.

... dont le dernier fait référence au PR dans systemd : #7811 (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 sur ce sujet peut vous être utile.

Nous avons donc 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 en utilisant le supercronic mentionné précédemment. Son Dockerfile ressemble à ceci :

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. Compétitivité lors de la planification des pods

Il a été remarqué que : si nous avons un pod placé sur un nœud et que son image est pompée pendant très longtemps, alors un autre pod qui « frappe » le même nœud va simplement ne commence pas à tirer l'image du nouveau pod. Au lieu de cela, il attend que l'image du pod précédent soit extraite. De ce fait, un pod qui était déjà programmé et dont l'image aurait pu être téléchargée en une minute seulement se retrouvera au statut de containerCreating.

Les événements ressembleront à ceci :

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

Il s'avère que une seule image provenant d'un registre lent peut bloquer le déploiement par nœud.

Malheureusement, il n'y a pas beaucoup de solutions pour sortir de la situation :

  1. Essayez d'utiliser votre Docker Registry directement dans le cluster ou directement avec le cluster (par exemple, GitLab Registry, Nexus, etc.) ;
  2. Utilisez des utilitaires tels que Kraken.

Histoire 5. Les nœuds se bloquent en raison d'un manque de mémoire

Lors du fonctionnement de diverses applications, nous avons également rencontré une situation où un nœud cesse complètement d'être accessible : SSH ne répond pas, tous les démons de surveillance tombent, et puis il n'y a rien (ou presque rien) d'anormal dans les logs.

Je vais vous le dire en images en utilisant l'exemple d'un nœud sur lequel MongoDB fonctionnait.

Voilà à quoi ressemble le sommet à les accidents:

6 bogues système amusants dans le fonctionnement de Kubernetes [et leur solution]

Et comme ça - après les accidents:

6 bogues système amusants dans le fonctionnement de Kubernetes [et leur solution]

Lors de la surveillance, il y a également un saut brusque, auquel le nœud cesse d'être disponible :

6 bogues système amusants dans le fonctionnement de Kubernetes [et leur solution]

Ainsi, d'après les captures d'écran, il ressort clairement que :

  1. La RAM de la machine est proche de la fin ;
  2. Il y a une forte augmentation de la consommation de RAM, après quoi l'accès à l'ensemble de la machine est brusquement désactivé ;
  3. Une tâche volumineuse arrive sur Mongo, ce qui oblige le processus SGBD à utiliser plus de mémoire et à lire activement à partir du disque.

Il s'avère que si Linux manque de mémoire libre (la pression de la mémoire s'installe) et qu'il n'y a pas d'échange, alors à Lorsque le tueur de MOO arrive, un équilibre peut survenir entre le lancement de pages dans le cache de pages et leur réécriture sur le disque. Ceci est fait par kswapd, qui libère courageusement autant de pages mémoire que possible pour une distribution ultérieure.

Malheureusement, avec une charge d'E/S importante associée à une petite quantité de mémoire libre, kswapd devient le goulot d'étranglement de tout le système, parce qu'ils y sont liés tous allocations (défauts de page) des pages mémoire dans le système. Cela peut durer très longtemps si les processus ne veulent plus utiliser de mémoire, mais sont fixés au bord même de l'abîme du tueur de MOO.

La question naturelle est : pourquoi le tueur OOM arrive-t-il si tard ? Dans son itération actuelle, le tueur de MOO est extrêmement stupide : il ne tuera le processus que lorsque la tentative d'allocation d'une page mémoire échoue, c'est-à-dire si le défaut de page échoue. Cela ne se produit pas avant assez longtemps, car kswapd libère courageusement des pages de mémoire, en vidant le cache des pages (l'intégralité des E/S disque du système, en fait) sur le disque. Plus en détail, avec une description des étapes nécessaires pour éliminer 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 En attente

Dans certains clusters, dans lesquels de très nombreux pods fonctionnent, nous avons commencé à remarquer que la plupart d'entre eux « pendent » très longtemps dans l'état Pending, bien que les conteneurs Docker eux-mêmes soient déjà exécutés sur les nœuds et puissent être utilisés manuellement.

Avec cela dans describe il n'y a rien de mal:

  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

Après quelques recherches, nous avons supposé que le kubelet n'avait tout simplement pas le temps d'envoyer toutes les informations sur l'état des pods et les tests d'activité/préparation au serveur API.

Et après avoir étudié l'aide, nous avons trouvé les paramètres suivants :

--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)

Comme vous pouvez le voir les valeurs par défaut sont assez petites, et à 90% ils couvrent tous les besoins... Cependant, dans notre cas, cela n'était pas suffisant. Par conséquent, nous définissons les valeurs suivantes :

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

... et redémarré les kubelets, après quoi nous avons vu l'image suivante dans les graphiques des appels au serveur API :

6 bogues système amusants dans le fonctionnement de Kubernetes [et leur solution]

... et oui, tout s'est mis à voler !

PS

Pour leur aide dans la collecte des bugs et la préparation de cet article, j'exprime ma profonde gratitude aux nombreux ingénieurs de notre entreprise, et notamment à mon collègue de notre équipe R&D Andrey Klimentyev (zuzzas).

PPS

A lire aussi sur notre blog :

Source: habr.com

Ajouter un commentaire