Risoluzione dei Problemi Comuni di Configurazione di RabbitMQ

Trova e correggi errori di exchange, coda, binding, acknowledgement e permessi di RabbitMQ senza inseguire falsi indizi.

Risoluzione dei Problemi Comuni di Configurazione di RabbitMQ

La maggior parte dei problemi di configurazione di RabbitMQ sembrano bug dell'applicazione all'inizio. Un publisher dice di aver inviato il messaggio. Un consumer dice di non averlo mai visto. Il grafico delle code è vuoto, o peggio, è pieno e nessuno sa perché. Il modo più veloce per uscirne è smettere di indovinare e seguire il percorso del messaggio: publisher, exchange, binding, coda, consumer, acknowledgement.

RabbitMQ è severo riguardo alla topologia. Un exchange diretto non corrisponde "quasi" a una routing key. Una coda dichiarata come esclusiva non si comporterà come una coda di lavoro condivisa. Un messaggio pubblicato come obbligatorio può essere restituito, mentre lo stesso messaggio non instradabile senza mandatory può essere semplicemente scartato dall'exchange. Questi dettagli sono piccoli finché non ti costano un pomeriggio.

Inizia con il percorso effettivo

Per una pubblicazione AMQP normale, il produttore invia un messaggio a un exchange con una routing key. L'exchange usa il suo tipo e i binding per decidere quali code devono ricevere il messaggio. I consumer poi prelevano le consegne dalle code e le riconoscono dopo l'elaborazione.

Quando un messaggio scompare, fai quattro domande:

  • Il produttore ha pubblicato sull'exchange e sul virtual host che pensi?
  • Quell'exchange esiste ed è del tipo che pensi?
  • C'è un binding da quell'exchange alla coda prevista?
  • La routing key corrisponde a quel binding per il tipo di exchange?

Sembra basilare, ma cattura molti incidenti reali. Staging e produzione spesso usano virtual host diversi. Uno script di deployment può dichiarare orders.created in un ambiente e order.created in un altro. Una coda può essere legata a un pattern topic che manca di una parola extra.

Usa l'interfaccia di gestione o la CLI per ispezionare il broker live, non il codice che speri sia in esecuzione:

rabbitmqctl list_exchanges name type durable auto_delete
rabbitmqctl list_bindings source_name source_kind destination_name destination_kind routing_key
rabbitmqctl list_queues name durable auto_delete exclusive messages_ready messages_unacknowledged consumers

Se usi più virtual host, includi -p:

rabbitmqctl -p production list_bindings source_name destination_name routing_key

Disallineamenti della routing key

Gli exchange diretti richiedono una corrispondenza esatta della binding key. Se una coda è legata con invoice.created, un messaggio pubblicato con invoices.created non arriverà. RabbitMQ non correggerà plurali, maiuscole, punti o trattini.

Gli exchange topic usano * per una parola e # per zero o più parole. Il separatore di parole è un punto. Un binding di logs.* corrisponde a logs.info, ma non a logs.app.info. Un binding di logs.# corrisponde a entrambi.

Un trucco utile per il troubleshooting è aggiungere una coda diagnostica temporanea con un binding ampio, poi pubblicare un messaggio di test noto:

rabbitmqadmin declare queue name=debug.routing durable=false auto_delete=true
rabbitmqadmin declare binding source=events destination=debug.routing destination_type=queue routing_key='#'

Fallo con attenzione in produzione e rimuovi il binding diagnostico quando hai finito. L'obiettivo è dimostrare se i messaggi raggiungono l'exchange.

Per publisher importanti, abilita i publisher returns con il flag mandatory in modo che i messaggi non instradabili siano visibili al publisher. Le publisher confirms ti dicono che il broker ha accettato la pubblicazione; i returns ti dicono che l'exchange non ha potuto instradare il messaggio a nessuna coda. Rispondono a domande diverse.

Errori di tipo di exchange

I cambiamenti di tipo di exchange sono una fonte comune di confusione perché dichiarare un exchange esistente con proprietà diverse fallisce. Se un servizio dichiara events come topic e un altro dichiara events come direct, la seconda dichiarazione dovrebbe ottenere un errore di precondizione.

Quell'errore è positivo. Impedisce a due applicazioni di discordare silenziosamente sull'instradamento. La soluzione non è catturare e ignorare l'eccezione. La soluzione è rendere chiara la proprietà della topologia. Di solito un passo di deployment o un modulo di infrastruttura dovrebbe dichiarare exchange e code condivisi, mentre le applicazioni dichiarano solo code di risposta private o affermano idempotentemente la topologia prevista.

Gli exchange fanout ignorano le routing key. Gli exchange headers instradano per header, non per routing key. Se il tuo messaggio di test ha la routing key giusta ma nessuna coda lo riceve, controlla il tipo di exchange prima di modificare ogni binding.

Proprietà delle code che sorprendono

Durable significa che la definizione della coda sopravvive a un riavvio del broker. Non significa che ogni messaggio all'interno della coda sopravviva. Perché i messaggi sopravvivano a un riavvio, la coda deve essere durable e il messaggio deve essere pubblicato come persistente. Anche in questo caso, i publisher dovrebbero usare confirms se hanno bisogno di sapere quando RabbitMQ ha accettato il messaggio in modo sicuro.

Le code auto-delete vengono rimosse dopo che il loro ultimo consumer se ne va. Sono utili per sottoscrizioni temporanee, ma non sono adatte per code di lavoro condivise. Le code esclusive sono limitate alla connessione che le dichiara e scompaiono quando quella connessione si chiude. Sono utili per code di risposta e consumer privati, non per più istanze worker.

Se una coda sembra "scomparire casualmente", controlla questi flag:

rabbitmqctl list_queues name durable auto_delete exclusive consumers

Controlla anche se il codice dell'applicazione dichiara la coda all'avvio con argomenti diversi rispetto alla coda esistente. RabbitMQ tratta gli argomenti della coda come tipo di coda, dead-letter exchange, lunghezza massima e alcune impostazioni relative alla durabilità come parte del contratto di dichiarazione. Una discrepanza può chiudere il canale con un errore di precondizione.

I messaggi sono pronti, ma i consumer non fanno nulla

Se messages_ready è alto e consumers è zero, RabbitMQ sta aspettando. L'applicazione consumer potrebbe essere giù, connessa al virtual host sbagliato, usando il nome di coda sbagliato o bloccata dai permessi.

Se i consumer sono connessi ma le consegne non avvengono, controlla prefetch e capacità del consumer:

rabbitmqctl list_consumers queue_name channel_pid consumer_tag ack_required prefetch_count active

Un consumer con acknowledgement manuali e una finestra di prefetch piena non riceverà più messaggi finché non riconosce o rifiuta alcuni dei messaggi che ha già. Questo spesso sembra che RabbitMQ abbia smesso di consegnare, quando in realtà il consumer sta trattenendo lavoro non riconosciuto.

Se messages_unacknowledged è alto, guarda i log del consumer e i sistemi a valle. Un database lento, una dipendenza HTTP bloccata o un handler che cattura eccezioni senza riconoscere possono tutti creare un muro di messaggi non riconosciuti.

Bug di acknowledgement

Gli acknowledgement manuali sono la scelta normale per un'elaborazione affidabile. Il consumer dovrebbe riconoscere solo dopo che il lavoro è completo. Se fallisce, dovrebbe rifiutare o nack con una decisione deliberata di riaccodamento.

Il pattern pericoloso è auto_ack=true per lavoro che può fallire. Con acknowledgement automatici, RabbitMQ considera il messaggio gestito non appena viene consegnato. Se il consumer crasha dopo averlo ricevuto, il messaggio è perso dalla coda.

Il bug opposto è non riconoscere mai. Il consumer elabora il messaggio con successo, magari scrive anche in un database, ma dimentica basic_ack. RabbitMQ mantiene la consegna non riconosciuta finché il canale non si chiude, poi la riconsegna. Questo crea lavoro duplicato e conteggi di non riconosciuti in crescita.

Una forma semplice di handler è più facile da controllare:

def handle(ch, method, properties, body):
    try:
        process(body)
    except RetryableError:
        ch.basic_nack(method.delivery_tag, requeue=True)
    except Exception:
        ch.basic_nack(method.delivery_tag, requeue=False)
    else:
        ch.basic_ack(method.delivery_tag)

Se riaccodi ogni fallimento per sempre, un messaggio difettoso può ciclare all'infinito. Usa un dead-letter exchange o un design di retry per i messaggi poison.

Permessi e virtual host

I permessi di RabbitMQ sono limitati per virtual host. Un utente può essere in grado di connettersi ma mancare ancora di permessi di configure, write o read per una coda o un exchange. Questo può manifestarsi come un'eccezione di canale nei log del client, non sempre come un errore amichevole dell'applicazione.

Controlla i permessi direttamente:

rabbitmqctl list_permissions -p production
rabbitmqctl list_user_permissions app_user

Per un servizio che pubblica solo, concedi permessi di write al pattern di exchange di cui ha bisogno ed evita diritti di configure ampi. Per un consumer, concedi read sulla coda e write se deve pubblicare nack a percorsi dead-letter o usare pattern di risposta. Permessi troppo ampi rendono il troubleshooting più facile oggi e le revisioni di sicurezza più difficili domani.

Errori di configurazione del dead-letter

I dead-letter exchange dovrebbero rendere visibili i fallimenti. La configurazione errata del dead-lettering fa l'opposto: i messaggi falliscono, vengono rifiutati e poi svaniscono in un exchange che non ha binding.

Controlla gli argomenti della coda, non solo il nome della coda:

rabbitmqctl list_queues name arguments

Per una coda che dovrebbe dead-letterare i lavori falliti, dovresti vedere argomenti come x-dead-letter-exchange e, a volte, x-dead-letter-routing-key. Poi ispeziona quell'exchange e i suoi binding allo stesso modo in cui ispezioni il percorso principale.

Un errore comune è configurare un dead-letter exchange chiamato jobs.dlx ma legare la coda dead-letter a jobs.failed su un exchange diverso. Un altro è impostare x-dead-letter-routing-key a un valore che nessun binding corrisponde. RabbitMQ instraderà il messaggio dead-letterato attraverso il dead-letter exchange come qualsiasi altra pubblicazione. Se nulla corrisponde, il messaggio non ha un posto utile dove andare.

Le code di retry necessitano della stessa cura. Se costruisci il retry con TTL più dead-lettering, disegna il percorso su carta:

coda principale -> rifiuto -> retry exchange -> coda retry -> TTL scade -> exchange principale -> coda principale

Poi verifica ogni exchange, coda, binding e routing key. I loop di retry sono facili da creare accidentalmente. Metti un limite ai tentativi negli header dei messaggi o nello stato dell'applicazione in modo che un payload difettoso non giri all'infinito.

Sorprese delle policy

Le policy possono cambiare il comportamento della coda senza che il codice dell'applicazione lo menzioni. Una policy può impostare il tipo di coda, la lunghezza massima, il TTL, il dead-letter exchange o altri argomenti opzionali. Questo è utile per le operazioni, ma può confondere il debug quando una coda si comporta diversamente dalla dichiarazione del codice.

Elenca le policy durante il troubleshooting:

rabbitmqctl list_policies

Guarda il pattern e la priorità. Una policy ampia come .* può influenzare code create successivamente da team non correlati. Se una coda sta scartando messaggi più vecchi, controlla le impostazioni di max-length o overflow. Se i messaggi scadono prima del previsto, controlla il TTL a livello di coda e la scadenza per messaggio.

Quando l'applicazione dichiara un insieme di argomenti e una policy ne applica un altro, le regole di RabbitMQ dipendono dall'impostazione. Alcuni argomenti opzionali possono essere controllati dalla policy; altri devono corrispondere alla dichiarazione. L'abitudine operativa sicura è mantenere il comportamento della coda in un posto ovvio e documentare qualsiasi policy che intenzionalmente sovrascrive le impostazioni predefinite dell'applicazione.

Quando produttori e consumatori dichiarano la topologia

Molte librerie client rendono facile per ogni servizio dichiarare exchange, code e binding all'avvio. Questo può essere comodo in sviluppo. In produzione, può creare problemi di proprietà.

Se sia il produttore che il consumatore dichiarano la stessa coda, devono concordare su ogni proprietà importante. Se un deployment cambia una coda da auto-delete a durable, o cambia un argomento dead-letter, il prossimo servizio ad avviarsi potrebbe fallire con un errore di precondizione. Questo è meglio di una deriva silenziosa, ma può comunque rompere un deploy.

Per la topologia condivisa, preferisci un proprietario: Terraform, Ansible, un job di migrazione o un servizio chiaramente responsabile. L'avvio dell'applicazione può ancora affermare che la topologia prevista esiste, ma non dovrebbe creare casualmente code condivise con impostazioni predefinite che nessuno ha revisionato.

La topologia privata è diversa. Un servizio che crea una coda di risposta temporanea o una coda di sottoscrizione esclusiva può possedere quella coda direttamente. La differenza è se un altro servizio dipende dal nome e dal comportamento della coda.

Mantieni un percorso di pubblicazione noto e funzionante

Per sistemi importanti, mantieni un piccolo publisher diagnostico o un comando runbook che invia un messaggio innocuo attraverso l'exchange, la routing key e il virtual host previsti. Dovrebbe usare la stessa classe di credenziali dell'applicazione reale, o almeno un insieme di permessi abbastanza vicino da catturare problemi di instradamento e accesso.

Quel percorso noto e funzionante è utile durante i deployment. Se il messaggio diagnostico viene instradato ma il messaggio dell'applicazione no, confronta la routing key effettiva, gli header e il virtual host dell'app. Se anche il messaggio diagnostico fallisce, il problema è probabilmente topologia, permessi o stato del broker.

Una checklist pratica per incidenti

Quando i messaggi mancano o sono bloccati, raccogli lo stato live prima di riavviare tutto:

rabbitmqctl -p production list_queues name messages_ready messages_unacknowledged consumers state
rabbitmqctl -p production list_bindings source_name destination_name routing_key
rabbitmqctl -p production list_exchanges name type durable
rabbitmqctl -p production list_connections name user vhost state

Poi invia un messaggio di test noto con un ID univoco e traccialo attraverso i log. Non testare con un messaggio di produzione casuale il cui percorso è già poco chiaro.

La maggior parte dei problemi di configurazione di RabbitMQ non sono misteriosi una volta che allinei la topologia dichiarata con il comportamento del publisher e del consumer. Il broker di solito fa esattamente ciò che gli è stato detto. Il lavoro è trovare il punto in cui ciò che gli è stato detto differisce da ciò che il team intendeva.