リクエストが遅い場合、その原因は CPU、ランキュー、ロック、またはデバイスにある可能性がある。トレースでは、まず測定対象とその開始・終了の境界 を特定し、仮説を検証するのに十分なイベントを収集する必要がある。
参照目標 該当セクション
ツール、イベント、サンプリングの選択 第 1~3 節
レイテンシの関連付け、欠落、オーバーヘッド 第 4~5 節
ダンプ、デバッグ、診断フロー 第 6~7 節
1. 問題と測定対象の特定
問題 必要な証拠 一般的なツール
CPU 時間はどこに使われているか? 代表的なスタックサンプリング perf
どの種類の操作が遅いのか? ペアになった開始/終了時刻と結果 tracepoint、BPF 集計
実行可能状態のタスクがなぜ実行されないのか? wakeup から実際の実行までの関連付け スケジューリングイベント、runqlat 系ツール
関数内部では何が呼び出されているか? 範囲限定の関数エントリ/リターン function_graph、fentry/fexit
カーネルクラッシュ時の状態はどうか? ダンプ、ログ、シンボルと設定 kdump、crash
特定の状態遷移が正しいか? 制御された環境でのブレークポイントとステップ実行 KGDB/KDB
同じ「リクエストの所要時間」でも、アプリケーションのキュー待ち、システムコール、ブロック層のキュー待ち、デバイス処理が含まれることがある。イベントのネストや並列関係が明確でない限り、異なる層のレイテンシを単純に足し合わせることはできない。
2. ftrace と tracefs
最新の tracefs は通常 /sys/kernel/tracing にマウントされるが、旧パス /sys/kernel/debug/tracing が残っている場合もある。available_tracers は現在利用可能なトレーサーをリストし、available_filter_functions は追跡可能な関数をリストする。設定によって見える内容は変化する。
cat /sys/kernel/tracing/available_tracers
cat /sys/kernel/tracing/events/sched/sched_switch/format
これらの読み取り専用コマンドは、まず能力とフィールドを確認するために使用する。読み取りに失敗したからといって、関数が存在しないとは即座に判断してはならない。function や function_graph を有効にするとトレース状態が変更されるため、実際の収集には独立した trace instance を使用し、フィルタリング範囲とバッファを設定してから収集を開始し、終了後に無効化してクリーンアップするのが望ましい。
function は選択された関数のエントリを記録する。フィルタなしで全量を有効にすると、大量のイベントが発生する可能性がある。
function_graph は関数のエントリとリターンを関連付け、呼び出し階層と持続時間を表示する。スケジューリングによって実行が中断された時間をカウントするかどうかは、トレーサーのオプションに依存する。
trace はフォーマットされたバッファビューを提供する。trace_pipe は消費型の読み取りであり、複数のリーダーがイベントを競合するため、互いに影響を受けないブロードキャストログとして扱うことはできない。
CPU バッファは記録を上書きしたり、欠落させたりする可能性がある。CPU 間で比較を行う場合、trace_clock の理解も必要である。trace instance は複数の収集設定を分離できるが、動的プローブ定義などのリソースは共有されるため、相互に影響を与える可能性がある。インターフェースとオプションについては ftrace ドキュメント を参照。
動的 ftrace は kprobe event とは異なる
動的 ftrace は、トレース可能な関数の挿入ポイントのオン/オフによって関数トレースを有効/無効にする。kprobe_events や dynamic_events への登録は動的プローブイベントであり、別のメカニズムに属する。state+0x12(%di) のような固定オフセット式を読み取るには、アーキテクチャ、関数シグネチャ、構造体レイアウトの一致を同時に確認する必要があり、一般的な TCP 状態の読み取り例として安易に使用してはならない。
適切な tracepoint があるかどうかを優先して確認し、動的関数プローブが確実に必要な場合に限り、対象のシンボル、BTF/DWARF、実際のソースコードと組み合わせて引数を解釈する。kprobe events を参照。
3. perf:カウントとサンプリングは異なる問いに答える
perf stat はカウントを集計し、perf record はイベント発生に応じてサンプルを記録し、perf report はサンプルを集計する。スリープ中のプロセスの cycles を記録しても、それはマシン全体の 10 秒間の CPU ハットスポット(ホットスポット)を代表するものではない。
perf stat - e cycles,instructions,cache-misses -- ./workload
perf record - e cycles - g -- call-graph dwarf -- ./workload
perf report
perf record - a - e cycles - g -- sleep 10
./workload は測定対象のプログラムを表し、実際のコマンドに置き換える必要がある。権限、PMU、仮想化、プラットフォームの能力がイベントの利用可否を決定する。cache-misses などの一般的な名前は、異なる CPU では必ずしも同じハードウェアの意味に対応するわけではない。
イベントが多すぎて多重化(multiplexing)が発生した場合、実行時間の割合とスケーリング結果を確認する必要がある。呼び出しスタックの品質は、フレームポインタ、DWARF、シンボル、展開方法に依存する。シンボルの欠落、サンプルの欠落、誤った展開は、ホットスポット図を変更する可能性がある。サンプルのパーセンテージは、現在のイベントと記録範囲に対する比率であり、任意の関数の正確なウォールクロック時間ではない。ツールのセマンティクスについては perf stat および perf record を参照。
4. レイテンシヒストグラムの正しい構築
レイテンシは一致するイベント間の時間差から来る
スレッド T が write をエントリ
start[T] = t0 を保存
カーネルの実行と待機
スケジューリングで中断される可能性あり
スレッド T が write をエグジット
t1 を読み取り、start[T] を削除
今回の所要時間 = t1 − t0
まず身元と完了の境界を一致させ、ヒストグラムを集計
個々の nsecs はタイムスタンプです。エントリからエグジットまでの所要時間にはその間の待機が含まれ、純粋な CPU 時間ではありません。
hist(nsecs) はタイムスタンプの分布を示すものであり、操作のレイテンシを示すものではない。以下に、bpftrace 0.24 を例として、指定されたプロセスが収集期間中に完全に発生させた write システムコールを記録し、TID を使用してエントリとエグジットを関連付ける方法を示す。
まず、対象の tracepoint とフィールドが存在することを確認する。
bpftrace - lv ' tracepoint:syscalls:sys_enter_write'
bpftrace - lv ' tracepoint:syscalls:sys_exit_write'
write-latency.bt として保存する。
tracepoint:syscalls:sys_enter_write
/pid == $1/
{
@start[tid] = nsecs(monotonic);
}
tracepoint:syscalls:sys_exit_write
/pid == $1 && @start[tid]/
{
@lat_us = hist((nsecs(monotonic) - @start[tid]) / 1000);
@returns[args->ret < 0] = count();
delete(@start, tid);
}
interval:s:10
{
exit();
}
END
{
clear(@start);
}
bpftrace write-latency.bt 12345
12345 を測定対象のプロセス PID に置き換える。@returns[0] は負でない戻り値、@returns[1] は負の戻り値を表す。正の値は短書き(short write)の可能性もあり、そのまま「完全なビジネス成功」と呼ぶことはできない。時間単位はマイクロ秒であり、10000 マイクロ秒は 10 ミリ秒である。
この測定には、syscall 中のブロックとスケジューリング待機が含まれるため、デバイス完了のレイテンシとは等しくない。例えば、ファイルへの write が返された後でも、データはページキャッシュに残っている可能性がある。また、writev、io_uring などの他の書き込みパスはカバーされない。収集開始前にエントリしていた呼び出しや、終了時にまだ返されていない呼び出しは完全なサンプルに含まれないため、ロングテール分析ではこのような切り捨てを考慮する必要がある。
身元の関連付け:同期 syscall と非同期リクエストは別々に設計
TID は通常の同期 syscall のエントリとエグジットを関連付けるのに十分だが、非同期 I/O リクエストはスレッドや割り込みを跨いで完了するため、真のリクエスト ID を使用し、再利用、マージ、リトライ、クリーンアップを処理する必要がある。汎用の再帰関数をトレースする場合、ネストの深さを区別する必要があり、単一の start[tid] をそのままコピーしてはならない。言語と時刻のインターフェースについては bpftrace 0.24 標準ライブラリ を参照。
同じスレッドから 2 つの重複するリクエストを発行し、完了スレッドを選択する。2 つの関連付け方法は同じイベントを同時に処理するが、誤った関連付けで正の数が算出されたとしても、それがそのリクエストの真のレイテンシであるとは限らない。
図を準備しています 再読み込み
同じ TID で 2 つの非同期リクエストを発行した場合、開始時間は誰のものか? TID は通常の同期 syscall を関連付けることができるが、非同期リクエストを一般的に識別することはできない。同じ発信元スレッドからの重複リクエストは start[tid] を上書きし、スレッドを跨いで完了すると対応する項目が見つからない。
{"id":"trace-request-correlation","title":"同じ TID で 2 つの非同期リクエストを発行した場合、開始時間は誰のものか?","summary":"TID は通常の同期 syscall を関連付けることができるが、非同期リクエストを一般的に識別することはできない。同じ発信元スレッドからの重複リクエストは start[tid] を上書きし、スレッドを跨いで完了すると対応する項目が見つからない。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"two\"><div class=\"card\"><header>#101</header><label for=\"tid-101\" data-i18n=\"tid\"></label><select id=\"tid-101\"><option value=\"7\">7</option><option value=\"9\" selected>9</option></select><button id=\"start-101\" data-i18n=\"start\"></button><button id=\"end-101\" data-i18n=\"end\"></button></div><div class=\"card\"><header>#202</header><label for=\"tid-202\" data-i18n=\"tid\"></label><select id=\"tid-202\"><option value=\"7\">7</option><option value=\"9\">9</option></select><button id=\"start-202\" data-i18n=\"start\"></button><button id=\"end-202\" data-i18n=\"end\"></button></div></div><div class=\"actions\"><button id=\"wait\" data-i18n=\"wait\"></button></div><div class=\"metrics\"><div><span data-i18n=\"clock\"></span><strong id=\"clock\"></strong></div><div><span data-i18n=\"overwrites\"></span><strong id=\"overwrites\"></strong></div></div><div class=\"track-row\"><span data-i18n=\"events\"></span><div id=\"events\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"results\"></span><div id=\"results\" class=\"track\"></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.card label{font-size:12px;display:block;min-height:3.2em}.card button{margin-top:8px;min-height:4.8em}#events,#results{display:grid;grid-template-columns:1fr}#events .cell{min-height:52px;font-size:11px}#results .cell{min-height:6.5em;font-size:12px;white-space:pre-line}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let time=0,overwrites=0,req={},tidMap={},idMap={},events=[];function draw(){set('clock',time);set('overwrites',overwrites);for(const id of [101,202]){$('#start-'+id).disabled=!!req[id];$('#end-'+id).disabled=!req[id]||req[id].done;$('#tid-'+id).disabled=!!req[id]?.done;}cells('events',Array.from({length:4},(_,i)=>({text:events[i]??'—',cls:events[i]?'on':'empty'})));cells('results',[101,202].map(id=>{const r=req[id];return{text:'#'+id+'\\n'+(!r?.done?t('pending'):r.truth+' ms / '+(r.tid===null?t('missing'):r.tid+' ms'+(r.wrong?' '+t('wrong'):''))+' / '+r.byID+' ms'),cls:r?.done&&(r.wrong||r.tid===null)?'gold':r?.done?'on':'empty'};}));set('explanation',t('note'));}for(const id of [101,202]){$('#start-'+id).onclick=()=>{time+=10;req[id]={start:time,done:false};if(tidMap[7])overwrites++;tidMap[7]={id,time};idMap[id]=time;events.push(time+' ms · START #'+id+' · TID 7');draw();};$('#end-'+id).onclick=()=>{time+=10;const tid=+$('#tid-'+id).value,r=req[id],item=tidMap[tid];r.done=true;r.truth=time-r.start;r.tid=item?time-item.time:null;r.wrong=!!item&&item.id!==id;r.byID=time-idMap[id];delete tidMap[tid];delete idMap[id];events.push(time+' ms · END #'+id+' · TID '+tid);draw();};}$('#wait').onclick=()=>{time+=20;draw();};$('#reset').onclick=()=>{time=overwrites=0;req={};tidMap={};idMap={};events=[];draw();};draw();","audio":false,"strings":{"clock":"モデル時間 / ms","end":"リクエストの完了","events":"実際のイベントストリーム","heading":"同じ TID で 2 つの非同期リクエストを発行した場合、開始時間は誰のものか?","intro":"リクエスト 101 と 202 をそれぞれ開始および完了させ、完了イベントに対して TID を選択する。2 つの関連付け方法は同じイベントバッチを同時に処理し、欠落や不一致の結果を保持する。","missing":"欠落","note":"まず 2 つのリクエストを発行し、101 を TID 7 で完了させる:これは 202 の起点を奪う可能性がある。正の数が算出されたとしても、それは正しい 101 のレイテンシではない。","overwrites":"TID 起点が上書きされた回数","pending":"未完了","reset":"リセット","results":"結果:真の期間 / TID 関連付け / リクエスト ID 関連付け","scope":"2 つの一意のリクエスト ID、1 つのライフサイクル、イベントの欠落や ID の再利用なし。各開始/完了でモデルクロックが 10 ms 増加し、追加の待機で 20 ms 増加する。TID=7 は発信者であり、9 を選択することは別の完了スレッドを表し、CPU 移行によって TID が変わったわけではない。","scopeLabel":"モデル範囲と規約","start":"リクエストの発行","tid":"完了イベントの TID","wait":"さらに 20 ms 待機","wrong":"不一致"}}
5. 集計、欠落、オーバーヘッド
イベントごとの printf は、ユーザー空間の出力をボトルネックにする可能性がある。分布のみが必要な場合は、カーネル側で count/hist/sum の集計を優先し、詳細が必要な場合は、サンプリング、フィルタリング、バッファ容量、欠落カウントを明確にする。
欠落箇所 生じる可能性のあるバイアス
エントリイベントの欠落 出口で対応する開始時間が見つからない
出口イベントの欠落 開始状態が残存し、次の関連付けを汚染する可能性がある
map がいっぱい、または出力バッファ不足 進行中の状態や詳細の欠落、サンプルが完全でなくなる
欠落を記録し、残存エントリをクリーンアップする必要がある。次のリクエストが古いタイムスタンプを継承しないようにする。
BCC には多くの既存ツールがあるが、従来の BCC ワークフローは通常、実行時に BPF をコンパイルするため、固定されたプリコンパイル済み CO-RE バイナリのセットではない。biolatency、runqlat、tcpretrans などの名前は用途の大まかな説明であり、正確な測定には開始点と終了点、フィルタ条件をインストールされたバージョンで確認する必要がある。BCC プロジェクト を参照。
6. ログ、動的デバッグ、障害ダンプ
printk のログレベルは記録される重大性の分類に影響し、コンソール閾値はどの記録をコンソールに出力するかを決定するが、他のレベルの記録がログリングから消えるわけではない。実際の KERN_* マクロエンコーディングにはカーネルヘッダーファイルを使用し、読みやすい <0> の表示形式を現在のマクロ定義として偽装してはならない。
dynamic debug は、コンパイル時に該当するメカニズムに組み込まれた pr_debug() / dev_dbg() などの呼び出しポイントのみを制御する。関数名で検索してエントリがないからといって、その関数が実行されていないわけではない。その関数に該当するデバッグ呼び出しポイントがない可能性がある。インターフェースは /proc/dynamic_debug/control または debugfs にあり、設定による。
kdump には crashkernel メモリの予約、キャプチャカーネル、initramfs、保存パスの準備が必要である。クラッシュ後、キャプチャカーネルに切り替わり、元のメモリは /proc/vmcore などのインターフェースを通じてエクスポートされ、ユーザー空間ツールによって保存またはフィルタリングされる。kexec -p を実行しただけで、次回必ず完全で読み可能なダンプが取得できるとは限らない。デバイス状態とキャプチャ環境もサポートしている必要がある。
ダンプ分析:シンボルはクラッシュ時のカーネルと一致させる必要がある
分析時には、現在の uname -r を機械的に適用するのではなく、クラッシュしたカーネル と一致する vmlinux、デバッグシンボル、および必要なモジュールを使用する。マシンはすでに別のバージョンに再起動している可能性がある。crash ユーティリティでは、bt は通常、現在の/指定されたタスクのバックトレースを表示し、foreach bt がタスクごとのバックトレースを行う。kdump および crash ヘルプ を参照。
KGDB はブレークポイントの設定、レジスタの読み取り、ステップ実行が可能だが、実行を一時停止すると時間動作が変化するため、ブレークポイントの結果から真のレイテンシを直接推論するには適さない。接続トランスポート、カーネル設定、シンボルも一致させる必要がある。設定と例については ビルドと開発ディレクトリ を参照。
7. 解釈可能な診断フローの例
シナリオの固定。 リクエスト、時間窓、負荷、レイテンシの境界を記録する。
方向の絞り込み。 カウントまたはサンプリングを使用して、on-CPU と待機を区別する。
仮説の検証。 少数のイベントを選択し、必要に応じて関連する関数パスを深く調べる。
観測の影響確認。 有効化前後のスループット、レイテンシ、欠落状況を比較する。
解釈条件の保持。 ツールバージョン、設定、フィルタ、測定制限を記録する。
各ステップは、単にログ量を増やすのではなく、不確実性を絞り込むべきである。
関連リンク:eBPF 基礎 、プログラムタイプ 、ユーザー空間での ftrace と bpftrace の実践 。