Skip to main content

概要

パフォーマンスとは、システムが同じ仕事をどれだけ少ない時間と資源でこなせるかという性質です。このページでは、Webサービス、中心的にはアプリケーションサーバーとデータベースを対象に、その改善の進め方を解説します。 改善のためのテクニックは無数にありますが、その手前には「何のために」「どこまで」「どの順で」という共通の判断があります。このページで扱うのは、個別のテクニックではなくこの判断です。 まず何のために速くするのかという目的の整理から始めて、問題の分類、十分さの基準、対処を検討する順序、そして計測の原則と可観測性の基本の順に進みます。

何のために速くするのか

具体的な改善手法の検討に入る前に、まず目的を言語化します。パフォーマンス改善の目的は速さそのものではなく、その多くは次の3つのどれかに行きつきます。

ユーザー体験

操作が遅い、タイムアウトするといった、ユーザーに直接見える問題を解消します。

システムの安定性

負荷の高止まりを解消し、障害への余裕を取り戻します。

コスト

増え続けるインフラ費用を抑えます。
どの目的のための改善なのかを最初に決めます。目的が違えば、後述するとおり見るべき指標も対処も変わるからです。

問題を3つに分類する

パフォーマンス改善で扱う問題は、大きく3つに分けられます。症状によって見るべき指標も対処も変わるため、最初にどれを解いているのかを特定します。 レイテンシとスループットは、ユーザーや業務が待たされる、外から見える問題です。一方リソース負荷は、誰にも気づかれないまま安定性の余裕とコストを侵食し、放置すればいずれ前の2つの問題として、あるいは障害として表面化します。 このうちレイテンシの定量化には、平均レスポンスタイムではなくパーセンタイル、すなわちリクエストを速い順に並べたときの「◯%目の速度」を使うのが一般的です。
  • p50(50パーセンタイル、中央値) — 速い方から50%目の速度。標準的なユーザーの体験です
  • p95 / p99(テールレイテンシ) — 速い方から95%目・99%目の速度。「これより遅い体験をしているユーザーがまだ5%・1%いる」と読みます
パーセンタイルを使うと、「100人中1人のユーザーが2秒以上待たされている(p99 = 2,000ms)」のように、体験の悪い側を具体的に語れます。それをどこまで許容するかはプロダクトや場面によって異なり、その線引きが次節で扱う目標設定です。観測するのは、p95やp99の推移です。
平均値を使わないのは、一部の極端な遅延を覆い隠すからです。10回に1回だけ1秒かかり、残りの9回はほぼ一瞬で返るAPIでは、平均は約100msですが、この速度を体験したユーザーは1人もいません。かといって最大値は、ごく稀な外れ値に振り回されます。少数の遅延を薄める平均と、外れ値で暴れる最大値の中間で、順位で切ることで安定した定点になるのがパーセンタイルです。

どこまで速ければ十分か

定量化の言葉が手に入ると、目標も決められるようになります。パフォーマンスチューニングには終わりがなく、200msを100msに縮めるコストより、100msを10msに縮めるコストの方がはるかに大きくなります。だからこそ、どこで止めるかを先に決めます。 「このAPIはp95で300ms以内を維持できていればユーザー体験として十分」「このバッチ処理は1時間以内に終われば翌朝の業務に支障はない」といった目標値を、SLO(Service Level Objective、サービスレベル目標)としてあらかじめチームで合意しておきます。 目標を達成した時点で、そのチューニングは成功です。それ以上の追求は投資対効果が下がっていき、際限のない改善は複雑さを買い込んで変更容易性を損ないます。
合意した目標は、負荷試験の合格基準にもなります。実トラフィックのない公開前でも、大きな変更やアクセス増の見込まれるイベントの前でも、負荷をかけて目標への余裕を確かめられます。

対処の階層

解く問題と目標が決まったら、対処を選びます。対処には、効果とコストのバランスによる階層があります。上から順に検討するのが基本です。
  1. 要求を減らす — 仕様の見直し。実は求められていない厳密さや網羅性を緩めて、重い計算そのものを不要にします(詳細は後述)。
  2. 仕事を減らす — 不要な処理の削除、過剰な頻度の間引き(ポーリングや再計算など)、N+1クエリの解消、データ構造やアルゴリズムの改善。これは無駄の除去であり、実装の手間はかかるものの、機能や整合性といったシステムの性質を何も損なわず、速くなります。実際のシステムには大抵この余地が残っています。
  3. 仕事を覚える — キャッシュ、事前集計。計算結果を保存して読むだけにするもので、1回の計算を何度も使い回すため仕事の総量そのものが減ります。効果は大きいものの、整合性(無効化・再計算の管理)という負債を買います。無効化漏れのバグは発見が難しいものです。
  4. 仕事をずらす — 非同期化、実行時間帯の分散。重い処理をリクエストの外に切り出して後から実行すれば、応答はその完了を待たずに返せます。夜間などの空いた時間帯に寄せれば、ピークの負荷も減らせます。ただし「いつやるか」を変えるだけで、仕事の総量は変わりません。
  5. 資源を増やす — スケールアップ、スケールアウト。需要の伸びに応じてキャパシティプランニングとして計画的に増やすのは健全です。エンジニアが時間をかけて修正するコストと今すぐ得られる安定を天秤にかけ、一時的にスケールアップして時間を稼ぐ判断が正解になる場面もあります。ただしN+1の往復、ロック競合、計算量の大きいアルゴリズムのように、資源を増やしても解決しないボトルネックがあることと、原因を隠したまま固定費だけが増え続けるリスクには注意が必要です。

仕様の見直しという選択肢:「90パーセントの解」

ボトルネックとなる処理が特定できたとき、すぐに「技術的にどう解くか」を考える前に、一度立ち止まって「そもそも、この重い処理を完璧な仕様のまま実行し続ける必要はあるのか」と問いかけます。 『UNIXという考え方』では、「90パーセントの解を目指す」という指針が紹介されています。完璧さを目指すとシステムの複雑性や処理コストが跳ね上がりますが、要求の9割を満たすシンプルな解にとどめれば効率は大きく向上する、という思想です。パフォーマンスを圧迫している原因が、実はユーザー体験やビジネス上そこまでの厳密性を求められていない仕様だった、ということは珍しくありません。 プログラムを高速に動かすことよりも、重い計算処理そのものを不要にすることの方が、投資対効果が高く、実装も単純になります。高速化の実装に取りかかる前に、仕様を決めている人たちと「少し緩められないか」を議論することは、最小の労力で最大の成果を生む選択肢です。

何を引き換えにするか

仕様を緩められない、あるいは緩めてもなお足りないときは、何かを引き換えにして速度を得ることになります。この引き換え(トレードオフ)の中身は、対処ごとに異なります。
  • キャッシュ ↔ 整合性(画面に数分古い値が出ることを許容できるか)
  • 事前集計 ↔ 柔軟性(集計軸が固定され、自由な条件指定と両立しません)
  • 非正規化 ↔ 更新コストと不整合リスク
  • 非同期化 ↔ 即時性(処理の完了が後になることを許容できるか。失敗時の再実行やリカバリの設計も必要になります)
  • スケールアップ・スケールアウト ↔ 継続するインフラコスト
どの手を選ぶかは、どの対価なら仕様として許容できるかで決めます。また、対価は意図せず生まれることもあります。無駄の除去のつもりでも、込み入った実装が必要になったなら、可読性と変更容易性を引き換えにしています。

早すぎる最適化と、後戻りの難しい決定

ここまでの流れは「問題と目標を見極め、順序立てて対処する」というものでした。裏を返せば、必要性が示される前に最適化のためのコードを書き込まない、ということでもあります。コードは後から直せるので、まず素直に書いて、計測が必要性を示してから手を入れれば間に合います。 ただし、後戻りの難しい決定は例外です。データの持ち方(テーブル設計、データの粒度)や外部に公開するAPIの形(ページネーションの有無、一括取得の可否)は、依存が積み上がった後に直すコストが桁違いに大きいため、最初から性能を考えるべきです。具体的には、「データ量が10倍、100倍になっても、この設計のままで成立するか」を設計の時点で問います。

推測ではなく計測から始める

パフォーマンス最適化には「ボトルネック以外の場所をどれだけ高速化しても、システム全体の処理時間はほとんど変わらない」という性質があります。全体処理時間10秒のうち8秒をデータベースクエリが占めているとき、1秒かかっているループ処理を極限までチューニングしても、縮むのは最大で1秒です。一方で、8秒のクエリを半分にすれば、それだけで全体は6秒になります。 ボトルネックがどこにあるのかを、データによって客観的に特定すること。これが最初にして最も重要な一歩です。そして修正に入ったら、1回に1つ変えて再計測します。同時に複数の変更を入れると、何が効いたのかが分からなくなります。 また、1つのボトルネックを解消すると、今度は別の箇所が新たなボトルネックとして現れます。特定と解消は一度きりの作業ではなく、目標に届くまで計測とともに繰り返すサイクルです。

可観測性の基本

「推測ではなく計測」を実行するには、システム内部の状態を観測できる必要があります。可観測性(Observability)は、次の3種類のデータを組み合わせて確保します。 メトリクスで全体の傾向をつかみ、トレースで経路を特定し、ログで詳細を調べる、という順で観測の解像度を上げていきます。 この3種類のデータ(テレメトリ)の収集・送出は、CNCFが推進するOpenTelemetryが業界標準になっています。かつてはAPMベンダーごとに固有のエージェントを組み込む必要があり、ベンダーロックインが課題でしたが、標準API/SDKで計装しておけば、送信先(DatadogやNew Relic、各クラウドの標準サービス、OSSのJaegerやPrometheus、Grafanaスタックなど)を切り替えたり併用したりできます。 こうした基盤がまだ整っていない環境でも、アクセスログへの処理時間の出力や、データベースのスロークエリログの有効化といった最小限の手段から観測を始められます。

APMとプロファイラ

集めたデータからボトルネック特定を効率化するのがAPM(Application Performance Monitoring)とプロファイラです。 APM — トレースの収集・可視化を中核とするツールです。多くの製品では、エンドポイントごとのレイテンシ(p95等)やエラー率といった集計も合わせて見られます。「Webリクエスト全体の中でどこがボトルネックか」(N+1クエリの発行、外部APIの応答待ちなど)を判断するのに向いています。 プロファイラ — コードレベルのCPU時間やメモリ割り当てを追跡します。「どの関数がCPUを使っているか」という、コード内部の効率評価に向いています。各言語に標準的なツールがあります。 実務では、まずAPM(トレース)で「どのコンポーネントやデータベースクエリが原因か」を大まかに絞り込み、必要に応じてコードレベルのプロファイラやログ分析へ移る、という階層的な調査が効率的です。
アプリケーションのさらに下、OSやカーネルのレベルにもプロファイラの世界があります(LinuxのperfやeBPFを使うツールなど)。このページの範囲を超えますが、そこまで掘る方法は『詳解 システム・パフォーマンス』が詳しく扱っています。

CPUを使っているのか、待っているのか

観測データを分析するとき、最初に識別すべきなのは「その遅延はCPUを使い切っているからか、何かを待っているからか」という性質です。 CPUが空いているのにサービスが遅い場合、何かを待っています。多くはデータベースやネットワークなどのI/O待ちですが、ロックの取り合いで詰まっていることもあります。この切り分けだけで、無駄な試行錯誤を避けて原因に近づけます。

どこから見始めるか

どこから見始めるかは、先に特定した問題の類型で決まります。特定の操作が遅いレイテンシ型なら、観測の解像度を上げながら次の順で絞り込みます。
1

リクエストの内訳を見る

APMのトレースで、そのリクエストのどこで時間を使ったかを特定します。支配的な1点は多くの場合ここで見つかります。
2

データベース全体の中での占有を見る

主因がクエリなら、データベース全体の統計でそのクエリがどれだけ負荷を占めているか、何を待っているかを確認します。
3

クエリ単体を調べる

EXPLAINで、なぜそのクエリが遅いのかを実行計画から特定します。
全体がじわじわ重いリソース負荷型なら、資源の飽和状況やデータベース全体統計から負荷上位の常連を探します。バッチ処理が終わらないスループット型は、レイテンシ型と同じ絞り込みを処理全体に対して行います。まずジョブ全体の時間内訳から支配的な区間を特定し、そこからクエリや処理単体へ降りていきます。

関連ページ

データベースパフォーマンス

最も頻繁にボトルネックになるデータベースを速くする方法を、「読むストレージ量の最小化」という原則から解説します。

変更容易性

低いコストと低いリスクで変更を受け入れる性質を解説します。行き過ぎた最適化が損なうのはこの性質です。

リファクタリング

性能改善は、ユーザーが気づく振る舞いの変更です。それを構造の変更と同じプルリクエストで混ぜない規律を解説します。