『詳解 システム・パフォーマンス 第2版』を読む:第5章 アプリケーションの全容

Progress 2 / 13
目次

はじめに

[!NOTE] この記事は、筆者が鈍器本を読みながら書き殴った個人的な読書メモをAIに食べさせ、ブログ用に読みやすく再構成させたものです。

前回はベンチマーキング業務の土台として「2章 メソドロジ」をまとめました。今回はその続きとして、一番手が届きやすくて効果もデカい「5章 アプリケーション」について整理します。

システムのパフォーマンスをもっとも劇的に改善できるのは、常に仕事が行われている現場の最前線、つまり「アプリケーション側」です。この章はどうしても避けて通れません。

今回も後から見返してサクッと記憶を呼び戻せるよう、絶対に覚えておきたい落とし穴やテクニックだけを、なるべくスッキリとまとめておきます。


1. いきなりコードに飛び込まない

ボトルネック調査となると、すぐにソースコードを開いてプロファイラを回したくなりますが、まずは**「システムの全体像(要件、構成、制限)」**をざっくり把握するのが定石みたいです。

  • よく実行されるコードから直す: 本番環境でもっともよく通るコードパスを見つけ出し、そこから集中的に改善するのが一番コスパが良いです。「そこを最適化せずに他を先に最適化するのは非効率」というのは、非常に耳が痛いですね。
  • 不要な仕事を取り除く: コードを究極まで研ぎ澄ますよりも、「そもそもやらなくていい処理を削る」方が圧倒的に大きな効果を生みます。

2. パフォーマンス向上のためのテクニック

アプリケーションの構造でパフォーマンスを左右する、代表的なトピックです。

I/Oサイズとバッファリング

「1回のI/Oで128KBのデータを送る方が、1KBを128回送るよりもずっと速い」という基本的な話です。ただ、ランダムな細かい読み出しが多いDBで無理にサイズを大きくすると、無駄なデータ転送が増えて逆効果になるので、「どんな仕事をしているか」を見極める必要があります。

ノンブロッキングI/O(非同期処理)

Unix プロセスはI/O実行中に「ブロック(スリープ状態)」されてしまいます。これをそのまま放っておくと、現場で必ず以下の問題に直面します。

  1. プロセス(スレッド)が大量に必要になり、メモリを圧迫する
  2. 短いI/Oを繰り返すと、「コンテキストスイッチ」のせいでCPUがムダに消費される

これを避けるために、今どきのアプリでは「ノンブロッキングI/O(非同期I/O)」のモデルを採用するのが一般的ですね。

ソフトウェアにも「USEメソッド」が使える!

前回(2章)のメモで、ハードウェアのボトルネックを探す「USEメソッド(使用率・飽和度・エラー)」を紹介しましたが、実はこれ、ソフトウェアのリソースにもそのまま応用できるんです。

たとえば「ワーカースレッドのプール」ならこんな感じです。

  • 使用率: ビジー状態になっているスレッドの割合
  • 飽和度: 処理待ちになっている要求キューの長さ
  • エラー: 拒否されたり失敗したりした要求の数

「ファイルディスクリプタ(開けるファイルの上限)」なども同様にUSEでモデリングできます。行き詰まったら、ソフトウェアのコンポーネントもUSEメソッドで整理してみると突破口が見えそうです。


3. 分析の「落とし穴」と注意点(※最重要)

この章で個人的に一番「なるほど」と思ったのがここです。現場でやらかしがちな罠が集まっています。

デバッガ利用の致命的な罠

アプリが固まっているように見えると、ついカッとなって gdbpstack のようなデバッガでスタックトレースを調べたくなりますよね。

でも、デバッガはターゲットのアプリケーションを完全に一時停止させてしまうため、その行為自体が深刻なパフォーマンス障害を引き起こすという仕様上の罠があります。本番環境でうかつにやると大惨事になりかねないので本当に注意です。

短いインターバルの罠

「5分間の平均使用率が低かったから大丈夫」と油断してはいけません。実はその5分のうちの一瞬だけ100%に張り付いてキューイング(渋滞)を起こしていた、というケースがよくあります。(高速道路の料金所で、1日の平均で見たら車が少なくても、ラッシュアワーだけ尋常じゃない渋滞が起きているのと同じ理屈です)。

関数名が消える問題(Unknownシンボル)

JavaやNode.jsなどの「JITコンパイラ」を使っている言語をプロファイリングすると、関数名が [unknown] になってしまう罠があります。これは実行時に動的にコンパイル(シンボル生成)が行われているため、OS側の標準プロファイラから名前が見えなくなってしまうのが原因です。

  • 解決策: ランタイムが生成する補助ファイル(/tmp/perf-<PID>.map)を読み込ませるか、プロファイリング直後に perf-map-agentasync-profiler といった専用のスタックヘルパーツールを利用してシンボルを解決します。

スタックトレースが途切れる問題(Unknownスタック)

Javaなどでプロファイリングを行うと、スタックトレースが [unknown] となって途中でちぎれてしまうことがあります。これはコンパイラのパフォーマンス最適化が原因です。

  • 解決策: Javaの場合、起動時に -XX:+PreserveFramePointer というオプションをつけます。
  • これをつけるとCPU性能が1%ほど落ちるみたいですが、ボトルネックを可視化して10%以上改善できるなら、最初からつけておいた方がトータルでは圧倒的に得ですよね。

おわりに

この章を読むことで、コードレベルのアルゴリズムだけでなく、I/Oの方式やコンパイラの最適化の仕様まで含めた「多角的な視点」を持っておくことの重要性がわかりました。

特に「本番環境でのデバッガの危険性」や「Unknownスタックの原因」などは、知らずにハマると丸一日溶かすようなトラップなので、今後の分析業務でもしっかり頭の片隅に置いておきたいと思います!