6 divertidos errores de sistema en el funcionamiento de Kubernetes [y su solución]

6 divertidos errores de sistema en el funcionamiento de Kubernetes [y su solución]

A lo largo de los años de uso de Kubernetes en producción, hemos acumulado muchas historias interesantes sobre cómo los errores en varios componentes del sistema llevaron a consecuencias desagradables y/o incomprensibles que afectaron el funcionamiento de contenedores y pods. En este artículo hemos hecho una selección de algunos de los más comunes o interesantes. Incluso si nunca tienes la suerte de encontrarte con situaciones así, leer sobre historias de detectives tan cortas, especialmente "de primera mano", siempre es interesante, ¿no es así?

Historia 1. Supercronic y Docker colgando

En uno de los clústeres, periódicamente recibimos un Docker congelado, lo que interfería con el funcionamiento normal del clúster. Al mismo tiempo, se observó lo siguiente en los registros de 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

…

Lo que más nos interesa de este error es el mensaje: pthread_create failed: No space left on device. Estudio rápido documentación Explicó que Docker no podía bifurcar un proceso, por lo que se congelaba periódicamente.

En el seguimiento, la siguiente imagen corresponde a lo que está sucediendo:

6 divertidos errores de sistema en el funcionamiento de Kubernetes [y su solución]

Una situación similar se observa en otros nodos:

6 divertidos errores de sistema en el funcionamiento de Kubernetes [y su solución]

6 divertidos errores de sistema en el funcionamiento de Kubernetes [y su solución]

En los mismos nodos vemos:

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>

Resultó que este comportamiento es consecuencia de que el pod trabaja con supercrónico (una utilidad Go que usamos para ejecutar trabajos cron en 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>
…

El problema es este: cuando una tarea se ejecuta en modo supercrónico, el proceso generado por ella no puede terminar correctamente, conviertiéndose en zombi.

Nota: Para ser más precisos, los procesos se generan mediante tareas cron, pero supercronic no es un sistema de inicio y no puede "adoptar" procesos que generaron sus hijos. Cuando se generan señales SIGHUP o SIGTERM, no se pasan a los procesos secundarios, lo que hace que los procesos secundarios no finalicen y permanezcan en estado zombie. Puedes leer más sobre todo esto, por ejemplo, en tal artículo.

Hay un par de formas de resolver problemas:

  1. Como solución temporal, aumente la cantidad de PID en el sistema en un solo momento:
           /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. O iniciar tareas en supercronic no directamente, sino usando el mismo tini, que es capaz de finalizar procesos correctamente y no generar zombis.

Historia 2. "Zombies" al eliminar un cgroup

Kubelet empezó a consumir mucha CPU:

6 divertidos errores de sistema en el funcionamiento de Kubernetes [y su solución]

A nadie le gustará esto, así que nos armamos. Perf y comenzó a lidiar con el problema. Los resultados de la investigación fueron los siguientes:

  • Kubelet dedica más de un tercio del tiempo de su CPU a extraer datos de memoria de todos los cgroups:

    6 divertidos errores de sistema en el funcionamiento de Kubernetes [y su solución]

  • En la lista de correo de los desarrolladores del kernel puede encontrar discusión del problema. En resumen, el punto se reduce a esto: varios archivos tmpfs y otras cosas similares no se eliminan por completo del sistema al eliminar un cgroup, el llamado memcg zombi. Tarde o temprano se eliminarán del caché de la página, pero hay mucha memoria en el servidor y el kernel no ve el sentido de perder el tiempo eliminándolos. Por eso se siguen acumulando. ¿Por qué sucede esto? Este es un servidor con trabajos cron que constantemente crea nuevos trabajos y con ellos nuevos pods. Por lo tanto, se crean nuevos cgroups para los contenedores que contienen, que pronto se eliminan.
  • ¿Por qué cAdvisor en kubelet pierde tanto tiempo? Esto es fácil de ver con la ejecución más simple. time cat /sys/fs/cgroup/memory/memory.stat. Si en una máquina en buen estado la operación tarda 0,01 segundos, en el cron02 problemático tarda 1,2 segundos. El caso es que cAdvisor, que lee datos de sysfs muy lentamente, intenta tener en cuenta la memoria utilizada en los cgroups zombies.
  • Para eliminar zombies por la fuerza, intentamos borrar los cachés como se recomienda en LKML: sync; echo 3 > /proc/sys/vm/drop_caches, - pero el núcleo resultó ser más complicado y estrelló el coche.

¿Qué hacer? El problema se está solucionando (comprometerse, y para una descripción ver mensaje de liberación) actualizando el kernel de Linux a la versión 4.16.

Historia 3. Systemd y su montaje

Nuevamente, kubelet consume demasiados recursos en algunos nodos, pero esta vez consume demasiada memoria:

6 divertidos errores de sistema en el funcionamiento de Kubernetes [y su solución]

Resultó que hay un problema en systemd usado en Ubuntu 16.04, y ocurre al administrar los montajes que se crean para la conexión. subPath de ConfigMaps o secretos. Después de que la cápsula haya completado su trabajo. el servicio systemd y su montaje de servicio permanecen en el sistema. Con el tiempo, se acumula una gran cantidad de ellos. Incluso hay problemas sobre este tema:

  1. #5916;
  2. #57345.

...el último de los cuales se refiere al PR en systemd: #7811 (problema en systemd - #7798).

El problema ya no existe en Ubuntu 18.04, pero si desea continuar usando Ubuntu 16.04, puede que le resulte útil nuestra solución alternativa a este tema.

Entonces creamos el siguiente 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

... y utiliza el siguiente 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

... y se ejecuta cada 5 minutos utilizando el supercrónico mencionado anteriormente. Su Dockerfile se ve así:

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

Historia 4. Competitividad al programar pods

Se observó que: si tenemos un pod colocado en un nodo y su imagen se emite durante mucho tiempo, entonces otro pod que "golpee" el mismo nodo simplemente no comienza a extraer la imagen del nuevo pod. En cambio, espera hasta que se extraiga la imagen del pod anterior. Como resultado, un pod que ya estaba programado y cuya imagen podría haberse descargado en apenas un minuto terminará en el estado de containerCreating.

Los eventos se verán así:

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

Resulta que una sola imagen de un registro lento puede bloquear la implementación por nodo.

Desafortunadamente, no hay muchas salidas a esta situación:

  1. Intente utilizar su Docker Registry directamente en el clúster o directamente con el clúster (por ejemplo, GitLab Registry, Nexus, etc.);
  2. Utilice utilidades como kraken.

Historia 5. Los nodos se bloquean por falta de memoria.

Durante el funcionamiento de varias aplicaciones, también nos encontramos con una situación en la que un nodo deja de ser accesible por completo: SSH no responde, todos los demonios de monitoreo caen y luego no hay nada (o casi nada) anómalo en los registros.

Te lo contaré en imágenes usando el ejemplo de un nodo donde funcionaba MongoDB.

Así se ve arriba a accidentes:

6 divertidos errores de sistema en el funcionamiento de Kubernetes [y su solución]

Y así - después accidentes:

6 divertidos errores de sistema en el funcionamiento de Kubernetes [y su solución]

En el seguimiento también hay un salto brusco, en el que el nodo deja de estar disponible:

6 divertidos errores de sistema en el funcionamiento de Kubernetes [y su solución]

Así, de las capturas de pantalla queda claro que:

  1. La RAM de la máquina está a punto de agotarse;
  2. Hay un fuerte salto en el consumo de RAM, después del cual el acceso a toda la máquina se desactiva abruptamente;
  3. Llega una gran tarea a Mongo, lo que obliga al proceso DBMS a utilizar más memoria y leer activamente desde el disco.

Resulta que si Linux se queda sin memoria libre (la presión de la memoria aumenta) y no hay intercambio, entonces a Cuando llega el asesino de OOM, puede surgir un acto de equilibrio entre arrojar páginas al caché de páginas y escribirlas nuevamente en el disco. Esto lo hace kswapd, que valientemente libera tantas páginas de memoria como sea posible para su posterior distribución.

Desafortunadamente, con una gran carga de E/S junto con una pequeña cantidad de memoria libre, kswapd se convierte en el cuello de botella de todo el sistema, porque están atados a ello todos Asignaciones (fallos de página) de páginas de memoria en el sistema. Esto puede continuar durante mucho tiempo si los procesos ya no quieren usar la memoria, sino que están fijos en el borde mismo del abismo asesino de OOM.

La pregunta natural es: ¿por qué el asesino de OOM llega tan tarde? En su versión actual, el asesino de OOM es extremadamente estúpido: matará el proceso sólo cuando falle el intento de asignar una página de memoria, es decir. si el error de página falla. Esto no sucede durante bastante tiempo, porque kswapd valientemente libera páginas de memoria, volcando el caché de la página (de hecho, toda la E/S del disco en el sistema) nuevamente al disco. Con más detalle, con una descripción de los pasos necesarios para eliminar dichos problemas en el kernel, puede leer aquí.

Este comportamiento debería mejorar con kernel de Linux 4.6+.

Historia 6. Los pods se atascan en el estado Pendiente

En algunos grupos, en los que realmente hay muchas cápsulas en funcionamiento, comenzamos a notar que la mayoría de ellas “cuelga” durante mucho tiempo en el estado. Pending, aunque los propios contenedores Docker ya se están ejecutando en los nodos y se puede trabajar con ellos manualmente.

Con esto en describe no hay nada malo:

  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

Después de investigar un poco, asumimos que kubelet simplemente no tiene tiempo para enviar toda la información sobre el estado de los pods y las pruebas de actividad/preparación al servidor API.

Y después de estudiar la ayuda, encontramos los siguientes parámetros:

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

Como puedes ver los valores predeterminados son bastante pequeños, y en un 90% cubren todas las necesidades... Sin embargo, en nuestro caso esto no fue suficiente. Por lo tanto, establecemos los siguientes valores:

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

... y reiniciamos los kubelets, tras lo cual vimos la siguiente imagen en los gráficos de llamadas al servidor API:

6 divertidos errores de sistema en el funcionamiento de Kubernetes [y su solución]

... y sí, ¡todo empezó a volar!

PS

Por su ayuda en la recopilación de errores y la preparación de este artículo, expreso mi profundo agradecimiento a los numerosos ingenieros de nuestra empresa, y especialmente a mi colega de nuestro equipo de I+D, Andrey Klimentyev (zuzas).

PPS

Lea también en nuestro blog:

Fuente: habr.com

Añadir un comentario