4 ポイント 投稿者 GN⁺ 2025-01-11 | 1件のコメント | WhatsAppで共有
  • 顧客からの 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$ContextOnlySpan
    • Expected 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 が失敗する

手動変換で OTel と OpenTracing を接続

  • SpanContextthis.traceLocal.currentContext() から取得されており、traceLocal は Lightbend の ExtendedTracer で初期化されていた
  • Lightbend のドキュメントには、ExtendedTracer にグローバルにアクセスする方法があった
    • GlobalExtendedTracer.get()
  • GlobalExtendedTracer.get()ExtendedTracer を返し、local() は計装ライブラリが使うものと同じ TraceLocal を返す
  • この TraceLocal には、OpenTracing SpanContext を受け取る 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 が伝播した
  • 例では 2 種類の計装を区別するために異なる名前を使ったが、実際の顧客環境では otel.service.namecinnamon.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件のコメント

 
GN⁺ 2025-01-11
Hacker News の意見
  • Otel を学んで移植している間ずっと、また Java の世界に戻ったような気分だった。コードを1行ずつ追うたびに EnterpriseFizzBuzz のようで、発見可能性はまったくなく、独自用語も何かに酔った人たちが作ったように見えた
    NodeJS では StatsD と比べて CPU 使用量が約4倍で、結局、自前で集計を作って使用量を下げ、タグの爆発も減らした。StatsD は複数プロセスが同じタグを報告しても問題ないが、OTEL は上書きしてしまう
    ピーク負荷では CPU 1つが 60〜80% の使用率で回っていて、何かを変えるまでは垂直スケールもできなかった。コアごとに1プロセスを使う言語に対して、OTEL は積極的に敵対しているようで冗談みたいだ。普通に Prometheus を使うほうがよく、実質的にほかの競合もない

    • OTEL は一種の ベンダーロックイン に近い状態だと思う。標準を作るには、複数の大企業と粗いスタートアップの要求を妥協させて、グルーガンで貼り合わせるしかなかったからだ
      .NET で単純な otel 設定をしようとして、組織が選んだベンダーのドキュメントを何時間も読んでも理解できず、同僚が運営する Discord に入った。その会社のビジネスモデルの一部は「オープンソース製品でまともな otel を使うなら金を払え」というものだったが、すぐに、費用がいくらであれそれだけの価値があると感じた
      OTEL 実装をするくらいなら、事前経験なしで安定したイベント/pub-sub ライブラリをもう1つ作るほうがましだ
    • 「ほかの競合もない」という点については、場合によっては Apache SkyWalking も見る価値がある。リソースをあまり食わず、設定と運用はかなりシンプルだが、UI やドキュメントはやや磨き込みが足りない: https://skywalking.apache.org/
      最小構成も実際かなり小さく、Web UI、サーバーインスタンス、すでに知っている DB 程度で足りる: https://skywalking.apache.org/docs/main/latest/en/setup/back...
      監視領域における Zabbix に似た面がある。どちらも誰かを感嘆させるものではないが、実用性は十分だ
    • 自分の結論も同じ。単に Prometheus と、好きな言語のクライアントライブラリを使えばよく、OTEL の話より1000倍はシンプルだ
    • C++ 方面で otel を使っているが、複数のアプリケーションから累積メトリクスを得るために、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

    • みんながとても簡単に作ったことで生じた問題を、この製品は 複雑に作ることで 解決するということ? ;P
    • Datadog よりは、単に良いメトリクス収集のほうを好む。一方で、OTEL が自分の好む方式より価値があるというのは理論上は理解している
      すべての 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

    • その用途にはこのプロジェクトが本当に良いです: https://github.com/grafana/docker-otel-lgtm
    • https://github.com/CtrlSpice/otel-desktop-viewer もあります
    • ただ https://github.com/openobserve/openobserve を使えばよいです
      最初にローカル開発マシンへ設定するのに5分で済み、その後は別のターミナルタブで /path/to/openobserve を実行するだけです。単一の静的リンクバイナリという巨大な複雑さを避けたいなら、ローカル・リモート実行用の Docker イメージも提供されています :P
      きれいなグラフがあるオールインワン OpenTelemetry バックエンドで、私のプロジェクトではまだ検知できる形で失敗したことがありません
    • .NET Aspire には納得していません。ローカル開発における複数サービスプロジェクトのサービスディスカバリとオーケストレーションという小さな問題を解きますが、それをアプリケーションレベルの関心事にして解決しています
      Aspire を使うとアプリレベルに不要な複雑さを追加し、狭いエコシステムに縛られます。ローカル開発には docker compose のような実証済みの代替手段が多くあり、Aspire が docker compose と環境変数より特にずっと簡単というわけでもありません
    • すべてが入った公式の all-in-one Docker イメージがあります
  • 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時間もかからず立ち上げられるし、カスタムコードを書かなくても多くのデータを得られる

    • SigNoz のリポジトリはこちら: https://github.com/signoz/signoz
    • マルチスレッドの非同期ランタイムにおけるコンテキスト伝播は簡単ではない。方法はいくつもあるが、バイトコードを計測するJVMエージェントが人気なのは、透過的に動作するからだ
  • OpenTelemetry はトレースから成長してきたが、メトリクスとログは専用ソリューションに任せたほうがはるかに良い
    「漏れのある抽象化」や「漏れのあるフレームワーク」の問題のように感じる。すべてを一つの傘の下に入れたいなら、SQLデータベースでもこれらを同時にすべて扱える。だからといって、そうすべきという意味ではない

    • Cramer は OTel からトレースを外したがっている。OpenTracing の創始者の一人であることを考えると皮肉だ
      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...
    • 最近、ごく単純な Flask アプリにこれを組み込む必要があった。社内イントラネット内でだけ動く必要があるので、単一サーバー上で docker compose により otel-collector-contrib、jaeger-all-in-one、prometheus を動かしている
      トレースは動作しており、spanmetrics exporter も設定したし、prometheus に直接クエリすると spanmetrics が見える。だが何をしても jaeger の「monitor」タブには表示されない
      これに3日費やしたところで、上司が「いっそ手動計測して全部 SQL サーバーに送り、Grafana ダッシュボードを作ればどうか」と言ったが、それもやりたくない
      最も単純なユースケースなのに動かない。さらに Grafana を載せる必要があるのだろうか?
    • gunicorn のようなものの背後でサーバーを動かすまではそうだ。その瞬間、すべての自動 importが止まり、全部自分でやらなければならなくなる
  • 複雑な理由は、実装するエンジニアのためではなく、Otel 互換ソフトウェアを売る企業のために設計されたからだ

    • それはよく分からない。ベンダーは自社専用のコード、エージェント、バックエンドで満足していたように思う。ロックイン効果により新しいコードをすべて書かなければならず、移行コストが非常に高くなるからだ
    • コントリビューターたちから見た姿はそうではなかった
      むしろバックエンド側の導入はかなり遅かったと思う
    • 細かい指摘に聞こえるかもしれないが、以前は「implement」は前者の活動、つまり仕様に合わせてソフトウェアを座って書くことを指していた。後者のように既存のソフトウェアを持ってきてサーバーにデプロイするという意味ではなかった
      意味は変わるものなので構わないが、今では仕様に合わせてソフトウェアを書くことを指す言葉がなくなり、「既存のソフトウェアをサーバーにデプロイすること」だけが残ったのは面白い