Mensageria e streaming de eventos

ActiveMQ Classic em produção: KahaDB, alta disponibilidade, segurança e monitoramento

Avançado11 min de leituraAtualizado

Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês

Rodar o ActiveMQ Classic fora de um notebook exige entender onde as mensagens ficam e quem pode mexer nelas. Este guia explica o journal e o índice do KahaDB, a entrega persistente e a não persistente, a reentrega e as filas de mensagens mortas, o failover master/slave sobre um store compartilhado, os plugins de autenticação e autorização, o TLS para os clientes e as ferramentas JMX, Jolokia e CLI usadas para acompanhar um broker e diagnosticar uma fila travada.

O que você vai aprender

  • Explicar como o KahaDB grava no journal, indexa e limpa os dados das mensagens, e ajustar suas principais configurações
  • Configurar políticas de reentrega, expiração e filas de mensagens mortas por fila
  • Montar um failover master/slave com store compartilhado ou JDBC e saber contra o que ele protege e contra o que não protege
  • Proteger um broker com autenticação JAAS, autorização por destino e TLS
  • Monitorar filas, memória e uso do store com JMX, Jolokia e a CLI do activemq
  • Diagnosticar as falhas comuns em produção: filas travadas, store que só cresce e produtores barrados pelo flow control

Um broker que funciona na sua máquina e um broker em que você pode confiar para processar pedidos diferem em três pontos: o que acontece com uma mensagem quando algo cai, quem tem permissão para enviar e receber, e se alguém percebe quando uma fila para de andar. Este guia cobre essas três áreas no ActiveMQ Classic 6. Ele parte do princípio de que você já tem um broker rodando e um activemq.xml básico, como descrito no guia de instalação, e foca nas decisões que importam quando o tráfego real chega.

Como as mensagens são armazenadas

O ActiveMQ mantém cada mensagem persistente em um persistence adapter até que todos os seus consumidores tenham feito o acknowledge. O adapter escolhido determina o throughput, o tempo de recuperação e suas opções de failover.

KahaDB: journal mais índice

O KahaDB é o store padrão e a escolha certa para quase todo mundo. Ele grava as mensagens em um journal append-only e mantém à parte um índice que mapeia filas e assinaturas para posições no journal. São essas gravações sequenciais que o tornam rápido: cada mensagem é gravada uma única vez, no fim do arquivo de journal atual, não importa a qual fila ela pertença.

O diretório data/kahadb contém alguns tipos de arquivo:

  • db-<n>.log — arquivos do journal, de 32 MB cada por padrão. Mensagens, acknowledges e registros de transação são anexados em ordem. Quando um arquivo enche, o próximo é criado.
  • db.data — o índice em B-tree: para cada fila, quais posições do journal guardam suas mensagens pendentes; para cada assinatura durável, até onde ela já consumiu.
  • db.redo — um log de redo usado para reconstruir o db.data se o broker foi derrubado no meio de uma gravação.
  • lock — o lock de arquivo pelo qual espera um segundo broker que compartilhe este diretório (a base do failover com store compartilhado).

Duas tarefas em segundo plano mantêm o store saudável. Um checkpoint (a cada 5 segundos por padrão) grava o índice em disco para que a recuperação tenha menos journal a reprocessar. Um cleanup (a cada 30 segundos) apaga os arquivos do journal que não contêm mais nenhuma mensagem referenciada. Essa segunda regra é a origem da clássica pergunta “por que meu disco está cheio?”: um arquivo do journal sobrevive enquanto uma única mensagem dele não tiver sido consumida, o que significa que uma mensagem esquecida em uma fila sem uso pode segurar gigabytes de arquivos de journal mais novos atrás dela.

As configurações que vale conhecer ficam no elemento <kahaDB>:

<!-- enableJournalDiskSyncs: fsync a cada envio persistente (seguro, mais lento).
     indexCacheSize: páginas do índice mantidas em memória; aumente se houver muitos destinos. -->
<persistenceAdapter>
  <kahaDB directory="${activemq.data}/kahadb"
          journalMaxFileLength="32mb"
          enableJournalDiskSyncs="true"
          checkpointInterval="5000"
          cleanupInterval="30000"
          indexCacheSize="10000"
          concurrentStoreAndDispatchQueues="true"/>
</persistenceAdapter>

enableJournalDiskSyncs é o botão que equilibra segurança e velocidade. Ligado, o broker chama fsync antes de confirmar um envio persistente, então uma queda de energia não consegue fazer perder uma mensagem já confirmada; desligado, quem decide é o page cache do sistema operacional, e o throughput se multiplica várias vezes. concurrentStoreAndDispatchQueues permite que o broker entregue uma mensagem a um consumidor à espera enquanto a gravação em disco ainda está em andamento, e até pule a gravação se o consumidor fizer o acknowledge antes — um ganho grande para filas que esvaziam tão rápido quanto enchem.

Quando um mesmo broker hospeda destinos com padrões de tráfego muito diferentes, o adapter mKahaDB atribui instâncias separadas do KahaDB a padrões de destino, para que uma fila lenta com mensagens antigas não segure os arquivos de journal de uma fila rápida.

Store JDBC

O jdbcPersistenceAdapter coloca as mensagens em três tabelas (ACTIVEMQ_MSGS, ACTIVEMQ_ACKS, ACTIVEMQ_LOCK) de qualquer banco relacional suportado. É bem mais lento que o KahaDB — cada mensagem custa um INSERT e um DELETE —, mas permite reaproveitar o backup, a replicação e o monitoramento do time de banco de dados, e oferece uma tabela de lock para o failover master/slave sem depender da semântica de um sistema de arquivos compartilhado.

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

A documentação mais antiga também menciona os adapters LevelDB e o AMQ store; os dois foram removidos, e uma configuração que os referencie falha na 6.x.

Entrega persistente e não persistente

O store só vê as mensagens enviadas com DeliveryMode.PERSISTENT (o padrão do JMS). Mensagens não persistentes vivem na memória do broker e, quando a memória aperta, são despejadas em um store temporário em arquivo limitado por tempUsage. A diferença de latência no produtor é grande, porque um envio persistente espera a gravação no journal, enquanto um não persistente é confirmado assim que o broker recebe os bytes. Nos topics, a regra tem uma pegadinha: uma mensagem persistente publicada em um topic só é armazenada se existir uma assinatura durável para ela; caso contrário, não há ninguém para quem guardá-la.

Reentrega, expiração e filas de mensagens mortas

Uma mensagem que o consumidor não consegue processar é reentregue, não descartada. O número de tentativas é definido pela RedeliveryPolicy do cliente: seis reentregas com um segundo de intervalo por padrão, configuráveis na connection factory ou na 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);

Depois da última tentativa, a mensagem é movida para uma fila de mensagens mortas (DLQ, dead-letter queue). O padrão é uma única ActiveMQ.DLQ compartilhada pelo broker inteiro, prática para uma demo e inútil em produção, porque não dá para saber de que fila veio uma mensagem morta sem abri-la. A correção usual é uma estratégia por destino na política de destinos:

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

Mensagens expiradas — aquelas cujo JMSExpiration passou antes da entrega — também vão para a DLQ quando processExpired é true, que é o padrão. Isso surpreende times que usam tempos de vida curtos como forma de jogar fora dados velhos: os dados velhos acabam na DLQ. Defina processExpired="false" nesses destinos, ou dê à DLQ sua própria expiração com o atributo expiration da estratégia, para que ela não cresça para sempre.

Seja qual for a configuração, monitore as DLQs: um contador subindo é o primeiro sinal de que um consumidor tem um bug ou de que uma dependência downstream está falhando.

Alta disponibilidade: master/slave sobre um store compartilhado

O ActiveMQ Classic não tem replicação embutida. A alta disponibilidade vem de rodar dois ou mais brokers apontando para o mesmo store; só um — o master — consegue segurar o lock do store e atender os clientes. Os outros sobem, não conseguem o lock e ficam esperando. Quando o master morre, o lock é liberado, um slave o adquire, reprocessa o índice e inicia seus transport connectors, e os clientes que usam o transporte failover: se reconectam a ele.

Store compartilhadoMecanismo de lockCuidado com
KahaDB em sistema de arquivos compartilhado (NFSv4, GFS2, uma SAN)Lock no arquivo lockO sistema de arquivos precisa implementar corretamente o locking POSIX; NFSv3 e alguns serviços de arquivos em nuvem não fazem isso, e um lock que não é liberado deixa você com dois masters
Store JDBCLock de linha em ACTIVEMQ_LOCK, renovado periodicamenteThroughput menor; o banco de dados passa a ser o ponto único de falha e precisa ele mesmo ser altamente disponível

A configuração do slave é idêntica à do master: mesmo local de store; um brokerName diferente não é obrigatório, mas ajuda na leitura dos logs. O failover não é instantâneo — reprocessar o índice do KahaDB de um store grande leva de segundos a minutos — e as mensagens que estavam sendo entregues a um consumidor no momento da falha são reentregues, então os consumidores precisam ser idempotentes. Repare que esse desenho protege contra a falha do processo do broker; se o armazenamento por baixo for perdido, as mensagens também serão, e é por isso que o store deve ficar em armazenamento replicado e entrar nos backups.

Protegendo o broker

Uma instalação padrão do ActiveMQ aceita conexões anônimas em todos os transportes. Protegê-la envolve três camadas: provar quem é o cliente, decidir o que ele pode fazer e criptografar a comunicação.

Autenticação

O simple authentication plugin lista os usuários diretamente no activemq.xml e serve bem para um broker interno pequeno:

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

O plugin JAAS delega a um login module Java padrão configurado em conf/login.config, com usuários e grupos em conf/users.properties e conf/groups.properties por padrão, ou a um módulo LDAP para diretórios corporativos. É a escolha certa a partir do momento em que as contas precisam ser gerenciadas fora da configuração do 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";
};

Os clientes passam as credenciais na conexão: factory.createContext("orders-svc", password). Certificados podem substituir senhas por meio do jaasCertificateAuthenticationPlugin, que mapeia o distinguished name de um certificado de cliente para um usuário; combine-o com transportes ssl:// que exijam autenticação do cliente (transport.needClientAuth=true).

Autorização

O authorization plugin concede aos grupos os direitos read, write e admin por padrão de destino. admin é o direito de criar o destino — algo de que os produtores precisam se as filas são criadas no primeiro uso. Os advisory topics precisam continuar legíveis por todos, senão os clientes que dependem deles falham de jeitos confusos.

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

As entradas são avaliadas da mais específica para a mais genérica, então a regra orders.> vence > para as filas de pedidos. Destinos temporários têm sua própria entrada; sem ela, os clientes request-reply não conseguem criar suas filas de resposta.

Criptografia

Adicione um <sslContext> dentro de <broker> apontando para um keystore e exponha um conector ssl:// ou nio+ssl://. A JVM do broker precisa conseguir ler o keystore, e os clientes precisam confiar no certificado — dentro de uma empresa, o arranjo usual é uma autoridade certificadora privada cuja raiz é importada no truststore dos clientes. O console web tem sua própria configuração de TLS em conf/jetty.xml, e as credenciais dele em conf/jetty-realm.properties não podem de jeito nenhum continuar como admin/admin; as senhas de qualquer um desses arquivos podem ser criptografadas com bin/activemq encrypt e referenciadas com ENC(...), para que texto puro não vá parar no controle de versão.

Monitoramento e diagnóstico

Tudo o que o broker sabe sobre si mesmo é exposto por MBeans JMX no domínio org.apache.activemq, e todas as outras ferramentas são apenas visões sobre esses beans.

FerramentaChega ao JMX viaMelhor para
Console web (http://host:8161/admin)No próprio processoOlhada rápida em filas e consumidores, navegação e purga de mensagens
bin/activemq bstat / dstat / queryJMX remotoScripts e diagnóstico pelo shell
Jolokia (http://host:8161/api/jolokia)Ponte HTTP/JSONExporters do Prometheus, dashboards, qualquer coisa que fale HTTP
JConsole, VisualVMJMX remotoHeap, threads e MBeans em um só lugar
HawtioJolokiaConsole completo no navegador, também usado pelo Artemis

O JMX remoto vem desligado por padrão. Ative-o em bin/env com ACTIVEMQ_SUNJMX_START="-Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.ssl=true ...", vincule-o a uma interface de gerenciamento e nunca exponha uma porta JMX sem autenticação: ela permite execução de código arbitrário.

O endpoint do Jolokia responde a requisições HTTP comuns, o que facilita escrever verificações em script:

# Profundidade, consumidores e mensagens em voo de uma fila (credenciais do console 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'

Os atributos que merecem alerta são poucos:

  • QueueSize por fila — uma fila que cresce por mais de alguns minutos perdeu seus consumidores ou tem um consumidor lento demais.
  • ConsumerCount por fila — zero em uma fila que deveria ter consumidores é um incidente em andamento.
  • InFlightCount — mensagens despachadas, mas sem acknowledge; um valor constante sem nenhum dequeue indica um consumidor travado.
  • MemoryPercentUsage, StorePercentUsage, TempPercentUsage no broker — perto de 100, o producer flow control está prestes a bloquear seus produtores.
  • ExpiredCount e a profundidade de cada fila DLQ.*.
  • TotalConnectionsCount e o número de conexões por transporte, comparados ao maximumConnections que você definiu.

O data/activemq.log é a outra fonte. Duas mensagens valem um grep: o aviso de flow control (Usage Manager Memory Limit reached) e Slow KahaDB access, que indica que o disco não está dando conta das gravações do journal — é a hora de mover data/ para um armazenamento mais rápido ou de repensar o enableJournalDiskSyncs.

Erros comuns

  • KahaDB em NFSv3 ou em um compartilhamento de arquivos na nuvem sem locking confiável. Os dois brokers acham que têm o lock, e o store se corrompe. Use NFSv4 com o locking verificado, um sistema de arquivos em cluster ou um store JDBC.
  • Uma única ActiveMQ.DLQ que ninguém lê. Configure DLQs por fila, crie alertas para elas e monte um procedimento de reprocessamento antes de precisar dele.
  • TTLs curtos como coleta de lixo. Por padrão, mensagens expiradas vão parar na DLQ. Defina processExpired="false" onde dados velhos devem simplesmente sumir.
  • Esquecer a entrada dos advisory topics no mapa de autorização. Os clientes recebem SecurityExceptions enigmáticas ao conectar. Mantenha ActiveMQ.Advisory.> legível por todos.
  • Desligar a sincronização do journal em disco para resolver latência. Funciona, e rebaixa silenciosamente a entrega persistente para “sobrevive a uma queda do broker, mas não da máquina”. Faça disso uma decisão consciente e documente-a.
  • Assinaturas duráveis sem monitoramento. Um assinante durável que se desconectou há meses segura arquivos do journal para sempre. Remova as assinaturas inativas no console, ou defina offlineDurableSubscriberTimeout no elemento <broker> para que as assinaturas offline há mais tempo que isso sejam removidas automaticamente.

Principais pontos

  • O KahaDB anexa as mensagens a arquivos de journal e as indexa separadamente; um arquivo só é recuperado quando todas as suas mensagens foram consumidas, então uma única mensagem esquecida pode segurar o store inteiro.
  • A entrega persistente espera a gravação no journal; a não persistente é mais rápida e só perde mensagens se o processo do broker morrer.
  • A reentrega é definida pela política do cliente; as mensagens que esgotam as tentativas e, por padrão, as expiradas vão para a fila de mensagens mortas — faça uma por destino e fique de olho nela.
  • Alta disponibilidade é um par master/slave compartilhando um store KahaDB ou JDBC protegido por lock; redes de brokers são roteamento, não redundância.
  • Proteja em três camadas: autenticação JAAS ou simples, autorização read/write/admin por destino e TLS nos transportes e no console.
  • Monitore QueueSize, ConsumerCount, InFlightCount, os percentuais de memória/store/temp e a profundidade das DLQs via JMX ou Jolokia, e dispare alertas antes que o producer flow control bloqueie suas aplicações.

Fontes e leituras complementares

Perguntas frequentes

Por que o diretório do KahaDB continua crescendo mesmo com as filas vazias?
Um arquivo do journal só é apagado quando nenhuma mensagem dentro dele ainda é referenciada. Uma única mensagem não consumida, uma única mensagem sem acknowledge em uma assinatura durável ou uma única transação não confirmada segura o arquivo inteiro de 32 MB, e todos os arquivos gravados depois dele. Procure uma fila com algumas mensagens antigas, um assinante durável inativo ou uma transação XA pendente e dê um jeito nisso: o próximo ciclo de limpeza recupera os arquivos.
Uma rede de brokers basta para ter alta disponibilidade?
Não. Uma rede encaminha mensagens entre brokers que estão no ar, mas as mensagens já armazenadas em um broker que caiu ficam indisponíveis até ele voltar. No ActiveMQ Classic, alta disponibilidade exige um par master/slave compartilhando o mesmo store — KahaDB em um armazenamento compartilhado com locking de arquivos correto, ou um store JDBC — para que o slave assuma exatamente as mesmas mensagens.
Entrega não persistente significa que a mensagem se perde em qualquer falha?
Significa que o broker não a grava no store antes de confirmar o envio, então ela se perde se o processo do broker morrer. Ela não se perde quando o consumidor falha ou a rede cai, e o transporte failover do cliente mantém em memória as mensagens não enviadas durante a reconexão. Para métricas, atualizações de presença e invalidações de cache, essa troca costuma valer a pena.
O que verificar primeiro quando uma fila para de esvaziar?
Se ela tem consumidores e o que eles estão fazendo. O console web ou o MBean da fila mostram o número de consumidores e o de mensagens em voo; um consumidor com mensagens em voo que nunca faz acknowledge está travado, muitas vezes em uma chamada a um serviço downstream sem timeout. Se há consumidores e eles estão ociosos, verifique se todas as mensagens estão em um mesmo grupo de mensagens ou bloqueadas por um seletor.
ActiveMQ Classic 6: instalação, activemq.xml e configuração de transportesInstale o ActiveMQ Classic 6 no Linux ou no Docker, entenda o activemq.xml e configure transportes, limites de memória, políticas, failover e redes de brokers.Intermediário10 min de leituraPadrões de mensageria e garantias de entrega: idempotência, outbox, retries e ordenaçãoAt-most-once, at-least-once e exactly-once explicados, e os padrões que tornam a mensageria confiável: consumer idempotente, outbox, retries e ordenação.Avançado13 min de leituraA API JMS (Jakarta Messaging 3.1): sessões, mensagens, acknowledgement e seletoresEnvie e receba mensagens com jakarta.jms: JMSContext, tipos de mensagem e headers, modos de acknowledgement, seletores, assinaturas duráveis e request-reply.Intermediário9 min de leituraNuvemAlta disponibilidade e recuperação de desastres: cálculos, RTO, RPO e padrões de DRCálculo de disponibilidade, domínios de falha, padrões de redundância e as quatro estratégias de recuperação de desastres, com seus custos e trade-offs.Avançado13 min de leituraSegurançaGestão de identidades e acessos: ciclo de vida, MFA e modelos de autorizaçãoIAM para desenvolvedores: admissão, movimentação e desligamento, passkeys e MFA, armazenamento de senhas, RBAC, ABAC ou ReBAC, LDAP, SCIM e revisão de acessos.Intermediário10 min de leituraLinuxLogs e troubleshooting no Linux: journalctl, rotação, OOM, carga e straceUm método repetível para incidentes no Linux: journal e /var/log, rotação, disco e memória esgotados, carga versus CPU, strace e lsof.Avançado12 min de leitura