Compartilhar via


Guia de solução de problemas do Spring JMS

Este artigo descreve como solucionar problemas conhecidos e erros comuns ao usar o Spring JMS. O artigo também responde a algumas perguntas frequentes sobre spring-cloud-azure-starter-servicebus-jms.

Problemas de conectividade

O MessageProducer foi fechado devido a um erro irrecuperável

Descrição do problema

Ao usar JmsTemplate para enviar mensagens, JmsTemplate fica indisponível durante um intervalo ocioso entre 10 e 15 minutos. Enviar mensagens nesse intervalo pode obter as exceções mostradas na seguinte saída de exemplo:

2022-11-06 11:12:05.762  INFO 25944 --- [   scheduling-1] c.e.demo.ServiceBusJMSMessageProducer    : Sending message: 2022-11-06T11:12:05.762072 message 1
2022-11-06 11:12:05.772 ERROR 25944 --- [   scheduling-1] o.s.s.s.TaskUtils$LoggingErrorHandler    : Unexpected error occurred in scheduled task

org.springframework.jms.IllegalStateException: The MessageProducer was closed due to an unrecoverable error.; nested exception is javax.jms.IllegalStateException: The MessageProducer was closed due to an unrecoverable error.
    at org.springframework.jms.support.JmsUtils.convertJmsAccessException(JmsUtils.java:274) ~[spring-jms-5.3.23.jar:5.3.23]
  ...
Caused by: org.apache.qpid.jms.provider.ProviderException: The link 'G0:36906660:qpid-jms:sender:azure:5caf3ef4-9602-413c-964d-cf1292d6e1f5:1:1:1:t4' is force detached. Code: publisher(link376). Details: AmqpMessagePublisher.IdleTimerExpired: Idle timeout: 00:10:00. [condition = amqp:link:detach-forced]
    at org.apache.qpid.jms.provider.amqp.AmqpSupport.convertToNonFatalException(AmqpSupport.java:181) ~[qpid-jms-client-0.53.0.jar:na]
  ...

Análise de causa

As exceções ocorrem para o Barramento de Serviço do Azure quando a conexão e o link amqp estão ativos, mas nenhuma chamada - por exemplo, enviar ou receber chamadas - é feita usando o link por 10 minutos. Nesse caso, o link é fechado. E quando todos os links na conexão foram fechados porque não havia atividade (ociosa) e um novo link não foi criado em 5 minutos, a conexão é fechada.

Para o início do JMS do Barramento de Serviço, o CachingConnectionFactory é usado por padrão, o que armazena em cache a sessão, o produtor e o consumidor. Quando o JmsProducer está ocioso por mais de 10 minutos, mas menos de 15, o link que o produtor armazenado em cache está ocupando foi fechado. As mensagens não podem ser enviadas durante esse intervalo. Em seguida, após mais 5 minutos ocioso, toda a conexão é fechada. Assim, qualquer operação de envio após o intervalo ocioso de 15 minutos faz com que o CachingConnectionFactory crie uma nova conexão para enviar. A operação de envio fica disponível após 15 minutos.

Solução alternativa

Atualmente, o início fornece uma solução alternativa para o problema de desanexação de vínculo aplicando o JmsPoolConnectionFactory, que agrupa Connection, Sessione MessageProducer, e gerencia o ciclo de vida das instâncias em pool. Essa solução alternativa pode garantir que um produtor seja removido após ficar indisponível e, portanto, todas as operações de envio sejam executadas em produtores ativos.

Para usar essa solução alternativa, adicione a seguinte configuração:

spring:
  jms:
    servicebus:
      pool:
        enabled: true
        max-connections: ${your-expected-max-connection-value}

Uso de spring.jms.servicebus.idle-timeout

As propriedades de tempo limite ocioso configuram o tempo limite ocioso de uma conexão AMQP. A especificação AMQP fornece a seguinte descrição:

As conexões estão sujeitas a um limite de tempo limite ocioso. O tempo limite é disparado por um par local quando nenhum quadro é recebido depois que um valor limite é excedido. O tempo limite ocioso é medido em milissegundos e começa a partir do momento em que o último quadro é recebido. Se o limite for excedido, um par deverá tentar fechar normalmente a conexão usando um quadro próximo com um erro explicando o motivo. Se o par remoto não responder normalmente dentro de um limite para isso, o par poderá fechar o soquete TCP.

Para um cliente JMS, ao configurar essa propriedade, você controla no lado do servidor quanto tempo espera que o servidor envie um quadro vazio para manter uma conexão ativa quando nenhuma mensagem for entregue. Essa propriedade controla o comportamento do par remoto e cada par pode ter seu próprio valor isolado.

Problemas de JmsTemplate

Mensagens agendadas

O Barramento de Serviço do Azure dá suporte ao processamento de mensagens atrasadas. Para obter mais informações, consulte a seção Mensagens agendadas de sequenciamento de mensagens e carimbos de data/hora. Para o JMS, para agendar uma mensagem, defina a propriedade ScheduledEnqueueTimeUtc usando o cabeçalho de anotação da mensagem x-opt-scheduled-enqueue-time.

Problemas de JmsListener

Muitas solicitações são enviadas ao Barramento de Serviço mesmo que não haja mensagens no servidor

Descrição do problema

Ao usar a API @JmsListener, em alguns casos, você pode ver no portal do Azure que há valores contínuos para solicitações de entrada enviadas para sua fila ou tópicos, mesmo que não haja mensagens no servidor a serem recebidas.

Análise de causa

@JmsListener é um ouvinte de votação, que é criado para repetidas tentativas de votação.

O ouvinte se senta em um loop de sondagem contínuo. Cada loop chama o método MessageConsumer.receive() JMS para sondar o consumidor local quanto às mensagens a serem consumidas. Por padrão, para cada operação de sondagem, o consumidor local envia solicitações de pull ao agente de mensagens para solicitar mensagens e, em seguida, bloqueia por um determinado período de tempo. O processo de sondagem concreto é decidido por várias propriedades, incluindo receiveTimeout, prefetchSizee receiveLocalOnly ou receiveNoWaitLocalOnly. O método receiveNoWaitLocalOnly é usado somente quando você define receiveTimeout como um valor negativo.

Quando esse problema acontecer com seu aplicativo, verifique as seguintes configurações:

  • Determine se a política de pré-busca é 0, que também é a opção padrão. 0-prefetch significa um consumidor pull que envia solicitações de pull para o Barramento de Serviço para cada votação.

  • Se você configurou pré-busca diferente de zero, determine se a propriedade receiveLocalOnly ou receiveNoWaitLocalOnly está definida como false, que é a opção padrão. Um valor false aqui ainda resulta no envio de solicitações de pull para o servidor porque ele não sonda apenas o consumidor local.

  • A configuração de receiveTimeout determina quanto tempo ela bloqueia para cada solicitação de pull, para que possa afetar a frequência de solicitações de pull enviadas ao servidor. O valor padrão é 1 segundo.

Para obter uma análise completa, consulte a discussão node problema do GitHub.

Soluções

As seções a seguir descrevem duas soluções para lidar com esse problema

Solução 1. Alterar somente para enviar por push o consumidor e a verificação local

Ao alterar o modo para push, o consumidor se torna um notificação assíncrona consumidor que não efetua pull de mensagens do agente, mas mantém uma quantidade de crédito de link de destino. O valor é decidido por uma propriedade de pré-busca. À medida que o Barramento de Serviço (remetente) envia mensagens por push, o link-crédito do remetente diminui e, quando o crédito de link do remetente fica abaixo de um limite, o cliente (receptor) envia uma solicitação ao servidor para aumentar o crédito de link do remetente de volta para o valor de destino desejado.

Para realizar essa solução, adicione a seguinte configuração:

Primeiro, configure o número de prefetch como diferente de zero, o que configura o consumidor como não pull. A tabela a seguir mostra várias propriedades de pré-busca, cada uma das quais controla diferentes entidades do Barramento de Serviço. Defina as propriedades que se aplicam ao seu caso.

Propriedade Descrição
spring.jms.servicebus.prefetch.all O valor de fallback para a opção de pré-busca neste namespace do Barramento de Serviço
spring.jms.servicebus.prefetch.queue-prefetch O número de pré-busca da fila.
spring.jms.servicebus.prefetch.queue-browser-prefetch O número de pré-busca do navegador da fila.
spring.jms.servicebus.prefetch.topic-prefetch O número de pré-busca do tópico.
spring.jms.servicebus.prefetch.durable-topic-prefetch O número de pré-busca do tópico durável.

Em segundo lugar, configure o non-local-check adicionando uma classe de configuração para o personalizador de fábrica, conforme mostrado no exemplo a seguir:

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

    @Bean
    ServiceBusJmsConnectionFactoryCustomizer customizer() {
        return factory -> {
            factory.setReceiveLocalOnly(true);
            // Configure the below ReceiveNoWaitLocalOnly instead if you have specified the property 
            // spring.jms.listener.receive-timeout with negative value. Otherwise, configure the above `ReceiveLocalOnly`.
            //factory.setReceiveNoWaitLocalOnly(true);
        };
    }
}

O valor de pré-busca pode afetar a rapidez com que as mensagens são enviadas para o buffer local do consumidor. Você deve ajustar o valor de acordo com o desempenho de consumo e os volumes de mensagens. Um valor adequado pode acelerar o processo de consumo, enquanto um valor muito grande pode fazer com que as mensagens em buffer local fiquem desatualizadas e enviadas novamente. Para volumes de mensagens baixos, em que cada mensagem leva muito tempo para ser processada, defina o pré-busca como 1. Esse valor garante que um consumidor esteja processando apenas uma mensagem por vez.

Solução 2. Aumentar o tempo limite de recebimento para diminuir a frequência de pull

A propriedade de tempo limite de recebimento determina a estratégia de quanto tempo o consumidor bloqueia para aguardar um resultado de pull. Portanto, ao estender o tempo limite, você pode reduzir a frequência de pull e, em seguida, reduzir o número de solicitações de pull ao escolher o modo de pull. Em casos extremos, você pode definir a estratégia para aguardar indefinidamente até que uma mensagem chegue, o que significa que o consumidor só efetua pull depois de consumir uma mensagem. Nesse caso, quando não houver mensagens no servidor, ele bloqueará a espera.

Para realizar essa solução, configure a propriedade spring.jms.listener.receive-timeout. Essa propriedade é do tipo java.time.Duration e tem um valor padrão de 1 segundo. A lista a seguir explica o efeito de vários valores:

  • Definir o tempo limite de recebimento como 0 significa que o pull bloqueia por tempo indeterminado até que uma mensagem seja expedida.
  • Definir o tempo limite de recebimento como um valor positivo significa que o pull bloqueia até o tempo limite.
  • Definir o tempo limite de recebimento como um valor negativo significa que o pull é um recebimento sem espera, o que significa que ele retorna uma mensagem imediatamente ou null se nenhuma mensagem estiver disponível.

Nota

Um valor de tempo limite alto pode trazer alguns efeitos colaterais. Por exemplo, um valor de tempo limite alto também estenderá o tempo que o thread principal está em um status de bloco. Esse status significa que o contêiner será menos responsivo às chamadas stop() e só poderá parar entre receive() chamadas.

Além disso, o contêiner só pode enviar solicitações depois que o intervalo de receive-timeout tiver passado. Se o intervalo for maior que 10 minutos, o Barramento de Serviço fechará o link e impedirá que o ouvinte envie ou receba. Para obter mais informações, consulte Link está fechado seção de erros amqp no barramento de serviço do Azure. Por padrão, o ouvinte usa um CachingConnectionFactory.

Se você precisar de um tempo limite de recebimento alto, use o JmsPoolConnectionFactory.

Para obter mais informações sobre o problema de fechamento do link e como usar JmsPoolConnectionFactory, consulte o O MessageProducer foi fechado devido a um erro irrecuperável seção.

Problema de pré-busca

Descrição do problema

Uma política de pré-busca inadequada pode causar os seguintes problemas:

  • As mesmas mensagens são consumidas repetidamente.
  • As mensagens são colocadas na fila de mensagens mortas após MaxDeliveryCountExceeded, mesmo quando as mensagens são processadas sem erro ou exceção.

Análise de causa

Esse problema geralmente acontece quando a pré-busca valor é maior do que a capacidade de consumo real, com o efeito de que muitas mensagens são pré-buscadas para o buffer local aguardando para serem consumidas. No entanto, as mensagens pré-buscadas são exibidas como despachadas em um modo de de de bloqueio de espiar do lado do Barramento de Serviço. Cada mensagem expedida tem um atributos de de contagem máxima de entrega e duração de bloqueio. No modo de recebimento de bloqueio de espiada, as mensagens buscadas no buffer de pré-busca são adquiridas no buffer em um estado bloqueado, com o relógio de tempo limite para o tique de bloqueio. Se o buffer de pré-busca for grande e o processamento levar tanto tempo que os bloqueios de mensagem expirarem enquanto estiverem no buffer de pré-busca, a mensagem será tratada como abandonada e será disponibilizada novamente para recuperação da fila.

Esse problema pode fazer com que a mensagem seja buscada no buffer de pré-busca e colocada no final. Se o buffer de pré-busca não for processado antes da expiração da mensagem, as mensagens serão repetidamente pré-buscadas, mas nunca efetivamente entregues em um estado utilizável (bloqueado validamente). Em seguida, quando essas cópias desatualizadas são desativadas, o aplicativo consome a mesma mensagem repetidamente e não é capaz de concluí-las. Em outro caso, as mensagens repetidas expiram no buffer antes de serem consumidas. Nesse caso, a mensagem no Barramento de Serviço será eventualmente movida para a fila de mensagens mortas depois que a contagem máxima de entrega for excedida.

Para obter mais informações, consulte o Por que Prefetch não é a opção padrão? seção de mensagens do Barramento de Serviço do Azure pré-busca.

Solução

Tenha cuidado com a configuração do pré-busca para garantir que ele se ajuste à funcionalidade de consumo. Você deve balancear a contagem máxima de pré-busca e a duração do bloqueio configurada na fila ou assinatura, de modo que o tempo limite de bloqueio pelo menos exceda o tempo de processamento de mensagem esperado cumulativo para o tamanho máximo do buffer de pré-busca, além de uma mensagem. Ao mesmo tempo, o tempo limite de bloqueio não deve demorar tanto que as mensagens possam exceder seu tempo máximo de vida ao serem descartadas acidentalmente, exigindo assim que seu bloqueio expire antes de ser resgatado.

Para configurar o atributo prefetch, que tem um valor padrão de zero, use uma das seguintes propriedades:

Propriedade Descrição
spring.jms.servicebus.prefetch.all O valor de fallback para a opção prefetch neste namespace do Barramento de Serviço.
spring.jms.servicebus.prefetch.queue-prefetch O número de pré-busca da fila.
spring.jms.servicebus.prefetch.queue-browser-prefetch O número de pré-busca do navegador da fila.
spring.jms.servicebus.prefetch.topic-prefetch O número de pré-busca do tópico.
spring.jms.servicebus.prefetch.durable-topic-prefetch O número de pré-busca do tópico durável.

Como executar a disposição AMQP no Barramento de Serviço?

O JMS dá suporte a cinco tipos de disposição AMQP ao reconhecer mensagens para o agente de mensagens. Os valores com suporte são ACCEPTED, REJECTED, RELEASED, MODIFIED_FAILEDe MODIFIED_FAILED_UNDELIVERABLE. Para obter mais informações, consulte a seção disposição amqp e mapeamento de operação do Barramento de Serviço seção de Usar o Serviço de Mensagens Java 1.1 com o Barramento de Serviço do Azure standard e o AMQP 1.0.

Portanto, para concluir manualmente, abandonar, enviar mensagens mortas, adiar ou liberar uma mensagem usando JmsListener, use as seguintes etapas:

  1. Desabilite a transação de sessão e use o modo de ack CLIENT.

    Para realizar essa tarefa, declare seu próprio JmsListenerContainerFactory bean e, em seguida, defina as propriedades ou poste o definido noinicial do . O exemplo a seguir usa a abordagem de declarar outro bean:

    @Configuration(proxyBeanMethods = false)
    public class CustomJmsConfiguration {
    
        @Bean
        public JmsListenerContainerFactory<?> customQueueJmsListenerContainerFactory(
                DefaultJmsListenerContainerFactoryConfigurer configurer, ConnectionFactory connectionFactory) {
            DefaultJmsListenerContainerFactory jmsListenerContainerFactory = new DefaultJmsListenerContainerFactory();
            configurer.configure(jmsListenerContainerFactory, connectionFactory);
            jmsListenerContainerFactory.setPubSubDomain(Boolean.FALSE);
            jmsListenerContainerFactory.setSessionTransacted(Boolean.FALSE);
            jmsListenerContainerFactory.setSessionAcknowledgeMode(Session.CLIENT_ACKNOWLEDGE);
            return jmsListenerContainerFactory;
        }
    }
    
  2. No manipulador de mensagens, conclua ou abandone mensagens explicitamente.

    @JmsListener(destination = "QUEUE_NAME", containerFactory = "customQueueJmsListenerContainerFactory")
    public void receiveMessage(JmsTextMessage message) throws Exception {
        String event = message.getBody(String.class);
        try {
            logger.info("Received event: {}", event);
            logger.info("Received message: {}", message);
            // by default complete the message
            message.acknowledge();
        } catch (Exception e) {
            logger.error("Exception while processing re-source event: " + event, e);
            JmsAcknowledgeCallback acknowledgeCallback = message.getAcknowledgeCallback();
            // explicitly abandon the message
            acknowledgeCallback.setAckType(MODIFIED_FAILED);
            message.setAcknowledgeCallback(acknowledgeCallback);
            message.acknowledge();
            throw e;
        }
    }
    

Problemas de configuração

Desabilitar a configuração automática do JMS do Barramento de Serviço

Descrição do problema

Alguns usuários importam um pouco do Spring Cloud Azure Starter para a configuração automática de um serviço do Azure que não seja o JMS do Barramento de Serviço. Eles também usam a estrutura do Spring JMS sem a necessidade de JMS do Barramento de Serviço. Em seguida, quando o aplicativo tenta iniciar, as seguintes exceções são geradas:

Caused by: java.lang.IllegalArgumentException: 'spring.jms.servicebus.connection-string' should be provided
    at com.azure.spring.cloud.autoconfigure.jms.properties.AzureServiceBusJmsProperties.afterPropertiesSet(AzureServiceBusJmsProperties.java:210)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800)
    ... 98 more

Análise de causa

Esse problema ocorre porque todas as classes de configuração automática do Spring Cloud Azure são colocadas no mesmo módulo, portanto, qualquer Spring Cloud Azure Starter realmente importa toda essa configuração automática, que também inclui o JMS do Barramento de Serviço. Em seguida, quando o aplicativo usa a API spring JMS, ele atende à condição de de configuração automática JMS do Barramento de Serviço e dispara-a. Em seguida, para os usuários que não pretendem usar spring-cloud-azure-starter-servicebus-jms, as condições de propriedade não são atendidas porque não há razão para que eles configurem o Barramento de Serviço para JMS. Essa situação faz com que as exceções sejam geradas.

Solução

O JMS do Spring Cloud Azure para Barramento de Serviço fornece uma propriedade para ativar ou desativar sua configuração automática. Você pode optar por desabilitar essa funcionalidade conforme necessário usando a seguinte configuração de propriedade:

spring.jms.servicebus.enabled=false

Configurar atributos de mensagem

Como definir o tipo de conteúdo de mensagens de saída?

Para configurar o tipo de conteúdo, personalize o Conversor de Mensagens para modificar o atributo de tipo de conteúdo ao converter mensagens. O código a seguir usa mensagens de byte como exemplo.

Primeiro, personalize o conversor de mensagens a ser usado no JmsTemplate, conforme mostrado no exemplo a seguir:

public class CustomMappingJackson2MessageConverter extends MappingJackson2MessageConverter {

    public static final String CONTENT_TYPE = "application/json";

    public CustomMappingJackson2MessageConverter() {
        this.setTargetType(MessageType.BYTES);
    }

    @Override
    protected BytesMessage mapToBytesMessage(Object object, Session session, ObjectWriter objectWriter)
            throws JMSException, IOException {
        final BytesMessage message = super.mapToBytesMessage(object, session, objectWriter);
        JmsBytesMessage msg = (JmsBytesMessage) message;
        AmqpJmsMessageFacade facade = (AmqpJmsMessageFacade) msg.getFacade();
        facade.setContentType(Symbol.valueOf(CONTENT_TYPE));
        return msg;
    }
}

Em seguida, declare o bean do conversor de mensagens personalizado, conforme mostrado neste exemplo:

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

    @Bean
    public MessageConverter messageConverter() {
        return new CustomMappingJackson2MessageConverter();
    }
}

Como definir o nome da propriedade ID do tipo para MappingJackson2MessageConverter?

O atributo type-id-property-name permite que o MappingJackson2MessageConverter determine qual classe usar para desserializar o conteúdo da mensagem. Ao serializar cada objeto Java para um conteúdo do Spring Message, o conversor armazena o tipo de conteúdo em uma propriedade de mensagem com o nome da propriedade registrado por type-id-property-name. Em seguida, ao desserializar a mensagem, o conversor lê a ID do tipo da mensagem e executa a desserialização.

Para definir o type-id-property-name, declare seu próprio MappingJackson2MessageConverter bean e configure essa propriedade, conforme mostrado no exemplo a seguir:

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

    @Bean
    public MessageConverter jacksonJmsMessageConverter()
    {
        MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
        converter.setTypeIdPropertyName("your-custom-type-id-property-name");
        return converter;
    }
}

Detecção duplicada

O Barramento de Serviço do Azure dá suporte a de detecção duplicada, que aplica a propriedade MessageId para identificar mensagens exclusivamente e descartar as duplicatas enviadas ao Barramento de Serviço.

No entanto, para a API JMS, você não deve definir a ID da mensagem JMS, que é considerada ilegal em especificações JMS. Portanto, esse recurso não tem suporte no momento para o Spring Cloud Azure Service Bus JMS Starter.

Para obter mais atualizações para esse recurso, consulte o problema de do GitHub.

Habilitar o registro em log de transporte AMQP

Para obter mais informações, consulte o habilitar o registro em log de transporte AMQP seção de solução de problemas do Barramento de Serviço.

Obter ajuda adicional

Para obter mais informações sobre maneiras de entrar em contato com o suporte, consulte suporte na raiz do repositório.

Recursos para o início do JMS do Barramento de Serviço do Azure no Spring Cloud

Arquivar problemas do GitHub

Ao apresentar problemas do GitHub, os seguintes detalhes são solicitados:

  • Configuração do Barramento de Serviço/Ambiente de Namespace
    • Qual camada é o namespace (standard ou premium)?
    • Que tipo de entidade de mensagens está sendo usada (fila ou tópico)? e sua configuração.
    • Qual é o tamanho médio de cada Mensagem?
  • Como é o padrão de tráfego? (ou seja, o número de mensagens por minuto e se o Cliente está sempre ocupado ou tem períodos de tráfego lentos).)
  • Repro code and steps
    • Isso é importante, pois muitas vezes não podemos reproduzir o problema em nosso ambiente.
  • Logs