伝播が切れる四つの地点と、それぞれの処方
一言でいうと
トレースというツリーを作る唯一の仕組みは、コンテキスト伝播です。呼び出す側がW3Cのtraceparentヘッダーに現在のトレースIDとスパンIDを入れ、受け取る側がそれを読んで、自分のスパンの親にします。この鎖が途切れる場所は、ほぼ決まっています。スレッドプール、メッセージキュー、バックグラウンド処理、ヘッダーを消す中間層です。
なぜ必要なのか
本番のトレースを1つ開いてみたら、リクエストが通過したサービスは6つなのに、SERVERスパンが2つしかありません。この状態で手動スパンを追加しても、意味がありません。今週やるべきことは、残り4か所の伝播を復活させることです。
どう動くのか
traceparentの分解
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
-- -------------------------------- ---------------- --
| | | |
| | | +-- flags: 01 이면 샘플링됨, 00 이면 저장 안 됨
| | +-- parent-id: 16자리 16진수(8바이트). 호출 단계마다 바뀐다
| +-- trace-id: 32자리 16진수(16바이트). 요청 전체에서 동일하다
+-- version: 현재 00
このコードブロックの韓国語のコメントは、順に、flagsは01ならサンプリング済みで00なら保存されないこと、parent-idは16桁の16進数(8バイト)で呼び出しの段階ごとに変わること、trace-idは32桁の16進数(16バイト)でリクエスト全体で同じであること、versionは現在00であることを述べています。
tracestateはベンダー固有の付加情報を運ぶ補助ヘッダーで、baggageはユーザー定義のキーと値をサービス境界の向こうへ運ぶ別の規約です。baggageはスパン属性ではないため、自動では保存されません。
途切れ1: スレッドプールとexecutor。伝播されるかどうかは、ランタイムと使用するAPIによって異なります。計装ラッパーのない通常のThreadPoolExecutor.submitは、このラボ環境では呼び出し元のコンテキストを自動で引き継ぎません。一方、Python 3.12のasyncio Taskとto_threadはContextのコピーを提供するので、コルーチンの生成と実際のスケジュール時点を区別する必要があります。自動対応がない境界では、呼び出し側でコンテキストをキャプチャしてワーカー内で有効化し、実行後にワーカーの以前の状態も復元します。あとのコンテキスト境界のラボで、2つの注文と再利用されるワーカーを使って自分で確認します。
途切れ2: メッセージキュー。キューはプロセスの境界であり、時間の境界でもあります。HTTPのようにヘッダーが自動では流れないため、プロデューサーがメッセージヘッダーにコンテキストを注入し、コンシューマーが抽出する必要があります。ここで重要な設計判断が1つあります。バッチコンシューマーは、親子ではなくLinkを使う必要があります。メッセージ100個を一度に処理すると親が100個になりますが、スパンは親を1つしか持てないからです。キューの待ち時間が長いパイプラインは、メッセージが1つでもLinkを検討します。親子でつなぐと、1つのトレースの所要時間が待ち時間の分だけ延び、バックエンドで扱いにくくなります。
途切れ3: バックグラウンド処理とスケジューラー。cronやバックグラウンドタスクには親がありません。よくある間違いは、リクエストのコンテキストを無理につなげることです。リクエストはすでにレスポンスを返して終わっているのに、そのトレースに30秒の子が付くと、リクエストのレイテンシ統計が汚染されます。処方は、新しいルートトレースとして開始し、原因となったリクエストはLinkとして残すことです。
途切れ4: ヘッダーを消す中間層。プロキシ、WAF、APIゲートウェイ、CDNが許可リスト方式でヘッダーをフィルタリングすると、traceparentが黙って消えます。ログには何も残らず、症状は「ゲートウェイの後ろからトレースが新しく始まる」です。許可リストにtraceparent、tracestate、baggageがあるかを確認し、B3ヘッダーを使うレガシーが混ざっている場合は、プロパゲーターを複数設定します。
リソース属性とスパン属性の境界も試験に出ます。
| 区分 | 対象 | 例 |
|---|---|---|
| リソース属性 | テレメトリを作った主体 | service.name, k8s.pod.name, host.name |
| スパン属性 | その1回の処理 | http.route, db.system, cart.item_count |
スパン属性の高カーディナリティは、メトリクスの場合と違って、爆発する問題ではありません。注文ID、ユーザーID、クエリパラメーターは、トレースに入れるためにある値であり、なければトレースはフィルタリングできない絵になります。問題になる場所は2か所です。スパンをメトリクスに変換するとき(spanmetricsのdimensionsに入れた属性が、そのままメトリクスのラベルになります)と、スパン自体のサイズです。
service.instance.idはカーディナリティが高いですが、リソース属性なので、トレースとログでは問題ありません。ただし、これをメトリクスのラベルに昇格させると、時系列がPodの数だけ掛け算になるため、コレクターでメトリクスのパイプラインに限って削除するのが一般的です。
現場での姿
伝播が生きているかを確認する最も安上がりな方法は、ゲートウェイのまねをすることです。既知のtrace IDをtraceparentヘッダーに直接入れてリクエストを送り、バックエンドでそのIDで検索したときに、スパンがその下に付くかを見ます。付かなければ、上の4つのどれかです。
計装が終わったと言える基準も、チェックリストにしておきます。本番のリクエストを1つ選んでトレースを開いたとき、関与したサービス数とSERVERスパンの数が一致するか、ルートスパンの所要時間がゲートウェイのアクセスログのレスポンス時間と合うか、最大のself timeが全体の20%未満か、trace IDでログが検索できるか、スパン名の上位20個がルートテンプレートであってIDではないか、キューをまたぐ処理が1つのトレースまたはLinkでつながっているか。最後の1項目、つまりコレクターを一度落としてみて、アプリのエラー率とレイテンシが揺らがないかを確認することは、実際にやってみないとわかりません。
次の確認で見ること
このモジュールは概念モジュールなので、ラボはありません。クイズでtraceparentのフィールドとLinkの判断基準を確認したあと、最後のモジュールでサンプリングポリシーとバッファメモリの計算に進みます。