Límites da CPU e limitación agresiva en Kubernetes
Nota. transl.: Esta historia reveladora de Omio, un agregador de viaxes europeo, leva aos lectores desde a teoría básica ata as fascinantes complejidades prácticas da configuración de Kubernetes. A familiaridade con estes casos axuda non só a ampliar os seus horizontes, senón tamén a evitar problemas non triviais.
Algunha vez tivo unha aplicación atascada no seu sitio, deixou de responder aos controis de saúde e non puido descubrir por que? Unha posible explicación está relacionada cos límites da cota de recursos da CPU. Isto é do que falaremos neste artigo.
TL; DR:
Recomendamos encarecidamente desactivar os límites de CPU en Kubernetes (ou desactivar as cotas CFS en Kubelet) se estás a usar unha versión do núcleo de Linux cun erro de cota CFS. No núcleo está dispoñible grave e ben coñecido un erro que leva a estrangulamentos e atrasos excesivos.
En Omio toda a infraestrutura está xestionada por Kubernetes. Todas as nosas cargas de traballo con estado e sen estado execútanse exclusivamente en Kubernetes (utilizamos Google Kubernetes Engine). Nos últimos seis meses, comezamos a observar desaceleracións aleatorias. As aplicacións conxélanse ou deixan de responder aos controis de saúde, perden a conexión á rede, etc. Este comportamento deixounos desconcertado durante moito tempo e, finalmente, decidimos tomar o problema en serio.
Resumo do artigo:
Unhas palabras sobre contedores e Kubernetes;
Como se implementan as solicitudes e os límites da CPU;
Como funciona o límite da CPU en ambientes multinúcleo;
Como rastrexar a limitación da CPU;
Solución do problema e matices.
Algunhas palabras sobre contenedores e Kubernetes
Kubernetes é esencialmente o estándar moderno no mundo das infraestruturas. A súa tarefa principal é a orquestración de contedores.
Contenedores
No pasado, tiñamos que crear artefactos como Java JAR/WAR, Python Eggs ou executables para executalos en servidores. Non obstante, para facelos funcionar houbo que facer un traballo adicional: instalar o entorno de execución (Java/Python), colocar os ficheiros necesarios nos lugares axeitados, garantir a compatibilidade cunha versión concreta do sistema operativo, etc. Noutras palabras, houbo que prestar especial atención á xestión da configuración (que a miúdo era unha fonte de disputa entre desenvolvedores e administradores de sistemas).
Os contedores cambiaron todo. Agora o artefacto é unha imaxe de recipiente. Pódese representar como unha especie de ficheiro executable estendido que contén non só o programa, senón tamén un ambiente de execución completo (Java/Python/...), así como os ficheiros/paquetes necesarios, preinstalados e listos para correr. Os contedores pódense implantar e executar en diferentes servidores sen ningún paso adicionais.
Ademais, os contedores funcionan no seu propio entorno sandbox. Teñen o seu propio adaptador de rede virtual, o seu propio sistema de ficheiros con acceso limitado, a súa propia xerarquía de procesos, as súas propias limitacións de CPU e memoria, etc. Todo isto implícase grazas a un subsistema especial do núcleo de Linux: espazos de nomes.
Kubernetes
Como se dixo anteriormente, Kubernetes é un orquestrador de contedores. Funciona así: dáslle un conxunto de máquinas e despois dis: "Ola, Kubernetes, imos facer dez instancias do meu contedor con 2 procesadores e 3 GB de memoria cada un, e mantemos funcionando!" Kubernetes encargarase do resto. Atopará capacidade libre, lanzará contedores e reinicialos se é necesario, lanzará unha actualización ao cambiar de versión, etc. Esencialmente, Kubernetes permítelle abstraer o compoñente de hardware e fai unha gran variedade de sistemas axeitados para implementar e executar aplicacións.
Kubernetes desde o punto de vista do profano
Cales son as solicitudes e os límites en Kubernetes
Está ben, cubrimos os contedores e Kubernetes. Tamén sabemos que varios recipientes poden residir na mesma máquina.
Pódese facer unha analoxía cun apartamento comunitario. Un amplo local (máquinas/unidades) é tomado e alugado a varios inquilinos (contedores). Kubernetes actúa como corretor de inmobles. Xorde a pregunta, como evitar que os inquilinos poidan conflitos entre eles? E se un deles, por exemplo, decide pedir prestado o baño para a metade do día?
Aquí é onde entran en xogo as peticións e os límites. CPU Solicitude necesarios exclusivamente para propósitos de planificación. Isto é algo así como unha "lista de desexos" do contedor e úsase para seleccionar o nodo máis axeitado. Ao mesmo tempo, a CPU Limitar pódese comparar cun contrato de aluguer - tan pronto como seleccionamos unha unidade para o recipiente, o non se pode superar os límites establecidos. E aquí é onde xorde o problema...
Como se implementan as solicitudes e os límites en Kubernetes
Kubernetes usa un mecanismo de limitación (omitir ciclos de reloxo) integrado no núcleo para implementar os límites da CPU. Se unha aplicación supera o límite, a limitación está habilitada (é dicir, recibe menos ciclos de CPU). As solicitudes e os límites de memoria organízanse de forma diferente, polo que son máis fáciles de detectar. Para iso, só tes que comprobar o estado do último reinicio do pod: se é "OOMKilled". A limitación da CPU non é tan sinxela, xa que K8s só fai que as métricas sexan dispoñibles por uso, non por cgroups.
Solicitude de CPU
Como se implementa a solicitude da CPU
Para simplificar, vexamos o proceso usando unha máquina cunha CPU de 4 núcleos como exemplo.
K8s usa un mecanismo de grupo de control (cgroups) para controlar a asignación de recursos (memoria e procesador). Dispoñible dun modelo xerárquico: o fillo herda os límites do grupo principal. Os detalles da distribución gárdanse nun sistema de ficheiros virtual (/sys/fs/cgroup). No caso dun procesador isto é /sys/fs/cgroup/cpu,cpuacct/*.
K8s usa ficheiro cpu.share para asignar recursos do procesador. No noso caso, o cgroup raíz obtén 4096 partes de recursos da CPU: o 100% da potencia do procesador dispoñible (1 núcleo = 1024; este é un valor fixo). O grupo raíz distribúe os recursos proporcionalmente en función das participacións dos descendentes rexistrados cpu.share, e eles, á súa vez, fan o propio cos seus descendentes, etc. Nun nodo Kubernetes típico, o cgroup raíz ten tres fillos: system.slice, user.slice и kubepods. Os dous primeiros subgrupos utilízanse para distribuír recursos entre as cargas críticas do sistema e os programas de usuario fóra de K8s. Derradeiro - kubepods — creado por Kubernetes para distribuír recursos entre pods.
O diagrama anterior mostra que o primeiro e o segundo subgrupos recibiron cada un 1024 accións, co subgrupo kuberpod asignado 4096 accións Como é posible: despois de todo, o grupo raíz só ten acceso a 4096 accións, e a suma das accións dos seus descendentes supera significativamente este número (6144)? A cuestión é que o valor ten sentido lóxico, polo que o planificador de Linux (CFS) utilízao para asignar proporcionalmente os recursos da CPU. No noso caso, os dous primeiros grupos reciben 680 accións reais (16,6% de 4096), e kubepod recibe o resto 2736 accións En caso de inactividade, os dous primeiros grupos non utilizarán os recursos asignados.
Afortunadamente, o planificador ten un mecanismo para evitar o desperdicio de recursos da CPU non utilizados. Transfire capacidade "inactiva" a un pool global, desde o que se distribúe a grupos que necesitan potencia adicional do procesador (a transferencia prodúcese por lotes para evitar perdas de redondeo). Un método similar aplícase a todos os descendentes de descendentes.
Este mecanismo garante unha distribución xusta da potencia do procesador e asegura que ningún proceso "roube" recursos aos demais.
Límite de CPU
A pesar de que as configuracións de límites e solicitudes en K8 parecen similares, a súa implementación é radicalmente diferente: isto máis enganosa e a parte menos documentada.
K8s engancha Mecanismo de cotas do CFS para implementar límites. A súa configuración especifícase en ficheiros cfs_period_us и cfs_quota_us no directorio cgroup (o ficheiro tamén se atopa alí cpu.share).
Ao contrario cpu.share, a cota baséase período de tempo, e non na potencia do procesador dispoñible. cfs_period_us especifica a duración do período (época) - sempre é 100000 μs (100 ms). Hai unha opción para cambiar este valor en K8s, pero só está dispoñible en alfa polo momento. O planificador usa a época para reiniciar as cotas usadas. Segundo arquivo cfs_quota_us, especifica o tempo dispoñible (cota) en cada época. Teña en conta que tamén se especifica en microsegundos. A cota pode exceder a lonxitude da época; noutras palabras, pode ser superior a 100 ms.
Vexamos dous escenarios en máquinas de 16 núcleos (o tipo de ordenador máis común que temos en Omio):
Escenario 1: 2 fíos e un límite de 200 ms. Sen estrangulamento
Escenario 2: 10 fíos e límite de 200 ms. A limitación comeza despois de 20 ms, o acceso aos recursos do procesador retómase despois doutros 80 ms
Digamos que estableces o límite da CPU 2 núcleos; Kubernetes traducirá este valor a 200 ms. Isto significa que o contedor pode usar un máximo de 200 ms de tempo de CPU sen limitar.
E aquí é onde comeza a diversión. Como se mencionou anteriormente, a cota dispoñible é de 200 ms. Se está a traballar en paralelo dez fíos nunha máquina de 12 núcleos (consulte a ilustración para o escenario 2), mentres que todos os outros pods están inactivos, a cota esgotarase en só 20 ms (xa que 10 * 20 ms = 200 ms) e todos os fíos deste pod colgaranse » (acelerador) durante os próximos 80 ms. O xa mencionado erro do planificador, polo que se produce un estrangulamento excesivo e o contedor nin sequera pode cumprir a cota existente.
Como avaliar o estrangulamento en pods?
Só ten que iniciar sesión no pod e executar cat /sys/fs/cgroup/cpu/cpu.stat.
nr_periods — o número total de períodos do programador;
nr_throttled — número de períodos estrangulados na composición nr_periods;
throttled_time — tempo de estrangulamento acumulado en nanosegundos.
Que está pasando realmente?
Como resultado, obtemos un alto estrangulamento en todas as aplicacións. Ás veces está dentro unha vez e media máis forte do esperado!
Isto leva a varios erros: fallos de verificación de preparación, conxelación do contedor, interrupcións da conexión de rede, tempo de espera nas chamadas de servizo. En definitiva, isto dá lugar a unha maior latencia e taxas de erro máis altas.
Decisión e consecuencias
Aquí todo é sinxelo. Abandonamos os límites da CPU e comezamos a actualizar o núcleo do sistema operativo en clústeres á última versión, na que se solucionou o erro. O número de erros (HTTP 5xx) nos nosos servizos baixou inmediatamente de forma significativa:
Erros HTTP 5xx
Erros HTTP 5xx para un servizo crítico
Tempo de resposta p95
Latencia de solicitude de servizo crítico, percentil 95
Custos de explotación
Número de horas de instancia dedicadas
Cal é a captura?
Como se indica ao comezo do artigo:
Pódese facer unha analoxía cun apartamento comunitario... Kubernetes actúa como corretor de inmobles. Pero como evitar conflitos entre os inquilinos? E se un deles, por exemplo, decide pedir prestado o baño para a metade do día?
Aquí está a captura. Un recipiente descoidado pode consumir todos os recursos de CPU dispoñibles nunha máquina. Se tes unha pila de aplicacións intelixentes (por exemplo, JVM, Go, Node VM están configurados correctamente), isto non é un problema: podes traballar en tales condicións durante moito tempo. Pero se as aplicacións están mal optimizadas ou non están optimizadas en absoluto (FROM java:latest), a situación pode saír de control. En Omio temos Dockerfiles base automatizados cunha configuración predeterminada adecuada para a pila de idiomas principal, polo que este problema non existía.
Recomendamos supervisar as métricas USO (uso, saturación e erros), atrasos da API e taxas de erro. Asegúrese de que os resultados cumpran as expectativas.
referencias
Esta é a nosa historia. Os seguintes materiais axudaron moito a comprender o que estaba a suceder:
Encontrou problemas similares na súa práctica ou ten experiencia relacionada coa limitación en ambientes de produción en contedores? Comparte a túa historia nos comentarios!