メトリクスとトレースに同じ問いを答えさせる
目標
スパンダンプだけでリクエスト数・エラー数・レイテンシ分布を数え、メトリクス側のエクスポジション形式ファイルと比べて、名前と値がずれた箇所を見つけて計装を合わせます。サンプラーを切り替えながら、同じデータから異なるエラー率が出る様子を自分で作り、サンプリング確率の逆数で補正したあと、区間ごとに代表トレースを選ぶブリッジを作り、ルールをファイルに固めて、2つ目のサービスに適用します。
なぜ重要なのか
メトリクスとトレースは、たいてい別々に作られます。メトリクス側はフレームワークがルートテンプレートを入れてくれ、トレース側は手でアドレスを入れるため、系列9本のメトリクスと、30個を超えるスパンのグループが向き合うことになります。その状態で「このピークの遅いリクエストを1つだけ開いてみよう」と言っても、誰も答えられません。さらに悪いのは、スパンで割合を数えることです。エラーはすべて残し、成功は5件に1件だけ残すという、よくあるサンプリングポリシーのもとでは、スパンで数えたエラー率は実際の4倍近くに膨らみ、サンプリング確率をスパンに書いていなければ、元に戻す方法すらありません。2つのシグナルを同じ名前・同じ値で結びつけ、確率も一緒に書いておけば、個数と割合はメトリクスから読み、トレースは例を探すために使うという、本来の役割分担に戻ります。メトリクスSDKの累積とデルタは別のモジュールの役割で、ここで作るのは、属性ルールのファイルと、2つのシグナルを突き合わせる検査プログラムです。
ステップ
/root/tp-metrics/collect.pyを作成してください(デフォルトのダンプのパスは/root/tp-metrics/01-spans.jsonl)。材料tracelab.tp_metrics.trafficのSHOPを順に処理しながら、リクエストごとにルートスパンGET <주소>(プレースホルダーはアドレスです)を作り、スパンを開始するときに属性を5つ渡します。request.id・request.index・http.target(リクエストのpath)・http.response.status_code(リクエストのstatus)・user.id(リクエストのuser)です。スパンの中でtraffic.work(tracer, req)を呼んでください。そのあと/root/tp-metrics/01-from-spans.tsvに、タブで区切った2つの欄の4行を書きます。requests<탭><루트 스팬 수>、errors<탭><상태 코드 500 이상인 루트 수>、p50_ms<탭><값>、p95_ms<탭><값>(プレースホルダーは順に、タブ、ルートスパン数、ステータスコードが500以上のルート数、値です)。パーセンタイルは、ルートスパンのduration_msを昇順に並べて1から数え、올림(비율 × 개수)(プレースホルダーは、割合と個数の積の切り上げです)番目の値を選び、小数第3位まで書きます。- メトリクスパイプラインが出力したファイルが
/opt/app/tracelab/tp_metrics/metrics/shop-api.promにあります(Prometheusのエクスポジション形式)。/root/tp-metrics/02-mapping.tsvに、タブで区切った3つの欄の3行を書いてください。1つ目の欄はメトリクスのラベルで、順にhandler・code・svc、2つ目の欄は、ステップ1のダンプでそのラベルに対応させたい場所(http.target・http.response.status_code・service.name)、3つ目の欄は、その2つの値がそのまま合っているかどうかで、yesまたはnoです。そして/root/tp-metrics/02-gap.txtに2行を書きます。metric_series=の後ろにそのファイルのhttp_requests_totalの系列数、span_groups=の後ろに、ステップ1のダンプのルートスパンをhttp.targetの値でまとめたときに出てくるグループ数です。 /root/tp-metrics/aligned.pyを作成してください(デフォルトのダンプのパスは/root/tp-metrics/03-aligned.jsonl)。ステップ1と同じトラフィックを処理しますが、ルートスパンにhttp.route(リクエストのroute、つまりルートテンプレート)を加えて渡し、スパン名もGET <경로 틀>(プレースホルダーはルートテンプレートです)にします。調査に使うhttp.targetはそのままにします。サンプラーはsamplers.keep_all()を使います。実行したら、/root/tp-metrics/03-joined.tsvに、タブで区切った4つの欄を系列ごとに1行ずつ書いてください。<handler><탭><code><탭><지표 값><탭><그 짝의 루트 스팬 수>(プレースホルダーは順に、タブ、メトリクスの値、そのペアのルートスパン数です)で、handlerの昇順、同じならcodeの昇順です。2つの数字が系列ごとに同じである必要があります。/root/tp-metrics/sampled.pyを作成してください。コマンドライン引数としてnth5またはerrbiasを受け取り、それぞれsamplers.every_nth(5)とsamplers.errors_and_nth(5)をサンプラーとして使い、残りはステップ3とまったく同じように計装します。デフォルトのダンプのパスは/root/tp-metrics/04-<인자>.jsonl(プレースホルダーは引数です)です。2回実行して/root/tp-metrics/04-nth5.jsonlと/root/tp-metrics/04-errbias.jsonlを作ったあと、ステップ3のダンプまで含めた3つを使って、/root/tp-metrics/04-rates.tsvに、タブで区切った4つの欄の3行を書いてください。1つ目の欄は順にfull・nth5・errbias(fullはステップ3のダンプです)、続いて<오류 루트 수><탭><전체 루트 수><탭><비율>(プレースホルダーは順に、エラーのルート数、タブ、全体のルート数、割合です)で、割合は小数第4位までです。- 2つのサンプルダンプのルートスパンには、
sampling.probabilityが書かれています。スパン1つが代表する件数は、その値の逆数です。/root/tp-metrics/05-adjusted.tsvに、タブで区切った4つの欄の2行を書いてください。1つ目の欄は順にnth5・errbias、続いて<보정한 오류 수><탭><보정한 전체 수><탭><보정한 비율>(プレースホルダーは順に、補正したエラー数、タブ、補正した全体数、補正した割合です)で、前の2つの欄は小数第4位まで、割合も小数第4位までです。そして/root/tp-metrics/05-limits.txtに2行を書きます。limit1=とlimit2=の後ろに、補正でも元に戻せないものを、それぞれ40文字以上で書いてください。ステップ4のfullの割合と比べて、補正がどこまで合わせてくれるかを確認してください。 /root/tp-metrics/bridge.pyを作成してください(デフォルトのダンプのパスは/root/tp-metrics/06-bridge.jsonl)。ステップ3と同じように計装してダンプを残したあと、そのダンプを読み直して、系列ごとに最も遅いルートスパン1件を選び、表に書きます。表のパスは、ダンプのパスの.jsonlを.tsvに変えたものです(デフォルトなら/root/tp-metrics/06-bridge.tsv)。1行は、タブで区切った4つの欄<http.route><탭><상태 코드><탭><duration_ms(소수 셋째 자리)><탭><trace_id>(プレースホルダーは順に、http.route、タブ、ステータスコード、duration_ms(小数第3位)です)で、http.routeの昇順、同じならステータスコードの昇順です。そして/root/tp-metrics/06-limits.txtに、limit1=・limit2=の2行で、このブリッジが答えられないものを、それぞれ40文字以上で書いてください。/root/tp-metrics/07-contract.tsvに、タブで区切った3つの欄の7行を書いてください。1つ目の欄はスパン属性名で、順にhttp.route・http.response.status_code・service.name・http.target・request.id・user.id・sampling.probabilityです。2つ目の欄は、その属性がメトリクスで使うラベル名で、メトリクスに置かないものは-にします。3つ目の欄は、both(2つのシグナルに同じ値で置く)またはtrace-only(トレースにだけ置く)です。この表は、ステップ3のダンプと/opt/app/tracelab/tp_metrics/metrics/shop-api.promを突き合わせて、実際に合っている必要があります。bothの属性は、ステップ3のダンプのルートスパンにすべてあり、trace-onlyの属性名は、メトリクスファイルのラベルとして現れてはいけません。/root/tp-metrics/pay.pyを作成してください(デフォルトのダンプのパスは/root/tp-metrics/08-pay.jsonl)。材料のtraffic.PAYをサービス名pay-apiで計装し、ステップ7のルールの属性をそのまま残し、ルートスパン名はPOST <경로 틀>(プレースホルダーはルートテンプレートです)、サンプラーはsamplers.keep_all()です。そして/root/tp-metrics/agree.pyを作成してください。python3 agree.py <스팬덤프> <노출형식파일>(プレースホルダーはスパンダンプとエクスポジション形式のファイルです)で実行すると、系列ごとにメトリクスの値とルートスパン数を比べ、異なる系列ごとにmismatch<탭><handler><탭><code><탭><지표 값><탭><스팬 수>(プレースホルダーは順に、タブ、メトリクスの値、スパン数です)を1行ずつ出力して終了コード1で終わり、すべて同じならok<탭><계열 수>(プレースホルダーはタブと系列数です)の1行を出力して0で終わります。検査プログラムを/opt/app/tracelab/tp_metrics/metrics/pay-api.promに実行した出力を、/root/tp-metrics/08-agree.txtに保存してください。
参考
- 作業ディレクトリは
/root/tp-metricsです。なければ先に作ってください。 - 計装プログラムは必ず
/opt/otel-lab/bin/python <파일>(プレースホルダーはファイル名です)で実行します。システムのpython3にはOpenTelemetry SDKがありません。逆に、ダンプとメトリクスファイルだけを読むプログラムは、システムのpython3で実行してください。 - 材料は、トラフィック
/opt/app/tracelab/tp_metrics/traffic.py(SHOP120件・PAY80件)、サンプラー/opt/app/tracelab/tp_metrics/samplers.py、メトリクス側のエクスポジション形式ファイル/opt/app/tracelab/tp_metrics/metrics/shop-api.prom・pay-api.prom・pay-api-broken.promです。それらのファイルを作ったジェネレーターは/opt/app/tracelab/tp_metrics/make_metrics.pyで、共通の配線は/opt/app/tracelab/dump.py、ダンプ読み込みのヘルパーは/opt/lab/checks/_tplib.pyです。 - このイメージにはPrometheusがありません。メトリクスはエクスポジション形式のテキストファイルとして扱い、それを読んで比べるところまでがこのラボの範囲で、exemplarを実際に保存したり照会したりすることは、ここではできません。
- よくある間違い: サンプリングの判定に使われる属性を、
set_attributeであとから付けてしまいます。サンプリングの決定はスパンが開始されるときに行われるため、そのとき渡した属性だけをサンプラーが見ます。 - HTTPスパンのセマンティックコンベンション・HTTPメトリクスのセマンティックコンベンション・確率サンプリングとadjusted count・Prometheusのエクスポジション形式・Prometheusの名前とラベルの付け方
スパンダンプだけでリクエスト数とレイテンシ分布を数えてみる
/root/tp-metrics/collect.pyを作成してください(デフォルトのダンプのパスは/root/tp-metrics/01-spans.jsonl)。材料tracelab.tp_metrics.trafficのSHOPを順に処理しながら、リクエストごとにルートスパンGET <주소>(プレースホルダーはアドレスです)を作り、スパンを開始するときに属性を5つ渡します。request.id・request.index・http.target(リクエストのpath)・http.response.status_code(リクエストのstatus)・user.id(リクエストのuser)です。スパンの中でtraffic.work(tracer, req)を呼んでください。そのあと/root/tp-metrics/01-from-spans.tsvに、タブで区切った2つの欄の4行を書きます。requests<탭><루트 스팬 수>、errors<탭><상태 코드 500 이상인 루트 수>、p50_ms<탭><값>、p95_ms<탭><값>(プレースホルダーは順に、タブ、ルートスパン数、ステータスコードが500以上のルート数、値です)。パーセンタイルは、ルートスパンのduration_msを昇順に並べて1から数え、올림(비율 × 개수)(プレースホルダーは、割合と個数の積の切り上げです)番目の値を選び、小数第3位まで書きます。
子スパン(db.query)もダンプに入るので、リクエストを数えるときはparent_idがない行だけを数える必要があります。パーセンタイルはmath.ceil(0.95 * n) - 1番目の欄(0から数えるPythonのインデックス)です。かかった時間はマシンによって少しずつ違うため、採点ツールは作成したダンプを読み直して、同じ方法で計算して比べます。絶対値を当てるものではありません。ダンプを作り直す前に、ファイルを削除してください。
メトリクス側の名前・値とスパン属性がずれた箇所を探す
メトリクスパイプラインが出力したファイルが/opt/app/tracelab/tp_metrics/metrics/shop-api.promにあります(Prometheusのエクスポジション形式)。/root/tp-metrics/02-mapping.tsvに、タブで区切った3つの欄の3行を書いてください。1つ目の欄はメトリクスのラベルで、順にhandler・code・svc、2つ目の欄は、ステップ1のダンプでそのラベルに対応させたい場所(http.target・http.response.status_code・service.name)、3つ目の欄は、その2つの値がそのまま合っているかどうかで、yesまたはnoです。そして/root/tp-metrics/02-gap.txtに2行を書きます。metric_series=の後ろにそのファイルのhttp_requests_totalの系列数、span_groups=の後ろに、ステップ1のダンプのルートスパンをhttp.targetの値でまとめたときに出てくるグループ数です。
エクスポジション形式の1行は이름{라벨="값",...} 값(プレースホルダーは名前、ラベル、値です)で、#で始まる行は説明です。2つの数字を並べて見ると、なぜ対応づけられないのかが一目でわかります。一方は手で数えられる数で、もう一方はアドレスごとに1つずつ増えていきます。service.nameは、スパンのattributesではなくresourceに入っています。
同じ名前・同じ値で2つのシグナルを結びつける
/root/tp-metrics/aligned.pyを作成してください(デフォルトのダンプのパスは/root/tp-metrics/03-aligned.jsonl)。ステップ1と同じトラフィックを処理しますが、ルートスパンにhttp.route(リクエストのroute、つまりルートテンプレート)を加えて渡し、スパン名もGET <경로 틀>(プレースホルダーはルートテンプレートです)にします。調査に使うhttp.targetはそのままにします。サンプラーはsamplers.keep_all()を使います。実行したら、/root/tp-metrics/03-joined.tsvに、タブで区切った4つの欄を系列ごとに1行ずつ書いてください。<handler><탭><code><탭><지표 값><탭><그 짝의 루트 스팬 수>(プレースホルダーは順に、タブ、メトリクスの値、そのペアのルートスパン数です)で、handlerの昇順、同じならcodeの昇順です。2つの数字が系列ごとに同じである必要があります。
メトリクスはラベルhandlerとcodeで系列を分け、スパンは属性http.routeとhttp.response.status_codeでまとめられます。ステータスコードは、メトリクスでは文字列、スパンでは整数なので、対応づけるときにどちらかに合わせる必要があります。keep_all()を使うと、すべてのルートスパンにsampling.probabilityが1.0で書かれますが、その値はステップ5で使うことになります。
サンプルを変えると、同じデータから異なるエラー率が出る
/root/tp-metrics/sampled.pyを作成してください。コマンドライン引数としてnth5またはerrbiasを受け取り、それぞれsamplers.every_nth(5)とsamplers.errors_and_nth(5)をサンプラーとして使い、残りはステップ3とまったく同じように計装します。デフォルトのダンプのパスは/root/tp-metrics/04-<인자>.jsonl(プレースホルダーは引数です)です。2回実行して/root/tp-metrics/04-nth5.jsonlと/root/tp-metrics/04-errbias.jsonlを作ったあと、ステップ3のダンプまで含めた3つを使って、/root/tp-metrics/04-rates.tsvに、タブで区切った4つの欄の3行を書いてください。1つ目の欄は順にfull・nth5・errbias(fullはステップ3のダンプです)、続いて<오류 루트 수><탭><전체 루트 수><탭><비율>(プレースホルダーは順に、エラーのルート数、タブ、全体のルート数、割合です)で、割合は小数第4位までです。
サンプラーはrequest.indexとhttp.response.status_codeを見て決定するので、その2つの属性をスパンを開始するときに渡さないと、サンプラーは何も見られません。3つの割合のうち1つだけが大きく跳ね上がるはずです。どれがなぜ跳ね上がるのか、考えてみてください。ダンプは追記されるので、実行する前に削除してください。
サンプリング確率の逆数で補正して数え直し、直せないものを書く
2つのサンプルダンプのルートスパンには、sampling.probabilityが書かれています。スパン1つが代表する件数は、その値の逆数です。/root/tp-metrics/05-adjusted.tsvに、タブで区切った4つの欄の2行を書いてください。1つ目の欄は順にnth5・errbias、続いて<보정한 오류 수><탭><보정한 전체 수><탭><보정한 비율>(プレースホルダーは順に、補正したエラー数、タブ、補正した全体数、補正した割合です)で、前の2つの欄は小数第4位まで、割合も小数第4位までです。そして/root/tp-metrics/05-limits.txtに2行を書きます。limit1=とlimit2=の後ろに、補正でも元に戻せないものを、それぞれ40文字以上で書いてください。ステップ4のfullの割合と比べて、補正がどこまで合わせてくれるかを確認してください。
補正した数は、ルートスパンごとに1 / sampling.probabilityを足した値です。子スパンには確率が書かれていないので、自然にルートだけを数えることになります。errbias側は補正するとステップ4のfullの割合にとても近づき、nth5側はもともとそれほど遠くなかったはずです。元に戻せないものを考えるときは、「サンプルに1件も入らなかった組み合わせ」と「パーセンタイル」を思い浮かべてください。
区間ごとに代表トレースを選び、メトリクスから渡るブリッジを作る
/root/tp-metrics/bridge.pyを作成してください(デフォルトのダンプのパスは/root/tp-metrics/06-bridge.jsonl)。ステップ3と同じように計装してダンプを残したあと、そのダンプを読み直して、系列ごとに最も遅いルートスパン1件を選び、表に書きます。表のパスは、ダンプのパスの.jsonlを.tsvに変えたものです(デフォルトなら/root/tp-metrics/06-bridge.tsv)。1行は、タブで区切った4つの欄<http.route><탭><상태 코드><탭><duration_ms(소수 셋째 자리)><탭><trace_id>(プレースホルダーは順に、http.route、タブ、ステータスコード、duration_ms(小数第3位)です)で、http.routeの昇順、同じならステータスコードの昇順です。そして/root/tp-metrics/06-limits.txtに、limit1=・limit2=の2行で、このブリッジが答えられないものを、それぞれ40文字以上で書いてください。
表のパスをダンプのパスから導き出すのには理由があります。採点ツールが作成したプログラムを自分の一時ディレクトリでもう一度実行するとき、表を固定のパスに書くと、提出されたファイルを上書きしてしまうからです。代表を選ぶ作業自体は、標準ではexemplarと呼びますが、この環境にはPrometheusがないため、実際に保存したり照会したりはできません。限界を書くときは、「普通のリクエスト」と「サンプルから抜けたリクエスト」を思い浮かべてください。
どの属性を2つのシグナルに共通で置くかをルールファイルに固める
/root/tp-metrics/07-contract.tsvに、タブで区切った3つの欄の7行を書いてください。1つ目の欄はスパン属性名で、順にhttp.route・http.response.status_code・service.name・http.target・request.id・user.id・sampling.probabilityです。2つ目の欄は、その属性がメトリクスで使うラベル名で、メトリクスに置かないものは-にします。3つ目の欄は、both(2つのシグナルに同じ値で置く)またはtrace-only(トレースにだけ置く)です。この表は、ステップ3のダンプと/opt/app/tracelab/tp_metrics/metrics/shop-api.promを突き合わせて、実際に合っている必要があります。bothの属性は、ステップ3のダンプのルートスパンにすべてあり、trace-onlyの属性名は、メトリクスファイルのラベルとして現れてはいけません。
分ける基準はカーディナリティです。メトリクスのラベルは、値の種類数がそのまま系列数に掛け算されるため、アドレスやユーザー識別子を入れると系列が爆発します。逆に、トレースはリクエスト1つを見つけ出すことが目的なので、そういう値がそこにある必要があります。service.nameはスパンのresourceに入っていますが、それでもbothです。2つのシグナルをサービス単位で結びつけるキーだからです。
2つ目のサービスにルールを適用し、検査プログラムで突き合わせる
/root/tp-metrics/pay.pyを作成してください(デフォルトのダンプのパスは/root/tp-metrics/08-pay.jsonl)。材料のtraffic.PAYをサービス名pay-apiで計装し、ステップ7のルールの属性をそのまま残し、ルートスパン名はPOST <경로 틀>(プレースホルダーはルートテンプレートです)、サンプラーはsamplers.keep_all()です。そして/root/tp-metrics/agree.pyを作成してください。python3 agree.py <스팬덤프> <노출형식파일>(プレースホルダーはスパンダンプとエクスポジション形式のファイルです)で実行すると、系列ごとにメトリクスの値とルートスパン数を比べ、異なる系列ごとにmismatch<탭><handler><탭><code><탭><지표 값><탭><스팬 수>(プレースホルダーは順に、タブ、メトリクスの値、スパン数です)を1行ずつ出力して終了コード1で終わり、すべて同じならok<탭><계열 수>(プレースホルダーはタブと系列数です)の1行を出力して0で終わります。検査プログラムを/opt/app/tracelab/tp_metrics/metrics/pay-api.promに実行した出力を、/root/tp-metrics/08-agree.txtに保存してください。
検査プログラムにotelは不要なので、システムのpython3で動くように書いてください。メトリクス側にだけある系列も、スパン側にだけある系列も、どちらもずれなので、2つのキーの集合の和集合を回す必要があります。採点ツールは、作成した検査プログラムを、わざと間違えて書いておいた/opt/app/tracelab/tp_metrics/metrics/pay-api-broken.promにも実行するので、ファイル名や特定の値で判定してはいけません。