TT Lab
はじめる
学ぶ 学習パス コース

OTCA — OpenTelemetry認定アソシエイト

自動計装がくれるのはネットワーク境界一つだけだ

TT Labで続きを見る

一言でいうと

自動計装を有効にして得られるのは、ちょうど1つ、ネットワーク境界です。HTTPのサーバーとクライアント、gRPC、DBドライバー、Redis、メッセージキューのクライアントにスパンができます。プロセスの中で起きることはどのスパンにも現れず、親スパンのself timeという空白としてだけ見えます。

なぜ必要なのか

計装に失敗するチームは、ほぼ同じやり方で失敗します。コードに手動スパンを仕込むところから始めて、2週間後にはスパンが300個あるのに、トレースは相変わらずサービス境界で途切れている状態になります。うまく機能する順序は決まっています。

順序 やること 飛ばすと
1 自動計装を有効にして、データが届くことを確認する 以降のデバッグがすべて推測になります
2 リソース属性を確定する あとから変えると過去のデータとつながらなくなります
3 サービス境界をまたぐ伝播を検証する スパンを増やしてもトレースが断片化します
4 self timeが大きいスパンにだけ手動スパンを入れる 自動計装の空白が永遠に残ります
5 加工とサンプリングをコレクターへ移行する ポリシーを変えるたびに全サービスを再デプロイすることになります

順序3が順序4より前に来る点が核心です。伝播が切れた状態で手動スパンを足すのは、断片化したトレースをさらに細かく断片化することです。

どう動くのか

自動計装を有効にすると、次のようなトレースが出ます。

SERVER    checkout-api   POST /v1/orders                       1421ms
├─ CLIENT    GET http://auth.internal/verify                      31ms
├─ CLIENT    SELECT carts WHERE id = ?                             6ms
├─ CLIENT    redis GET promo:rules:t-8871                          2ms
├─ CLIENT    POST http://payment.internal/charge                  74ms
└─ (나머지 1308ms 는 어떤 스팬에도 속하지 않음)

最後の行がすべてです。自動計装は、どこが問題ではないかを1,308msの空白で教えてくれます。その空白がself timeで、手動スパンはここにだけ入れます。

自動計装が絶対に見えないものは次のとおりです。プロセス内のCPU作業(シリアライズ・圧縮・テンプレートのレンダリング・暗号化)、ロック待ちとコネクションプール待ち、GILの競合とイベントループの遅延、計装パッケージがないサードパーティSDKの呼び出し、そしてビジネスロジックの分岐です。

手動スパンを入れる場所は5か所です。

  1. ループとバッチの境界(繰り返し回数を属性として残します)
  2. キャッシュの参照(ヒットしたかどうかを属性として残すと、キャッシュ効率がトレースからすぐ見えます)
  3. 計装パッケージがないサードパーティSDKの呼び出し
  4. ロック、キュー、コネクションプールの待ち
  5. CPUを長く使う処理(シリアライズ、圧縮、レポート生成)

入れると、先ほどの空白が次のように埋まります。

├─ INTERNAL  checkout.apply_promotions                          1298ms
│  ├─ INTERNAL  promotion.load_rules   cache.hit=false           1241ms  <-- 여기
│  └─ INTERNAL  promotion.evaluate     evaluated=812               54ms

守るべきスパン名のルールは1つだけです。名前は低カーディナリティでなければなりません。GET /v1/orders/A-99183ではなくGET /v1/orders/:idにし、具体的な値はすべて属性に入れます。バックエンドはスパン名でグループ化してレイテンシの統計とサービスグラフを作るため、名前にIDが入ると、その集計ビュー全体が崩れます。

リソース属性はあとから直せません。スパン属性と違い、リソース属性はそのプロセスが出力するすべてのシグナルに付き、service.nameを変えた瞬間に、ダッシュボード・アラート・サービスグラフ・過去データとのつながりがすべて切れます。

属性 例 変更できるか
service.name checkout-api 実質的に不可
service.namespace commerce 難しい
service.version 2.7.1 デプロイのたびに変わる
deployment.environment.name prod 不可
service.instance.id Pod名 再起動のたびに変わる

ここでよく間違えるものが2つあります。1つ目は、環境属性の名前はdeployment.environment.nameだということです。昔の名前deployment.environmentはもう使われておらず、名前が違うと2つの別々の属性になり、ダッシュボードの変数はそのうち片方しか読みません。2つ目は、service.nameはデプロイ単位ではなくサービス単位だということです。カナリアをcheckout-api-canaryと呼んだ瞬間に、サービスグラフに幽霊ノードができます。

サンプラーは、最初はparentbased_always_onで始めることをお勧めします。最初から比率サンプリングを有効にすると、トレースが見えないときに、計装の問題なのかサンプリングのせいなのか区別できません。比率に移るときも、必ずparentbased_traceidratioを使います。親の判断に従わず、サービスごとに独立して確率判定を行うと、トレースが途中で切れてしまいます。

現場での姿

計装がアプリを壊す形は、いくつかのパターンの繰り返しです。

症状 原因 対応
デプロイ後にメモリが増え続ける バックエンドの遅延でエクスポーターのキューが満杯になる キューの上限を明示し、コレクター経由に切り替える
スパンが一部しか届かない 終了時にflushせずにプロセスが終了する shutdownを呼び出し、終了までの猶予時間を延ばす
1つのスパンが数百KBになる リクエスト本文全体を属性に入れている 属性値の長さに上限を設定する
サービスグラフに幽霊ノードが出る カナリアを別のservice.nameでデプロイしている service.nameはサービス単位で固定する

属性値の長さには、SDKのデフォルトの上限がありません。属性の個数はデフォルトで128個に制限されますが、値の長さは無制限なので、リクエスト本文がそのまま入ると、送信と保存のコストがそのまま跳ね返ってきます。明示的に上限を設定しておくほうが安全です。

次のラボですること

/root/otca-sdk/の下にSDKの環境変数ファイルを書き、リソース属性を規約に合わせて決め、KubernetesのDownward APIでPod名とネームスペースを注入するDeploymentを作って実際に適用します。最後に、スパン名の一覧を整理し、IDが入った名前を検出するリンターを自分で作成します。