Padroneggiare le Impostazioni di Prefetch di RabbitMQ per Prestazioni Ottimali dei Consumatori
Regola il prefetch di RabbitMQ in modo che i consumatori rimangano occupati senza accumulare messaggi o nascondere elaborazioni lente.
Padroneggiare le Impostazioni di Prefetch di RabbitMQ per Prestazioni Ottimali dei Consumatori
Il prefetch di RabbitMQ è una di quelle impostazioni che sembrano piccole ma cambiano tutto. Controlla quanti messaggi non riconosciuti RabbitMQ permette a un consumatore di tenere contemporaneamente. Impostalo troppo basso e i consumatori veloci passano troppo tempo ad aspettare la consegna successiva. Impostalo troppo alto e i consumatori lenti accumulano silenziosamente lavoro, aumentano la latenza e fanno mentire i grafici della profondità delle code.
Il modo utile di pensare al prefetch è come lavoro non finito. Un prefetch di 20 significa che un consumatore può avere 20 messaggi consegnati ma non ancora riconosciuti. Quei messaggi non sono più pronti nella coda. Sono non riconosciuti, in attesa presso il consumatore fino a quando non li riconosce, li rifiuta, li respinge o si disconnette.
Ciò significa che il prefetch non è solo una manopola per la produttività. È una manopola per l'equità, una manopola per la memoria e una manopola per il recupero da guasti.
Cosa fa basic.qos in RabbitMQ
I consumatori impostano il prefetch con basic.qos. Nella maggior parte delle librerie client si imposta prefetch_count; prefetch_size è usato raramente e di solito è lasciato a zero.
In Python con Pika:
channel.basic_qos(prefetch_count=10)
channel.basic_consume(
queue="jobs",
on_message_callback=handle_message,
auto_ack=False,
)
In Node.js con amqplib:
await channel.prefetch(10);
await channel.consume("jobs", async (msg) => {
try {
await handleMessage(msg.content);
channel.ack(msg);
} catch (err) {
channel.nack(msg, false, false);
}
}, { noAck: false });
Il riconoscimento manuale è importante. Se si utilizzano riconoscimenti automatici, RabbitMQ considera il messaggio completato non appena viene consegnato. Il prefetch non protegge più l'affidabilità dell'elaborazione allo stesso modo, perché non c'è una finestra di messaggi non riconosciuti da gestire.
RabbitMQ applica il prefetch per consumatore per impostazione predefinita nell'uso moderno, anche se la formulazione originale di AMQP è orientata al canale. Alcuni client espongono un flag global. Fai attenzione con esso. Un limite condiviso a livello di canale o connessione può creare interazioni confuse tra i consumatori. La maggior parte dei servizi è più facile da ragionare quando ogni consumatore ha il proprio canale e il proprio conteggio di prefetch.
Perché il prefetch cambia la latenza
Immagina una coda con due consumatori. Il consumatore A riceve un lotto di 100 messaggi e poi incontra una API esterna lenta. Il consumatore B è sano e veloce, ma quei 100 messaggi sono già stati assegnati ad A. RabbitMQ non li darà a B a meno che A non li rifiuti o il suo canale si chiuda.
Dal punto di vista della coda, quei messaggi non sono pronti. Dal punto di vista dell'utente, sono ritardati. Questo è il motivo per cui un prefetch alto può far sembrare un sistema migliore nei grafici del broker mentre peggiora la latenza reale.
Un prefetch basso dà a RabbitMQ più possibilità di distribuire il lavoro equamente. Un prefetch alto dà ai consumatori più lavoro locale e meno viaggi di andata e ritorno al broker. Nessuno dei due è sempre corretto.
Valori iniziali che hanno senso
Per lavori lenti, inizia piccolo. Se ogni messaggio chiama una API di terze parti, scrive diverse righe di database o esegue trasformazioni pesanti per la CPU, prova prefetch_count=1 a 10. Vuoi che un consumatore fallito o lento tenga solo una piccola quantità di lavoro.
Per lavori medi che richiedono decine o centinaia di millisecondi e vengono eseguiti su worker stabili, valori come 10, 20 o 50 sono punti di partenza comuni. Misura prima di andare più in alto.
Per gestori molto veloci dove il broker e il consumatore sono su una rete a bassa latenza, un prefetch più alto può ridurre i viaggi di andata e ritorno e migliorare la produttività. Anche in questo caso, evita di scegliere un numero enorme solo perché ha fatto sembrare buono un benchmark per cinque minuti. Tieni d'occhio la memoria del consumatore e la latenza di coda.
Una semplice regola pratica è dimensionare il prefetch intorno alla quantità di lavoro che un consumatore può comodamente tenere per una breve finestra. Se un worker elabora circa 20 messaggi al secondo e ti senti a tuo agio con circa un secondo di lavoro bufferizzato localmente, un prefetch vicino a 20 è un esperimento ragionevole.
Come capire se il prefetch è troppo alto
Il prefetch è probabilmente troppo alto quando:
messages_unacknowledgedè grande rispetto ai consumatori attivi.- Alcuni consumatori hanno molti messaggi non riconosciuti mentre altri sono inattivi.
- La latenza dei messaggi è alta anche quando
messages_readyè basso. - La memoria del consumatore aumenta durante i picchi.
- Un crash del consumatore causa una grande ondata di riconsegne.
Quest'ultimo punto è facile da perdere. Se un worker tiene 1.000 messaggi non riconosciuti e si blocca, RabbitMQ può riconsegnare quei messaggi. Questo è un comportamento corretto, ma può creare pressione duplicata sui sistemi a valle se il gestore non è idempotente.
Abbassare il prefetch spesso migliora l'equità e il comportamento di recupero. Può ridurre un po' la produttività di picco, ma può migliorare la latenza che gli utenti effettivamente percepiscono.
Come capire se il prefetch è troppo basso
Il prefetch è probabilmente troppo basso quando:
- I consumatori hanno un basso utilizzo della CPU e della memoria mentre
messages_readycontinua a crescere. - Il tempo di elaborazione è molto breve, ma la velocità di consegna è limitata.
- La latenza di rete tra i consumatori e RabbitMQ è evidente.
- Aumentare il prefetch migliora la produttività senza aumentare la latenza di coda o la pressione sulla memoria.
L'esempio classico è un worker veloce che esegue un piccolo calcolo in memoria e riconosce immediatamente. Con prefetch_count=1, potrebbe passare troppo tempo ad aspettare il messaggio successivo. Aumentare il prefetch gli dà un piccolo buffer locale e lo mantiene occupato.
Non nascondere i colli di bottiglia a valle
La regolazione del prefetch non risolverà un database lento. Può solo cambiare come il lavoro viene distribuito e bufferizzato. Se ogni messaggio aspetta la stessa API sovraccarica, un prefetch più alto può far sembrare la produttività migliore per un breve periodo mentre aumenta i timeout e i tentativi.
Misura all'interno del consumatore. Registra o emetti metriche per il tempo speso a decodificare il messaggio, aspettare il database, chiamare servizi esterni e riconoscere. RabbitMQ può mostrarti i conteggi di messaggi pronti e non riconosciuti, ma non può dirti perché il tuo gestore impiega otto secondi.
Quando un servizio a valle è limitato in velocità, il prefetch dovrebbe spesso essere più basso, non più alto. Lascia che la coda assorba il backlog visibilmente invece di nascondere migliaia di chiamate in corso all'interno dei worker.
Prefetch e concorrenza sono diversi
Un prefetch di 50 non significa automaticamente che il tuo consumatore elabori 50 messaggi in parallelo. Significa solo che RabbitMQ può consegnare 50 messaggi prima di ricevere riconoscimenti. Se vengono eseguiti contemporaneamente dipende dal tuo codice consumatore.
Un consumatore a thread singolo con prefetch 50 può elaborare un messaggio alla volta mentre 49 aspettano in memoria. Un pool di worker con concorrenza 10 e prefetch 50 può mantenere dieci attività attive e quaranta bufferizzate. A volte quel buffer è utile. A volte è solo latenza.
Abbina il prefetch alla concorrenza effettiva. Se il tuo processo può eseguire cinque gestori contemporaneamente, un prefetch da 5 a 20 è più facile da ragionare rispetto a 500.
Compromessi tra ordinamento ed equità
Le code di RabbitMQ preservano l'ordine a livello di coda, ma il comportamento del consumatore può cambiare l'ordine in cui il lavoro viene completato. Con più consumatori e prefetch maggiore di 1, il messaggio 20 può finire prima del messaggio 3 perché è andato a un worker più veloce o aveva un lavoro più facile.
Per la maggior parte delle code di lavoro, l'ordine di completamento non ha importanza. Per aggiornamenti di account, modifiche di inventario o flussi di lavoro che devono essere elaborati in sequenza, potrebbe importare molto. In questi casi, usare una coda per chiave di ordinamento, suddividere per chiave o mantenere il prefetch basso può essere più sicuro che inseguire la massima produttività.
L'equità ha un compromesso simile. Un prefetch basso permette a RabbitMQ di distribuire il lavoro in modo più uniforme perché i consumatori tornano per i messaggi più spesso. Un prefetch alto premia i consumatori che ricevono i messaggi per primi. Se i messaggi hanno tempi di elaborazione irregolari, ciò può portare a un worker che tiene un mucchio di lavori lenti mentre un altro finisce rapidamente il suo lotto.
Quando le persone dicono "il bilanciamento del carico di RabbitMQ è irregolare", il prefetch è una delle prime cose da controllare. Il broker può bilanciare solo i messaggi che non sono già stati consegnati.
Il comportamento in caso di guasto è importante
Il prefetch cambia ciò che accade quando un consumatore muore. Con prefetch_count=1, una consegna non riconosciuta torna indietro quando il canale si chiude. Con prefetch_count=500, centinaia possono tornare tutte insieme. Se il consumatore ha eseguito effetti collaterali parziali prima di bloccarsi, quelle riconsegne possono innescare scritture duplicate, email duplicate o chiamate API duplicate a meno che il gestore non sia idempotente.
Ciò non significa che un prefetch alto sia sbagliato. Significa che un prefetch alto va con gestori idempotenti, regole chiare di ripetizione e monitoraggio dei tassi di riconsegna. Se l'elaborazione duplicata sarebbe pericolosa, mantieni la finestra di messaggi non riconosciuti piccola finché l'applicazione non è costruita per gestirla.
Guarda il flag redelivered nel consumatore. Non è un contatore completo di tentativi, ma è un segnale utile che il messaggio è stato consegnato prima. Per limiti di ripetizione robusti, tieni traccia dei tentativi nelle intestazioni o nello stato dell'applicazione e instrada i messaggi esauriti a una coda di lettere morte.
Code multiple e carichi di lavoro misti
Un singolo valore di prefetch raramente si adatta a ogni coda. Un servizio che consuma thumbnail.generate e email.send può aver bisogno di impostazioni diverse per ciascuno. La generazione di miniature può essere pesante per la CPU e migliore con bassa concorrenza. L'invio di email può essere vincolato dalla rete e tollerare più messaggi in volo.
Se un singolo processo consuma diverse code su un canale, il comportamento QoS può diventare più difficile da ragionare. Preferisci canali separati per carichi di lavoro significativamente diversi. Questo rende il prefetch, il monitoraggio e la gestione dei guasti più ovvi.
Messaggi di dimensioni miste sono un altro segnale di avvertimento. Se una coda contiene sia eventi minuscoli che payload enormi, un prefetch basato sul conteggio non riflette bene la pressione sulla memoria. Dieci messaggi piccoli e dieci messaggi grandi non hanno lo stesso costo. In questa situazione, dividi il carico di lavoro o sposta i payload grandi fuori da RabbitMQ e passa riferimenti invece.
Osserva i messaggi non riconosciuti per consumatore, non solo per coda
Un conteggio di messaggi non riconosciuti a livello di coda ti dice che c'è lavoro non finito, ma può nascondere lo squilibrio. Un consumatore può tenere la maggior parte dei messaggi non riconosciuti mentre gli altri sono quasi vuoti. Questo spesso indica un prefetch alto, un costo dei messaggi irregolare o un worker non sano.
Usa metriche a livello di consumatore dall'interfaccia di gestione, Prometheus o rabbitmqctl list_consumers durante un test. Se la distribuzione è irregolare, abbassare il prefetch o suddividere i tipi di messaggi lenti può migliorare la latenza reale anche quando la produttività totale cambia solo un po'.
Rivedi il prefetch dopo i deployment
I valori di prefetch invecchiano. Un valore che funzionava quando un gestore scriveva solo una riga di database può essere sbagliato dopo che la prossima release aggiunge una chiamata API, una validazione extra o un payload più grande. Tratta il prefetch come parte della configurazione delle prestazioni, non come un numero che imposti una volta e dimentichi.
Dopo una release del consumatore, confronta la latenza di elaborazione, i conteggi di messaggi non riconosciuti, le riconsegne e la memoria del consumatore con la versione precedente. Se la latenza aumenta ma la CPU non è satura, il gestore potrebbe aspettare qualcosa di esterno e un prefetch più basso potrebbe mantenere il sistema più equo. Se la CPU è alta e ogni messaggio è vincolato dalla CPU, aggiungere worker o ridurre il lavoro per messaggio potrebbe essere più importante che cambiare il prefetch.
Documenta il motivo del valore scelto vicino alla configurazione del consumatore. I futuri manutentori dovrebbero sapere se prefetch_count=5 è stato scelto per equità, memoria, ordinamento, limiti di velocità a valle o solo come default temporaneo.
Testa con forme di messaggi reali
Non regolare il prefetch con messaggi fittizi minuscoli se i messaggi di produzione sono grandi payload JSON o includono costose ricerche nel database. La dimensione del messaggio e il costo del gestore contano.
Un ciclo di test utile è:
- Scegli un valore di prefetch.
- Esegui un tasso di pubblicazione realistico per un tempo sufficiente a vedere un comportamento stabile.
- Osserva
messages_ready,messages_unacknowledged, CPU del consumatore, memoria del consumatore, latenza di elaborazione e tasso di errore. - Uccidi un consumatore e vedi quanti messaggi vengono riconsegnati.
- Aumenta o diminuisci il prefetch e ripeti.
Il valore migliore è raramente quello con la produttività di benchmark più alta a breve termine. È il valore che mantiene i consumatori occupati, mantiene la latenza accettabile e fallisce in un modo che il tuo sistema può gestire.
Un default pratico
Se non hai ancora dati, inizia con riconoscimenti manuali e prefetch_count=10 per code di lavoro ordinarie. Usa 1 per elaborazione lenta, costosa o strettamente equa. Prova 20 o 50 per gestori veloci e stabili dopo aver misurato. Vai più in alto solo quando le metriche mostrano che i viaggi di andata e ritorno di consegna sono il collo di bottiglia e i consumatori hanno margine di memoria.
La regolazione del prefetch di RabbitMQ non è una configurazione una tantum. Rivedila quando la dimensione dei messaggi cambia, il codice del consumatore cambia, le dipendenze a valle cambiano o aggiungi più istanze di worker. Il giusto valore di prefetch è quello che corrisponde alla forma attuale del lavoro.