Límites de CPU y aceleración agresiva en Kubernetes

Nota perev. : Esta historia de advertencia de Omio, el agregador de viajes europeo, lleva a los lectores desde la teoría básica hasta las fascinantes complejidades prácticas de la configuración de Kubernetes. La familiaridad con tales casos ayuda no solo a ampliar los horizontes, sino también a prevenir problemas no triviales.



¿Alguna vez se ha encontrado con el hecho de que la aplicación "se atascó" en su lugar, dejó de responder a las solicitudes de verificación de estado y no pudo entender la razón de este comportamiento? Una posible explicación es el límite de cuota para los recursos de la CPU. Será discutido en este artículo.

TL; DR:
Recomendamos encarecidamente que salga de los límites de CPU en Kubernetes (o deshabilite las cuotas CFS en Kubelet) si está utilizando una versión del kernel de Linux con un error de cuota CFS. En el centro hayUn error grave y conocido que conduce a una aceleración excesiva y retrasos
.

En Omio, toda la infraestructura es administrada por Kubernetes . Todas nuestras cargas con estado y sin estado funcionan exclusivamente en Kubernetes (utilizamos el motor de Google Kubernetes). En los últimos seis meses, comenzamos a observar ralentizaciones aleatorias. Las aplicaciones se congelan o dejan de responder a las comprobaciones de estado, pierden su conexión a la red, etc. Tal comportamiento nos ha dejado perplejos durante mucho tiempo y, finalmente, decidimos abordar el problema de cerca.

Resumen del artículo:

  • Algunas palabras sobre contenedores y Kubernetes;
  • Cómo se implementan los límites y las solicitudes de CPU;
  • Cómo funciona el límite de CPU en entornos multinúcleo;
  • Cómo rastrear la aceleración de la CPU;
  • Resolviendo el problema y los matices.

Algunas palabras sobre contenedores y Kubernetes


Kubernetes, de hecho, es el estándar moderno en el mundo de la infraestructura. Su tarea principal es la orquestación de contenedores.

Contenedores


En el pasado, teníamos que crear artefactos como Java JAR / WAR, Python Eggs o ejecutables para su posterior lanzamiento en los servidores. Sin embargo, para que funcionen, tuvieron que hacer un trabajo adicional: instalar el tiempo de ejecución (Java / Python), colocar los archivos necesarios en los lugares correctos, garantizar la compatibilidad con una versión específica del sistema operativo, etc. En otras palabras, tenía que prestar mucha atención a la gestión de la configuración (que a menudo causaba conflictos entre los desarrolladores y los administradores del sistema).

Los contenedores lo han cambiado todo.Ahora la imagen del contenedor actúa como un artefacto. Se puede representar como un tipo de archivo ejecutable extendido que contiene no solo un programa, sino también un tiempo de ejecución completo (Java / Python / ...), así como los archivos / paquetes necesarios que están preinstalados y listos para ejecutarse. Los contenedores se pueden implementar y ejecutar en varios servidores sin ningún paso adicional.

Además, los contenedores funcionan en su propio entorno de caja de arena. Tienen su propio adaptador de red virtual, su propio sistema de archivos con acceso limitado, su propia jerarquía de procesos, sus propias restricciones en la CPU y la memoria, etc. Todo esto se realiza gracias a un subsistema especial del kernel de Linux: espacios de nombres (espacio de nombres).

Kubernetes


Como se dijo anteriormente, Kubernetes es una orquesta de contenedores. Funciona de la siguiente manera: le proporciona un grupo de máquinas y luego dice: "Hola Kubernetes, inicie diez instancias de mi contenedor con 2 procesadores y 3 GB de memoria cada uno, ¡y manténgalos operativos!" Kubernetes se encarga del resto. Encontrará capacidades gratuitas, lanzará contenedores y los reiniciará si es necesario, implementará una actualización al cambiar las versiones, etc. De hecho, Kubernetes le permite abstraer del componente de hardware y hace que toda la variedad de sistemas sea adecuada para la implementación y operación de aplicaciones.


Kubernetes desde el punto de vista de un simple laico

Que son solicitud y límite en Kubernetes


Bien, descubrimos los contenedores y Kubernetes. También sabemos que varios contenedores pueden estar en la misma máquina.

Puedes hacer una analogía con un apartamento comunitario. Se toma una habitación espaciosa (automóviles / nodos) y se alquila a varios inquilinos (contenedores). Kubernetes actúa como agente inmobiliario. Surge la pregunta, ¿cómo evitar que los inquilinos entren en conflicto? ¿Qué pasa si uno de ellos, por ejemplo, decide ocupar el baño durante medio día?

Aquí es donde entran en juego la solicitud y el límite. La solicitud de CPU es solo para fines de planificación. Esto es algo así como la "lista de deseos" de un contenedor, y se utiliza para seleccionar el nodo más adecuado. Al mismo tiempo, el límite de CPU se puede comparar con un arrendamiento, tan pronto como recojamos un nodo para el contenedor, esono podrá ir más allá de los límites establecidos. Y aquí surge un problema ...

Cómo se implementan las solicitudes y los límites en Kubernetes


Kubernetes usa el mecanismo de limitación del núcleo (reloj de omisión) para implementar los límites de la CPU. Si la aplicación excede el límite, se habilita la limitación (es decir, recibe menos ciclos de CPU). Las solicitudes y los límites para la memoria se organizan de manera diferente, por lo que son más fáciles de detectar. Para hacer esto, es suficiente verificar el último estado de reinicio del pod: si es "OOMKilled". Con la aceleración de la CPU, todo no es tan simple, ya que K8 solo pone a disposición métricas para su uso, y no para cgroups.

Solicitud de CPU



Cómo se implementa la solicitud de CPU

Para simplificar, veamos un proceso utilizando un ejemplo de una máquina con una CPU de 4 núcleos.

K8s utiliza el mecanismo de cgroups para controlar la asignación de recursos (memoria y procesador). Un modelo jerárquico está disponible para él: un descendiente hereda los límites del grupo principal. Los detalles de distribución se almacenan en el sistema de archivos virtual ( /sys/fs/cgroup). En el caso del procesador, esto /sys/fs/cgroup/cpu,cpuacct/*.

K8s utiliza el archivo cpu.sharepara asignar recursos del procesador. En nuestro caso, el grupo de control raíz recibe 4096 recursos compartidos de CPU: 100% de la potencia del procesador disponible (1 núcleo = 1024; este es un valor fijo). El grupo raíz distribuye los recursos proporcionalmente en función de los porcentajes de descendientes prescritos encpu.share, y aquellos, a su vez, hacen lo mismo con sus descendientes, etc. Típicamente Kubernetes raíz del grupo de control de nodo tiene tres niño: system.slice, user.slicey kubepods. Los primeros dos subgrupos se utilizan para distribuir recursos entre cargas críticas del sistema y programas de usuario fuera de K8. El último - - kubepodses creado por Kubernetes para distribuir recursos entre pods.

El diagrama anterior muestra que el primer y segundo subgrupos recibieron 1024 recursos compartidos, con 4096 recursos compartidos asignados al subgrupo kuberpod . Cómo es esto posible: después de todo, el grupo raíz solo tiene 4096 acciones disponibles, y la suma de las acciones de sus descendientes supera significativamente este número ( 6144)? El hecho es que el valor tiene sentido lógico, por lo que el Programador de Linux (CFS) lo usa para asignar proporcionalmente los recursos de la CPU. En nuestro caso, los dos primeros grupos reciben 680 acciones reales (16.6% de 4096), y Kubepod recibe las 2736 acciones restantes . En caso de tiempo de inactividad, los dos primeros grupos no utilizarán los recursos asignados.

Afortunadamente, el planificador tiene un mecanismo para evitar la pérdida de recursos de CPU no utilizados. Transfiere capacidades "inactivas" al grupo global, desde el cual se distribuyen entre los grupos que necesitan capacidades de procesador adicionales (la transferencia se realiza en lotes para evitar pérdidas de redondeo). Un método similar se aplica a todos los descendientes de descendientes.

Este mecanismo garantiza una distribución equitativa de la potencia del procesador y garantiza que ningún proceso "robe" recursos de otros.

Límite de CPU


A pesar del hecho de que las configuraciones de límites y solicitudes en K8 parecen similares, su implementación es fundamentalmente diferente: esta es la parte más engañosa y menos documentada.

K8s utiliza el mecanismo de cuotas CFS para implementar límites. Su configuración se especifica en los archivos cfs_period_usy cfs_quota_usen el directorio cgroup (el archivo también se encuentra allí cpu.share).

Por el contrario cpu.share, la cuota se basa en un período de tiempo y no en la potencia del procesador disponible. cfs_period_usestablece la duración del período (era): siempre es de 100.000 μs (100 ms). K8s tiene la capacidad de cambiar este valor, pero actualmente solo está disponible en la versión alfa. El planificador usa la era para reiniciar las cuotas usadas. Segundo archivocfs_quota_us, establece el tiempo disponible (cuota) en cada era. Tenga en cuenta que también se indica en microsegundos. La cuota puede exceder la duración de la era; en otras palabras, puede ser más de 100 ms.

Veamos dos escenarios en máquinas de 16 núcleos (el tipo más común de computadoras que tenemos en Omio):


Escenarios de escenario 1: 2 y un límite de 200 ms. Sin estrangulamiento


Escenario 2: 10 flujos y un límite de 200 ms. La aceleración comienza después de 20 ms, el acceso a los recursos del procesador se reanuda después de otros 80 ms.

Suponga que establece el límite de la CPU en 2 núcleos; Kubernetes traducirá este valor a 200 ms. Esto significa que el contenedor puede usar un máximo de 200 ms de tiempo de CPU sin limitación.

Y aquí comienza la diversión. Como se mencionó anteriormente, la cuota disponible es de 200 ms. Si tiene diez subprocesos ejecutándose en paralelo en una máquina de 12 núcleos (consulte la ilustración para el escenario 2), mientras todos los demás pods están inactivos, la cuota se agotará en solo 20 ms (ya que 10 * 20 ms = 200 ms) y todos los subprocesos de este pod es el acelerador durante los próximos 80 ms. El error del planificador ya mencionado agrava la situación , debido a que se produce una aceleración excesiva y el contenedor ni siquiera puede resolver la cuota existente.

¿Cómo evaluar la aceleración en las vainas?


Solo ve al pod y corre cat /sys/fs/cgroup/cpu/cpu.stat.

  • nr_periods - el número total de períodos del planificador;
  • nr_throttled- el número de períodos estrangulados en la composición nr_periods;
  • throttled_time - tiempo de aceleración acumulativo en nanosegundos.



¿Qué pasa en realidad?


Como resultado, obtenemos una alta aceleración en todas las aplicaciones. ¡A veces es una vez y media más fuerte que la calculada!

Esto lleva a varios errores: la disponibilidad verifica fallas, bloqueos de contenedores, interrupciones de conexión de red, tiempos de espera en llamadas de servicio. En última instancia, esto se traduce en una mayor latencia y mayores errores.

Decisión y consecuencias


Todo es simple aquí. Abandonamos los límites de la CPU y comenzamos a actualizar el núcleo del sistema operativo en los clústeres a la última versión, en la que se solucionó el error. El número de errores (HTTP 5xx) en nuestros servicios se redujo significativamente de inmediato:

Errores HTTP 5xx



Errores HTTP 5xx de un servicio crítico

Tiempo de respuesta P95



Retardo de solicitud de servicio crítico, percentil 95

Costos de operacion



Número de horas dedicadas

¿Cuál es el truco?


Como se indicó al comienzo del artículo:

Puedes hacer una analogía con un apartamento comunitario ... Kubernetes actúa como agente inmobiliario. Pero, ¿cómo evitar que los inquilinos entren en conflicto? ¿Qué pasa si uno de ellos, por ejemplo, decide ocupar el baño durante medio día?

Esa es la trampa. Un contenedor negligente puede absorber todos los recursos de procesador disponibles en la máquina. Si tiene una pila de aplicaciones inteligente (por ejemplo, JVM, Go, Node VM están configuradas correctamente), entonces esto no es un problema: puede trabajar en tales condiciones durante mucho tiempo. Pero si las aplicaciones están mal optimizadas o no están optimizadas ( FROM java:latest), la situación puede salirse de control. En Omio, tenemos Dockerfiles básicos automatizados con la configuración predeterminada adecuada para la pila de idiomas principales, por lo que no hubo tal problema.

Recomendamos que supervise las métricas de USO (uso, saturación y errores), retrasos de API y tasas de error. Asegúrese de que los resultados sean los esperados.

Referencias


Esa es nuestra historia. Los siguientes materiales han ayudado enormemente a comprender lo que está sucediendo:


Informe de errores de Kubernetes:


¿Ha encontrado problemas similares en su práctica o tiene experiencia con la aceleración en entornos de producción en contenedores? ¡Comparte tu historia en los comentarios!

PD del traductor


Lea también en nuestro blog:


All Articles