SpeedData

システムトレース

AWS Kinesis 上で OpenTelemetry トレースヘッダを伝搬する方法: パート2

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つの相互接続されたサービス、イベントを生成して送信するプロデューサーと、それを受信して処理するコンシューマーを使用します。
この初期シナリオでは、システムに変更を加えることなく、サービス間でのトレース伝播の自然な挙動を観察することができます。

テストシナリオと設定をシンプルに保ち、主要な問題に集中しました。

※訳注: シャードとは、巨大なデータセットを複数の小さな部分に分割し、それぞれを異なるサーバーやノードに保存する方法です。
データのスケーラビリティとパフォーマンスを向上させるために使用されます。
Kinesisでは、シャードはデータストリームを構成する基本単位であり、各シャードは独立してデータの読み書きを処理します。

テストシナリオを実行すると、event-producerevent-consumerの両方のアプリケーションがOTEL SDKによって自動的にトレースされますが、同じフローに属しているにもかかわらず、互いに独立した2つの異なるトレースが作成されます。

event-producerアプリケーションのトレース
event-consumerアプリケーションのトレース

最初の試み - PartitionKeyを通じたトレースコンテキストの伝播

AWS KinesisのPut Recordリクエストモデルを確認すると、以下の6つのパラメータがあることが分かります。

しかし、traceparentヘッダを伝播するために、これらの多くのパラメータを使用したり変更したりすることはできません。

したがって、最初の試みでは、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を持つevent-producerevent-consumerアプリケーションのトレース(ブローカーのパーティショニング)

この方法でトレースIDを伝播することはできましたが、このアプローチには以下の副作用がありました。
PartitionKeyパラメータにトレースペアレントヘッダを設定したため、イベントはイベントグループIDではなくtraceparentヘッダに基づいてシャードにランダムに分配されるようになりました。
これにより、アプリケーションのビジネスロジックが変更されます。

言い換えると、event-consumer側に同じグループに属するイベントを同じシャードに配置し、順次処理する必要があるロジックがある場合、このロジックが機能しなくなります。

グループID group-1を持つ1番目のイベントを処理するためのevent-consumer関数の呼び出しログ
グループID group-1を持つ2番目のイベントを処理するためのevent-consumer関数の呼び出しログ

AWS CloudWatchログからわかるように、traceparentヘッダを注入する際にPartitionKeyパラメータをtraceparentヘッダの値に置き換えたため、同じグループIDを持つイベントが異なるシャードに分割されています。
その結果、イベントは生成されたトレースID(およびスパンID)に基づいてKinesisシャード全体にランダムに分割され、同じグループ内のイベントが並行して処理されます。
それぞれのイベントが異なるシャードに割り当てられ、各シャードは異なるAWS Lambda関数のマイクロVMインスタンスによって並行して処理されるため、この動作はevent-consumer関数のビジネスロジックを破壊します。

私たちのビジネスロジックでは、同じグループ内のイベントは、同じグループIDの以前に処理されたイベントを確認して順次処理されることを期待していますが、同じグループのイベントの並行処理は競合状態を引き起こす可能性があります。

パート2の結論

私たちの探求により、PartitionKeyを使用したトレースコンテキストの伝播の初期戦略は一見有望に見えたものの、アプリケーションのビジネス要件にとって重要なイベントのシーケンスロジックを無意識に乱してしまうことが判明しました。
これは分散トレーシングにおける重要な課題を浮き彫りにしています。
トレーサビリティとシステムの機能的整合性のバランスを取る必要があるということです。

次回のシリーズ最終投稿では、この微妙なバランスを保つために、イベントの順序とトレーシングの明確さの両方を保つための代替ソリューションを探求します。