OpenTelemetry は動かせたのに、なぜこんなに複雑なのか?
(iconsolutions.com)- 顧客からの OpenTelemetry 対応 要望により、従来の Prometheus・Jaeger・OpenTracing ベースの可観測性が、OTel のメトリクス・ログ・トレース要件と完全には一致しないことが明らかになった
- ログとメトリクスは appender と設定の追加で比較的簡単に移行できたが、分散トレーシングは Spring と Akka が同じ JVM 内でトレースコンテキストを共有する必要があり、はるかに難しかった
- OTel と Lightbend Telemetry/OpenTracing は互いに異なる Tracer API と SpanContext 実装を使っているため、同じトランザクションが別々の trace に分かれてしまった
- 解決策は、OTel コンテキストを Java の
Mapに inject し、Jaeger SpanContext として extract したうえで、Lightbend のGlobalExtendedTracer.get().local().activateContext()により手動で有効化する方法だった - OTel は可観測性の標準化に役立つが、既存の Akka・Jaeger ベースの計装と混在させる場合は、明示的なコンテキスト接続が必要になることがある
OTel が統合しようとしている可観測性シグナル
- OpenTelemetry(OTel) は可観測性フレームワーク兼ツールキットであり、これまで分かれていたツール領域を、メトリクス、ログ、トレースという 3 つのシグナルに標準化しようとしている
- 従来の組み合わせは役割ごとに分かれていた
- Prometheus: メトリクス
- Logstash や Elasticsearch などの中央集約器: ログ
- OpenTracing: 分散トレーシング
- OTel は仕様だけでなく、中核コンポーネントも提供する
- OpenTelemetry Protocol(OTLP): アプリケーションが telemetry データを報告するためのプロトコル
- OpenTelemetry Collector: ベンダー中立に telemetry データを受信・処理・エクスポートするコンポーネント
- 10 以上の言語向け SDK: OTLP と telemetry export の実装
- アプリケーションは自動計装、API、言語 SDK を通じて OTel Collector にシグナルを送り、インフラも Collector にシグナルを送ることができる
IPF の既存サポートと顧客要望
- IPF はすでに ELK Stack 連携、モニタリング、可観測性のドキュメントを備えており、顧客も本番環境で利用してきた
- フレームワークの性質上、ライブラリを使うアプリケーションがどの環境にデプロイされるか分からないため、特定ベンダーを強制せず、抽象化と上書き可能なデフォルト値を提供する必要があった
- 基本的な推奨事項は、ログを Elasticsearch や LogScale のような集約器に送り、メトリクスについては事実上の標準となった Prometheus を有効化・サポートする方式だった
- 顧客からは OTel ベースの tracing に関する要望がほぼ同時に 3 件寄せられ、tracing を OTel にするついでに logs と metrics も一緒に移行する流れが生まれた
- ログとメトリクスは新しい appender と設定追加で比較的簡単に対応できたが、tracing は別の問題として残った
分散トレーシングでコンテキスト伝播が必要な理由
- 分散システムで 1 つのトランザクションを追跡するには、システム間で特定の呼び出しとトランザクションを結び付ける情報を渡す必要があり、これを context propagation と呼ぶ
- trace は複数の作業単位である span を包む親構造であり、span は複数存在でき、ネストもできる
- EC サイトで「buy now」ボタンを押す流れは、1 つの trace としてまとめられる
- フロントエンドがバックエンドと通信する span
- バックエンドが決済・配送・注文管理サービスと通信する子 span
- 各ダウンストリームサービスがさらに別のシステムと通信する追加の子 span
- OTel を使う場合、各分散サービスは自分の trace の一部を OTel Collector に報告し、OTel は単一の trace ID の下で全体の流れを構成する
4 つのコンテキスト伝播標準と OpenTracing の痕跡
- OTel が必ずサポートしなければならない context propagation 実装は 4 つある
- IPF の Akka 領域は、すでに廃止予定の OpenTracing をサポートしており、Lightbend Telemetry の OpenTracing サポート を使用していた
- Lightbend Telemetry 2.20.0 は OpenTelemetry logs/events と metrics のサポートを追加したが、重要な tracing サポートは欠けていた
- 理論上は、OTel tracing は OpenTracing のリブランディングに近く、Lightbend Telemetry の OpenTracing も 4 つの伝播方式をサポートしているため動作しそうに見えたが、実際の結果は異なっていた
Spring と Akka の間で途切れた trace
- IPF は Spring と Akka を一緒に使用している
- Spring Boot と Spring IoC: アプリケーションのブートストラップ、設定、依存関係の構成
- Akka: イベントソーシング、スケジューリング、クラスタリング、シャーディング、統合など
- 顧客は Spring REST controller や
@KafkaListenerが付いたメソッドで決済フローを開始できる - OTel 以前は、3 つの領域は大きな衝突なく動作していた
- Metrics: Spring と Akka が別々の Prometheus endpoint を公開
- Logs: 両フレームワークとも SLF4J と Logback を使用
- Traces: どちらも OpenTracing を使用
- OTel 使用時、metrics と logs は Spring と Akka が独立して動作して正常に送信されたが、tracing は JVM 内部で同じ trace context を共有する必要があった
- アプリケーション内部で同じトレーシング概念を 2 つの異なる API が表現しており、その 2 つの API が互いに通信しないことが実際の問題だった
- 正しい動作は、Akka HTTP client が既存の trace ID
123を再利用し、新しい span ID だけを作って同じ trace の新しい作業単位として表示することだったが、実際には関係のない 2 つの trace が作成された
opentracing-shim と実装の衝突
- OTel 側には、OTel の
Tracerを OpenTracing のTracerのように見せるopentracing-shimがある - しかし Lightbend Telemetry はカスタム Tracer 実装を使用しており、そのため shim と Jaeger はどちらも失敗した
- ログには次のエラーが出力された
Expected to have an OpenTelemetry Span but got cinnamon.opentracing.TraceLocal$ContextOnlySpanExpected to have a JaegerSpanContext but got io.opentelemetry.opentracingshim.SpanContextShim
- Jaeger は OTel shim のコンテキストを受け取って失敗し、OTel は Lightbend のコンテキストを受け取って失敗した
Java Agent と Lightbend 内部トレーシングの調査
- OTel と Lightbend Telemetry の計装はいずれも Java Agent を使い、特定クラスの特定メソッド呼び出しに hook を掛けて tracer に活動を報告する
- Lightbend Telemetry の計装はオープンソースではないため、動作を把握するには decompile された tracer コードを調査する必要があった
- 縮小した再現例を作成し、Spring 側の OTel trace context が Akka 側へ渡るどの地点で途切れるのかを確認した
- Jaeger が失敗する地点は
JaegerTracer.javaの 該当コード だった - 呼び出し元は Lightbend Telemetry のプロプライエタリな計装クラスである
OpenTracingAkkaPersistenceActorInstrumentation.classだった - decompile されたコードで重要な条件は
var5の状態だった- OpenTracing
SpanContextであるvar5が null でなければ、新しい span を現在アクティブな span の子として付ける var5が null なら、既存 trace と関係のない span になるvar5が Jaeger SpanContext でなければ、Jaeger が失敗する
- OpenTracing
手動変換で OTel と OpenTracing を接続
SpanContextはthis.traceLocal.currentContext()から取得されており、traceLocalは Lightbend のExtendedTracerで初期化されていた- Lightbend のドキュメントには、
ExtendedTracerにグローバルにアクセスする方法があったGlobalExtendedTracer.get()
GlobalExtendedTracer.get()はExtendedTracerを返し、local()は計装ライブラリが使うものと同じTraceLocalを返す- この
TraceLocalには、OpenTracingSpanContextを受け取るactivateContextメソッドがあった - 解決の流れは 4 段階だった
- OTel shim を使わない
- 現在の OpenTelemetry Context を Java の
Mapに inject する - その
Mapの値を使って Jaeger SpanContext を extract する - Akka に入る前に Lightbend
TraceLocalで Jaeger SpanContext を有効化する
- 使用した OTel propagators API の操作は inject and extract operations である
- 中核コードは次の流れに従う
- 空の
HashMapを作成 GlobalOpenTelemetry.get().getPropagators().getTextMapPropagator().inject(...)で OTel context を map に注入new TextMapCodec(false).extract(new TextMapAdapter(otelContext))で JaegerSpanContext を作成GlobalExtendedTracer.get().local().activateContext(openTracingContext)で Akka へ入る前に context を有効化
- 空の
確認された動作と運用上の推奨
- 手動接続後、trace は期待どおりにつながった
- Spring REST 呼び出し
/submitから始まり、全体の流れが 単一 trace につながった - OTel API で計装された部分と OpenTracing API で計装された部分を混在して使えた
- HTTP 境界を越えて trace が伝播した
- Spring REST 呼び出し
- 例では 2 種類の計装を区別するために異なる名前を使ったが、実際の顧客環境では
otel.service.nameとcinnamon.applicationを合わせ、外部から 1 つのアプリケーションのように見えるよう推奨する
複雑だった理由と残る懸念
- Lightbend Telemetry を OTel API で書き直すには、現在 Jaeger API に結び付いている多くの計装を OTel API に移植する必要があり、大きな作業になり得る
- OTel Collector は legacy Zipkin 形式の trace 収集をサポートしているため、Lightbend は legacy サポートに依存して完全な OTel サポートを提供できる
- 複雑さの直接的な原因は、異なる tracing ライブラリを使う 2 つの計装体系を結合しようとしたことにある
- OTel プロジェクトは semantic conventions のような標準化の試みによって可観測性領域を整理しようとしており、最初に理解するにはやや複雑だが、有用な FOSS プロジェクトと評価されている
- Akka が actor model 内部で thread 間の trace context を正しく伝達するかについては、まだ懸念が残っている
- 小規模な負荷テストでは期待どおり動作した
- 関連チケットを Akka 側にオープンしている状態である
1件のコメント
Hacker News の意見
Otel を学んで移植している間ずっと、また Java の世界に戻ったような気分だった。コードを1行ずつ追うたびに EnterpriseFizzBuzz のようで、発見可能性はまったくなく、独自用語も何かに酔った人たちが作ったように見えた
NodeJS では StatsD と比べて CPU 使用量が約4倍で、結局、自前で集計を作って使用量を下げ、タグの爆発も減らした。StatsD は複数プロセスが同じタグを報告しても問題ないが、OTEL は上書きしてしまう
ピーク負荷では CPU 1つが 60〜80% の使用率で回っていて、何かを変えるまでは垂直スケールもできなかった。コアごとに1プロセスを使う言語に対して、OTEL は積極的に敵対しているようで冗談みたいだ。普通に Prometheus を使うほうがよく、実質的にほかの競合もない
.NET で単純な otel 設定をしようとして、組織が選んだベンダーのドキュメントを何時間も読んでも理解できず、同僚が運営する Discord に入った。その会社のビジネスモデルの一部は「オープンソース製品でまともな otel を使うなら金を払え」というものだったが、すぐに、費用がいくらであれそれだけの価値があると感じた
OTEL 実装をするくらいなら、事前経験なしで安定したイベント/pub-sub ライブラリをもう1つ作るほうがましだ
最小構成も実際かなり小さく、Web UI、サーバーインスタンス、すでに知っている DB 程度で足りる: https://skywalking.apache.org/docs/main/latest/en/setup/back...
監視領域における Zabbix に似た面がある。どちらも誰かを感嘆させるものではないが、実用性は十分だ
process.vpidのような比較的低カーディナリティの整数属性を作った。アプリが生きている間、その値が一意になるようにグローバルオブジェクトのようなもので調整できるそのうえで、合算してその属性を削除する何かを置けばよい。statsd/delta ではシグナル送信を1つ失うと全体のデータが歪むが、累積方式では精度を失うだけだ
自分の用途は、スクレイプ可能な長時間実行プロセスではなく、「バッチ」ツールから来る プッシュ型メトリクス だ
Otel が複雑に見える理由は、複数のオブザーバビリティベンダーが自社の 専用 SDK、エージェント、API でオブザーバビリティの実装を非常に簡単にしてきたからだ。Otel はこの問題を解決しようとしているもので、作っている人たちは見事にやっていると思う
Grafana が OpenTelemetry をエコシステムの第一級の構成要素として受け入れたことも称賛に値する
数年間 Datadog の利用を推してきたが、中堅企業から大企業の間くらいの規模では価格が耐えがたいものになった。時間がたって OpenTelemetry API と SDK が安定し、アプリケーションのオブザーバビリティ標準として採用するようになった
ただしドキュメントは全体的にもっと良くできるし、オンボーディング文書が言語ごとに違う点は理想的ではない
現在のチームは NodeJS/Typescript スタックで、OpenTelemetry を素早く始められるようにパッケージ群とサンプルの Grafana スタックを作った: https://github.com/zonneplan/open-telemetry-js
すべての APM ベンダーで最大の問題は、魔法のようなエージェントでカーネルフックを持つようになった瞬間、開発者が説明できないさまざまなことが起きる点だ
以前、別の会社で Dynatrace を導入したが、幸いアプリにはすでに十分な内蔵メトリクスがあり、リード SRE が計測の「模範」と見るほどだった。ところがアプリホストに Dynatrace エージェントをインストールした途端、ノード再起動が必要な複数の ハイゼンバグ が生じ、性能低下も直接測定された
皮肉にもメトリクスのおかげで苦痛を避けられたが、直し方は誰にも分からなかった。最もひどかったのは、MSSQL 更新時のフェイルオーバーが ADO.NET 接続プールを妙に汚染するケースだった
必要な分だけ複雑になる構造です。魔法のような機能は避け、文脈上もっとも価値があり、理解しやすい部分集合だけを使えばよいです
私たちのチームでは非常にシンプルです。トレースだけを送るライブラリを使っており、トレースはアプリケーションの観察に最も大きな価値をもたらし、他の種類のデータも入れられます。基本的には、文字列や浮動小数点数の代わりにハッシュマップを使っているようなものです
自動計装ではなく手動計装を使い、何を観察するかを意図的に決め、どのコードがスパンを出しているのかをよく理解しています。コード構成に合った命名規則もあります
バックエンドには安価なサードパーティサービスと、ローカル開発用の all-in-one Jaeger インストールを併用しています。後者は実行ファイル1つ、または Docker コンテナ1つを動かすだけでよく、スパンをディスクに保存しません。主に、チームメンバーがサードパーティサービスに大量に流し込んでしまわないよう安心させる用途です
インフラ監視には既存の設定があり、私たちの場合、すべてのインフラログとメトリクスを収集することに大きな価値は感じていません。OTEL のメトリクスとログはまだ初期段階だと思っていますが、ベンダーはそうは言いません
まだスクレイプできないインフラの一部から、単純な単発メトリクスを送るためのエンドポイントを探しています
Otel は新規プロジェクトなら良いかもしれませんが、すでにテレメトリがある本番サービスで有効にするのは、走行中の車のタイヤを交換するような感覚でした
otel だけを使い、非 otel フレームワークのないグリーンフィールドプロジェクトなら良いかもしれないとは思います。ですが、私はまだそういう世界には住んでいません
最大の問題の1つはローカル開発体験でした。ローカルでログ、トレース、メトリクスをサポートしたかったのですが、そのために Docker イメージを大量に立ち上げたくはありませんでした。デプロイ前に、メトリクス、トレース、バゲージ、アクティビティスパンがどう見えるかをログで確認したかったのです
最近 .NET チームが .NET Aspire を出しましたが、とても良いです。ローカル開発スタックであらゆるものを一か所に可視化しやすく、コードで書くオーケストレーターのように動作します
k8s にデプロイするときは、OTEL エンドポイントを DataDog Agent に向ければすべてがそのまま動きます。DataDog のカスタムトレースライブラリや SDK は避け、OTEL だけを使っています
今では開発体験がかなり良くなりました
https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals...
https://docs.datadoghq.com/opentelemetry/#overview
最初にローカル開発マシンへ設定するのに5分で済み、その後は別のターミナルタブで
/path/to/openobserveを実行するだけです。単一の静的リンクバイナリという巨大な複雑さを避けたいなら、ローカル・リモート実行用の Docker イメージも提供されています :Pきれいなグラフがあるオールインワン OpenTelemetry バックエンドで、私のプロジェクトではまだ検知できる形で失敗したことがありません
Aspire を使うとアプリレベルに不要な複雑さを追加し、狭いエコシステムに縛られます。ローカル開発には docker compose のような実証済みの代替手段が多くあり、Aspire が docker compose と環境変数より特にずっと簡単というわけでもありません
Python で otel をやるなら Logfire のクライアントを使うのがよいです。Logfire サービスを使わない場合でも同じです
オープンソースで、どの otel 互換エンドポイントにも送れます。しかも pydantic チームが作ったクライアントは、公式 otel ライブラリより10倍は良く、シンプルです
Samuel Colvin がどうやってここまで来たのかを説明する興味深いインタビューもあります: https://www.bitecode.dev/p/samuel-colvin-on-logfire-mixing-p...
そこで、OpenTelemetry の導入をコマンド1行くらい簡単にすることに焦点を当てたオープンソースプロジェクトを始めました: https://github.com/odigos-io/odigos
最近は多くのWebフレームワークが、ほとんどの計測を代わりにやってくれる。たとえば opentelemetry-js を使い、https://signoz.io のようなものを自前でホスティングすれば、1時間もかからず立ち上げられるし、カスタムコードを書かなくても多くのデータを得られる
OpenTelemetry はトレースから成長してきたが、メトリクスとログは専用ソリューションに任せたほうがはるかに良い
「漏れのある抽象化」や「漏れのあるフレームワーク」の問題のように感じる。すべてを一つの傘の下に入れたいなら、SQLデータベースでもこれらを同時にすべて扱える。だからといって、そうすべきという意味ではない
https://cra.mr/the-problem-with-otel/
それでも OTel を扱うたびに、やはり嫌になる
最後まで読めば、苦痛の大半は自分で作り出したものだと分かる。Python の標準的なスタック(mysql、flask、redis、requests など)ではとても簡単だった。サービスの先頭にいくつか import を入れるだけで自動的につながり、特に騒ぐこともなくすべてを追跡してくれる
aiohttpは最新バージョンが 3.11.X で、自動計測は3.Xをサポートするとされているが [0]、実際の結果は使っているaiohttpがどれだけ新しいかと、自動計測側の状態に左右されるすべてがうまく噛み合えば魔法のように動くが、その針の穴はかなり狭い
[0]: https://github.com/open-telemetry/opentelemetry-python-contr...
トレースは動作しており、spanmetrics exporter も設定したし、prometheus に直接クエリすると spanmetrics が見える。だが何をしても jaeger の「monitor」タブには表示されない
これに3日費やしたところで、上司が「いっそ手動計測して全部 SQL サーバーに送り、Grafana ダッシュボードを作ればどうか」と言ったが、それもやりたくない
最も単純なユースケースなのに動かない。さらに Grafana を載せる必要があるのだろうか?
複雑な理由は、実装するエンジニアのためではなく、Otel 互換ソフトウェアを売る企業のために設計されたからだ
むしろバックエンド側の導入はかなり遅かったと思う
意味は変わるものなので構わないが、今では仕様に合わせてソフトウェアを書くことを指す言葉がなくなり、「既存のソフトウェアをサーバーにデプロイすること」だけが残ったのは面白い