🐶

Datadog Tracer実装のすすめ

2022/12/05に公開

これは ZOZO Advent Calendar 2022 Vol.5の5日目の記事です。

概要

アプリケーションを運用する場合、速やかな障害対応やサイトの信頼性を向上させるためにモニタリングが重要です。
モニタリングのためのツールにはAmazon CloudWatchSplunkDatadogなどさまざまな製品が提供されています。
ZOZOTOWNではモニタリングの一環として、マイクロサービスの分散トレーシングにDatadogを活用しています。
https://techblog.zozo.com/entry/datadog-japan-meetup-2022-summer

Datadogを使うことで複数のマイクロサービスを跨いだリクエストの解析ができるようになり、とても便利です。
JavaやGoで開発されたアプリケーションにはDatadogが提供するトレーシングライブラリを使うことで、簡単にアプリケーションのトレースデータ収集処理を組み込むことが出来ます。
しかし、ZOZOTOWNの一部で使われている言語(VBScript)にはトレーシングライブラリが提供されていません。
そのため、VBScriptでもJavaやGoのアプリケーションと同様にトレーシングを実現するために、自前でトレーシングライブラリの開発を行いました。
今回はその経験を元に、Datadogのトレーシングライブラリを開発する上で学んだ知識を紹介します。

Datadogを利用したモニタリングシステムのアーキテクチャ

そもそもDatadogはどのようにしてトレーシングを実現しているのでしょうか。
モニタリングツールは大きくプル型とプッシュ型に分類されますが、Datadogはプッシュ型でアプリケーションのトレースデータを収集します。モニタリング対象のアプリケーションと同じ環境にエージェントを起動し、このエージェントがトレースデータを収集してDatadogに送信します。以下のようなイメージでトレースデータを収集します。

Datadogを利用するモニタリングのアーキテクチャ
ロゴ引用

モニタリングやモニタリングツールのアーキテクチャについては、「達人が教えるWebパフォーマンスチューニング ISUCONから学ぶ高速化の実践」の2章が詳しくて分かりやすいです。

Agentにトレースを送信する方法

Datadogの場合、エージェントがトレースデータを収集するために以下2つの方法が用意されています。

  1. トレーシングライブラリを使う
  2. HTTP APIを使う

1つ目の方法に関しては概要に書いた通り、JavaやGoなどトレーシングライブラリが提供されている言語を使う場合です。そのような言語を使っている場合は、提供されているトレーシングライブラリを使うのが最も簡単な方法です。例えばGoのnet/httpパッケージを使ったアプリケーションでは、http.ServeMuxをラップしたHTTPハンドラーを使うことで、http.ServeMuxの呼び出し時にトレースデータをエージェントに送信してくれます。
https://github.com/DataDog/dd-trace-go/blob/v1.43.1/contrib/net/http/example_test.go#L14-L20
Goの場合、dd-trace-go/contrib配下に良く使われるパッケージのラッパーが実装されており、これらのexampleを見ると使用方法が分かりやすいです。

2つ目の方法に関して、DatadogではHTTP APIを介してAgentにトレースデータを送信する方法も提供されています。
トレーシングライブラリが提供されていない言語で開発されたアプリケーションのトレースデータを収集するためには、自前でクライアントを開発し、このAPIにトレースデータを送信する必要があります。

ZOZOTOWNリプレイスとモニタリング

ZOZOTOWNではレガシーなシステムのリプレイスプロジェクトを進めていますが、まだまだレガシーなシステムが稼働しているのが現状です。
https://logmi.jp/tech/articles/325239

レガシーなシステムはVBScriptで開発されていますが、VBScript向けにはDatadogのトレーシングライブラリが提供されていません。
今まではレガシーなシステムは別のツールを使ってモニタリングをしていましたが、全てのシステムをDatadogで一貫してモニタリングできた方が便利です。なので、VBScript向けのトレーシングライブラリを自前実装することになりました。
他の言語向けに提供されているトレーシングライブラリの実装はOSSとして公開されているので(例えばdd-trace-go)、それらを参考にしながら開発を進めました。今回は自前でトレーシングライブラリを開発する過程で学んだ知識を紹介します。

トレーシングライブラリの実装

用語

以下の説明で使う用語を紹介します。

用語 意味
App モニタリング対象のアプリケーションのことを意味します。この記事ではHTTPサーバーを対象としています。
Agent Datadogにデータを送信するソフトウェアで、Appと同じサーバーにインストールされます。
tracer Appに対してグローバルなtracerが1つ存在します。このtracer経由でAppとトレーシングライブラリ間のデータのやり取りを行ったり、Agentにデータを送信したりします。
channel トレースデータのための一時的な記憶領域です。トレースデータが生成される度にAgentと通信すると非効率なため、一定期間で溜まったデータをまとめて送信します。
worker 一定期間でchannleに溜まったデータを取り出し、Agentに送信します。
トレース トレースは、アプリケーションがリクエストを処理するのにかかった時間とこのリクエストのステータスを追跡するために使用されます。各トレースは、1 つ以上のスパンで構成されます。
スパン スパンは、特定の期間における分散システムの論理的な作業単位を表します。複数のスパンでトレースが構成されます。

APMで見れる下図の1つ1つのブロックがスパン、これらのスパンのかたまりをトレースを呼びます。
Datadog APM
図引用

処理の過程

HTTPサーバーにトレーシングライブラリを導入すると、以下の流れでトレースデータがエージェントに送信されます。

  1. tracerの初期化

https://github.com/ytakaya/dd-trace-example/blob/main/main.go#L11

  1. workerが起動する

https://github.com/DataDog/dd-trace-go/blob/v1.43.1/ddtrace/tracer/tracer.go#L238-L247

  1. アプリケーションでエンドポイントの処理が始まるタイミングで親スパンを生成する(トレースの開始)
    • net/httpの場合はServeHTTPの中で共通化したスパン生成処理を呼び出す

https://github.com/DataDog/dd-trace-go/blob/v1.43.1/contrib/net/http/trace.go#L50

  1. 場合により、アプリケーションで子スパンを生成する
    • 外部サービスへのリクエストやDB操作時など、スパンとして記録したい処理がある場合は親スパンを元に子スパンを生成する

https://github.com/ytakaya/dd-trace-example/blob/main/main.go#L16-L18

  1. アプリケーションのエンドポイントの処理が終わるタイミングで親スパンを終了する(トレースの終了)

https://github.com/DataDog/dd-trace-go/blob/v1.43.1/contrib/net/http/trace.go#L52-L54

  1. リクエスト内のトレースで溜まったスパンをworkerのチャネルに送る

https://github.com/DataDog/dd-trace-go/blob/v1.43.1/ddtrace/tracer/spancontext.go#L342-L345

  1. workerはエージェントにトレースを送信する

https://github.com/DataDog/dd-trace-go/blob/v1.43.1/ddtrace/tracer/tracer.go#L286-L294

トレーシングライブラリのchannelやworkerを踏まえると、以下のようなイメージになります。

トレースのフロー

リクエストが複数スレッドで処理されることを意識しながら、グローバルなtracerにトレースデータを集めることができれば上手く動きそうです。
以降では、この流れを効率的にするために行われていた工夫について紹介します。

効率的な処理の工夫

監視ツールは、監視対象に与える負荷を最小限に止めるために工夫して実装されているはずです。

観察者効果は気にしない
今は2017年で、1999年ではありません。つまり、負荷は非常に小さい(はず)なので、システムは負荷が増えても処理できます。
入門 監視

上記で説明したトレーシングライブラリの実装の中でも、アプリケーションのパフォーマンスに影響を与えないようにいくつかの工夫がされていました。
見つけた中で実践できそうな3つの工夫を紹介します。

  • エージェントとの通信にはUnixドメインソケットを使う
  • エンコーディングにMessagePackを使う
  • トレースデータのバッファリングを行う

エージェントとの通信にはUnixドメインソケットを使う

代表的なソケットにはTCP、UDP、Unixドメインソケットなどがありますが、その中でもUnixドメインソケットは高速です。
Unixドメインソケットはローカル通信でしか使えませんが、ネットワークのインターフェイスを介さない分TCPなどよりも高速です。
さらに詳しくは、「Goならわかる システムプログラミング」がおすすめです。UnixドメインソケットとTCPのベンチーマークも掲載されており、Unixドメインソケットの方が80倍から90倍高速との結果が出ています。

https://ascii.jp/elem/000/001/415/1415088/

アーキテクチャで紹介した通りAppとAgentは同一サーバーで動いているので、Unixドメインソケットを使った通信が可能です。
なので公式が提供しているトレーシングライブラリでは、デフォルトでUnixドメインソケットが利用されるように実装されています。

トレースクライアントはデフォルトで Unix ドメインソケット /var/run/datadog/apm.socket にトレースを送信しようとします。
https://docs.datadoghq.com/ja/tracing/trace_collection/dd_libraries/go

https://github.com/DataDog/dd-trace-go/blob/v1.43.1/ddtrace/tracer/option.go#L325-L332

エンコーディングにMessagePackを使う

よく使われるエンコーディングにJSONやXMLがあります。HTTPのContent-Typeヘッダーでいうところのapplication/jsonapplication/xmlです。
テキスト形式のJSONやXMLは人間にも読みやすく、広く普及しているので使いやすいエンコーディングですが、冗長なためサイズが大きくなってしまう場合があります。
そのため、JSONやXMLのデータモデルを基にMessagePack、BSON、WBXMLなどのバイナリエンコーディングが開発されました。

例えばMessagePackのトップページには、JSONで27byteのデータを18byteで表現できる例が示されています。
https://msgpack.org/ja.html

JSONやXMLほど普及してないのが難点ですがAgentとの通信のような限られたユースケースでは、これらのコンパクトでパースが高速なフォーマットを用いることができます。
dd-trace-goでは、Agentへ送信するデータの実態であるpayloadをMessagePackでエンコードしたデータを読み出せるio.Readerとして実装しています。
https://github.com/DataDog/dd-trace-go/blob/v1.43.1/ddtrace/tracer/payload.go#L130-L139

HTTP通信時のContent-Typeヘッダーにはapplication/msgpackを指定しています。
https://github.com/DataDog/dd-trace-go/blob/v1.43.1/ddtrace/tracer/transport.go#L93-L99

データ指向アプリケーションデザインの4章にはMessagePackの他にも、ThriftやProtocol Buffersなどのエンコーディングが詳しく紹介されています。

トレースデータのバッファリングを行う

処理の過程でも軽く説明しましたが、トレースデータが生成される度にAgentとの通信を行うのは少し非効率です。
そのためdd-trace-goでは一定期間トレースデータをpayloadに溜めてから、まとめてAgentに送信がされるように実装されていました。
さらに具体的には、Agentにデータが送信されるタイミングは2通りあります。

  • payloadのサイズが一定サイズを超える時

https://github.com/DataDog/dd-trace-go/blob/v1.43.1/ddtrace/tracer/writer.go#L65-L68

  • workerが動いた時

https://github.com/DataDog/dd-trace-go/blob/v1.43.1/ddtrace/tracer/tracer.go#L291-L294

このようにしてAgentとの通信回数を抑えることで、サーバーのリソース消費を少なくしています。

まとめ

Datadogのトレーシングライブラリを実装する上で調査した点を紹介しました。
全てを実践できてはいませんが、書籍で読んだことのある技術などが多く登場しており面白かったです。

Discussion