eBPFを使ったOpenTelemetryのGo自動計装ライブラリを調べる
opentelemetry-go-instrumentation
というGoでOpenTelemetryの自動計装を実現するライブラリを知りました。eBPFを活用しているようです。このライブラリを実際に動かしてみながら簡単に調べてみます。
opentelemetry-go-instrumentation
はWork in Progress(v0.8.0-alpha
)となっています。その後大きく変わっている可能性にご注意を。自動計装とは?
OpenTelemetryを中心とした分散トレーシングでは、Contextを伝播したりMetrics, Logs, Tracesを出力したりするための計装(Instrumentation)をアプリケーションに施す必要があります。この作業はアプリケーションに手を加える必要があり手間です。そこで計装を自動化する、つまりアプリケーションのコードに手を加えずに実現する方法がJavaなどで実現されています1。
しかしGoはJavaやPythonなどとは違って、マシンコードにネイティブにコンパイルされます。そのため実行時にコードを追加することができません。少なくとも自分はGoでの自動計装は難しいと思っていました2。
今回紹介するopentelemetry-go-instrumentation
はeBPFを使ってGoでの自動計装を実現しようと試みています。
eBPFとは?
eBPFはカーネル空間のサンドボックス環境で安全にユーザー定義のプログラムを実行するLinuxの技術です。Networkingを中心としてObservabilityやTracingの分野での研究開発が盛んです。自分が読んだ論文をいくつか簡単に紹介しているのでよければご覧ください。
個人的にはCiliumを中心としてコンテナネットワーク関連でよく使われる印象を持っています。
opentelemetry-go-instrumentation
では、eBPFを用いて実行プロセスのコードと変数にattachしているようです。
動かしてみる
調べる前に動かしてみます。getting-started3があるのでそれに従います。
準備
kindでk8sクラスタ作成&imageをロード
$ kind create cluster --name=otel-go-inst
$ make docker-build
$ kind load docker-image otel-go-instrumentation --name=otel-go-inst
アプリケーションをデプロイ
$ kubectl apply -k docs/getting-started/emojivoto/
namespace/emojivoto created
serviceaccount/emoji created
serviceaccount/voting created
serviceaccount/web created
service/emoji-svc created
service/voting-svc created
service/web-svc created
deployment.apps/emoji created
deployment.apps/vote-bot created
deployment.apps/voting created
deployment.apps/web created
Jaegerをデプロイ
$ kubectl apply -f docs/getting-started/jaeger.yaml -n emojivoto
deployment.apps/jaeger created
service/jaeger created
$ kubectl port-forward svc/jaeger 16686:16686 -n emojivoto
計装前
計装前にどんなアプリケーションか見てみます。
$ kubectl port-forward svc/web-svc 8080:80 -n emojivoto
絵文字を投票するアプリケーションのようです。いくつかリクエストを送ってからJaegerを見てもTraceは見れません。
計装後
計装したバージョンのアプリケーションをデプロイ
$ kubectl apply -f docs/getting-started/emojivoto-instrumented.yaml -n emojivoto
deployment.apps/emoji configured
deployment.apps/voting configured
deployment.apps/web configured
計装用のコンテナ定義はこちら。特権を与えた上で計装対象のコンテナと同じPodで動作させる&プロセス名前空間を共有する4ことで実現しています。コンテナ定義を追加するだけでアプリケーションのコード・イメージには変更はありません。
再びアプリケーションを操作してからJaegerを見ます5。 ここでアプリケーションがJaegerからconnection refusedされていることに気づきました。
2023/11/30 15:41:05 traces export: Post "http://jaeger:4318/v1/traces": dial tcp 10.96.187.248:4318: connect: connection refused
imageをjaegertracing/opentelemetry-all-in-one
からjaegertracing/all-in-one
に変更したら動いたので、PRをupstreamに投げておきました。
これだけでかなり詳細なTraceを見ることができました。 ちなみになんとなくアプリケーションの動作が重くなった気がしますが気のせいでしょうか。ちゃんと計測してみないと分かりませんが。
仕組み
ドキュメントを読んでいきます6。
計装が必要な処理は大きく以下の3つです。
- HTTP/gRPCのリクエスト・レスポンスのSpanContext7を読み書きする
- Spanを作成する
- SpanContextをeBPF Mapに格納する
eBPFプログラムはスタックとCPUレジスタを解析してユーザーコードと変数にアクセスします。このときhttp.Requestのような構造体のSpanContextを読み書きするためには、構造体内のそのフィールドのオフセットを知る必要があります。しかしオフセットは構造体定義が変更されるたびに変化します。そこでオフセットを解析し、その情報をバージョン・構造体ごとにJSONに保存してくれるのがoffsets-tracker
です。
1ではoffsets-tracker
を利用して、http.Requestやgrpc.ClientConnといった構造体のSpanContextを読み書きしているようです。
2では適切な位置で自動でSpanを作成します。例えばHTTP Serverのハンドラ内でgRPCのリクエストを送信するときにSpanを作成します。 またこのライブラリは手動で作成したスパンにも対応しています。その場合はSpanContextを更新します。
3ではSpanContextをeBPF Mapに格納することで、同じgoroutine内の他の場所でSpanContextを利用できるようにしています。例えば受け取ったHTTPリクエストのSpanContextをeBPF Mapに格納し、最後HTTPレスポンスを返す際に取り出して書き込むといった流れです。SpanContextの更新は、1で新たなSpanを受け取った際や2で現在のSpanが更新された際に必要です。
現在の実装ではeBPF MapのKeyをgoroutine IDに、ValueをSpanContextとしています。そのため複数goroutineに跨ったSpanContextの共有は難しいそうです。将来的にはgoroutineの木構造の依存関係をトラッキングすることも考えているようです。
他には、Spanの開始・終了時にタイムスタンプを取得する必要があります。関数の最後でeBPFコードを呼び出すuretprobes
はGoとの相性が悪いらしく、代わりにreturn
句を検出して直前にuprobes
を置くことで終了タイムスタンプを収集するeBPFコードを呼び出しているそうです。
終わりに
ドキュメントが整備されており非常に調べやすかったです。今回は実装までちゃんと読めていないので次はもっと調べてみたいと思っています。
開発が進んでもっと色々な状況で使用できたら非常に便利だと感じました。一方で実現方法のゴリ押し感・動作が重い可能性・コンテナに特権を与えるセキュリティ上の問題が気になります。引き続き追っていきたいと思います。
https://github.com/open-telemetry/opentelemetry-java-instrumentation ↩︎
https://github.com/open-telemetry/opentelemetry-go-instrumentation/tree/v0.8.0-alpha/docs/getting-started ↩︎
https://kubernetes.io/ja/docs/tasks/configure-pod-container/share-process-namespace/ ↩︎
適当に投票してくれるBotもデプロイされていたので操作しなくてもよかったっぽいです ↩︎
https://github.com/open-telemetry/opentelemetry-go-instrumentation/tree/v0.8.0-alpha/docs ↩︎
TraceID, SpanIDなどを含んだもの ↩︎