1. 平均レイテンシの何が問題か
「このAPIの平均レイテンシは38msです」という報告を受けたとき、あなたはそれをどう解釈するだろうか。多くの人は無意識に「だいたいのリクエストが38ms前後で返ってきているのだろう」と読んでしまう。ところが、実際にレイテンシを大量に生成して調べてみると、この直感は大きく外れる。
以下は、対数正規分布(実務のレイテンシでよく見られる、裾が重く右に伸びる分布)に従うレイテンシを10万件生成し、平均・中央値(p50)・p95・p99・p99.9を実測した結果である。
import numpy as np
rng = np.random.default_rng(42)
N = 100_000
# 中央値が30ms程度になるよう対数正規分布のパラメータを設定
mu, sigma = np.log(30.0), 0.7
latency_ms = rng.lognormal(mean=mu, sigma=sigma, size=N)
mean_ = latency_ms.mean()
p50 = np.percentile(latency_ms, 50)
p95 = np.percentile(latency_ms, 95)
p99 = np.percentile(latency_ms, 99)
p999 = np.percentile(latency_ms, 99.9)
frac_slower_than_mean = (latency_ms > mean_).mean()
実測結果は次の通りである。
| 統計量 | 値 |
|---|---|
| 平均 | 38.3ms |
| p50(中央値) | 29.8ms |
| p95 | 95.0ms |
| p99 | 154.7ms |
| p99.9 | 262.2ms |
| 最大値 | 998.5ms |

ここで注目すべき点は2つある。第一に、平均(38.3ms)は中央値(29.8ms)より28%も高い。分布のピーク(最も多くのリクエストが集まる場所)は中央値の近くにあり、平均はそこから右にずれている。第二に、実測してみると全リクエストの63.9%は平均より速く終わっている(逆に言えば36.1%が平均より遅い)。つまり「平均的なリクエスト」というものは実は少数派の体験であり、平均という一つの数字は「典型的なユーザー体験」を代表していない。
さらにp99(154.7ms)は中央値の5.19倍、p99.9(262.2ms)は平均の6.84倍にまで達する。平均だけを見ていては、この「稀だが実在する極端な遅さ」の存在にまったく気づけない。これがレイテンシ計測において平均値がほぼ役に立たない理由であり、代わりにパーセンタイルを使う理由でもある。
2. パーセンタイルの定義と読み方
パーセンタイルとは、データを小さい順に並べたときに「全体のp%がその値以下に収まる境界値」のことである1。レイテンシの文脈で言えば、p99が200msであるとは「観測したリクエストのうち99%は200ms以内に応答が返り、残り1%はそれより遅かった」という意味になる。
実務でよく使われるパーセンタイルとその読み方は次の通りである。
- p50(中央値): 「半分のリクエストがこれより速く、半分がこれより遅い」という、いわば分布の重心に近い値。平均よりも外れ値に引っ張られにくく、「典型的なユーザー体験」を代表する指標として平均より適している。
- p95: 上位5%を除いた「まあまあ普通に遅いリクエストまで」を含む値。ダッシュボードでの日常監視によく使われる。
- p99: 上位1%を除いた値。多くのSLO(Service Level Objective)がこの水準で定義される。100リクエストに1回程度発生する「そこそこ困った遅さ」を表す。
- p99.9: 上位0.1%まで含む値。1000リクエストに1回のレベルの、稀だが実際に発生する最悪級の遅さを表す。大規模サービスでは1秒間に数千〜数万リクエストが来るため、p99.9で切り捨てられる「稀な事象」でも1日に何度も発生し、無視できない。
どのパーセンタイルを見るべきかは目的次第だが、共通する原則は「平均ではなく分布の右側(遅い側)を直接見る」という点である。次節では、なぜレイテンシの分布がこのように右に長く伸びる形になるのかを整理する。
3. レイテンシ分布はなぜ裾が重いか
第1節の実験で使った対数正規分布は恣意的に選んだわけではない。ネットワークやシステムの応答時間は経験的に対数正規分布に近い形になることが知られており2、レイテンシは「多数の要因の掛け算(乗法的な変動)」として決まることが多い、というのがその直感的な理由である。CPUキャッシュのヒット率、ロック待ち、ガベージコレクション、OSのスケジューリング、ネットワークのゆらぎ、ディスクI/O、隣接コンテナとのリソース競合など、レイテンシに影響する要因は独立に存在し、それぞれが基本ケースを何%か引き延ばす形で効いてくる。こうした乗法的な変動の積み重なりは、対数を取ると正規分布に近づく(中心極限定理の乗法版)ため、結果として分布全体は対数正規分布的な、右に長く伸びる裾を持つことになる3。
裾が重いということは、「まれに大きく外れる値が、無視できない頻度で発生し続ける」ということを意味する。正規分布であれば平均から離れるほど確率は急速にゼロへ近づくが、対数正規分布のような裾の重い分布では、平均の何倍もの値がそれなりの頻度で観測される。第1節で見た「平均より6.84倍遅いp99.9」はまさにこの性質の表れである。
裾が重い分布を平均だけで要約すると、平均という1つの数字が「めったに起きない極端な値」に大きく引きずられる。極端な例として、99人のユーザーが1秒で応答を得て、1人だけ10分(600秒)待たされたとすると、平均は約6.99秒になり、実に99%のユーザーが体験した1秒の7倍近い値になる2。このように平均は「多数派の体験」からかけ離れた値になりやすく、これがレイテンシ監視で平均を避けるべき理由の核心である。
4. 落とし穴1: パーセンタイルは平均できない
パーセンタイルを使い始めると、次に多くの人がやってしまうミスがある。「10台のサーバーそれぞれのp99を出して、それを平均すれば全体のp99になるだろう」という発想である。これは統計的に誤りであり、Prometheusの公式ドキュメントでも明確に指摘されている。
aggregating the precomputed quantiles from a summary rarely makes sense. In this particular case, averaging the quantiles yields statistically nonsensical values.4
実際にどれくらいズレるのかを実測してみる。10台のサーバーのうち9台は同じ(比較的行儀の良い)対数正規分布に従い、残り1台だけディスクI/OかGCの問題を抱えているかのように裾が重い分布にしてある。
import numpy as np
rng = np.random.default_rng(7)
N_PER_SERVER = 50_000
N_SERVERS = 10
mu_normal, sigma_normal = np.log(20.0), 0.5 # 9台: 通常のサーバー
mu_bad, sigma_bad = np.log(20.0), 1.3 # 1台: 裾が重い偏ったサーバー
all_latencies, per_server_p99 = [], []
for i in range(N_SERVERS):
if i == N_SERVERS - 1:
lat = rng.lognormal(mean=mu_bad, sigma=sigma_bad, size=N_PER_SERVER)
else:
lat = rng.lognormal(mean=mu_normal, sigma=sigma_normal, size=N_PER_SERVER)
all_latencies.append(lat)
per_server_p99.append(np.percentile(lat, 99))
avg_of_p99 = np.mean(per_server_p99)
overall_p99 = np.percentile(np.concatenate(all_latencies), 99)
実測結果は次の通りである。
| 指標 | 値 |
|---|---|
| 通常サーバー9台のp99(それぞれ) | 62.9〜65.0ms |
| 偏った1台のp99 | 407.2ms |
| 各サーバーのp99の単純平均 | 98.4ms |
| 全リクエストを合算したp99(真値) | 108.5ms |
| 相対誤差 | 9.3% |

「p99の単純平均(98.4ms)」と「全リクエストを合算した真のp99(108.5ms)」の間には9.3%の相対誤差が生じた。しかも今回はまだ穏やかな部類で、偏ったサーバーの割合や裾の重さ次第では誤差はさらに拡大する。理由は単純で、パーセンタイルは分布の形そのものに依存する非線形な統計量であり、平均のように「足して割る」という線形演算とは相性が悪いからである。合成された分布(9台+1台の全体)における99パーセンタイル点は、個々のサーバーの99パーセンタイル点を単純平均しても再現できない。
正しい合成方法は、パーセンタイルという「加工済みの数字」を平均するのではなく、生の分布(ヒストグラムのビン単位の度数)を先に合算してから、合算後のヒストグラムに対して1回だけパーセンタイルを計算することである。これがまさにPrometheusのhistogram_quantile()関数がヒストグラム型のメトリクスに対して行っていることであり4、summary型で各インスタンスが個別に計算済みのパーセンタイルを後から平均・集約する設計とは根本的に異なる。
5. 落とし穴2: coordinated omission(協調的な見落とし)
パーセンタイルそのものは正しく計算していても、そもそも計測方法自体に欠陥があり、実際より良い値が出てしまうケースがある。この問題を体系的に指摘したのがGil Tene(Azul Systems)の講演 How NOT to Measure Latency で、彼はこれを coordinated omission(協調的な見落とし) と呼んだ56。
典型的な発生パターンは、負荷試験ツールが「1秒に1回リクエストを送る」というように、前のリクエストの応答を待ってから次を送る、あるいは固定間隔で送るクローズドループ方式になっている場合である。システムが一時的に詰まり、本来1秒間隔で送るはずのリクエストが実際には10秒間隔でしか送れなかったとすると、その10秒間に「本来送られるはずだったのに送られなかった9回分のリクエスト」が計測から丸ごと抜け落ちる。抜け落ちたリクエストは、まさにシステムが詰まっていた期間に送られるはずだったリクエストなので、実際に計測されていれば非常に遅い値を記録したはずである。つまり、最も悪い状況で発生するはずだったサンプルほど選択的に計測から漏れるため、計測されたパーセンタイルは実態より大幅に良く見えてしまう。
この効果がどれほど劇的かは、Tyler Treatが紹介している具体例が分かりやすい7。1ミリ秒で捌けるリクエストを100req/sで100秒間処理したあと、システムが100秒間フリーズしたとする。フリーズ中に到着するはずだったリクエストは、フリーズが明けた瞬間に一気に処理されて「見かけ上」記録される。この状況で素朴に(処理にかかった時間だけを)計測すると、平均10.9ms・p99.99が1msという、一見健全なグラフが出来上がる。しかし実際にユーザーが体感した待ち時間まで含めて正しく計測すると、平均は25秒、p99.99は100秒に達する。素朴な計測は「本番投入して問題ない」と教えてくれるが、それは真っ赤な嘘である7。
coordinated omissionを避けるための実務上のポイントは次の通りである。
- 負荷試験ツールは、応答を待たずに予定された時刻通りにリクエストを発行するオープンループ方式(例:
wrk2)を使う。応答待ちで次の送信を止めるクローズドループ方式は、システムが詰まるほど負荷を弱めてしまい、詰まりを隠してしまう。 - アプリケーション側の計測でも、「処理を開始してから終わるまでの時間」だけでなく、「本来処理を開始すべきだった時刻から終わるまでの時間」を意識する。キューに積まれて待っている時間も含めて計測しなければ、行列が伸びている状況そのものを見逃す。
6. 落とし穴3: テール増幅(The Tail at Scale)
パーセンタイル単体の性質を理解しても、それを分散システムに適用するときにもう一つ罠がある。GoogleのJeffrey DeanとLuiz André Barrosoが2013年の論文 The Tail at Scale で指摘した「テール増幅(tail latency amplification)」である8。
1つのユーザーリクエストが、内部で複数のバックエンドへの呼び出しに分岐し、すべての応答がそろって初めて完了するシステムを考える。各バックエンド呼び出しが独立に、p99=Xミリ秒(=1%の確率でXミリ秒を超える)という同じ分布に従うとすると、n個の呼び出しのうち少なくとも1つがXミリ秒を超える確率は
\[ P(\text{最も遅い応答} > X) = 1 - (1 - 0.01)^n = 1 - 0.99^n \]となる。nが大きくなるほど、この確率は急速に1に近づく。「1回あたりは1%しか起きない遅さ」でも、100個の呼び出しに分岐すれば、そのうち誰か1人が引っかかる確率は63%を超える計算になる。
実際に、モンテカルロシミュレーションでこの理論値を検証してみる。単一バックエンド呼び出しのp99しきい値を500万件の基準サンプルから求めたうえで、n=1〜100個の並列呼び出しについて、それぞれ30万回分のリクエストをシミュレートし「n個のうち最も遅い応答がしきい値を超える割合」を実測した。
import numpy as np
rng = np.random.default_rng(2026)
mu, sigma = np.log(20.0), 0.6
# 基準サンプルからp99しきい値を推定
ref = rng.lognormal(mean=mu, sigma=sigma, size=5_000_000)
threshold_p99 = np.percentile(ref, 99) # -> 80.876 ms
N_REQUESTS = 300_000
for n in [1, 2, 5, 10, 20, 50, 100]:
calls = rng.lognormal(mean=mu, sigma=sigma, size=(N_REQUESTS, n))
slowest = calls.max(axis=1)
empirical_prob = (slowest > threshold_p99).mean()
theory_prob = 1.0 - (0.99 ** n)
実測結果は次の通りである。
| 並列呼び出し数 n | 理論値 \(1-(0.99)^n\) | 実測値 | 相対誤差 |
|---|---|---|---|
| 1 | 1.0% | 1.03% | 2.6% |
| 2 | 2.0% | 1.96% | 1.5% |
| 5 | 4.9% | 4.97% | 1.4% |
| 10 | 9.6% | 9.54% | 0.3% |
| 20 | 18.2% | 18.17% | 0.2% |
| 50 | 39.5% | 39.53% | 0.1% |
| 100 | 63.4% | 63.12% | 0.4% |

理論値と実測値は全てのnで相対誤差3%未満で一致しており、\(1-(0.99)^n\) という単純な式が、独立同分布(iid)を仮定した理想的な条件下ではテール増幅の挙動を正確に説明することが確認できた。
この現象は理論上の懸念にとどまらない。Dean & Barrosoは同じ論文で、Googleの実サービスにおける実測例も報告している。ルートサーバーから見て単一のリクエストの99パーセンタイル・レイテンシは10msだが、多数のリーフサーバーへ分岐した全リクエストが完了するまでの99パーセンタイル・レイテンシは140msに達し、しかも最も遅い5%のリクエストを待つ時間だけで、この99パーセンタイル・レイテンシ全体の半分を占めていたという8。これは、実際のサービスでは各呼び出しが完全な独立同分布ではなく相関を持つため、上記の単純なiid計算とは数値が一致しないものの、「並列化された呼び出しの遅さは、個々の呼び出しの遅さより深刻な形で増幅される」という定性的な結論は、理想化した実験でも実サービスの実測でも共通して成り立っている。
マイクロサービス化が進み1つのリクエストが裏側で何十ものサービス呼び出しに分岐する現代のアーキテクチャでは、個々のサービスのp99がどれだけ良好でも、末端のユーザーが体感するレイテンシはそれよりずっと悪くなりうる。これがp99を「バックエンド1つの指標」としてだけでなく、「システム全体でどれだけ増幅されるか」まで含めて設計・監視すべき理由である。
7. 実務での使い方
ここまでの実験を踏まえ、パーセンタイルを実務でどう使うべきかを整理する。
- SLO(Service Level Objective)はp99を基準に切る: 平均は少数派の体験しか代表しないため、SLOの合否判定に使うと大多数のユーザーが実際には満足していても、あるいは逆に一部のユーザーが深刻な体験をしていても、その事実を隠してしまう。p99(または場合によってp95・p99.9)を基準にすることで、「ほぼ全員が許容できる遅さに収まっている」ことを直接保証できる。
- ヒストグラムのバケット単位で近似する: すべてのリクエストの生の値を保存してパーセンタイルを厳密に計算するのはメモリ・ネットワークコストが高い。Prometheusの
histogram型は、あらかじめ決めた境界値(バケット)ごとの累積度数だけを保持し、histogram_quantile()関数でバケット間を線形補間してパーセンタイルを近似する4。バケットの境界を細かく取るほど精度は上がるが、境界の粗さに応じた誤差は常に残る点は認識しておく必要がある。 - 複数インスタンスのp99を見たいときは、必ず生データ(ヒストグラム)を先に合算してから1回だけパーセンタイルを計算する: 第4節で実測した通り、個別に計算済みのp99を後から平均・加重平均しても正しい値にはならない。監視基盤側の設計として、
summary型ではなくhistogram型を選ぶ、あるいは集計前の生ログを保持しておく、といった対応が必要になる。 - 負荷試験・レイテンシ計測ツールがcoordinated omissionを起こしていないか確認する: 応答を待ってから次のリクエストを送るクローズドループ方式の負荷試験ツールを使っていないか、監視コードが「キューで待っていた時間」を計測に含めているかを確認する。第5節で見た通り、これを怠ると本番相当の負荷でも問題なく見えてしまう。
- 並列化された呼び出しではテール増幅を織り込む: 1つのリクエストが多数のバックエンド呼び出しに分岐する構成では、末端のレイテンシは個々の呼び出しのp99よりずっと悪化しうる(第6節)。個々のサービスのp99を追うだけでなく、エンドツーエンドのレイテンシも別途監視し、必要であればhedged request(複数のバックエンドに投げて最初に返ってきた応答を使う手法)などの緩和策を検討する。
近い分野の実測記事として、ベクトル検索エンジンの内部動作を計測したhttps://yuhi-sa.github.io/posts/20260719_milvus_internals/1/や、コネクションプールサイズを待ち行列理論でモデル化したhttps://yuhi-sa.github.io/posts/20260716_db_pool_queueing/1/も、いずれも「平均ではなく分布・待ち時間の裾をどう扱うか」という本記事と共通する視点を扱っている。
8. まとめ
対数正規分布のレイテンシ10万件・10台のサーバーのp99合成・並列呼び出しのテール増幅という3つの実験を通じて、次の点が定量的に確認できた。
- 平均(38.3ms)は中央値(29.8ms)より28%高く、全リクエストの36.1%は平均より遅い。平均は「典型的な体験」を代表しない。
- 10台のサーバーそれぞれのp99を単純平均した値(98.4ms)は、全リクエストを合算した真のp99(108.5ms)と9.3%ズレる。パーセンタイルは平均できない。
- 並列バックエンド呼び出し数nが増えるほど、最も遅い応答がp99を超える確率は理論値\(1-(0.99)^n\) 通りに急増し、n=100では63%を超える(相対誤差はいずれも3%未満で理論と一致)。
これらはいずれも、パーセンタイルという道具そのものは正しくても、使い方を誤ると簡単に間違った結論を導いてしまうという実例である。平均ではなくパーセンタイルを見ること、パーセンタイルを安易に平均・合成しないこと、計測方法自体がcoordinated omissionを起こしていないか疑うこと、そして並列化がテールを増幅することを織り込むこと。この4点を押さえるだけで、レイテンシに関する意思決定の精度は大きく変わる。
FAQ
Q1. p50とp95、どちらを見るべきか? A. 目的によって使い分ける。「典型的なユーザー体験を知りたい」ならp50(中央値)が平均より適切な代表値になる。一方「大多数のユーザーが困っていないことを保証したい」「SLOの合否を決めたい」場合はp95やp99のような分布の右側(遅い側)を見るべきである。両方を併記し、p50とp99の乖離が大きい(第1節ではp99/p50=5.19倍)ことそのものを「分布がどれだけ裾を引いているか」の指標として監視するのも有効である。
Q2. p99.9まで必要か? A. リクエスト数が少ない(1日数千件程度の)サービスであれば、p99.9はサンプル数不足で数値が不安定になりやすく、必須ではないことが多い。しかし秒間数百〜数千リクエストを捌く大規模サービスでは、p99.9の「1000回に1回」の事象でも1日に何百回も発生するため、無視できない実害になる。トラフィック量とビジネス上の重要度に応じて、p99.9やp99.99まで監視対象を広げるかを判断する。
Q3. 平均とp50(中央値)の乖離は何を意味するか? A. 両者が近ければ分布は比較的対称に近く、外れ値の影響が小さいことを示す。逆に平均がp50より大きく上回っている(第1節では28%高い)場合、分布が右に強く裾を引いている、つまり「極端に遅いリクエストが平均を引き上げている」ことを意味する。この乖離幅自体を監視しておくと、裾の悪化(p99・p99.9の悪化)を平均の変化だけからでも早期に察知する手がかりになる。
Q4. 複数サーバー・複数インスタンスのパーセンタイルはどう合成すればよいか?
A. 個別に計算済みのパーセンタイル値を平均・加重平均してはいけない(第4節で実測した通り9.3%以上ズレうる)。正しい方法は、生の観測値、あるいは少なくともヒストグラムのバケット単位の度数を先に合算し、合算後のデータに対して1回だけパーセンタイルを計算することである。Prometheusであればsummary型ではなくhistogram型のメトリクスを使い、histogram_quantile()で集計後に1回だけ計算するのが実務的な解法になる4。
技術書での体系的な学び直しには エンジニアにおすすめの技術書10選 も参考にしてほしい。
参考文献
Percentile. Wikipedia. https://en.wikipedia.org/wiki/Percentile ↩︎
Vitillo, R. Why You Should Measure Tail Latencies. https://robertovitillo.com/why-you-should-measure-tail-latencies/ ↩︎ ↩︎
Tail latency. Wikipedia. https://en.wikipedia.org/wiki/Tail_latency ↩︎
Histograms and summaries. Prometheus documentation. https://prometheus.io/docs/practices/histograms/ ↩︎ ↩︎ ↩︎ ↩︎
Tene, G. (2013). How NOT to Measure Latency. QCon London 2013 発表資料. https://www.slideshare.net/slideshow/how-not-to-measure-latency-london-oct-2013/27088981 ↩︎
Tene, G. How NOT to Measure Latency. YouTube. https://www.youtube.com/watch?v=lJ8ydIuPFeU ↩︎
Treat, T. (2015). Everything You Know About Latency Is Wrong. Brave New Geek. https://bravenewgeek.com/everything-you-know-about-latency-is-wrong/ ↩︎ ↩︎
Dean, J., & Barroso, L. A. (2013). The Tail at Scale. Communications of the ACM, 56(2), 74-80. https://cacm.acm.org/research/the-tail-at-scale/ ( https://research.google/pubs/the-tail-at-scale/ も参照) ↩︎ ↩︎