traceparent一行の構造
一言でいうと
traceparentヘッダーを次の呼び出しに引き継ぐことが、分散トレーシングのすべてです。自動計装がそれを代行してくれますが、コードがリクエストオブジェクトを新しく作った瞬間に途切れます。
なぜ必要なのか
「Istioを入れたからトレーシングはできている」という話はよくありますが、間違いです。サイドカーは自分が見たリクエストについてスパンを作れますが、サービスAが受け取ったリクエストと、AがBに送ったリクエストが同じトレースであることを、サイドカーは知りません。それをつなぐのはアプリケーションです。
W3C標準のヘッダーはこのような形をしています。
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
^^ ^------------ trace-id ---------^ ^-- span-id --^ ^^
버전 32자리(트레이스 전체) 16자리(이 구간) 플래그
このコードブロックの韓国語コメントは、順にバージョン、32桁(トレース全体)のtrace-id、16桁(このスパン)のspan-id、フラグを示しています。
最後の01がサンプリングフラグです。1なら「このトレースを記録する」、0なら「記録しない」を意味します。この決定は最初に1回だけ行われ、後ろへ伝播します。そうすることで、トレースが半分だけ残る事態を防げます。
どう動くのか
サービスAがBを呼ぶときにすべきことは、ただ1つです。
- 受け取ったリクエストの
traceparentを読みます - 新しいspan-idを作り、trace-idはそのままにして、送信するリクエストに付けます
自動計装(auto-instrumentation)がこれを代行します。ただし、次の場合に途切れます。
| 途切れる箇所 | 理由 |
|---|---|
| 新しいHTTPクライアントをコードで直接作る | 計装ライブラリがラップしていない経路です |
| キュー・メッセージで渡す | HTTPヘッダーがないため、メッセージ属性に直接入れる必要があります |
| スレッド/コルーチンを新しく起動する | コンテキストがスレッドローカルなので、引き継がれません |
| バッチ処理 | リクエスト1つにトレース1つ、という関係ではありません |
特にキューがよくあります。Kafkaで渡すときは、メッセージヘッダーにtraceparentを入れ、コンシューマーがそれを取り出してコンテキストを復元する必要があります。やらないと、プロデューサー側のトレースとコンシューマー側のトレースが互いに無関係になってしまいます。
サンプリング
すべて記録するとコストが高くなります。そのためサンプリングします。方式は2つあります。
ヘッドサンプリング: 最初の時点で「これは記録する」を決めます。安くてシンプルですが、遅いリクエストだけを選んで見ることはできません。開始時点ではそれが遅くなるかどうかわからないからです。
テールサンプリング: トレースが終わったあとに判断します。「エラーがあるものか、1秒を超えたものだけを保存」といった具合です。ほしいものを正確に選べますが、コレクターがトレース全体をメモリにためておいてから判断する必要があるため高価で、同じトレースのスパンが同じコレクターに届くようにルーティングする必要があります。
実務での組み合わせは、たいてい次のとおりです。ヘッドで10–20%に絞り、エラーは100%強制し、重要なエンドポイントだけにテールサンプリングをかけます。
よくある勘違い
「スパンは多いほどよい」という考え: 関数ごとにスパンを作ると、トレース1つが数千スパンになり、UIで読めず、保存コストが爆発します。スパンはネットワーク境界と遅い処理に置きます。
属性に個人情報を入れること: スパンの属性はそのまま保存され、検索されます。ユーザーのメールアドレスやトークンを入れると、それがトレーシングバックエンドに平文でたまります。
スパンに何を書くか
スパンは名前と時間だけでは、調査にあまり役立ちません。そのスパンが何をしたのかを区別できる属性が付いていて初めて、遅いもの同士の共通点を探せます。ただし、何でも入れると保存コストと個人情報の問題が一緒に生じるため、基準が必要です。
入れる価値があるのは、おおむねその値で結果を切り分けられるものです。
- 呼び出し先(サービス名、正規化したパス、データベース名)
- 結果(ステータスコード、エラーの種類)
- 規模(取得した行数、本文のサイズ、バッチのサイズ)
- 条件(キャッシュヒットの有無、どの経路を通ったか、リトライ回数)
規模と条件は特に価値があります。「このクエリが遅い」より「このクエリは結果が1万行のときだけ遅い」のほうがはるかに速く原因に届き、その区別は行数を属性として書き残しておいて初めて可能になります。
逆に、入れてはいけないものもはっきりしています。個人情報と認証情報はもちろん、値の種類が事実上無限のものにも注意が必要です。トレーシングバックエンドは属性で検索できるようにインデックスを作るため、リクエスト本文をまるごと入れたり、正規化していないパスを入れたりすると、インデックスが爆発します。前にメトリクスのラベルについて話したのと同じことです。
エラーを記録する方式も決めておくとよいです。スパンには成功・失敗を表すステータスが別にあるため、それを正確に設定することが先決です。この値がないと、「失敗したトレースだけを見る」が動作しません。例外の内容はスパンイベントとして残しますが、スタックトレース全体を入れるかどうかは、保存コストとあわせて判断します。たいていはエラーの種類とメッセージ1行で十分で、詳細はログにあればよいです。前に述べたようにログとトレースがつながっていれば、そちらへ移動すればよいためです。
実務で本当に大切なこと
トレースとログをつなぐことが、実際の効用の大部分です。ログの1行にtrace_idを入れておけば、遅いトレースを見つけたときに、そのリクエストが残したログを一度に探せます。このつながりがなければ、トレーシングはきれいな絵にとどまります。