Felsökningsguide för Spring JMS

Den här artikeln beskriver hur du felsöker kända problem och vanliga fel när du använder Spring JMS. Artikeln besvarar också några vanliga frågor för spring-cloud-azure-starter-servicebus-jms.

Anslutningsproblem

MessageProducer stängdes på grund av ett oåterkalleligt fel

Problembeskrivning

När du använder JmsTemplate för att skicka meddelanden blir JmsTemplate otillgängliga under ett inaktivt intervall mellan 10 och 15 minuter. Om du skickar meddelanden i det intervallet kan du få de undantag som visas i följande exempelutdata:

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

Orsaksanalys

Undantagen inträffar för Azure Service Bus- när AMQP-anslutningen och länken är aktiva, men inga anrop – till exempel skicka eller ta emot anrop – görs med hjälp av länken i 10 minuter. I det här fallet är länken stängd. Och när alla länkar i anslutningen har stängts på grund av att det inte fanns någon aktivitet (inaktiv) och en ny länk inte har skapats på 5 minuter stängs anslutningen.

För Service Bus JMS-startprogrammet används CachingConnectionFactory- som standard, vilket cachelagrar sessionen, producenten och konsumenten. När JmsProducer har varit inaktiv i mer än 10 minuter men mindre än 15 minuter har länken som den cachelagrade producenten använder stängts. Meddelanden kan inte skickas ut under det här intervallet. Sedan, efter ytterligare 5 minuters inaktivitet, stängs hela anslutningen. En sändningsåtgärd efter det 15 minuter långa inaktivitetsintervallet gör därför att CachingConnectionFactory skapar en ny anslutning som ska skickas. Sändningsåtgärden blir tillgänglig efter 15 minuter.

Lösning

För närvarande tillhandahåller startmallen en temporär lösning på problemet med att länken kopplas från genom att använda JmsPoolConnectionFactory, som samlar Connection, Session och MessageProducer i en pool och hanterar de poolade instansernas livscykel. Den här lösningen kan säkerställa att en producent avlägsnas när den är otillgänglig och därför utförs alla sändningsåtgärder på aktiva producenter.

Om du vill använda den här lösningen lägger du till följande konfiguration:

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

Användning av spring.jms.servicebus.idle-timeout

Egenskaperna för tidsgräns för inaktivitet konfigurerar tidsgränsen för inaktivitet för en AMQP-anslutning. AMQP-specifikationen innehåller följande beskrivning:

Anslutningar omfattas av ett tröskelvärde för tidsgräns för inaktivitet. Timeouten utlöses av en lokal nod när inga ramar har tagits emot och ett tröskelvärde har överskridits. Tidsgränsen för inaktivitet mäts i millisekunder och startar från den tidpunkt då den sista ramen tas emot. Om tröskelvärdet överskrids bör en part försöka stänga anslutningen på ett korrekt sätt med hjälp av en stängningsram med ett felmeddelande som förklarar varför. Om den fjärranslutna motparten inte svarar på ett korrekt sätt inom den angivna tidsgränsen, får motparten stänga TCP-socketen.

När du konfigurerar den här egenskapen för en JMS-klient styr du på serversidan hur länge du förväntar dig att servern ska skicka en tom ram för att hålla anslutningen vid liv när inga meddelanden levereras. Den här egenskapen styr fjärrpeerens beteende, och varje peer kan ha ett eget, isolerat värde.

JmsTemplate-problem

Schemalagda meddelanden

Azure Service Bus stöder fördröjd meddelandebearbetning. Mer information finns i avsnittet Schemalagda meddelanden i Meddelandesekvensering och tidsstämplar. Om du vill schemalägga ett meddelande för JMS anger du egenskapen ScheduledEnqueueTimeUtc med hjälp av meddelandeanteckningsrubriken x-opt-scheduled-enqueue-time.

JmsListener-problem

För många begäranden skickas till Service Bus trots att det inte finns några meddelanden på servern

Problembeskrivning

När du använder @JmsListener API kan du i vissa fall se i Azure-portalen att det finns pågående värden för inkommande begäranden som skickas till deras kö eller ämnen, även om det inte finns några meddelanden på servern att ta emot.

Orsaksanalys

@JmsListener är en pollningsavlyssnare som är utformad för upprepade pollningsförsök.

Lyssnaren sitter i en pågående avsökningsloop. Varje loop anropar metoden JMS MessageConsumer.receive() för att avsöka den lokala konsumenten efter meddelanden som ska användas. För varje avsökningsåtgärd skickar den lokala konsumenten som standard pull-begäranden till meddelandekoordinatorn för att be om meddelanden och blockerar sedan under en viss tidsperiod. Den konkreta pollningsprocessen bestäms av flera egenskaper, inklusive receiveTimeout, prefetchSize och receiveLocalOnly eller receiveNoWaitLocalOnly. Metoden receiveNoWaitLocalOnly används bara när du anger receiveTimeout till ett negativt värde.

När det här problemet uppstår för ditt program kontrollerar du följande konfigurationsinställningar:

  • Avgör om din prefetch-princip är 0, vilket också är standardalternativet. 0-prefetch innebär en pull-konsument som skickar pull-begäranden till Service Bus för varje omröstning.

  • Om du har konfigurerat prefetch som inte är noll avgör du om egenskapen receiveLocalOnly eller receiveNoWaitLocalOnly är inställd på false, vilket är standardalternativet. Ett false värde här resulterar fortfarande i att pull-begäranden skickas till servern eftersom det inte bara avsöker den lokala konsumenten.

  • Konfigurationen receiveTimeout avgör hur länge den blockerar vid varje hämtningsbegäran, så den kan därför påverka hur ofta hämtningsbegäranden skickas till servern. Standardvärdet är 1 sekund.

En fullständig analys finns i diskussionen i GitHub-problem.

Lösningar

I följande avsnitt beskrivs två lösningar för att hantera det här problemet

Lösning 1. Ändra till endast push-användare och lokal kontroll

Genom att ändra läget till push blir konsumenten en Asynchronous Notification-konsument som inte hämtar meddelanden från förmedlaren, utan upprätthåller en viss mängd länkkredit. Beloppet bestäms av en prefetch-egenskap. När Service Bus (avsändaren) skickar meddelanden minskar avsändarens länkkredit, och när avsändarens länkkredit understiger ett tröskelvärde skickar klienten (mottagaren) en begäran till servern för att öka avsändarens länkkredit tillbaka till önskat målbelopp.

Lägg till följande konfiguration för att åstadkomma den här lösningen:

Konfigurera först prefetch-värdet så att det inte är noll, vilket konfigurerar konsumenten så att den inte använder pull. I följande tabell visas flera prefetch-egenskaper, som var och en styr olika Service Bus-entiteter. Ange de egenskaper som gäller för ditt ärende.

Egenskap Beskrivning
spring.jms.servicebus.prefetch.all Reservvärdet för alternativet prefetch i det här Service Bus-namnområdet
spring.jms.servicebus.prefetch.queue-prefetch Prefetch-numret för kön.
spring.jms.servicebus.prefetch.queue-browser-prefetch Prefetch-numret för köwebbläsaren.
spring.jms.servicebus.prefetch.topic-prefetch Prefetch-numret för ämnet.
spring.jms.servicebus.prefetch.durable-topic-prefetch Prefetch-numret för det hållbara ämnet.

För det andra konfigurerar du non-local-check genom att lägga till en konfigurationsklass för fabriksanpassaren, som du ser i följande exempel:

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

Prefetch-värdet kan påverka hur snabbt meddelanden skickas till konsumentens lokala buffert. Du bör justera värdet enligt dina förbrukande prestanda- och meddelandevolymer. Ett lämpligt värde kan påskynda användningsprocessen, medan ett värde som är för stort kan göra att de lokalt buffrade meddelandena blir inaktuella och skickas igen. För låga meddelandevolymer, där varje meddelande tar lång tid att bearbeta, anger du prefetch till 1. Det här värdet säkerställer att en konsument endast bearbetar ett meddelande i taget.

Lösning 2. Öka tidsgränsen för mottagningen för att minska pull-frekvensen

Egenskapen för tidsgräns för mottagning anger strategin för hur länge konsumenten blockeras i väntan på resultatet av en pull-begäran. Genom att utöka tidsgränsen kan du minska hämtningsfrekvensen och sedan minska antalet pull-begäranden när du väljer pull-läge. I extrema fall kan du ange en strategi för att vänta på obestämd tid tills ett meddelande anländer, vilket innebär att konsumenten endast hämtar ett nytt meddelande efter att ha konsumerat det föregående. I det här fallet, när det inte finns några meddelanden på servern, blockeras den i väntan.

Konfigurera egenskapen spring.jms.listener.receive-timeout för att åstadkomma den här lösningen. Den här egenskapen är av typen java.time.Duration och har ett standardvärde på 1 sekund. I följande lista förklaras effekten av olika värden:

  • Om du ställer in tidsgränsen för mottagning till 0 innebär det att pull-blocken är obegränsade tills ett meddelande skickas.
  • Att ställa in timeout för mottagning till ett positivt värde innebär att hämtningen blockeras i upp till den angivna timeouttiden.
  • Att ange timeout-värdet för mottagning till ett negativt värde innebär att pull-begäran blir en mottagning utan väntan, vilket innebär att den returnerar ett meddelande omedelbart, eller null om inga meddelanden finns tillgängliga.

Obs!

Ett högt timeout-värde kan ge vissa biverkningar. Till exempel förlänger ett högt timeout-värde även tiden som huvudtråden har blockstatus. Den här statusen innebär att containern är mindre dynamisk för stop()-anrop och kan bara stoppa mellan receive()-anrop.

Containern kan också bara skicka begäranden efter att receive-timeout-intervallet har passerat. Om intervallet är längre än 10 minuter stänger Service Bus länken och förhindrar att lyssnaren skickar eller tar emot. Mer information finns i avsnittet Link är stängt i AMQP-fel i Azure Service Bus. Lyssnaren använder som standard en CachingConnectionFactory.

Om du behöver en hög tidsgräns för mottagning ska du använda JmsPoolConnectionFactory.

Mer information om problemet med länkstängning och hur du använder JmsPoolConnectionFactoryfinns i avsnittet MessageProducer stängdes på grund av ett oåterkalleligt fel.

Problem med förhämtning

Problembeskrivning

En olämplig prefetch-princip kan orsaka följande problem:

  • Samma meddelanden används upprepade gånger.
  • Meddelanden läggs i dead letter-kön efter MaxDeliveryCountExceeded, även när meddelanden bearbetas utan fel eller undantag.

Orsaksanalys

Det här problemet uppstår vanligtvis när prefetch värdet är högre än den faktiska förbrukande kapaciteten, vilket innebär att för många meddelanden är förinställda till den lokala bufferten som väntar på att förbrukas. De förhämtade meddelandena betraktas dock som skickade i läget peek-lock på Service Bus-sidan. Varje skickat meddelande har attributen max-delivery-count och lock-duration. I peek-lock ta emot-läget tas meddelanden som hämtas till prefetchbufferten emot där i låst tillstånd, samtidigt som tidsgränsen för låset börjar räknas ned. Om prefetch-bufferten är stor och bearbetningen tar så lång tid att meddelandelåsen upphör att gälla medan det finns kvar i prefetch-bufferten behandlas meddelandet som övergivet och görs återigen tillgängligt för hämtning från kön.

Det här problemet kan leda till att meddelandet hämtas till prefetch-bufferten och placeras i slutet. Om prefetch-bufferten inte bearbetas innan meddelandet löper ut, hämtas meddelanden upprepade gånger i förväg men levereras aldrig i ett användbart tillstånd (med ett giltigt lås). När de inaktuella kopiorna sedan tas bort från kön bearbetar programmet samma meddelande om och om igen och kan inte slutföra bearbetningen av dem. I ett annat fall upphör upprepade meddelanden att gälla i bufferten innan de förbrukas. I det här fallet flyttas meddelandet i Service Bus så småningom till kön med obeställbara meddelanden när det maximala leveransantalet har överskridits.

Mer information finns i Varför är Prefetch inte standardalternativet? avsnitt i Prefetch Azure Service Bus-meddelanden.

Lösning

Var försiktig med konfigurationen av prefetch för att säkerställa att den passar den förbrukande funktionen. Du måste balansera det maximala antalet förhämtade meddelanden och den låstid som konfigurerats för kön eller prenumerationen, så att låsets tidsgräns åtminstone överstiger den sammanlagda förväntade bearbetningstiden för meddelandena vid den maximala storleken på förhämtningsbufferten, plus ett meddelande. Samtidigt bör tidsgränsen för låset inte vara så lång att meddelanden kan överskrida sin maximala tid att leva när de av misstag tas bort, vilket kräver att låset upphör att gälla innan de levereras på nytt.

Om du vill konfigurera prefetch-attributet, som har ett standardvärde på noll, använder du någon av följande egenskaper:

Egenskap Beskrivning
spring.jms.servicebus.prefetch.all Reservvärdet för prefetch-alternativet i det här Service Bus-namnområdet.
spring.jms.servicebus.prefetch.queue-prefetch Prefetch-numret för kön.
spring.jms.servicebus.prefetch.queue-browser-prefetch Prefetch-numret för köwebbläsaren.
spring.jms.servicebus.prefetch.topic-prefetch Prefetch-numret för ämnet.
spring.jms.servicebus.prefetch.durable-topic-prefetch Prefetch-värdet för det varaktiga ämnet.

Hur utför jag en AMQP-disposition för Service Bus?

JMS stöder fem AMQP-dispositionstyper när meddelanden bekräftas till meddelandeförmedlaren. De värden som stöds är ACCEPTED, REJECTED, RELEASED, MODIFIED_FAILEDoch MODIFIED_FAILED_UNDELIVERABLE. Mer information finns i avsnittet AMQP-borttagning och Service Bus-åtgärdsmappning i Använda Java Message Service 1.1 med Azure Service Bus Standard och AMQP 1.0.

Använd följande steg för att manuellt slutföra, överge, skjuta upp eller släppa ett meddelande med hjälp av JmsListener:

  1. Inaktivera transaktionshantering för sessioner och använd CLIENT_ACK-läge.

    För att göra detta deklarerar du antingen din egen JmsListenerContainerFactory-bean och anger sedan dess egenskaper, eller efterbearbetar JmsListenerContainerFactory som definierats i starter. I följande exempel används metoden för att deklarera en annan böna:

    @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. Slutför eller överge meddelanden explicit i meddelandehanteraren.

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

Konfigurationsproblem

Inaktivera automatisk konfiguration av Service Bus JMS

Problembeskrivning

Vissa användare importerar vissa Spring Cloud Azure Starter för automatisk konfiguration av en annan Azure-tjänst än Service Bus JMS. De använder också Spring JMS-ramverket utan behov av Service Bus JMS. När programmet sedan försöker starta genereras följande undantag:

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

Orsaksanalys

Det här problemet beror på att alla azure-autokonfigurationsklasser för Spring Cloud placeras i samma modul, så alla Spring Cloud Azure Starter importerar faktiskt all den automatiska konfigurationen, vilket även inkluderar Service Bus JMS. När programmet sedan använder Spring JMS-API:et uppfyller det villkoret för Service Bus JMS-autokonfiguration och utlöser det. För användare som inte tänker använda spring-cloud-azure-starter-servicebus-jmsuppfylls inte egenskapsvillkoren eftersom det inte finns någon anledning för dem att konfigurera Service Bus för JMS. Den här situationen leder till att undantagen kastas.

Lösning

Spring Cloud Azure for Service Bus JMS tillhandahåller en egenskap för att aktivera eller inaktivera den automatiska konfigurationen. Du kan välja att inaktivera den här funktionen efter behov med hjälp av följande egenskapsinställning:

spring.jms.servicebus.enabled=false

Konfigurera meddelandeattribut

Hur anger du innehållstypen för utgående meddelanden?

Om du vill konfigurera innehållstypen anpassar du meddelandekonverteraren så att den ändrar attributet för innehållstyp när meddelanden konverteras. Följande kod tar bytemeddelanden som exempel.

Anpassa först meddelandekonverteraren som ska användas i JmsTemplate, enligt följande exempel:

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

Deklarera sedan din anpassade meddelandekonverterareböna, som du ser i det här exemplet:

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

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

Hur anger du typ-ID-egenskapsnamn för MappingJackson2MessageConverter?

Med attributet type-id-property-name kan MappingJackson2MessageConverter avgöra vilken klass som ska användas för att deserialisera meddelandenyttolasten. Vid serialisering av varje Java-objekt till en meddelandenyttolast i Spring lagrar konverteraren nyttolasttypen i en meddelandeegenskap med det egenskapsnamn som registreras av type-id-property-name. När meddelandet deserialiseras läser konverteraren sedan typ-ID:t från meddelandet och utför deserialisering.

Om du vill ange type-id-property-namedeklarerar du din egen MappingJackson2MessageConverter böna och konfigurerar egenskapen enligt följande exempel:

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

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

Identifiering av dubbletter

Azure Service Bus stöder dubblettidentifiering, som tillämpar egenskapen MessageId för att unikt identifiera meddelanden och ta bort dubbletter som skickas till Service Bus.

För JMS-API:et bör du dock inte ange JMS-meddelande-ID:t, som betraktas som olagliga i JMS-specifikationer. Den här funktionen stöds för närvarande inte för Spring Cloud Azure Service Bus JMS Starter.

Ytterligare uppdateringar för den här funktionen finns i GitHub-problem.

Aktivera AMQP-transportloggning

Mer information finns i avsnittet aktivera AMQP-transportloggning i Felsökning av Service Bus-problem.

Få ytterligare hjälp

Mer information om hur du kontaktar support finns i Support i lagringsplatsens rot.

Resurser för Spring Cloud Azure Service Bus JMS Starter

Skapa GitHub-ärenden

Vid arkivering av GitHub-problem begärs följande information:

  • Service Bus-konfiguration/namnområdesmiljö
    • Vilken nivå är namnområdet (standard eller premium)?
    • Vilken typ av meddelandeentitet används (kö eller ämne)? och dess konfiguration.
    • Vilken är den genomsnittliga storleken på varje meddelande?
  • Hur ser trafikmönstret ut? (dvs. antalet meddelanden per minut och om klienten alltid är upptagen eller har långsamma trafikperioder.)
  • Återskapa kod och steg
    • Detta är viktigt eftersom vi ofta inte kan återskapa problemet i vår miljö.
  • Loggar