PerfectScale
Il tuo servizio JVM ha bisogno di 2 CPU per 30 secondi e di 0,3 CPU per sempre
È nei primi secondi che una JVM richiede più CPU. Scopri come i limiti CPU di Kubernetes rallentano l'avvio della JVM, come misurarlo e come dimensionare i pod.
Questa pagina è disponibile anche in English, Deutsch, Español, Français, 日本語 e Português.
È nei primi secondi che un servizio Java ha bisogno di più CPU. La JVM carica le classi, compila il codice e costruisce l'applicazione. Dopo questa fase, il servizio richiede molta meno CPU. Nella maggior parte dei cluster, un pod mantiene la stessa CPU request e lo stesso CPU limit per tutta la sua vita. Il pod, quindi, non può seguire questo andamento.
Questo disallineamento causa il CPU throttling di Kubernetes durante l'avvio della JVM. Se il limit è basso, la JVM non riesce a completare rapidamente il lavoro di avvio e il pod diventa Ready in ritardo. Se la request è alta, il nodo riserva CPU che il servizio non usa dopo l'avvio. Nessuna delle due impostazioni va bene per entrambe le fasi.
I numeri nel titolo sono un esempio. Il tuo servizio ha i suoi numeri, e questo articolo ti mostra come verificare cosa fa il tuo servizio. L'articolo spiega perché l'avvio richiede più CPU, come Kubernetes applica le impostazioni di CPU e cosa la JVM decide a partire da quelle impostazioni. Alla fine troverai le opzioni disponibili oggi.
Perché l'avvio di Java richiede più CPU rispetto al regime?
Prima che una JVM serva la sua prima richiesta, deve completare una grande quantità di lavoro. Questo lavoro ha quattro parti principali:
Caricamento e linking delle classi
La JVM carica una classe quando il codice ne ha bisogno per la prima volta. Legge il file della classe, lo verifica e lo collega. Un'applicazione Spring ha bisogno di un gran numero di classi nei primi secondi, perché il framework scansiona l'applicazione e crea i suoi bean.
Compilazione JIT con C1 e C2
La JVM inizia interpretando il bytecode, un processo lento. Conta quante volte viene eseguito ciascun metodo. HotSpot compila poi i metodi più utilizzati in codice macchina con due compilatori. C1 compila velocemente e produce codice semplice. C2 compila più lentamente e produce codice più veloce per i metodi più utilizzati. Questo design si chiama tiered compilation. I compilatori girano su thread dedicati e la JVM sceglie il numero predefinito di questi thread in base al numero di CPU.
Thread di garbage collection e crescita dell'heap
L'avvio alloca molti oggetti. L'heap cresce e il collector viene eseguito per liberare memoria. La JVM sceglie il collector e il numero dei suoi thread in base al numero di CPU e alla dimensione della memoria.
Inizializzazione del framework
Il framework costruisce i bean, crea i proxy, legge la configurazione e apre le connessioni. Spring Boot Actuator può elencare ciascuno di questi passaggi nel suo endpoint di startup quando usi BufferingApplicationStartup, il che ti aiuta a capire dove se ne va il tempo.
Ogni parte ha una fine. Una volta caricate le classi, compilati i metodi più utilizzati e creati i bean, il fabbisogno di CPU cala. Il calo non è istantaneo, perché i compilatori JIT continuano a compilare metodi man mano che arriva il traffico reale. Ciononostante, a regime il fabbisogno di CPU è molto inferiore rispetto all'avvio. Il divario tra le due fasi è il motivo per cui una sola impostazione di CPU non può andare bene per entrambe.
Come Kubernetes applica le CPU request e i CPU limit?
Kubernetes controlla la CPU di un container con due impostazioni:
CPU request e scheduling
La CPU request è la quantità di CPU che Kubernetes riserva per un container. Lo scheduler somma le request dei pod su ciascun nodo e colloca un nuovo pod solo su un nodo con abbastanza CPU non riservata. La request determina anche la ripartizione della CPU su un nodo sotto carico. Quando i container competono per la CPU, il kernel concede più tempo CPU al container con la request più alta. Su cgroup v2, il container runtime scrive la request come cpu.weight. Su cgroup v1, usa cpu.shares.
Se imposti un limit senza request, Kubernetes usa il limit come request. Request e limit determinano anche la classe QoS del pod. Un pod è Guaranteed quando ogni container ha lo stesso valore per la sua CPU request e il suo CPU limit, e lo stesso valore per la memory request e il memory limit. È Burstable quando ha alcune request o limit ma non soddisfa la regola Guaranteed. È BestEffort quando non ne ha nessuno.
CPU limit e throttling
Il CPU limit è un tetto rigido, e il kernel lo applica con il throttling. Il kernel divide il tempo in periodi e il kubelet usa per impostazione predefinita un periodo di 100 ms. In ogni periodo, il container può usare una quota di tempo CPU. Il limit determina la dimensione della quota. Il runtime scrive il limit come cpu.max su cgroup v2. Un limit di 500m diventa 50000 100000, ovvero una quota di 50.000 microsecondi in ogni periodo di 100.000 microsecondi.
resources: requests: cpu: 300m # riserva CPU e determina la ripartizione della CPU su un nodo sotto carico memory: 2Gi limits: cpu: "1" # tetto rigido: scritto come 100000 100000 in cpu.max memory: 2GiChe cos'è il CPU throttling in Kubernetes?
Il throttling è il modo in cui il kernel applica un CPU limit. Quando i thread di un container esauriscono la quota di un periodo, il kernel li mette tutti in pausa fino all'inizio del periodo successivo.
I thread condividono una sola quota, e questo dettaglio è importante per una JVM. Prendi un limit di 500m, che concede 50 ms di tempo CPU in ogni periodo. Un singolo thread impegnato impiega 50 ms a esaurire la quota, quindi gira per metà del periodo. Otto thread impegnati esauriscono la stessa quota in circa 6 ms, e poi il kernel li mette in pausa tutti e otto per il resto del periodo. All'avvio, una JVM esegue molti thread impegnati contemporaneamente, quindi esaurisce la quota all'inizio di ogni periodo.
Kubernetes non ferma un container perché usa troppa CPU. Lo rallenta. Kubernetes non segnala il throttling come evento e i log del container non mostrano alcun errore. Il danno si manifesta sotto forma di ritardo, e lo scopri solo se lo misuri.
Il CPU throttling durante l'avvio della JVM
Gran parte dell'avvio della JVM è lavoro CPU-bound. La JVM ha una quantità fissa di lavoro da completare prima che l'applicazione possa servire traffico, e il limit determina quanto velocemente il container può svolgere quel lavoro. Con un limit più alto, il lavoro termina prima. Un limit più basso distribuisce lo stesso lavoro su più periodi, e il container resta in attesa in ogni periodo dopo aver esaurito la quota. Il throttling durante l'avvio causa due problemi:
Avvio lento e readiness in ritardo
Una JVM sottoposta a throttling impiega più tempo ad avviarsi, quindi il pod diventa Ready più tardi. Questo ritardo ha costi anche al di fuori del pod. Un rolling update attende che ogni nuovo pod diventi Ready, quindi ogni rollout dura di più. Le nuove repliche aiutano solo dopo essere diventate Ready, quindi lo scale-out è più lento quando il traffico cresce. Durante il guasto di un nodo, anche i pod sostitutivi richiedono lo stesso tempo extra.
Probe fallite e riavvii
Una startup probe dice al kubelet quando l'applicazione è partita. Mentre la startup probe è in esecuzione, Kubernetes non esegue le liveness e readiness probe. Se la startup probe non ha successo entro failureThreshold × periodSeconds, il kubelet riavvia il container. Per un container che richiede molto tempo ad avviarsi conviene usare una startup probe e puntarla allo stesso endpoint della liveness probe.
Senza una startup probe, un avvio lento può causare un loop di riavvii. La liveness probe fallisce mentre la JVM si sta ancora avviando, il kubelet riavvia il container e il nuovo container ripete lo stesso lavoro di CPU. Una JVM sottoposta a throttling rende questo loop più probabile, perché il suo avvio dura di più.
Una startup probe con una finestra ampia previene il loop:
startupProbe: httpGet: path: /actuator/health/liveness port: 8080 periodSeconds: 5 failureThreshold: 24 # attende fino a 24 x 5 = 120 secondiUna finestra ampia ferma i riavvii, ma non rende l'avvio più veloce. Il pod diventa comunque Ready in ritardo.
Come misurare il throttling?
Due contatori di cAdvisor mostrano il throttling. Il contatore container_cpu_cfs_periods_total conta tutti i periodi. Il contatore container_cpu_cfs_throttled_periods_total conta i periodi in cui il container ha esaurito tutta la sua quota. Dividi il secondo contatore per il primo per ottenere la percentuale di periodi throttled.
sum(rate(container_cpu_cfs_throttled_periods_total{container="app"}[5m]))/sum(rate(container_cpu_cfs_periods_total{container="app"}[5m]))Un rate su cinque minuti nasconde un avvio che dura solo pochi secondi. Per l'avvio, leggi i contatori grezzi quando il pod diventa Ready e fai tu la divisione. Su cgroup v2, il container espone gli stessi dati anche in cpu.stat. I campi nr_periods, nr_throttled e throttled_usec mostrano il numero di periodi, il numero di periodi throttled e il tempo totale di throttling.
kubectl exec <pod-name> -- cat /sys/fs/cgroup/cpu.statPuoi anche leggere i valori di cAdvisor dal kubelet:
kubectl get --raw /api/v1/nodes/<node-name>/proxy/metrics/cadvisor | grep container_cpu_cfsTieni presente l'intervallo di campionamento. Il sistema di metriche aggiorna i suoi valori solo a intervalli fissi. Un picco di avvio più breve di questo intervallo sembra più piccolo di quanto sia realmente, oppure non appare affatto. Per i picchi brevi, campiona i contatori grezzi più spesso.
Come il CPU limit cambia il comportamento della JVM?
Il CPU limit non si limita a rallentare la JVM. Su Linux, la JVM supporta i container per impostazione predefinita (-XX:+UseContainerSupport). Legge il limit all'avvio. Questi tre punti mostrano come quel valore cambia ciò che fa la JVM:
Numero di processori attivi derivato dai CPU limit
La JVM divide la quota di CPU per il periodo e arrotonda il risultato per eccesso al numero intero successivo. Un limit di 1001m dà quindi 2 CPU. JDK 19 e successivi, e JDK 17.0.5 e successivi, non usano la CPU request (cpu.shares) per contare le CPU. Molte impostazioni della JVM dipendono da questo conteggio, inclusi il numero di thread del compilatore JIT e il numero di thread del GC.
Selezione del garbage collector (Serial vs. G1)
La JVM considera una macchina server-class quando ha almeno 2 processori e almeno 1792 MB di memoria. Su una macchina server-class, la JVM seleziona G1. Altrimenti, seleziona il collector Serial. Una piccola variazione del limit può quindi cambiare il collector. Un limit di 1 CPU dà un solo processore, quindi la JVM seleziona Serial. Un limit di 1100m dà due processori, quindi la JVM seleziona G1 quando la memoria è almeno 1792 MB. I due collector si comportano in modo diverso all'avvio. G1 avvia diversi worker thread che girano in parallelo, quindi può usare più CPU contemporaneamente. Serial esegue la collection con un solo thread.
Pod senza CPU limit
Senza un limit, la JVM conta le CPU del nodo. Su un nodo grande, vede molte CPU e avvia molti thread di compilazione e di GC. Il pod quindi non subisce throttling, ma può usare in totale una grande quantità di CPU, e compete con i suoi vicini in proporzione alla sua request. Una request piccola dà a questo pod una porzione ridotta quando il nodo è sotto carico.
Puoi verificare le scelte della tua JVM. Abilita il log dei container e leggi i flag della JVM in esecuzione:
env: - name: JAVA_TOOL_OPTIONS value: "-Xlog:os+container=trace -Xlog:gc*"kubectl exec <pod-name> -- jcmd 1 VM.flags -all \ | grep -E 'UseSerialGC|UseG1GC|ParallelGCThreads|CICompilerCount'Nota: il comando richiede jcmd nell'immagine e presuppone che la JVM giri come processo 1. Se una shell avvia la JVM, usa il vero ID di processo della JVM. Un'immagine solo JRE non include jcmd.
Il flag -XX:ActiveProcessorCount=<n> sostituisce il numero di CPU rilevato. Usalo quando vuoi lo stesso comportamento della JVM in ogni ambiente.
Perché una sola CPU request non è mai sicura?
Ogni modo di impostare la request ha un costo. Ecco le quattro opzioni:
Dimensionare per l'avvio
Una request e un limit alti danno alla JVM abbastanza CPU per avviarsi velocemente. Ma il nodo riserva la request alta per tutta la vita del pod, e gli altri pod non possono usare quella CPU. Dopo l'avvio, il servizio usa solo una piccola parte della riserva. Su molte repliche e molti nodi, questa CPU inutilizzata diventa un costo elevato.
Dimensionare per il regime
Una request e un limit piccoli vanno bene per il servizio dopo l'avvio. Ma ora la JVM ha troppo poca CPU per il lavoro di avvio. Il kernel la sottopone a throttling, il pod diventa Ready in ritardo, le probe possono fallire e i rollout durano di più. Il servizio è economico ma fragile.
Rimuovere il CPU limit
Senza un limit, il kernel non sottopone il container a throttling e l'avvio può usare qualsiasi CPU libera sul nodo. Questa scelta comporta dei compromessi. La JVM dimensiona i suoi thread per tutte le CPU del nodo. Su un nodo sotto carico, il container ottiene tempo CPU solo in proporzione alla sua request, quindi una request piccola può comunque rallentare l'avvio. Inoltre, senza un limit, un pod può prendersi CPU di cui i suoi vicini hanno bisogno.
HPA e il periodo di inizializzazione della CPU
L'Horizontal Pod Autoscaler (HPA) misura l'uso di CPU come percentuale della request. Una request piccola fa sembrare il picco di avvio un carico molto alto, e l'HPA può aggiungere repliche di cui il servizio non ha bisogno. Kubernetes riduce questo rischio, perché l'HPA può ignorare i campioni di CPU di un pod nuovo. Per impostazione predefinita, il ritardo di readiness è di 30 secondi (--horizontal-pod-autoscaler-initial-readiness-delay) e il periodo di inizializzazione della CPU è di 5 minuti (--horizontal-pod-autoscaler-cpu-initialization-period). Entrambi i flag appartengono a kube-controller-manager, quindi su un cluster gestito potresti non poterli modificare.
Come gestire la CPU di avvio oggi
Ecco i modi in cui puoi gestire la CPU di avvio:
Ridurre il lavoro di avvio
Meno lavoro significa un avvio più breve con qualsiasi limit. Spring Boot può creare i bean al primo utilizzo con spring.main.lazy-initialization=true. Il Class Data Sharing e la cache AOT possono eliminare parte del lavoro di caricamento delle classi. La cache AOT è arrivata con JDK 24 (JEP 483). Spring Framework supporta sia CDS sia la cache AOT. Il profiling ti mostra dove se ne va il tuo tempo di avvio.
Impostare il CPU limit sopra il fabbisogno di avvio
Mantieni la request vicina al livello di regime e imposta il limit abbastanza alto per l'avvio. Il pod rientra così nella classe QoS Burstable. Lo scheduler riserva solo la request, e il pod può usare CPU fino al limit quando il nodo ha CPU libera. All'avvio, il nodo potrebbe non avere CPU libera, quindi questa opzione aiuta soprattutto sui nodi con capacità di riserva. Un limit più alto cambia anche il numero di CPU che la JVM vede.
Resize in-place dei pod
Il resize in-place dei pod cambia CPU e memoria di un pod in esecuzione senza riavvio. La funzionalità è arrivata in beta in Kubernetes 1.33 ed è diventata stabile in Kubernetes 1.35. Con questa funzionalità, un pod può partire con un CPU limit alto e passare a uno più basso dopo l'avvio. Un container controlla il comportamento con una resize policy. La policy predefinita per la CPU è NotRequired, e puoi dichiararla nel manifest:
containers: - name: app resizePolicy: - resourceName: cpu restartPolicy: NotRequiredLe risorse si modificano tramite il subresource resize. Questo comando richiede kubectl 1.32 o successivo:
kubectl patch pod <pod-name> --subresource resize --patch \ '{"spec":{"containers":[{"name":"app","resources":{"requests":{"cpu":"300m"},"limits":{"cpu":"500m"}}}]}}'Il campo status.containerStatuses[].resources mostra le risorse che il container usa in questo momento. Alcuni pod non possono usare il resize in-place. I pod Windows e i pod sotto la policy static del CPU o Memory manager ne sono esempi. Un resize, inoltre, non può cambiare la classe QoS del pod. Qualcuno deve anche attivare il resize. Una persona può eseguire la patch a mano, ma per molti pod serve un controller.
Un resize cambia il limite del cgroup, ma la JVM sceglie alcune impostazioni una sola volta. Seleziona il collector all'avvio e lo mantiene. Anche le impostazioni che dipendono dal numero di CPU, come il numero massimo di thread del GC, vengono fissate all'avvio. Runtime.availableProcessors() può cambiare mentre la JVM è in esecuzione, quindi il codice che legge il numero di CPU a runtime può vedere il nuovo valore. Testa un resize sul tuo JDK prima di farci affidamento.
Un'avvertenza sulla memoria della JVM
Non abbassare il memory limit di un container JVM. La JVM fissa il suo heap massimo all'avvio. Per impostazione predefinita, l'heap massimo è il 25% della memoria del container (-XX:MaxRAMPercentage). La JVM usa anche memoria fuori dall'heap, come metaspace, stack dei thread e code cache. Un memory limit inferiore all'uso reale del processo causa un OOM kill. Per questo motivo, ridimensiona la CPU dopo l'avvio e aumenta il memory limit solo quando serve.
VPA e opzioni gestite di startup boost
Il Vertical Pod Autoscaler (VPA) ha una modalità di aggiornamento InPlaceOrRecreate. Questa modalità prova prima un resize in-place e ricrea il pod quando un resize in-place non è possibile. Su GKE, il CPU startup boost funziona con il VPA. Aumenta la CPU request alla creazione del pod e mantiene la request più alta per durationSeconds dopo che il pod diventa Ready. Poi GKE riporta la request al suo valore di base in-place. Il boost si applica solo alla creazione del pod, quindi il riavvio di un container all'interno dello stesso pod non riceve alcun boost.
La funzionalità è in Preview al momento della stesura di questo articolo e richiede GKE 1.36.0-gke.4447000 o successivo.
apiVersion: autoscaling.k8s.io/v1kind: VerticalPodAutoscalermetadata: name: example-vpaspec: targetRef: apiVersion: apps/v1 kind: Deployment name: example updatePolicy: updateMode: "Off" startupBoost: cpu: type: "Factor" factor: 2 durationSeconds: 10In questo esempio, il boost raddoppia la CPU request (factor: 2) e la mantiene per 10 secondi dopo che il pod diventa Ready. Con updateMode impostato su Off, il VPA applica solo il boost e non apporta altre modifiche. Se usi un HPA insieme al boost, definisci una readiness probe e imposta durationSeconds su 0. Questo impedisce all'HPA di aggiungere repliche a causa del picco di CPU all'avvio.
L'automazione a doppia fase di PerfectScale
Un servizio JVM ha due fabbisogni di risorse. All'avvio, la JVM carica le classi, compila il codice e costruisce l'applicazione, quindi il servizio ha bisogno di molta CPU. Dopo, ne richiede molta meno. Una singola raccomandazione media non funziona per entrambe le fasi. PerfectScale fornisce due raccomandazioni per ogni workload: una per l'avvio e una per il runtime a regime. PerfectScale dimensiona la raccomandazione di avvio sul fabbisogno più alto tra le due fasi, così il nodo ha capacità sufficiente durante l'avvio.
PerfectScale applica la raccomandazione di avvio alla creazione del pod. Quando il pod è a regime, PerfectScale applica la raccomandazione di runtime in-place, senza riavvio. PerfectScale tiene traccia della fase di ogni pod e aggiunge controlli di sicurezza quando un workload non può passare in modo pulito alla fase di runtime.
Il resize in-place richiede Kubernetes 1.33 o successivo e la resize policy del container restartPolicy: NotRequired. Supporta Deployment, StatefulSet, DaemonSet e Argo Rollouts, e non supporta i pod Windows. Quando il cluster non supporta il resize in-place, PerfectScale ricorre al suo approccio standard e sicuro.
PerfectScale protegge l'heap della JVM. Quando imposti una dimensione massima o minima dell'heap, PerfectScale non applica raccomandazioni che escono da quei limiti. Per il resize in-place, il memory limit può solo aumentare. Per i container Java, PerfectScale raccoglie anche dati su heap, non-heap e garbage collection. Mostra il tempo di GC, il numero di processori attivi e il tipo di GC. Puoi abilitare la raccolta delle metriche JVM quando installi l'agente PerfectScale, e la funzione supporta solo i nodi Linux. PerfectScale mostra separatamente le risorse di avvio e di regime, così le metriche di costo e spreco riflettono ciò di cui il workload ha avuto bisogno in ciascuna fase.
Partecipa al workshop. Vikram Seshadri di DoiT mostra come dimensionare un pod JVM per la fase di avvio e come ridimensionarlo al regime senza riavvio nel workshop pratico "Java on K8s: The Startup vs. Steady-State Sizing Dilemma" il 28 ottobre 2026 alle 11:00 ET. Prenota il tuo posto.
Vuoi vedere come PerfectScale applica il right-sizing ai tuoi workloads Java? Inizia gratis o prenota una demo.