この記事では、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 Service Bus で、AMQP 接続とリンクはアクティブであるものの、そのリンクを使用した送信や受信などの呼び出しが 10 分間行われない場合に発生します。 この場合、リンクは閉じられます。 また、アクティビティ (アイドル) がなく、5 分以内に新しいリンクが作成されていないため、接続内のすべてのリンクが閉じられた場合、接続は閉じられます。
Service Bus JMS スターターの場合、CachingConnectionFactory が既定で使用され、セッション、プロデューサー、およびコンシューマーがキャッシュされます。
JmsProducer が 10 分を超えて 15 分未満アイドル状態である場合、キャッシュされたプロデューサーが使用しているリンクは閉じられています。 この期間中はメッセージを送信できません。 その後、さらに 5 分間アイドル状態が続いた後、接続全体が閉じられます。 したがって、アイドル間隔が 15 分経過した後の送信操作により、CachingConnectionFactory は送信する新しい接続を作成します。 送信操作は 15 分後に使用可能になります。
回避策
現時点では、スターターは、プール JmsPoolConnectionFactory、Connection、および SessionMessageProducerを適用し、プールされたインスタンスのライフサイクルを管理することで、リンクデタッチの問題の回避策を提供します。 この回避策により、プロデューサーが使用できなくなった後に確実に削除されるため、すべての送信操作がアクティブなプロデューサーに対して実行されます。
この回避策を使用するには、次の構成を追加します。
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 Service Bus では、遅延メッセージ処理がサポートされています。 詳細については、「メッセージシーケンスとタイムスタンプの スケジュールされたメッセージ」セクションを参照してください。 JMS の場合、メッセージをスケジュールするには、メッセージ注釈ヘッダー ScheduledEnqueueTimeUtcを使用して、x-opt-scheduled-enqueue-time プロパティを設定します。
JmsListener の問題
サーバーにメッセージがない場合でも、Service Bus に送信される要求が多すぎます
問題の説明
@JmsListener API を使用する場合、受信するメッセージがサーバーに存在しない場合でも、キューまたはトピックに送信される受信要求の継続的な値があることを Azure portal で確認できる場合があります。
原因分析
@JmsListener は、繰り返しポーリングを実行するために設計されたポーリング リスナーです。
リスナーは常時ポーリング ループ内で待機します。 各ループで、メッセージを消費するためにローカルコンシューマーをポーリングする JMS MessageConsumer.receive() メソッドを呼び出します。 既定では、ポーリング操作ごとに、ローカル コンシューマーはメッセージ ブローカーにプル要求を送信してメッセージを要求し、一定期間ブロックします。 具体的なポーリング プロセスは、receiveTimeout、prefetchSize、receiveLocalOnly、receiveNoWaitLocalOnlyなど、いくつかのプロパティによって決定されます。
receiveNoWaitLocalOnly メソッドは、receiveTimeout を負の値に設定した場合にのみ使用されます。
この問題がアプリケーションで発生した場合は、次の構成設定を確認します。
プリフェッチ ポリシーが 0 であるかどうかを判断します。これは既定のオプションでもあります。 0-プリフェッチとは、ポーリングのたびに Service Bus にプル要求を送信するプルコンシューマーを指します。
0 以外のプリフェッチを構成した場合は、既定のオプションである
receiveLocalOnlyまたはreceiveNoWaitLocalOnlyプロパティがfalseに設定されているかどうかを確認します。 ここでfalse値を指定しても、ローカル コンシューマーのみをポーリングしないため、プル要求がサーバーに送信されます。receiveTimeout構成では、プル要求ごとにブロックする時間が決まります。そのため、サーバーに送信するプル要求の頻度に影響を与える可能性があります。 既定値は 1 秒です。
完全な分析については、GitHub の問題の説明を参照してください。
ソリューション
以降のセクションでは、この問題に対処するための 2 つの解決策について説明します
解決策 1. プッシュ コンシューマーとローカルチェックのみに変更する
モードを pushに変更すると、コンシューマーはブローカーからメッセージをプルせず、リンク クレジットの目標量を維持する 非同期通知 コンシューマーになります。 量はプリフェッチ プロパティによって決定されます。 Service Bus (送信者) がメッセージをプッシュすると、送信者のリンク クレジットが減少し、送信者のリンク クレジットがしきい値を下回ると、クライアント (受信側) は、送信者のリンク クレジットを目的のターゲット量に戻す要求をサーバーに送信します。
このソリューションを実現するには、次の構成を追加します。
まず、prefetch の値を 0 以外に設定し、コンシューマーをプル型ではないものとして設定します。 次の表は、それぞれ異なる Service Bus エンティティを制御するいくつかのプリフェッチ プロパティを示しています。 ケースに適用されるプロパティを設定します。
| 財産 | 説明 |
|---|---|
spring.jms.servicebus.prefetch.all |
この Service Bus 名前空間のプリフェッチ オプションのフォールバック値 |
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 に設定します。 この値により、コンシューマーが一度に処理するメッセージは 1 つだけになります。
解決策 2. プルの頻度を減らすには、受信タイムアウトを増やします
受信タイムアウトプロパティは、コンシューマーがプル結果を待つためにどのくらいの時間ブロックするかを決定します。 そのため、タイムアウトを延長することで、プルの頻度を減らし、プル モードを選択するときにプル要求の数を減らすことができます。 極端なケースでは、メッセージが到着するまで無期限に待機する戦略を設定できます。つまり、コンシューマーはメッセージを使用した後にのみプルします。 この場合、サーバーにメッセージがない場合は、待機がブロックされます。
このソリューションを実現するには、spring.jms.listener.receive-timeout プロパティを構成します。 このプロパティは java.time.Duration 型で、既定値は 1 秒です。 次の一覧では、さまざまな値の効果について説明します。
- 受信タイムアウトを 0 に設定すると、メッセージがディスパッチされるまでプル ブロックは無期限になります。
- 受信タイムアウトを正の値に設定すると、プルはタイムアウト時間までブロックされます。
- 受信タイムアウトを負の値に設定すると、プルは待機なしの受信になります。つまり、すぐにメッセージが返されます。または、使用可能なメッセージがない場合は
null。
手記
タイムアウト値が大きいと、いくつかの副作用が発生する可能性があります。 たとえば、タイムアウト値が大きいと、メイン スレッドがブロック状態になる時間も長くなります。 この状態は、コンテナーが stop() 呼び出しに対する応答性が低下し、receive() 呼び出しの間でのみ停止できることを意味します。
また、コンテナーは、receive-timeout 間隔が経過した後にのみ要求を送信できます。 間隔が 10 分を超える場合、Service Bus はリンクを閉じ、リスナーが送受信できないようにします。 詳細については、AMQP errors in Azure Service Bus の 「リンクが閉じている」 セクションを参照してください。 既定では、リスナーは CachingConnectionFactoryを使用します。
高い受信タイムアウトが必要な場合は、JmsPoolConnectionFactoryを使用してください。
リンククローズの問題と
プリフェッチの問題
問題の説明
不適切なプリフェッチ ポリシーは、次の問題を引き起こす可能性があります。
- 同じメッセージが繰り返し使用されます。
- メッセージは、エラーや例外なしで処理された場合でも、
MaxDeliveryCountExceeded後に配信不能キューに入れられます。
原因分析
この問題は、通常、 prefetch の値が実際の消費容量よりも大きい場合に発生します。その結果、メッセージが多すぎると、ローカル バッファーにプリフェッチされて消費されるのを待機しています。 ただし、プリフェッチされたメッセージは、Service Bus 側から peek-lock モードでディスパッチされたと見なされます。 ディスパッチされた各メッセージには、 max-delivery-count 属性と lock-duration 属性があります。
peek-lock 受信モードでは、プリフェッチバッファーにフェッチされたメッセージは、ロック状態のままバッファーに取り込まれ、ロックのタイムアウト時間のカウントが開始されます。 プリフェッチ バッファーが大きく、処理に時間がかかるため、プリフェッチ バッファー内に留まっている間にメッセージ ロックが期限切れになる場合、メッセージは破棄済みとして扱われ、キューから再度取得できるようになります。
この問題により、メッセージがプリフェッチ バッファーにフェッチされ、末尾に配置される可能性があります。 プリフェッチ バッファーがメッセージの有効期限前に処理されない場合、メッセージは繰り返しプリフェッチされますが、有効な (有効にロックされた) 状態で効果的に配信されることはありません。 その後、古いコピーがデキューされると、アプリケーションは同じメッセージを繰り返し使用し、完了できません。 別のケースでは、繰り返し送信されるメッセージは、消費される前にバッファー内ですべて期限切れになります。 この場合、Service Bus 内のメッセージは、最大配信回数を超過すると、最終的に配信不能キューに移動されます。
詳細については、Azure Service Bus メッセージをプリフェッチする の プリフェッチが既定のオプションではない理由 セクションを参照してください。
ソリューション
プリフェッチの設定は、消費側の処理能力に見合ったものになるよう注意してください。 キューまたはサブスクリプションで設定された最大プリフェッチ数とロック期間のバランスを取り、ロックのタイムアウトが、最大サイズのプリフェッチ バッファー内のメッセージに加えてさらに 1 件のメッセージを処理するために予想される累積時間を少なくとも上回るようにする必要があります。 同時に、ロック タイムアウトは長すぎてもいけません。長すぎると、メッセージが誤って破棄された場合に、再配信される前にロックの期限が切れるのを待つ必要があるため、メッセージの最大 Time to Live を超えてしまう可能性があります。
既定値が 0 のプリフェッチ属性を構成するには、次のいずれかのプロパティを使用します。
| 財産 | 説明 |
|---|---|
spring.jms.servicebus.prefetch.all |
この Service Bus 名前空間のプリフェッチ オプションのフォールバック値。 |
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 |
永続トピックのプリフェッチ番号。 |
Service Bus に対する AMQP ディスポジションを実行する方法
JMS では、メッセージング ブローカーにメッセージを受信確認するときに、5 種類の AMQP 処理がサポートされます。 サポートされている値は、ACCEPTED、REJECTED、RELEASED、MODIFIED_FAILED、および MODIFIED_FAILED_UNDELIVERABLEです。 詳細については、「Azure Service Bus Standard および AMQP 1.0で Java Message Service 1.1 を使用する」の「AMQP の処理と Service Bus 操作マッピングの」セクションを参照してください。
そのため、JmsListenerを使用してメッセージを手動で完了、破棄、配信不能、延期、または解放するには、次の手順を使用します。
セッションのトランザクションを無効にし、CLIENT ack モードを使用します。
このタスクを実行するには、独自の 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; } }
構成の問題
Service Bus JMS の自動構成を無効にする
問題の説明
一部のユーザーは、Service Bus JMS 以外の Azure サービスの自動構成のために、一部の Spring Cloud Azure Starter をインポートします。 また、Service Bus JMS を必要とせずに Spring 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 では、Service Bus JMS も含まれるすべての自動構成が実際にインポートされます。 次に、アプリケーションが Spring JMS API を使用すると、Service Bus JMS 自動構成 spring-cloud-azure-starter-servicebus-jmsを使用しないユーザーの場合、プロパティの条件は満たされません。これは、ユーザーが Service Bus for JMS を構成する理由がないためです。 この状況では例外がスローされます。
ソリューション
Spring Cloud Azure for Service Bus JMS には、自動構成のオンとオフを切り替えるプロパティが用意されています。 必要に応じて、次のプロパティ設定を使用して、この機能を無効にすることができます。
spring.jms.servicebus.enabled=false
メッセージ属性を構成する
送信メッセージのコンテンツ タイプを設定する方法
コンテンツ タイプを構成するには、メッセージの変換時にコンテンツ タイプ属性を変更するように Message Converter をカスタマイズします。 次のコードは、バイト メッセージを例として受け取ります。
まず、次の例に示すように、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 Service Bus では、重複検出がサポートされています。この MessageId プロパティを適用して、メッセージを一意に識別し、Service Bus に送信された重複を破棄します。
ただし、JMS API の場合、JMS 仕様では無効な と見なされる JMS メッセージ ID
この機能のその他の更新については、GitHub の問題を参照してください。
AMQP トランスポート ログを有効にする
詳細については、Service Bus の問題のトラブルシューティング の AMQP トランスポートのログ記録を有効にする セクションを参照してください。
その他のヘルプを表示する
サポートに連絡する方法の詳細については、リポジトリのルートにある サポート を参照してください。
Spring Cloud Azure Service Bus JMS スターターのリソース
- JMS で Azure Service Bus を使用する
- Spring で JMS を使用して Azure Service Bus にアクセスする
- Spring Cloud Azure 4.0 の移行ガイド
- サンプル
GitHub の問題の報告
GitHub の問題を提出するときに、次の詳細が要求されます。
- Service Bus の構成/名前空間環境
- 名前空間 (Standard または Premium) はどのレベルですか?
- 使用されているメッセージング エンティティの種類 (キューまたはトピック) とその構成。
- 各メッセージの平均サイズは何ですか?
- トラフィック パターンは何ですか? (つまり、1 分あたりのメッセージ数と、クライアントが常にビジー状態であるか、トラフィック期間が遅いか)。
- 再現コードと手順
- これは、多くの場合、環境で問題を再現できないため、重要です。
- ログ