design rabbitmq esb amqp spring-amqp

design - RabbitMQ/AMQP-Diseño de mejores prácticas de cola/tema en una arquitectura de MicroService



esb spring-amqp (3)

Estamos pensando en introducir un enfoque basado en AMQP para nuestra infraestructura de microservicios (coreografía). Tenemos varios servicios, digamos el servicio al cliente, el servicio al usuario, el servicio de artículos, etc. Estamos planeando presentar RabbitMQ como nuestro sistema de mensajería central.

Estoy buscando las mejores prácticas para el diseño del sistema con respecto a temas / colas, etc. Una opción sería crear una cola de mensajes para cada evento que pueda ocurrir en nuestro sistema, por ejemplo:

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

Creo que no es el enfoque correcto crear cientos de colas de mensajes, ¿no es así?

Me gustaría usar Spring y estas bonitas anotaciones, por ejemplo:

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

¿No es mejor tener algo como "notificaciones de servicio de usuario" como una cola y luego enviar todas las notificaciones a esa cola? Todavía me gustaría registrar a los oyentes solo en un subconjunto de todos los eventos, entonces, ¿cómo resolver eso?

Mi segunda pregunta: si quiero escuchar en una cola que no se creó antes, obtendré una excepción en RabbitMQ. Sé que puedo "declarar" una cola con el AmqpAdmin, pero ¿debo hacer esto para cada cola de mis cientos en cada microservicio, ya que siempre puede suceder que la cola no se haya creado hasta ahora?


Antes de responder el "¿un intercambio, o muchos?" pregunta. De hecho, quiero hacer otra pregunta: ¿realmente necesitamos un intercambio personalizado para este caso?

Los diferentes tipos de eventos de objetos son tan naturales que coinciden con los diferentes tipos de mensajes que se publicarán, pero a veces no es realmente necesario. ¿Qué pasa si abstraemos los 3 tipos de eventos como un evento de "escritura", cuyos subtipos son "creados", "actualizados" y "eliminados"?

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

Solución 1

La solución más sencilla para respaldar esto es que solo podríamos diseñar una cola "user.write" y publicar todos los mensajes de eventos de escritura del usuario en esta cola directamente a través del intercambio global predeterminado. Cuando se publica directamente en una cola, la mayor limitación es que supone que solo una aplicación se suscribe a este tipo de mensajes. Múltiples instancias de una aplicación que se suscribe a esta cola también está bien.

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

Solución 2

La solución más simple no podría funcionar cuando hay una segunda aplicación (que tiene una lógica de procesamiento diferente) que desea suscribirse a cualquier mensaje publicado en la cola. Cuando hay varias aplicaciones suscritas, al menos necesitamos un intercambio de tipo "fanout" con enlaces a múltiples colas. Para que los mensajes se publiquen en el intercambio, y el intercambio duplica los mensajes en cada una de las colas. Cada cola representa el trabajo de procesamiento de cada aplicación diferente.

| 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 |

Esta segunda solución funciona bien si cada suscriptor se preocupa y quiere manejar todos los subtipos de eventos "user.write" o al menos exponer todos estos eventos de subtipos a cada suscriptor no es un problema. Por ejemplo, si la aplicación de suscriptor es simplemente para mantener el registro de transición; o aunque el suscriptor maneja solo user.created, está bien informarle cuándo sucede user.updated o user.deleted. Se vuelve menos elegante cuando algunos suscriptores son externos a su organización, y solo desea notificarles sobre algunos eventos de subtipo específicos. Por ejemplo, si app2 solo quiere manejar user.created y no debería tener el conocimiento de user.updated o user.deleted en absoluto.

Solución 3

Para resolver el problema anterior, tenemos que extraer el concepto "user.created" de "user.write". El tipo de intercambio de "tema" podría ayudar. Al publicar los mensajes, usemos user.created / user.updated / user.deleted como claves de enrutamiento, para que podamos establecer que la clave de enlace de la cola "user.write.app1" sea "user. *" Y la clave de enlace de La cola "user.created.app2" debe ser "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 |

Solución 4

El tipo de intercambio "tema" es más flexible en caso de que potencialmente haya más subtipos de eventos. Pero si conoce claramente el número exacto de eventos, también podría usar el tipo de intercambio "directo" para obtener un mejor rendimiento.

| 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 |

Regrese a la pregunta “¿un intercambio o muchos?”. Hasta ahora, todas las soluciones usan solo un intercambio. Funciona bien, nada de malo. Entonces, ¿cuándo podríamos necesitar intercambios múltiples? Hay una ligera caída en el rendimiento si un intercambio de "tema" tiene demasiados enlaces. Si la diferencia de rendimiento de demasiados enlaces en el "intercambio de temas" realmente se convierte en un problema, por supuesto, podría usar más intercambios "directos" para reducir el número de enlaces de intercambio de "temas" para un mejor rendimiento. Pero, aquí quiero centrarme más en las limitaciones de función de las soluciones de "un intercambio".

Solución 5

Un caso que podríamos considerar de forma nativa en intercambios múltiples es para diferentes grupos o dimensiones de eventos. Por ejemplo, además de los eventos creados, actualizados y eliminados mencionados anteriormente, si tenemos otro grupo de eventos: inicio y cierre de sesión: un grupo de eventos que describe "comportamientos del usuario" en lugar de "escritura de datos". Debido a que diferentes grupos de eventos pueden necesitar estrategias de enrutamiento completamente diferentes y convenciones de nomenclatura de claves y colas de enrutamiento, es natural que tenga un intercambio de comportamiento de usuario por separado.

| 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.* |

Otras soluciones

Hay otros casos en los que podríamos necesitar intercambios múltiples para un tipo de objeto. Por ejemplo, si desea establecer diferentes permisos en los intercambios (por ejemplo, solo los eventos seleccionados de un tipo de objeto pueden publicarse en un intercambio desde aplicaciones externas, mientras que el otro intercambio acepta los eventos de aplicaciones internas). Para otra instancia, si desea utilizar diferentes intercambios con el sufijo de un número de versión para admitir diferentes versiones de estrategias de enrutamiento del mismo grupo de eventos. Para otra instancia más, es posible que desee definir algunos "intercambios internos" para enlaces de intercambio a intercambio, que podrían administrar las reglas de enrutamiento de una manera en capas.

En resumen, aún así, "la solución final depende de las necesidades de su sistema", pero con todos los ejemplos de soluciones anteriores, y con las consideraciones de fondo, espero que al menos pueda pensar en las direcciones correctas.

También creé una publicación en el blog , reuniendo este fondo del problema, las soluciones y otras consideraciones relacionadas.


El consejo de Derick está bien, excepto por cómo nombra sus colas. Las colas no deben simplemente imitar el nombre de la clave de enrutamiento. Las claves de enrutamiento son elementos del mensaje, y las colas no deberían preocuparse por eso. Para eso están los enlaces.

Los nombres de la cola se deben nombrar después de lo que hará el consumidor adjunto a la cola. ¿Cuál es la intención de la operación de esta cola? Supongamos que desea enviar un correo electrónico al usuario cuando se crea su cuenta (cuando se envía un mensaje con la clave de enrutamiento user.event.created usando la respuesta de Derick anterior). Crearía un nombre de cola sendNewUserEmail (o algo por el estilo, en el estilo que considere apropiado). Esto significa que es fácil de revisar y saber exactamente qué hace esa cola.

¿Porque es esto importante? Bueno, ahora tienes otra clave de enrutamiento, user.cmd.create. Supongamos que este evento se envía cuando otro usuario crea una cuenta para otra persona (por ejemplo, miembros de un equipo). Todavía desea enviar un correo electrónico a ese usuario también, por lo que crea el enlace para enviar esos mensajes a la cola sendNewUserEmail.

Si la cola recibió el nombre del enlace, puede causar confusión, especialmente si cambian las claves de enrutamiento. Mantenga los nombres de la cola desacoplados y autodescriptivos.


Generalmente encuentro que es mejor tener intercambios agrupados por combinaciones de tipo de objeto / tipo de intercambio.

En su ejemplo de eventos de usuario, puede hacer varias cosas diferentes según lo que necesite su sistema.

en un escenario, podría tener sentido tener un intercambio por evento como lo ha enumerado. podrías crear los siguientes intercambios

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

esto encajaría con el patrón " pub/sub " de transmisión de eventos a cualquier oyente, sin preocuparse por lo que está escuchando.

con esta configuración, cualquier cola que enlace a cualquiera de estos intercambios recibirá todos los mensajes publicados en el intercambio. Esto es ideal para pub / sub y algunos otros escenarios, pero puede que no sea lo que quiere todo el tiempo, ya que no podrá filtrar mensajes para consumidores específicos sin crear un nuevo intercambio, cola y enlace.

en otro escenario, es posible que se creen demasiados intercambios porque hay demasiados eventos. También puede combinar el intercambio de eventos de usuario y comandos de usuario. Esto podría hacerse con un intercambio directo o de tema:

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

Con una configuración como esta, puede usar claves de enrutamiento para publicar mensajes específicos en colas específicas. Por ejemplo, podría publicar user.event.created como una clave de enrutamiento y hacer que se user.event.created con una cola específica para un consumidor específico.

| 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 este escenario, termina con un solo intercambio y las claves de enrutamiento se utilizan para distribuir el mensaje a la cola adecuada. observe que también incluí una clave de enrutamiento "crear comando" y una cola aquí. Esto ilustra cómo combinar patrones.

Todavía me gustaría registrar a los oyentes solo en un subconjunto de todos los eventos, entonces, ¿cómo resolver eso?

Al usar un intercambio de fanout, crearía colas y enlaces para los eventos específicos que desea escuchar. cada consumidor crearía su propia cola y enlace.

Al utilizar un intercambio de temas, puede configurar claves de enrutamiento para enviar mensajes específicos a la cola que desee, incluidos todos los eventos con un enlace como user.events.# .

Si necesita mensajes específicos para ir a consumidores específicos, hágalo a través de las rutas y enlaces .

finalmente, no hay una respuesta correcta o incorrecta para qué tipo de intercambio y configuración usar sin conocer los detalles de las necesidades de cada sistema. puede usar cualquier tipo de intercambio para casi cualquier propósito. hay compensaciones con cada uno, y es por eso que cada aplicación deberá ser examinada de cerca para entender cuál es la correcta.

en cuanto a declarar sus colas. cada consumidor de mensajes debe declarar las colas y enlaces que necesita antes de intentar adjuntarlo. Esto se puede hacer cuando se inicia la instancia de la aplicación, o puede esperar hasta que se necesite la cola. De nuevo, esto depende de lo que necesite su aplicación.

Sé que la respuesta que proporciono es bastante vaga y llena de opciones, en lugar de respuestas reales. Sin embargo, no hay respuestas sólidas específicas. todo es lógica difusa, escenarios específicos y mirar las necesidades del sistema.

FWIW, he escrito un pequeño libro electrónico que cubre estos temas desde una perspectiva única de contar historias. aborda muchas de las preguntas que tiene, aunque a veces indirectamente.