Padroneggiare le Richieste e i Limiti delle Risorse Kubernetes per Prestazioni Ottimali
Scopri le differenze cruciali tra Richieste e Limiti delle Risorse Kubernetes per CPU e Memoria. Questa guida spiega come queste impostazioni determinano le classi di Qualità del Servizio (QoS) (Garantita, Burstabile, BestEffort), prevengono l'instabilità dei nodi e ottimizzano l'efficienza dello scheduling del cluster. Include esempi pratici YAML e migliori pratiche per il tuning delle prestazioni.
Padroneggiare le Richieste e i Limiti delle Risorse Kubernetes per Prestazioni Ottimali
Le richieste e i limiti delle risorse Kubernetes sembrano semplici in YAML, ma influenzano quasi ogni comportamento di secondo giorno in un cluster: dove finiscono i pod, quali carichi di lavoro vengono sfrattati per primi, se un'applicazione subisce throttling della CPU sotto carico e quanta capacità di riserva pensi di avere. Un'impostazione sbagliata può far sembrare un'applicazione sana come se fosse rotta. Un'impostazione mancante può far sì che lo scheduler impacchetti i pod su un nodo fino a quando il primo picco di traffico non si trasforma in un incidente di "vicino rumoroso".
La parte che coglie di sorpresa i team è che richieste e limiti vengono utilizzati da sistemi diversi. Le richieste sono principalmente una promessa di scheduling. I limiti sono un confine di enforcement. Trattarli come la stessa cosa porta a risultati strani, specialmente con la CPU.
Comprendere i Concetti Fondamentali: Richieste vs. Limiti
In Kubernetes, un container può definire il consumo di risorse previsto utilizzando resources.requests e resources.limits. Non sono tecnicamente obbligatori a meno che il tuo cluster non utilizzi policy come LimitRange o controlli di ammissione, ma i carichi di lavoro in produzione dovrebbero solitamente definire almeno le richieste per CPU e memoria. Senza richieste, lo scheduler ha poche informazioni utili e il pod cade in una postura di qualità del servizio più debole.
1. Richieste di Risorse (requests)
Le richieste rappresentano la quantità di risorse che un container ha garantito di ricevere al momento dello scheduling. Questa è la quantità minima di risorse che il kube-scheduler utilizza quando decide su quale nodo posizionare un Pod.
- Scheduling: Un nodo deve avere risorse allocabili disponibili sufficienti a soddisfare la somma di tutte le richieste dei Pod prima che un nuovo Pod possa essere schedulato lì.
- Priorità a runtime: Le richieste influenzano le quote di CPU e le decisioni di evizione della memoria. Non sono una prenotazione magica che previene ogni rallentamento, ma forniscono a Kubernetes e al kernel informazioni migliori durante la contesa.
2. Limiti di Risorse (limits)
I limiti definiscono la quantità massima di risorse che un container può consumare. Il superamento di questi limiti comporta comportamenti specifici e definiti per CPU e Memoria.
- Limiti CPU: Se un container tenta di utilizzare più CPU del suo limite, i cgroups del kernel Linux limiteranno (throttling) il suo utilizzo, impedendogli di consumare ulteriori cicli.
- Limiti Memoria: Se un container supera il suo limite di memoria, il kernel può terminare un processo all'interno del container. Kubernetes segnala questo come
OOMKilledquando questa è la ragione di terminazione registrata.
Comportamento CPU vs. Memoria
È fondamentale comprendere la differenza qualitativa nel modo in cui Kubernetes applica i confini di CPU rispetto a quelli di Memoria:
| Risorsa | Comportamento al Superamento del Limite | Meccanismo di Enforcement |
|---|---|---|
| CPU | Limitata (rallentata) | cgroups (controllo banda CPU) |
| Memoria | Terminata (OOMKill) | Kernel OOM Killer |
Avvertenza pratica: I limiti CPU possono proteggere un nodo da un processo incontrollato, ma possono anche creare problemi di latenza quando impostati troppo bassi. Molti team di piattaforma impostano i limiti di memoria in modo coerente e sono più selettivi con i limiti CPU per i servizi sensibili alla latenza, a seconda della loro tolleranza al rischio e della policy del cluster.
Definire le Risorse nelle Specifiche dei Pod
Le risorse sono definite all'interno del blocco spec.containers[*].resources. Le quantità sono specificate utilizzando suffissi standard di Kubernetes (ad es., m per milli-CPU, Mi per Mebibyte).
Definizioni Unità CPU
1unità CPU equivale a 1 core completo (o vCPU sui provider cloud).1000m(millicore) equivale a 1 unità CPU.
Definizioni Unità Memoria
Mi(Mebibyte) oGi(Gibibyte) sono comuni.1024Mi=1Gi.
Esempio di Configurazione YAML
Considera un container che richiede un minimo garantito di 500m CPU e 256Mi di memoria, ma non dovrebbe mai superare 1 CPU e 512Mi:
resources:
requests:
memory: "256Mi"
cpu: "500m"
limits:
memory: "512Mi"
cpu: "1"
I numeri dovrebbero provenire dal comportamento osservato, non da supposizioni. Per una piccola API HTTP, una richiesta iniziale potrebbe essere basata sull'utilizzo normale p50 o p90 durante il traffico lavorativo, poi aggiustata dopo i test di carico. Per un servizio JVM, la memoria deve includere heap, metaspace, memoria nativa, stack dei thread, buffer diretti e overhead del sidecar. Per un job batch, il picco di memoria durante l'input più grande può essere più importante della memoria media.
Classi di Qualità del Servizio (QoS)
La relazione tra Richieste e Limiti determina la classe di Qualità del Servizio (QoS) assegnata a un Pod. Questa classe detta la priorità del Pod quando le risorse diventano scarse e il nodo deve recuperare memoria (evizione).
Kubernetes definisce tre classi QoS:
1. Garantita (Guaranteed)
Definizione: Tutti i container nel Pod devono avere Richieste e Limiti identici e non nulli per sia CPU che Memoria.
- Vantaggio: Questi Pod sono gli ultimi ad essere sfrattati durante la pressione sulle risorse, garantendo la massima stabilità.
- Caso d'Uso: Componenti critici di sistema o database che richiedono un isolamento rigoroso delle prestazioni.
2. Burstabile (Burstable)
Definizione: Almeno un container nel Pod ha Richieste definite, ma o Richieste e Limiti non sono uguali per tutti i container, oppure alcune risorse non sono limitate (anche se impostare i limiti è altamente raccomandato).
- Vantaggio: Permette ai container di superare (burst) le loro richieste, utilizzando la capacità inutilizzata sul nodo, fino ai loro limiti definiti.
- Priorità di Evizione: Sfrattati prima dei Pod BestEffort, ma dopo i Pod Garantiti.
- Caso d'Uso: La maggior parte delle applicazioni standard senza stato dove una leggera variazione nella latenza è accettabile.
3. BestEffort
Definizione: Il Pod non ha Richieste o Limiti definiti per nessun container.
- Vantaggio: Nessuno, a parte la semplicità.
- Rischio: Questi Pod sono i primi candidati per l'evizione quando il nodo sperimenta pressione sulla memoria. Possono anche competere male per la CPU perché non è stata dichiarata alcuna richiesta.
- Caso d'Uso: Job batch non critici o agenti di logging che possono essere facilmente riavviati.
Strategie di Ottimizzazione Pratiche
Una gestione efficace delle risorse richiede misurazione, iterazione e pianificazione attenta.
Strategia 1: Misurare e Impostare le Richieste con Precisione
Le richieste dovrebbero riflettere la quantità di risorse di cui l'applicazione ha bisogno per funzionare in modo accettabile la maggior parte del tempo. Se imposti le richieste troppo alte, sprechi capacità del cluster perché lo scheduler tratta quella capacità come già occupata. Se le imposti troppo basse, lo scheduler potrebbe posizionare troppi pod su un nodo e il carico di lavoro potrebbe essere più incline a soffrire durante la contesa.
Utilizza strumenti di monitoraggio come Prometheus e Grafana per confrontare i valori delle richieste con l'utilizzo reale. Un punto di partenza comune è osservare diversi giorni di traffico normale, ignorare gli incidenti isolati evidenti e impostare le richieste vicino a un percentile sostenuto piuttosto che al singolo picco più alto. Il percentile esatto è una scelta di policy; il punto principale è usare i dati e rivederli.
Ad esempio, se un servizio utilizza normalmente 180m CPU, raggiunge picchi intorno a 450m durante il riscaldamento del deploy e ha picchi rari a 900m durante un'attività batch nota, impostare una richiesta di 900m potrebbe sprecare capacità tutto il giorno. Impostare una richiesta di 50m potrebbe rendere il pod economico da schedulare ma instabile sotto contesa. Una richiesta intorno al range normale sostenuto, più una gestione separata per il percorso batch, è spesso una conversazione migliore.
Strategia 2: Definire Limiti Conservativi
I limiti agiscono come un confine di sicurezza, ma non sono gratuiti. Per la memoria, un limite leggermente superiore al picco di utilizzo misurato può impedire a un container di consumare l'intero nodo. Per la CPU, un limite impedisce a un processo incontrollato di utilizzare CPU illimitata, ma limiti aggressivi possono limitare un servizio anche quando il nodo ha core inattivi.
Avvertenza sui Limiti CPU: Impostare i limiti CPU al di sotto della domanda effettiva può causare una latenza visibile attraverso il throttling. La QoS Burstabile è una scelta ragionevole per molti servizi senza stato, mentre la QoS Garantita è meglio riservata per carichi di lavoro dove il compromesso dell'isolamento è intenzionale.
Strategia 3: Sfruttare il Vertical Pod Autoscaler (VPA)
Ottimizzare manualmente le risorse è difficile e richiede tempo. Il Vertical Pod Autoscaler (VPA) monitora l'utilizzo a runtime e può raccomandare o aggiornare le richieste di risorse, a seconda della sua modalità. In molte configurazioni, il VPA viene prima utilizzato in modalità di raccomandazione in modo che i team possano rivedere le richieste suggerite prima di consentire aggiornamenti automatici.
Fai attenzione quando combini VPA con l'Horizontal Pod Autoscaler. HPA spesso scala in base all'utilizzo relativo alle richieste, quindi modificare le richieste può cambiare il comportamento di scaling. Può funzionare bene, ma dovrebbe essere testato deliberatamente.
Strategia 4: Quote di Risorsa per Namespace
Per prevenire l'accaparramento di risorse tra team o ambienti, gli amministratori dovrebbero utilizzare Quote di Risorsa a livello di Namespace. Una ResourceQuota impone limiti aggregati sulla quantità totale di Richieste e Limiti di CPU/Memoria che possono esistere all'interno di quel namespace, garantendo equità.
Esempio di Quota Namespace
apiVersion: v1
kind: ResourceQuota
metadata:
name: compute-quota
namespace: development
spec:
hard:
requests.cpu: "10"
limits.memory: "20Gi"
Ciò garantisce che la CPU totale richiesta attraverso tutti i Pod nel namespace development non possa superare 10 core e che i limiti di memoria totali non possano superare 20Gi.
Come le Impostazioni Errate si Manifestano nei Cluster Reali
Gli errori di risorse raramente si annunciano come "errori di risorse". Di solito sembrano incidenti applicativi.
Se i limiti CPU sono troppo bassi, potresti vedere un'alta latenza delle richieste mentre i grafici di utilizzo CPU appaiono limitati. Il container vuole più CPU, ma il throttling dei cgroups lo trattiene. Nelle configurazioni basate su Prometheus, metriche come container_cpu_cfs_throttled_periods_total e container_cpu_cfs_periods_total possono aiutare a mostrare se il throttling fa parte della storia.
Se i limiti di memoria sono troppo bassi, il pod potrebbe riavviarsi con Reason: OOMKilled. I log dell'applicazione potrebbero terminare bruscamente perché il processo non ha ricevuto uno spegnimento graduale. kubectl describe pod di solito dice la verità più velocemente del log dell'app in questo caso.
Se le richieste sono troppo alte, i pod potrebbero rimanere in Pending anche se i dashboard mostrano un utilizzo medio del nodo basso. Lo scheduler non posiziona i pod in base all'utilizzo medio effettivo; confronta le risorse richieste con le risorse allocabili. Questo è il motivo per cui un cluster può sembrare sottoutilizzato e comunque rifiutare un nuovo pod.
Se le richieste mancano, il carico di lavoro potrebbe sembrare a posto durante i periodi calmi e poi diventare la prima cosa ad essere schiacciata quando il nodo è occupato. Questo può essere accettabile per job usa-e-getta, ma è un'impostazione predefinita scadente per i servizi rivolti agli utenti.
Un Flusso di Lavoro di Ottimizzazione Più Sicuro
Un ciclo di ottimizzazione pratico assomiglia a questo:
- Inizia con richieste esplicite di CPU e memoria per ogni container di produzione, inclusi i sidecar.
- Imposta i limiti di memoria basati sul picco osservato più un margine, poi monitora i riavvii per OOMKilled.
- Decidi se i limiti CPU sono richiesti dalla policy o dal rischio del carico di lavoro. Se li usi, monitora il throttling.
- Confronta la CPU e la memoria richieste con l'utilizzo effettivo settimanalmente o mensilmente, specialmente dopo rilasci importanti.
- Tratta il dimensionamento corretto come gestione del cambiamento. Una richiesta più bassa può aumentare la densità di bin-packing, ma può anche cambiare il comportamento in caso di guasto durante la pressione del nodo.
I sidecar meritano attenzione. Un proxy di service mesh, un log shipper o un agente di sicurezza possono consumare abbastanza CPU o memoria da cambiare l'impronta reale del pod. Se solo il container dell'app principale viene ottimizzato, il pod può ancora essere rappresentato male allo scheduler.
Esempio: Risolvere un Picco di Latenza Causato dal Throttling della CPU
Immagina un container API con questa configurazione:
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "200m"
memory: "512Mi"
Durante un evento di vendita, la latenza aumenta. Il nodo ha ancora CPU inattiva, ma il container è limitato a 200m. Aumentare le repliche può aiutare, ma ogni pod è ancora individualmente limitato. Una soluzione migliore potrebbe essere aumentare o rimuovere il limite CPU, aumentare la richiesta per corrispondere alla domanda sostenuta normale e utilizzare HPA in modo che il servizio scalì prima che la latenza diventi brutta.
La lezione importante è che un basso utilizzo della CPU nel grafico non significa sempre che l'app è inattiva. Potrebbe significare che all'app non è permesso usarne di più.
Controllo Finale
Le richieste dicono a Kubernetes come posizionare e dare priorità al pod. I limiti dicono al kernel dove fermarlo. I valori corretti provengono da metriche reali, test di carico e una decisione chiara su cosa conta di più per ogni carico di lavoro: densità, isolamento, latenza o costo. Rivedi i valori dopo cambiamenti di traffico, cambiamenti di dipendenze e aggiornamenti runtime. Le impostazioni delle risorse obsolete sono uno dei modi più silenziosi con cui un cluster scivola in scarse prestazioni.