Le traduzioni sono generate tramite traduzione automatica. In caso di conflitto tra il contenuto di una traduzione e la versione originale in Inglese, quest'ultima prevarrà.
Efficienza dei nodi e dei carichi di lavoro
Essere efficienti con i nostri carichi di lavoro e nodi riduce al complexity/cost contempo le prestazioni e la scalabilità. Ci sono molti fattori da considerare quando si pianifica questa efficienza, ed è più facile pensare in termini di compromessi rispetto a un'impostazione basata sulle best practice per ogni funzionalità. Esploriamo questi compromessi in modo approfondito nella sezione seguente.
Selezione del nodo
L'uso di nodi di dimensioni leggermente più grandi (4-12 volte più grandi) aumenta lo spazio disponibile per far funzionare i pod, poiché riduce la percentuale del nodo utilizzata per «sovraccarichi», ad esempio DaemonSets
Nota
Poiché k8s è scalabile orizzontalmente come regola generale, per la maggior parte delle applicazioni non ha senso prendere in considerazione l'impatto delle dimensioni dei nodi NUMA sulle prestazioni, da cui la raccomandazione di un intervallo inferiore a tale dimensione del nodo.

Le grandi dimensioni dei nodi ci consentono di avere una percentuale più elevata di spazio utilizzabile per nodo. Tuttavia, questo modello può essere portato all'estremo comprimendo il nodo con così tanti pod da causare errori o saturare il nodo. Il monitoraggio della saturazione dei nodi è fondamentale per utilizzare con successo nodi di dimensioni maggiori.
La selezione dei nodi è raramente una one-size-fits-all proposta. Spesso è meglio suddividere i carichi di lavoro con tassi di abbandono notevolmente diversi in diversi gruppi di nodi. I carichi di lavoro in batch di piccole dimensioni con un tasso di abbandono elevato sarebbero più adatti alla famiglia di istanze 4xlarge, mentre un'applicazione su larga scala come Kafka, che richiede 8 vCPU e ha un tasso di abbandono basso, sarebbe meglio gestita dalla famiglia 12xlarge.

Nota
Un altro fattore da considerare con nodi di dimensioni molto grandi è dato dal fatto che i CGROUPS non nascondono il numero totale di vCPU all'applicazione containerizzata. I runtime dinamici possono spesso generare un numero involontario di thread del sistema operativo, creando una latenza difficile da risolvere. Per queste applicazioni si consiglia il pinning della CPU.
Node Bin-packing
Kubernetes e regole Linux
Esistono due serie di regole a cui dobbiamo prestare attenzione quando gestiamo carichi di lavoro su Kubernetes. Le regole di Kubernetes Scheduler, che utilizza il valore di richiesta per pianificare i pod su un nodo, e poi cosa succede dopo la pianificazione del pod, che è l'ambito di Linux, non di Kubernetes.
Al termine dello scheduler Kubernetes, subentra un nuovo set di regole, Linux Completely Fair Scheduler (CFS). Il punto fondamentale è che Linux CFS non ha il concetto di core. Spiegheremo perché pensare in base ai core può portare a gravi problemi di ottimizzazione dei carichi di lavoro in termini di scalabilità.
Pensare in base ai nuclei
La confusione inizia perché lo scheduler Kubernetes ha il concetto di core. Dal punto di vista dello scheduler Kubernetes, se considerassimo un nodo con 4 pod NGINX, ciascuno con la richiesta di un set principale, il nodo avrebbe questo aspetto.

Tuttavia, facciamo un esperimento mentale su come questo aspetto sia diverso dal punto di vista di Linux CFS. La cosa più importante da ricordare quando si utilizza il sistema Linux CFS è che i container occupati (CGROUPS) sono gli unici contenitori che contano ai fini del sistema di condivisione. In questo caso, solo il primo contenitore è occupato, quindi è consentito utilizzare tutti e 4 i core del nodo.

Perché questo è importante? Supponiamo di aver eseguito i nostri test delle prestazioni in un cluster di sviluppo in cui un'applicazione NGINX era l'unico contenitore occupato su quel nodo. Quando spostiamo l'app in produzione, accade quanto segue: l'applicazione NGINX richiede 4 vCPU di risorse, tuttavia, poiché tutti gli altri pod sul nodo sono occupati, le prestazioni della nostra app sono limitate.

Questa situazione ci porterebbe ad aggiungere inutilmente altri contenitori perché non consentivamo alle nostre applicazioni di scalare fino al loro «`punto dolce"`. Esploriamo questo importante concetto di a "sweet spot"
in modo un po' più dettagliato.
Dimensionamento corretto dell'applicazione
Ogni applicazione ha un certo punto in cui non può più ricevere traffico. Superare questo limite può aumentare i tempi di elaborazione e persino ridurre il traffico se viene spinto ben oltre questo punto. Questo è noto come punto di saturazione dell'applicazione. Per evitare problemi di scalabilità, dovremmo tentare di scalare l'applicazione prima che raggiunga il punto di saturazione. Chiamiamo questo punto il punto debole.

Dobbiamo testare ciascuna delle nostre applicazioni per comprenderne il punto debole. Non ci sarà una guida universale in questo caso poiché ogni applicazione è diversa. Durante questo test stiamo cercando di capire la metrica migliore che mostri il punto di saturazione delle nostre applicazioni. Spesso, le metriche di utilizzo vengono utilizzate per indicare che un'applicazione è satura, ma ciò può portare rapidamente a problemi di scalabilità (esploreremo questo argomento in dettaglio in una sezione successiva). Una volta raggiunto questo «`punto dolce"`, possiamo utilizzarlo per scalare in modo efficiente i nostri carichi di lavoro.
Al contrario, cosa succederebbe se aumentassimo ben prima del punto debole e creassimo pod non necessari? Esploriamolo nella prossima sezione.
Espansione dei baccelli
Per vedere come la creazione di pod inutili possa rapidamente sfuggire di mano, diamo un'occhiata al primo esempio a sinistra. La scala verticale corretta di questo contenitore richiede circa due volte di utilizzo quando si gestiscono 100 richieste al secondo. CPUs Tuttavia, se dovessimo sottodimensionare il valore delle richieste impostando le richieste su mezzo core, ora avremmo bisogno di 4 pod per ogni pod di cui abbiamo effettivamente bisogno. Ad aggravare ulteriormente il problema, se il nostro HPA

Aumentando questo problema possiamo vedere rapidamente come questo possa sfuggire di mano. Un'implementazione di dieci pod il cui punto ottimale è stato impostato in modo errato potrebbe rapidamente arrivare a 80 pod e all'infrastruttura aggiuntiva necessaria per gestirli.

Ora che comprendiamo l'impatto di non consentire alle applicazioni di funzionare nel loro punto ideale, torniamo al livello dei nodi e chiediamoci perché questa differenza tra lo scheduler Kubernetes e Linux CFS è così importante?
In caso di scalabilità verso l'alto e verso il basso con HPA, possiamo avere uno scenario in cui abbiamo molto spazio per allocare più pod. Questa sarebbe una decisione sbagliata perché il nodo raffigurato a sinistra utilizza già la CPU al 100%. In uno scenario irrealistico ma teoricamente possibile, potremmo avere l'altro estremo in cui il nostro nodo è completamente pieno, ma l'utilizzo della CPU è pari a zero.

Impostazione delle richieste
Si sarebbe tentati di impostare la richiesta sul valore «ottimale» per quell'applicazione, tuttavia ciò causerebbe inefficienze, come illustrato nel diagramma seguente. Qui abbiamo impostato il valore di richiesta su 2 vCPU, tuttavia l'utilizzo medio di questi pod richiede solo 1 CPU per la maggior parte del tempo. Questa impostazione ci farebbe sprecare il 50% dei cicli della CPU, il che sarebbe inaccettabile.

Questo ci porta alla risposta complessa al problema. L'utilizzo dei container non può essere concepito in modo isolato; è necessario tenere conto delle altre applicazioni in esecuzione sul nodo. Nell'esempio seguente, i contenitori di natura esplosiva vengono mescolati con due contenitori a basso utilizzo della CPU che potrebbero avere limiti di memoria. In questo modo consentiamo ai container di raggiungere il loro punto di forza senza gravare sul nodo.

Il concetto importante da trarre da tutto ciò è che l'utilizzo del concetto di core di pianificazione Kubernetes per comprendere le prestazioni dei container Linux può portare a decisioni sbagliate in quanto non sono correlati.
Nota
Linux CFS ha i suoi punti di forza. Questo è particolarmente vero per i carichi di lavoro I/O basati. Tuttavia, se l'applicazione utilizza core completi senza sidecar e non ha I/O requisiti, il pinning della CPU può eliminare una notevole complessità da questo processo ed è consigliato con questi avvertimenti.
Utilizzo vs. saturazione
Un errore comune nella scalabilità delle applicazioni consiste nell'utilizzare solo l'utilizzo della CPU per la metrica di scalabilità. Nelle applicazioni complesse questo è quasi sempre un indicatore insufficiente del fatto che un'applicazione sia effettivamente satura di richieste. Nell'esempio a sinistra, vediamo che tutte le nostre richieste arrivano effettivamente al server web, quindi l'utilizzo della CPU segue bene la saturazione.
Nelle applicazioni del mondo reale, è probabile che alcune di queste richieste vengano gestite da un livello di database o da un livello di autenticazione, ecc. In questo caso più comune, notate che la CPU non esegue il tracciamento con saturazione poiché la richiesta viene gestita da altre entità. In questo caso la CPU è un indicatore di saturazione molto scarso.

L'utilizzo di una metrica errata nelle prestazioni delle applicazioni è il motivo principale per cui il ridimensionamento non necessario e imprevedibile in Kubernetes. È necessario prestare molta attenzione nella scelta della metrica di saturazione corretta per il tipo di applicazione che si sta utilizzando. È importante notare che non è possibile fornire una raccomandazione valida per tutti. A seconda della lingua utilizzata e del tipo di applicazione in questione, esiste una serie diversificata di metriche per la saturazione.
Potremmo pensare che questo problema riguardi solo l'utilizzo della CPU, tuttavia anche altre metriche comuni come la richiesta al secondo possono rientrare nello stesso identico problema discusso in precedenza. Nota che la richiesta può anche essere indirizzata a livelli DB, livelli di autenticazione, non essendo gestita direttamente dal nostro server web, quindi è una metrica scadente per la vera saturazione del server web stesso.

Sfortunatamente non ci sono risposte facili quando si tratta di scegliere la giusta metrica di saturazione. Ecco alcune linee guida da tenere in considerazione:
-
Comprendete il tempo di esecuzione del linguaggio: le lingue con più thread del sistema operativo reagiranno in modo diverso rispetto alle applicazioni a thread singolo, influendo quindi in modo diverso sul nodo.
-
Comprendi la scala verticale corretta: quanto buffer vuoi inserire nella scala verticale delle tue applicazioni prima di scalare un nuovo pod?
-
Quali metriche riflettono realmente la saturazione della tua applicazione - La metrica di saturazione per un Kafka Producer sarebbe molto diversa da quella di un'applicazione web complessa.
-
In che modo tutte le altre applicazioni sul nodo si influenzano a vicenda? Le prestazioni delle applicazioni non avvengono in modo isolato, gli altri carichi di lavoro sul nodo hanno un impatto importante.
Per chiudere questa sezione, sarebbe facile considerare quanto sopra riportato come eccessivamente complesso e non necessario. Spesso può succedere che si verifichi un problema, ma non siamo consapevoli della vera natura del problema perché stiamo esaminando le metriche sbagliate. Nella prossima sezione vedremo come ciò potrebbe accadere.
Saturazione dei nodi
Ora che abbiamo esplorato la saturazione delle applicazioni, esaminiamo lo stesso concetto dal punto di vista dei nodi. Prendiamone due CPUs utilizzati al 100% per vedere la differenza tra utilizzo e saturazione.
La vCPU a sinistra è utilizzata al 100%, tuttavia non ci sono altre attività in attesa di essere eseguite su questa vCPU, quindi in senso puramente teorico, è abbastanza efficiente. Nel frattempo, nel secondo esempio abbiamo 20 applicazioni a thread singolo in attesa di essere elaborate da una vCPU. Tutte le 20 applicazioni ora sperimenteranno un certo tipo di latenza mentre attendono il loro turno per essere elaborate dalla vCPU. In altre parole, la vCPU sulla destra è satura.
Non solo non vedremmo questo problema se ci limitassimo a guardare all'utilizzo, ma potremmo attribuire questa latenza a qualcosa di non correlato, come la rete, che ci condurrebbe sulla strada sbagliata.

È importante visualizzare le metriche di saturazione, non solo le metriche di utilizzo, quando si aumenta il numero totale di pod in esecuzione su un nodo in un dato momento, poiché è facile non notare che un nodo è troppo saturo. Per questo compito possiamo utilizzare le metriche relative alle informazioni sullo stallo della pressione, come illustrato nella tabella seguente.
ProMQL - I/O bloccato
topk(3, ((irate(node_pressure_io_stalled_seconds_total[1m])) * 100))

Con queste metriche possiamo capire se i thread sono in attesa sulla CPU o anche se tutti i thread sulla scatola sono bloccati in attesa di risorse come la memoria o I/O. For example, we could see what percentage every thread on the instance was stalled waiting on I/O per un periodo di 1 minuto.
topk(3, ((irate(node_pressure_io_stalled_seconds_total[1m])) * 100))
Usando questa metrica, possiamo vedere nel grafico qui sopra che ogni thread sulla console è rimasto bloccato il 45% del tempo di attesa I/O al picco massimo, il che significa che in quel minuto stavamo buttando via tutti i cicli della CPU. Capire che ciò sta accadendo può aiutarci a recuperare una notevole quantità di tempo vCPU, rendendo così la scalabilità più efficiente.
HPA V2
Si consiglia di utilizzare la versione autoscaling/v2 dell'API HPA. Le versioni precedenti dell'API HPA potrebbero bloccarsi nella scalabilità in alcuni casi limite. Inoltre, i pod si limitavano a raddoppiare solo durante ogni fase di scalabilità, il che creava problemi per le piccole implementazioni che dovevano scalare rapidamente.
AutoScaling/v2 ci consente una maggiore flessibilità nell'includere più criteri di scalabilità e ci consente una grande flessibilità nell'utilizzo di metriche personalizzate ed esterne (metriche diverse da K8s).
Ad esempio, possiamo scalare in base al più alto dei tre valori (vedi sotto). Scaliamo se l'utilizzo medio di tutti i pod è superiore al 50%, se i parametri personalizzati indicano che i pacchetti al secondo di ingresso superano la media di 1.000 o l'oggetto in ingresso supera le 10.000 richieste al secondo.
Nota
Questo è solo per dimostrare la flessibilità dell'API di auto-scaling, che consigliamo di evitare regole eccessivamente complesse che possono essere difficili da risolvere in produzione.
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: php-apache spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: php-apache minReplicas: 1 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 50 - type: Pods pods: metric: name: packets-per-second target: type: AverageValue averageValue: 1k - type: Object object: metric: name: requests-per-second describedObject: apiVersion: networking.k8s.io/v1 kind: Ingress name: main-route target: type: Value value: 10k
Tuttavia, abbiamo appreso il pericolo dell'utilizzo di tali metriche per applicazioni web complesse. In questo caso sarebbe meglio utilizzare metriche personalizzate o esterne che riflettano accuratamente la saturazione della nostra applicazione rispetto all'utilizzo. HPAv2 lo consente grazie alla possibilità di scalare in base a qualsiasi metrica, tuttavia dobbiamo comunque trovare ed esportare quella metrica su Kubernetes per utilizzarla.
Ad esempio, possiamo esaminare il numero di thread attivi in Apache. Questo spesso crea un profilo di scalabilità «più fluido» (maggiori informazioni su questo termine presto). Se un thread è attivo, non importa se quel thread è in attesa su un livello di database o sta gestendo una richiesta localmente, se vengono utilizzati tutti i thread dell'applicazione, è un'ottima indicazione che l'applicazione è satura.
Possiamo usare questo esaurimento dei thread come segnale per creare un nuovo pod con un pool di thread completamente disponibile. Questo ci dà anche il controllo sulla dimensione del buffer che vogliamo assorbire nell'applicazione durante i periodi di traffico intenso. Ad esempio, se avessimo un pool di thread totale di 10, il ridimensionamento a 4 thread utilizzati rispetto agli 8 thread utilizzati avrebbe un impatto notevole sul buffer disponibile per il ridimensionamento dell'applicazione. Un'impostazione di 4 sarebbe utile per un'applicazione che deve scalare rapidamente in presenza di carichi pesanti, mentre un'impostazione di 8 sarebbe più efficiente con le nostre risorse se avessimo molto tempo per scalare a causa del numero di richieste che aumenta lentamente anziché bruscamente nel tempo.

Cosa intendiamo con il termine «fluido» quando si parla di scalabilità? Notate il grafico seguente in cui utilizziamo la CPU come metrica. I pod utilizzati in questa implementazione aumenteranno in breve tempo, passando da 50 a 250 pod, per poi ridurli immediatamente di nuovo. Si tratta di un fenomeno altamente inefficiente: la scalabilità è la causa principale del tasso di abbandono dei cluster.

Notate come, dopo aver scelto una metrica che rifletta lo sweet spot corretto della nostra applicazione (la parte centrale del grafico), siamo in grado di scalare senza problemi. La nostra scalabilità è ora efficiente e i nostri pod possono scalare completamente con lo spazio di crescita che abbiamo fornito modificando le impostazioni delle richieste. Ora un gruppo più ristretto di pod sta facendo il lavoro che centinaia di pod facevano prima. I dati del mondo reale mostrano che questo è il fattore numero uno nella scalabilità dei cluster Kubernetes.

Il punto fondamentale è che l'utilizzo della CPU è solo una dimensione delle prestazioni delle applicazioni e dei nodi. L'utilizzo della CPU come unico indicatore dello stato di salute dei nostri nodi e delle nostre applicazioni crea problemi di scalabilità, prestazioni e costi, tutti concetti strettamente collegati. Più l'applicazione e i nodi sono performanti, meno è necessario scalare, il che a sua volta riduce i costi.
L'individuazione e l'utilizzo delle metriche di saturazione corrette per la scalabilità di una particolare applicazione consente inoltre di monitorare e segnalare eventuali problemi di tale applicazione. Se questo passaggio fondamentale viene saltato, le segnalazioni di problemi di prestazioni saranno difficili, se non impossibili, da comprendere.
Impostazione dei limiti della CPU
Per completare questa sezione su argomenti fraintesi, tratteremo i limiti della CPU. In breve, i limiti sono i metadati associati al contenitore che ha un contatore che si resetta ogni 100 ms. Questo aiuta Linux a tenere traccia di quante risorse CPU vengono utilizzate a livello di nodo da un contenitore specifico in un periodo di tempo di 100 ms.

Un errore comune nell'impostazione dei limiti consiste nel presupporre che l'applicazione sia a thread singolo e che sia in esecuzione solo sulla sua vCPU «`assigned"`. Nella sezione precedente abbiamo appreso che CFS non assegna core, e in realtà un container che esegue pool di thread di grandi dimensioni eseguirà la pianificazione su tutte le vCPU disponibili sulla confezione.
Se 64 thread del sistema operativo sono in esecuzione su 64 core disponibili (dal punto di vista di un nodo Linux), sommando il tempo di esecuzione su tutti i 64 core, il totale del tempo impiegato dalla CPU in un periodo di 100 ms sarà abbastanza grande. Poiché ciò può verificarsi solo durante un processo di raccolta dei rifiuti, può essere abbastanza facile non accorgersi di qualcosa del genere. Questo è il motivo per cui è necessario utilizzare metriche per garantire l'utilizzo corretto nel tempo prima di tentare di impostare un limite.
Fortunatamente, abbiamo un modo per vedere esattamente quanta vCPU viene utilizzata da tutti i thread di un'applicazione. Useremo la metrica container_cpu_usage_seconds_total
per questo scopo.
Poiché la logica di throttling si verifica ogni 100 ms e questa metrica è una metrica al secondo, utilizzeremo ProMQL in modo che corrisponda a questo periodo di 100 ms. Se desideri approfondire questo lavoro sull'istruzione PromQL, consulta il seguente blog.
Interrogazione PromQL:
topk(3, max by (pod, container)(rate(container_cpu_usage_seconds_total{image!="", instance="$instance"}[$__rate_interval]))) / 10

Una volta che sentiamo di avere il giusto valore, possiamo porre il limite alla produzione. Diventa quindi necessario verificare se la nostra applicazione viene limitata a causa di qualcosa di imprevisto. Possiamo farlo guardando container_cpu_throttled_seconds_total
topk(3, max by (pod, container)(rate(container_cpu_cfs_throttled_seconds_total{image!=``""``, instance=``"$instance"``}[$__rate_interval]))) / 10

Memoria
L'allocazione della memoria è un altro esempio in cui è facile confondere il comportamento di pianificazione di Kubernetes con il comportamento di Linux. CGroup Si tratta di un argomento più articolato, in quanto sono state apportate importanti modifiche al modo in cui la versione CGroup 2 gestisce la memoria in Linux e Kubernetes ha modificato la sua sintassi in base a ciò; leggi questo blog per ulteriori dettagli.
A differenza delle richieste della CPU, le richieste di memoria non vengono utilizzate dopo il completamento del processo di pianificazione. Questo perché non possiamo comprimere la memoria nella CGroup v1 nello stesso modo in cui possiamo comprimere la CPU. Questo ci lascia solo dei limiti di memoria, progettati per fungere da protezione contro le perdite di memoria chiudendo completamente il pod. Si tratta di una proposta in stile «tutto o niente», tuttavia ora ci sono stati dati nuovi modi per affrontare questo problema.
Innanzitutto, è importante capire che impostare la giusta quantità di memoria per i contenitori non è così semplice come sembra. Il file system di Linux utilizzerà la memoria come cache per migliorare le prestazioni. Questa cache crescerà nel tempo e può essere difficile sapere quanta memoria sia utile avere per la cache, ma può essere recuperata senza un impatto significativo sulle prestazioni delle applicazioni. Ciò si traduce spesso in un'interpretazione errata dell'utilizzo della memoria.
La capacità di «comprimere» la memoria era uno dei fattori principali alla base della v2. CGroup Per saperne di più sui motivi per cui la CGroup V2 era necessaria, consulta la presentazione
Fortunatamente, Kubernetes ora ha il concetto di e sotto. memory.min
memory.high
requests.memory
Questo ci dà la possibilità di rilasciare in modo aggressivo questa memoria cache per utilizzarla in altri contenitori. Una volta che il contenitore raggiunge il limite massimo di memoria, il kernel può recuperare in modo aggressivo la memoria del contenitore fino al valore impostato su. memory.min
Ciò ci offre una maggiore flessibilità quando un nodo è sottoposto a pressioni di memoria.
La domanda chiave diventa: a quale valore memory.min
impostare? È qui che entrano in gioco le metriche di stallo della pressione della memoria. Possiamo usare queste metriche per rilevare il «danneggiamento» della memoria a livello di contenitore. Quindi possiamo usare controller come fbtaxmemory.min
memory.min
Riepilogo
Per riassumere la sezione, è facile confondere i seguenti concetti:
-
Utilizzo e saturazione
-
Regole prestazionali di Linux con logica Kubernetes Scheduler
È necessario prestare molta attenzione per tenere separati questi concetti. Prestazioni e scalabilità sono legate a un livello profondo. Un ridimensionamento non necessario crea problemi di prestazioni, che a loro volta creano problemi di scalabilità.