動的トレース技術についての雑談
本稿は 2016 年 5 月初旬に執筆され、2020 年 2 月中旬に大幅な更新と改訂が行われました。今後も継続的に更新される予定です。
動的トレーシングとは
動的トレーシング技術(Dynamic Tracing)というテーマについて、皆様と共有できることを大変嬉しく思います。私個人にとっても、非常に興味深いトピックです。では、動的トレーシング技術とは何でしょうか?
動的トレーシング技術は、ポストモダンな高度なデバッグ技術の一つです。この技術により、ソフトウェアエンジニアは非常に低コストで、短時間のうちに、ソフトウェアシステムに関する複雑な問題に対する答えを得ることができ、より迅速な問題の特定と解決が可能となります。この技術が発展し普及した背景には、急速に成長するインターネット時代という大きな文脈があります。エンジニアとして、私たちは二つの大きな課題に直面しています。一つは規模の問題です。ユーザー規模、データセンターの規模、マシンの数など、すべてが急速に拡大しています。もう一つは複雑性の課題です。ビジネスロジックはますます複雑になり、運用するソフトウェアシステムも複雑化の一途をたどっています。システムは多くの層に分かれており、オペレーティングシステムのカーネルを基盤とし、その上にデータベースや Web サーバーなどの様々なシステムソフトウェアが存在します。さらにその上には、スクリプト言語や他の高級言語の仮想マシン、インタプリタ、そして JIT(Just-In-Time)コンパイラがあります。最上位には、アプリケーションレベルの様々なビジネスロジックの抽象層と、多くの複雑なコードロジックが存在します。
これらの大きな課題がもたらす最も深刻な結果は、今日のソフトウェアエンジニアが本番システムに対する洞察力と制御力を急速に失いつつあることです。このように複雑で巨大なシステムでは、様々な問題が発生する可能性が大幅に高まっています。致命的な問題もあります。例えば、500 エラーページ、メモリリーク、誤った結果を返すなどの問題です。もう一つの大きな問題カテゴリーはパフォーマンスの問題です。ソフトウェアが特定の時間帯や特定のマシンで非常に遅く動作することがありますが、その理由が分からないことがあります。現在、クラウドコンピューティングとビッグデータが普及する中、このような大規模な本番環境での不可解な問題は増加の一途をたどり、エンジニアの時間と労力の大部分を占めることになります。ほとんどの問題は本番環境でしか発生せず、再現が困難または不可能です。また、発生率が 1%、0.1%、あるいはそれ以下という問題もあります。理想的には、マシンをオフラインにしたり、コードや設定を変更したり、サービスを再起動したりすることなく、システムが稼働中のまま問題を分析し、特定し、それに基づいて適切な解決策を講じることができれば完璧です。そうすれば、毎晩ぐっすり眠ることができます。
動的トレーシング技術は、まさにこのようなビジョンの実現を可能にし、エンジニアの生産性を大幅に向上させることができます。私は今でも、中国の Yahoo! で働いていた頃、深夜にタクシーで会社に駆けつけて本番の問題に対応しなければならなかったことを覚えています。これは明らかに望ましくない、挫折感の強い生活と仕事のスタイルでした。その後、私はアメリカの CDN 企業で働いていましたが、そこでのお客様は独自の運用チームを持っており、彼らは CDN が提供する生ログを頻繁にチェックしていました。私たちにとっては 1% や 0.1% の問題かもしれませんが、彼らにとっては重要な問題であり、報告が上がってきました。そして私たちは調査を行い、真の原因を突き止めて、彼らにフィードバックしなければなりませんでした。これらの実際に存在する多くの現実的な問題が、新しい技術の発明と創造を促進しています。
動的トレーシング技術の素晴らしい点の一つは、それが「生体分析」技術だということです。つまり、あるプログラムやソフトウェアシステム全体が稼働中で、本番環境でサービスを提供し、実際のリクエストを処理している状態でも、(それ自身の意思とは関係なく)分析を行うことができ、まるでデータベースにクエリを実行するようなことが可能です。これは非常に興味深いことです。多くのエンジニアが見落としがちな点は、実行中のソフトウェアシステム自体が、貴重な情報の大部分を含んでおり、リアルタイムに変化する特別な「データベース」として直接クエリできるということです。もちろん、この特別な「データベース」は読み取り専用でなければなりません。そうでなければ、私たちの分析やデバッグ作業がシステム自体の動作に影響を与え、オンラインサービスを危険にさらす可能性があります。オペレーティングシステムのカーネルの助けを借りて、外部から一連の的確なクエリを発行し、このソフトウェアシステムの実行過程における多くの貴重な一次情報の詳細を取得し、問題分析やパフォーマンス分析などの作業を導くことができます。
動的トレーシング技術は通常、オペレーティングシステムのカーネルをベースに実装されています。オペレーティングシステムのカーネルは、ソフトウェアの世界全体を制御できます。なぜなら、それは「創造主」のような立場にあるからです。カーネルは絶対的な権限を持ち、同時にソフトウェアシステムに対する私たちの様々な「クエリ」がシステム自体の正常な動作に影響を与えないことを保証できます。言い換えれば、このようなクエリは十分に安全で、本番システムで大規模に使用できるものでなければなりません。ソフトウェアシステムを「データベース」としてクエリする際には、クエリ方法の問題が出てきます。明らかに、私たちは SQL のような方法でこの特別な「データベース」にクエリを実行するわけではありません。
動的トレーシングでは、通常プローブというメカニズムを通じてクエリを発行します。ソフトウェアシステムの特定のレベル、または複数のレベルに、プローブを設置し、これらのプローブに関連する処理プログラムを自分で定義します。これは東洋医学の鍼灸に似ています。ソフトウェアシステムを人体と見なすと、特定のツボに「針」を刺し、それらの針先には通常、自分で定義した「センサー」があり、必要な重要な情報をそれらのツボから自由に収集し、それらの情報を集約して、信頼できる診断と実行可能な治療計画を生成することができます。ここでのトレーシングは通常、二つの次元に関係します。一つは時間の次元で、ソフトウェアが継続的に実行されているため、時間軸上で連続的に変化するプロセスがあります。もう一つは空間の次元で、複数の異なるプロセス(カーネルプロセスを含む)が関与する可能性があり、各プロセスには独自のメモリ空間やプロセス空間があるため、異なるレベル間、および同じレベルのメモリ空間内で、縦横両方向に多くの貴重な空間情報を取得できます。これは、蜘蛛が巣の上で獲物を探すのに似ています。
オペレーティングシステムのカーネル内から情報を取得することも、ユーザーモードプログラムなどの高いレベルから情報を収集することもできます。これらの情報は時間軸上で関連付けることができ、完全なソフトウェアの全体像を構築し、複雑な分析を効果的に導くことができます。ここで非常に重要な点は、これが非侵襲的であるということです。ソフトウェアシステムを人体に例えるなら、病気を診断するためだけに生きている人の体を切り開きたくはないでしょう。その代わりに、X 線撮影をしたり、MRI 検査をしたり、脈を診たり、あるいは最も単純に聴診器で聴診したりします。本番システムの診断も同様であるべきです。動的トレーシング技術により、非侵襲的な方法で、オペレーティングシステムのカーネルを修正したり、アプリケーションプログラムを修正したり、ビジネスコードや設定を変更することなく、迅速かつ効率的に必要な情報、一次情報を正確に取得し、調査中の様々な問題の特定を支援することができます。
私は、ほとんどのエンジニアがソフトウェア構築のプロセスに特に精通していると思います。これは実際に私たちの基本的なスキルです。通常、異なる抽象化レベルを確立し、ボトムアップまたはトップダウンで、層ごとにソフトウェアを構築していきます。ソフトウェアの抽象化レベルを確立する方法は多くあり、オブジェクト指向のクラスやメソッド、あるいは直接関数やサブルーチンなどを通じて行うことができます。一方、デバッグのプロセスは、ソフトウェア構築の方法とは正反対です。私たちは元々確立された抽象化レベルを容易に「破る」ことができ、任意の一つまたは複数の抽象化レベルで必要な情報を自由に取得できる必要があります。これは、カプセル化設計や分離設計、あるいはソフトウェア構築時に人為的に確立された制約に関係なく行えます。なぜなら、デバッグ時には可能な限り多くの情報を取得したいからです。問題は任意のレベルで発生する可能性があるからです。
動的トレーシング技術は通常カーネルベースであり、カーネルは「創造主」として絶対的な権威を持つため、この技術は容易にソフトウェアの各抽象化レベルとカプセル化を貫通することができます。したがって、ソフトウェア構築時に確立された抽象化とカプセル化レベルは実際には障害とはなりません。逆に、ソフトウェア構築時に確立された適切な設計の抽象化とカプセル化レベルは、デバッグプロセスに役立ちます。この点については、後で詳しく説明します。私は自分の仕事の中で、本番環境で問題が発生したときに非常にパニックになり、可能性のある原因を無作為に推測するものの、その推測や仮説を支持または反証する証拠が不足しているエンジニアをよく見かけます。彼らは本番環境で繰り返し試行錯誤を行い、繰り返し混乱を引き起こし、手がかりを全く掴めず、自分自身と周囲の同僚を苦しめ、貴重なトラブルシューティングの時間を無駄にしています。動的トレーシング技術を手に入れた後、問題の調査自体が非常に興味深いプロセスとなり、本番環境で不可解な問題に遭遇すると興奮を覚え、まるで魅力的なパズルを解く機会を得たかのような感覚になります。もちろん、これはすべて、情報収集と推論を支援し、仮説や推測を迅速に証明または反証するための強力なツールを持っているという前提があってこそです。
動的トレーシングの利点
動的トレーシング技術は通常、対象アプリケーションの協力を必要としません。例えば、ある人が運動場で走っている最中に健康診断を行う場合、その人がまだ運動している間に、動的な X 線写真を撮ることができ、しかもその本人は気付きません。よく考えてみると、これは実に素晴らしいことです。各動的トレーシングに基づく分析ツールはすべて「ホットプラグ」方式で動作します。つまり、いつでもこのツールを実行し、いつでもサンプリングを開始し、いつでもサンプリングを終了することができ、対象システムの現在の状態を気にする必要はありません。多くの統計と分析は、実際にはシステムが本番環境に展開された後に必要性が生じたものです。将来どのような問題に遭遇するか、また未知の問題を調査するために必要な情報をすべて予測することは不可能です。動的トレーシングの利点は、「いつでもどこでも、必要に応じて収集」できることです。さらにもう一つの利点は、それ自体のパフォーマンスオーバーヘッドが極めて小さいことです。慎重に作成されたデバッグツールのシステムの極限性能への影響は、通常 5% 以下、さらにはそれ以下です。そのため、通常はエンドユーザーに観察可能なパフォーマンスの影響を与えることはありません。また、このような小さなパフォーマンスオーバーヘッドも、実際にサンプリングを行う数十秒または数分間のみ発生します。デバッグツールの実行が終了すると、本番システムは自動的に元の 100% のパフォーマンスに戻り、引き続き前進します。
DTrace と SystemTap
動的トレーシングについて語る際、DTrace について触れないわけにはいきません。DTrace は現代の動的トレーシング技術の先駆けと言えます。21世紀初頭に Solaris オペレーティングシステムで誕生し、当時の Sun Microsystems 会社のエンジニアによって開発されました。多くの方は Solaris システムと Sun 会社の名前をご存知かもしれません。
最初に生まれた時、こんな話があったと記憶しています。当時、Solaris オペレーティングシステムの数人のエンジニアが、一見非常に不可解な本番の問題を調査するために数日間を費やしました。最初は高度な問題だと思い、特に力を入れて取り組みましたが、数日かけた後、実際には非常に単純な、目立たない箇所の設定の問題だったことが判明しました。この出来事の後、これらのエンジニアは深く反省し、DTrace という非常に高度なデバッグツールを作成し、将来の仕事で単純な問題に過度の労力を費やすことを避けようとしました。結局、いわゆる「不可解な問題」のほとんどは単純な問題で、「デバッグできないと悩ましく、デバッグできたらさらに悩ましい」タイプのものです。
DTrace は非常に汎用的なデバッグプラットフォームで、C 言語に似た D というスクリプト言語を提供しています。DTrace ベースのデバッグツールはすべてこの言語を使用して作成されています。D 言語は「プローブ」を指定するための特別な構文をサポートしており、この「プローブ」には通常、位置を記述する情報があります。カーネル関数の入口や出口、あるいはユーザーモードプロセスの関数の入口や出口、さらにはプログラムの任意のステートメントやマシン命令に配置することができます。D 言語でデバッグプログラムを作成するには、システムに関する一定の理解と知識が必要です。これらのデバッグプログラムは、複雑なシステムに対する洞察力を取り戻すための強力なツールです。Sun 会社の元エンジニアである Brendan Gregg は、DTrace の最初期のユーザーの一人で、DTrace がオープンソース化される前からユーザーでした。Brendan は多くの再利用可能な DTrace ベースのデバッグツールを作成し、DTrace Toolkit というオープンソースプロジェクトにまとめました。DTrace は最も早期の動的トレーシングフレームワークであり、最も有名なものの一つです。
DTrace の利点は、オペレーティングシステムのカーネルと密接に統合されたアプローチを採用していることです。D 言語の実装は実際には仮想マシン(VM)で、Java 仮想マシン(JVM)に似ています。その利点の一つは、D 言語のランタイムがカーネルに常駐し、非常にコンパクトであるため、各デバッグツールの起動時間と終了時間が非常に短いことです。しかし、私は DTrace にも明らかな欠点があると感じています。私が特に不便に感じる欠点の一つは、D 言語にループ構造が欠けていることです。これにより、対象プロセス内の複雑なデータ構造を分析するツールの作成が非常に困難になっています。DTrace の公式見解では、ループを欠いている理由は過熱したループを避けるためとされていますが、明らかに DTrace は VM レベルで各ループの実行回数を効果的に制限することができます。もう一つの大きな欠点は、DTrace のユーザーモードコードのトレーシングサポートが比較的弱く、ユーザーモードのデバッグシンボルを自動的にロードする機能がないため、D 言語内でユーザーモードの C 言語構造体などの型を自分で宣言する必要があることです。1
DTrace の影響は非常に大きく、多くのエンジニアが他のオペレーティングシステムに移植しました。例えば、アップルの Mac OS X オペレーティングシステムには DTrace の移植版があります。実際、近年リリースされたすべてのアップルのノートパソコンやデスクトップコンピュータには、すぐに使える dtrace コマンドラインツールが搭載されています。興味のある方は、アップルマシンのコマンドラインターミナルで試してみることができます。これはアップルシステム上の DTrace の移植版です。FreeBSD オペレーティングシステムにもこのような DTrace の移植版があります。ただし、デフォルトでは有効になっていません。FreeBSD の DTrace カーネルモジュールをコマンドでロードする必要があります。OOracle も自社の Oracle Linux ディストリビューションで Linux カーネル向けの DTrace の移植を開始しました。しかし、Oracle の移植作業はあまり進展がないようです。結局、Linux カーネルは Oracle の管理下にはなく、DTrace はオペレーティングシステムのカーネルと密接に統合する必要があるからです。同様の理由で、一部の勇敢なエンジニアによる DTrace の Linux 移植の試みも、本番環境レベルの要件からはまだ遠い状態です。
Solaris 上のネイティブな DTrace と比較して、これらの DTrace 移植版はいずれも特定の高度な機能が不足しており、機能面では元の DTrace に及びません。
DTrace の Linux オペレーティングシステムへのもう一つの影響は、SystemTap というオープンソースプロジェクトに反映されています。これは Red Hat 社のエンジニアによって作成された比較的独立した動的トレーシングフレームワークです。SystemTap は独自の小言語を提供しており、D 言語とは異なります。明らかに、Red Hat は多くのエンタープライズユーザーにサービスを提供しており、彼らのエンジニアは毎日多くの本番環境の「不可解な問題」に対処する必要があります。このような技術の誕生は、必然的に現実のニーズによって促進されたものです。私は SystemTap が現在の Linux 世界で最も強力で実用的な動的トレーシングフレームワークだと考えています。自分の仕事では何年もうまく使ってきました。SystemTap の作者である Frank Ch. Eigler と Josh Stone らは、非常に熱心で賢明なエンジニアです。私が IRC やメーリングリストで質問すると、彼らは通常、非常に迅速かつ詳細に回答してくれます。特筆すべきは、私も SystemTap に比較的重要な新機能を提供したことがあり、任意のプローブコンテキストでユーザーモードのグローバル変数の値にアクセスできるようにしました。当時、私が SystemTap のメインラインにマージしたC++ パッチは約 1,000 行の規模でしたが、SystemTap の作者たちの熱心な支援のおかげでした。この新機能は、私が SystemTap をベースに実装した動的スクリプト言語(Perl や Lua など)のフレームグラフツールで重要な役割を果たしています。
SystemTap の利点は、非常に成熟したユーザーモードデバッグシンボルの自動ロード機能を持ち、同時にループなどの言語構造を持っているため、より複雑な探針処理プログラムを作成でき、多くの複雑な分析処理をサポートできることです。SystemTap は初期の実装の未熟さにより、インターネット上には多くの時代遅れの批判や非難が溢れています。ここ数年、SystemTap は大きな進歩を遂げています。2017 年に私が設立した OpenResty Inc. も SystemTap に対して非常に大きな強化と最適化を行いました。
もちろん、SystemTap にも欠点があります。まず、Linux カーネルの一部ではないため、つまりカーネルと密接に統合されていなくて、メインラインカーネルの変更を常に追いかける必要があります。もう一つの欠点は、通常、その「小言語」スクリプト(D 言語に似ています)を Linux カーネルモジュールの C ソースコードに動的にコンパイルする必要があるため、オンラインで C コンパイラツールチェーンと Linux カーネルのヘッダーファイルをデプロイする必要があることです。これらの理由により、SystemTap スクリプトの起動は DTrace と比べてはるかに遅く、JVM の起動時間に似ています。これらの欠点2はありますが、全体として SystemTap は非常に成熟した動的トレーシングフレームワークです。
DTrace も SystemTap も、実際には完全なデバッグツールの作成をサポートしていません。なぜなら、どちらもコマンドライン対話のための便利なプリミティブが欠けているからです。そのため、現実世界では、これらに基づく多くのツールの外側に、Perl、Python、または Shell スクリプトで書かれたラッパーがあります。クリーンな言語を使用して、完全なデバッグツールを記述するために、私は以前 SystemTap 言語を拡張し、より高レベルの「マクロ言語」を実装しました。これを stap++ 3と呼んでいます。私自身が Perl で実装した stap++ インタープリターは、stap++ ソースコードを直接解釈実行し、内部で SystemTap コマンドラインツールを呼び出すことができます。興味のある方は、GitHub 上の stapxx というコードリポジトリをチェックしてください。このリポジトリには、私の stap++ マクロ言語を使用して実装された多くの完全なデバッグツールも含まれています。
SystemTap の実運用での活用
DTrace が今日このように大きな影響力を持つようになったのは、有名な DTrace の伝道師である Brendan Gregg 氏の功績を抜きには語れません。前述したように、彼は最初 Sun Microsystems 会社で Solaris のファイルシステム最適化チームに所属し、最も早期の DTrace ユーザーでした。DTrace とパフォーマンス最適化に関する数冊の本を執筆し、動的トレーシングに関する多くのブログ記事も書いています。
2011 年に淘宝を離れた後、私は福州で 1 年ほど「田園生活」を送りました。田園生活の最後の数ヶ月間、私は Brendan の公開ブログを通じて DTrace と動的トレーシング技術を体系的に学びました。実は、最初に DTrace を知ったのは、ある Weibo の友人のコメントで、DTrace という名前だけが言及されていたからです。そこで、これが一体何なのか理解しようと思いました。調べてみると、これは全く新しい世界で、コンピューティングの世界全体に対する私の見方を完全に変えました。そこで、私は多くの時間を費やし、Brendan の個人ブログを一つ一つ丁寧に読み込みました。そしてついにある日、大きな悟りを得たような感覚を覚え、動的トレーシング技術の精髄を理解することができました。
2012 年、私は福州での「田園生活」を終え、前述した CDN 企業に入社するため渡米しました。そして直ちに、SystemTap と私が理解した動的トレーシングの一連の手法を、この CDN 企業のグローバルネットワークに適用し、非常に不可解な本番環境の問題を解決し始めました。この企業で私が観察したところ、多くのエンジニアが本番環境の問題を調査する際、しばしばソフトウェアシステムに自らポイントを埋め込んでいました。これは主にビジネスコード、さらには Nginx のようなシステムソフトウェアのコードベースに、自ら変更を加え、カウンターを追加したり、ログを記録するポイントを埋め込んだりするというものでした。この方法により、大量のログが本番環境でリアルタイムに収集され、専用のデータベースに入り、その後オフライン分析が行われます。明らかにこの方法のコストは膨大で、ビジネスシステム自体の修正と保守コストが急激に増加するだけでなく、大量の埋め込みポイント情報を全量収集し保存するオンラインのオーバーヘッドも非常に大きなものとなります。また、よく起こる状況として、ある人が今日ビジネスコードに収集ポイントを埋め込み、他の人が明日また似たようなポイントを埋め込み、その後これらのポイントはコードベースに忘れ去られ、誰も気にしなくなります。最終的にこのようなポイントはますます増え、コードベースはますます混乱していきます。このような侵襲的な修正により、システムソフトウェアもビジネスコードも、ますます保守が困難になっていきます。
ポイントを埋め込む方式には主に二つの問題があります。一つは「多すぎる」問題、もう一つは「少なすぎる」問題です。「多すぎる」とは、私たちが実際には必要のない情報を収集してしまうことを指します。一時的な欲張りから、不必要な収集と保存のオーバーヘッドを引き起こしています。多くの場合、サンプリングで分析できる問題に対して、習慣的に全ネットワーク全量の収集を行っており、このようなコストを積み重ねると明らかに非常に高価なものとなります。一方「少なすぎる」問題とは、私たちは最初から必要なすべての情報収集ポイントを計画することが非常に困難だということです。結局のところ、誰も予言者ではなく、将来調査が必要となる問題を予測することはできません。そのため、新しい問題に遭遇したとき、既存の収集ポイントが集めた情報はほとんど常に不十分です。これにより、ソフトウェアシステムの頻繁な修正、頻繁な本番環境へのデプロイが必要となり、開発エンジニアと運用エンジニアの作業量が大幅に増加し、同時に本番環境でより大きな障害が発生するリスクも増加します。
もう一つの乱暴なデバッグ方法も、私たちの一部の運用エンジニアがよく採用するものです。それは、マシンをオフラインにし、一連の一時的なファイアウォールルールを設定してユーザートラフィックや自身のモニタリングトラフィックをブロックし、その後本番マシン上でいろいろと試行錯誤を行うというものです。これは非常に煩雑で影響の大きなプロセスです。まず、マシンがサービスを継続できなくなり、オンラインシステム全体の総スループットが低下します。同時に、実際のトラフィックでしか再現できない問題は、もはや再現できなくなります。このような乱暴な方法がいかに頭を悩ませるものか想像できるでしょう。
実際には、SystemTap 動的トレーシング技術を活用することで、このような問題を非常にうまく解決することができ、「春風のごとく静かにすべての物を潤す」という効果があります。まず、ソフトウェアスタック(software stack)自体を修正する必要はありません。システムソフトウェアもビジネスソフトウェアも修正の必要はありません。私はよく、目的を絞ったツールを作成し、システムの重要な「ツボ」に慎重に配置された探針を設置します。これらの探針はそれぞれの情報を収集し、同時にデバッグツールはこれらの情報を集約してターミナルに出力します。この方法により、特定のマシンまたは複数のマシン上で、サンプリング方式を通じて、迅速に必要な重要情報を得ることができ、基本的な質問に素早く答え、その後のデバッグ作業の方向性を示すことができます。
前述したように、本番システムに手動でポイントを埋め込んでログを記録し、ログを収集してデータベースに入れるよりも、本番システム全体を直接クエリ可能な「データベース」として扱い、この「データベース」から安全かつ迅速に必要な情報を得る方が良いでしょう。しかも、痕跡を残さず、必要のない情報は一切収集しません。利この考えに基づき、私は多くのデバッグツールを作成しました。その大部分は既に GitHub 上でオープンソース化されており、その多くは Nginx、LuaJIT、オペレーティングシステムカーネルなどのシステムソフトウェア向けのもので、また一部は OpenResty のような Web フレームワークなど、より高レベルのものを対象としています。興味のある方は、GitHub 上の nginx-systemtap-toolkit、perl-systemtap-toolkit、stappxx などのコードリポジトリをご覧ください。
これらのツールを使用して、私は数え切れないほどの本番環境の問題を特定することに成功し、その中には偶然発見したものもありました。以下にいくつかの例を挙げてみましょう。
最初の例は、SystemTap ベースのフレームグラフツールを使用してオンラインの Nginx プロセスを分析した際、かなりの CPU 時間が非常に奇妙なコードパスに費やされていることを発見したというものです。これは実際には、ある同僚が以前に古い問題をデバッグする際に残した一時的なデバッグコードで、前述した「埋め込みコード」のようなものでした。その結果、それは本番環境に、そして企業のコードリポジトリに忘れ去られたまま残されていました。当時の問題は実際にはとっくに解決していたにもかかわらずです。このコストの高い「埋め込みコード」が除去されていなかったため、常に大きなパフォーマンスの損失が発生していましたが、誰も気付いていませんでした。当時、私はサンプリング方式を通じて、ツールに自動的にフレームグラフを描画させました。このグラフを一目見ただけで問題を発見し、対策を講じることができました。これは非常に非常に効果的な方法です。
二つ目の例は、非常に少数のリクエストにレイテンシーが長い問題、いわゆる「request latency outliers」が存在するというものです。これらのリクエストの数は非常に少ないものの、「秒単位」のようなレイテンシーに達する可能性がありました。当時、ある同僚が私の OpenResty にバグがあると推測しましたが、私は納得できず、すぐに SystemTap ツールを作成してオンラインでサンプリングを行い、1 秒を超える総レイテンシーを持つリクエストを分析しました。このツールは、これらの問題リクエスト内部の時間分布を直接測定し、リクエスト処理プロセス中の各典型的な I/O 操作のレイテンシーと純粋な CPU 計算のレイテンシーを含みます。その結果、すぐに OpenResty が Go で書かれた DNS サーバーにアクセスする際にレイテンシーが遅いことが特定されました。その後、私はツールにこれらのロングテール DNS クエリの具体的な内容を出力させ、すべて CNAME の展開に関係していることを発見しました。明らかに、これは OpenResty とは無関係で、さらなる調査と最適化の方向性も明確になりました。
三つ目の例は、あるデータセンターのマシンに、他のデータセンターと比べて明らかに高い割合のネットワークタイムアウトの問題が存在していましたが、それでも 1% の割合でした。最初は自然にネットワークプロトコルスタックの詳細を疑いましたが、後に一連の専用の SystemTap ツールを通じて、それらのタイムアウトリクエストの内部詳細を直接分析し、ハードディスクの設定の問題であることを特定しました。ネットワークからハードディスクまで、このようなデバッグは非常に興味深いものです。一次データにより、私たちは迅速に正しい軌道に乗ることができました。
もう一つの例は、私たちがかつてフレームグラフを通じて、Nginx プロセス内でファイルのオープンとクローズ操作が比較的多くの CPU 時間を占めていることを観察し、そこで自然に Nginx 自身のファイルハンドルキャッシュ設定を有効にしましたが、最適化の効果は明確ではありませんでした。そこで新しいフレームグラフを作成したところ、今度は Nginx のファイルハンドルキャッシュのメタデータが使用する「スピンロック」が多くの CPU 時間を占めていることが分かりました。これは、キャッシュを有効にしたものの、キャッシュサイズを大きく設定しすぎたため、メタデータのスピンロックのオーバーヘッドがキャッシュがもたらす利点を相殺してしまったためです。これらはすべてフレームグラフ上で一目瞭然でした。フレームグラフがなければ、単に試行錯誤を行うだけで、Nginx のファイルハンドルキャッシュが役に立たないという誤った結論を導き出し、キャッシュのパラメータを調整することを考えなかったかもしれません。
最後の例は、ある本番環境へのデプロイ後、最新のフレームグラフで正規表現のコンパイル操作が多くの CPU 時間を占めていることを観察しましたが、実際には本番環境で正規表現コンパイル結果のキャッシュを有効にしていました。明らかに、ビジネスシステムで使用される正規表現の数が、最初に設定したキャッシュサイズを超えていたため、自然に本番環境の正規表現キャッシュをより大きく調整することを思いつきました。その後、本番環境のフレームグラフでは正規表現コンパイル操作が見られなくなりました。
これらの例を通じて、実際に異なるデータセンター、異なるマシン、さらには同じマシンの異なる時間帯で、それぞれ独自の新しい問題が発生することがわかります。私たちに必要なのは、問題自体を直接分析し、サンプリングを行うことであり、無作為な推測や試行錯誤ではありません。強力なツールがあれば、トラブルシューティングは実際に非常に効率的な作業となります。
OpenResty Inc. を設立して以来、私たちは OpenResty XRay という全く新しい世代の動的トレーシングプラットフォームを開発しました。私たちは SystemTap のようなオープンソースソリューションを手動で使用することはもうありません。
フレームグラフ
前述の中で、私たちは何度もフレームグラフ(Flame Graph)について言及しましたが、フレームグラフとは何でしょうか?これは実際には非常に優れた可視化手法で、前述の Brendan Gregg 氏によって発明されました。
フレームグラフは、ソフトウェアシステムの X 線写真のようなもので、時間と空間という二つの次元の情報を自然に一枚の図に融合させ、非常に直観的な形で表示し、システムのパフォーマンスに関する多くの定量的な統計的規則性を反映することができます。
例えば、最も古典的なフレームグラフは、あるソフトウェアのすべてのコードパスの CPU 上での時間分布を統計化します。この分布図を通じて、どのコードパスが多くの CPU 時間を消費し、どれが重要でないかを直観的に見ることができます。さらに、私たちは異なるソフトウェアレベルでフレームグラフを生成することができます。例えば、システムソフトウェアの C/C++ 言語レベルで図を描き、さらに上位の - 例えば - 動的スクリプト言語のレベル、例えば Lua や Perl コードのレベルでフレームグラフを描くことができます。異なるレベルのフレームグラフは、しばしば異なる視点を提供し、それによって異なるレベルのコードのホットスポットを反映します。
私自身が OpenResty のようなオープンソースソフトウェアのコミュニティを維持しているため、私たちには独自のメーリングリストがあります。私はよく問題を報告するユーザーに、自分で描いたフレームグラフを積極的に提供するよう促します。そうすれば、私たちは快適に図を見ながら話し合うことができ、ユーザーのパフォーマンス問題の特定を迅速に支援し、繰り返しの試行錯誤を避け、ユーザーと一緒に無作為な推測を行うことを避け、それによって双方の大量の時間を節約し、皆が満足できます。
ここで注目に値するのは、私たちが全く知らない見慣れないプログラムに遭遇した場合でも、フレームグラフを見ることで、パフォーマンスの問題がどこにあるかを大まかに推測することができ、そのソースコードを一行も読んだことがなくても可能だということです。これは非常に素晴らしいことです。なぜなら、ほとんどのプログラムは実際には適切に作成されており、つまりソフトウェア構築時に抽象化レベルを使用しており、例えば関数を通じて行われています。これらの関数名は通常、意味的な情報を含んでおり、フレームグラフ上に直接表示されます。これらの関数名を通じて、対応する関数、さらには対応する特定のコードパスが大体何をしているのかを推測することができ、それによってこのプログラムが持つパフォーマンスの問題を推論することができます。そのため、あの古い言葉に戻りますが、プログラムコードの命名は非常に重要で、ソースコードの読解を助けるだけでなく、問題のデバッグにも役立ちます。逆に、フレームグラフは私たちに見慣れないソフトウェアシステムを学ぶための近道を提供します。結局のところ、重要なコードパスは、ほとんど常に時間を多く消費するものであり、そのため私たちが重点的に研究する価値があります。そうでなければ、このソフトウェアの構築方法に大きな問題が存在するはずです。
フレームグラフは実際に他の次元に拡張することができます。例えば、先ほど説明したフレームグラフは、プログラムが CPU 上で実行される時間のすべてのコードパス上での分布を見るもので、これは on-CPU 時間という次元です。同様に、あるプロセスが任意の CPU 上で実行されていない時間も非常に興味深く、私たちはこれを off-CPU 時間と呼んでいます。off-CPU 時間は通常、このプロセスが何らかの理由でスリープ状態にある時間です。例えば、システムレベルのロックを待っている場合や、非常にビジーなプロセススケジューラ(scheduler)によって強制的に CPU タイムスライスを奪われた場合などです。これらの状況はすべて、このプロセスが CPU 上で実行できない状態を引き起こしますが、依然として多くの壁時計時間を消費します。この次元のフレームグラフを通じて、非常に異なる別の図を得ることができます。この次元の情報を通じて、システムロックのオーバーヘッド(例えば sem_wait
のようなシステムコール)、一部のブロッキング I/O 操作(例えば open
、read
など)を分析し、さらにプロセスやスレッド間の CPU の競合の問題を分析することができます。off-CPU フレームグラフを通じて、すべてが一目瞭然となります。
off-CPU フレームグラフも私自身の大胆な試みの一つと言えます。カリフォルニア州とネバダ州の間にある Tahoe という湖の畔で、Brendan の off-CPU 時間に関するブログ記事を読んでいたことを覚えています。私は当然、おそらく off-CPU 時間を on-CPU 時間の代わりにフレームグラフという表示方法に適用できるのではないかと考えました。そこで帰社後、会社の本番システムでこのような試みを行い、SystemTap を使用して Nginx プロセスの off-CPU フレームグラフを描画しました。Twitter でこの成功した試みを公表した後、Brendan が特に私に連絡を取り、彼自身も以前この方法を試みたが、効果は良くなかったと言いました。私は、これは彼が当時マルチスレッドのプログラム、例えば MySQL に適用しようとしたためだと推測しています。マルチスレッドのプログラムは、スレッド同期の関係で、off-CPU グラフ上に多くのノイズが発生し、本当に興味深い部分を隠してしまう可能性があります。一方、私が off-CPU フレームグラフを適用したシーンは、Nginx のような単一スレッドのプログラムであり、そのため off-CPU フレームグラフはしばしば Nginx イベントループをブロックするシステムコール、あるいは sem_wait
のようなロック操作、または先取り型のプロセススケジューラの強制的な介入を即座に示すことができ、そのため一連のパフォーマンスの問題の分析に非常に役立ちます。このような off-CPU フレームグラフでは、唯一の「ノイズ」は実際には Nginx イベントループ自体の epoll_wait
のようなシステムコールで、識別して無視するのは容易です。
同様に、私たちはフレームグラフを他のシステム指標の次元に拡張することができます。例えばメモリリークのバイト数です。あるとき、私は「メモリリークフレームグラフ」を使用して、Nginx コア内の非常に微妙なリーク問題を迅速に特定しました。このリークは Nginx 自身のメモリプール内で発生したため、Valgrind や AddressSanitizer のような従来のツールでは捕捉できませんでした。また別の機会には、「メモリリークフレームグラフ」を使用して、あるヨーロッパの開発者が自分で書いた Nginx C モジュール内のリークを簡単に特定しました。そのリークは非常に微細で緩やかで、彼を長い間悩ませていましたが、私は彼を助けて特定する前に、彼のソースコードを読む必要すらありませんでした。よく考えてみると、私自身でも少し不思議に感じます。もちろん、私たちはフレームグラフをファイル I/O のレイテンシーやデータ量など、他のシステム指標にも拡張することができます。そのため、これは本当に素晴らしい可視化手法で、多くの全く異なる問題カテゴリーに使用することができます。
私たちの OpenResty XRay 製品は、様々な種類のフレームグラフの自動サンプリングをサポートしています。これには、C/C++ レベルのフレームグラフ、Lua レベルのフレームグラフ、off-CPU と on-CPU フレームグラフ、メモリ動的割り当てフレームグラフ、メモリオブジェクト参照関係フレームグラフ、ファイル IO フレームグラフなどが含まれます。
方法論
前述の中で、私たちはサンプリングに基づく可視化手法としてフレームグラフを紹介しましたが、これは実際にはかなり汎用的な手法です。どのようなシステムであれ、どのような言語で書かれていても、私たちは通常、何らかのパフォーマンス次元上のフレームグラフを得ることができ、その後簡単に分析を行うことができます。しかし、より多くの場合、私たちはより深層のより特殊な問題を分析し調査する必要があるかもしれません。この時、一連の専門化されたトレーシングツールを作成し、計画的かつ段階的に真の問題に迫る必要があります。
このプロセスにおいて、私たちが推奨する戦略は、小さなステップで前進し、連続的に質問を行うという方法です。つまり、非常に大きく複雑なデバッグツールを一度に作成し、必要となる可能性のあるすべての情報を一度に収集し、最終的な問題を一度に解決することは期待しません。逆に、私たちは最終的な問題の仮説を一連の小さな仮説に分解し、その後段階的に探求し、段階的に検証し、私たちの方向性を継続的に確認し修正し、私たちの軌道と仮説を継続的に調整して、最終的な問題に近づいていきます。このようにする利点は、各ステップ各段階のツールが十分にシンプルであり、これらのツール自体がエラーを引き起こす可能性が大幅に低減されることです。rBrendan も、多目的の複雑なツールを作成しようとすると、このような複雑なツール自体がバグを引き起こす可能性が大幅に高まることに気付いています。誤ったツールは誤った情報を提供し、誤った結論を導き出す可能性があります。これは非常に危険です。シンプルなツールのもう一つの大きな利点は、サンプリングプロセス中に本番システムに与える負荷も比較的小さいことです。結局のところ、導入されるプローブの数が少なく、各プローブの処理プログラムも非常に複雑な計算を行うことはないからです。ここでの各デバッグツールには独自の目的があり、単独で使用することができるため、これらのツールが将来再利用される機会も大幅に高まります。そのため全体として、このようなデバッグ戦略は非常に有益です。
特筆すべきは、ここで私たちは所謂「ビッグデータ」のデバッグ手法を拒否するということです。つまり、私たちは一度にできるだけ完全な情報とデータを収集しようとはしません。逆に、私たちは各段階各ステップで、その段階で本当に必要な情報のみを収集します。各ステップで、既に収集した情報に基づいて、元のプランと元の方向性を支持または修正し、その後次のステップのより詳細な分析ツールの作成を導きます。
また、非常に低頻度で発生するオンラインイベントに対しては、通常「待ち伏せ」戦略を採用しています。つまり、閾値やその他のフィルタリング条件を設定し、興味深いイベントが我々のプローブに捕捉されるのを待つアプローチです。例えば、低頻度の高遅延リクエストを追跡する場合、デバッグツールで特定の閾値を超える遅延を持つリクエストをまずフィルタリングし、それらのリクエストに対して必要な詳細情報をできる限り収集します。この戦略は、従来の全量統計データを収集する手法とは正反対のアプローチです。このように、特定の目的と具体的な戦略に基づいてサンプリング分析を行うことで、オーバーヘッドを最小限に抑え、不要なリソースの浪費を防ぐことができます。
当社の OpenResty XRay 製品は、ナレッジベースと推論エンジンを通じて、様々な動的トレーシングの方法論を自動的に適用し、システマティックなアプローチで問題の範囲を段階的に絞り込み、根本原因を特定して、ユーザーに報告し、最適化または修正方法を提案することが可能です。
知識は力なり
動的トレーシング技術は「知識は力なり」という古い格言を見事に体現していると考えています。
動的トレーシングツールを通じて、システムに関する我々の理解と知識を、実際の問題を解決する実用的なツールへと変換することができます。コンピュータサイエンスの教育で教科書を通じて学んだ抽象的な概念(仮想ファイルシステム、仮想メモリシステム、プロセススケジューラーなど)が、今や非常に具体的で生き生きとしたものとなります。初めて実際の本番システムにおいて、オペレーティングシステムのカーネルやシステムソフトウェアのソースコードを改変することなく、それらの具体的な動作や統計的な特性を直接観察することができます。これらの非侵襲的なリアルタイム観測能力は、すべて動的トレーシング技術のおかげです。
この技術は、金庸の小説に登場する楊過が使用する玄鉄重剣のようなものです。武術を全く知らない人には扱えませんが、基本的な武術を身につけていれば、使えば使うほど上達し、最終的には木剣でも天下を制することができるようになります。システムに関する基本的な知識があれば、この「剣」を振るい始めることができ、基本的ではあるものの、これまで想像もできなかった問題を解決することができます。システムに関する知識が増えれば増えるほど、この「剣」をより巧みに使いこなすことができます。さらに興味深いことに、新しい知識を得るたびに、新たな問題を解決できるようになります。逆に、これらのデバッグツールを使用して多くの問題を解決し、本番システムのミクロレベルやマクロレベルの興味深い統計的特性を測定し学習できることで、さらなるシステム知識を学ぶ強力な動機となります。このように、向上心のあるエンジニアにとって、自然と「レベルアップツール」となるのです。
以前、私は Weibo で「エンジニアの継続的な深い学習を促進するツールこそが、将来性のある優れたツールである」と述べました。これは実際に、相互に促進し合う良好なプロセスなのです。
オープンソースとデバッグシンボル
前述したように、動的トレーシング技術は実行中のソフトウェアシステムをクエリ可能なリアルタイムの読み取り専用データベースに変換できますが、これには通常、完全なデバッグシンボルが必要という条件があります。デバッグシンボルとは何でしょうか?デバッグシンボルは通常、ソフトウェアのコンパイル時にコンパイラによって生成される、デバッグ用のメタ情報です。これらの情報により、コンパイル後のバイナリプログラム内の多くの詳細情報(関数や変数のアドレス、データ構造のメモリレイアウトなど)を、ソースコード内の抽象的なエンティティの名前(関数名、変数名、型名など)にマッピングすることができます。Linux の世界で一般的なデバッグシンボルのフォーマットは DWARF(英単語の「ドワーフ」と同じ)と呼ばれています。これらのデバッグシンボルがあることで、私たちは冷たく暗いバイナリの世界に地図を持ち、灯台を持つことができ、この低レベルの世界の細かな側面の意味を解釈し復元し、高レベルの抽象概念と関係性を再構築することが可能になります。
通常、デバッグシンボルを生成できるのはオープンソースソフトウェアだけです。大多数のクローズドソースソフトウェアは、セキュリティ上の理由から、デバッグシンボルを提供しません。リバースエンジニアリングやクラッキングの困難さを増します。その一例が Intel 社の IPP ライブラリです。IPP は Intel のチップ向けに多くの一般的なアルゴリズムの最適化実装を提供しています。私たちも本番システムで IPP ベースの gzip 圧縮ライブラリの使用を試みましたが、残念ながら問題に遭遇しました—— IPP が本番環境で時々クラッシュしたのです。明らかに、デバッグシンボルのないクローズドソースソフトウェアのデバッグは非常に困難です。Intel のエンジニアとリモートで何度もコミュニケーションを取りましたが、問題の特定と解決ができず、最終的に断念せざるを得ませんでした。ソースコードやデバッグシンボルがあれば、このデバッグプロセスはもっと簡単になっていたかもしれません。
オープンソースと動的トレーシング技術のこのような相性の良さについて、Brendan Gregg も以前のプレゼンテーションで言及しています。特に、ソフトウェアスタック全体がオープンソースである場合、動的トレーシングの力を最大限に発揮することができます。ソフトウェアスタックには通常、オペレーティングシステムカーネル、各種システムソフトウェア、そしてより上位の高級言語プログラムが含まれます。スタック全体がオープンソースの場合、各ソフトウェア層から必要な情報を容易に取得し、それを知識に、そして行動計画に変換することができます。
デバッグシンボルに依存する複雑な動的トレーシングにおいて、一部の C コンパイラが生成するデバッグシンボルには問題があります。これらの誤ったデバッグ情報は、動的トレーシングの効果を大きく損なう、あるいは分析を直接妨げる可能性があります。例えば、広く使用されている GCC コンパイラは、バージョン 4.5 以前では生成されるデバッグシンボルの品質が非常に低く、4.5 以降では大きな進歩を遂げ、特にコンパイラの最適化を有効にした場合に顕著な改善が見られます。
当社の OpenResty XRay 動的トレーシングプラットフォームは、インターネット上の一般的なオープンソースソフトウェアのデバッグシンボルパッケージとバイナリパッケージをリアルタイムで取得し、分析とインデックス作成を行っています。現在、このデータベースには数十テラバイトのデータがインデックス化されています。
Linux カーネルのサポート
前述のように、動的トレース技術は一般的にオペレーティングシステムのカーネルに基づいています。私たちが日常的に広く使用している Linux オペレーティングシステムカーネルにおいて、動的トレースのサポートは長く困難な道のりでした。その主な理由の一つは、おそらく Linux の創始者である Linus がこの技術を不要だと考えていたためです。
当初、Red Hat 会社のエンジニアは Linux カーネル向けに utrace というパッチを用意し、ユーザー空間での動的トレース技術をサポートしました。これは SystemTap のような枠組みが最初に依存していた基盤でした。長年にわたり、RHEL、CentOS、Fedora などの Red Hat 系 Linux ディストリビューションには、このutrace パッチがデフォルトで含まれていました。utrace が主流だった時代には、SystemTap は Red Hat 系のオペレーティングシステムでのみ意味を持っていました。この utrace パッチは最終的に Linux カーネルのメインラインバージョンには統合されず、別の妥協案に取って代わられました。
Linux のメインラインバージョンには早くから kprobes というメカニズムが存在し、指定したカーネル関数の入口や出口などの位置に動的にプローブを配置し、独自のプローブハンドラを定義することができました。
ユーザー空間での動的トレースサポートは遅れて実現し、数多くの議論と改訂を経ました。公式 Linux カーネルのバージョン 3.5 から、inode ベースの uprobes カーネルメカニズムが導入され、ユーザー空間関数の入口などの位置に安全に動的プローブを設定し、独自のプローブハンドラを実行できるようになりました。その後、カーネル 3.10 から uretprobes というメカニズム4が統合され、ユーザー空間関数のリターンアドレスにも動的プローブを設定できるようになりました。uprobes と uretprobes を組み合わせることで、utrace の主要な機能を置き換えることが可能となり、utrace パッチはその歴史的使命を終えました。現在、SystemTap は新しいカーネル上で自動的に uprobes と uretprobes メカニズム4を使用し、utrace パッチに依存しなくなっています。
近年、Linux のメインライン開発者たちは、ファイアウォールの netfilter で使用されていた動的コンパイラである BPF を拡張し、eBPF と呼ばれる、より汎用的なカーネル仮想マシンを作り出しました。このメカニズムにより、Linux 上で DTrace のような常駐カーネルの動的トレース仮想マシンを構築することが可能になりました。実際に、BPF コンパイラ(BCC)のようなツールが登場し、LLVM ツールチェーンを使用して C コードを eBPF 仮想マシンが受け入れるバイトコードにコンパイルすることができるようになりました。全体として、Linux の動的トレースサポートは着実に改善されています。特にカーネルバージョン 3.15 以降、動的トレース関連のカーネルメカニズムは比較的堅牢で安定したものとなりました。しかし残念ながら、eBPF は設計上深刻な制限があり、eBPF ベースの動的トレースツールは比較的単純なレベルに留まっています。言い換えれば、まだ「石器時代」の段階にあると言えます。SystemTap も最近 eBPF ランタイムをサポートし始めましたが、このランタイムがサポートする stap 言語機能は非常に限定的で、SystemTap のリーダーである Frank もこの点について懸念を表明しています。
ハードウェアトレース
動的トレース技術がソフトウェアシステムの分析において非常に重要な役割を果たすことを見てきましたが、同様の方法や考え方をハードウェアのトレースにも適用できないかという疑問が自然と生まれます。
オペレーティングシステムはハードウェアと直接やり取りを行うため、オペレーティングシステムの特定のドライバプログラムやその他の部分をトレースすることで、接続されているハードウェアデバイスの動作や問題を間接的に分析することができます。同また、Intel CPU のような現代のハードウェアには、通常、性能統計用の特殊なレジスタ(Hardware Performance Counter)が組み込まれており、ソフトウェアでこれらの特殊レジスタの情報を読み取ることで、ハードウェアに関する多くの興味深い情報を直接得ることができます。Linux の世界の perf ツールは、当初この目的のために作られました。VMWare のような仮想マシンソフトウェアでさえ、このような特殊なハードウェアレジスタをエミュレートしています。この特殊レジスタを基に、Mozilla rr のような興味深いデバッグツールも生まれ、プロセス実行の効率的な記録と再生が可能になりました。
ハードウェア内部に直接動的プローブを設置して動的トレースを実施することは、現時点ではまだ SF の領域にあるかもしれません。興味のある方々からの新しいアイデアや情報の提供を歓迎いたします。
終了プロセスの解析
これまでは実行中のプロセス、つまり動作中のプログラムの分析について見てきました。では、終了したプロセスについてはどうでしょうか?終了したプロセスの最も一般的な形態は、プロセスが異常終了して core dump ファイルを生成した場合です。このような終了したプロセスの「遺骸」に対しても、深い分析を行うことができ、その死因を特定できる可能性があります。この意味で、プログラマーは「法医学者」のような役割を果たしています。
終了プロセスの遺骸を分析する最も有名なツールは、GNU Debugger(GDB)です。LLVM の世界には LLDB という類似のツールもあります。GDB のネイティブコマンド言語には明らかに制限があり、手動でコマンドを一つずつ実行して core dump を分析しても、得られる情報は非常に限られています。実際、多くのエンジニアは core dump の分析において、bt full
コマンドで現在の C コールスタックトレースを確認したり、info reg
コマンドで各 CPU レジスタの現在の値を確認したり、クラッシュ位置のマシンコードシーケンスを確認したりする程度です。しかし、より多くの情報はヒープ(heap)に割り当てられた様々な複雑なバイナリデータ構造の中に隠されています。ヒープ内の複雑なデータ構造をスキャンして分析するには、明らかに自動化が必要で、複雑な core dump の分析ツールをプログラムで作成できる方法が必要です。
この要求に応えて、GDB は比較的新しいバージョン(バージョン 7.0 からだと記憶しています)から、Python スクリプトのサポートを組み込みました。現在では Python を使用してより複雑な GDB コマンドを実装し、core dump などの深い分析を行うことができます。実際、私も Python を使用して多くの GDB ベースの高度なデバッグツールを作成しており、その多くは生きているプロセスを分析する SystemTap ツールと一対一で対応しています。動的トレースと同様に、デバッグシンボルを利用することで、「死の世界」の中でも光明を見出すことができます。
しかし、この方法にはツールの開発と移植が大きな負担になるという問題があります。Python のようなスクリプト言語を使用して C スタイルのデータ構造を走査することは、楽しい作業ではありません。このような奇妙な Python コードを多く書くと本当に苦痛になります。また、同じツールを SystemTap のスクリプト言語で一度書き、さらに GDB の Python コードでも書く必要があります。これは明らかに大きな負担で、両方の実装を慎重に開発してテストする必要があります。同様のことを行っているにもかかわらず、実装コードと対応する API は完全に異なります(ここで注目すべきは、LLVM の世界の LLDB ツールも同様の Python プログラミングサポートを提供していますが、その Python API は GDB のものと互換性がないということです)。
GDB を使用して生きているプログラムを分析することもできますが、SystemTap と比較すると、最も顕著な違いはパフォーマンスの問題です。私は以前、複雑なツールの SystemTap バージョンと GDB Python バージョンを比較したことがあります。それらのパフォーマンスには一桁の差がありました。GDB は明らかにこのようなオンライン分析用に設計されておらず、むしろインタラクティブな使用方法を重視しています。バッチモードで実行することもできますが、内部の実装方式により、パフォーマンスに非常に深刻な制限があります。最も困ったのは、GDB が通常のエラー処理に longjmp を過度に使用していることで、これにより深刻なパフォーマンスの低下が生じており、SystemTap で生成した GDB フレームグラフでは非常に明確に表れています。幸いなことに、死んだプロセスの分析は常にオフラインで行うことができ、オンラインでこのような作業を行う必要はないため、時間的な考慮はそれほど重要ではありません。しかし残念なことに、私たちの複雑な GDB Python ツールの中には、実行に数分かかるものもあり、オフラインで行う場合でも挫折感を感じさせます。
私は SystemTap を使用して GDB + Python のパフォーマンス分析を行い、フレームグラフを基に GDB 内部の最大の2つの実行ホットスポットを特定しました。その後、GDB 公式に2つの C パッチを提出しました。1つは Python 文字列操作に関するもので、もう1つは GDB のエラー処理方式に関するものです。これらにより、最も複雑な GDB Python ツールの全体的な実行速度が 100% 向上しました。GDB 公式は現在、これらのパッチの1つを統合しています。動的トレース技術を使用して従来のデバッグツールを分析し改善することも、非常に興味深い取り組みです。
私は以前の仕事で作成した多くの GDB Python デバッグツールを GitHub でオープンソース化しています。興味のある方はご覧ください。通常、nginx-gdb-utils のような GitHub リポジトリに格納されており、主に Nginx と LuaJIT を対象としています。これらのツールを使用して、LuaJIT の作者である Mike Pall が LuaJIT 内部の10個以上のバグを特定するのを支援しました。これらのバグの多くは長年隠れていたもので、ほとんどが Just-in-Time (JIT) コンパイラ内の非常に微妙な問題でした。
死んだプロセスには時間とともに変化する可能性がないため、この core dump に対する分析を「静的トレース」と呼ぶことにしましょう。
OpenResty XRay 製品は、Y 言語 コンパイラを通じて、Y 言語で書かれた様々な分析ツールが GDB のようなプラットフォームもサポートできるようにし、core dump ファイルの深い分析を自動化することができます。
従来のデバッグ技術
GDB について語る際、動的トレースと従来のデバッグ手法との違いや関連性について触れる必要があります。経験豊富なエンジニアであれば、動的トレースの「前身」は GDB でブレークポイントを設定し、そのブレークポイントで一連の検査を行う手法であることにお気づきでしょう。ただし、動的トレースは非インタラクティブのバッチ処理を重視し、可能な限り低いパフォーマンス損失を目指すという点が異なります。一方、GDB のようなツールは本質的にインタラクティブ操作のために設計されており、本番環境での安全性やパフォーマンスへの影響をあまり考慮していません。通常、そのパフォーマンス損失は非常に大きくなります。また、GDB が基盤としている ptrace という古いシステムコールには、多くの課題や問題が存在します。例えば、ptrace はデバッグ対象プロセスの親プロセスを変更する必要があり、複数のデバッガーが同時に同一プロセスを分析することができません。そのため、ある意味で GDB の使用は「簡易的な動的トレース」と見なすことができます。
多くのプログラミング初学者は GDB での「ステップ実行」を好みますが、実際の産業界での開発において、この方法は非常に非効率的であることが多いです。これは、ステップ実行時にプログラムの実行順序が変化し、タイミングに関連する問題が再現できなくなることがあるためです。また、複雑なソフトウェアシステムでは、ステップ実行によって複雑なコードパスの中で迷子になりやすく、いわゆる「木を見て森を見ず」の状態に陥りやすいのです。
そのため、日常の開発プロセスにおけるデバッグでは、最も単純で一見すると最も原始的な方法、つまり重要なコードパスにログ出力文を挿入することをお勧めします。これにより、ログなどの出力を通じて完全なコンテキストを得ることができ、プログラムの実行結果を効果的に分析することが可能になります。この手法はテスト駆動開発と組み合わせると特に効果的です。明らかに、このようなログ追加やブレークポイントの方法は本番環境でのデバッグには現実的ではありません。これについては既に十分に議論しました。一方、従来のパフォーマンス分析ツール(Perl の DProf、C 言語の gprof、その他の言語や環境のプロファイラー)は、特別なオプションでプログラムを再コンパイルするか、特別な方法でプログラムを再実行する必要があることが多いです。このような特別な処理や設定が必要なパフォーマンス分析ツールは、オンラインでのリアルタイム分析には明らかに適していません。
混沌としたデバッグの世界
現在のデバッグの世界は非常に混沌としています。前述した DTrace、SystemTap、eBPF/BCC、GDB、LLDB の他にも、ここで触れていない多くのツールがあり、それらはインターネットで調べることができます。おそらく、これは私たちが直面している現実世界の混沌を反映しているのかもしれません。
長年、私は統一的なデバッグ言語の設計と実装について考えてきました。そして最終的に OpenResty Inc. で Y 言語 という形で実現することができました。このコンパイラーは、様々なデバッグフレームワークや技術が受け入れる入力コードを自動生成することができます。例えば、DTrace 用の D 言語コード、SystemTap 用の stap スクリプト、GDB 用の Python スクリプト、LLDB 用の互換性のない API を持つ Python スクリプト、eBPF 用のバイトコード、さらには BCC が受け入れる C 言語と Python コードの混合物などを生成できます。
デバッグツールを複数の異なるデバッグフレームワークに移植する必要がある場合、前述のように手動での移植作業は非常に労力を要します。しかし、このような統一的な Y 言語があれば、そのコンパイラーが同じ Y コードを各種デバッグプラットフォーム向けの入力コードに自動変換し、それぞれのプラットフォームに最適化することができます。これにより、各デバッグツールを Y 言語で一度だけ記述すれば済むようになります。これは大きな負担軽減となります。また、デバッガー自身も、すべての具体的なデバッグ技術の複雑な詳細を学び、各デバッグ技術の「落とし穴」を自分で経験する必要がなくなります。
Y 言語は現在、OpenResty XRay 製品の一部として、ユーザーの皆様にご提供しています。
なぜ Y と呼ぶのかと疑問に思われる方もいらっしゃるかもしれません。これは私の名前が「亦春」であり、「亦」の中国語のピンインの最初の文字が Y であることに由来します。さらに重要な理由として、これは「なぜ(why)」で始まる質問に答えるための言語であり、「why」と「Y」が音が似ているということもあります。
OpenResty XRay
OpenResty XRay は OpenResty Inc. が提供する商用製品です。OpenResty XRay は、対象プログラムの協力を必要とせずに、オンラインまたはオフラインの様々なソフトウェアシステムの動作の詳細を深く洞察し、パフォーマンスの問題、信頼性の問題、セキュリティの問題を効果的に分析・特定することができます。技術的には、SystemTap よりも強力なトレース機能を持ち、パフォーマンスと使いやすさの面でも SystemTap を上回ります。また、core dump ファイルのような「プログラムの遺骸」の自動分析もサポートしています。
ご興味のある方は、お問い合わせいただき、無料トライアルをお申し込みください。
さらに詳しく
動的トレースの技術、ツール、方法論、事例についてさらに詳しく知りたい方は、OpenResty Inc. のブログサイトをご覧ください。
また、OpenResty XRay 商用製品のトライアルもぜひご検討ください。
動的トレースの先駆者である Brendan Gregg 氏のブログにも多くの優れたコンテンツがあります。
謝辞
本文は多くの友人や家族の助けを得て完成しました。まず、1時間に及ぶ音声共有の書き起こし作業を担当してくれた師蕊に感謝します。また、丁寧な校正とフィードバックをしてくれた多くの友人たちにも感謝します。そして、文章の推敲に辛抱強く協力してくれた父と妻にも感謝の意を表します。
著者について
章亦春(Zhang Yichun)は、オープンソースの OpenResty® プロジェクトの創始者であり、OpenResty Inc. の CEO および創業者です。
章亦春(GitHub ID: agentzh)は中国江蘇省生まれで、現在は米国ベイエリアに在住しております。彼は中国における初期のオープンソース技術と文化の提唱者およびリーダーの一人であり、Cloudflare、Yahoo!、Alibaba など、国際的に有名なハイテク企業に勤務した経験があります。「エッジコンピューティング」、「動的トレーシング」、「機械プログラミング」 の先駆者であり、22 年以上のプログラミング経験と 16 年以上のオープンソース経験を持っております。世界中で 4000 万以上のドメイン名を持つユーザーを抱えるオープンソースプロジェクトのリーダーとして、彼は OpenResty® オープンソースプロジェクトをベースに、米国シリコンバレーの中心部にハイテク企業 OpenResty Inc. を設立いたしました。同社の主力製品である OpenResty XRay動的トレーシング技術を利用した非侵襲的な障害分析および排除ツール)と OpenResty XRay(マイクロサービスおよび分散トラフィックに最適化された多機能
翻訳
英語版の原文と日本語訳版(本文)をご用意しております。読者の皆様による他の言語への翻訳版も歓迎いたします。全文翻訳で省略がなければ、採用を検討させていただきます。心より感謝申し上げます!
-
SystemTap と OpenResty XRay にはこれらの制限や欠点はありません。 ↩︎
-
OpenResty XRay の動的トレースプラットフォームには、SystemTap が持つこれらの欠点はありません。 ↩︎
-
stap++ プロジェクトはメンテナンスを終了し、次世代の動的トレース技術を採用した OpenResty XRay プラットフォームとツールセットに置き換えられました。 ↩︎
-
uretprobes は実装上大きな問題がありました。対象プログラムのシステムランタイムスタックを直接修正するため、stack unwinding などの重要な機能を破壊してしまうためです。OpenResty XRay では、uretprobes と同様の効果を持つ独自の実装を行い、これらの欠点を解消しています。 ↩︎