Azure Service Bus のトラブルシューティング

この記事では、Azure Service Bus Java クライアント ライブラリのエラー調査手法、コンカレンシー、および一般的なエラーについて説明します。 このガイダンスを使用して根本原因を特定し、軽減手順を適用してAzure Service Bus問題をより迅速に解決します。

ログ記録を有効にして構成する

Azure SDK for Java では、アプリケーション エラーのトラブルシューティングに役立ち、解決を迅速化するために役立つ一貫したログ記録のストーリーが提供されます。 生成するログは、ルートの問題の特定に役立つターミナル状態に到達する前に、アプリケーションのフローをキャプチャします。 ログ記録のガイダンスについては、「Azure SDK for Java でのログ記録の構成」および「トラブルシューティングの概要を参照してください。

ログ記録を有効にするだけでなく、ログ レベルを VERBOSE または DEBUG に設定すると、ライブラリの状態に関する分析情報が得られます。 次のセクションでは、詳細ログが有効になっている場合の過剰なメッセージを減らすための log4j2 と logback の構成の例を示します。

Log4J 2 の構成

Log4J 2 を構成するには、次の手順に従います。

  1. 「Log4j2 に必要な依存関係」セクションのlogging sample pom.xmlにある依存関係を使用して、お使いのpom.xmlに依存関係を追加します。
  2. log4j2.xmlsrc/main/resources フォルダーに追加します。

Logbackを設定する

ログバックを構成するには、次の手順に従います。

  1. ログサンプルの pom.xml の「ログバックに必要な依存関係」セクションにある依存関係を使用して、 pom.xmlに依存関係を追加します。
  2. logback.xmlsrc/main/resources フォルダーに追加します。

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

問題を診断するのにクライアント ログを有効にするだけでは不十分な場合は、基になる AMQP ライブラリ (Qpid Proton-Jファイルへのログ記録を有効にできます。 Qpid Proton-J では java.util.loggingが使用されます。 ログ記録を有効にするには、次のセクションに示す内容を含む構成ファイルを作成します。 または、proton.trace.level=ALL と、java.util.logging.Handler 実装に必要な構成オプションを設定します。 実装クラスとそのオプションについては、Java 8 SDK ドキュメント java.util.logging をパッケージ化するを参照してください。

AMQP トランスポート フレームをトレースするには、PN_TRACE_FRM=1 環境変数を設定します。

「logging.properties」サンプル ファイル

次の構成ファイルは、Proton-J からの TRACE レベルの出力をファイル proton-trace.logに記録します。

handlers=java.util.logging.FileHandler
.level=OFF
proton.trace.level=ALL
java.util.logging.FileHandler.level=ALL
java.util.logging.FileHandler.pattern=proton-trace.log
java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.SimpleFormatter.format=[%1$tF %1$tr] %3$s %4$s: %5$s %n

ログ記録の削減

ログ記録を減らす 1 つの方法は、詳細度を変更する方法です。 もう 1 つの方法は、com.azure.messaging.servicebuscom.azure.core.amqpなどのロガー名パッケージからログを除外するフィルターを追加することです。 例については、「Log4J 2 の構成」セクション および「logback の構成」セクション の XML ファイルを参照してください。

バグを送信すると、次のパッケージのクラスからのログ メッセージが興味深いものになります。

  • com.azure.core.amqp.implementation
  • com.azure.core.amqp.implementation.handler
    • 例外は、onDeliveryReceiveLinkHandler メッセージを無視できることです。
  • com.azure.messaging.servicebus.implementation

ServiceBusProcessorClient でのコンカレンシー

ServiceBusProcessorClient を使用すると、メッセージ ハンドラーへの呼び出しの数を同時に構成できます。 この構成により、複数のメッセージを並列で処理できます。 セッション以外のエンティティからメッセージを使用 ServiceBusProcessorClient 場合は、 maxConcurrentCalls API を使用して目的のコンカレンシーを構成できます。 セッション対応エンティティの場合、必要なコンカレンシーは maxConcurrentSessionsmaxConcurrentCalls 倍です。

構成されたコンカレンシーよりもメッセージ ハンドラーへの同時呼び出しの数が少ない場合は、スレッド プールのサイズが適切に設定されていない可能性があります。

ServiceBusProcessorClient は、Reactor グローバル boundedElastic スレッド プールのデーモン スレッドを使用してメッセージ ハンドラーを呼び出します。 このプール内の同時実行スレッドの最大数は、上限によって制限されます。 既定では、この上限は使用可能な CPU コアの 10 倍です。 ServiceBusProcessorClient がアプリケーションの目的のコンカレンシー (maxConcurrentCalls または maxConcurrentSessions 時間 maxConcurrentCalls) を効果的にサポートするには、必要なコンカレンシーよりも高い boundedElastic プールの上限値が必要です。 既定の上限をオーバーライドするには、システム プロパティを reactor.schedulers.defaultBoundedElasticSize設定します。

スレッド プールと CPU 割り当てをケース バイ ケースで調整します。 ただし、プールの上限をオーバーライドする場合は、開始点として、同時実行スレッドを CPU コアあたり約 20 から 30 に制限します。 ServiceBusProcessorClient インスタンスごとに目的のコンカレンシーを約 20 から 30 に上限を設定します。 特定のユース ケースをプロファイリングして測定し、それに応じてコンカレンシーの側面を調整します。 負荷の高いシナリオでは、新しい ServiceBusProcessorClient インスタンスから各インスタンスがビルドされる複数の ServiceBusClientBuilder インスタンスを実行することを検討してください。 また、1 つのホストのダウンタイムがメッセージ処理全体に影響しないように、コンテナーや VM などの専用ホストで各 ServiceBusProcessorClient を実行することを検討してください。

CPU コアが少ないホストでプールの上限に高い値を設定すると、悪影響を及ぼす点に注意してください。 CPU リソースが不足しているか、CPU の数が少ないスレッドが多すぎるプールの兆候には、タイムアウトの頻度、ロックの損失、デッドロック、スループットの低下があります。 コンテナーで Java アプリケーションを実行している場合は、2 つ以上の vCPU コアを使用します。 コンテナー化された環境でアプリケーションを実行するときは、1 vCPU コア未満Java選択しないでください。 リソース管理に関する詳細な推奨事項については、「Java アプリケーションをコンテナー化する」を参照してください。

接続共有のボトルネック

共有ServiceBusClientBuilder インスタンスから作成するすべてのクライアントは、Service Bus名前空間への同じ接続を共有します。

共有接続を使用すると、1 つの接続でクライアント間で多重化操作が可能になりますが、クライアントが多数存在する場合や、クライアントが一緒に高負荷を生成する場合にも、共有がボトルネックになる可能性があります。 各接続には、I/O スレッドが関連付けられています。 接続を共有すると、クライアントはこの共有 I/O スレッドの作業キューに作業を配置し、各クライアントの進行状況は、キュー内の作業がタイムリーに完了するかどうかによって異なります。 I/O スレッドは、エンキューされた作業を順次処理します。 つまり、共有接続の I/O スレッドのワーク キューに未処理の作業が大量にたまると、その症状は CPU 使用率が低い場合の症状と似ています。 この条件は、コンカレンシーに関する前のセクションで説明されています。たとえば、クライアントのストール、タイムアウト、ロックの喪失、回復パスの速度低下などです。

Service Bus SDK は、接続 I/O スレッドに reactor-executor-* 名前付けパターンを使用します。 アプリケーションで共有接続のボトルネックが発生すると、I/O スレッドの CPU 使用率に反映される可能性があります。 また、ヒープ ダンプまたはライブ メモリでは、 ReactorDispatcher$workQueue オブジェクトは I/O スレッドの作業キューです。 ボトルネック期間中のメモリ スナップショット内の長い作業キューは、共有 I/O スレッドが保留中の作業でオーバーロードされていることを示している可能性があります。

そのため、Service Bus エンドポイントへのアプリケーションの負荷が、送受信されたメッセージの総数またはペイロード サイズの観点からかなり高い場合は、ビルドするクライアントごとに個別のビルダー インスタンスを使用します。 たとえば、エンティティ (キューまたはトピック) ごとに、新しい ServiceBusClientBuilder を作成し、そこからクライアントを構築できます。 特定のエンティティへの負荷が非常に高い場合は、そのエンティティに対して複数のクライアント インスタンスを作成するか、複数のホスト (コンテナーや VM など) でクライアントを実行して負荷分散を行うことができます。

Application Gateway カスタム エンドポイントの使用時にクライアントが停止する

カスタム エンドポイント アドレスとは、Service Busに解決したり、トラフィックをService Busにルーティングするように構成したりできる、アプリケーションが提供する HTTPS エンドポイント アドレスを指します。 Azure Application Gatewayを使用すると、トラフィックをService Busに転送する HTTPS フロントエンドを簡単に作成できます。 Service Busに接続するカスタム エンドポイントとして Application Gateway フロントエンド IP アドレスを使用するように、アプリケーションの Service Bus SDK を構成できます。

Application Gateway には、異なる TLS プロトコル バージョンをサポートするいくつかのセキュリティ ポリシーが用意されています。 TLS 1.2 を最小バージョンとして適用する定義済みのポリシーと、TLS 1.0 を最小バージョンとして使用する古いポリシーがあります。 HTTPS フロントエンドに TLS ポリシーを適用します。

現時点では、Service Bus SDK は、最小バージョンとして TLS 1.0 を使用する Application Gateway フロントエンドによる特定のリモート TCP 終了を認識しません。 たとえば、フロントエンドが TCP FIN および ACK パケットを送信して、プロパティを更新するときに接続を閉じる場合、SDK はこれらのパケットを検出できません。 そのため、再接続は行われず、クライアントはメッセージを送受信できなくなります。 この停止は、TLS 1.0 を最小バージョンとして使用する場合にのみ発生します。 この問題を軽減するには、Application Gateway フロントエンドの最小バージョンとして TLS 1.2 以降を設定するセキュリティ ポリシーを使用します。

すべてのAzure サービスでの TLS 1.0 と 1.1 のサポートは、2024 年 10 月 31 日までに終了することが既に発表されているため、TLS 1.2 に移行します。

メッセージまたはセッション ロックが失われた

Service Bus キューまたはトピック サブスクリプションには、リソース レベルでロック期間が設定されています。 受信側クライアントがリソースからメッセージをプルすると、Service Bus ブローカーはメッセージに初期ロックを適用します。 最初のロックは、リソース レベルで設定されたロック期間に続きます。 メッセージ ロックが期限切れになる前に更新されない場合、Service Bus ブローカーはメッセージを解放して、他の受信者が使用できるようにします。 アプリケーションがロックの有効期限後にメッセージを完了または破棄しようとすると、API 呼び出しはエラー com.azure.messaging.servicebus.ServiceBusException: The lock supplied is invalid. Either the lock expired, or the message has already been removed from the queueで失敗します。

Service Bus クライアントは、有効期限が切れる前にメッセージ ロックを継続的に更新するバックグラウンド ロック更新タスクの実行をサポートしています。 既定では、ロック更新タスクは 5 分間実行されます。 ロックの更新期間は、ServiceBusReceiverClientBuilder.maxAutoLockRenewDuration(Duration)を使用して調整できます。 Duration.ZERO 値を渡すと、ロック更新タスクは無効になります。

次の一覧では、ロック損失エラーの原因となる可能性がある使用パターンまたはホスト環境の一部について説明します。

  • ロック更新タスクが無効になり、アプリケーションのメッセージ処理時間がリソース レベルで設定されたロック期間を超えています。

  • アプリケーションのメッセージ処理時間が、構成されたロック更新タスク期間を超えています。 ロックの更新期間が明示的に設定されていない場合、既定値は 5 分であることに注意してください。

  • アプリケーションは、プリフェッチ機能を有効にするには、 ServiceBusReceiverClientBuilder.prefetchCount(prefetch)を使用してプリフェッチ値を正の整数に設定します。 プリフェッチ機能が有効になっている場合、クライアントは、Service Bus エンティティ (キューまたはトピック) からプリフェッチと等しいメッセージの数を取得し、それらをメモリ内プリフェッチ バッファーに格納します。 メッセージは、アプリケーションに受信されるまでプリフェッチ バッファーに保持されます。 クライアントは、プリフェッチ バッファー内にある間、メッセージのロックを拡張しません。 プリフェッチ バッファーに留まっている間にメッセージ ロックの有効期限が切れるほどアプリケーションの処理に時間がかかる場合、アプリケーションは期限切れのロックでメッセージを取得する可能性があります。 詳細については、「プリフェッチが既定のオプションではない理由」を参照してください。

  • ホスト環境には、ネットワークの一時的な障害や停止など、ロック更新タスクがロックを更新できないというネットワークの問題が時折発生します。

  • ホスト環境に十分な CPU が不足しているか、間欠的に CPU サイクルが不足しているため、ロック更新タスクの実行が期限切れになります。

  • ホスト システムの時刻が正確ではありません 。たとえば、クロックが歪んでいる場合、ロック更新タスクが遅れ、時間に従って実行されないようにします。

  • 接続 I/O スレッドがオーバーロードされ、ロック更新ネットワーク呼び出しを時間通り実行する機能に影響します。 この問題は、次の 2 つのシナリオで発生する可能性があります。

    • アプリケーションで、同じ接続を共有する受信側クライアントが多すぎます。 詳細については、「接続共有のボトルネック」セクションを参照してください。
    • アプリケーションは、大きなServiceBusReceiverClient.receiveMessagesまたはServiceBusProcessorClient値を持つmaxMessagesまたはmaxConcurrentCallsを構成します。 詳細については、「ServiceBusProcessorClient での コンカレンシー」セクションを参照してください。
  • ロック損失エラーの可能性を高める一般的なアプリケーション パターンには、実行時間の長いロック更新タスク (たとえば、数時間にわたるタスク) のスケジュールが含まれます。 前述のように、Service Bus クライアントの制御外のさまざまな要因がロックの更新の成功を妨げる可能性があるため、アプリケーションの設計では、長期間にわたって保証された更新を想定しないようにする必要があります。 実行時間の長い操作の再処理を避けるためには、作業をより小さなチャンクに分割するか、べき等性のあるチェックポイントの処理ロジックを実装することを検討してください。

クライアントのロック更新タスクの数は、maxMessages または maxConcurrentCallsに設定された ServiceBusProcessorClient または ServiceBusReceiverClient.receiveMessages パラメーター値と同じです。 複数のネットワーク呼び出しを行うロック更新タスクの数が多い場合は、Service Bus 名前空間の調整にも悪影響を及ぼす可能性があります。

ホストに十分なリソースが確保されていない場合でも、ロック更新タスクが少数実行されている場合でもロックが失われる可能性があります。 コンテナーで Java アプリケーションを実行している場合は、2 つ以上の vCPU コアを使用します。 コンテナー化された環境で Java アプリケーションを実行するときは、1 vCPU コア未満のものを選択しないでください。 リソース管理に関する詳細な推奨事項については、「Java アプリケーションをコンテナー化する」を参照してください。

ロックに関する同じ注釈は、セッションが有効になっている Service Bus キューまたはトピック サブスクリプションにも関連します。 受信側クライアントがリソース内のセッションに接続すると、ブローカーはセッションに初期ロックを適用します。 セッションのロックを維持するには、クライアントのロック更新タスクが期限切れになる前にセッション ロックを更新し続ける必要があります。 セッションが有効なリソースの場合、基になるパーティションは、Service Bus ノード間で負荷分散を実現するために移動することがあります。たとえば、負荷を共有するために新しいノードが追加された場合などです。 その場合、セッション ロックが失われる可能性があります。 セッション ロックが失われた後にアプリケーションがメッセージを完了または破棄しようとすると、API 呼び出しはエラー com.azure.messaging.servicebus.ServiceBusException: The session lock was lost. Request a new session receiverで失敗します。

次の手順

この記事のトラブルシューティング ガイダンスで、Java クライアント ライブラリのAzure SDKを使用するときに問題を解決できない場合は、Java GitHub リポジトリのAzure SDKに問題を提出してください。