1. 導入: アプリケーションの「なぜ?」を解き明かす
日々運用しているアプリケーションが、突然パフォーマンスが低下したり、理解不能なエラーで停止したりすることはありませんか?ログを見ても原因が掴めず、「まるでブラックボックスだ!」と感じることも少なくないでしょう。そんな時、アプリケーションとOSカーネルとの対話を直接覗き見ることができたら、どれほどデバッグが楽になるでしょうか。
今回ご紹介するシステムコールトレースは、まさにその「覗き見」を実現する強力な技術です。アプリケーションがOSに対してどのようなリクエスト(ファイルの読み書き、ネットワーク通信、プロセスの生成など)を発行しているかを監視することで、ソースコードを修正することなく、以下のようなインフラレベルの根本原因を特定できます。
- 謎のパフォーマンス低下: 予想外の大量I/Oや不必要な同期処理
- ファイルのパーミッションエラー: どのファイルに、なぜアクセスできなかったのか
- ネットワーク接続拒否: どのポートに、なぜ接続できなかったのか
システムコールトレースは、アプリケーションの振る舞いをOSレベルから客観的に評価し、問題解決への最短ルートを示してくれる、DevOps・インフラエンジニアの強力な武器となるでしょう。
2. 基礎知識: OSとの「約束事」
まずは、システムコールトレースを理解するために必要な基礎知識から見ていきましょう。
システムコールとは?
アプリケーションは、ファイルへのアクセス、ネットワーク通信、メモリの確保、プロセスの管理といったOSの機能を利用する際に、直接カーネルにアクセスすることはできません。代わりに、特定の「窓口」を通じてOSに処理を依頼します。この「窓口」がシステムコールです。
アプリケーションがシステムコールを発行すると、CPUは「ユーザーモード」から「カーネルモード」に切り替わり、OSカーネルがその要求を処理します。システムコールは、OSとアプリケーション間の明確な「約束事」であり、OSの種類によって提供されるシステムコールの種類や名前は異なります。
strace (System Call Trace) とは?
straceは、Linux環境で最も広く使われているシステムコールトレースツールの一つです。特定のプロセスやコマンドが呼び出すシステムコール、その引数、そして戻り値をリアルタイムで表示します。非常にシンプルで使いやすい反面、トレース対象のプロセスに多少のオーバーヘッドをかけるため、本番環境での長時間の利用には注意が必要です。
eBPF (extended Berkeley Packet Filter) とは?
eBPFは、Linuxカーネル上で非常に効率的かつ安全にカスタムプログラムを実行できる強力なフレームワークです。元々はネットワークパケットフィルタリングのために開発されましたが、現在ではシステムコールのトレース、カーネル関数の監視、ユーザー空間のプロファイリングなど、多岐にわたる用途で利用されています。
straceと比較して、eBPFはカーネル内部で動作するため、非常に低オーバーヘッドで詳細な情報を収集できるのが最大の特長です。これにより、本番環境でのプロファイリングや監視にも適用可能な場合が多く、近年DevOps・SRE界隈で大きな注目を集めています。dtrace(Solaris発祥の動的トレースツール)の思想を受け継ぎつつ、Linuxカーネルで進化を遂げた技術と言えるでしょう。
3. 実装/解決策: ツールを使いこなす
それでは、実際にstraceとeBPF(既存ツール)を使ったシステムコールトレースの方法を見ていきましょう。
straceを使ったトレース
straceは、コマンドの前に付けるだけで簡単に使えます。
特定のコマンドのシステムコールをトレース
strace ls -l
既存のプロセスID (PID) にアタッチしてトレース
strace -p <PID>
特定のシステムコールのみをトレース (例: open, read, write)
strace -e open,read,write ls -l
システムコールの実行時間統計を表示
strace -c ls -l
出力をファイルに保存
strace -o output.txt ls -l
straceの出力は非常に詳細ですが、適切なフィルタリングをしないと情報が多すぎて読み解くのが困難になります。-eオプションで対象のシステムコールを絞り込んだり、-Pオプションで特定のファイルパスへのアクセスのみを監視したりするなど、目的に応じたオプションを使いこなすことが重要です。
eBPFを使ったトレース (既存ツール BCC/bpftrace)
eBPFプログラムを自作するのは学習コストが高いですが、幸いなことに、BCC (BPF Compiler Collection) や bpftrace といったツールキットが豊富なスクリプトを提供しており、これらを使えば手軽にeBPFの恩恵を受けられます。
BCCツールキットの例:
BCCには、ファイルオープンを監視するopensnoop、プロセス実行を監視するexecsnoopなど、様々なスクリプトが含まれています。多くの場合、sudo権限が必要です。
BCCツールキットのインストール (例: Ubuntu)
sudo apt update
sudo apt install bpfcc-tools
ファイルオープンをリアルタイムで監視
sudo opensnoop
プロセス実行をリアルタイムで監視
sudo execsnoop
bpftraceの例:
bpftraceは、ワンライナーで手軽にeBPFプログラムを書ける高レベルなトレーシング言語です。
bpftraceのインストール (例: Ubuntu)
sudo apt update
sudo apt install bpftrace
すべての openat システムコールを監視し、プロセス名とファイル名を表示
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'
これらのツールは、straceよりもはるかに低オーバーヘッドで、かつ詳細な情報(スタックトレースなど)を収集できるため、本番環境でのトラブルシューティングやプロファイリングに非常に有用です。
4. サンプルプログラム: ファイル操作をトレースする
ここでは、簡単なC言語プログラムを作成し、それをstraceでトレースしてみましょう。
C言語プログラム (file_ops_app.c)
このプログラムは、ファイルを作成し、文字列を書き込み、読み込み、そして削除します。
include <stdio.h> // printf, perror
include <stdlib.h> // exit
include <unistd.h> // close, unlink, read, write
include <fcntl.h> // open, O_CREAT, O_WRONLY, O_TRUNC, O_RDONLY
include <string.h> // strlen
int main() {
const char filename = "trace_test_file.txt"; // 操作対象のファイル名
const char message = "Hello, strace and eBPF world!\n"; // ファイルに書き込むメッセージ
char buffer[128]; // ファイルから読み込むためのバッファ
int fd; // ファイルディスクリプタ
// --- ファイル作成と書き込み ---
printf("--- ファイル作成と書き込みの開始 ---\n");
// ファイルを書き込みモードで開く (存在しなければ作成、あれば内容をtruncate)
// 0644はパーミッション (rw-r--r--)
fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (fd == -1) {
perror("ファイルオープン失敗 (書き込み)");
return 1;
}
printf("ファイル '%s' をオープンしました。ファイルディスクリプタ: %d\n", filename, fd);
// ファイルにメッセージを書き込む
ssize_t bytes_written = write(fd, message, strlen(message));
if (bytes_written == -1) {
perror("ファイル書き込み失敗");
close(fd);
return 1;
}
printf("%zd バイトをファイルに書き込みました。\n", bytes_written);
// ファイルを閉じる
close(fd);
printf("ファイルを閉じました。\n\n");
// --- ファイル読み込み ---
printf("--- ファイル読み込みの開始 ---\n");
// ファイルを読み込みモードで開く
fd = open(filename, O_RDONLY);
if (fd == -1) {
perror("ファイルオープン失敗 (読み込み)");
return 1;
}
printf("ファイル '%s' を読み込みモードでオープンしました。ファイルディスクリプタ: %d\n", filename, fd);
// ファイルから読み込む
ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);
if (bytes_read == -1) {
perror("ファイル読み込み失敗");
close(fd);
return 1;
}
buffer[bytes_read] = '\0'; // ヌル終端して文字列として扱えるようにする
printf("%zd バイトをファイルから読み込みました: '%s'\n", bytes_read, buffer);
// ファイルを閉じる
close(fd);
printf("ファイルを閉じました。\n\n");
// --- ファイル削除 ---
printf("--- ファイル削除の開始 ---\n");
// ファイルを削除する
if (unlink(filename) == -1) {
perror("ファイル削除失敗");
return 1;
}
printf("ファイル '%s' を削除しました。\n", filename);
return 0;
}
コンパイルとstraceでの実行
Cプログラムをコンパイル
gcc file_ops_app.c -o file_ops_app
straceで実行し、ファイル関連のシステムコールのみをトレース
strace -e open,openat,close,read,write,unlink ./file_ops_app
上記のstraceコマンドを実行すると、open, write, read, close, unlinkといったシステムコールがどのような引数で呼び出され、どのような戻り値(成功時にはファイルディスクリプタや書き込みバイト数、失敗時には-1とエラーコード)を返しているかが詳細に表示されるはずです。これにより、プログラムがOSとどのようにやり取りしているか、具体的な動作を「見える化」できます。
eBPFツールでの監視 (opensnoopの利用)
別のターミナルを開き、opensnoopを実行した状態で上記の./file_ops_appを実行してみてください。
opensnoopでファイルオープンを監視 (管理者権限が必要)
sudo opensnoop
opensnoopの出力に、file_ops_appがtrace_test_file.txtを開いている様子が表示されるはずです。straceよりも簡潔ながら、リアルタイムにシステム全体のファイルI/Oを監視できる強力なツールであることがわかるでしょう。