ProHoster > Blog > administración > 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:
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):
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:
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
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:
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:
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:
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:
...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.
#!/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í:
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:
Intente utilizar su Docker Registry directamente en el clúster o directamente con el clúster (por ejemplo, GitLab Registry, Nexus, etc.);
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:
Y así - después accidentes:
En el seguimiento también hay un salto brusco, en el que el nodo deja de estar disponible:
Así, de las capturas de pantalla queda claro que:
La RAM de la máquina está a punto de agotarse;
Hay un fuerte salto en el consumo de RAM, después del cual el acceso a toda la máquina se desactiva abruptamente;
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:
... y reiniciamos los kubelets, tras lo cual vimos la siguiente imagen en los gráficos de llamadas al servidor API:
... 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).