Muy buenas, soy Luis y en esta ocasión les traigo otro tutorial.
Kubernetes es una parte crucial de nuestra infraestructura. No solo implementamos aplicaciones en Kubernetes en producción, sino que también las usamos mucho para nuestra infraestructura de desarrollo y CI / CD. Mientras desarrollamos nuestra infraestructura de CI / CD, nos ocupamos de un problema de rendimiento particular de nuestros entornos de desarrollo y CI que tomaban mucho tiempo para funcionar.
En este artículo, intentaremos profundizar en el problema que degradó el rendimiento de nuestras aplicaciones y cómo finalmente resolvimos ese problema.
Índice
Antecedentes
En Grofers, seguimos una arquitectura de microservicio donde todos los componentes críticos como pagos, carritos, inventario, etc. se organizan como un microservicio. Por lo tanto, los desarrolladores no pueden trabajar en varios servicios al mismo tiempo en el mismo espacio de nombres. Esto nos llevó a adoptar un diseño en el que cada desarrollador tiene su propio espacio de nombres con todos los servicios implementados en un entorno aislado para realizar pruebas y depurar. Esto es lo que llamamos internamente ‘Grofers-in-a-namespace’. Para lograr esto, hemos desarrollado una herramienta interna llamada mft. Mft se utiliza para crear nuevos espacios de nombres en Kubernetes e inyectar las dependencias necesarias de Bóveda y Cónsul.
Todos los servicios necesarios para ejecutar una copia de Grofers se implementan a través de la configuración común de Jenkins. Luego, los servicios se exponen a través de la entrada para que los desarrolladores los usen con el conjunto de pruebas o como punto final para aplicaciones de depuración para pruebas de aplicaciones.
La cuestión
Adoptamos USE-ROJO paneles de control de nuestros servicios para ayudarnos a rastrear métricas críticas. Esto nos permitió optimizar aún más la infraestructura para obtener el máximo rendimiento.
Cuando comenzamos a analizar dichas métricas, observamos que ciertas aplicaciones tardaron mucho en iniciarse y prepararse, algo que los desarrolladores no observaron en la configuración local. Además, algunos de nuestros trabajos de Jenkins tomaron mucho tiempo para crear los entornos y prepararlos, algo que anteriormente no teníamos en cuenta. Esto nos llevó a revisar todas las métricas para identificar qué podría haber causado este inicio lento en nuestra infraestructura de Kubernetes.
Además, estos problemas no se estaban observando en entornos de producción. Las implementaciones de producción tienen manifiestos ligeramente diferentes, con una asignación de CPU y memoria mucho más liberal. Además, nuestro clúster de producción tiene mucho más margen para escalar, lo que nos llevó a creer que se trata de un problema específico de infraestructura.
Causa principal
Después de depurar durante casi un mes, decidimos volver a visitar nuestras pruebas y la configuración de Kubernetes para ayudar a aislar el problema. Durante el proceso de optimización de nuestro RAV (Regresión UNADakota del Norte Verificación), comenzamos a trazar todas las métricas de Kubernetes que podrían afectar el rendimiento de nuestros contenedores. Una métrica interesante que identificamos fue la aceleración de la CPU (container_cpu_cfs_throttled_seconds_total
). Una vez que trazamos esa métrica, encontramos resultados impactantes e interesantes. Algunos de nuestros servicios más críticos estaban acelerando la CPU y no teníamos idea. Además, observamos que en nuestros entornos de desarrollo y CI, esto sucedía mucho con algunos contenedores específicos en el momento del inicio; estos eran contenedores en los que estábamos ejecutando algún tipo de operación intensiva de CPU en el momento del inicio.
Inmediatamente comenzamos a planificar un análisis de causa y efecto y se nos ocurrieron las siguientes causas:
- Límite incorrecto de CPU de contenedores que hace que la aplicación alcance los límites rápidamente, lo que hace que Kubernetes la acelere.
- Actividad en segundo plano como GC que se activa después de un tiempo y hace que aumenten los ciclos de CPU. Esto también puede deberse a tamaños de pila incorrectos para aplicaciones basadas en JVM.
- Cierta actividad periódica intensiva de la CPU en el nodo que roba los ciclos de la CPU disponibles para cgroups, lo que también apunta a los límites de la CPU que no se decidió mantener picos periódicos en la lógica de la aplicación
¿Qué es la aceleración de la CPU?
Casi todos los orquestadores de contenedores se basan en los mecanismos del grupo de control del kernel (cgroup) para gestionar las limitaciones de recursos. Cuando se establecen límites estrictos de CPU en un orquestador de contenedores, el kernel utiliza Programador completamente justo (CFS) Cgroup control de ancho de banda para hacer cumplir esos límites. El mecanismo de control de ancho de banda de CFS-Cgroup administra la asignación de CPU mediante dos configuraciones: cuota y período. Cuando una aplicación ha utilizado su cuota de CPU asignada durante un período determinado, se ralentiza hasta el siguiente período.
Todas las métricas de CPU para un cgroup se encuentran en /sys/fs/cgroup/cpu,cpuacct/<container>
. La configuración de cuotas y períodos está en cpu.cfs_quota
y cpu.cfs_period
.
También puede ver las métricas de limitación en cpu.stat. Dentro de cpu.stat encontrará:
- nr_periods: número de períodos en los que se pudo ejecutar cualquier hilo del grupo c
- nr_throttled: número de períodos ejecutables en los que la aplicación usó toda su cuota y se limitó
- throttled_time – suma la cantidad total de tiempo que se limitaron los subprocesos individuales dentro del cgroup.
Supervisión de los límites de memoria para muertes de OOM
Otra métrica interesante a considerar es la cantidad de reinicios del contenedor debido a OOM. Esto destaca que los contenedores alcanzan con frecuencia los límites de memoria especificados en sus manifiestos de Kubernetes.
kube_pod_container_status_terminated_reasonreason=”OOMKilled”)
Resolución
Entonces, una solución rápida al problema fue aumentar los límites entre un 10 y un 25% para garantizar que los picos se alcancen con menos frecuencia o se eviten por completo.
Después de identificar la causa raíz, se nos ocurrieron algunas posibles soluciones. Tuvimos en cuenta las siguientes consideraciones:
La limitación de la CPU se debe principalmente a los límites bajos de la CPU. Sus límites que realmente afectan el comportamiento de Cgroup. Entonces, una solución rápida al problema fue aumentar los límites entre un 10 y un 25% para garantizar que los picos se alcancen con menos frecuencia o se eviten por completo. Esto tampoco afecta los requisitos de recursos para iniciar pods, ya que las solicitudes permanecen intactas.
Mientras tanto, para ciertas aplicaciones intensivas, especialmente aquellas que utilizan sistemas basados en JVM, decidimos perfilar la aplicación nuevamente para los requisitos correctos de CPU y memoria, ya que JVM es conocido por su alto consumo de recursos. Ajustar los parámetros de JVM será la solución adecuada a largo plazo para tales aplicaciones.
Lo que aprendimos y los próximos pasos
Fue una experiencia reveladora para nosotros. Nos dimos cuenta de que algunas de las métricas menos consideradas (o en este caso pasadas por alto) pueden tener un impacto profundo en el rendimiento de la aplicación. También obtuvimos información asombrosa sobre CFS y cgroups, y sobre cómo el kernel maneja la virtualización de recursos.
Basándonos en este ejercicio, creamos un plan de creación de perfiles de aplicaciones para nuestras aplicaciones principales y agregamos la limitación de CPU a uno de nuestros principales sospechosos de rendimiento deficiente de las aplicaciones.
Referencias
- Capítulo 1: Introducción a los grupos de control (CGROUPS).
- cgroups (7) – página del manual de Linux.
- https://sysdig.com/blog/troubleshoot-kubernetes-oom.
Gracias por leer.
Añadir comentario