分散トレーシング

このページの目次

リクエストがゲートウェイ、注文サービス、在庫サービス、データベースを通過する際、各ノードのログは局所的な事象しか記録しません。分散トレーシングは、これらの局所的な操作を因果関係に基づいて関連付け、「このリクエストはどの経路を通り、どこに時間がかかったか」という問いに答えます。

これはメトリクスやログと補完関係にあります:メトリクスは大量のリクエストの分布を記述し、トレースは特定のリクエストの構造を示し、ログは局所的なイベントの詳細を保持します。1つのトレースは1つの遅延リクエストを説明できますが、それだけでは全体の障害率を代表することはできません。全体測定の詳細は メトリクスとアラート を参照してください。

Trace と Span:時間区間と因果関係

Trace は関連する一連の操作の集合であり、span はその中の開始と終了時刻を持つ操作です。例えば、サーバー側リクエスト、データベースアクセス、または内部計算フェーズなどが該当します。span には通常、名前、ID、親スパン、属性、イベント、ステータスが含まれます。

親子関係は「この操作がどのコンテキストで開始されたか」を表しますが、「親スパンの所要時間が全ての子スパンの所要時間の合計に等しい」という意味ではありません。複数の子操作が並列に実行される場合、親操作には計測されていない計算、キュー待ち、シリアライズ時間などが含まれる可能性があります。

1つのリクエストのタイムライン:並列ブランチは単純に足せない リクエストエントリポイント 120 ms 在庫 RPC 70 ms 価格 RPC 100 ms レスポンス整形 10 ms 0 30 60 90 120 エンドツーエンド 120 ms;70 ms と 100 ms の2つのブランチが重複しており、クリティカルパスは価格 RPC を通ります。

図において、在庫と価格は同時に開始されます。エンドツーエンドの所要時間は、より遅い価格ブランチと、前後のフェーズによって決定されます。全てのスパンの所要時間を単純に合計すると、重複する時間が二重に計算されてしまいます。クリティカルパスとは、全体の完了時刻を制限する依存関係のチェーンのことです。クリティカルパス上にない作業を短縮しても、必ずしもこのリクエストのレイテンシが低下するわけではありません。

さらに、ウォールクロックタイム(wall time)と CPU タイムを区別する必要があります。ある RPC スパンが 100 ms 持続した場合、その大部分はネットワーク待ちやスレッドスケジューリングのために費やされている可能性があります。トレーシングは範囲を絞り込むことができますが、CPU のホットスポットを特定するには、通常、サンプリングプロファイリングツールによる確認が必要です。

水落図から改善可能な量を算出し、スパンの長さを単純に足さない

図の依存関係を引き続き使用します:準備(10 ms)の後、在庫と価格の RPC を並列に開始し、両方が終了してからレスポンスの整形(10 ms)を行います。あるブランチの短縮が他のフェーズに影響を与えないと仮定すると、各変更がエンドツーエンドの所要時間に与える影響を計算できます。

def latency(stock, price):
    return 10 + max(stock, price) + 10

assert latency(70, 100) == 120
assert latency(30, 100) == 120  # 在庫が 40 ms 短縮しても、全体は価格を待つ
assert latency(70, 80) == 100
assert latency(70, 50) == 90    # クリティカルパスは在庫に移行した

価格を 100 ms から 50 ms に下げると、全体は 120 ms から 90 ms に短縮されます。これは、在庫が完了時刻を制限し始めているためです。この推論は「両方の RPC が完了する必要がある」という既知のビジネス構造に依存しています。もし「最初に成功したものを使用する」「タイムアウトでフォールバックする」「他のブランチをキャンセルする」などのロジックがある場合、式は変化し、単に2つの重複する時間バーから待ちのセマンティクスを推測することはできません。

親スパンの所要時間から子スパンの所要時間を引く際にも、重複を除く必要があります。ルート区間 [0,120)、在庫 [10,80)、価格 [10,110) とすると、2つの子区間の合計長さは 170 ですが、カバーされる時間の和集合は 100 に過ぎません。これら2つの RPC でカバーされていない区間の合計は 20 です。

def covered(intervals, parent):
    lo, hi = parent
    assert lo <= hi
    clipped = []
    for start, end in intervals:
        if start > end:
            raise ValueError("reversed interval")
        start, end = max(start, lo), min(end, hi)
        if start < end:
            clipped.append((start, end))
    total, right = 0, lo
    for start, end in sorted(clipped):
        total += max(0, end - max(start, right))
        right = max(right, end)
    return total

assert covered([(10, 80), (10, 110)], (0, 120)) == 100
assert covered([(10, 110), (10, 110)], (0, 120)) == 100
assert covered([(-5, 10), (110, 140)], (0, 120)) == 20
assert covered([], (0, 120)) == 0

20 ms は「選択された子区間でカバーされていないウォールクロックタイム」に過ぎず、キュー待ち、計算、シリアライズ、または計測の欠落を含む可能性があります。これを直接「親サービスの CPU 時間」と呼ぶことはできません。この区間モデルは、すべての時間が比較可能な同じタイムライン上にあることを前提としています。マシン間の生タイムスタンプにオフセットがある場合、まずクロックの問題を処理する必要があり、切り詰め処理によってデータ異常が隠れてしまうのを防がなければなりません。

ブランチを1つ短縮した場合、全体のリクエストはどれだけ速くなるか?

以下の図では、2つの RPC が準備終了の起点を共有し、整形フェーズがより遅い終了点に追従するようになっています。価格が 100 から 70 ms に下がると、全体を短縮できます。さらに短縮すると、在庫ブランチが総所要時間を決定し始めます。図では金色で現在完了を制限しているパスが示されており、両方が同じ長さの場合、両方のブランチがクリティカルパスとなります。これは、最長スパンに目立つマークを付けるだけでなく、最適化の境界をよりよく説明します。

図を準備しています
ブランチを1つ短縮した場合、全体のリクエストはどれだけ速くなるか?

並列でかつすべて完了する必要がある場合、総レイテンシはより長いブランチによって決まります。短いブランチを短縮しても、現在のクリティカルパスは変化しません。子スパンの所要時間の合計は、重複する時間を二重に計算してしまいます。

コンテキスト伝播:分散呼び出しをどのように接続するか

単一プロセス内では、実行コンテキストはスレッドローカル変数や非同期タスクのメカニズムを通じて渡すことができます。プロセスを跨ぐ場合、通信キャリアに明示的にエンコードする必要があります。OpenTelemetry では、送信側が書き込むことを inject、受信側が読み込むことを extract と呼びます。

リクエスト受信時にコンテキストを抽出し、送信時にコンテキストを注入する A:クライアントスパン trace=T、span=A1 B:サーバースパン trace=T、span=B1 B:クライアントスパン trace=T、span=B2 リクエストヘッダー:T / A1 B1 の parent = A1 後続のリクエストで T / B2 を伝播 同じ trace ID が呼び出しを繋ぎます。各スパンは独立した ID を持ち、親子関係が因果関係を表現します。

正しい順序は以下の通りです:A がクライアントスパンを作成し、そのコンテキストをリクエストに書き込みます。B はリクエストを受信した時点でコンテキストを抽出し、サーバースパンを作成します。B が次のサービスを呼び出す際、新しいクライアントスパンを作成し、次のリクエストに注入します。これは、レスポンスを受信してから下流が親子関係を設定するのとは異なります。OpenTelemetry コンテキスト伝播 は、プロパゲーターと実行コンテキストの関係について説明しています。

W3C traceparent のバージョン 00 の構造は以下の通りです。これはプロトコルフォーマットであり、フローチャートではありません:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
フィールド意味
00フォーマットバージョン
32 桁の16進数trace ID。呼び出しを跨いで関連性を維持します
16 桁の16進数送信側の現在スパン ID。受信側のリモート親となります
01trace フラグ。この例では sampled ビットが設定されています

tracestate はベンダー固有のトレース状態を運ぶことができます。gRPC メタデータやメッセージヘッダーも伝播フィールドを運ぶことができ、歴史的なバイナリヘッダーをすべての gRPC トレースの唯一のフォーマットとして扱うべきではありません。フィールドの検証とバージョンの動作は W3C Trace Context に準拠します。

非同期タスクとメッセージ:親子ツリーが常に十分とは限らない

同期 RPC には通常、明確な呼び出し元が1つあります。メッセージのバッチ処理では、多くのソースが1つの操作に流れ込む可能性があります。1つのスパンには親が1つだけですが、links を使用して複数のスパンコンテキストに関連付けることができます。100個のメッセージのソースを無理やり単一の親チェーンに詰め込むと、バッチ処理の真の関連性が失われます。

また、「メッセージがキューで待機している状態」と「コンシューマーが実際に処理している状態」を別々に表現する必要があります。プロデューサーの送信完了からコンシューマーの処理開始までの時間は、キュー残留時間に属する可能性があります。コンシューマーが作成した処理スパンが実行フェーズのみをカバーする場合、その所要時間をエンドツーエンドのメッセージレイテンシとして偽って使用することはできません。

タスクプールももう1つの一般的な境界です。スレッド切り替え後にコンテキストがキャプチャおよび復元されない場合、後続のスパンは新しいルートになります。誤って古いコンテキストを再利用すると、無関係なリクエストが接続されてしまう可能性があります。非同期伝播は、スレッドのライフサイクルだけでなく、タスクのライフサイクルに合わせて処理する必要があります。

関連するデータモデルについては OpenTelemetry Traces を参照してください。

サンプリング:コストと解釈可能性のトレードオフ

サンプリングの位置が異なれば、意思決定に使用できる情報も異なる SDK / リクエスト開始時 開始点の情報しか見えない Collector / 待機ウィンドウ 一部または大部分のスパンが既にある トレースバックエンド 保存、検索、分析 Head:開始点に基づいて記録を決定 Tail:観察された結果に基づいて保持を決定 アップストリームで破棄されたトレースは、ダウンストリームのテールサンプリングでは復元できません。テールサンプリングも待機時間と容量の制約を受けます。

ヘッドサンプリングは早期の段階で記録するかどうかを決定するため、SDK と転送のコストを制御しやすいです。確率的サンプリングは代表的なサンプルを近似するために適していますが、低頻度のエラーが偶然すべて見逃される可能性があります。関連サービスは互換性のあるサンプリング決定を維持する必要があり、そうでないと切断されたトレースになりやすくなります。

テールサンプリングはスパンを収集した後、エラー、レイテンシ、または属性に基づいてどのトレースを保持するかを決定します。代償として、データの受信、キャッシュの維持、待機が必要です。同じトレースのスパンは、共同で決定を行うことができる位置にルーティングされる必要があります。遅延スパン、超長時間タスク、容量不足は完全性に影響を与えます。OpenTelemetry サンプリング は、両者の能力の境界について説明しています。

カスタム容量の見積もり:1秒あたり 2000 トレース、平均 8 スパン、スパンあたり 1 KB のキャッシュ、15 秒の待機を仮定すると、これらのスパンのみの原始容量は 2000 × 8 × 1 KB × 15 = 240 MB 程度です。実際には、インデックス、オブジェクト、キューのオーバーヘッドも加算されます。この見積もりは成長の方向性を理解するためのものであり、製品のパフォーマンス保証ではありません。

テールサンプリングが観察されたエラーをすべて保持しても、システムで発生したすべてのエラーを保持したわけではありません。アップストリームに計測がない、エクスポートがない、またはヘッドサンプリングで既に破棄されている可能性があります。「エラートレース数 / 保持トレース数」で全体のエラー率を直接計算することはできません。サンプルは異常リクエストに偏っているためです。

サンプル内のエラー率が 0.1% から約 9.1% に変わる理由

100,000 回のリクエストのうち 100 回がエラーであり、実際のエラー率が 0.1% であると仮定します。ここで、すべてのエラーを保持し、正常なリクエストの 1% のみを保持します。このバッチで正常なトレースが 999 個残った場合、サンプル内のエラー比率は 100/1099 ≈ 9.10% となります。2つの比率は異なる母集団を記述しており、互いに代替できません。

from fractions import Fraction
all_errors, all_successes = 100, 99900
kept_errors, kept_successes = 100, 999
true_rate = Fraction(all_errors, all_errors + all_successes)
sample_rate = Fraction(kept_errors, kept_errors + kept_successes)
assert true_rate == Fraction(1, 1000)
assert Fraction(9, 100) < sample_rate < Fraction(10, 100)
# 実際の各カテゴリの纳入確率と、他の損失がないことが既知のモデルでのみ、重み付け推定を行う。
estimated_errors = Fraction(kept_errors, 1)
estimated_successes = Fraction(kept_successes, 1) / Fraction(1, 100)
assert estimated_errors / (estimated_errors + estimated_successes) == true_rate

重み付けの式は、任意のテールサンプリングデータの一般的な修正ではありません。実際には、レイテンシ、属性、容量ポリシーに基づいて破棄される可能性があり、ルーティングやエクスポートの失敗も纳入確率を変更します。未知の損失は、最後に残ったトレースからは自動的に復元できません。確率が既知であっても、有限のランダムサンプルの比率推定にはばらつきがあり、この例はちょうど対応する数量を取得した計算例に過ぎません。

したがって、エラー率と SLO(Service Level Objective)は、セマンティクスが一貫したリクエストメトリクスによって通常統計処理され、トレースは選択されたリクエストがなぜ失敗したかを説明するために使用されます。メトリクスがスパンから派生する場合、それがサンプリング前かサンプリング後に生成されたかを明確にする必要があり、そうでないと同じ選択バイアスを継承する可能性があります。OpenTelemetry サンプリング

エラー優先保持後、サンプルの構成はすでに変化している 元のリクエスト 100000 エラー 100;正常 99900 異なる確率で保持 エラー 100%;正常 1% サンプル 1099 エラー 100;正常 999 母集団のエラー率 0.1% → サンプルのエラー比率 約 9.10% サンプルはエラーの詳細を調べるのに適していますが、全体のサービス品質が突然約 91 倍悪化したことを意味するものではありません。

収集パイプラインと責任

一般的なパイプラインは、アプリケーションインストルメンテーション、SDK、Collector、ストレージおよびクエリバックエンドで構成されます。インストルメンテーションは操作の境界を選択する責任を負います。SDK はスパンのライフサイクルとエクスポートを管理します。Collector はバッチ処理、フィルタリング、サンプリング、ルーティングを引き受けることができます。バックエンドは保存と検索を担当します。

自動インストルメンテーションは通常、HTTP、RPC、データベースの境界から始め、手動スパンはビジネス意味のあるフェーズを補完するために使用されます。すべての微小関数にスパンを追加すると、オーバーヘッドが増加し、呼び出し図が読みづらくなります。スパン名は安定しているべきです。例えば、ルートテンプレート /orders/{id} のようにし、特定のリクエスト ID は管理された属性に配置します。

Baggage はコンテキストとともに伝播されるアプリケーションのキーバリューペアであり、スパン属性でもなく、認証されたアイデンティティ主張でもありません。エントリポイントで外部コンテキストを受け入れる際は、検証と信頼境界が必要です。キーや個人情報全体を、サービス間で伝播されるフィールドに配置しないでください。詳細は OpenTelemetry Baggage を参照してください。

遅延リクエストの読み方

まず、トレースが完全かどうかを確認し、ルートスパンの総所要時間からクリティカルパスを探します。親スパンが長く、すべての子スパンが短い場合、カバーされていないキュー待ち、アプリケーション計算、シリアライズフェーズを確認します。あるクライアントスパンが長く、サーバースパンが短い場合、接続確立、ネットワーク、プロキシ、またはサーバー側の計測以外の待ち時間を検討します。

マシン間のクロックにはずれがある可能性があるため、2つのノードの絶対タイムスタンプのみに基づいて正確なネットワークレイテンシを主張することはできません。トレースの parent/link 関係は因果関係の手がかりを提供し、タイムラインは観察値を提供します。これらを混同して、完璧なグローバルクロックを作るべきではありません。

最後に、同種のリクエストのメトリクスを使用して、この現象が一般的かどうかを検証し、トレース ID を介してログやプロファイリングデータにジャンプします。トレーシングの役割は、仮説を提起し、説明を絞り込むことであり、ウォーターフォールチャートがすべての証拠に取って代わるものではありません。

参考

キーワード:trace、span、コンテキスト伝播、クリティカルパス、links、ヘッドサンプリング、テールサンプリング、baggage