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

Nota. traducir: Esta reveladora historia de Omio, un 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. Familiarizarse con estos casos no sólo ayuda a ampliar sus horizontes, sino también a prevenir problemas no triviales.

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

¿Alguna vez una aplicación se quedó atascada, dejó de responder a los controles de estado y no pudo entender por qué? Una posible explicación está relacionada con los límites de cuota de recursos de CPU. De esto es de lo que hablaremos en este artículo.

TL; DR:
Recomendamos encarecidamente deshabilitar los límites de CPU en Kubernetes (o deshabilitar las cuotas de CFS en Kubelet) si está utilizando una versión del kernel de Linux con un error de cuota de CFS. en el nucleo hay serio y bien conocido un error que provoca limitaciones y retrasos excesivos
.

En Omio toda la infraestructura está gestionada por Kubernetes. Todas nuestras cargas de trabajo con y sin estado se ejecutan exclusivamente en Kubernetes (utilizamos Google Kubernetes Engine). En los últimos seis meses, comenzamos a observar desaceleraciones aleatorias. Las aplicaciones se congelan o dejan de responder a los controles de estado, pierden la conexión a la red, etc. Este comportamiento nos desconcertó durante mucho tiempo y finalmente decidimos tomarnos el problema en serio.

Resumen del artículo:

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

Algunas palabras sobre contenedores y Kubernetes

Kubernetes es esencialmente 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 ejecutar en servidores. Sin embargo, para que funcionaran fue necesario realizar un trabajo adicional: instalar el entorno 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, había que prestar especial atención a la gestión de la configuración (que a menudo era motivo de discordia entre desarrolladores y administradores de sistemas).

Los contenedores lo cambiaron todo. Ahora el artefacto es una imagen contenedora. Se puede representar como una especie de archivo ejecutable extendido que contiene no sólo el programa, sino también un entorno de ejecución completo (Java/Python/...), así como los archivos/paquetes necesarios, preinstalados y listos para correr. Los contenedores se pueden implementar y ejecutar en diferentes servidores sin ningún paso adicional.

Además, los contenedores operan en su propio entorno sandbox. Tienen su propio adaptador de red virtual, su propio sistema de archivos con acceso limitado, su propia jerarquía de procesos, sus propias limitaciones de CPU y memoria, etc. Todo esto se implementa gracias a un subsistema especial del kernel de Linux: los espacios de nombres.

Kubernetes

Como se indicó anteriormente, Kubernetes es un orquestador de contenedores. Funciona así: le das un grupo de máquinas y luego dices: "Oye, Kubernetes, lancemos diez instancias de mi contenedor con 2 procesadores y 3 GB de memoria cada una, ¡y mantengámoslas funcionando!". Kubernetes se encargará del resto. Encontrará capacidad libre, iniciará contenedores y los reiniciará si es necesario, implementará una actualización al cambiar de versión, etc. Básicamente, Kubernetes le permite abstraer el componente de hardware y hace que una amplia variedad de sistemas sean adecuados para implementar y ejecutar aplicaciones.

Límites de CPU y aceleración agresiva en Kubernetes
Kubernetes desde el punto de vista del profano

¿Qué son las solicitudes y los límites en Kubernetes?

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

Se puede establecer una analogía con un apartamento comunal. Se alquila un local espacioso (máquinas/unidades) a varios inquilinos (contenedores). Kubernetes actúa como agente inmobiliario. Surge la pregunta: ¿cómo evitar que los inquilinos entren en conflicto entre sí? ¿Qué pasa si uno de ellos, por ejemplo, decide tomar prestado el baño durante medio día?

Aquí es donde entran en juego las peticiones y los límites. UPC SOLICITUD necesaria únicamente para fines de planificación. Esto es algo así como una "lista de deseos" del contenedor y se utiliza para seleccionar el nodo más adecuado. Al mismo tiempo la CPU Límite se puede comparar con un contrato de alquiler: tan pronto como seleccionamos una unidad para el contenedor, el no podrá ir más allá de los límites establecidos. Y aquí es donde surge el problema...

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

Kubernetes utiliza un mecanismo de aceleración (omitiendo ciclos de reloj) integrado en el kernel para implementar los límites de la CPU. Si una aplicación excede el límite, se habilita la limitación (es decir, recibe menos ciclos de CPU). Las solicitudes y los límites de memoria se organizan de forma diferente, por lo que son más fáciles de detectar. Para hacer esto, simplemente verifique el último estado de reinicio del pod: si es "OOMKilled". La aceleración de la CPU no es tan simple, ya que K8s solo hace que las métricas estén disponibles por uso, no por cgroups.

Solicitud de CPU

Límites de CPU y aceleración agresiva en Kubernetes
Cómo se implementa la solicitud de CPU

Para simplificar, veamos el proceso usando una máquina con una CPU de 4 núcleos como ejemplo.

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

K8s usa archivo cpu.share para asignar recursos del procesador. En nuestro caso, el grupo raíz obtiene 4096 recursos compartidos de la CPU: el 100% de la potencia del procesador disponible (1 núcleo = 1024; este es un valor fijo). El grupo raíz distribuye los recursos proporcionalmente dependiendo de las participaciones de los descendientes registrados en cpu.share, y ellos, a su vez, hacen lo mismo con sus descendientes, etc. En un nodo típico de Kubernetes, el cgroup raíz tiene tres hijos: system.slice, user.slice и kubepods. Los dos primeros subgrupos se utilizan para distribuir recursos entre cargas críticas del sistema y programas de usuario fuera de K8. El último - kubepods — creado por Kubernetes para distribuir recursos entre pods.

El diagrama anterior muestra que el primer y segundo subgrupo recibieron cada uno 1024 acciones, con el subgrupo kuberpod asignado 4096 Comparte ¿Cómo es esto posible? Después de todo, el grupo raíz sólo tiene acceso a 4096 acciones, y la suma de las acciones de sus descendientes excede significativamente este número (6144)? El punto es que el valor tiene sentido lógico, por lo que el programador de Linux (CFS) lo usa para asignar proporcionalmente recursos de CPU. En nuestro caso, los dos primeros grupos reciben 680 acciones reales (16,6% de 4096), y kubepod recibe el resto 2736 Comparte En caso de inactividad, los dos primeros grupos no utilizarán los recursos asignados.

Afortunadamente, el programador tiene un mecanismo para evitar desperdiciar recursos de CPU no utilizados. Transfiere capacidad "inactiva" a un grupo global, desde el cual se distribuye a grupos que necesitan potencia de procesador adicional (la transferencia se produce en lotes para evitar pérdidas por redondeo). Se aplica un método similar a todos los descendientes de descendientes.

Este mecanismo garantiza una distribución justa de la potencia del procesador y garantiza que ningún proceso “robe” recursos a otros.

Límite de CPU

A pesar de que las configuraciones de límites y solicitudes en los K8 parecen similares, su implementación es radicalmente diferente: esto más engañoso y la parte menos documentada.

K8 se activa Mecanismo de cuotas del CSA para implementar límites. Sus configuraciones se especifican en archivos. cfs_period_us и cfs_quota_us en el directorio cgroup (el archivo también se encuentra allí cpu.share).

Desemejante cpu.share, la cuota se basa en periodo de tiempo, y no en la potencia del procesador disponible. cfs_period_us especifica la duración del período (época); siempre es 100000 μs (100 ms). Hay una opción para cambiar este valor en K8, pero por ahora solo está disponible en alfa. El planificador utiliza la época para reiniciar las cuotas utilizadas. Segundo archivo cfs_quota_us, especifica el tiempo disponible (cuota) en cada época. Tenga en cuenta que también se especifica en microsegundos. La cuota puede exceder la duración de la época; en otras palabras, puede ser superior a 100 ms.

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

Límites de CPU y aceleración agresiva en Kubernetes
Escenario 1: 2 subprocesos y un límite de 200 ms. Sin estrangulamiento

Límites de CPU y aceleración agresiva en Kubernetes
Escenario 2: 10 subprocesos y 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

Digamos que estableces el límite de CPU en 2 granos; Kubernetes traducirá este valor a 200 ms. Esto significa que el contenedor puede utilizar un máximo de 200 ms de tiempo de CPU sin limitación.

Y aquí es donde comienza la diversión. Como se mencionó anteriormente, la cuota disponible es de 200 ms. Si estás trabajando en paralelo diez subprocesos en una máquina de 12 núcleos (consulte la ilustración del 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 se colgarán. » (acelerador) durante los siguientes 80 ms. lo ya mencionado error del programador, por lo que se produce una estrangulación excesiva y el contenedor ni siquiera puede cumplir con la cuota existente.

¿Cómo evaluar la aceleración en pods?

Simplemente inicie sesión en el pod y ejecute cat /sys/fs/cgroup/cpu/cpu.stat.

  • nr_periods — el número total de períodos del planificador;
  • nr_throttled — número de períodos limitados en la composición nr_periods;
  • throttled_time — tiempo acelerado acumulado en nanosegundos.

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

¿Qué está pasando realmente?

Como resultado, obtenemos una alta aceleración en todas las aplicaciones. A veces él está en una vez y media ¡Más fuerte de lo calculado!

Esto conduce a varios errores: fallas en la verificación de preparación, congelaciones de contenedores, interrupciones de la conexión de red, tiempos de espera dentro de las llamadas de servicio. En última instancia, esto da como resultado una mayor latencia y mayores tasas de error.

Decisión y consecuencias.

Aquí todo es sencillo. Abandonamos los límites de la CPU y comenzamos a actualizar el kernel del sistema operativo en clústeres a la última versión, en la que se solucionó el error. El número de errores (HTTP 5xx) en nuestros servicios disminuyó significativamente inmediatamente:

Errores HTTP 5xx

Límites de CPU y aceleración agresiva en Kubernetes
Errores HTTP 5xx para un servicio crítico

Tiempo de respuesta p95

Límites de CPU y aceleración agresiva en Kubernetes
Latencia de solicitud de servicio crítico, percentil 95

Costos de operacion

Límites de CPU y aceleración agresiva en Kubernetes
Número de horas de instancia invertidas

¿Cuál es la trampa?

Como se indica al principio del artículo:

Se puede establecer una analogía con un apartamento comunal... 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 tomar prestado el baño durante medio día?

Aquí está el truco. Un contenedor descuidado puede consumir todos los recursos de CPU disponibles en una máquina. Si tiene una pila de aplicaciones inteligente (por ejemplo, JVM, Go, Node VM están configurados 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 en absoluto (FROM java:latest), la situación puede salirse de control. En Omio tenemos Dockerfiles base automatizados con configuraciones predeterminadas adecuadas para la pila de idiomas principal, por lo que este problema no existía.

Recomendamos monitorear las métricas USO (uso, saturación y errores), retrasos de API y tasas de error. Asegurar que los resultados cumplan con las expectativas.

referencias

Esta es nuestra historia. Los siguientes materiales ayudaron mucho a comprender lo que estaba sucediendo:

Informes de errores de Kubernetes:

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

PD del traductor

Lea también en nuestro blog:

Fuente: habr.com

Añadir un comentario