Dimensiona un pool di thread applicativo in base a quanto le richieste lavorano rispetto a quanto attendono.
Ogni thread che tocca il database trattiene una connessione mentre attende, quindi questo pool fissa il minimo per il tuo pool di connessioni.
Perché il pool di thread giusto è più piccolo di quanto pensi, finché all'improvviso non lo è
Un pool di thread ha un solo compito: tenere occupata la CPU senza lasciare che il lavoro si accumuli dentro il processo. La dimensione che ci riesce dipende quasi interamente da un rapporto: quanto una richiesta passa ad attendere qualcos'altro rispetto a quanto passa davvero a calcolare. Questo strumento applica la formula standard per quel rapporto, la verifica con la legge di Little e ti dice quando il risultato significa che serve un altro modello di concorrenza anziché un numero più grande.
Come funziona
- Applica la formula classica: core × utilizzo obiettivo × (1 + attesa ÷ servizio).
- Usa la legge di Little per ricavare la concorrenza che il tuo throughput obiettivo implica davvero.
- Riporta il throughput che il pool consigliato può sostenere, per vedere se copre il tuo obiettivo.
- Avvisa quando l'aritmetica è andata oltre ciò che un modello un-thread-per-richiesta può ragionevolmente fare.
thread = core x utilizzo x (1 + tempo_attesa / tempo_servizio) legge di Little concorrenza = tasso_arrivo x latenza tetto di throughput max_rps = thread / (tempo_servizio + tempo_attesa)
Esempio pratico
Un servizio su 8 core, con obiettivo dell'80% di CPU, dove una richiesta calcola 20 ms e attende 80 ms il database, a un picco di 300 richieste al secondo.
- attesa ÷ servizio = 80 ÷ 20 = 4
- thread = 8 × 0,8 × (1 + 4) = 32
- latenza = 20 + 80 = 100 ms
- legge di Little: 300 richieste/s × 0,1 s = 30 richieste concorrenti
- 32 thread ≥ 30, quindi il pool copre l'obiettivo
- tetto: 32 ÷ 100 ms = 320 richieste al secondo
Trentadue thread su otto core. Il pool è quattro volte il numero di core non perché i thread siano gratuiti, ma perché ciascuno è inattivo per l'80% del tempo: tiene un posto nella coda del database, non usa la CPU. Porta l'attesa a zero e la stessa formula restituisce 6, che è la risposta giusta per lavoro che non si blocca mai.
Come leggere il risultato
- La formula vale quanto valgono i due tempi che le dai. Il tempo di servizio è tempo CPU, non tempo di orologio, e i due si confondono facilmente: l'orologio include l'attesa, cioè l'altra variabile. Un profiler li separa, un cronometro no.
- Ogni thread che interroga il database trattiene una connessione mentre attende. Questo rende il pool di thread il minimo per il pool di connessioni, ed è il motivo consueto per cui si raggiunge un limite di connessioni: nessuno ha dimensionato i due insieme.
- Oltre qualche centinaio di thread il problema è il modello stesso. Ogni thread di piattaforma costa circa un megabyte di stack e uno slot nello scheduler, quindi un pool da 1.300 passa più tempo a cambiare contesto che a lavorare. Con quel rapporto la risposta è I/O non bloccante o thread virtuali: per questo lo strumento lo dice invece di stampare il numero e lasciarti lì.
- Una coda davanti al pool non è capacità aggiuntiva. Se gli arrivi superano ciò che il pool riesce a smaltire, la coda cresce senza limite e con essa la latenza: le richieste falliscono comunque, solo più tardi e dopo aver consumato memoria. Limita la coda e scarta carico anziché scambiare la profondità per throughput.
- Obiettivi di utilizzo oltre circa l'80% smettono di essere sicuri. La teoria delle code è spietata vicino alla saturazione: quando l'utilizzo si avvicina al 100%, il tempo di attesa cresce senza limite, e l'ultimo 20% di un core costa molta più latenza di quanta ne renda in throughput.
Domande frequenti
- Da dove viene la formula (1 + attesa/servizio)?
- È il risultato standard per tenere occupati N core quando un'attività occupa il core solo per parte della sua vita, reso noto da Brian Goetz in Java Concurrency in Practice. Senza attesa si riduce a core × utilizzo, che è corretto per lavoro CPU-bound; il rapporto contabilizza semplicemente i thread parcheggiati su I/O in ogni istante.
- Il pool di thread deve coincidere con quello di connessioni?
- Il pool di connessioni deve essere grande almeno quanto il numero di thread che possono trovarsi contemporaneamente dentro una chiamata al database, ma non oltre ciò che il limite del database regge sull'intera flotta. Se i due confliggono, la soluzione è meno thread o più istanze applicative, non un pool di connessioni più grande, che sposta soltanto la contesa dentro il database.
- I thread virtuali rendono tutto questo obsoleto?
- Spostano quale numero conta. I thread virtuali costano abbastanza poco da averne uno per richiesta senza dimensionare un pool, quindi il rapporto di attesa smette di vincolarti. Resta il limite a valle: il pool di connessioni, il rate limit di un'API esterna, la CPU stessa. Quelli vanno ancora dimensionati, e questo calcolo continua a dirti dove stringono.
- Come misuro tempo di servizio e tempo di attesa?
- Il tempo di servizio dal profiling CPU o dalla differenza tra tempo di orologio e somma delle chiamate esterne strumentate. Il tempo di attesa dagli span che il tuo tracing già emette per chiamate a database e HTTP. Se non hai né l'uno né l'altro, parti da una stima informata e verifica la previsione: se il pool sostiene molto meno throughput di quanto dica la formula, la tua stima dell'attesa era bassa.