本文介绍如何排查使用 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 分钟后可用。
解决方法
目前,starter 通过应用 JmsPoolConnectionFactory 为链接分离问题提供了一种变通方案;JmsPoolConnectionFactory 会池化 Connection、Session 和 MessageProducer,并管理这些池化实例的生命周期。 此变通方法可确保生产者在变得不可用后会被移除,从而确保所有发送操作都在活跃的生产者上执行。
若要使用此解决方法,请添加以下配置:
spring:
jms:
servicebus:
pool:
enabled: true
max-connections: ${your-expected-max-connection-value}
spring.jms.servicebus.idle-timeout 的用法
空闲超时属性用于配置 AMQP 连接的 空闲超时。 AMQP 规范提供以下说明:
连接受空闲超时阈值限制。 当超过阈值后仍未收到任何帧时,超时将由本地对端触发。 空闲超时时间以毫秒为单位,从接收到最后一帧时开始计时。 如果超出阈值,则对等方 SHOULD 尝试使用带有说明原因的错误信息的关闭帧,优雅地关闭连接。 如果远程对等方未在对此设定的阈值时间内作出妥善响应,则该对等方可以关闭 TCP 套接字。
对于 JMS 客户端,在配置此属性时,可在服务器端控制服务器在未传递消息时发送空帧以保持连接活动状态的时间。 此属性控制远程对等方的行为,每个对等方可以有自己的独立值。
JmsTemplate 问题
计划消息
Azure 服务总线支持延迟的消息处理。 有关详细信息,请参阅 消息排序和时间戳计划消息 部分。 对于 JMS,若要计划消息,请使用消息注释标头 ScheduledEnqueueTimeUtc设置 x-opt-scheduled-enqueue-time 属性。
JmsListener 问题
即使服务器中没有消息,也会向服务总线发送过多请求
问题说明
使用 @JmsListener API 时,在某些情况下,即使服务器中没有要接收的消息,你也可以在 Azure 门户中看到发送到其队列或主题的传入请求存在持续值。
原因分析
@JmsListener 是一种轮询侦听器,专为反复轮询而设计。
侦听器位于正在进行的轮询循环上。 每个循环调用 JMS MessageConsumer.receive() 方法,以轮询本地使用者以获取要使用的消息。 默认情况下,对于每次轮询操作,本地消费者都会向消息代理发送拉取请求以获取消息,然后阻塞一段时间。 具体轮询过程由几个属性决定,包括 receiveTimeout、prefetchSize和 receiveLocalOnly 或 receiveNoWaitLocalOnly。 仅当将 receiveNoWaitLocalOnly 设置为负值时,才使用 receiveTimeout 方法。
如果应用程序遇到此问题,请检查以下配置设置:
确定预提取策略是否为 0,这也是默认选项。 预提取数为 0 表示拉取消费者会在每次轮询时向服务总线发送拉取请求。
如果已将预取配置为非零值,请确定您的
receiveLocalOnly或receiveNoWaitLocalOnly属性是否设置为false,这是默认选项。 此处的false值仍会导致向服务器发送拉取请求,因为它不仅会轮询本地使用者。receiveTimeout配置决定每次拉取请求会阻塞多长时间,因此可能会影响向服务器发送拉取请求的频率。 默认值为 1 秒。
有关完整分析,请参阅 GitHub 问题中的讨论。
解决方案
以下部分介绍了两种处理此问题的解决方案
解决方案 1. 更改为仅推送使用者和本地检查
通过将模式更改为 push,使用者将成为 异步通知 使用者,该使用者不会从中转站拉取消息,但会保留目标数量的链接信用额度。 该数量由预提取属性决定。 当服务总线(发送方)推送消息时,发送方的链接信用额度会减少,当发送方的链接信用额度低于阈值时,客户端(接收方)向服务器发送请求,以将发件人的链接信用额度重新提高到所需的目标金额。
若要完成此解决方案,请添加以下配置:
首先,将 prefetch 的值设置为非零,从而将消费者配置为非拉取模式。 下表显示了多个预提取属性,每个属性控制不同的服务总线实体。 设置适用于事例的属性。
| 财产 | 描述 |
|---|---|
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 意味着拉取操作会一直阻塞,直到有消息被分发为止。
- 将 receive-timeout 设置为正值意味着拉取会阻塞长达超时时间的时间量。
- 将接收超时设置为负值意味着拉取操作为无等待接收操作,这表示它会立即返回一条消息;如果没有可用消息,则返回
null。
注意
超时时间设置过高可能会带来一些负面影响。 例如,较高的超时值还会延长主线程处于阻塞状态的时间。 此状态意味着容器对 stop() 调用的响应较少,并且只能在 receive() 调用之间停止。
此外,容器只能在传递 receive-timeout 间隔后发送请求。 如果间隔超过 10 分钟,服务总线将关闭链接,并阻止侦听器发送或接收。 有关详细信息,请参阅 Azure 服务总线中的 AMQP 错误 的 链接已关闭 部分。 默认情况下,侦听器使用 CachingConnectionFactory。
如果需要较长的接收超时时间,请务必使用 JmsPoolConnectionFactory。
有关链接关闭问题以及如何使用 JmsPoolConnectionFactory 的更多信息,请参阅 MessageProducer 因不可恢复的错误而关闭 部分。
预提取问题
问题说明
不适合的预提取策略可能会导致以下问题:
- 相同的消息被重复消费。
- 消息在经过
MaxDeliveryCountExceeded后会被放入死信队列,即使消息在处理过程中未发生任何错误或异常。
原因分析
此问题通常发生在 prefetch 值高于实际消费能力时,导致过多消息被预提取到本地缓冲区中等待被消费。 但是,从 服务总线 一侧来看,预取的消息在 peek-lock 模式下被视为已分派。 每个已分派的消息都有 max-delivery-count 和 lock-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 |
持久主题的预提取编号。 |
如何对 服务总线 执行 AMQP 处置操作?
在向消息代理确认消息时,JMS 支持五种 AMQP 处置类型。 支持的值为 ACCEPTED、REJECTED、RELEASED、MODIFIED_FAILED和 MODIFIED_FAILED_UNDELIVERABLE。 有关详细信息,请参阅 AMQP 处置和服务总线操作映射 部分 将 Java 消息服务 1.1 与 Azure 服务总线标准和 AMQP 1.0配合使用。
因此,若要使用 JmsListener手动完成、放弃、死信、延迟或释放消息,请执行以下步骤:
禁用会话事务,并使用 CLIENT 确认模式。
若要完成此任务,可以声明您自己的 JmsListenerContainerFactory Bean 并设置其属性,或者对 starter 中定义的
JmsListenerContainerFactory进行后处理。 以下示例使用声明另一个 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; } }在消息处理程序中,显式完成或放弃消息。
@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 自动配置
问题说明
某些用户导入一些 Spring Cloud Azure Starter 来自动配置 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
原因分析
出现此问题的原因是所有 Spring Cloud Azure 自动配置类都放置在同一模块中,因此任何 Spring Cloud Azure Starter 实际上都会导入所有自动配置,其中包括服务总线 JMS。 然后,当应用程序使用 Spring JMS API 时,它满足 服务总线 JMS 自动配置 并触发它的条件。 那么,对于不打算使用 spring-cloud-azure-starter-servicebus-jms 的用户而言,将不满足该属性条件,因为他们没有理由为 JMS 配置 服务总线。 这种情况会导致抛出异常。
解决方案
Spring Cloud Azure for 服务总线 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;
}
}
然后,声明自定义的消息转换器 Bean,如下例所示:
@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {
@Bean
public MessageConverter messageConverter() {
return new CustomMappingJackson2MessageConverter();
}
}
如何为 MappingJackson2MessageConverter 设置类型 ID 属性名称?
type-id-property-name 属性使 MappingJackson2MessageConverter 能够确定用于反序列化消息有效负载的类。 将每个 Java 对象序列化为 Spring Message 有效负载时,转换器会将有效负载类型存储在消息属性中,该属性名称由 type-id-property-name记录。 然后,反序列化消息时,转换器从消息中读取类型 ID 并执行反序列化。
若要设置 type-id-property-name,请声明自己的 MappingJackson2MessageConverter bean 并配置该属性,如以下示例所示:
@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 属性以唯一标识消息并丢弃发送到服务总线的重复项。
但是,对于 JMS API,不应设置 JMS 消息 ID,该 ID 被视为 JMS 规范中的 非法。 因此,Spring Cloud Azure 服务总线 JMS Starter 目前尚不支持此功能。
有关此功能的任何其他更新,请参阅 GitHub 问题。
启用 AMQP 传输日志
有关更多信息,请参阅 排查服务总线问题 中的 启用 AMQP 传输日志记录 部分。
获取其他帮助
有关联系支持的方法的详细信息,请参阅存储库根目录中 支持。
Spring Cloud Azure 服务总线 JMS 启动器资源
- 将 Azure 服务总线与 JMS 配合使用
- 在 Spring 中使用 JMS 访问 Azure 服务总线
- Spring Cloud Azure 4.0 迁移指南
- 示例
提交 GitHub 问题
提交 GitHub 问题时,会请求以下详细信息:
- 服务总线配置/命名空间环境
- 命名空间(标准层或高级层)是什么?
- 正在使用哪种类型的消息传送实体(队列或主题) ? 及其配置。
- 每个邮件的平均大小是多少?
- 流量模式是什么样的? (也就是说,每分钟的消息数以及客户端是始终繁忙还是流量缓慢。
- 重现代码和步骤
- 这很重要,因为我们经常无法在我们的环境中重现该问题。
- 日志