Průvodce odstraňováním potíží s Spring JMS

Tento článek popisuje, jak řešit známé problémy a běžné chyby při používání Spring JMS. Článek také odpovídá na některé časté otázky k spring-cloud-azure-starter-servicebus-jms.

Problémy s připojením

MessageProducer byl zavřen kvůli neopravitelné chybě.

Popis problému

Při použití JmsTemplate k odesílání zpráv se JmsTemplate během intervalu nečinnosti mezi 10 až 15 minutou stane nedostupný. Odesílání zpráv v tomto intervalu může získat výjimky uvedené v následujícím příkladu výstupu:

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

Analýza příčin

K výjimkám dochází u služby Azure Service Bus, když jsou připojení a propojení AMQP aktivní, ale nejsou k dispozici žádná volání – například odesílání nebo přijímání volání – pomocí odkazu po dobu 10 minut. V tomto případě je propojení uzavřeno. A když byly všechny odkazy v připojení uzavřeny, protože nedošlo k žádné aktivitě (nečinné) a za 5 minut se nevytvořil nový odkaz, připojení se zavře.

Ve výchozím nastavení se pro starter Service Bus JMS používá CachingConnectionFactory, která ukládá do mezipaměti relaci, producenta a konzumenta. Pokud je JmsProducer nečinný déle než 10 minut, ale méně než 15 minut, spojení, které producent uložený v mezipaměti využívá, bylo ukončeno. Během tohoto intervalu nelze odesílat zprávy. Po dalších 5 minutách nečinnosti se celé připojení zavře. Proto jakákoli operace odesílání po 15minutovém intervalu nečinnosti způsobí, že CachingConnectionFactory vytvoří nové připojení k odeslání. Operace odesílání bude k dispozici po 15 minutách.

Alternativní řešení

V současné době starter poskytuje dočasné řešení problému s odpojením propojení pomocí JmsPoolConnectionFactory, který sdružuje Connection, Session a MessageProducer do fondu a spravuje životní cyklus sdružených instancí. Toto alternativní řešení může zajistit vyřazení producenta po nedostupnosti, a proto se všechny odesílající operace provádějí u aktivních výrobců.

Pokud chcete použít toto alternativní řešení, přidejte následující konfiguraci:

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

Využití spring.jms.servicebus.idle-timeout

Vlastnosti časového limitu nečinnosti konfigurují časový limit nečinnosti připojení AMQP. Specifikace AMQP obsahuje následující popis:

Připojení podléhají prahové hodnotě časového limitu nečinnosti. Časový limit je místním protějškem vyvolán, pokud po překročení prahové hodnoty nejsou přijaty žádné rámce. Časový limit nečinnosti se měří v milisekundách a začíná od okamžiku přijetí posledního rámce. Pokud je prahová hodnota překročena, protistrana by se měla pokusit připojení korektně uzavřít pomocí uzavíracího rámce s chybovým údajem vysvětlujícím důvod. Pokud vzdálený protějšek na to nereaguje korektně v rámci stanoveného limitu, protějšek MŮŽE uzavřít socket TCP.

Pokud pro klienta JMS nakonfigurujete tuto vlastnost, řídíte na straně serveru, jak dlouho očekáváte, že server odešle prázdný rámec, aby zůstal připojení aktivní, když nejsou doručeny žádné zprávy. Tato vlastnost určuje chování vzdáleného protějšku a každý protějšek může mít svou vlastní, oddělenou hodnotu.

Problémy s JmsTemplate

Naplánované zprávy

Azure Service Bus podporuje zpožděné zpracování zpráv. Další informace naleznete v části Naplánované zprávypořadí zpráv a časových razítek. Chcete-li v JMS naplánovat zprávu, nastavte vlastnost ScheduledEnqueueTimeUtc pomocí záhlaví anotace zprávy x-opt-scheduled-enqueue-time.

Problémy s JmsListener

Do služby Service Bus se odesílá příliš mnoho požadavků, i když na serveru nejsou žádné zprávy.

Popis problému

Při použití rozhraní API @JmsListener můžete v některých případech v portálu Azure vidět průběžně vykazované hodnoty příchozích požadavků odeslaných do fronty nebo témat, i když na serveru nejsou k přijetí žádné zprávy.

Analýza příčin

@JmsListener je naslouchací proces dotazování, který je vytvořený pro opakované pokusy o dotazování.

Proces naslouchání běží v nepřetržité smyčce dotazování. V každé iteraci se volá metoda JMS MessageConsumer.receive(), aby se dotázala místního příjemce zpráv na zprávy určené ke zpracování. Ve výchozím nastavení při každé operaci dotazování lokální konzument odesílá zprostředkovateli zpráv požadavky na načtení, aby si vyžádal zprávy, a poté se na určitou dobu zablokuje. Konkrétní proces dotazování je určen několika vlastnostmi, včetně receiveTimeout, prefetchSizea receiveLocalOnly nebo receiveNoWaitLocalOnly. Metoda receiveNoWaitLocalOnly se používá pouze při nastavení receiveTimeout na zápornou hodnotu.

Pokud k tomuto problému dojde v aplikaci, zkontrolujte následující nastavení konfigurace:

  • Ověřte, zda je hodnota vaší zásady přednačítání 0, což je také výchozí hodnota. 0-prefetch znamená příjemce typu pull, který při každém dotazování odesílá do služby Service Bus požadavky na vyzvednutí zpráv.

  • Pokud jste nakonfigurovali nenulový prefetch, určete, jestli je vlastnost receiveLocalOnly nebo receiveNoWaitLocalOnly nastavená na false, což je výchozí možnost. Hodnota false zde stále vede k odesílání pull requestů na server, protože nedochází pouze k dotazování místního klienta.

  • Konfigurace receiveTimeout určuje, jak dlouho blokuje pro každou žádost o přijetí změn, aby mohla ovlivnit frekvenci žádostí o přijetí změn odesílaných na server. Výchozí hodnota je 1 sekunda.

Úplnou analýzu najdete v diskuzi o problému GitHubu.

Řešení

Následující části popisují dvě řešení tohoto problému.

Řešení 1. Změna na příjemce push oznámení a pouze místní kontrolu

Změnou režimu na push se klient stane klientem Asynchronous Notification, který nestahuje zprávy ze zprostředkovatele, ale udržuje cílovou hodnotu kreditu odkazu. Hodnota je určena vlastností prefetch. S tím, jak služba Service Bus (odesílatel) odesílá zprávy, sníží se kredit odkazu odesílatele a když odkazový kredit odesílatele klesne pod prahovou hodnotu, klient (příjemce) odešle požadavek na server, aby zvýšil odkaz-kredit odesílatele zpět na požadovanou cílovou částku.

Pokud chcete toto řešení provést, přidejte následující konfiguraci:

Nejprve nastavte číslo prefetch na nenulovou hodnotu, čímž se příjemce nakonfiguruje jako non-pull. Následující tabulka uvádí několik předem načtených vlastností, z nichž každá řídí různé entity služby Service Bus. Nastavte vlastnosti, které platí pro váš případ.

Vlastnost Popis
spring.jms.servicebus.prefetch.all Záložní hodnota pro možnost předběžného načítání v tomto oboru názvů služby Service Bus
spring.jms.servicebus.prefetch.queue-prefetch Počet předem načítaných položek ve frontě.
spring.jms.servicebus.prefetch.queue-browser-prefetch Číslo předběžného načtení pro prohlížeč front.
spring.jms.servicebus.prefetch.topic-prefetch Počet předem načítaných položek pro téma.
spring.jms.servicebus.prefetch.durable-topic-prefetch Číslo předběžného načtení pro trvalé téma.

Za druhé nakonfigurujte non-local-check přidáním konfigurační třídy pro přizpůsobení továrny, jak je znázorněno v následujícím příkladu:

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

Hodnota předběžného načítání může ovlivnit, jak rychle jsou zprávy doručovány do místní vyrovnávací paměti konzumenta. Hodnotu byste měli upravit podle výkonu zpracování a objemu zpráv. Vhodná hodnota může urychlit proces využívání, zatímco hodnota, která je příliš velká, může způsobit, že zprávy v místní vyrovnávací paměti budou zastaralé a znovu odeslány. Při malém objemu zpráv, kdy zpracování každé zprávy trvá dlouho, nastavte prefetch na 1. Tato hodnota zajišťuje, že příjemce zpracovává pouze jednu zprávu najednou.

Řešení 2. Zvyšte časový limit příjmu, aby se snížila frekvence stahování

Vlastnost časového limitu příjmu určuje strategii, jak dlouho se příjemce blokuje při čekání na výsledek načtení. Prodloužením časového limitu tedy můžete snížit frekvenci přijetí změn a snížit počet žádostí o přijetí změn, když zvolíte režim přijetí změn. V extrémních případech můžete nastavit strategii čekání na neurčito, dokud nedorazí zpráva, což znamená, že příjemce načte další zprávu až po zpracování předchozí zprávy. V takovém případě, pokud na serveru nejsou žádné zprávy, zablokuje čekání.

Chcete-li toto řešení provést, nakonfigurujte vlastnost spring.jms.listener.receive-timeout. Tato vlastnost je typu java.time.Duration a má výchozí hodnotu 1 sekundu. Následující seznam vysvětluje účinek různých hodnot:

  • Nastavení časového limitu příjmu na 0 znamená, že operace pull bude blokovat neomezeně dlouho, dokud nebude doručena zpráva.
  • Nastavení časového limitu pro příjem na kladnou hodnotu znamená, že operace pull blokuje až na dobu odpovídající nastavenému časovému limitu.
  • Nastavení časového limitu pro příjem na zápornou hodnotu znamená, že operace pull bude příjem bez čekání, tedy že okamžitě vrátí zprávu nebo null, pokud nejsou k dispozici žádné zprávy.

Poznámka

Vysoká hodnota časového limitu může přinést některé vedlejší účinky. Například vysoká hodnota časového limitu také prodlouží dobu, po kterou je hlavní vlákno ve stavu bloku. Tento stav znamená, že kontejner bude méně reagovat na stop() volání a může se zastavit pouze mezi receive() voláními.

Kontejner může také odesílat požadavky pouze po uplynutí receive-timeout intervalu. Pokud je interval delší než 10 minut, Service Bus uzavře připojení a zabrání naslouchací službě v odesílání nebo přijímání. Další informace najdete v části Odkaz je uzavřen dokumentu Chyby AMQP ve službě Azure Service Bus. Ve výchozím nastavení listener používá CachingConnectionFactory.

Pokud potřebujete dlouhý časový limit pro příjem, nezapomeňte použít JmsPoolConnectionFactory.

Další informace o problému zavření odkazu a o tom, jak používat JmsPoolConnectionFactory, najdete v MessageProducer byl zavřen kvůli neopravitelné chybě oddílu.

Problém s předběžným načtením

Popis problému

Nevhodná zásada předběžného načtení může způsobit následující problémy:

  • Stejné zprávy se opakovaně spotřebovávají.
  • Zprávy se za MaxDeliveryCountExceededzařadí do fronty nedoručených zpráv, a to i v případě, že se zprávy zpracovávají bez chyby nebo výjimky.

Analýza příčin

K tomuto problému obvykle dochází, když je hodnota prefetch vyšší než skutečná kapacita zpracování, což má za následek, že se do lokálního bufferu předem načítá příliš mnoho zpráv, které čekají na zpracování. Předem načtené zprávy se ale zobrazují jako odeslané v peek-lock režimu ze strany služby Service Bus. Každá odesílaná zpráva má atributy max-delivery-count a lock-duration. V režimu příjmu peek-lock jsou zprávy načtené do bufferu předběžného načítání získány v uzamčeném stavu, přičemž běží časový limit zámku. Pokud je vyrovnávací paměť pro předběžné načítání velká a zpracování trvá tak dlouho, že zámky zpráv vyprší, zatímco zprávy zůstávají ve vyrovnávací paměti pro předběžné načítání, je zpráva považována za opuštěnou a znovu je zpřístupněna k načtení z fronty.

Tento problém může způsobit, že se zpráva načte do bufferu předběžného načítání a bude umístěna na konec. Pokud se buffer předběžného načítání nezpracuje před vypršením platnosti zprávy, zprávy se opakovaně předběžně načítají, ale nikdy nejsou skutečně doručeny v použitelném stavu (s platným uzamčením). Potom, když jsou tyto zastaralé kopie odebrány z fronty, aplikace opakovaně zpracovává stejnou zprávu a nedokáže její zpracování dokončit. V jiném případě jsou všechny opakované zprávy před spotřebou ve vyrovnávací paměti vypršeny. V takovém případě se zpráva ve službě Service Bus nakonec přesune do fronty nedoručených zpráv po překročení maximálního počtu doručení.

Další informace najdete v části Proč není Prefetch výchozí volbou? v článku Načtení zpráv služby Azure Service Bus předem.

Řešení

Při konfiguraci předběžného načtení buďte opatrní, abyste měli jistotu, že odpovídá spotřebě funkcí. Musíte vyvážit maximální počet předem načtených zpráv a dobu uzamčení nastavenou pro frontu nebo odběr tak, aby doba vypršení zámku byla alespoň delší než kumulativní očekávaná doba zpracování zpráv odpovídající maximální velikosti vyrovnávací paměti pro předběžné načítání plus jedné další zprávě. Současně by doba vypršení zámku neměla být tak dlouhá, aby zprávy při svém náhodném zahození mohly překročit svou maximální dobu života, a před opětovným doručením tak musely čekat na vypršení zámku.

Chcete-li nakonfigurovat atribut předběžného načtení, který má výchozí hodnotu nula, použijte jednu z následujících vlastností:

Vlastnost Popis
spring.jms.servicebus.prefetch.all Výchozí hodnota pro možnost předběžného načítání v tomto oboru názvů služby Service Bus.
spring.jms.servicebus.prefetch.queue-prefetch Počet předem načítaných položek ve frontě.
spring.jms.servicebus.prefetch.queue-browser-prefetch Číslo předběžného načtení pro prohlížeč front.
spring.jms.servicebus.prefetch.topic-prefetch Počet předem načítaných položek pro téma.
spring.jms.servicebus.prefetch.durable-topic-prefetch Číslo předběžného načtení pro trvalé téma.

Jak v Service Bus provést dispoziční operaci AMQP?

JMS podporuje pět typů dispozic AMQP při potvrzování zpráv zprostředkovateli zpráv. Podporované hodnoty jsou ACCEPTED, REJECTED, RELEASED, MODIFIED_FAILEDa MODIFIED_FAILED_UNDELIVERABLE. Další informace najdete v části Dispozice AMQP a mapování operací Service Bus článku Použití služby Java Message Service 1.1 se službou Azure Service Bus Standard a protokolem AMQP 1.0.

Pokud tedy chcete zprávu ručně dokončit, opustit, odložit, odložit nebo uvolnit zprávu pomocí JmsListener, postupujte následovně:

  1. Zakažte transakce relace a použijte režim KLIENT ack.

    Chcete-li tento úkol provést, buď deklarujte vlastní bean JmsListenerContainerFactory a poté nastavte vlastnosti, nebo dodatečně zpracujte , definovanou v modulu starter. Následující příklad používá přístup k deklarování jiného beanu:

    @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. V obslužné rutině zpráv explicitně dokončete nebo odmítněte zprávy.

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

Problémy s konfigurací

Zakažte automatickou konfiguraci JMS pro Service Bus

Popis problému

Někteří uživatelé naimportují sadu Spring Cloud Azure Starter pro automatickou konfiguraci jiné služby Azure než Service Bus JMS. Používají také architekturu Spring JMS bez nutnosti použití JMS služby Service Bus. Když se aplikace pokusí spustit, zobrazí se následující výjimky:

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

Analýza příčin

K tomuto problému dochází, protože všechny třídy automatické konfigurace Spring Cloud Azure jsou umístěny ve stejném modulu, takže jakýkoli Spring Cloud Azure Starter ve skutečnosti importuje veškerou tuto automatickou konfiguraci, jejíž součástí je i Service Bus JMS. Když pak aplikace používá rozhraní Spring JMS API, splňuje podmínku automatické konfigurace JMS služby Service Bus a aktivuje ji. Potom pro uživatele, kteří nemají v úmyslu používat spring-cloud-azure-starter-servicebus-jms, podmínky vlastností nejsou splněny, protože neexistuje důvod, proč by mohli nakonfigurovat Service Bus pro JMS. Tato situace způsobuje vyvolání výjimek.

Řešení

Spring Cloud Azure for Service Bus JMS poskytuje vlastnost pro zapnutí nebo vypnutí jeho automatické konfigurace. Tuto funkci můžete podle potřeby zakázat pomocí následujícího nastavení vlastnosti:

spring.jms.servicebus.enabled=false

Konfigurace atributů zpráv

Jak nastavit typ obsahu odchozích zpráv?

Chcete-li nakonfigurovat typ obsahu, přizpůsobte převaděč zpráv úpravě atributu typu obsahu při převodu zpráv. Následující kód přebírá jako příklad bajtové zprávy.

Nejprve upravte převaděč zpráv, který se má použít v JmsTemplate, jak je znázorněno v následujícím příkladu:

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

Potom deklarujte vlastní převaděč zpráv bean, jak je znázorněno v tomto příkladu:

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

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

Jak nastavit název vlastnosti typu ID pro MappingJackson2MessageConverter?

Atribut type-id-property-name umožňuje MappingJackson2MessageConverter určit, kterou třídu použít k deserializaci datové části zprávy. Při serializaci každého objektu Java do datové části zprávy Spring převaděč ukládá typ datové části do vlastnosti zprávy pod názvem zaznamenaným pomocí type-id-property-name. Při deserializaci zprávy pak převaděč přečte ID typu ze zprávy a provede deserializaci.

Chcete-li nastavit type-id-property-name, deklarujte vlastní MappingJackson2MessageConverter bean a nakonfigurujte tuto vlastnost, jak je znázorněno v následujícím příkladu:

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

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

Detekce duplicit

Azure Service Bus podporuje detekce duplicit , která používá vlastnost MessageId k jedinečné identifikaci zpráv a zahození duplicit odeslaných do služby Service Bus.

Pro rozhraní API JMS byste ale neměli nastavit ID zprávy JMS, které se považuje za nelegální ve specifikacích JMS. Tato funkce se v současné době nepodporuje pro úvodní sadu Spring Cloud Azure Service Bus JMS.

Další aktualizace k této funkci najdete v problému na GitHubu issue.

Povolení protokolování přenosu AMQP

Další informace najdete v povolení protokolování přenosu AMQP části Řešení potíží se službou Service Bus.

Získání další nápovědy

Další informace o způsobech, jak kontaktovat podporu, najdete v tématu podpora v kořenovém adresáři úložiště.

Zdroje informací pro starter Spring Cloud Azure Service Bus JMS

Zakládání issue na GitHubu

Při vyplňování problémů GitHubu se požadují následující podrobnosti:

  • Konfigurace služby Service Bus / prostředí oboru názvů
    • V jaké úrovni je prostor názvů (Standard nebo Premium)?
    • Jaký typ entity zasílání zpráv se používá (fronta nebo téma)? a jeho konfiguraci.
    • Jaká je průměrná velikost každé zprávy?
  • Jaký je vzor provozu? (tj. počet zpráv za minutu a to, zda je Klient neustále vytížený nebo zda má období slabého provozu.)
  • Reprodukční kód a kroky
    • To je důležité, protože často nemůžeme problém reprodukovat v našem prostředí.
  • Protokoly