2015-08-26 17 views
37

Stiamo pensando di introdurre un approccio basato su AMQP per la nostra infrastruttura di microservizi (coreografia). Disponiamo di diversi servizi, ad esempio il servizio clienti, il servizio utente, il servizio articolo, ecc. Stiamo pianificando di introdurre RabbitMQ come sistema centrale di messaggistica.RabbitMQ/AMQP - Coda best practice/progettazione di argomenti in un'architettura MicroService

I'am alla ricerca di migliori pratiche per la progettazione del sistema per quanto riguarda argomenti/code ecc Una possibilità potrebbe essere quella di creare una coda di messaggi per ogni singolo evento che può verificarsi nel nostro sistema, ad esempio:

user-service.user.deleted 
user-service.user.updated 
user-service.user.created 
... 

Penso che non sia l'approccio giusto per creare centinaia di code di messaggi, vero?

Vorrei utilizzare queste belle annotazioni primavera e, così per esempio:

@RabbitListener(queues="user-service.user.deleted") 
    public void handleEvent(UserDeletedEvent event){... 

Non è meglio avere solo qualcosa come "user-service-notifiche", come uno coda e poi inviare tutte le notifiche a quella coda? Mi piacerebbe comunque registrare gli ascoltatori solo per un sottoinsieme di tutti gli eventi, quindi come risolverlo?

La mia seconda domanda: se voglio ascoltare su una coda che non è stata creata in precedenza, otterrò un'eccezione in RabbitMQ. So che posso "dichiarare" una coda con AmqpAdmin, ma dovrei farlo per ogni coda delle mie centinaia in ogni singolo microservizio, come può sempre accadere che la coda non sia stata creata finora?

risposta

25

In genere, è preferibile disporre di scambi raggruppati per tipo di oggetto/combinazioni di tipo di scambio.

in un esempio di eventi utente, è possibile eseguire una serie di operazioni diverse a seconda delle esigenze del sistema.

in uno scenario, potrebbe avere senso avere uno scambio per evento come hai elencato. è possibile creare i seguenti scambi

 
| exchange  | type | 
|-----------------------| 
| user.deleted | fanout | 
| user.created | fanout | 
| user.updated | fanout | 

questo si adatterebbe il "pub/sub" modello di eventi di diffusione a tutti i listener, senza alcuna preoccupazione per ciò che sta ascoltando.

con questa configurazione, qualsiasi coda che si associa a uno di questi scambi riceverà tutti i messaggi pubblicati nella centrale. questo è ottimo per pub/sub e alcuni altri scenari, ma potrebbe non essere quello che desideri in ogni momento dato che non sarai in grado di filtrare i messaggi per specifici consumatori senza creare un nuovo scambio, coda e associazione.

in un altro scenario, è possibile che ci siano troppi scambi in fase di creazione perché ci sono troppi eventi. potresti anche voler combinare lo scambio di eventi utente e comandi utente. questo potrebbe essere fatto con uno scambio diretto o un argomento:

 
| exchange  | type | 
|-----------------------| 
| user   | topic | 

Con una configurazione di questo tipo, è possibile utilizzare i tasti di routing per pubblicare messaggi specifici per code specifiche. Ad esempio, è possibile pubblicare user.event.created come chiave di instradamento e impostarlo come percorso con una coda specifica per un consumatore specifico.

 
| exchange  | type | routing key  | queue    | 
|-----------------------------------------------------------------| 
| user   | topic | user.event.created | user-created-queue | 
| user   | topic | user.event.updated | user-updated-queue | 
| user   | topic | user.event.deleted | user-deleted-queue | 
| user   | topic | user.cmd.create | user-create-queue | 

Con questo scenario, si finisce con un singolo scambio e le chiavi di routing vengono utilizzati per distribuire il messaggio alla coda appropriata. notare che ho anche incluso una chiave di routing "create comando" e la coda qui.questo illustra come è possibile combinare i modelli.

Vorrei ancora registrare gli ascoltatori solo per un sottoinsieme di tutti gli eventi, quindi come risolverlo?

utilizzando uno scambio di fanout, si creerebbero code e associazioni per gli eventi specifici che si desidera ascoltare. ogni consumatore creerebbe la propria coda e vincolante.

utilizzando uno scambio argomento, è possibile impostare il routing chiavi per inviare messaggi specifici per la coda che si desidera, tra cui tutti eventi con un binding come user.events.#.

se avete bisogno di messaggi specifici per andare a consumatori specifici, you do this through the routing and bindings.

in definitiva, non esiste una risposta corretta o errata per il tipo di scambio e la configurazione da utilizzare senza conoscere le specifiche delle esigenze di ciascun sistema. è possibile utilizzare qualsiasi tipo di scambio per qualsiasi scopo. ci sono dei compromessi con ciascuno, ed è per questo che ogni applicazione dovrà essere esaminata attentamente per capire quale è corretta.

come per dichiarare le code. ogni utente deve dichiarare le code e le associazioni di cui ha bisogno prima di provare a collegarsi ad esso. questo può essere fatto all'avvio dell'istanza dell'applicazione oppure attendere fino a quando non è necessaria la coda. di nuovo, questo dipende da ciò di cui l'applicazione ha bisogno.

so che la risposta che sto fornendo è piuttosto vaga e piena di opzioni, piuttosto che risposte reali. non ci sono risposte solide specifiche, però. è tutta una logica fuzzy, scenari specifici e le esigenze del sistema.

FWIW, ho scritto a small eBook that covers these topics da una prospettiva piuttosto unica di raccontare storie. affronta molte delle domande che hai, anche se a volte indirettamente.

18

Il consiglio di Derick va bene, tranne per come chiama le sue code. Le code non devono semplicemente imitare il nome della chiave di routing. Le chiavi di routing sono elementi del messaggio e le code non dovrebbero interessarsene. Ecco a cosa servono i binding.

I nomi di coda devono essere denominati in base a ciò che farà il consumatore collegato alla coda. Qual è l'intento dell'operazione di questa coda. Supponiamo che tu voglia inviare una e-mail all'utente quando viene creato il suo account (quando un messaggio con la chiave di routing user.event.created viene inviato usando la risposta di Derick sopra). Dovresti creare un nome di coda sendNewUserEmail (o qualcosa del genere, in uno stile che ritieni appropriato). Ciò significa che è facile controllare e sapere esattamente cosa fa quella coda.

Perché è importante? Bene, ora hai un'altra chiave di routing, user.cmd.create. Diciamo che questo evento viene inviato quando un altro utente crea un account per qualcun altro (ad esempio, i membri di un team). Desideri comunque inviare un'email anche a quell'utente, in modo da creare l'associazione per inviare quei messaggi alla coda sendNewUserEmail.

Se la coda è stata denominata dopo l'associazione, può causare confusione, soprattutto se le chiavi di routing cambiano. Mantieni i nomi delle code disgiunti e autodescrittivi.

+9

buoni punti! Guardando indietro alla mia risposta sopra, mi piace il modo in cui ti stai avvicinando ai nomi delle code come un'azione da eseguire o l'intento di cosa dovrebbe accadere ai messaggi in questa coda. –

14

Prima di rispondere a "uno scambio, o molti?" domanda. In realtà voglio fare un'altra domanda: abbiamo davvero bisogno di uno scambio personalizzato per questo caso?

Diversi tipi di eventi oggetto sono così nativi da corrispondere a diversi tipi di messaggi da pubblicare, ma a volte non è veramente necessario.Cosa succede se estraiamo tutti i 3 tipi di eventi come un evento di "scrittura", i cui sotto-tipi sono "creati", "aggiornati" e "cancellati"?

| object | event | sub-type | 
|-----------------------------| 
| user | write | created | 
| user | write | updated | 
| user | write | deleted | 

Soluzione 1

La soluzione più semplice per sostenere questa è solo potessimo progettare una coda “user.write”, e pubblicare tutti i messaggi di evento utente scrittura a questa coda direttamente attraverso lo scambio di default globale . Quando si pubblica direttamente su una coda, la limitazione più grande è che si presume che solo un'app sia abbonata a questo tipo di messaggi. Anche più istanze di un'app che si iscrive a questa coda va bene.

| queue  | app | 
|-------------------| 
| user.write | app1 | 

Soluzione 2

La soluzione più semplice non poteva funziona quando c'è una seconda app (con logica di elaborazione diversa) utile per iscriversi a eventuali messaggi pubblicati alla coda. Quando ci sono più app che sottoscrivono, abbiamo almeno bisogno di uno scambio di tipo "fanout" con associazioni a più code. In modo che i messaggi vengano pubblicati su excahnge e lo scambio duplica i messaggi su ciascuna coda. Ogni coda rappresenta il processo di elaborazione di ogni diversa app.

| queue   | subscriber | 
|-------------------------------| 
| user.write.app1 | app1  | 
| user.write.app2 | app2  | 

| exchange | type | binding_queue | 
|---------------------------------------| 
| user.write | fanout | user.write.app1 | 
| user.write | fanout | user.write.app2 | 

Questa seconda soluzione funziona bene se ogni abbonato fa a cuore e vogliono gestire tutti i sottotipi di eventi “user.write”, o almeno per esporre tutti questi eventi sub-tipo per ogni abbonati non è un problema. Ad esempio, se l'app dell'abbonato è semplicemente per mantenere il registro di trascrizione; o anche se l'utente gestisce solo user.created, è opportuno informarlo quando avviene user.updated o user.deleted. Diventa meno elegante quando alcuni abbonati provengono dall'esterno della propria organizzazione e si desidera solo notificarli in merito a eventi specifici del sottotipo. Ad esempio, se app2 vuole solo gestire user.created e non dovrebbe avere la conoscenza di user.updated o user.deleted.

Soluzione 3

Per risolvere il problema di cui sopra, dobbiamo estrarre concetto di “creato dall'utente” da “user.write”. Il tipo di scambio "argomento" potrebbe aiutare. Quando pubblichiamo i messaggi, usiamo user.created/user.updated/user.deleted come chiavi di routing, in modo da poter impostare la chiave di associazione della coda "user.write.app1" essere "utente. *" E la chiave di associazione di La coda "user.created.app2" è "user.created".

| queue    | subscriber | 
|---------------------------------| 
| user.write.app1 | app1  | 
| user.created.app2 | app2  | 

| exchange | type | binding_queue  | binding_key | 
|-------------------------------------------------------| 
| user.write | topic | user.write.app1 | user.*  | 
| user.write | topic | user.created.app2 | user.created | 

Soluzione 4

Il tipo di cambio “argomento” è più flessibile in caso potenzialmente ci saranno più sotto-tipi di eventi. Ma se si conosce chiaramente il numero esatto di eventi, è possibile utilizzare anche il tipo di scambio "diretto" per ottenere prestazioni migliori.

| queue    | subscriber | 
|---------------------------------| 
| user.write.app1 | app1  | 
| user.created.app2 | app2  | 

| exchange | type | binding_queue | binding_key | 
|--------------------------------------------------------| 
| user.write | direct | user.write.app1 | user.created | 
| user.write | direct | user.write.app1 | user.updated | 
| user.write | direct | user.write.app1 | user.deleted | 
| user.write | direct | user.created.app2 | user.created | 

Torna alla domanda "uno scambio o molti?". Finora, tutte le soluzioni utilizzano solo uno scambio. Funziona bene, niente di sbagliato. Quindi, quando potremmo aver bisogno di più scambi? C'è un leggero calo di prestazioni se uno scambio "argomento" ha troppe associazioni. Se la differenza di prestazioni di troppi legami sullo "scambio di argomenti" diventa davvero un problema, ovviamente è possibile utilizzare più scambi "diretti" per ridurre il numero di associazioni di scambio "argomento" per prestazioni migliori. Ma qui voglio concentrarmi maggiormente sui limiti di funzione delle soluzioni "one exchange".

Soluzione 5

Un caso potremmo considerare natually scambi multipli è per i diversi gruppi o dimensioni di eventi. Ad esempio, oltre agli eventi creati, aggiornati e cancellati già citati sopra, se abbiamo un altro gruppo di eventi: login e logout - un gruppo di eventi che descrivono "comportamenti dell'utente" piuttosto che "scrittura dati". Coz diversi gruppi di eventi potrebbero aver bisogno di strategie di routing completamente diverse e di chiavi di instradamento convenzioni di denominazione delle code &, è così che nativo avere uno scambio separato di user.behavior.

| queue    | subscriber | 
|----------------------------------| 
| user.write.app1 | app1  | 
| user.created.app2 | app2  | 
| user.behavior.app3 | app3  | 

| exchange  | type | binding_queue  | binding_key  | 
|--------------------------------------------------------------| 
| user.write | topic | user.write.app1 | user.*   | 
| user.write | topic | user.created.app2 | user.created | 
| user.behavior | topic | user.behavior.app3 | user.*   | 

Altre soluzioni

Ci sono altri casi in cui potremmo aver bisogno scambi multipli per un tipo di oggetto. Ad esempio, se desideri impostare autorizzazioni diverse per gli scambi (ad esempio, solo gli eventi selezionati di un tipo di oggetto possono essere pubblicati in uno scambio da app esterne, mentre l'altro scambio accetta qualsiasi evento dalle app interne). Per un'altra istanza, se si desidera utilizzare diverse piattaforme con suffisso con un numero di versione per supportare versioni diverse delle strategie di routing dello stesso gruppo di eventi. Per un'altra istanza, è possibile definire alcuni "scambi interni" per i collegamenti di scambio-scambio, che potrebbero gestire le regole di routing in modo stratificato.

In sintesi, ancora, "la soluzione finale dipende dalle esigenze del sistema", ma con tutti gli esempi di soluzioni sopra, e con le considerazioni sullo sfondo, spero che possa almeno far pensare a una persona nella giusta direzione.

Ho anche creato a blog post, mettendo insieme questo background di problemi, le soluzioni e altre considerazioni correlate.