本記事は「Ylang:eBPF、Stap+、GDB などに対応する汎用言語」シリーズの第4回です。他の回は第1回第2回第3回をご参照ください。

コンテナを透過的に横断するトレース機能

Ylang は、コンテナの境界を越えて透過的なトレースを実現します。Docker や Kubernetes コンテナを通常のターゲットプロセスと同様にトレースすることが可能です。コンテナ化されたプロセスのプロセス ID またはプロセスグループ ID を指定してトレースを行うことができます。OpenResty XRay は、同一ホスト上で稼働している一部のコンテナ内のアプリケーションを自動的に検出することも可能です。

以下のスクリーンショットは、OpenResty XRay の Web コンソールが自動的に検出した、Kubernetes コンテナ内で実行されている Perl ターゲットアプリケーションを示しています。

Kubernetes 容器中的 Perl 应用程序

対象となるコンテナに対して、修正や追加の権限は一切必要ありません。これは 100% 非侵襲的な動的トレーシングの利点です。

Ylang コンパイラが生成したツールは、OpenResty XRay の Agent デーモンによって実行・管理されます。この Agent は、同一ホストOS上で稼働している Docker や Kubernetes コンテナを、対象コンテナ自体の協力なしに透過的に監視することができます。

追踪容器

OpenResty XRay の Agent プロセスをコンテナ内で実行することを好むユーザーもいます。この方式もサポートしていますが、Agent コンテナは特権を持つ必要があります。特権がない場合、他のコンテナを検査する権限がありません(これは特権コンテナの定義でもあります)。

容器追踪容器

従来通り、OpenResty XRay はターゲットプロセスにコードを注入したり、変更を要求したりすることはありません。対象コンテナの既存のセキュリティ分離や権限は一切損なわれません。

効率的なスタックアンワインディング(Stack Unwinding)

Ylang コンパイラは、スタックアンワインディング(OpenResty XRay のパッケージデータベースによってインデックス化)を非常に効率的なネイティブコードに自動的にコンパイルします。このコードは、実行時スタックからコールスタックトレースを生成したり、現在の実行時スタックから特定のローカル変数を読み取ったりする際に実行されます。

バックトレース (Backtrace) は、現在のコード実行コンテキストまたは「コードパス」を自然に表現する手段として極めて重要です。これらは、CPU、レイテンシー、メモリ使用量の分析用に生成されるフレームグラフを含む、多くのアナライザーの基盤となっています。

以下は Ylang の簡単な例です:

_probe usleep() {
    _print(_sym_ubt(_ubt()));
}

C 関数 usleep のエントリーポイントに動的プローブを設置し、簡易的な C 関数バックトレースを出力します。典型的な出力は以下の通りです:

f95e0: usleep[1]
401134: main[0]
27082: __libc_start_main[1]
40106e: _start[0]

なお、角括弧([])内の整数は、対象プログラムモジュールファイルのインデックスを示しています。0 はメインの実行ファイル(a.out)を表し、1a.out が依存する libc-2.27.so ファイルを表します。実際のマッピング関係は OpenResty XRay で確認することができます。

完全な C 関数バックトレース

Ylang の一部のバックエンドでは、組み込み関数 _print_full_ubt() をスタック上のすべての関数フレームに適用し、引数とローカル変数の値を含む完全なバックトレース出力することができます。以下は例となります:

_probe usleep() {
    _print_full_ubt();
}

以下は ODB バックエンドを使用した場合の出力例です:

f95e0: usleep[1] (useconds=0x3)
  ts=0x0
401134: main[0]
27082: __libc_start_main[1] (main=0x401126, argc=1, argv=0x7ffcd83d8378, init=<optimized>, fini=<optimized>, rtld_fini=<optimized>, stack_end=0x7ffcd83d8368)
  result = <optimized>
  unwind_buf=0x0
  not_first_call = <optimized>
  afct = <optimized>
  head = <optimized>
  cnt = <optimized>
  __value = <optimized>
  __value = <optimized>
  ptr = <optimized>
  __p = <optimized>
  __result = <optimized>
40106e: _start[0]

これは GDBbt full コマンドの出力に非常に類似しています。

C ランタイムスタックから特定の変数値を読み取る

完全なバックレースの出力は非常にコストがかかる場合があります。時には、現在のランタイムスタックから特定の変数のみを読み取る必要がある場合があり、これはより効率的です。例えば、 _stack_var("r", 1) という Ylang 関数呼び出しを使用すると、現在のスタック上(スタックの上部から下部まで)で r という名前の最初の非最適化ローカル変数(関数の引数を含む)の値を返します。この _stack_var 関数は、私たちの内部コードリポジトリではすでに利用可能ですが、OpenResty XRay 製品ではまだ利用できません。リリースされ次第、この記事を更新する予定です。

動的言語のバックトレース

Ylang は、動的言語のバックトレースを生成するためのヘッダーファイルを通じて標準ライブラリを提供しています。動的言語には Lua、PHP、Python、Perl などのスクリプト言語が含まれます。Go (golang)、Rust、C++ などの高度な静的型付け言語もサポートされています。今後、Ruby、Java(JVM)、JavaScript(NodeJS)、OCamel、Haskell、Erlang などの言語もサポートされる予定です。

以下では、Lua と PHP 言語のバックトレースの出力方法について説明いたします。

Lua バックトレース

例えば、LuaJIT 2.1 上で Lua コードが実行されている場合、以下のコードを書くことで Lua レベルのバックトレース文字列を生成することができます:

#include "lj21.y"

_probe lj_cf_os_exit() {
    printf("%s", lj_dump_bt(NULL, "min"));
}

ここでは、C 関数 lj_cf_os_exit() のエントリーポイントに動的プローブを配置し、最小化された形式で Lua レベルのバックトレースを出力しています。出力例は以下の通りです:

$ run-y -c 'luajit test.lua'
test.lua:c
test.lua:b
test.lua:a
test.lua:0
C:pmain

test.lua ファイルの内容:

local function c()
    local baz = "hello"
    os.exit(0)
end

local function b()
    local bar = 3.14
    c()
end

local function a()
    local foo = 32
    b()
end

a()

Lua 関数 os.exit()LuaJIT 仮想マシン内部の C 関数 lj_cf_os_exit() をトリガーします。

OpenResty XRay が提供する Lua レベルのフレームグラフアナライザーは、同様の Ylang コードを使用してフレームグラフを生成します。以下は例示図です:

完全な Lua バックトレース

前述の Ylang プログラムで lj_dump_bt(NULL, "full") を記述することで、各 Lua 関数スタックフレーム内のローカル Lua 変数値を含む完全な Lua バックトレースを取得できます。上記の test.lua スクリプトに対する出力例は以下の通りです:

[builtin#os.exit]
exit
test.lua:3
    baz = "hello"
test.lua:c
test.lua:8
    bar = 3.140000
test.lua:b
test.lua:13
    foo = 32
test.lua:a
test.lua:16
    c = function @test.lua:1: (GCfunc *)0x7feff0851578
    b = function @test.lua:6: (GCfunc *)0x7feff0851658
    a = function @test.lua:11: (GCfunc *)0x7feff08516c8
C:pmain

Lua 標準ライブラリは、デフォルトでは完全なバックトレースをサポートしていません。YlangLuaJIT 仮想マシンまたはターゲットプロセスとの協力やプロトコルを必要としません。これは、LuaJIT 仮想マシンの内部を理解しているためです。

PHP バックトレース

以下は PHP 7 のバックトレースをダンプする例です:

#include "php7.y"

_probe _timer.profile {
    printf("%s\n", php7_dump_bt());
    _exit();
}

以下は出力の例です:

C:sapi_cli_single_write
Application->setLogger
/tmp/test.php:24
C:sapi_cli_single_write
Application->getLogger
/tmp/test.php:30
C:sapi_cli_single_write
class@anonymous@/tmp/test.php:24$0->log
/tmp/test.php:30

オープンソースツールチェーンとの比較

SystemTap は、スタックアンワインディング(通常は DWARF 形式)をコンパイルされたツールに直接組み込み、実行時にスタックアンワインディングを解析します。この方法が遅い理由は、スタックアンワインディング形式(例えば DWARF)が複雑なデータ形式であり1、通常は速度ではなく、コンパクト性とスペース効率のために最適化されているためです。 Ylangは真のコンパイラであり、DWARF データを専用のネイティブコードに変換し、可能な限り高速に実行します。GDB も同様に DWARF データを解析するだけで、実際のコンパイラとしては機能しません。

Linux の perf は、スタックアンワインディングのために完全な実行時スタックメモリの内容をユーザーランド(User-land)にコピーしますが、この方法には以下の欠点があります:

  1. カーネル空間から過剰なデータをユーザーランドにコピーする可能性があり、そのデータの大部分は実行時のスタックアンワインディングには不要です。このコピー操作によってメモリバスが飽和状態になりやすく、機密データが露出してセキュリティ脆弱性の対象となる可能性があります。

  2. 複雑なツールがカーネル空間内でバックトレースを直接利用できません。

オープンソースの eBPF ツールチェーンは、ターゲットプログラムでのフレームポインタレジスタ2の使用に依存しており、これは x86_64 の ABI 仕様に反しています。ユーザーは C/C++ コンパイラフラグ -fno-omit-frame-pointer を使用してほとんどのターゲットプログラムを再コンパイルする必要があり、これは動的トレーシングの黄金律に反します:ターゲットプログラムの最適化を無効にする必要も、特別な対応も必要ありません。

オープンソースツールチェーンは、動的言語の関数のスタックアンワインディングや、それらのバックトレースの生成をサポートしていません。

終了したプロセスの分析(Core Dumps ファイル)

YlangGDB バックエンドは、クラッシュしたプロセスが生成した core dump ファイルの分析に非常に有用です。Stap+ や eBPF などの他のバックエンドは、core dump ファイルをサポートしていません。

これは、同じ分析ツールでアクティブなプロセスと終了したプロセスの両方を分析できる初めての機会です。高級言語である Ylang のおかげで実現しました。

Ylang を core dumps の分析に使用する場合、_oneshot_begin 以外のプローブを指定してもほとんど意味がありません。結局のところ、core dump は終了したプロセスの「遺骸」なのです3

OpenResty XRay 用于 Core Dump 分析

また、Red Hat の crash コマンドラインユーティリティをサポートする新しい Ylang バックエンドを追加する計画もあります。これにより、Ylang を使用して Linux カーネルのクラッシュダンプ(例えば kdump からのファイル)をデバッグできるようになります。オペレーティングシステムカーネルの「遺骸」を分析することも興味深い取り組みとなるでしょう。

極めて低いトレーシングオーバーヘッド

動的トレーシングは、特定の分析目的に必要な情報のみを収集するため、実行時のオーバーヘッドが極めて低くなっています。これは、ログデータにできるだけ多くの情報を収集する従来の方法4とは大きく異なります。後者は、大量のデータの書き込みと転送によって、より高いオーバーヘッドが発生します5

通常、動的トレーシングはサンプリングベースでもあります。ターゲットプロセスにコードを注入したり特別なモジュールをロードしたりすることがないため、サンプリングを行っていない時のオーバーヘッドは厳密にゼロです。ほとんどの分析ツールのオーバーヘッドは、サンプリングウィンドウ中でも通常は測定不可能なレベルです。ターゲットアプリケーションが最大スループットに達している場合でも、サンプリングコストは通常、スループットの 5% 未満です。一部の全数検査ツールでは、最大アプリケーションスループットの 30% 以上といった、より高いオーバーヘッドが発生する可能性があります。しかし、オンラインパフォーマンスが既に極めて低い場合でも、本番環境で使用することができます。

Ylang は最適化コンパイラでもあり、様々なバックエンド向けにコンパクトで効率的なコードを生成します。例えば、YlangGDB バックエンドが生成する Python コードは、手書きのコードと比べて約 4 倍の速度で実行されます6

標準 Ylang ライブラリとツール

前のセクションで見たように、Ylang は、C/C++ のコード再利用方式と同様に、より多くの関数やその他の機能をインポートするための標準ヘッダーファイルを提供しています。lj21.yphp7.y のような標準ヘッダーファイルは、Ylang の標準ライブラリを構成しています。これらの標準ライブラリも Ylang で実装されています。

将来的には、Ylang7で複数のコンパイル単位をサポートし、コンパイラが再コンパイルする必要のあるコード量を削減する予定です。

また、OpenResty XRay は、様々な種類のオープンソースソフトウェア向けの標準アナライザーやツールを提供しており、そのほとんどが Ylang で書かれています。一部は YLuaYSQL などのより高度な言語で書かれています。Ylang の上に言語抽象化を構築できる完全な「フードチェーン」を持っています。例えば、C 構文を使用して C レベルのデータ構造を検査するよりも、Lua 構文を使用して Lua レベルのデータ構造を操作する方が自然です。

Y 语言食物链

ネットワークフィルタリングと制御

Ylang を使用して、カーネル内で実行されるネットワークプログラムを作成し、ネットワークパケットを操作・処理することが可能です。これは Linux カーネルのネットワークプロトコルスタックにおける eBPF のサポートによって実現されています。eBPF の前身である BPF は、ネットワークフィルタリング専用に設計されました。YlangOpenResty XRay を持つ eBPF ツールチェーンにより、従来の eBPF ツールチェーンや仮想マシン実装における制限から解放されました。XDP や TC サブシステムに強力なプログラムを接続することが可能となりました。

この機能は現在 OpenResty XRay に実装されており、OpenResty DDoS プレゼンテーションOpenResty Edge の製品ページでご覧いただけます。

Ylang コンパイラの実装

Ylang コンパイラは Fan 言語(fanlang とも呼ばれる)で実装されています。Fan 言語は、汎用および特定用途向け言語の最適化コンパイラを実装するために特別に設計された Perl 6(または Raku)の方言です。Fanlang コンパイラは最適化された LuaJIT バイトコードを生成し、Rakudo8 などのオープンソースの Perl 6 実装と比較して大幅に高速な実行を実現しています。Fanlang コンパイラは近日中に OpenResty Edge および OpenResty Plus 製品の一部となる予定です。

オペレーティングシステムのサポート

Ylangおよび OpenResty XRay は、現在サポート期間中の主要な Linux ディストリビューションの大部分をサポートしています。Ubuntu 14.04 や CentOS 6 のような、すでにサポート期間が終了したディストリビューションバージョンでも、ある程度の機能は利用可能です。

次にサポートが予定されている主要なオペレーティングシステムは Android です。これは、そのカーネルが本質的に Linux ベースであるためです。Ylang の eBPF+ バックエンドも、このプラットフォームで直接実行できるようになる予定です。

また、BSD や macOS などの一般的でないオペレーティングシステムのサポートも計画しています。Windows についても技術的には実現可能です。今後の展開にご期待ください。

オープンソースコミュニティへの貢献

私たちは、様々なオープンソースソフトウェアのトラブルシューティングと最適化を支援するため、YlangOpenResty XRay を開発いたしました。現代では、オープンソースソフトウェアは至る所で使用されておりますが、日常的に使用し愛用しているオープンソースソフトウェアについて、深い理解を持っている方は少ないのが現状です。多くの場合、これらのオープンソースソフトウェアが適切に活用されていない、あるいは最適化されていない状況にあります。YlangOpenResty XRay 自体も、多くの高品質なオープンソースコードを活用しております。

Ylangにより、非常に複雑な分析ツールの作成が容易になりましたが、これは基盤となるオープンソースインフラストラクチャに前例のない負荷をかけることとなりました。実際、SystemTap、Clang/LLVM、libbpf、GDB、Linux カーネル(eBPF メカニズム、perf イベント、libbpf、btftool など)といった、使用しているほぼすべてのオープンソースコンポーネントにおいて、多くの複雑な課題に直面いたしました。

私たちは、これらのオープンソースプロジェクトに対して、積極的に問題を報告し、修正パッチを提出することで、オープンソースコミュニティの改善と最適化に貢献してまいりました。特に、SystemTap プロジェクトの作者である Frank Ch. Eigler 氏には、私たちのパッチを迅速に審査・承認していただき、長年にわたりご支援いただいておりますことを、深く感謝申し上げます。また、OpenResty などの自社のオープンソースプロジェクトも主導しており、オープンソース活動の価値を強く信じております。

結論

Ylang は、多様なデバッグフレームワークとランタイム環境に対応した汎用的なデバッグおよび動的トレーシング言語です。さらに、Ylangが使用するバックエンドは、既存のオープンソース製品(存在する場合)の制限を解決し、多くの新機能を追加しております。動的トレーシングツールの開発が、これまでになく容易になりました。ユーザーの皆様は、OpenResty XRay 製品を通じて Ylangとそのツールチェーンをご利用いただけるほか、弊社が開発した Ylang標準アナライザーもご活用いただけます。

本シリーズでは、Ylangの特徴と利点について、簡単な例を用いて概要をご紹介させていただきました。より詳細な情報は、公式ドキュメントをご参照ください。

謝辞

私たちの取り組みは、動的トレーシングおよびデバッグ分野における多くの先駆者の方々の功績の上に成り立っております。

Brendan Gregg 氏のブログは、2012年に初めて私の関心を引きました。当時、彼は主に DTrace について論じており、近年の Linux における eBPF および perf ツールチェーンに関する彼の業績は、現在も私たちに大きな刺激を与え続けております。

2012年から2016年にかけて Cloudflare に在籍していた際、大規模クラウド環境における実際の問題に動的トレース技術を適用する貴重な機会を得ることができました。

Frank Ch. Eigler 氏の SystemTap は、最も強力なオープンソースの動的トレーシングフレームワークを提供し、初期の段階で大きな助けとなりました。長年にわたり、Frank 氏および Red Hat の SystemTap 開発エンジニアの方々と密接に協力させていただいております。

Linux の eBPF に携わる方々にも敬意を表します。彼らは DTrace のカーネル仮想マシンの強力な機能を Linux カーネルに導入し、ネットワークおよびトレース分野へと拡張されました。

YlangOpenResty XRayの実現に尽力した OpenResty Inc. の開発エンジニアの皆様、そして日々製品の改善にご協力いただいている OpenResty XRay のユーザーの皆様に、心より感謝申し上げます。

最後に、2000年代初頭に Sun Microsystems で DTrace を開発されたエンジニアの方々にも感謝の意を表します。彼らのイノベーションは、コンピューター世界に新たな章を開きました。

著者について

章亦春(Zhang Yichun)は、オープンソースの OpenResty® プロジェクトの創始者であり、OpenResty Inc. の CEO および創業者です。

章亦春(GitHub ID: agentzh)は中国江蘇省生まれで、現在は米国ベイエリアに在住しております。彼は中国における初期のオープンソース技術と文化の提唱者およびリーダーの一人であり、Cloudflare、Yahoo!、Alibaba など、国際的に有名なハイテク企業に勤務した経験があります。「エッジコンピューティング」、「動的トレーシング」、「機械プログラミング」 の先駆者であり、22 年以上のプログラミング経験と 16 年以上のオープンソース経験を持っております。世界中で 4000 万以上のドメイン名を持つユーザーを抱えるオープンソースプロジェクトのリーダーとして、彼は OpenResty® オープンソースプロジェクトをベースに、米国シリコンバレーの中心部にハイテク企業 OpenResty Inc. を設立いたしました。同社の主力製品である OpenResty XRay動的トレーシング技術を利用した非侵襲的な障害分析および排除ツール)と OpenResty XRay(マイクロサービスおよび分散トラフィックに最適化された多機能ゲートウェイソフトウェア)は、世界中の多くの上場企業および大企業から高い評価を得ております。OpenResty 以外にも、章亦春は Linux カーネル、Nginx、LuaJITGDBSystemTapLLVM、Perl など、複数のオープンソースプロジェクトに累計 100 万行以上のコードを寄与し、60 以上のオープンソースソフトウェアライブラリを執筆しております。

翻訳

英語版の原文と日本語訳版(本文)をご用意しております。読者の皆様による他の言語への翻訳版も歓迎いたします。全文翻訳で省略がなければ、採用を検討させていただきます。心より感謝申し上げます!


  1. DWARF 形式は非常に強力で、ほぼチューリング完全(Turing-Complete)と見なすことができます。 ↩︎

  2. 例えば、x86_64 アーキテクチャでは、フレームポインタレジスタは rbp です。 ↩︎

  3. 私たちは、core dump ファイルを復活したプロセスにロードすることで、クラッシュしたプロセスを復活させる独自の技術を持っています。関数プローブやシステムコールプローブなど、Ylang のすべてのプローブを自由に使用してトレースすることができます。 ↩︎

  4. ログデータはファイルシステムに保存することも、ネットワークにリアルタイムで送信することもできます。 ↩︎

  5. これには大量の CPU 時間、メモリ帯域幅、ディスク/ネットワーク帯域幅などが必要です。 ↩︎

  6. Ylang コンパイラが生成する GDB Python コードは、gdb.Valuegdb.Type オブジェクトを使用しないため、実行時のオーバーヘッドが大幅に削減されます。人間の脳にとっては複雑すぎるため、このような Python コードを手書きすることは不可能です。 ↩︎

  7. C 言語や C++ 言語にも似ています。 ↩︎

  8. Fan コンパイラのコンパイル速度も Rakudo と比較して大幅に高速です。 ↩︎