次の方法で共有


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

原因分析

この例外は、AMQP 接続とリンクがアクティブであるが、呼び出しの送受信などの呼び出しが 10 分間使用されていない場合に、Azure Service Bus で発生します。 この場合、リンクは閉じられます。 また、アクティビティ (アイドル) がなく、5 分以内に新しいリンクが作成されていないため、接続内のすべてのリンクが閉じられた場合、接続は閉じられます。

Service Bus JMS スターターの場合、CachingConnectionFactory が既定で使用され、セッション、プロデューサー、およびコンシューマーがキャッシュされます。 JmsProducer が 10 分以上アイドル状態で 15 未満の場合、キャッシュされたプロデューサーが占有しているリンクは閉じられています。 この期間中はメッセージを送信できません。 その後、さらに 5 分間アイドル状態が続いた後、接続全体が閉じられます。 したがって、アイドル間隔が 15 分経過した後の送信操作により、CachingConnectionFactory は送信する新しい接続を作成します。 送信操作は 15 分後に使用可能になります。

回避策

現時点では、スターターは、プール JmsPoolConnectionFactoryConnection、および 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() メソッドを呼び出して、メッセージが使用されるようにローカル コンシューマーをポーリングします。 既定では、ポーリング操作ごとに、ローカル コンシューマーはメッセージ ブローカーにプル要求を送信してメッセージを要求し、一定期間ブロックします。 具体的なポーリング プロセスは、receiveTimeoutprefetchSizereceiveLocalOnlyreceiveNoWaitLocalOnlyなど、いくつかのプロパティによって決定されます。 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 はリンクを閉じ、リスナーが送受信できないようにします。 詳細については、Azure Service Busでの AMQP エラーの の「リンクの閉じ」セクションを参照してください。 既定では、リスナーは CachingConnectionFactoryを使用します。

高い受信タイムアウトが必要な場合は、JmsPoolConnectionFactoryを使用してください。

リンククローズの問題と の使用方法の詳細については、「回復不能なエラー が原因で MessageProducer が閉じられた 」セクションを参照してください。

プリフェッチの問題

問題の説明

不適切なプリフェッチ ポリシーは、次の問題を引き起こす可能性があります。

  • 同じメッセージが繰り返し使用されます。
  • メッセージは、エラーや例外なしで処理された場合でも、MaxDeliveryCountExceeded後に配信不能キューに入れられます。

原因分析

この問題は、通常、プリフェッチの 値が実際の消費容量よりも大きい場合に発生します。その結果、使用を待機しているローカル バッファーにプリフェッチされるメッセージが多すぎます。 ただし、プリフェッチされたメッセージは、Service Bus 側から ピーク ロック モードでディスパッチされたと見なされます。 ディスパッチされた各メッセージには、max-delivery-count 属性とロック期間属性があります。 ピーク ロック受信モードでは、プリフェッチ バッファーにフェッチされたメッセージがロック状態のバッファーに取得され、ロックタイマーのタイムアウト クロックが発生します。 プリフェッチ バッファーが大きく、処理に時間がかかるため、プリフェッチ バッファー内に留まっている間にメッセージ ロックが期限切れになる場合、メッセージは破棄済みとして扱われ、キューから再度取得できるようになります。

この問題により、メッセージがプリフェッチ バッファーにフェッチされ、末尾に配置される可能性があります。 プリフェッチ バッファーがメッセージの有効期限前に処理されない場合、メッセージは繰り返しプリフェッチされますが、有効な (有効にロックされた) 状態で効果的に配信されることはありません。 その後、古いコピーがデキューされると、アプリケーションは同じメッセージを繰り返し使用し、完了できません。 別のケースでは、繰り返されるメッセージはすべて、バッファー内で有効期限が切れてから使用されます。 この場合、Service Bus 内のメッセージは、配信の最大数を超えた後、最終的に配信不能キューに移動されます。

詳細については、「プリフェッチが既定のオプションではない理由」を参照してください。azure Service Bus メッセージ プリフェッチの セクション。

解決

プリフェッチの構成に注意して、使用する機能に合うようにしてください。 プリフェッチ バッファーの最大サイズに対して、少なくとも 1 つのメッセージが予想される累積メッセージ処理時間を超えるロック タイムアウトになるように、キューまたはサブスクリプションで構成されている最大プリフェッチ数とロック期間のバランスを取る必要があります。 同時に、ロックタイムアウトは、メッセージが誤って削除されたときにメッセージが最大有効期間を超える可能性があるため、再配信される前にロックの有効期限が切れる必要があります。

既定値が 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 処理がサポートされます。 サポートされている値は、ACCEPTEDREJECTEDRELEASEDMODIFIED_FAILED、および MODIFIED_FAILED_UNDELIVERABLEです。 詳細については、「Azure Service Bus Standard および AMQP 1.0で Java Message Service 1.1 を使用する」の「AMQP の処理と Service Bus 操作マッピングの」セクションを参照してください。

そのため、JmsListenerを使用してメッセージを手動で完了、破棄、配信不能、延期、または解放するには、次の手順を使用します。

  1. セッション トランザクションを無効にし、CLIENT ack モードを使用します。

    このタスクを実行するには、独自の JmsListenerContainerFactory Bean を宣言してからプロパティを設定するか、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;
        }
    }
    
  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;
        }
    }
    

構成の問題

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 設定しないでください。 そのため、この機能は現在、Spring Cloud Azure Service Bus JMS Starter ではサポートされていません。

この機能のその他の更新については、GitHub の問題を参照してください。

AMQP トランスポート ログを有効にする

詳細については、「Service Bus の問題のトラブルシューティング」の「AMQP トランスポート ログ を有効にする 」セクションを参照してください。

その他のヘルプを表示する

サポートに連絡する方法の詳細については、リポジトリのルートにある サポート を参照してください。

Spring Cloud Azure Service Bus JMS スターターのリソース

GitHub の問題の報告

GitHub の問題を提出するときに、次の詳細が要求されます。

  • Service Bus の構成/名前空間環境
    • 名前空間 (Standard または Premium) はどのレベルですか?
    • 使用されているメッセージング エンティティの種類 (キューまたはトピック) とその構成。
    • 各メッセージの平均サイズは何ですか?
  • トラフィック パターンは何ですか? (つまり、1 分あたりのメッセージ数と、クライアントが常にビジー状態であるか、トラフィック期間が遅いか)。
  • 再現コードと手順
    • これは、多くの場合、環境で問題を再現できないため、重要です。
  • ログ