🌟

分散トレーシングの仕組み 「ログの対応付け」と「受け渡すHTTPヘッダー」を大体5分で理解する

5 min read

はじめに

これは Qiita Advent Calendar 2021 の D言語カレンダー 11日目 の記事です。

マイクロサービスの監視で出てくる「分散トレーシング」(Distributed tracing)の概念を簡単に解説し、分散トレーシングに対応したアプリ実装に必要なことと実装例をご紹介します。

サービスの実装言語やフレームワークが様々ある昨今ですが、何をすればちゃんとトレースされるのか、単にライブラリやサービスの使い方ではなくその仕組みや目的を理解しておくことも大切だろう、といった主旨の記事になります。

こちら粗方理解できると、主に「ログは記録されるけど対応付けがされないなぁ」といった問題の解決ができるようになるはずです。
気軽に作った自作アプリで何度もやらかしているので皆さんもご注意ください!

なお今回は簡単のためにHTTP通信のみを対象とし、データの保存方法にはほとんど触れず、かなり簡略化します。

特にサービスメッシュだのプロキシだのサイドカーパターンだの言及すると終わらなくなってしまうので、本記事はざっくり取っ掛かりのみ、あとは調べる用のキーワードを散りばめるスタイルでお届けします。

分散トレーシングとは

マイクロサービスといえば、まず複数のサービスが独立して動き、それらがお互いに疎結合で協調動作することが求められます。

このとき、特にログが個々のストアに保存されたり、独自の形式になっていると、ユーザーから見えていないサービスが絡むログ解析でつらい思いをすることがあります。

これを統一的に解析するために考えられた仕組みが「分散トレーシング」です。

分散トレーシングの意義

「トレーシング」は日本語で言えば「追跡」です。つまり、マイクロサービスの処理を追いかけるときにその効力を発揮します。

たとえば、フロントエンドのサーバーA、バックエンドのサービスBとCがあって、1度のリクエストに対し以下のように連携するとします。

graph LR

クライアント -->|アクセス| A -->|データを取る| B -->|データを取る| C

ここでサーバー部分に着目します。
ほぼ同時に2つのリクエストを処理したところ、ちょっと普段より遅い処理時間のログが4つ残ったとしましょう。

graph LR

A -->|10秒| B
A -->|5秒| B
B -->|4秒| C
B -->|1秒| C

もし普段、AとBの間で応答が5秒程度であれば、10秒は遅いと考えるでしょう。Cの応答も普段が1秒程度であれば、4秒は遅いと考えるかもしれません。

ここで、仮に1,2,3,4と以下のように連番を振ってみます。

graph LR

A -->|1: 10秒| B
A -->|2: 5秒| B
B -->|3: 4秒| C
B -->|4: 1秒| C

すべてが架空のシナリオですが、ざっと原因を考えてみると、ありそうな仮説として以下の2つが浮かびます。

  1. 原因は1のリクエストである説
    • 1と3、2と4がペアであり、1のリクエスト内容によって3(C)が遅くなる可能性
  2. 原因は2のリクエストである説
    • 1と4、2と3がペアであり、2のリクエストによって1(B)が影響を受け遅くなる可能性

全部調べれば分かることですが、ここで 「どれとどれがペアなのかわからない」 ということが複雑性を挙げる要因となっています。

そういうことなら分かるようにしよう!というのが分散トレーシングです。

どうやってトレースするか

すべてのサーバーには「入る通信(Inbound)」と「出る通信(Outbound)」があります。当然すべてが追跡対象です。

先の例では、1と2がBにおけるInbound、3と4がBにおけるOutboundでありCにおけるInboundです。

これらに対応付けがしたいわけですから、Inbound/Outboundそれぞれに同じIDを振る、という対処が考えられ、これを連鎖的にすべてのサービスでやっていく、というのが分散トレーシングの大枠です。

さらに単純化してしまえば、「一連の処理に対して、通しのIDを振ってしまおう」ということですが、ID1つで済ませようと思えばできるものの、本当にそうするのは一部の規格やサービスだけです。実際には複数の情報をやり取りして実現されています。

どうやってIDを受け渡しているか

分散トレーシングが成立するには、リクエスト毎に振られるIDを内部リクエストで再利用する必要があります。

構造的には以下の状況です。

sequenceDiagram

クライアント ->> A : IDなし
A ->> A : IDなしなら発行
A ->> B : IDを付けてリクエスト
B ->> B : リクエストからIDを再利用
B ->> C : IDを付けてリクエスト

ここで、リクエストを投げる側にだけ着目すると、「リクエストからIDを読み取る」「IDを付けてリクエスト」ができればOKなわけですね。
「IDを発行する」ところはありますが、これは実際サービスメッシュなど導入すると勝手にやってくれるので一旦置いておきます。

また、大体のサーバーはHTTP通信で出来ているので、HTTP通信でどうやって渡すか?が問題です。
本来やりたい処理にトレース情報を上乗せしたいわけですから、そういった用途であれば 「HTTPヘッダー」が最適だ と考えられます。

そこで、どうやって実現するかサンプルを交えつつ、転送すべきHTTPヘッダーを整理します。

実装

ここまで、書くだけなら簡単ですが、いちいちこのトレースの仕組みを前提にコードを書くのは面倒なので、世の中には言語毎のライブラリやフレームワーク、サービスメッシュなどの対応方法が溢れかえっているわけですね。

ところが、もし分散トレーシングを考慮せずにHTTP通信をする簡便なライブラリだと、ここのヘッダー転送が上手くいかなかったり、あとから指定できないかもしれません。自作でHTTP通信を書いたら転送し忘れるかもしれません。

こういったコードを自分で書かねばならない事態というのは割とあるものですが、ここまで仕組みが分かれば何となく作れそうな気がしてくるかと思います。

実装例

実際にHTTPヘッダーを指定して分散トレーシングができるようにする実装をいくつかご紹介します。

自作のD言語サーバー実装とIstioのBookinfoの各例で、リンクは実装箇所ピンポイントのリンクとなっています。

転送すべきHTTPヘッダー

世の中が大REST時代になって以降、HTTPヘッダー中心の分散トレーシングが模索されてきました。(知る限り、2016年あたりには聞き及ぶ程度になっていた気がします)

様々な企業や団体が手法を提案し、Web標準のW3Cが取りまとめようとし、いくつか派閥が割れたかと思ったら現状は OpenTelemetry あたりを中心に収束しつつあります。

そのような経緯があったので、世の中でトレース目的に指定されるHTTPヘッダーも多岐にわたります。

もし何かライブラリを作るのであれば、念のため全パターン網羅しておくべきといったところでしょう。
そこで以下、どんなヘッダーがあるのかざっくり整理して終わりとしたいと思います。

参考

サービスメッシュのプロキシであるEnvoyのページにおおよそまとめられています。

転送すべきHTTPヘッダーの一覧

今後増減も考えられますが、抜き出してみると以下15個のヘッダーが対象になります。
ちなみに、HTTPヘッダーは大文字小文字を区別しないので全部大文字にしています

  • Envoy
    • X-REQUEST-ID
  • W3C Trace Context
    • TRACEPARENT
    • TRACESTATE
  • Datadog
    • X-DATADOG-TRACE-ID
    • X-DATADOG-PARENT-ID
    • X-DATADOG-SAMPLING-PRIORITY
  • Cloud trace context
    • X-CLOUD-TRACE-CONTEXT
  • B3 Trace Headers(Zipkin)
    • X-B3-TRACEID
    • X-B3-SPANID
    • X-B3-PARENTSPANID
    • X-B3-SAMPLED
    • X-B3-FLAGS
  • AWS X-Ray
    • X-AMZN-TRACE-ID
  • Lightstep Tracer
    • X-OT-SPAN-CONTEXT
  • gRPC binary trace context
    • GRPC-TRACE-BIN

まとめ

以前、軽い気持ちで作ったサーバーを運用に乗せようと思ったら分散トレースできてないじゃん、という今後マイクロサービスが浸透したらよくありそうなシナリオを個人的に踏み抜いたので、自戒半分にまとめてみました。

こういった監視技術も割と重要ですので、皆さんは忘れないでくださいね。

というわけで、D言語要素少なめでしたが引き続きよろしくお願いします!

Discussion

ログインするとコメントできます