Limiti della CPU e limitazione aggressiva in Kubernetes

Nota. trad.: questa storia illuminante di Omio, un aggregatore di viaggi europeo, accompagna i lettori dalla teoria di base alle affascinanti complessità pratiche della configurazione di Kubernetes. La familiarità con questi casi aiuta non solo ad ampliare i propri orizzonti, ma anche a prevenire problemi non banali.

Limiti della CPU e limitazione aggressiva in Kubernetes

Ti è mai capitato che un'applicazione si bloccasse, smettesse di rispondere ai controlli sanitari e non riuscissi a capire il motivo? Una possibile spiegazione è legata ai limiti di quota delle risorse della CPU. Questo è ciò di cui parleremo in questo articolo.

TL; DR:
Ti consigliamo vivamente di disabilitare i limiti della CPU in Kubernetes (o disabilitare le quote CFS in Kubelet) se stai utilizzando una versione del kernel Linux con un bug delle quote CFS. Nel nucleo c'è serio e ben noto un bug che porta a throttling e ritardi eccessivi
.

A Omio l'intera infrastruttura è gestita da Kubernetes. Tutti i nostri carichi di lavoro stateful e stateless vengono eseguiti esclusivamente su Kubernetes (usiamo Google Kubernetes Engine). Negli ultimi sei mesi abbiamo iniziato a osservare rallentamenti casuali. Le applicazioni si bloccano o smettono di rispondere ai controlli di integrità, perdono la connessione alla rete, ecc. Questo comportamento ci ha lasciato perplessi per molto tempo e alla fine abbiamo deciso di prendere sul serio il problema.

Riepilogo dell'articolo:

  • Qualche parola sui container e su Kubernetes;
  • Come vengono implementate le richieste e i limiti della CPU;
  • Come funziona il limite della CPU in ambienti multi-core;
  • Come monitorare la limitazione della CPU;
  • Soluzione del problema e sfumature.

Qualche parola sui container e Kubernetes

Kubernetes è essenzialmente lo standard moderno nel mondo delle infrastrutture. Il suo compito principale è l'orchestrazione del contenitore.

contenitori

In passato, dovevamo creare artefatti come Java JAR/WAR, Python Egg o eseguibili da eseguire sui server. Tuttavia, per farli funzionare, è stato necessario fare del lavoro aggiuntivo: installare l'ambiente runtime (Java/Python), posizionare i file necessari nei posti giusti, garantire la compatibilità con una versione specifica del sistema operativo, ecc. In altre parole, bisognava prestare molta attenzione alla gestione della configurazione (che spesso era motivo di contesa tra sviluppatori e amministratori di sistema).

I contenitori hanno cambiato tutto. Ora l'artefatto è un'immagine del contenitore. Può essere rappresentato come una sorta di file eseguibile esteso contenente non solo il programma, ma anche un ambiente di esecuzione completo (Java/Python/...), nonché i file/pacchetti necessari, preinstallati e pronti per essere utilizzati. correre. I contenitori possono essere distribuiti ed eseguiti su server diversi senza passaggi aggiuntivi.

Inoltre, i contenitori operano nel proprio ambiente sandbox. Hanno il proprio adattatore di rete virtuale, il proprio file system con accesso limitato, la propria gerarchia di processi, le proprie limitazioni su CPU e memoria, ecc. Tutto ciò è implementato grazie a uno speciale sottosistema del kernel Linux: gli spazi dei nomi.

kubernetes

Come affermato in precedenza, Kubernetes è un orchestratore di contenitori. Funziona in questo modo: gli dai un pool di macchine e poi dici: "Ehi, Kubernetes, lanciamo dieci istanze del mio contenitore con 2 processori e 3 GB di memoria ciascuna, e manteniamole in esecuzione!" Kubernetes si occuperà del resto. Troverà la capacità libera, avvierà i container e li riavvierà se necessario, rilascerà un aggiornamento quando si cambia versione, ecc. In sostanza, Kubernetes consente di astrarre la componente hardware e rende un'ampia varietà di sistemi adatti alla distribuzione e all'esecuzione di applicazioni.

Limiti della CPU e limitazione aggressiva in Kubernetes
Kubernetes dal punto di vista dei non addetti ai lavori

Cosa sono le richieste e i limiti in Kubernetes

Ok, abbiamo trattato container e Kubernetes. Sappiamo anche che più contenitori possono risiedere sulla stessa macchina.

Si può tracciare un'analogia con un appartamento comune. Un locale spazioso (macchine/unità) viene preso e affittato a diversi inquilini (container). Kubernetes funge da agente immobiliare. Sorge la domanda: come evitare che gli inquilini entrino in conflitto tra loro? E se uno di loro, per esempio, decidesse di prendere in prestito il bagno per mezza giornata?

È qui che entrano in gioco richieste e limiti. processore RICHIEDI necessari esclusivamente per scopi di pianificazione. Questa è una sorta di “lista dei desideri” del contenitore e viene utilizzata per selezionare il nodo più adatto. Allo stesso tempo la CPU Limitare può essere paragonato ad un contratto di noleggio: non appena selezioniamo un'unità per il container, il non sarà in grado andare oltre i limiti stabiliti. Ed è qui che sorge il problema...

Come vengono implementate richieste e limiti in Kubernetes

Kubernetes utilizza un meccanismo di limitazione (salto dei cicli di clock) integrato nel kernel per implementare i limiti della CPU. Se un'applicazione supera il limite, viene abilitata la limitazione (ovvero riceve meno cicli della CPU). Le richieste e i limiti della memoria sono organizzati in modo diverso, quindi sono più facili da rilevare. Per fare ciò basta controllare lo stato dell'ultimo riavvio del pod: se è “OOMKilled”. La limitazione della CPU non è così semplice, poiché K8 rende disponibili le metriche solo in base all'utilizzo, non ai cgroup.

Richiesta CPU

Limiti della CPU e limitazione aggressiva in Kubernetes
Come viene implementata la richiesta della CPU

Per semplicità, esaminiamo il processo utilizzando come esempio una macchina con una CPU a 4 core.

K8s utilizza un meccanismo di gruppo di controllo (cgroups) per controllare l'allocazione delle risorse (memoria e processore). Per questo è disponibile un modello gerarchico: il bambino eredita i limiti del gruppo genitore. I dettagli della distribuzione sono archiviati in un file system virtuale (/sys/fs/cgroup). Nel caso di un processore questo è /sys/fs/cgroup/cpu,cpuacct/*.

K8 utilizza file cpu.share per allocare le risorse del processore. Nel nostro caso, il cgroup root ottiene 4096 quote di risorse della CPU - il 100% della potenza del processore disponibile (1 core = 1024; questo è un valore fisso). Il gruppo radice distribuisce le risorse proporzionalmente a seconda delle quote di discendenti registrate cpu.share, e loro, a loro volta, fanno lo stesso con i loro discendenti, ecc. Su un tipico nodo Kubernetes, il cgroup root ha tre figli: system.slice, user.slice и kubepods. I primi due sottogruppi vengono utilizzati per distribuire le risorse tra i carichi critici del sistema e i programmi utente esterni a K8. L'ultimo - kubepods — creato da Kubernetes per distribuire le risorse tra i pod.

Il diagramma sopra mostra che il primo e il secondo sottogruppo hanno ricevuto ciascuno 1024 condivisioni, con il sottogruppo kuberpod allocato 4096 azioni Com'è possibile: dopotutto, il gruppo root ha accesso solo a 4096 azioni e la somma delle azioni dei suoi discendenti supera significativamente questo numero (6144)? Il punto è che il valore ha un senso logico, quindi lo scheduler Linux (CFS) lo utilizza per allocare proporzionalmente le risorse della CPU. Nel nostro caso, i primi due gruppi ricevono 680 azioni reali (16,6% di 4096) e kubepod riceve il resto 2736 azioni In caso di inattività, i primi due gruppi non utilizzeranno le risorse assegnate.

Fortunatamente, lo scheduler dispone di un meccanismo per evitare di sprecare risorse della CPU inutilizzate. Trasferisce la capacità “inattiva” a un pool globale, da cui viene distribuita ai gruppi che necessitano di ulteriore potenza del processore (il trasferimento avviene in batch per evitare perdite di arrotondamento). Un metodo simile viene applicato a tutti i discendenti dei discendenti.

Questo meccanismo garantisce un'equa distribuzione della potenza del processore e garantisce che nessun processo “rubi” risorse ad altri.

Limite CPU

Nonostante le configurazioni dei limiti e delle richieste nei K8 sembrino simili, la loro implementazione è radicalmente diversa: questa più fuorviante e la parte meno documentata.

K8 si impegna Meccanismo delle quote CFS per implementare i limiti. Le loro impostazioni sono specificate nei file cfs_period_us и cfs_quota_us nella directory cgroup (anche il file si trova lì cpu.share).

A differenza di cpu.share, su cui si basa la quota periodo di tempoe non dalla potenza del processore disponibile. cfs_period_us specifica la durata del periodo (epoca): è sempre 100000 μs (100 ms). C'è un'opzione per modificare questo valore in K8s, ma per ora è disponibile solo in alpha. Lo scheduler utilizza l'epoca per riavviare le quote utilizzate. Secondo fascicolo cfs_quota_us, specifica il tempo disponibile (quota) in ciascuna epoca. Tieni presente che è specificato anche in microsecondi. La quota può superare la durata dell'epoca; in altre parole, può essere maggiore di 100 ms.

Diamo un'occhiata a due scenari su macchine a 16 core (il tipo di computer più comune che abbiamo su Omio):

Limiti della CPU e limitazione aggressiva in Kubernetes
Scenario 1: 2 thread e un limite di 200 ms. Nessuna limitazione

Limiti della CPU e limitazione aggressiva in Kubernetes
Scenario 2: 10 thread e limite di 200 ms. La limitazione inizia dopo 20 ms, l'accesso alle risorse del processore riprende dopo altri 80 ms

Supponiamo che tu abbia impostato il limite della CPU su 2 noccioli; Kubernetes tradurrà questo valore in 200 ms. Ciò significa che il contenitore può utilizzare un massimo di 200 ms di tempo CPU senza limitazioni.

Ed è qui che inizia il divertimento. Come accennato in precedenza, la quota disponibile è di 200 ms. Se lavori in parallelo dieci thread su una macchina a 12 core (vedere l'illustrazione per lo scenario 2), mentre tutti gli altri pod sono inattivi, la quota verrà esaurita in soli 20 ms (poiché 10 * 20 ms = 200 ms) e tutti i thread di questo pod si bloccheranno » (acceleratore) per i successivi 80 ms. Il già menzionato bug dello scheduler, a causa del quale si verifica un eccessivo throttling e il contenitore non riesce nemmeno a soddisfare la quota esistente.

Come valutare il throttling nei pod?

Basta accedere al pod ed eseguire cat /sys/fs/cgroup/cpu/cpu.stat.

  • nr_periods — il numero totale di periodi di pianificazione;
  • nr_throttled — numero di periodi limitati nella composizione nr_periods;
  • throttled_time — tempo cumulativo di strozzamento in nanosecondi.

Limiti della CPU e limitazione aggressiva in Kubernetes

Cosa sta succedendo veramente?

Di conseguenza, otteniamo un elevato throttling in tutte le applicazioni. A volte è dentro una volta e mezza più forte di quanto calcolato!

Ciò porta a vari errori: errori di verifica della disponibilità, blocchi del contenitore, interruzioni della connessione di rete, timeout nelle chiamate di servizio. Ciò si traduce in definitiva in una maggiore latenza e tassi di errore più elevati.

Decisione e conseguenze

Tutto è semplice qui. Abbiamo abbandonato i limiti della CPU e abbiamo iniziato ad aggiornare il kernel del sistema operativo in cluster all'ultima versione, in cui il bug è stato corretto. Il numero di errori (HTTP 5xx) nei nostri servizi è immediatamente diminuito in modo significativo:

Errori HTTP 5xx

Limiti della CPU e limitazione aggressiva in Kubernetes
Errori HTTP 5xx per un servizio critico

Tempo di risposta p95

Limiti della CPU e limitazione aggressiva in Kubernetes
Latenza della richiesta di servizio critica, 95° percentile

Costi operativi

Limiti della CPU e limitazione aggressiva in Kubernetes
Numero di ore di istanza trascorse

Qual è il trucco?

Come affermato all’inizio dell’articolo:

Si può tracciare un'analogia con un appartamento comune... Kubernetes funge da agente immobiliare. Ma come evitare che gli inquilini entrino in conflitto tra loro? E se uno di loro, per esempio, decidesse di prendere in prestito il bagno per mezza giornata?

Ecco il problema. Un contenitore trascurato può consumare tutte le risorse CPU disponibili su una macchina. Se disponi di uno stack di applicazioni intelligente (ad esempio, JVM, Go, Node VM sono configurati correttamente), questo non è un problema: puoi lavorare in tali condizioni per molto tempo. Ma se le applicazioni sono scarsamente ottimizzate o non ottimizzate affatto (FROM java:latest), la situazione potrebbe sfuggire al controllo. In Omio disponiamo di Dockerfile di base automatizzati con impostazioni predefinite adeguate per lo stack di lingue principali, quindi questo problema non esisteva.

Consigliamo di monitorare le metriche USO (utilizzo, saturazione ed errori), ritardi API e tassi di errore. Garantire che i risultati soddisfino le aspettative.

riferimenti

Questa è la nostra storia I seguenti materiali hanno aiutato molto a capire cosa stava succedendo:

Segnalazioni di bug Kubernetes:

Hai riscontrato problemi simili nella tua pratica o hai esperienza relativa alla limitazione in ambienti di produzione containerizzati? Condividi la tua storia nei commenti!

PS da traduttore

Leggi anche sul nostro blog:

Fonte: habr.com

Aggiungi un commento