Condividi tramite


Guida alla risoluzione dei problemi di Spring JMS

Questo articolo descrive come risolvere i problemi noti e gli errori comuni quando si usa Spring JMS. L'articolo risponde anche ad alcune domande frequenti per spring-cloud-azure-starter-servicebus-jms.

Problemi di connettività

MessageProducer è stato chiuso a causa di un errore irreversibile

Descrizione del problema

Quando si usa JmsTemplate per inviare messaggi, JmsTemplate diventa non disponibile durante un intervallo di inattività compreso tra 10 e 15 minuti. L'invio di messaggi in tale intervallo può ottenere le eccezioni illustrate nell'output di esempio seguente:

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

Analisi delle cause

Le eccezioni si verificano per bus di servizio di Azure quando la connessione e il collegamento AMQP sono attivi, ma non vengono effettuate chiamate, ad esempio l'invio o la ricezione, usando il collegamento per 10 minuti. In questo caso, il collegamento viene chiuso. Quando tutti i collegamenti nella connessione sono stati chiusi perché non è stata eseguita alcuna attività (inattiva) e non è stato creato un nuovo collegamento in 5 minuti, la connessione viene chiusa.

Per l'avvio JMS del bus di servizio, il CachingConnectionFactory viene usato per impostazione predefinita, che memorizza nella cache la sessione, il producer e il consumer. Quando la JmsProducer è inattiva per più di 10 minuti ma inferiore a 15, il collegamento occupato dal producer memorizzato nella cache è stato chiuso. I messaggi non possono essere inviati durante questo intervallo. Quindi, dopo altri 5 minuti di inattività, l'intera connessione viene chiusa. Pertanto, qualsiasi operazione di invio dopo l'intervallo di inattività di 15 minuti fa sì che il CachingConnectionFactory crei una nuova connessione da inviare. L'operazione di invio diventa disponibile dopo 15 minuti.

Soluzione alternativa

Attualmente, lo starter offre una soluzione alternativa per il problema di scollegamento del collegamento applicando il JmsPoolConnectionFactory, che raggruppa Connection, Sessione MessageProducere gestisce il ciclo di vita delle istanze in pool. Questa soluzione alternativa può garantire che un producer venga rimosso dopo essere stato non disponibile e quindi tutte le operazioni di invio vengano eseguite sui producer attivi.

Per usare questa soluzione alternativa, aggiungere la configurazione seguente:

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

Utilizzo di spring.jms.servicebus.idle-timeout

Le proprietà di inattività-timeout configurano il timeout di inattività di una connessione AMQP. La specifica AMQP fornisce la descrizione seguente:

Le connessioni sono soggette a una soglia di timeout di inattività. Il timeout viene attivato da un peer locale quando non viene ricevuto alcun frame dopo il superamento di un valore soglia. Il timeout di inattività viene misurato in millisecondi e inizia dal momento in cui viene ricevuto l'ultimo fotogramma. Se la soglia viene superata, un peer DOVREBBE provare a chiudere normalmente la connessione usando un frame di chiusura con un errore che spiega perché. Se il peer remoto non risponde correttamente entro una soglia, il peer POTREBBE chiudere il socket TCP.

Per un client JMS, quando si configura questa proprietà, si controlla sul lato server per quanto tempo si prevede che il server invii un frame vuoto per mantenere attiva una connessione quando non vengono recapitati messaggi. Questa proprietà controlla il comportamento del peer remoto e ogni peer può avere un proprio valore isolato.

Problemi relativi a JmsTemplate

Messaggi pianificati

Il bus di servizio di Azure supporta l'elaborazione ritardata dei messaggi. Per altre informazioni, vedere la sezione messaggi pianificati di sequenziazione dei messaggi e timestamp. Per JMS, per pianificare un messaggio, impostare la proprietà ScheduledEnqueueTimeUtc utilizzando l'intestazione dell'annotazione del messaggio x-opt-scheduled-enqueue-time.

Problemi di JmsListener

Troppe richieste vengono inviate al bus di servizio anche se non sono presenti messaggi nel server

Descrizione del problema

Quando si usa l'API @JmsListener, in alcuni casi è possibile vedere nel portale di Azure che esistono valori in corso per le richieste in ingresso inviate alla coda o agli argomenti, anche se non sono presenti messaggi nel server da ricevere.

Analisi delle cause

@JmsListener è un listener di polling, creato per i tentativi di polling ripetuti.

Il listener si trova in un ciclo di polling in corso. Ogni ciclo chiama il metodo di MessageConsumer.receive() JMS per eseguire il polling del consumer locale affinché i messaggi vengano utilizzati. Per impostazione predefinita, per ogni operazione di polling, il consumer locale invia richieste pull al broker di messaggi per richiedere messaggi e quindi blocca per un determinato periodo di tempo. Il processo di polling concreto viene deciso da diverse proprietà, tra cui receiveTimeout, prefetchSizee receiveLocalOnly o receiveNoWaitLocalOnly. Il metodo receiveNoWaitLocalOnly viene utilizzato solo quando si imposta receiveTimeout su un valore negativo.

Quando questo problema si verifica nell'applicazione, controllare le impostazioni di configurazione seguenti:

  • Determinare se il criterio di prelettura è 0, che è anche l'opzione predefinita. Prelettura 0 indica un consumer di pull che invia richieste pull al bus di servizio per ogni polling.

  • Se è stato configurato un prelettura diverso da zero, determinare se la proprietà receiveLocalOnly o receiveNoWaitLocalOnly è impostata su false, ovvero l'opzione predefinita. Un valore false in questo caso comporta comunque l'invio di richieste pull al server perché non esegue solo il polling del consumer locale.

  • La configurazione receiveTimeout determina per quanto tempo si blocca per ogni richiesta pull, in modo che possa influire sulla frequenza delle richieste pull inviate al server. Il valore predefinito è 1 secondo.

Per un'analisi completa, vedere la discussione nel problema di GitHub.

Soluzioni

Le sezioni seguenti descrivono due soluzioni per la gestione di questo problema

Soluzione 1. Passare al push del consumer e del solo controllo locale

Modificando la modalità in push, il consumer diventa un notifica asincrona consumer che non esegue il pull dei messaggi dal broker, ma mantiene un credito di collegamento di destinazione. L'importo viene determinato da una proprietà di prelettura. Quando il bus di servizio (mittente) esegue il push dei messaggi, il credito di collegamento del mittente diminuisce e quando il credito di collegamento del mittente scende al di sotto di una soglia, il client (destinatario) invia una richiesta al server per aumentare il credito di collegamento del mittente all'importo di destinazione desiderato.

Per eseguire questa soluzione, aggiungere la configurazione seguente:

Prima di tutto, configurare il numero prefetch come diverso da zero, che configura il consumer come non pull. Nella tabella seguente vengono illustrate diverse proprietà di prelettura, ognuna delle quali controlla diverse entità del bus di servizio. Impostare le proprietà applicabili al caso.

Proprietà Descrizione
spring.jms.servicebus.prefetch.all Valore di fallback per l'opzione di prelettura in questo spazio dei nomi del bus di servizio
spring.jms.servicebus.prefetch.queue-prefetch Numero di prelettura per la coda.
spring.jms.servicebus.prefetch.queue-browser-prefetch Numero di prelettura per il browser di accodamento.
spring.jms.servicebus.prefetch.topic-prefetch Numero di prelettura per l'argomento.
spring.jms.servicebus.prefetch.durable-topic-prefetch Numero di prelettura per l'argomento durevole.

In secondo luogo, configurare il non-local-check aggiungendo una classe di configurazione per il personalizzatore di factory, come illustrato nell'esempio seguente:

@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);
        };
    }
}

Il valore di prelettura può influire sulla velocità di invio dei messaggi al buffer locale del consumer. È consigliabile modificare il valore in base ai volumi di prestazioni e messaggi che utilizzano. Un valore appropriato può velocizzare il processo di utilizzo, mentre un valore troppo grande può causare l'aggiornamento e l'invio dei messaggi memorizzati nel buffer locale. Per i volumi di messaggi bassi, in cui ogni messaggio richiede molto tempo per l'elaborazione, impostare il prelettura su 1. Questo valore garantisce che un consumer stia elaborando un solo messaggio alla volta.

Soluzione 2. Aumentare il timeout di ricezione per ridurre la frequenza di pull

La proprietà di timeout di ricezione determina la strategia per quanto tempo il consumer blocca l'attesa di un risultato pull. Estendendo quindi il timeout, è possibile ridurre la frequenza di pull, quindi ridurre il numero di richieste pull quando si sceglie la modalità pull. In casi estremi, è possibile impostare la strategia per l'attesa illimitata fino all'arrivo di un messaggio, il che significa che il consumer esegue il pull solo dopo l'utilizzo di un messaggio. In questo caso, quando non sono presenti messaggi nel server, verrà bloccato per l'attesa.

Per eseguire questa soluzione, configurare la proprietà spring.jms.listener.receive-timeout. Questa proprietà è di tipo java.time.Duration e ha un valore predefinito di 1 secondo. L'elenco seguente illustra l'effetto di vari valori:

  • L'impostazione del timeout di ricezione su 0 indica che i blocchi pull indefiniti fino a quando non viene inviato un messaggio.
  • L'impostazione del timeout di ricezione su un valore positivo indica che il pull si blocca fino alla quantità di tempo di timeout.
  • L'impostazione del timeout di ricezione su un valore negativo indica che il pull è una ricezione senza attesa, ovvero restituisce immediatamente un messaggio o null se non sono disponibili messaggi.

Nota

Un valore di timeout elevato può portare alcuni effetti collaterali. Ad esempio, un valore di timeout elevato estenderà anche il tempo in cui il thread principale si trova in uno stato di blocco. Questo stato indica che il contenitore sarà meno reattivo alle chiamate stop() e può interrompersi solo tra receive() chiamate.

Inoltre, il contenitore può inviare richieste solo dopo il superamento dell'intervallo di receive-timeout. Se l'intervallo è superiore a 10 minuti, il bus di servizio chiuderà il collegamento e impedirà al listener di inviare o ricevere. Per altre informazioni, vedere la sezione Link is closed di errori AMQP nel bus di servizio di Azure. Per impostazione predefinita, il listener usa un CachingConnectionFactory.

Se è necessario un timeout di ricezione elevato, assicurarsi di usare il JmsPoolConnectionFactory.

Per altre informazioni sul problema di chiusura dei collegamenti e su come usare JmsPoolConnectionFactory, vedere la sezione MessageProducer è stata chiusa a causa di un errore irreversibile.

Problema di prelettura

Descrizione del problema

Un criterio di prelettura non adatto può causare i problemi seguenti:

  • Gli stessi messaggi vengono utilizzati ripetutamente.
  • I messaggi vengono inseriti nella coda dei messaggi non recapitabili dopo MaxDeliveryCountExceeded, anche quando i messaggi vengono elaborati senza errori o eccezioni.

Analisi delle cause

Questo problema si verifica in genere quando il valore di prelettura è superiore alla capacità di consumo effettiva, con l'effetto che troppi messaggi vengono preletati nel buffer locale in attesa di essere utilizzati. Tuttavia, i messaggi prelettura vengono visualizzati come inviati in una modalità di visualizzazione del blocco dal lato bus di servizio. Ogni messaggio inviato ha un numero massimo di recapito e gli attributi di durata del blocco. Nella modalità di ricezione peek-lock i messaggi recuperati nel buffer di prelettura vengono acquisiti nel buffer in uno stato bloccato, con l'orologio di timeout per il tick del blocco. Se il buffer di prelettura è di grandi dimensioni e l'elaborazione richiede così tanto tempo che i blocchi dei messaggi scadono mentre rimangono nel buffer di prelettura, il messaggio viene considerato abbandonato e viene nuovamente reso disponibile per il recupero dalla coda.

Questo problema potrebbe causare il recupero del messaggio nel buffer di prelettura e posizionato alla fine. Se il buffer di prelettura non viene elaborato prima della scadenza del messaggio, i messaggi vengono prelettura ripetutamente ma mai recapitati in modo efficace in uno stato utilizzabile (valido bloccato). Quindi, quando tali copie obsolete vengono rimosse dalla coda, l'applicazione usa ripetutamente lo stesso messaggio e non è in grado di completarle. In un altro caso, i messaggi ripetuti sono tutti scaduti nel buffer prima di essere utilizzati. In questo caso, il messaggio nel bus di servizio verrà infine spostato nella coda dei messaggi non recapitabili dopo il superamento del numero massimo di recapito.

Per altre informazioni, vedere il Perché prelettura non è l'opzione predefinita? sezione di prelettura dei messaggi del bus di servizio di Azure.

Soluzione

Prestare attenzione alla configurazione del prelettura per assicurarsi che si adatti alla funzionalità di utilizzo. È necessario bilanciare il numero massimo di prelettura e la durata del blocco configurata nella coda o nella sottoscrizione in modo che il timeout di blocco superi almeno il tempo di elaborazione cumulativo previsto per le dimensioni massime del buffer di prelettura, più un messaggio. Allo stesso tempo, il timeout di blocco non deve essere così lungo che i messaggi possono superare il tempo massimo di vita quando vengono accidentalmente eliminati, richiedendo così che il blocco scada prima di essere recapitato nuovamente.

Per configurare l'attributo di prelettura, che ha un valore predefinito pari a zero, utilizzare una delle proprietà seguenti:

Proprietà Descrizione
spring.jms.servicebus.prefetch.all Valore di fallback per l'opzione di prelettura in questo spazio dei nomi del bus di servizio.
spring.jms.servicebus.prefetch.queue-prefetch Numero di prelettura per la coda.
spring.jms.servicebus.prefetch.queue-browser-prefetch Numero di prelettura per il browser di accodamento.
spring.jms.servicebus.prefetch.topic-prefetch Numero di prelettura per l'argomento.
spring.jms.servicebus.prefetch.durable-topic-prefetch Numero di prelettura per l'argomento durevole.

Come eseguire l'eliminazione AMQP nel bus di servizio?

JMS supporta cinque tipi di eliminazione AMQP quando si riconoscono i messaggi nel broker di messaggistica. I valori supportati sono ACCEPTED, REJECTED, RELEASED, MODIFIED_FAILEDe MODIFIED_FAILED_UNDELIVERABLE. Per altre informazioni, vedere la sezione disposizione AMQP e mapping delle operazioni del bus di servizio di Usare Java Message Service 1.1 con il bus di servizio di Azure standard e AMQP 1.0.

Quindi, per completare manualmente, abbandonare, inviare o rilasciare un messaggio usando JmsListener, seguire questa procedura:

  1. Disabilitare le transazioni di sessione e usare la modalità Ack CLIENT.

    Per eseguire questa attività, dichiarare il proprio JmsListenerContainerFactory bean e quindi impostare le proprietà oppure pubblicare il definito nellainiziale . Nell'esempio seguente viene usato l'approccio per dichiarare un altro 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. Nel gestore messaggi completare o abbandonare in modo esplicito i messaggi.

    @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;
        }
    }
    

Problemi di configurazione

Disabilitare la configurazione automatica JMS del bus di servizio

Descrizione del problema

Alcuni utenti importano spring cloud Azure Starter per la configurazione automatica di un servizio di Azure diverso da JMS del bus di servizio. Usano anche il framework Spring JMS senza la necessità di JMS del bus di servizio. Quindi, quando l'applicazione tenta di avviare, vengono generate le eccezioni seguenti:

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

Analisi delle cause

Questo problema si verifica perché tutte le classi di configurazione automatica di Spring Cloud di Azure vengono inserite nello stesso modulo, quindi qualsiasi avvio di Azure Spring Cloud importa effettivamente tutta la configurazione automatica, che include anche JMS del bus di servizio. Quindi, quando l'applicazione usa l'API Spring JMS, soddisfa la condizione di configurazione automatica JMS del bus di servizio e la attiva. Quindi, per gli utenti che non intendono usare spring-cloud-azure-starter-servicebus-jms, le condizioni delle proprietà non vengono soddisfatte perché non esistono motivi per configurare il bus di servizio per JMS. Questa situazione causa la generazione delle eccezioni.

Soluzione

Spring Cloud Azure per JMS del bus di servizio fornisce una proprietà per attivare o disattivare la configurazione automatica. È possibile scegliere di disabilitare questa funzionalità in base alle esigenze usando l'impostazione della proprietà seguente:

spring.jms.servicebus.enabled=false

Configurare gli attributi dei messaggi

Come impostare il tipo di contenuto dei messaggi in uscita?

Per configurare il tipo di contenuto, personalizzare Message Converter per modificare l'attributo content-type durante la conversione dei messaggi. Il codice seguente accetta i messaggi di byte come esempio.

Prima di tutto, personalizzare il convertitore di messaggi da usare nella JmsTemplate, come illustrato nell'esempio seguente:

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

Dichiarare quindi il bean del convertitore di messaggi personalizzato, come illustrato in questo esempio:

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

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

Come impostare il nome della proprietà ID di tipo per MappingJackson2MessageConverter?

L'attributo type-id-property-name consente al MappingJackson2MessageConverter di determinare quale classe utilizzare per deserializzare il payload del messaggio. Quando si serializza ogni oggetto Java in un payload Spring Message, il convertitore archivia il tipo di payload in una proprietà del messaggio con il nome della proprietà registrato da type-id-property-name. Quindi, durante la deserializzazione del messaggio, il convertitore legge l'ID del tipo dal messaggio ed esegue la deserializzazione.

Per impostare il type-id-property-name, dichiarare il proprio MappingJackson2MessageConverter bean e configurare tale proprietà, come illustrato nell'esempio seguente:

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

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

Rilevamento duplicati

Il bus di servizio di Azure supporta rilevamento duplicati, che applica la proprietà MessageId per identificare in modo univoco i messaggi ed eliminare i duplicati inviati al bus di servizio.

Tuttavia, per l'API JMS, non è consigliabile impostare l'ID messaggio JMS, considerato illegale nelle specifiche JMS. Questa funzionalità non è quindi attualmente supportata per Spring Cloud Service Bus JMS Starter.

Per altri aggiornamenti per questa funzionalità, vedere il problema di GitHub .

Abilitare la registrazione del trasporto AMQP

Per altre informazioni, vedere la sezione abilitare la registrazione del trasporto AMQP di Risoluzione dei problemi del bus di servizio.

Ottenere assistenza aggiuntiva

Per altre informazioni sui modi per contattare il supporto, vedere supporto nella radice del repository.

Risorse per Spring Cloud Azure Service Bus JMS starter

Risoluzione dei problemi di GitHub

Quando si archiviano problemi di GitHub, vengono richiesti i dettagli seguenti:

  • Configurazione del bus di servizio/Ambiente dello spazio dei nomi
    • Quale livello è lo spazio dei nomi (standard o Premium)?
    • Quale tipo di entità di messaggistica viene usato (coda o argomento)? e la relativa configurazione.
    • Qual è la dimensione media di ogni messaggio?
  • Qual è il modello di traffico? Ovvero, il numero di messaggi al minuto e se il client è sempre occupato o ha periodi di traffico lenti.
  • Ripetere il codice e i passaggi
    • Questo aspetto è importante perché spesso non è possibile riprodurre il problema nell'ambiente.
  • Registri