AWS Kinesis 上で OpenTelemetry トレースヘッダを伝搬する方法: パート2
PartitionKeyの課題を理解する
2024年6月27日
翻訳: 竹洞 陽一郎
この記事は米Catchpoint Systems社のブログ記事「How to propagate OpenTelemetry trace headers over AWS Kinesis: Part 2」の翻訳です。
Spelldataは、Catchpointの日本代理店です。
この記事は、Catchpoint Systemsの許可を得て、翻訳しています。
シリーズの最初の記事では、トレースヘッダの重要性とその伝播に伴う複雑さについて探りました。
理論から実践へと移行します。
この第二弾では、AWS KinesisでPartitionKeyパラメータを使用してOpenTelemetryトレースコンテキストを伝播するための実践的なベースラインシナリオと初期戦略について説明します。
ベースラインの探求
まず、OpenTelemetry SDKが提供する自動計測とトレースコンテキスト伝播について、そのデフォルト機能に焦点を当てて調査します。
私たちのテストでは、AWS Kinesis内の2つの相互接続されたサービス、イベントを生成して送信するプロデューサーと、それを受信して処理するコンシューマーを使用します。
この初期シナリオでは、システムに変更を加えることなく、サービス間でのトレース伝播の自然な挙動を観察することができます。
テストシナリオと設定をシンプルに保ち、主要な問題に集中しました。
- AWS Kinesisストリームには2つのシャード(※)があります。
event-producer
というアプリがあります。- このアプリはイベントを生成し、AWS SDKを介してAWS Kinesisストリームに送信します。
- イベントはイベントグループIDによってシャードに分割され、同じグループのイベントは同じシャードに配置され、順次処理されます。
event-producer
アプリはOTEL Javaエージェント(バージョン1.30.1)によって自動計測およびトレースされます。event-consumer
というアプリがあります。- このアプリはAWS Lambda関数(
otel-lambda-playground-kinesis-handler
)であり、AWS Kinesisストリームからトリガーされます。 - トリガーのバッチサイズは1に設定されており、複数の上流トレースリンクケースを防ぐためです。
(イベントは同じ呼び出しでバッチ処理されますが、各イベントはイベントプロデューサーアプリケーション内の異なる下流トレースに配置されます) event-consumer
のAWS Lambda関数ハンドラはOTEL Java AWS Lambdaパッケージ(opentelemetry-aws-lambda-core-1.0
およびopentelemetry-aws-lambda-events-2.2
)によって自動的にトレースされます。
※訳注: シャードとは、巨大なデータセットを複数の小さな部分に分割し、それぞれを異なるサーバーやノードに保存する方法です。
データのスケーラビリティとパフォーマンスを向上させるために使用されます。
Kinesisでは、シャードはデータストリームを構成する基本単位であり、各シャードは独立してデータの読み書きを処理します。
テストシナリオを実行すると、event-producer
とevent-consumer
の両方のアプリケーションがOTEL SDKによって自動的にトレースされますが、同じフローに属しているにもかかわらず、互いに独立した2つの異なるトレースが作成されます。
最初の試み - PartitionKey
を通じたトレースコンテキストの伝播
AWS KinesisのPut Recordリクエストモデルを確認すると、以下の6つのパラメータがあることが分かります。
Data
ExplicitHashKey
PartitionKey
SequenceNumberForOrdering
StreamARN
StreamName
しかし、traceparent
ヘッダを伝播するために、これらの多くのパラメータを使用したり変更したりすることはできません。
Data
は変更すべきではありません。
以前のセクションで、このリクエストボディを変更した場合に発生する可能性のある問題について説明しました。StreamName
とStreamARN
も変更できません。
これらのパラメータは、イベントを送信するAWS Kinesisストリームを指定します。SequenceNumberForOrdering
パラメータも変更できません。
同じクライアントから同じシャードに送信するイベントの順序が変わり、ここで単調増加するシーケンス番号を付与できない場合、いくつかのレコードが処理されずに無視される可能性があります。
この状況がどのように発生するかについては、以下のAWS re:Postの質問で簡単な例とともに説明されています。
Understanding Kinesis sequence numbers
したがって、最初の試みでは、PartitionKey
パラメータを使用してtraceparent
ヘッダを伝播します。
このため、次のサンプルコードブロックのように、W3Cコンテキストヘッダ形式でtraceparent
ヘッダをPartitionKey
パラメータに手動で設定します。
private PutRecordRequest injectTraceHeader(PutRecordRequest request){ if (!TRACE_CONTEXT_PROPAGATION_ENABLED) { return request; } Span currentSpan = Span.current(); if (currentSpan == null) { return request; } SpanContext currentSpanContext = currentSpan.getSpanContext(); if (currentSpanContext == null) { return request; } PutRecordRequest.Builder requestBuilder = request.toBuilder(); String traceParent = String.format("00-%s-%s-%s", currentSpanContext.getTraceId(), currentSpanContext.getSpanId(), currentSpanContext.getTraceFlags().asHex()); requestBuilder.partitionKey(traceParent); return requestBuilder.build(); }
これを行うことで、PartitionKey
パラメータを介してtraceparent
ヘッダをKinesisイベント内でevent-consumer
のAWS Lambda関数に渡すことができます。
しかし、ここで使用する方法は標準的な方法ではないため、event-consumer
関数側で、このPartitionKey
パラメータからtraceparent
ヘッダを手動で抽出し、それをトレースコンテキストに渡す必要があります。
こうすることで、event-consumer
側のトレースはここで伝播されたトレースIDを使用でき、結果としてevent-producer
およびevent-consumer
の両方のアプリケーションが同じトレース内に表示されるようになります。
... public class KinesisHandler extends TracingRequestHandler { private static final String TRACE_PARENT = "traceparent"; private static final String TRACE_PARENT_PREFIX = "00-"; ... @Override protected Void doHandleRequest(KinesisEvent event, Context context) { ... return null; } @Override protected Map extractHttpHeaders(KinesisEvent event) { List records = event.getRecords(); if (!records.isEmpty()) { Map headers = new HashMap<>(); KinesisEvent.KinesisEventRecord record = records.get(0); String partitionKey = record.getKinesis().getPartitionKey(); if (partitionKey != null && partitionKey.startsWith(TRACE_PARENT_PREFIX)) { headers.put(TRACE_PARENT, partitionKey); } if (!headers.isEmpty()) { return headers; } } return super.extractHttpHeaders(event); } }
これらの変更を行い、再度テストシナリオを実行すると、予想通り、前回のテストで別々のトレースにあったevent-producer
およびevent-consumer
アプリケーションが、今度は同じトレースに含まれることがわかります。
これは、PutRecord
リクエストのPartitionKey
パラメータを介してトレースIDが伝播されたためです。
この方法でトレースIDを伝播することはできましたが、このアプローチには以下の副作用がありました。
PartitionKey
パラメータにトレースペアレントヘッダを設定したため、イベントはイベントグループIDではなくtraceparent
ヘッダに基づいてシャードにランダムに分配されるようになりました。
これにより、アプリケーションのビジネスロジックが変更されます。
言い換えると、event-consumer
側に同じグループに属するイベントを同じシャードに配置し、順次処理する必要があるロジックがある場合、このロジックが機能しなくなります。
AWS CloudWatchログからわかるように、traceparent
ヘッダを注入する際にPartitionKey
パラメータをtraceparent
ヘッダの値に置き換えたため、同じグループIDを持つイベントが異なるシャードに分割されています。
その結果、イベントは生成されたトレースID(およびスパンID)に基づいてKinesisシャード全体にランダムに分割され、同じグループ内のイベントが並行して処理されます。
それぞれのイベントが異なるシャードに割り当てられ、各シャードは異なるAWS Lambda関数のマイクロVMインスタンスによって並行して処理されるため、この動作はevent-consumer
関数のビジネスロジックを破壊します。
私たちのビジネスロジックでは、同じグループ内のイベントは、同じグループIDの以前に処理されたイベントを確認して順次処理されることを期待していますが、同じグループのイベントの並行処理は競合状態を引き起こす可能性があります。
パート2の結論
私たちの探求により、PartitionKey
を使用したトレースコンテキストの伝播の初期戦略は一見有望に見えたものの、アプリケーションのビジネス要件にとって重要なイベントのシーケンスロジックを無意識に乱してしまうことが判明しました。
これは分散トレーシングにおける重要な課題を浮き彫りにしています。
トレーサビリティとシステムの機能的整合性のバランスを取る必要があるということです。
次回のシリーズ最終投稿では、この微妙なバランスを保つために、イベントの順序とトレーシングの明確さの両方を保つための代替ソリューションを探求します。