En DevOps, uno de los problemas más difíciles son aquellos que bloquean activamente su propia investigación. En la contienda por el título del campeonato, el segundo peor tipo de problemas son los que ocurren de manera intermitente. Este artículo es una historia de aventuras de una época en la que el sistema de integración continua/implementación continua (CI/CD) ascendente de Podman encontró ambos simultáneamente.
Una tormenta perfecta
Érase una vez, la automatización de pruebas de Podman comenzó a crecer más que sus pantalones de "niño grande". Esto ocurrió hace años, cuando prácticamente todos los sistemas de CI/CD estaban basados en contenedores. Podman, al ser una herramienta de administración (y depuración) de contenedores (y pod), no se puede ejercer completamente dentro de un contenedor. Quizás peor, la mayoría de los servicios de automatización de productos básicos solo admitían Ubuntu. Esto rápidamente se convirtió en un fracaso absoluto, ya que Podman necesitaba ejecutarse en máquinas virtuales (VM). También necesitaba ejecutarse en varias distribuciones, incluida la distribución ascendente de Red Hat, Fedora.
Al tener experiencia con flujos de trabajo de CI/CD injertados debajo de una configuración cloud-sdk + SSH (y/o Ansible) para el acceso a la nube, la mayor complejidad siempre parece causar problemas. Entonces, un día, me topé con Cirrus-CI. Cirrus-CI es una herramienta de automatización centrada en Git capaz de orquestar contenedores y máquinas virtuales, utilizando una gran cantidad de proveedores de nube. Permitió que el equipo de Podman pagara y administrara el servicio en la nube independientemente de su orquestación. Podríamos mantener el control total sobre las máquinas virtuales y los datos de registro, con Cirrus-CI solo administrando la orquestación.
El flujo de trabajo general es algo así:
- Un desarrollador envía los cambios de código propuestos al repositorio Git de Podman.
- Cirrus-CI avisa, lee un archivo de configuración y luego activa las máquinas virtuales necesarias en nuestra nube.
- Los servicios nativos de metadatos en la nube gestionan la ejecución de scripts y el inicio de sesión en Cirrus-CI.
- Cirrus-CI elimina las máquinas virtuales y proporciona comentarios de aprobación/rechazo al desarrollador.
- Se realizan cambios y el ciclo se repite hasta que se acepta el código.
Durante años, este flujo de trabajo ha funcionado casi a la perfección, con la mayoría de los problemas centrados en pruebas y guiones, fallas que el equipo de Podman maneja fácilmente. En raras ocasiones, los problemas dentro de Cirrus-CI, Internet y/o nuestro proveedor de nube dan como resultado máquinas virtuales huérfanas (no eliminadas). Por lo demás, el personal de soporte de Cirrus Labs ha sido fantástico, muy accesible, con una capacidad de respuesta y responsabilidad de primer nivel.
Luego, un día de octubre de 2020, después de rotar un conjunto de imágenes de VM recién actualizadas (es decir, la imagen de disco copiada para cada nueva instancia de VM), los trabajos aparentemente aleatorios comenzaron a fallar. La investigación inicial de la salida del script no proporcionó información. Literalmente, toda la producción se detendría repentinamente, sin un patrón discernible en relación con otras fallas. Como era de esperar, Cirrus-CI limpiaría diligentemente la VM y le presentaría la falla resultante al desarrollador para que la averigüe. A menudo, al volver a ejecutar el trabajo fallido, se realizaba correctamente sin incidentes.
Esta situación se prolongó durante varias semanas sin problemas, en correspondencia con cualquier interrupción de la infraestructura en nuestra nube, GitHub o Cirrus. El problema era poco frecuente, tal vez un puñado de fallas por día, de cientos de trabajos exitosos. La búsqueda de fallas era difícil y la repetición perpetua de trabajos no podía ser una solución temporal a largo plazo. Además de los informes regulares de incidentes de mi equipo, no pude discernir ningún patrón de alto nivel en las fallas. Dado que las nuevas imágenes de VM brindaron beneficios significativos, el costo de revertir también habría sido alto.
Para casi cualquier problema sistémico como este, encontrar patrones de comportamiento es un elemento clave para una solución de problemas exitosa. Dado que el éxito laboral confiable es el estado deseado, tener al menos alguna noción, o una pista, era un requisito estricto. Desafortunadamente, en este caso, nuestra capacidad para observar patrones estaba extremadamente limitada por las fallas aleatorias y la característica altamente deseable:Limpiar máquinas virtuales en la nube en desuso, que desperdician dinero real.
La ejecución simple, repetida y manual de la prueba no reprodujo el problema en absoluto. Lanzar más recursos de CPU y memoria tampoco afectó el comportamiento. Pasé días reflexionando y pensando en opciones para recopilar datos adicionales asociados con las fallas. Finalmente, me di cuenta de que necesitaba una forma de interrumpir selectivamente la limpieza de la máquina virtual, pero solo en los casos en que las pruebas no terminaron de ejecutarse.
[ También te puede interesar: De administrador de sistemas a DevOps ]
En otras palabras, necesitaba asociar de alguna manera la finalización exitosa de la prueba (no solo aprobar/reprobar) con permitir que se realizara la limpieza. Fue entonces cuando recordé una pequeña casilla de verificación que vi una vez mientras hurgaba en la WebUI de nuestra nube:Protección contra eliminación . Cuando se establece este indicador, Cirrus-CI se quejará en voz alta porque no puede eliminar una VM, pero de lo contrario, no se alterará.
Necesitaba instrumentar nuestro flujo de trabajo para que las propias máquinas virtuales pudieran establecer y desactivar su propio indicador de protección contra eliminación. Algo como esto:
- VM habilita la protección de eliminación en sí misma.
- Ejecutar pruebas.
- VM desactiva su propia protección de eliminación.
- Cirrus-CI limpia o no limpia la máquina virtual y hace un escándalo al respecto.
De esta forma, cuando se completen las pruebas, Cirrus-CI eliminaría felizmente la máquina virtual como de costumbre. Sin embargo, si ocurriera el problema, las pruebas no se completarían y Cirrus-CI se encontraría con la protección contra eliminación (todavía) habilitada. Afortunadamente, este flujo de trabajo fue completamente posible de realizar a través de opciones de línea de comandos simples para el programa de utilidad de administración de la nube que pude instalar en las máquinas virtuales.
Con esta instrumentación de flujo de trabajo en su lugar, todo lo que tenía que hacer era activar repetidamente la matriz de prueba y esperar a que aparecieran las máquinas virtuales huérfanas. Los destinos me sonreían ese día porque el problema se reprodujo casi al instante. Sin embargo, lo ejecuté unas cuantas veces más, por lo que tendría más de unas pocas máquinas virtuales huérfanas para inspeccionar.
Inesperadamente, aquí es cuando la situación se volvió aún más interesante:no podía SSH en las máquinas virtuales huérfanas. De hecho, ni siquiera responderían a los pings desde dentro o fuera de nuestra nube. Hice un restablecimiento completo en una máquina virtual y verifiqué los registros del sistema una vez que se inició. No había nada. Cremallera. Nada. Ni un solo ápice de una pista aparte de lo que ya sabíamos:las pruebas simplemente dejaron de ejecutarse, muertas, junto con el resto del sistema.
Como ya era mi día de suerte, decidí presionarlo y nuevamente me puse a hurgar en la WebUI de mi nube. Eventualmente, encontré otra pequeña configuración increíblemente útil:Registro de salida de la consola en serie . Esta era básicamente una línea de comunicación directa de bajo nivel directamente al kernel. Si algo estuviera sucediendo lo suficientemente horrible como para que el sistema se colgara por completo, seguramente el núcleo estaría gritando desde su puerto serie virtual. ¡Bingo, Yahtzee y huzzah!
[ 1203.905090] BUG: kernel NULL pointer dereference, address: 0000000000000000
[ 1203.912275] #PF: supervisor read access in kernel mode
[ 1203.917588] #PF: error_code(0x0000) - not-present page
[ 1203.922881] PGD 8000000124e2d067 P4D 8000000124e2d067 PUD 138d5e067 PMD 0
[ 1203.929939] Oops: 0000 [#1] SMP PTI
...blah...blah...blah
[ 1204.052766] Call Trace:
[ 1204.055440] bfq_idle_extract+0x52/0xb0
[ 1204.059468] bfq_put_idle_entity+0x12/0x60
[ 1204.063722] bfq_bfqq_served+0xb0/0x190
[ 1204.067718] bfq_dispatch_request+0x2c2/0x1070
¡Hola, viejo amigo! ¡Efectivamente, fue un día de mucha suerte!
Este fue un pánico del kernel que había visto un año antes y había trabajado incansablemente durante meses con la ingeniería del kernel anterior para solucionarlo. Fue un error en el subsistema de almacenamiento del kernel responsable de mejorar la eficiencia y garantizar que los preciosos 0 y 1 se escriban en el disco. En este caso, en lugar de dañar el almacenamiento, el kernel levantó una bandera blanca y detuvo todo en seco.
Habiendo trabajado casi exactamente en este mismo problema antes, ya conocía la solución de una línea. No hubo necesidad de arrastrar a mi equipo a través de meses de depuración nuevamente. Simplemente podría informar la recurrencia e implementar con confianza la solución alternativa, sabiendo que resolvería el problema al 100 %:
echo mq-deadline > /sys/block/sda/queue/scheduler
Ahora, no recomiendo ejecutar ese comando de cualquier manera en cada sistema Linux que posea. En este caso específico, sabía por experiencias pasadas que era completamente seguro cambiar el algoritmo de almacenamiento en búfer (también llamado elevador de E/S). Ningún desarrollador de Podman notaría el cambio en los resultados de las pruebas.
[ ¿Empezando con los contenedores? Consulta este curso gratuito. Implementación de aplicaciones en contenedores:una descripción técnica general. ]
Resumir
Si usted o alguien a quien ama alguna vez encuentra un kernel panic propio, le recomiendo que consulte este artículo reciente sobre el tema. De lo contrario, la conclusión principal es esta:al solucionar problemas a ciegas, resolver los aspectos de ofuscación de la causa es absolutamente vital. El segundo es trabajar con los datos para hacer que el problema sea más reproducible. Le resultará muy difícil hacer lo último sin la ayuda de lo primero.