6 errores de sistema interesantes al operar Kubernetes [y sus soluciones]

6 errores de sistema interesantes al operar Kubernetes [y sus soluciones]

A lo largo de los años de uso de Kubernetes en producción, hemos acumulado bastantes historias interesantes sobre cómo los errores en diversos componentes del sistema han conducido a consecuencias incómodas y/o inexplicables, afectando el funcionamiento de los contenedores y los pods. En este artículo, hemos recopilado algunas de las más frecuentes o interesantes. Incluso si nunca te encuentras en tales situaciones, leer sobre estos breves misterios — especialmente «de primera mano» — siempre es intrigante, ¿no es así?

Historia 1. Supercronic y Docker colgado

En uno de los clústeres, ocasionalmente experimentamos un Docker «colgado», lo que interfería con el funcionamiento normal del clúster. En los registros de Docker se observó lo siguiente:

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

…

En este error, lo que más nos interesa es el mensaje: pthread_create failed: No space left on device. Un rápido estudio la documentación aclaró que Docker no podía crear un proceso, lo que provocaba que, en ocasiones, se «colgara».

El monitoreo mostraba lo siguiente:

6 errores de sistema interesantes al operar Kubernetes [y sus soluciones]

Una situación similar se observa en otros nodos:

6 errores de sistema interesantes al operar Kubernetes [y sus soluciones]

6 errores de sistema interesantes al operar Kubernetes [y sus soluciones]

En estos 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] 
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]

Resultó que este comportamiento es consecuencia del funcionamiento del pod con supercronic (una utilidad escrita en Go que usamos para ejecutar tareas cron en los 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] 
…

El problema es el siguiente: cuando una tarea se inicia en supercronic, el proceso generado por él no puede finalizar correctamente, convirtiéndose en zombis.

Nota: En términos más precisos, los procesos son creados por tareas de cron, sin embargo, supercronic no es un sistema init y no puede "adoptar" los procesos que sus hijos han creado. Cuando ocurre la señal SIGHUP o SIGTERM, no se envían a los procesos generados, lo que resulta en que los procesos secundarios no se terminan y quedan en estado de zombi. Se puede leer más sobre todo esto, por ejemplo, en este artículo.

Hay un par de maneras de resolver problemas:

  1. Como una solución temporal, aumentar el número 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 ejecutar tareas en supercronic no directamente, sino utilizando el mismo tini, que puede finalizar correctamente los procesos y no generar zombis.

Historia 2. ‘Zombis’ al eliminar cgroup

Kubelet comenzó a consumir una gran cantidad de CPU:

6 errores de sistema interesantes al operar Kubernetes [y sus soluciones]

Eso no le gustará a nadie, así que nos armamos perf y comenzamos a investigar el problema. Los resultados de la investigación resultaron ser los siguientes:

  • Kubelet utiliza más de un tercio del tiempo de CPU extrayendo datos de memoria de todos los cgroup:

    6 errores de sistema interesantes al operar Kubernetes [y sus soluciones]

  • En la lista de correo de desarrolladores del núcleo, se puede encontrar una discusión del problema. En resumen, se reduce a que diferentes archivos tmpfs y otras cosas similares no se eliminan completamente del sistema al eliminar cgroup — quedan lo que se llaman memcg zombis. Tarde o temprano, se eliminarán del caché de la página, aunque el servidor tiene mucha memoria y el núcleo no ve sentido en dedicar tiempo a eliminarlos. Por eso, siguen acumulándose. ¿Por qué ocurre esto? Es un servidor con tareas cron que constantemente genera nuevos trabajos, y con ellos, nuevos pods. De este modo, para los contenedores se crean nuevos cgroups, que pronto se eliminan.
  • ¿Por qué cAdvisor en kubelet tarda tanto tiempo? Esto se puede ver fácilmente ejecutando time cat /sys/fs/cgroup/memory/memory.stat. Si en una máquina sana la operación toma 0,01 segundos, en la problemática cron02 toma 1,2 segundos. Todo se debe a que cAdvisor, que lee los datos de sysfs muy lentamente, intenta tener en cuenta la memoria utilizada y los cgroups zombi.
  • Para eliminar forzosamente los zombis, intentamos limpiar las cachés, como se recomienda en el LKML: sync; echo 3 > /proc/sys/vm/drop_caches, — pero el núcleo resultó ser más complicado y colapsó la máquina.

¿Qué hacer? El problema se puede resolver (commit, y para más detalles, vea el aviso de lanzamiento) actualizando el núcleo de Linux a la versión 4.16.

Historia 3. Systemd y su montaje

De nuevo kubelet consume demasiados recursos en algunos nodos, pero esta vez — ya es memoria:

6 errores de sistema interesantes al operar Kubernetes [y sus soluciones]

Resulta que hay un problema en systemd, utilizado en Ubuntu 16.04, que ocurre al gestionar los montajes que se crean para conectar subPath de ConfigMaps o secretos. Después de que el pod finaliza, el servicio systemd y su montaje auxiliar permanecen en el sistema. Con el tiempo, se acumulan en gran cantidad. Hay incluso temas sobre esto:

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

… en el último de los cuales se hace referencia a un PR en systemd: #7811 (tema en systemd — #7798).

El problema ya no está en Ubuntu 18.04, pero si desea seguir utilizando Ubuntu 16.04, puede que le sirva nuestra solución alternativa sobre este tema.

Así que hicimos 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 en él se 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 mediante el ya mencionado supercronic. Su Dockerfile es 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. Concurrencia en la programación de pods

Se observó que: si se coloca un pod en el nodo y su imagen tarda mucho en descargarse, otro pod que "cayó" en el mismo nodo simplemente no comienza a descargar la imagen del nuevo pod.En su lugar, espera a que se descargue la imagen del pod anterior. Como resultado, el pod que ya estaba programado y cuya imagen podría haberse descargado en un minuto, se quedará por un tiempo prolongado en estado containerCreating.

En los eventos habrá algo 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 el despliegue en el nodo.

Desafortunadamente, no hay muchas soluciones para esta situación:

  1. Intenta usar tu Docker Registry directamente en el clúster o directamente con el clúster (por ejemplo, GitLab Registry, Nexus, etc.);
  2. Utiliza herramientas como kraken.

Historia 5. Congelamiento de nodos por falta de memoria

Durante la operación de diversas aplicaciones, también hemos tenido situaciones en las que un nodo deja de ser completamente accesible: no responde a SSH, todos los demonios de monitoreo se caen, y en los registros luego no hay nada (o casi nada) anómalo.

Te lo explicaré con imágenes usando un ejemplo de un nodo donde funcionaba MongoDB.

Así se ve atop hasta fallos:

6 errores de sistema interesantes al operar Kubernetes [y sus soluciones]

Y así — después de fallos:

6 errores de sistema interesantes al operar Kubernetes [y sus soluciones]

También se observa un aumento abrupto en el monitoreo, donde el nodo deja de ser accesible:

6 errores de sistema interesantes al operar Kubernetes [y sus soluciones]

Así, a partir de las capturas de pantalla se puede observar que:

  1. La memoria RAM de la máquina está casi al límite;
  2. Se observa un aumento abrupto en el consumo de memoria RAM, después del cual se corta el acceso a toda la máquina;
  3. Mongo recibe una gran tarea que hace que el proceso del SGBD use más memoria y lea activamente del disco.

Resulta que si en Linux se acaba la memoria libre (se produce pressure de memoria) y no hay swap, hasta la llegada del OOM killer puede llevar a un equilibrio entre colocar páginas en el page cache y enviarlas de vuelta al disco. Esto lo hace el kswapd, que valientemente libera tantas páginas de memoria como puede para la distribución posterior.

Desafortunadamente, bajo una alta carga de entrada/salida junto con una pequeña cantidad de memoria libre, kswapd se convierte en el cuello de botella de todo el sistema, ya que está vinculado a cargas de trabajo dejarán de funcionar! las asignaciones (page faults) de páginas de memoria en el sistema. Esto puede continuar durante mucho tiempo, si los procesos no desean usar más memoria y se quedan al borde del abismo del OOM killer.

Es lógico preguntarse: ¿por qué llega el OOM killer tan tarde? En su iteración actual, el OOM killer es extremadamente tonto: solo matará un proceso cuando falle el intento de asignar una página de memoria, es decir, si el page fault ocurre con error. Esto no sucede durante mucho tiempo, porque kswapd libera valientemente las páginas de memoria al volcar el page cache (toda la E/S de disco en el sistema, en esencia) de vuelta al disco. Para obtener más información, con una descripción de los pasos necesarios para solucionar este tipo de problemas en el núcleo, se puede leer aquí.

Este comportamiento debería mejorar con el núcleo de Linux 4.6+.

Historia 6. Los pods quedan en estado Pending

En algunos clústeres donde realmente funcionan muchos pods, comenzamos a notar que la mayoría se queda 'colgada' en estado Pending, aunque los contenedores de Docker ya están en funcionamiento en los nodos y se pueden manejar manualmente.

Sin embargo, en describe no hay nada de malo:

  Type    Reason                  Age                From                     Message
  ----    ------                  ----               ----                     -------
  Normal  Scheduled               1m                 default-scheduler        Asignado exitosamente sphinx-0 a ss-dev-kub07
  Normal  SuccessfulAttachVolume  1m                 attachdetach-controller  AttachVolume.Attach tuvo éxito para el volumen "pvc-6aaad34f-ad10-11e8-a44c-52540035a73b"
  Normal  SuccessfulMountVolume   1m                 kubelet, ss-dev-kub07    MountVolume.SetUp tuvo éxito para el volumen "sphinx-config"
  Normal  SuccessfulMountVolume   1m                 kubelet, ss-dev-kub07    MountVolume.SetUp tuvo éxito para el volumen "default-token-fzcsf"
  Normal  SuccessfulMountVolume   49s (x2 en 51s)  kubelet, ss-dev-kub07    MountVolume.SetUp tuvo éxito para el volumen "pvc-6aaad34f-ad10-11e8-a44c-52540035a73b"
  Normal  Pulled                  43s                kubelet, ss-dev-kub07    La imagen del contenedor "registry.example.com/infra/sphinx-exporter/sphinx-indexer:v1" ya está presente en la máquina
  Normal  Created                 43s                kubelet, ss-dev-kub07    Contenedor creado
  Normal  Started                 43s                kubelet, ss-dev-kub07    Contenedor iniciado
  Normal  Pulled                  43s                kubelet, ss-dev-kub07    La imagen del contenedor "registry.example.com/infra/sphinx/sphinx:v1" ya está presente en la máquina
  Normal  Created                 42s                kubelet, ss-dev-kub07    Contenedor creado
  Normal  Started                 42s                kubelet, ss-dev-kub07    Contenedor iniciado

Al investigar, supusimos que kubelet simplemente no puede enviar toda la información sobre el estado de los pods al servidor API a tiempo, incluidas las pruebas de liveness/readiness.

Y al estudiar la ayuda, encontramos los siguientes parámetros:

--kube-api-qps - QPS a utilizar al comunicarse con el servidor API de Kubernetes (predeterminado 5)
--kube-api-burst  - Burst a utilizar al comunicarse con el servidor API de Kubernetes (predeterminado 10) 
--event-qps - Si > 0, limita la creación de eventos por segundo a este valor. Si es 0, ilimitado. (predeterminado 5)
--event-burst - Tamaño máximo de los registros de eventos espásticos, permite temporalmente que los registros de eventos excedan este número, sin superar el event-qps. Solo se utiliza si --event-qps > 0 (predeterminado 10) 
--registry-qps - Si > 0, limita el QPS de extracción del registro a este valor.
--registry-burst - Tamaño máximo de las extracciones espáticas, permite temporalmente que las extracciones excedan este número, sin superar el registry-qps. Solo se utiliza si --registry-qps > 0 (predeterminado 10)

Como se puede ver, los valores por defecto son bastante pequeños, y en el 90 % de los casos cubren todas las necesidades... Sin embargo, en nuestro caso no fueron suficientes. Por lo tanto, establecimos 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 kubelet, tras lo cual en los gráficos de acceso al servidor API vimos la siguiente situación:

6 errores de sistema interesantes al operar Kubernetes [y sus soluciones]

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

P.D.

Agradezco enormemente a los numerosos ingenieros de nuestra empresa por su ayuda en la recopilación de errores y en la preparación del artículo, y especialmente a mi colega del equipo de I+D, Andrei Klimentyev (zuzzas).

P.P.D.

También puedes leer en nuestro blog:

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster