Руководство по устранению неполадок Spring JMS

В этой статье описывается устранение известных проблем и распространенных ошибок при использовании Spring JMS. В статье также приведены ответы на некоторые часто задаваемые вопросы о spring-cloud-azure-starter-servicebus-jms.

Проблемы с подключением

MessageProducer был закрыт из-за неустранимой ошибки

Описание проблемы

При использовании JmsTemplate для отправки сообщений JmsTemplate становится недоступным в течение интервала простоя от 10 до 15 минут. Отправка сообщений в этом интервале может получить исключения, показанные в следующем примере выходных данных:

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

Анализ причин

Исключения возникают для Служебная шина Azure, когда AMQP-подключение и ссылка активны, но в течение 10 минут по этой ссылке не выполняются вызовы, например операции отправки или получения. В этом случае ссылка закрыта. И когда все ссылки в соединении были закрыты, так как не было действия (бездействия) и новая ссылка не была создана в течение 5 минут, подключение закрывается.

Для JMS-стартера служебная шина по умолчанию используется CachingConnectionFactory, кэширующая сеансы, производителей и потребителей. Если JmsProducer простаивает более 10, но менее 15 минут, соединение, занятое кэшированным производителем, закрыто. Сообщения не могут отправляться в течение этого интервала. Затем, через еще 5 минут бездействия, все подключение закрывается. Таким образом, любая операция отправки после 15-минутного периода простоя приводит к тому, что CachingConnectionFactory создает новое подключение для отправки. Операция отправки становится доступной через 15 минут.

Обходное решение

В настоящее время стартер предоставляет обходной путь для проблемы отсоединения связей, применяя JmsPoolConnectionFactory, который объединяет в пул Connection, Session и MessageProducer и управляет жизненным циклом экземпляров в пуле. Этот обходной путь позволяет гарантировать, что производитель будет исключён после того, как станет недоступным, и, следовательно, все операции отправки будут выполняться активными производителями.

Чтобы использовать это решение, добавьте следующую конфигурацию:

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

Использование spring.jms.servicebus.idle-timeout

Свойства времени ожидания простоя настраивают время ожидания простоя подключения AMQP. Спецификация AMQP содержит следующее описание:

Подключения подвергаются порогу времени ожидания простоя. Тайм-аут срабатывает на локальном узле, если после превышения порогового значения не поступают кадры. Время ожидания простоя измеряется в миллисекундах и начинается с момента получения последнего кадра. Если пороговое значение превышено, то одноранговый узел должен попытаться корректно закрыть соединение с помощью близкого кадра с ошибкой, объясняющей причину. Если удаленный пир не отвечает на это корректно в течение заданного порогового времени, то пир МОЖЕТ закрыть TCP-сокет.

Для клиента JMS настройка этого свойства позволяет задать на стороне сервера, как долго сервер должен отправлять пустой кадр, чтобы поддерживать соединение активным, если сообщения не передаются. Это свойство управляет поведением удаленного однорангового узла, и каждый одноранговый узел может иметь собственное, изолированное значение.

Проблемы С JmsTemplate

Запланированные сообщения

Служебная шина Azure поддерживает отложенную обработку сообщений. Дополнительные сведения см. в разделе Запланированные сообщения в Последовательность сообщений и метки времени. Для JMS, чтобы запланировать отправку сообщения, установите свойство ScheduledEnqueueTimeUtc с помощью заголовка аннотации сообщения x-opt-scheduled-enqueue-time.

Проблемы JmsListener

Слишком много запросов отправляются в служебную шину, несмотря на отсутствие сообщений на сервере

Описание проблемы

При использовании API @JmsListener в некоторых случаях можно увидеть на портале Azure текущие значения входящих запросов, отправленных в очередь или разделы, даже если на сервере нет сообщений.

Анализ причин

@JmsListener — это прослушиватель опроса, созданный для повторных попыток опроса.

Слушатель работает в непрерывном цикле опроса. На каждой итерации цикла вызывается метод JMS MessageConsumer.receive() для опроса локального потребителя на наличие сообщений для обработки. По умолчанию, при каждой операции опроса локальный потребитель отправляет брокеру сообщений запросы на получение сообщений, а затем ожидает в течение определенного периода времени. Конкретный процесс опроса определяется несколькими свойствами, включая receiveTimeout, prefetchSize, а также receiveLocalOnly или receiveNoWaitLocalOnly. Метод receiveNoWaitLocalOnly используется только при установке receiveTimeout в отрицательное значение.

При возникновении этой проблемы с приложением проверьте следующие параметры конфигурации:

  • Определите, равно ли значение политики предварительной загрузки 0, так как это значение используется по умолчанию. Нулевая предварительная выборка означает, что потребитель pull отправляет запросы на получение в служебная шина при каждом опросе.

  • Если вы настроили ненулевую предварительную выборку, определите, имеет ли свойство receiveLocalOnly или receiveNoWaitLocalOnly значение false, которое используется по умолчанию. Значение false здесь всё равно приводит к отправке pull-запросов на сервер, поскольку выполняется опрос не только локального потребителя.

  • Конфигурация receiveTimeout определяет, сколько времени он блокирует для каждого запроса на вытягивание, поэтому он может повлиять на частоту отправки запросов на вытягивание на сервер. Значение по умолчанию — 1 секунда.

Полный анализ см. в обсуждении проблемы GitHub.

Решения

В следующих разделах описаны два решения для решения этой проблемы.

Решение 1. Изменение на push-потребитель и только локальную проверку

Изменив режим на push, потребитель становится потребителем асинхронных уведомлений, который не получает сообщения от брокера, а поддерживает заданный уровень кредита канала. Объём задаётся свойством prefetch. По мере того как служебная шина (отправитель) отправляет сообщения, кредит на связь отправителя уменьшается, и когда кредит на связь отправителя снижается ниже порогового значения, клиент (получатель) отправляет запрос серверу, чтобы увеличить кредит ссылки отправителя обратно на нужную целевую сумму.

Чтобы выполнить это решение, добавьте следующую конфигурацию:

Сначала задайте для prefetch ненулевое значение, чтобы настроить потребителя не в режиме pull. В следующей таблице приведено несколько свойств предварительной выборки, каждое из которых отвечает за разные сущности служебная шина. Задайте свойства, которые применяются к вашему делу.

Свойство Описание
spring.jms.servicebus.prefetch.all Резервное значение для параметра предварительной выборки в этом пространстве имен служебной шины
spring.jms.servicebus.prefetch.queue-prefetch Номер предварительной выборки для очереди.
spring.jms.servicebus.prefetch.queue-browser-prefetch Номер предварительной выборки для браузера очередей.
spring.jms.servicebus.prefetch.topic-prefetch Номер предварительной выборки для раздела.
spring.jms.servicebus.prefetch.durable-topic-prefetch Номер предварительной выборки для устойчивого раздела.

Во-вторых, настройте non-local-check, добавив класс конфигурации для настройщика фабрики, как показано в следующем примере:

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

Значение предварительной выборки может повлиять на то, как быстро сообщения отправляются в локальный буфер потребителя. Необходимо скорректировать значение исходя из скорости потребления и объёма сообщений. Подходящее значение может ускорить процесс потребления, а слишком большое значение может привести к тому, что локально буферированные сообщения становятся устаревшими и перенаправляются снова. Для небольших томов сообщений, где каждое сообщение занимает много времени для обработки, задайте для предварительной выборки значение 1. Это значение гарантирует, что потребитель обрабатывает только одно сообщение одновременно.

Решение 2. Увеличьте время ожидания получения, чтобы уменьшить частоту вытягивания

Свойство времени ожидания получения определяет стратегию того, как долго потребитель блокирует ожидание результата извлечения. Таким образом, расширив время ожидания, можно уменьшить частоту вытягивания, а затем уменьшить количество запросов на вытягивание при выборе режима вытягивания. В исключительных случаях можно настроить стратегию ожидания, при которой система будет ждать неограниченно долго, пока не поступит сообщение, то есть потребитель запрашивает следующее сообщение только после обработки предыдущего. В этом случае, если на сервере нет сообщений, он блокирует ожидание.

Чтобы выполнить это решение, настройте свойство spring.jms.listener.receive-timeout. Это свойство имеет тип java.time.Duration и имеет значение по умолчанию в 1 секунду. В следующем списке объясняется влияние различных значений:

  • Установка тайм-аута получения в значение 0 означает, что операция pull блокируется на неопределённое время, пока не будет отправлено сообщение.
  • Установка тайм-аута получения в положительное значение означает, что операция извлечения блокируется на время до истечения тайм-аута.
  • Если задать для receive-timeout отрицательное значение, это означает, что операция pull выполняет получение без ожидания, то есть немедленно возвращает сообщение или null, если доступных сообщений нет.

Заметка

Большое значение времени ожидания может привести к некоторым побочным эффектам. Например, большое значение тайм-аута также увеличивает время, в течение которого основной поток находится в заблокированном состоянии. Это состояние означает, что контейнер будет менее реагировать на вызовы stop() и может останавливаться только между вызовами receive().

Кроме того, контейнер может отправлять запросы только после прохождения интервала receive-timeout. Если интервал превышает 10 минут, служебная шина закроет соединение и не позволит получателю отправлять или получать сообщения. Дополнительные сведения см. в разделе Ссылка закрыта раздела об ошибках AMQP в служебной шине Azure. По умолчанию прослушиватель использует CachingConnectionFactory.

Если требуется высокое время ожидания получения, обязательно используйте JmsPoolConnectionFactory.

Дополнительные сведения о проблеме, связанной с закрытием канала, и о том, как использовать JmsPoolConnectionFactory, см. в разделе MessageProducer был закрыт из-за неустранимой ошибки.

Проблема с предзагрузкой

Описание проблемы

Неподходящая политика предварительной выборки может вызвать следующие проблемы:

  • Те же сообщения многократно используются.
  • Сообщения помещаются в очередь недоставленных писем после MaxDeliveryCountExceeded, даже если сообщения обрабатываются без ошибок или исключений.

Анализ причин

Эта проблема обычно возникает, когда значение prefetch выше фактической способности обработки, из-за чего слишком много сообщений заранее считывается в локальный буфер в ожидании обработки. Однако сообщения, полученные с предварительной выборкой, со стороны служебная шина считаются отправленными в режиме peek-lock. Каждое отправленное сообщение имеет и max-delivery-countlock-duration атрибуты. В режиме peek-lock получения сообщения, извлечённые в буфер предварительной выборки, помещаются в буфер в заблокированном состоянии, при этом начинается отсчёт тайм-аута блокировки. Если буфер предварительной выборки большой, и обработка занимает так долго, что срок действия блокировки сообщений истекает во время пребывания в буфере предварительной выборки, сообщение обрабатывается как заброшенное и снова становится доступным для получения из очереди.

Эта проблема может привести к тому, что сообщение попадёт в буфер предзагрузки и будет перемещено в конец. Если буфер предварительной выборки не обрабатывается до истечения времени жизни сообщения, сообщения многократно считываются в буфер предварительной выборки, но так и не доставляются в пригодном для использования состоянии (с действительной блокировкой). Затем, когда эти устаревшие копии извлекаются из очереди, приложение многократно обрабатывает одно и то же сообщение и не может завершить их обработку. В другом случае все повторные сообщения успевают истечь в буфере до того, как их обработают. В этом случае сообщение в служебная шина в конечном итоге будет перемещено в очередь недоставленных сообщений после превышения максимального числа попыток доставки.

Дополнительные сведения см. в разделе Почему предварительная выборка не используется по умолчанию? статьи Предварительная выборка сообщений Служебная шина Azure.

Решение

Будьте осторожны с конфигурацией предварительной выборки, чтобы убедиться, что она соответствует возможности использования. Необходимо сбалансировать максимальное число сообщений предварительной выборки и длительность блокировки, настроенные для очереди или подписки, таким образом, чтобы время блокировки как минимум превышало совокупное ожидаемое время обработки сообщений для максимального размера буфера предварительной выборки плюс ещё одного сообщения. В то же время тайм-аут блокировки не должен быть настолько длительным, чтобы сообщения, если они случайно теряются, могли превысить свое максимальное время жизни, из-за чего пришлось бы ждать истечения срока блокировки, прежде чем они будут повторно доставлены.

Чтобы настроить атрибут предварительной выборки, имеющий значение по умолчанию нулю, используйте одно из следующих свойств:

Свойство Описание
spring.jms.servicebus.prefetch.all Резервное значение для параметра предварительной выборки в этом пространстве имен служебной шины.
spring.jms.servicebus.prefetch.queue-prefetch Номер предварительной выборки для очереди.
spring.jms.servicebus.prefetch.queue-browser-prefetch Номер предварительной выборки для браузера очередей.
spring.jms.servicebus.prefetch.topic-prefetch Номер предварительной выборки для раздела.
spring.jms.servicebus.prefetch.durable-topic-prefetch Номер предварительной выборки для устойчивого раздела.

Как выполнить операцию disposition в AMQP для служебная шина?

JMS поддерживает пять типов диспозиций AMQP при подтверждении сообщений брокеру сообщений. Поддерживаемые значения: ACCEPTED, REJECTED, RELEASED, MODIFIED_FAILEDи MODIFIED_FAILED_UNDELIVERABLE. Дополнительные сведения см. в разделе Сопоставление состояний AMQP и операций служебная шина статьи Использование Java Message Service 1.1 со стандартным уровнем Служебная шина Azure и AMQP 1.0.

Таким образом, чтобы вручную завершить, отказаться, отложить или освободить сообщение с помощью JmsListener, выполните следующие действия:

  1. Отключите транзакции сеанса и используйте режим подтверждения CLIENT.

    Чтобы выполнить эту задачу, либо объявите собственный компонент JmsListenerContainerFactory и затем задайте его свойства, либо выполните постобработку JmsListenerContainerFactory, определённого в starter. В следующем примере используется подход объявления другой бобов:

    @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. В обработчике сообщений явно завершайте или отклоняйте сообщения.

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

Проблемы с конфигурацией

Отключить автоконфигурацию JMS для служебная шина

Описание проблемы

Некоторые пользователи импортируют некоторые начальные параметры Azure Spring Cloud для автоматической настройки службы Azure, отличной от JMS служебной шины. Они также используют платформу Spring JMS без необходимости использовать служебная шина JMS. Затем при попытке запуска приложения возникают следующие исключения:

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

Анализ причин

Эта проблема возникает из-за того, что все классы автонастройки Azure Spring Cloud помещаются в один модуль, поэтому любой начальный модуль Azure Spring Cloud фактически импортирует все эти автонастройки, включая JMS служебной шины. Затем, когда приложение использует API Spring JMS, оно соответствует условию автоматической настройки JMS служебной шины и активирует его. Затем для пользователей, которые не собираются использовать spring-cloud-azure-starter-servicebus-jms, условия для свойства не выполняются, поскольку им незачем настраивать служебная шина для JMS. Эта ситуация приводит к возникновению исключений.

Решение

Spring Cloud Azure для служебная шина JMS предоставляет свойство, позволяющее включать или отключать автоконфигурацию. Вы можете отключить эту функцию по мере необходимости с помощью следующего параметра свойства:

spring.jms.servicebus.enabled=false

Настройка атрибутов сообщения

Как задать тип контента исходящих сообщений?

Чтобы настроить тип контента, настройте конвертер сообщений для изменения атрибута типа контента при преобразовании сообщений. В следующем примере кода используются байтовые сообщения.

Сначала настройте преобразователь сообщений для использования в JmsTemplate, как показано в следующем примере:

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

Затем объявите настроенный преобразователь сообщений, как показано в следующем примере:

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

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

Как задать имя свойства идентификатора типа для MappingJackson2MessageConverter?

Атрибут type-id-property-name позволяет MappingJackson2MessageConverter определить, какой класс следует использовать для десериализации полезной нагрузки сообщения. При сериализации каждого объекта Java в полезную нагрузку сообщения Spring преобразователь сохраняет тип полезной нагрузки в свойстве сообщения, имя которого задаётся с помощью type-id-property-name. Затем при десериализации сообщения преобразователь считывает идентификатор типа из сообщения и выполняет десериализацию.

Чтобы задать type-id-property-name, объявите собственный бин MappingJackson2MessageConverter и настройте это свойство, как показано в следующем примере:

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

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

Обнаружение повторяющихся данных

Служебная шина Azure поддерживает обнаружение дубликатов, при котором используется свойство MessageId для уникальной идентификации сообщений и отбрасывания дубликатов, отправляемых в служебная шина.

Однако для API JMS не следует задавать идентификатор сообщения JMS, который считается незаконным в спецификациях JMS. Таким образом, эта функция в настоящее время не поддерживается для Начального модуля JMS служебной шины Azure Spring Cloud.

Чтобы следить за дальнейшими обновлениями этой функции, см. issue на GitHub.

Включить ведение журнала транспорта AMQP

Дополнительные сведения см. в разделе включение ведения журнала транспорта AMQP статьи Устранение неполадок служебная шина.

Получить дополнительную справку

Дополнительные сведения о способах получения поддержки см. в разделе Поддержка в корневом каталоге репозитория.

Ресурсы для стартера Spring Cloud Служебная шина Azure JMS

Создание сообщений о проблемах в GitHub

При отправке проблем с GitHub запрашиваются следующие сведения:

  • Конфигурация служебной шины / среда пространства имен
    • Какая ценовая категория у пространства имен (стандартная или премиальная)?
    • Какой тип сущности обмена сообщениями используется (очередь или тема)? и её конфигурацию.
    • Какой средний размер каждого сообщения?
  • Каков шаблон трафика? (то есть число сообщений в минуту и всегда ли клиент занят или имеет медленные периоды трафика.)
  • Код и шаги для воспроизведения
    • Это важно, так как мы часто не можем воспроизвести проблему в нашей среде.
  • Журналы