Mensajería y streaming de eventos

ActiveMQ Classic en producción: KahaDB, alta disponibilidad, seguridad y monitorización

Avanzado11 min de lecturaActualizado

Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés

Llevar ActiveMQ Classic más allá de un portátil exige entender dónde viven los mensajes y quién puede tocarlos. Esta guía explica el journal y el índice de KahaDB, la entrega persistente frente a la no persistente, la reentrega y las colas de mensajes muertos, el failover master/slave sobre un almacén compartido, los plugins de autenticación y autorización, TLS para los clientes y las herramientas JMX, Jolokia y CLI con las que vigilar un broker y diagnosticar una cola atascada.

Lo que aprenderás

  • Explicar cómo KahaDB registra en el journal, indexa y limpia los datos de los mensajes, y ajustar sus parámetros clave
  • Configurar políticas de reentrega, expiración y colas de mensajes muertos por cola
  • Montar un failover master/slave con almacén compartido o JDBC y saber de qué protege y de qué no
  • Proteger un broker con autenticación JAAS, autorización por destino y TLS
  • Monitorizar colas, memoria y uso del almacén mediante JMX, Jolokia y la CLI de activemq
  • Diagnosticar los fallos habituales en producción: colas atascadas, almacenes que no paran de crecer y productores frenados por el flow control

Entre un broker que funciona en tu máquina y uno al que puedes confiarle pedidos hay tres diferencias: qué le pasa a un mensaje cuando algo se cae, quién tiene permiso para enviar y recibir, y si alguien se entera cuando una cola deja de moverse. Esta guía cubre esas tres áreas para ActiveMQ Classic 6. Da por hecho que tienes un broker en marcha y un activemq.xml básico, como se describe en la guía de instalación, y se centra en las decisiones que importan cuando llega el tráfico real.

Cómo se almacenan los mensajes

ActiveMQ guarda cada mensaje persistente en un persistence adapter hasta que todos sus consumidores lo han confirmado. El adaptador que elijas determina el throughput, el tiempo de recuperación y tus opciones de failover.

KahaDB: journal más índice

KahaDB es el almacén por defecto y la opción adecuada para casi todo el mundo. Escribe los mensajes en un journal append-only y mantiene aparte un índice que relaciona colas y suscripciones con posiciones del journal. Lo que lo hace rápido son esas escrituras secuenciales: cada mensaje se escribe una sola vez, al final del fichero de journal actual, sea cual sea la cola a la que pertenezca.

El directorio data/kahadb contiene varios tipos de ficheros:

  • db-<n>.log — ficheros del journal, de 32 MB cada uno por defecto. Mensajes, confirmaciones y registros de transacciones se añaden en orden. Cuando un fichero se llena, se crea el siguiente.
  • db.data — el índice en B-tree: para cada cola, qué posiciones del journal contienen sus mensajes pendientes; para cada suscripción duradera, hasta dónde ha consumido.
  • db.redo — un log de redo que sirve para reconstruir db.data si el broker murió a mitad de una escritura.
  • lock — el bloqueo de fichero por el que espera un segundo broker que comparta este directorio (la base del failover con almacén compartido).

Dos tareas en segundo plano mantienen sano el almacén. Un checkpoint (cada 5 segundos por defecto) vuelca el índice a disco para que la recuperación tenga menos journal que reproducir. Un cleanup (cada 30 segundos) borra los ficheros del journal que ya no contienen ningún mensaje referenciado. Esta segunda regla es el origen de la clásica pregunta «¿por qué tengo el disco lleno?»: un fichero del journal sobrevive mientras un solo mensaje suyo siga sin consumir, de modo que un mensaje olvidado en una cola que nadie usa puede retener detrás de él gigabytes de ficheros de journal más recientes.

Los parámetros que conviene conocer están en el elemento <kahaDB>:

<!-- enableJournalDiskSyncs: fsync en cada envío persistente (seguro, más lento).
     indexCacheSize: páginas del índice en memoria; súbelo si hay muchos destinos. -->
<persistenceAdapter>
  <kahaDB directory="${activemq.data}/kahadb"
          journalMaxFileLength="32mb"
          enableJournalDiskSyncs="true"
          checkpointInterval="5000"
          cleanupInterval="30000"
          indexCacheSize="10000"
          concurrentStoreAndDispatchQueues="true"/>
</persistenceAdapter>

enableJournalDiskSyncs es el mando que equilibra seguridad y velocidad. Activado, el broker llama a fsync antes de confirmar un envío persistente, así que un corte de luz no puede hacer perder un mensaje ya confirmado; desactivado, decide la caché de páginas del sistema operativo y el throughput se multiplica varias veces. concurrentStoreAndDispatchQueues permite al broker entregar un mensaje a un consumidor en espera mientras la escritura en disco sigue en curso, e incluso saltarse la escritura si el consumidor confirma antes: una gran mejora para colas que se vacían tan rápido como se llenan.

Cuando un mismo broker aloja destinos con patrones de tráfico muy distintos, el adaptador mKahaDB asigna instancias de KahaDB separadas a patrones de destinos, de modo que una cola lenta con mensajes antiguos no pueda retener los ficheros del journal de una rápida.

Almacén JDBC

El jdbcPersistenceAdapter guarda los mensajes en tres tablas (ACTIVEMQ_MSGS, ACTIVEMQ_ACKS, ACTIVEMQ_LOCK) de cualquier base de datos relacional soportada. Es mucho más lento que KahaDB —cada mensaje supone un INSERT y un DELETE—, pero te permite aprovechar las copias de seguridad, la replicación y la monitorización del equipo de bases de datos, y ofrece una tabla de bloqueo para el failover master/slave sin depender de la semántica de un sistema de ficheros compartido.

<bean id="postgres" class="org.postgresql.ds.PGSimpleDataSource">
  <property name="url" value="jdbc:postgresql://db:5432/activemq"/>
  <property name="user" value="activemq"/>
  <property name="password" value="${db.password}"/>
</bean>

<persistenceAdapter>
  <jdbcPersistenceAdapter dataSource="#postgres" createTablesOnStartup="true"/>
</persistenceAdapter>

La documentación más antigua también menciona los adaptadores LevelDB y el AMQ store; ambos se han eliminado, y una configuración que los referencie falla en 6.x.

Entrega persistente frente a no persistente

El almacén solo ve los mensajes enviados con DeliveryMode.PERSISTENT (el valor por defecto de JMS). Los mensajes no persistentes viven en la memoria del broker y, cuando esta escasea, se vuelcan a un almacén temporal de ficheros limitado por tempUsage. La diferencia de latencia en el productor es grande, porque un envío persistente espera a la escritura en el journal, mientras que uno no persistente se confirma en cuanto el broker tiene los bytes. Con los topics la regla tiene un matiz: un mensaje persistente publicado en un topic solo se almacena si existe una suscripción duradera para él; si no, no hay nadie para quien guardarlo.

Reentrega, expiración y colas de mensajes muertos

Un mensaje que el consumidor no consigue procesar se reentrega, no se descarta. El número de intentos lo decide la RedeliveryPolicy del cliente: seis reentregas separadas un segundo por defecto, configurables en la connection factory o en la URL.

ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("failover:(tcp://broker:61616)");
RedeliveryPolicy policy = factory.getRedeliveryPolicy();
policy.setMaximumRedeliveries(5);
policy.setInitialRedeliveryDelay(1_000);
policy.setUseExponentialBackOff(true);
policy.setBackOffMultiplier(3);          // 1 s, 3 s, 9 s, 27 s, 81 s
policy.setMaximumRedeliveryDelay(60_000);

Tras el último intento, el mensaje pasa a una cola de mensajes muertos (DLQ, dead-letter queue). Por defecto es una única ActiveMQ.DLQ compartida por todo el broker, cómoda para una demo e inútil en producción, porque no puedes saber de qué cola viene un mensaje muerto sin abrirlo. La solución habitual es una estrategia por destino en la política de destinos:

<policyEntry queue=">">
  <deadLetterStrategy>
    <individualDeadLetterStrategy queuePrefix="DLQ." useQueueForQueueMessages="true"
                                  processExpired="true" processNonPersistent="false"/>
  </deadLetterStrategy>
</policyEntry>

Los mensajes expirados —aquellos cuyo JMSExpiration venció antes de la entrega— también van a la DLQ cuando processExpired es true, que es el valor por defecto. Esto sorprende a los equipos que usan tiempos de vida cortos para deshacerse de datos obsoletos: esos datos acaban en la DLQ. Pon processExpired="false" en esos destinos, o da a la DLQ su propia expiración con el atributo expiration de la estrategia para que no crezca sin fin.

Configures lo que configures, monitoriza las DLQ: un contador que sube es la primera señal de que un consumidor tiene un bug o de que falla una dependencia posterior.

Alta disponibilidad: master/slave sobre un almacén compartido

ActiveMQ Classic no trae replicación integrada. La alta disponibilidad se consigue ejecutando dos o más brokers que apuntan al mismo almacén; solo uno —el master— puede tener el bloqueo del almacén y atender a los clientes. Los demás arrancan, no consiguen el bloqueo y esperan. Cuando el master muere, su bloqueo se libera, un slave lo adquiere, reproduce el índice y arranca sus transport connectors, y los clientes que usan el transporte failover: se reconectan a él.

Almacén compartidoMecanismo de bloqueoCuidado con
KahaDB en sistema de ficheros compartido (NFSv4, GFS2, una SAN)Bloqueo sobre el fichero lockEl sistema de ficheros debe implementar bien el bloqueo POSIX; NFSv3 y algunos servicios de ficheros en la nube no lo hacen, y un bloqueo que no se libera te deja con dos masters
Almacén JDBCBloqueo de fila en ACTIVEMQ_LOCK, renovado periódicamenteMenor throughput; la base de datos pasa a ser el punto único de fallo y debe ser a su vez de alta disponibilidad

La configuración del slave es idéntica a la del master: la misma ubicación del almacén; un brokerName distinto no es obligatorio, pero ayuda al leer los logs. El failover no es instantáneo —reproducir el índice de KahaDB de un almacén grande lleva de segundos a minutos— y los mensajes que se estaban entregando a un consumidor en el momento del fallo se reentregan, así que los consumidores deben ser idempotentes. Ten en cuenta que este diseño protege frente a la caída del proceso del broker; si se pierde el almacenamiento subyacente, se pierden también los mensajes, y por eso el almacén debe estar en almacenamiento replicado y en las copias de seguridad.

Proteger el broker

Una instalación de ActiveMQ por defecto acepta conexiones anónimas en todos los transportes. Protegerla implica tres capas: demostrar quién es un cliente, decidir qué puede hacer y cifrar las comunicaciones.

Autenticación

El simple authentication plugin declara los usuarios directamente en activemq.xml y basta para un broker interno pequeño:

<plugins>
  <simpleAuthenticationPlugin anonymousAccessAllowed="false">
    <users>
      <authenticationUser username="orders-svc" password="${orders.password}" groups="producers"/>
      <authenticationUser username="fulfilment" password="${fulfilment.password}" groups="consumers"/>
      <authenticationUser username="ops"        password="${ops.password}"        groups="admins"/>
    </users>
  </simpleAuthenticationPlugin>
</plugins>

El plugin JAAS delega en un login module estándar de Java configurado en conf/login.config, con usuarios y grupos en conf/users.properties y conf/groups.properties por defecto, o en un módulo LDAP para directorios corporativos. Es la opción correcta en cuanto las cuentas deban gestionarse fuera de la configuración del broker:

<plugins>
  <jaasAuthenticationPlugin configuration="activemq"/>
</plugins>
activemq {
    org.apache.activemq.jaas.PropertiesLoginModule required
        org.apache.activemq.jaas.properties.user="users.properties"
        org.apache.activemq.jaas.properties.group="groups.properties";
};

Los clientes pasan sus credenciales al conectarse: factory.createContext("orders-svc", password). Los certificados pueden sustituir a las contraseñas mediante el jaasCertificateAuthenticationPlugin, que asocia el distinguished name de un certificado de cliente a un usuario; combínalo con transportes ssl:// que exijan autenticación del cliente (transport.needClientAuth=true).

Autorización

El authorization plugin concede a los grupos los permisos read, write y admin por patrón de destino. admin es el permiso para crear el destino, que los productores necesitan si las colas se crean en el primer uso. Los advisory topics deben seguir siendo legibles por todos; si no, los clientes que dependen de ellos fallan de formas desconcertantes.

<authorizationPlugin>
  <map>
    <authorizationMap>
      <authorizationEntries>
        <authorizationEntry queue="orders.>"  read="consumers"  write="producers" admin="producers,consumers"/>
        <authorizationEntry queue="DLQ.>"     read="admins"     write="admins"    admin="admins"/>
        <authorizationEntry queue=">"         read="admins"     write="admins"    admin="admins"/>
        <authorizationEntry topic=">"         read="admins"     write="admins"    admin="admins"/>
        <authorizationEntry topic="ActiveMQ.Advisory.>" read="everyone" write="everyone" admin="everyone"/>
      </authorizationEntries>
      <tempDestinationAuthorizationEntry>
        <tempDestinationAuthorizationEntry read="everyone" write="everyone" admin="everyone"/>
      </tempDestinationAuthorizationEntry>
    </authorizationMap>
  </map>
</authorizationPlugin>

Las entradas se evalúan de la más específica a la más general, así que la regla orders.> gana a > para las colas de pedidos. Los destinos temporales tienen su propia entrada; sin ella, los clientes request-reply no pueden crear sus colas de respuesta.

Cifrado

Añade un <sslContext> dentro de <broker> que apunte a un keystore y expón un conector ssl:// o nio+ssl://. La JVM del broker debe poder leer el keystore, y los clientes deben confiar en el certificado: dentro de una empresa lo habitual es una autoridad de certificación privada cuya raíz se importa en el truststore de los clientes. La consola web tiene su propia configuración TLS en conf/jetty.xml, y sus credenciales en conf/jetty-realm.properties no pueden quedarse en admin/admin bajo ningún concepto; las contraseñas de cualquiera de estos ficheros pueden cifrarse con bin/activemq encrypt y referenciarse con ENC(...) para no subir texto en claro al control de versiones.

Monitorización y diagnóstico

Todo lo que el broker sabe de sí mismo se expone mediante MBeans JMX en el dominio org.apache.activemq, y todas las demás herramientas son vistas sobre esos beans.

HerramientaLlega a JMX medianteIdeal para
Consola web (http://host:8161/admin)En el propio procesoVistazo rápido a colas y consumidores, examinar y purgar mensajes
bin/activemq bstat / dstat / queryJMX remotoScripts y diagnóstico desde la shell
Jolokia (http://host:8161/api/jolokia)Puente HTTP/JSONExporters de Prometheus, dashboards, cualquier cosa que hable HTTP
JConsole, VisualVMJMX remotoHeap, threads y MBeans en un solo sitio
HawtioJolokiaConsola completa en el navegador, que también usa Artemis

El JMX remoto viene desactivado por defecto. Actívalo en bin/env con ACTIVEMQ_SUNJMX_START="-Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.ssl=true ...", enlázalo a una interfaz de gestión y no expongas nunca un puerto JMX sin autenticación: permite ejecutar código arbitrario.

El endpoint de Jolokia responde a peticiones HTTP normales, lo que facilita escribir comprobaciones:

# Profundidad, consumidores y mensajes en vuelo de una cola (credenciales de la consola web)
curl -s -u ops:secret \
  'http://broker:8161/api/jolokia/read/org.apache.activemq:type=Broker,brokerName=orders-broker,destinationType=Queue,destinationName=orders.placed/QueueSize,ConsumerCount,InFlightCount,EnqueueCount,DequeueCount'

Los atributos que merecen una alerta son pocos:

  • QueueSize por cola — una cola que crece durante más de unos minutos se ha quedado sin consumidores o tiene uno demasiado lento.
  • ConsumerCount por cola — cero en una cola que debería tener consumidores es una caída en curso.
  • InFlightCount — mensajes despachados pero no confirmados; un valor constante sin ningún dequeue indica un consumidor atascado.
  • MemoryPercentUsage, StorePercentUsage, TempPercentUsage en el broker — al acercarse a 100, el producer flow control está a punto de bloquear a tus productores.
  • ExpiredCount y la profundidad de cada cola DLQ.*.
  • TotalConnectionsCount y el número de conexiones por transporte, frente al maximumConnections que hayas fijado.

data/activemq.log es la otra fuente. Hay dos mensajes que conviene buscar con grep: el aviso de flow control (Usage Manager Memory Limit reached) y Slow KahaDB access, que indica que el disco no da abasto con las escrituras del journal; es el momento de mover data/ a un almacenamiento más rápido o de replantearse enableJournalDiskSyncs.

Errores habituales

  • KahaDB sobre NFSv3 o un recurso compartido en la nube sin bloqueo fiable. Los dos brokers creen tener el bloqueo y el almacén se corrompe. Usa NFSv4 con el bloqueo verificado, un sistema de ficheros en clúster o un almacén JDBC.
  • Una única ActiveMQ.DLQ que nadie lee. Configura DLQ por cola, pon alertas sobre ellas y prepara un procedimiento de reproceso antes de necesitarlo.
  • TTL cortos como recolector de basura. Por defecto, los mensajes expirados acaban en la DLQ. Pon processExpired="false" allí donde los datos obsoletos deban desaparecer sin más.
  • Olvidar la entrada de los advisory topics en el mapa de autorización. Los clientes reciben SecurityException crípticas al conectarse. Mantén ActiveMQ.Advisory.> legible para todos.
  • Desactivar la sincronización a disco del journal para arreglar la latencia. Funciona, y rebaja en silencio la entrega persistente a «sobrevive a una caída del broker, pero no de la máquina». Que sea una decisión consciente, y déjala por escrito.
  • Suscripciones duraderas sin vigilar. Un suscriptor duradero que se desconectó hace meses retiene ficheros del journal para siempre. Elimina las suscripciones inactivas en la consola, o define offlineDurableSubscriberTimeout en el elemento <broker> para que las suscripciones que lleven desconectadas más de ese tiempo se eliminen automáticamente.

Puntos clave

  • KahaDB añade los mensajes a ficheros de journal y los indexa por separado; un fichero solo se recupera cuando todos sus mensajes se han consumido, así que un único mensaje olvidado puede retener todo el almacén.
  • La entrega persistente espera a la escritura en el journal; la no persistente es más rápida y solo pierde mensajes si muere el proceso del broker.
  • La reentrega la decide la política del cliente; los mensajes que agotan sus intentos y, por defecto, los expirados van a la cola de mensajes muertos: hazla por destino y vigílala.
  • La alta disponibilidad es un par master/slave que comparte un almacén KahaDB o JDBC con un bloqueo; las redes de brokers son enrutamiento, no redundancia.
  • Protege en tres capas: autenticación JAAS o simple, autorización read/write/admin por destino y TLS en los transportes y en la consola.
  • Monitoriza QueueSize, ConsumerCount, InFlightCount, los porcentajes de memoria/almacén/temp y la profundidad de las DLQ mediante JMX o Jolokia, y alerta antes de que el producer flow control bloquee tus aplicaciones.

Fuentes y lecturas recomendadas

Preguntas frecuentes

¿Por qué el directorio de KahaDB sigue creciendo aunque mis colas estén vacías?
Un fichero del journal solo se borra cuando ninguno de sus mensajes sigue referenciado. Un único mensaje sin consumir, un único mensaje sin confirmar en una suscripción duradera o una única transacción sin confirmar retiene el fichero entero de 32 MB, y todos los que se escribieron después. Busca una cola con unos pocos mensajes antiguos, un suscriptor duradero inactivo o una transacción XA pendiente, y ocúpate de ello: el siguiente ciclo de limpieza recupera los ficheros.
¿Basta con una red de brokers para tener alta disponibilidad?
No. Una red reenvía mensajes entre brokers que están en marcha, pero los mensajes ya almacenados en un broker que se cae no están disponibles hasta que vuelve. En ActiveMQ Classic, la alta disponibilidad exige un par master/slave que comparta un mismo almacén —KahaDB sobre un almacenamiento compartido con un bloqueo de ficheros correcto, o un almacén JDBC— para que el slave pueda hacerse cargo de los mismos mensajes.
¿Entrega no persistente significa que el mensaje se pierde ante cualquier fallo?
Significa que el broker no lo escribe en el almacén antes de confirmar el envío, así que se pierde si el proceso del broker muere. No se pierde si falla el consumidor o se cae la red, y el transporte failover del cliente conserva en memoria los mensajes no enviados durante la reconexión. Para métricas, actualizaciones de presencia e invalidaciones de caché, el compromiso suele merecer la pena.
¿Qué es lo primero que hay que revisar cuando una cola deja de vaciarse?
Si tiene consumidores y qué están haciendo. La consola web o el MBean de la cola muestran el número de consumidores y el de mensajes en vuelo; un consumidor con mensajes en vuelo que nunca confirma está atascado, a menudo en una llamada a un servicio posterior sin timeout. Si hay consumidores y están ociosos, comprueba si todos los mensajes pertenecen a un mismo grupo de mensajes o si los bloquea un selector.
ActiveMQ Classic 6: instalación, activemq.xml y configuración de transportesInstala ActiveMQ Classic 6 en Linux o Docker, entiende activemq.xml y configura transportes, límites de memoria, políticas, failover y redes de brokers.Intermedio10 min de lecturaPatrones de mensajería y garantías de entrega: idempotencia, outbox, reintentos y ordenAt-most-once, at-least-once y exactly-once explicados, y los patrones que hacen fiable la mensajería: consumidor idempotente, outbox, reintentos y orden.Avanzado13 min de lecturaLa API JMS (Jakarta Messaging 3.1): sesiones, mensajes, acuse de recibo y selectoresEnvía y recibe mensajes con jakarta.jms: JMSContext, tipos de mensaje y cabeceras, modos de acuse, selectores, suscripciones durables y request-reply.Intermedio10 min de lecturaNubeAlta disponibilidad y recuperación ante desastres: cálculos, RTO, RPO y patrones de DRCálculo de disponibilidad, dominios de fallo, patrones de redundancia y las cuatro estrategias de recuperación ante desastres con sus costes y contrapartidas.Avanzado13 min de lecturaSeguridadGestión de identidades y accesos: ciclo de vida, MFA y modelos de autorizaciónIAM para desarrolladores: altas, cambios y bajas, passkeys y MFA, almacenamiento de contraseñas, RBAC, ABAC o ReBAC, LDAP, SCIM y revisión de accesos.Intermedio10 min de lecturaLinuxLogs y resolución de problemas en Linux: journalctl, rotación, OOM, carga y straceUn método repetible ante incidentes en Linux: el journal y /var/log, rotación, disco y memoria agotados, carga frente a CPU, strace y lsof.Avanzado12 min de lectura