Service Bus メッセージングを介した分散トレースおよび相関付け

マイクロサービス開発に共通する問題の 1 つは、クライアントからの操作を、処理に関係するすべてのサービスにおいてトレースする機能です。 これは、デバッグ、パフォーマンス分析、A/B テスト、およびその他の一般的な診断シナリオで役立ちます。 この問題の 1 つとして、処理の論理的な部分の追跡があります。 これにはメッセージの処理結果、待機時間、外部依存関係呼び出しが含まれます。 もう 1 つの部分は、これらの診断イベントのプロセス境界を越えた相関付けです。

プロデューサーがキューを介してメッセージを送信すると、通常、そのメッセージは他の論理操作 (他のクライアントまたはサービスが開始した) のスコープ内で発生します。 メッセージを受信すると、同じ操作がコンシューマーによって続行されます。 プロデューサーとコンシューマーの両方 (および操作を処理する他のサービス) は、操作のフローと結果をトレースするためにテレメトリ イベントを出力すると推定されます。 このようなイベントを相関付けて、操作をエンドツーエンドでトレースするために、テレメトリをレポートする各サービスは、すべてのイベントにトレース コンテキストをスタンプする必要があります。 開発者が既定でこのテレメトリをすべて出力できるようにするライブラリの 1 つが NServiceBus です。

Microsoft Azure Service Bus メッセージングによってペイロード プロパティが定義され、プロデューサーとコンシューマーはこれを使用してトレース コンテキストを渡す必要があります。 プロトコルは W3C トレースコンテキストに基づいています。

プロパティ名 説明
Diagnostic-Id プロデューサーからキューへの外部呼び出しの一意識別子。 形式については、「W3C Trace-Context traceparent header」(W3C トレースコンテキスト traceparent ヘッダー) を参照してください

Service Bus .NET クライアントの自動トレース

.NET 用 Azure Messaging Service Bus クライアントServiceBusProcessor クラスにより、トレース インストルメンテーション ポイントが提供され、トレーシング システムまたはクライアント コードの一部でこれを受け取ることができます。 インストルメンテーションによって、すべての呼び出しをクライアント側から Service Bus メッセージング サービスまで追跡できるようになります。 メッセージの処理が ServiceBusProcessorProcessMessageAsync (メッセージ ハンドラー パターン) を使用して行われる場合は、メッセージの処理もインストルメント化されます。

Azure Application Insights で追跡する

Microsoft Application Insights では、自動的な要求と依存関係の追跡も含め、豊富なパフォーマンス監視機能が提供されます。

プロジェクト タイプに応じて次のいずれかの Application Insights SDK をインストールします。

  • ASP.NET - バージョン 2.5-beta2 以上をインストールします
  • ASP.NET Core - バージョン 2.2.0-beta2 以上をインストールします。 これらのリンクには、SDK のインストール、リソースの作成、SDK の構成 (必要な場合) に関する説明があります。 ASP.NET 以外のアプリケーションについては、コンソール アプリケーションのための Azure Application Insights に関する記事をご覧ください。

ServiceBusProcessorProcessMessageAsync (メッセージ ハンドラー パターン) を使用してメッセージを処理する場合、メッセージの処理もインストルメント化されます。 サービスによって実行されたすべての Service Bus の呼び出しは自動的に追跡され、他のテレメトリ項目と関連付けられます。 それ以外の場合は、手動でのメッセージ追跡について次の例をご覧ください。

メッセージ処理のトレース

async Task ProcessAsync(ProcessMessageEventArgs args)
{
    ServiceBusReceivedMessage message = args.Message;
    if (message.ApplicationProperties.TryGetValue("Diagnostic-Id", out var objectId) && objectId is string diagnosticId)
    {
        var activity = new Activity("ServiceBusProcessor.ProcessMessage");
        activity.SetParentId(diagnosticId);
        // If you're using Microsoft.ApplicationInsights package version 2.6-beta or higher, you should call StartOperation<RequestTelemetry>(activity) instead
        using (var operation = telemetryClient.StartOperation<RequestTelemetry>("Process", activity.RootId, activity.ParentId))
        {
            telemetryClient.TrackTrace("Received message");
            try 
            {
            // process message
            }
            catch (Exception ex)
            {
                telemetryClient.TrackException(ex);
                operation.Telemetry.Success = false;
                throw;
            }

            telemetryClient.TrackTrace("Done");
        }
    }
}

この例では、処理されるメッセージごとに、タイムスタンプ、期間、結果 (成功) を含む要求テレメトリが報告されます。 テレメトリには、相関関係プロパティのセットも含まれます。 メッセージ処理中に報告された入れ子のトレースと例外にも、RequestTelemetry の "子" であることを表す相関関係プロパティがスタンプされます。

サポートされる外部コンポーネントへの呼び出しをメッセージ処理中に行った場合は、それも自動的に追跡されて相関付けられます。 手動での追跡と相関付けについて詳しくは、「Application Insights .NET SDK でカスタム操作を追跡する」をご覧ください。

Application Insights SDK に加えて外部コードを実行している場合は、Application Insights ログを表示するときの所要時間が長くなることが予想されます。

Application Insights ログの期間が長くなる

これは、メッセージの受信で遅延があったことを意味するわけではありません。 このシナリオでは、メッセージは SDK コードにパラメーターとして渡されるため、メッセージは既に受信されています。 また、App Insights ログの name タグ (Process) は、メッセージが現在、外部イベント処理コードによって処理されていることを示しています。 この問題は、Azure に関連したものではありません。 メッセージが Service Bus から既に受信されていることを考えると、これらのメトリックは外部コードの効率を意味します。

OpenTelemetry を使用した追跡

Service Bus .NET クライアント ライブラリ バージョン 7.5.0 以降では、試験モードで OpenTelemetry がサポートされています。 詳しくは、.NET SDK での分散トレースに関するページをご覧ください。

トレース システムなしで追跡する

ご使用のトレーシング システムで Service Bus 呼び出しの自動追跡がサポートされない場合は、トレーシング システムまたはアプリケーションにそのようなサポートを追加することを検討してください。 このセクションでは、Service Bus .NET クライアントによって送信される診断イベントについて説明します。

Service Bus .NET クライアントは、.NET トレース プリミティブ System.Diagnostics.Activity および System.Diagnostics.DiagnosticSource を使用してインストルメント化されます。

Activity はトレース コンテキストとして使用され、DiagnosticSource は通知メカニズムです。

DiagnosticSource イベントのリスナーがない場合は、インストルメント化はオフになり、インストルメンテーション コストは 0 のままになります。 DiagnosticSource はすべてのコントロールをリスナーに与えます。

  • リスナーは、リッスンするソースとイベントを制御します。
  • リスナーは、イベント レートおよびサンプリングを制御します。
  • イベントは、すべてのコンテキストを提供するペイロードと一緒に送信されるため、ユーザーがイベント中にメッセージ オブジェクトにアクセスして変更できます。

実装を進める前に、DiagnosticSource ユーザー ガイドを読むことをお勧めします。

Microsoft.Extension.Logger でログを書き込む ASP.NET Core アプリに、Service Bus イベントのリスナーを作成します。 これは System.Reactive.Core ライブラリを使用して DiagnosticSource をサブスクライブします (このライブラリなしでも簡単に DiagnosticSource をサブスクライブできます)。

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory factory, IApplicationLifetime applicationLifetime)
{
    // configuration...

    var serviceBusLogger = factory.CreateLogger("Azure.Messaging.ServiceBus");

    IDisposable innerSubscription = null;
    IDisposable outerSubscription = DiagnosticListener.AllListeners.Subscribe(delegate (DiagnosticListener listener)
    {
        // subscribe to the Service Bus DiagnosticSource
        if (listener.Name == "Azure.Messaging.ServiceBus")
        {
            // receive event from Service Bus DiagnosticSource
            innerSubscription = listener.Subscribe(delegate (KeyValuePair<string, object> evnt)
            {
                // Log operation details once it's done
                if (evnt.Key.EndsWith("Stop"))
                {
                    Activity currentActivity = Activity.Current;
                    serviceBusLogger.LogInformation($"Operation {currentActivity.OperationName} is finished, Duration={currentActivity.Duration}, Id={currentActivity.Id}, StartTime={currentActivity.StartTimeUtc}");
                }
            });
        }
    });

    applicationLifetime.ApplicationStopping.Register(() =>
    {
        outerSubscription?.Dispose();
        innerSubscription?.Dispose();
    });
}

この例では、リスナーが、Service Bus の各操作の期間、結果、一意識別子、開始時刻を記録します。

events

すべてのイベントには、公開されているテレメトリ仕様 (https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/api.md ) に準拠する次のプロパティがあります。

  • message_bus.destination – キュー/トピック/サブスクリプションのパス
  • peer.address – 完全修飾名前空間
  • kind – producer、consumer、または client のいずれか。 メッセージを送信する場合は producer、受信する場合は consumer、設定する場合は client を使用します。
  • componentservicebus

すべてのイベントには、EntityEndpoint プロパティもあります。

  • Entity - エンティティ (キュー、トピックなど) の名前
  • Endpoint - Service Bus エンドポイント URL

インストルメント化された操作

インストルメント化された操作の全一覧を次に示します。

操作の名前 追跡される API
ServiceBusSender.Send ServiceBusSender.SendMessageAsync
ServiceBusSender.SendMessagesAsync
ServiceBusSender.Schedule ServiceBusSender.ScheduleMessageAsync
ServiceBusSender.ScheduleMessagesAsync
ServiceBusSender.Cancel ServiceBusSender.CancelScheduledMessageAsync
ServiceBusSender.CancelScheduledMessagesAsync
ServiceBusReceiver.Receive ServiceBusReceiver.ReceiveMessageAsync
ServiceBusReceiver.ReceiveMessagesAsync
ServiceBusReceiver.ReceiveDeferred ServiceBusReceiver.ReceiveDeferredMessagesAsync
ServiceBusReceiver.Peek ServiceBusReceiver.PeekMessageAsync
ServiceBusReceiver.PeekMessagesAsync
ServiceBusReceiver.Abandon ServiceBusReceiver.AbandonMessagesAsync
ServiceBusReceiver.Complete ServiceBusReceiver.CompleteMessagesAsync
ServiceBusReceiver.DeadLetter ServiceBusReceiver.DeadLetterMessagesAsync
ServiceBusReceiver.Defer ServiceBusReceiver.DeferMessagesAsync
ServiceBusReceiver.RenewMessageLock ServiceBusReceiver.RenewMessageLockAsync
ServiceBusSessionReceiver.RenewSessionLock ServiceBusSessionReceiver.RenewSessionLockAsync
ServiceBusSessionReceiver.GetSessionState ServiceBusSessionReceiver.GetSessionStateAsync
ServiceBusSessionReceiver.SetSessionState ServiceBusSessionReceiver.SetSessionStateAsync
ServiceBusProcessor.ProcessMessage ServiceBusProcessor で設定されるプロセッサ コールバック。 ProcessMessageAsync プロパティ
ServiceBusSessionProcessor.ProcessSessionMessage ServiceBusSessionProcessor で設定されるプロセッサ コールバック。 ProcessMessageAsync プロパティ

フィルター処理およびサンプリング

場合によっては、パフォーマンスのオーバーヘッドや記憶域の消費量を抑えるためにイベントの一部分のみを記録する必要があります。 'Stop' イベントのみを記録するか (前の例と同様)、イベントの一定の割合をサンプリングします。 DiagnosticSource では、IsEnabled 術後を使用してこれを実現する方法が提供されます。 詳しくは、DiagnosticSource でのコンテキストに基づくフィルター処理に関する記事をご覧ください。

IsEnabled を 1 つの操作で複数回呼び出して、パフォーマンスへの影響を最小限にすることができます。

IsEnabled は次のように呼び出します。

  1. IsEnabled(<OperationName>, string entity, null) を、たとえば IsEnabled("ServiceBusSender.Send", "MyQueue1") と指定します。 最後に 'Start' も 'Stop' もないことに注意してください。 これを使用して、特定の操作またはキューをフィルター処理で除外します。 コールバック メソッドから false が返されると、その操作のイベントは送信されません。

    • 'Process' 操作および 'ProcessSession' 操作では IsEnabled(<OperationName>, string entity, Activity activity) コールバックも受け取ります。 これを使用して、activity.Id または Tags プロパティに基づいてイベントをフィルター処理します。
  2. IsEnabled(<OperationName>.Start) を、たとえば IsEnabled("ServiceBusSender.Send.Start") と指定します。 'Start' イベントが起動されるかどうかを確認します。 この結果は 'Start' イベントのみに影響し、それ以降のインストルメンテーションがそれに依存することはありません。

'Stop' イベントに対する IsEnabled はありません。

一部の操作の結果が例外になると、IsEnabled("ServiceBusSender.Send.Exception") が呼び出されます。 'Exception' イベントをサブスクライブするだけで、その後のインストルメンテーションを防ぐことができます。 このケースでは、例外も処理する必要があります。 他のインストルメンテーションが無効になっているため、トレース コンテキストがメッセージと一緒にコンシューマーからプロデューサーに送られることを期待しないでください。

IsEnabled を使用して、サンプリング戦略を実装することもできます。 Activity.Id または Activity.RootId に基づくサンプリングにより、すべての層で一貫性のあるサンプリングが保証されます (トレーシング システムまたはユーザー独自のコードによって伝達される場合)。

同じソースに対して複数の DiagnosticSource リスナーが存在するとき、イベントを受け取るには 1 つのリスナーだけで十分です。したがって、IsEnabled の呼び出しは保証されません。

次のステップ