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に preBuildHookpostBuildHook というものがある。これはbuildの前と後で任意のscriptを実行させることができる。

buildの開始と終了がわかればtrace情報が作成できるので、これらを使っていく。

ちなみに、検証した結果hookは以下のような仕様になっている

  • pre-build-hook$1derivationの名前$2derivationの名前.chroot が入った状態で呼ばれる。
  • post-build-hookDRV_PATH OUT_PATHS といった環境変数が入った状態で呼ばれる。
  • pre-build-hook は sandbox optionが有効になっていないと呼ばれない

また、shell scriptからtrace情報を送信するためのツールとして otel-cli というものがあり、今回はこれで送信する。

github.com

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に関しては公式のドキュメントがあるので、以下を読む。

www.envoyproxy.io

ドキュメントによると、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 を使う。

www.envoyproxy.io

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しておいた

github.com

ただ、現段階では動かすのにテクがいるのでそれを書いておく。

まず、上記のスクリーンショットにも使っているがjaegerを動かす必要があるが、jaeger自体がかなり複雑なので提供されているdocker imageを使うのが良いだろう。

上記のrepositoryで言うと example ディレクトリの中で docker-compose up で立ち上げてもらう。

opentelemetry collectorが 4317で立ち上がるので、example/envoy.yamlexapmle/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の日本語コミュがあるので、動かない人は以下で聞いてください

github.com

今後の方針

上記の実装方法ではenvoyの設定を書き換えながらやるので複数のビルドを同時に扱うことができない。

なので、netrc-filelocalhost:8081に対する認証情報をtrace idを埋め込むようにして、1段目のproxyで認証情報からtraceparent headerを組み立てると理屈上はenvoyの設定を動的に変更せずにtrace idをビルドごとに送ることができる可能性がある

また、ZeroCodeChangesではできないことも多いので、詳細なメタデータを送るために C++ でtracingの実装を追加するのも面白いかもしれない。

nixosのtoplevelをビルドした例。途中でcacheのfetchに失敗している(httpの分割requestが失敗している雰囲気)

cacheの取得を進めた上で max-jobsとcoreを8指定でした例。並行で実行されている様子がわかる