PerfectScale
Scaling granulare dei pod in Kubernetes: oltre HPA e VPA
Questa pagina è disponibile anche in English, Deutsch, Español, Français, 日本語 e Português.
About Joshua Fox
Joshua Fox has been a software architect in innovative technology companies for 20 years. Now, he advises tech startups and growth companies on architecture and cost optimization for Google Cloud Platform and Amazon Web Services; also publishing articles, and speaking to cloud engineers.
He has a PhD from Harvard University and a BA in math from Brandeis.
My personal page1. Introduzione
L'autoscaling di Kubernetes nasce per garantire robustezza contenendo al tempo stesso i costi. Gli autoscaler possono regolare il numero di nodi tramite il Cluster Autoscaler, oppure il numero e la dimensione dei pod con un Vertical o Horizontal Pod Autoscaler.
Ci sono però dei limiti: gli autoscaler standard si affidano a metriche generiche come l'utilizzo medio della CPU. Si tratta di un approccio troppo impreciso che porta a due esiti opposti: si tengono in piedi risorse sprecate, oppure si espone il sistema al rischio di instabilità sotto carico. In questo articolo mi concentrerò sui pod autoscaler e mostrerò come una telemetria granulare proveniente da cAdvisor, Kube-State-Metrics ed eBPF possa portare a decisioni di scaling più precise e realmente calibrate sui workloads. Queste decisioni vengono poi applicate ai pod attivi tramite l'API InPlacePodVerticalScaling di Kubernetes.
2. I limiti dell'autoscaling attuale
L'autoscaling standard presenta tre limiti di fondo:
- Granularità del segnale insufficiente: le semplici medie della CPU non rispecchiano l'intero profilo prestazionale di applicazioni complesse, e producono decisioni di scaling tardive o sbagliate.
- Conflitto HPA/VPA: l'HPA scala in base alle risorse richieste, mentre il VPA modifica quelle stesse richieste. Quando i due operano in modo indipendente, possono emettere istruzioni contraddittorie e generare oscillazioni.
- Ridimensionamento basato sull'eviction: il VPA tradizionale richiede l'eviction dei pod per applicare le modifiche alle risorse, penalizzando workloads stateful o sensibili alla cache che non possono permettersi la latenza del cold-start.
3. La pipeline di telemetria multi-livello
Partiamo dalle metriche a livello di nodo come baseline, per poi aggiungere quattro ulteriori livelli di telemetria che restituiscono un segnale più granulare:
- Livello 0 (Baseline): utilizzo di CPU e memoria a livello di nodo dal Kubernetes Metrics Server, che costituisce la base a granularità grossolana.
- Livello 1: telemetria a livello di container tramite cAdvisor. Questo livello intercetta la latenza dei micro-burst — picchi di CPU di breve durata che gonfiano le medie senza riflettere un carico sostenuto — e i memory working set, evitando così decisioni di scaling motivate dalla crescita innocua della page cache del kernel.
- Livello 2: Kube-State-Metrics (KSM) fornisce il contesto sullo stato di salute del workload, come le repliche in attesa e i segnali di saturazione dell'HPA. In questo modo il controller può stabilire se lo scaling sia davvero necessario o già in corso prima di intraprendere ulteriori azioni.
- Livello 3: eBPF interroga il kernel per portare in superficie segnali applicativi profondi, come i tassi di allocazione dell'heap della JVM e la pressione del Garbage Collection (GC). Si possono così regolare le risorse in modo preventivo, prima che si verifichino eventi OOM. Per esempio, una probe
bpftraceche sfrutta i marker USDT di HotSpot comeusdt:/path/to/libjvm.so:hotspot:mem__pool__gc__beginpuò rilevare i picchi di frequenza del GC alcuni secondi prima che si ripercuotano sulla latenza, dando al controller un margine di vantaggio. - Livello 4: le GPU sono ormai una risorsa critica per costi e prestazioni di AI/ML. Le metriche specifiche per GPU offerte da NVIDIA DCGM consentono di scalare in base ai colli di bottiglia della GPU, come l'utilizzo degli SM o la saturazione della banda di memoria.
4. Costruire un controller di scaling unificato
Un controller unificato armonizza questi segnali alla stregua di una macchina a stati: mantiene stati espliciti (stable, scaling-horizontal, scaling-vertical, cooldown) e passa dall'uno all'altro solo al verificarsi di condizioni precise. Si differenzia così dagli scaler attuali, che in genere operano come motori di regole, valutando ogni segnale in modo indipendente con il rischio concreto di emettere istruzioni in conflitto.
Il controller opera in tre fasi:
Fase 1: aggregazione dei segnali
Ogni livello di telemetria contribuisce con un punteggio ponderato a un'unica metrica di pressione per ciascun workload. I segnali a livello applicativo, come quelli del Livello 3 (eBPF/pressione GC), hanno un peso maggiore.
Fase 2: arbitraggio degli assi
Il controller evita i conflitti riconoscendo che uno scaling orizzontale è già in corso; lo si capisce dalla presenza di AbleToScale=True insieme a una differenza diversa da zero tra currentReplicas e desiredReplicas.
Viceversa, se un workload è dichiarato singleton o stateful (tramite annotation o appartenenza a uno StatefulSet), il controller lo indirizza verso lo scaling verticale, mai orizzontale.
Fase 3: riconciliazione e cooldown
Dopo ogni azione di scaling, un periodo di cooldown evita il thrashing. Il controller torna a valutare la situazione solo dopo aver osservato almeno due finestre di metriche consecutive sopra o sotto la soglia, in modo che i picchi transitori non inneschino una serie di eventi di scaling a catena.
5. Vertical Scaling in-place dei pod
L'API InPlacePodVerticalScaling, arrivata in General Availability nel dicembre 2025 con Kubernetes v1.35, consente di modificare le risorse senza eviction dei pod. Il controller applica una patch alla specifica del pod e il kubelet aggiorna dinamicamente i cgroup del container — nessuna connessione interrotta, nessun riavvio di processo.
Un esempio concreto: se il livello eBPF rileva una pressione sostenuta sul GC di Java che lascia presagire l'esaurimento dell'heap entro 30 secondi, il controller emette una PATCH /api/v1/namespaces/{ns}/pods/{name} con resources.requests.memory e resources.limits.memory aggiornati. Il kubelet applica la modifica regolando in-place il limite di memoria del cgroup del container.
È una soluzione particolarmente preziosa per i servizi con tempi di cold-start significativi, come applicazioni JVM, sidecar di database e cache in-memory, dove il ridimensionamento basato sull'eviction introdurrebbe picchi di latenza dell'ordine di decine di secondi.
6. Conclusione
Le configurazioni standard di HPA e VPA spesso non riescono a tenere insieme uptime elevato ed efficienza dei costi. Combinando l'osservabilità granulare di eBPF e cAdvisor con uno scaling orizzontale/verticale coordinato attraverso una macchina a stati unificata, e applicando le modifiche tramite ridimensionamento in-place dei pod, le organizzazioni possono costruire cluster self-healing e correttamente dimensionati con un impatto minimo sull'operatività.
PerfectScale è un'implementazione ispirata ai pattern architetturali descritti in questo articolo. Lavoro nel team Forward Deployed Engineers di DoiT e affianco i clienti su AWS e Google Cloud Platform. Non esiti a contattarmi per qualsiasi domanda.