NixのbuildをOpenTelemetryを使ってtraceする
概要
Nixのbuildは依存関係が複雑でどのビルドにどの程度の時間がかかっているかを把握するのは難しい。
そこで OpenTelemetry を使ってbuildやcacheの取得にかかった時間を可視化することを試みる。
NixはC++で書かれているので GitHub - open-telemetry/opentelemetry-cpp: The OpenTelemetry C++ Client を使ってNix自体にpatchを当てることでtraceの機能を実現することもできるが
- Observabilityを上げる上でコード変更を伴わない方がversionの変更に対応しやすい
- C++を書きたくない
といった理由から、今回はNixのsource codeを変更せずに可観測性を上げることを目標とする。ちなみに後者の理由が9割である。
実装方針(build)
Nixのoptionに preBuildHook
と postBuildHook
というものがある。これはbuildの前と後で任意のscriptを実行させることができる。
buildの開始と終了がわかればtrace情報が作成できるので、これらを使っていく。
ちなみに、検証した結果hookは以下のような仕様になっている
pre-build-hook
は$1
にderivationの名前
が$2
にderivationの名前.chroot
が入った状態で呼ばれる。post-build-hook
はDRV_PATH
OUT_PATHS
といった環境変数が入った状態で呼ばれる。- pre-build-hook は sandbox optionが有効になっていないと呼ばれない
また、shell scriptからtrace情報を送信するためのツールとして otel-cli というものがあり、今回はこれで送信する。
pre-build-hookとpost-build-hookの間で情報を受け渡す手段が用意されていないので、 pre-build-hookで $TMPDIR/$drv
に開始時刻を書き込んで、 post-build-hookで otel-cliからtrace情報を送る方針にする。
自分の認識の限りではあるビルドの中で同じderivationがビルドされることはないのでうまくいくはず。
雑に試す場合は以下のようにする
pre-build-hook
drv=$1 name=${drv#/nix/store/} $path/bin/date +%s.%N > /tmp/$name"
post-build-hook
drv=$DRV_PATH name=${drv#/nix/store/} ${pkgs.otel-cli}/bin/otel-cli span \ --start $($path/bin/cat /tmp/$name) \ --end $($path/bin/date +%s.%N) \ --config $OTEL_CLI_CONFIG \ -n $name \ -s nix-build \ --fail
dateコマンドやotel-cliへのコマンドは当然絶対PATHで用意すること
helloコマンドをビルドした感じは以下の様な感じ
実装方針(cache)
上記のtraceはbuildの時間しか計測されず、cacheを取得している時間は含まれていない
また、cacheの取得はbuildではないので hookを使うことができない
しかしcacheをhttpで取得している場合はproxyを使うことでtrace情報を取得できる可能性がある
今回はEnvoy proxyを使ってcacheのtrace情報を取得する。
envoyのtraceに関しては公式のドキュメントがあるので、以下を読む。
ドキュメントによると、traceの設定をした上で traceparent
headerをつければ送ってくれるようだ
ただ、cacheの取得のリクエストに対して traceparent
headerにtrace idを乗せた上で送る方法を考える必要がある。
nix側のコードを見た感じではそのような設定はないので、今回は traceparent
headerを載せるenvoy proxyとtraceの送信を行うproxyの2段構成でやる。
つまり 、nix -> (traceparent headerをつけるproxy:8081) -> (cache.nixos.orgにrequestを通しながらtraceの送信を行うproxy:8080)
といった感じだ。
trace id を envoyのconfigに対して直接書き込でおけば比較的簡単に1段目のproxyは実装することができるが、今回はビルドごとにidを発行したい。
なので、envoyのconfigを動的に変更する xDS configuration API を使う。
xDSはistioとかを使っていると比較的に簡単に使うことができるのだが流石に大袈裟な仕組みになってしまうので、今回はファイルシステムの変更を検知して設定を書き換えてくれるDynamic from filesystemを使う
Configuration: Dynamic from filesystem — envoy 1.27.0-dev-f12afb documentation
ファイルシステムの変更検知にどのsyscallを使っているのか知らないが mvコマンド相当を使わないといけないらしい。
雑にやる場合、以下の様になる。
export ENVOY_LDS_FILE_TMP="$TMPDIR/tmp.json" jq --arg traceparent "$TRACEPARENT" \ '.resources[0].filter_chains[0].filters[0].typed_config.route_config.request_headers_to_add[0]={"header": {"key": "traceparent","value": $traceparent}}' \ ${./lds.json} > "$ENVOY_LDS_FILE_TMP" rm "$ENVOY_LDS_FILE" mv "$ENVOY_LDS_FILE_TMP" "$ENVOY_LDS_FILE" sleep 1 nix build "$1" \ --option substituters "http://localhost:8081?priority=20" \ --no-link \ -L
sleep 1を挟んでいるのはファイルの変更検知後にenvoyの設定変更を待つためである
実行した感じは以下の様になる
tracing-nix-build
今回試しやすい様にある程度再現しやすい形でCodeをpushしておいた
ただ、現段階では動かすのにテクがいるのでそれを書いておく。
まず、上記のスクリーンショットにも使っているがjaegerを動かす必要があるが、jaeger自体がかなり複雑なので提供されているdocker imageを使うのが良いだろう。
上記のrepositoryで言うと example ディレクトリの中で docker-compose up
で立ち上げてもらう。
opentelemetry collectorが 4317で立ち上がるので、example/envoy.yaml
と exapmle/config.json
の中にある 192.168.64.1
を そのアドレス(127.0.0.1など)に書き換えてもらう。
これは私の環境ではdocker自体はmacos上で動かしておりUTMを使ってnixos上でenvoyと今回のtracing-nix-buildを実行しているので、上記の様な設定になっている。
また、/tmp/envoy/lds.yaml
に空ファイルを用意しておかないとenvoyが立ち上がらないので用意しておく必要がある。この値は ENVOY_LDS_FILE
で変更することができる。
まとめると
nix develop cd example export OTEL_CLI_CONFIG_FILE=$PWD/config.json export ENVOY_LDS_FILE=/tmp/envoy/lds.yaml docker-compose up -d vi envoy.yaml vi config.json touch $ENVOY_LDS_FILE envoy -c envoy.yaml & tracing-nix-build ".#hello"
の様になる
Nixの日本語コミュがあるので、動かない人は以下で聞いてください
今後の方針
上記の実装方法ではenvoyの設定を書き換えながらやるので複数のビルドを同時に扱うことができない。
なので、netrc-file
に localhost:8081に対する認証情報をtrace idを埋め込むようにして、1段目のproxyで認証情報からtraceparent headerを組み立てると理屈上はenvoyの設定を動的に変更せずにtrace idをビルドごとに送ることができる可能性がある
また、ZeroCodeChangesではできないことも多いので、詳細なメタデータを送るために C++ でtracingの実装を追加するのも面白いかもしれない。
例
nixosのtoplevelをビルドした例。途中でcacheのfetchに失敗している(httpの分割requestが失敗している雰囲気)
cacheの取得を進めた上で max-jobsとcoreを8指定でした例。並行で実行されている様子がわかる