P99 レイテンシーとは、すべてのリクエストを応答時間の速い順に並べたとき、99 パーセンタイルの位置にある値です。99% のリクエストはこの値より速く、最も遅い 1% だけがこれを超えます。大規模システムにおいて、この「最も遅い 1%」は、毎秒数百から数千の実ユーザーの体験を意味します。平均レイテンシーはこうしたスパイクを覆い隠せますが、P99 は覆い隠しません。

あるオンラインサービスを運用しているとします。サポートから苦情が転送されてきます。「決済がときどきぐるぐる回ったまま止まり、再試行すると直る」。監視ダッシュボードを開くと、平均レイテンシーは平坦、エラー率はゼロ、CPU も低く、画面は一面グリーンです。経験則に従って遅いクエリ、外部依存、並行設定を調べても、いずれも正常。負荷をかけて再現しようとしても再現しません。その間も苦情はぽつぽつと届き続け、上流・下流のチームが SLA を盾に問い合わせてくるのに、問題の存在を証明できる指標が手元に一つもない——。

これがテールレイテンシー問題の典型的な状況です。厄介なのは技術が難解だからではなく、次の 4 つの調査の盲点を同時に突いてくるからです。監視が示すのは集約値であり、遅いリクエストは平均に薄められる。散発的に発生するため、テスト環境には存在しない。根本原因は本番の実負荷でしか発火しないことが多い——O(n) のクエリは実際の規模でしか爆発せず、正規表現は特定の入力でしかバックトラックしない。そして本番環境こそ、コード変更・計装の追加・サービス再起動が最もできない場所である。このとき最初に見るべき指標が P99 です。本記事は定義を起点に、根本原因の全体像と診断の道筋を展開します。以下の事例の数値はすべて本番環境から得たものです。

P99 レイテンシーとは?

P99 レイテンシー(99th percentile latency)は、一群のリクエストのうち最も遅い 1% の応答時間の境界を表します。データ上ではどう見えるのでしょうか。100 件のリクエストの応答時間を収集し、速い順に並べたとします。

#1  … #98 :  ≤ 10ms
#99       :  300ms   ← P99 = 300ms
#100      :  450ms

最初の 98 件はすべて 10ms 以内に完了していますが、99 件目は 300ms であり、P99 は 300ms となります。これは机上の構成ではありません。あるフィンテックゲートウェイの事例で見られたのが、まさにこの形でした(詳細は後述します)。

算出方法

P99 の算出に平均演算は含まれません。純粋なソートと位置の取り出しです。

  1. 一群のリクエストの応答時間を収集する
  2. 小さい順に並べる
  3. 99 パーセンタイルの位置にある値を取る

注意すべき点として、P99 は一切の平均化を行いません。99 パーセンタイルの位置にある値がそのまま答えです。だからこそ、平均値のように中間の大量の「正常な」リクエストに薄められることがなく、テールの体験を直接反映します。

P50・P95・P99:各パーセンタイルが示すもの

パーセンタイルレイテンシーはそれぞれ異なる問いに答えます。正しい指標を選ぶための対照表を以下に示します。

パーセンタイル意味答える問い典型的な用途
P50(中央値)50% のリクエストがこの値より速い典型的なユーザー体験は?日常監視のベースライン、キャパシティプランニング
P9595% のリクエストがこの値より速い大半のユーザーにとっての最悪の体験は?アラート閾値、性能リグレッションの検出
P9999% のリクエストがこの値より速い最も遅い 1% はどれほどひどいか?SLO/SLA の定義、テールレイテンシーの管理

たとえば、同じ 1,000 件のリクエストから 3 つの読み値が得られたとします。P50 = 8ms、P95 = 40ms、P99 = 300ms。3 つの数値はいずれも真実ですが、テールを暴くのは P99 だけです。前の 2 つだけを見ていると、システムは健全だと思い込んでしまいます。

P50 は「通常の状態」を、P95 は「大半の場合の上限」を、P99 は「最悪の実体験」を教えてくれます。高トラフィックなシステムにおいて、1% は無視できる数字ではありません。毎秒 10,000 件のリクエストを処理するサービスなら、P99 が表すのは毎秒 100 ユーザーの実際の遭遇です。

どのパーセンタイルを SLO とするかはビジネス上の文脈によります。決済ゲートウェイや取引システムは通常 P99、あるいは P99.9 を基準とし、社内ツールなら P95 だけを見ればよいこともあります。重要な原則はこうです。P50 しか見ていなければ、システムへの理解は楽観的になる。平均値しか見ていなければ、その理解は誤っているかもしれない。

平均レイテンシーが嘘をつく理由

たとえて言えば、空港の保安検査は 1 人あたり平均 30 秒ですが、目の前で手荷物を開けて調べられた乗客は 10 分かかりました。「平均 30 秒」という数値は嘘ではありませんが、いま列に並んでいる際の体験を説明する力はまったくありません。レイテンシー指標も同じ道理です。

レイテンシー分布にはある数学的な特性があります。下限はある(0ms を下回ることはできない)が上限はない(1 回の GC 停止やロック競合が、単一のリクエストのレイテンシーを任意に高く押し上げうる)という点です。この右に裾を引いた分布は、ごく少数の極端に遅いリクエストが平均値をほとんど動かさない一方で、P99 を驚くほどの高さまで押し上げることを意味します。

これがどれほど見つけにくいかを、2 つの実事例が示しています。

  • 監視は一面グリーン、SLA は突破される。 ピーク 500,000 QPS のフィンテックゲートウェイでは、P50 は安定して 10ms 以内、監視ダッシュボードもすべて正常に見えていました。しかし OpenResty XRay の詳細サンプリングにより、最も遅い 1% のリクエストが 300ms を超えていることが判明します。すでに主要なトランザクションパスの SLA を脅かしており、さらに悪化すればゲートウェイ側の HTTP 504 タイムアウトにつながる状態でした。根本原因が何だったかは、次節の全体像で扱います。
  • エラー率・スループット・平均値はすべて正常、P99 だけが急騰する。 ある D 言語の注文サービスは、通常監視は完全に健全でしたが、P99 は 120ms のベースラインから 350ms へと急騰しました。チームはデータベース、外部依存、並行設定を順に調べましたが、すべて空振り。OpenResty XRay のフレームグラフが最終的に特定したのは、サービス自身の内部にある 2 つの根本原因でした——これも次節の全体像に収録しています。

2 つの事例は同じことを物語ります。平均レイテンシーは「大半の人に問題がない」ことの証明であって、「誰にも問題がない」ことの証明ではありません。 アラートが平均値しか見ていなければ、テールの異常を知るのはユーザーが苦情を寄せた後になります。

P99 レイテンシーが高くなる理由:根本原因の全体像

P99 を調査する前に、自分自身に一つ問いかけてください。スパイクが起きているとき、CPU は忙しいのか?

この問いは、P99 の根本原因を一本の軸に沿って 2 つの大きなクラスに切り分けます。CPU が本来すべきでない処理で忙しい(on-CPU の根本原因)と、CPU が待っている(off-CPU の根本原因)です。この分類は学術的なものではありません。どの種類のツールで診断すべきかを直接決めます。

CPU が誤った処理で忙しい(on-CPU の根本原因)

CPU 使用率が高く、P99 も高いとき、時間は無用な計算に浪費されています。以下の 6 分類は、いずれも本番環境で実際に特定した根本原因です。

① 正規表現のバックトラック。 ネストした量指定子のパターン((a+)+$ のような形)は、特定の入力で指数関数的なバックトラックに退化しえます。前述のゲートウェイ事例の 300ms スパイクは、まさにこれが原因でした。ある文字列マッチング関数が、特定の入力で1 回の実行に 244.64ms を要していたのです。

② リクエストごとの再コンパイル。 log フェーズのある正規表現がリクエストごとに毎回再コンパイルされ、この「低オーバーヘッド」のはずのフェーズが逆に CPU の 26.5% を占めていました。コンパイルキャッシュを有効にすると、その CPU がそのまま解放されました。(同じ事例

③ ビルドの欠陥。 根本原因はコードにではなく、ビルドパイプラインにあります。ある事例ではベースイメージが --with-pcre-jit ビルドパラメータを欠いており、クラスター全体で PCRE JIT による高速化が一度も効いていませんでした(詳細事例)。別の事例では、デバッグ用の -O0 コンパイルオプションを元に戻し忘れており、実測でスループットの 10% を食っていました(keepalive 事例)。

④ コネクションストーム。 upstream 設定ブロックに keepalive の 1 行が欠けていると、リクエストごとに完全な TCP ハンドシェイクと切断が発生します。

upstream backend {
    server 10.0.0.2:8080;
    # ここに keepalive ディレクティブが欠けている。connect()/close() が CPU フレームグラフ上に 2 つの幅広いフレームを広げる
}

カーネルはデータの処理ではなく、コネクションの確立と破棄に時間を費やします。keepalive 64; の 1 行を補ったところ、スループットは 6,301 QPS から 21,923 QPS へと回復し、3.48 倍になりました

⑤ GC 停止。 D 言語の保守的GCは、ポインタと「ポインタのように見える整数」を区別できず、ヒープ全体を 1 ワードずつ走査せざるをえません。高頻度の割り当ての下では正のフィードバックが形成されます——ヒープが大きいほど走査は長引き、停止も長くなる。フレームグラフでは GC が CPU の 26.4% を占めていました。(D 言語事例

⑥ O(n) アルゴリズムが規模で爆発する。 ある注文クエリが配列全体を走査して線形フィルタを行っており、低負荷では何の異常もないのに、実トラフィックの下では CPU の 59.4% を占めていました。コードレビューでは捕まえられず、本番負荷の下のフレームグラフでしか見えません。(同じ事例

on-CPU の根本原因を特定する方法は、サンプリングと CPU フレームグラフです。フレームグラフの読み方と実践的なテクニックは、弊社のフレームグラフ解説ガイドをご覧ください。

CPU が待機している(off-CPU の根本原因)

P99 は高いのに CPU 使用率が低い場合、時間は計算ではなく待機に費やされています——ロック競合、ディスク/ネットワーク I/O のブロック、上流・下流の依存の応答遅延、スケジューリングのキュー待ち(スレッドは準備完了なのにコアを取れない)などです。この種の問題は top では原因が見えず、CPU フレームグラフ上にも顕著な幅広いブロックは現れません。スレッドが CPU を離れている時間は on-CPU サンプリングには不可視であり、どれだけサンプリングしても「待機」そのものは捉えられないからです。

この種の根本原因がテールレイテンシーに与える打撃は、イベント駆動型のサーバーで特に大きくなります。単一の worker がブロックされると、その worker 上のすべての並行リクエストが同時に空しく待つことになります。ある本番環境の OpenResty アプリケーションでは、プロセスの off-CPU 時間の 99.8% が Lua コード内の io.popen 呼び出しとそのパイプ読み取りに追跡されました——同期 I/O がイベントループ全体を止めていたのです。非ブロッキングな API に切り替えたところ、単一コアのスループットは 126 RPS から 18,537 RPS へと向上しました。別の事例では、ブロックの深刻さが定量化されています。イベントループの 1 回の反復が最長 75 ミリ秒ブロックされており——この 75 ミリ秒が、その worker 上のすべての並行リクエストのレイテンシースパイクに直結していました。

この種の根本原因を診断するには off-CPU 分析が必要です。スレッドが CPU を離れている間の時間とコールスタックを測定し、on-CPU フレームグラフとちょうど補完し合います。後者は「CPU が何で忙しいか」に、前者は「プロセスが何を待っているか」に答えます。具体的な手法・ツール、および Perl、Go、Python、Nginx など様々なランタイムでの実際の根本原因の連鎖については、弊社の off-CPU 分析ガイドをご覧ください。

リクエスト分配の偏り(設定起因の根本原因)

reuseport が無効。 Nginx のリスニングポートで reuseport が有効になっていないと、リクエストを各 worker プロセスへ均等に分配できません——一部の worker が飽和し、残りはほぼアイドルとなり、飽和した側がテールレイテンシーを生みます。この種の根本原因はオペレーティングシステムのリクエスト分配層に隠れており、top の総 CPU 使用率には異常が見えません。設定をチューニングしたところ、全体性能が 20〜30% 向上しました

根本原因がサーバー側にない場合(クライアント)

P99 の元凶がサーバー側にまったくないこともあります。ある旅行業界のお客様は、一部の API リクエストに 200ms の余分なレイテンシーが乗っていることに気づきました。OpenResty XRay のインテリジェントなパケットキャプチャは、遅い TCP コネクションだけを狙って捕捉し、最終的にレイテンシーの発生源がクライアント側であることを突き止めます——同社の Android アプリが、リクエストヘッダーを送信した後、意図的に 200ms 遅延させてからリクエストボディを送信していたのです。サーバー側をどれだけ最適化しても、この問題は決して解決しません。

高い P99 レイテンシーの診断方法

冒頭のあの状況に戻りましょう。苦情は手元にあり、ダッシュボードは一面グリーン、再現には失敗している。このとき必要なのは、さらなる推測ではなく、まずレイテンシーがどこに費やされているかを突き止めることです。診断せずに最適化するのは、運任せの最適化です。 前述の D 言語事例がその格好の論拠です。チームはデータベース、外部依存、並行設定を順に調べましたが、すべて空振りでした——根本原因がサービス自身のランタイムの挙動にあった一方、あらゆる「経験則」が外部を指していたからです。

以下は、複数の本番事例で検証された診断の道筋です。

ステップ 1:テールが本物かを確認する

低トラフィックの下では、P99 は信頼できないことがあります——サンプル数が少なすぎて、1〜2 件の散発的な遅いリクエストが P99 を押し上げてしまうのです。観測している P99 のばらつきが、十分なリクエスト量の下で持続的に再現するものであって、統計ノイズではないことを確認してください。

ステップ 2:集約指標ではなく、遅いリクエストそのものを捕捉する

集約指標(平均、P95、エラー率)は「問題がある」ことは教えてくれますが、「問題がどこにあるか」は教えてくれません。具体的な遅いリクエストを捕捉する必要があります。それらはどの URI にヒットしたか? どのコードパスを通ったか? どのステップで最も時間を費やしたか? 前述のクライアントレイテンシー事例では、まさに遅いコネクションを狙った選択的なパケットキャプチャによって、200ms のレイテンシーがサーバー側の処理ではなくクライアントの送信挙動に正確に帰着されました。

ステップ 3:切り分け——忙しいのか、待っているのか?

このステップが、次にどの種類のツールを使うかを決めます。

観測結論使うべきツール
CPU 高 + P99 高時間は計算に費やされているCPU フレームグラフでホットな関数を特定。フレームグラフの読み方を参照
CPU 低 + P99 高時間は待機に費やされているoff-CPU 分析でブロック箇所を特定。off-CPU 分析ガイドを参照

ステップ 4:本番環境で関数・行レベルまで特定する

このステップで突き当たるのは、冒頭に挙げた 4 つの盲点のうち最も厄介なものです。根本原因は本番環境の実負荷の下でしか現れないため、関数レベル、さらにはコード行レベルの性能データが必要なのに、コードは変更できず、計装も追加できず、サービスも再起動できない——とりわけ金融のコアシステムにとって、これは越えられない一線です。従来の方法論はここで限界に達します。

OpenResty XRay は、非侵入型の動的トレーシングでこの問題を解決します。実行中のプロセスを直接サンプリングし、コード変更・計装の追加・サービス再起動を一切必要とせず、フレームグラフを自動生成して具体的な関数まで特定します。前述の 2 つの事例では、漠然とした「P99 がときどき基準を超える」が、まさにこの方法で、数分のうちに具体的な関数と具体的なパーセンテージへと還元されたのです。

P99 レイテンシーを下げる方法:見つけた問題を直す

診断の後、修正の方向は根本原因が決めます——チェックリストを一項目ずつ試すのではなく、フレームグラフやパケットキャプチャで見えた問題を直接直します。以下の表は、弊社が実事例で特定した根本原因、対応する修正、実測の効果をまとめたものです。

根本原因修正の方向実測の効果
① 正規表現バックトラック + ② 再コンパイル + ③ PCRE JIT の欠落非効率なマッチング関数を置き換える。コンパイルキャッシュを有効にする。イメージのビルドパラメータを修正する300ms のレイテンシースパイクを解消、CPU 約 30% 低下(診断の全過程
⑤ 保守的GC + ⑥ O(n) クエリホットパスのメモリ割り当てを削減。クエリのアルゴリズムを書き換えるP99 を 350ms から 95ms へ、73% の削減(特定と修正の全過程
④ keepalive の欠落 + ③ -O0 ビルドupstream keepalive を有効にする。最適化コンパイルを復旧するQPS を 6,301 から 21,923 へ、3.48 倍(分析の全過程
reuseport が無効すべてのリスニングポートで reuseport を有効にする全体性能が 20〜30% 向上(チューニングの全過程
同期 io.popen がイベントループをブロック(off-CPU)ブロッキングな呼び出し箇所を非ブロッキングな API で書き換える単一コアのスループットを 126 RPS から 18,537 RPS へ(フレームグラフによる特定
クライアントがリクエストボディの送信を遅延クライアントアプリの送信ロジックを修正する200ms のレイテンシーを解消(パケットキャプチャによる分析

上の表は結論にすぎません。各根本原因が、監視が一面グリーンのままいかに一歩ずつ特定され、修正後にいかに効果を検証されたか——フレームグラフをどう読むか、証拠の連鎖をどう閉じるか——その全過程は各事例の原文にあり、その部分こそが再利用できる方法です。

一つ重要な点に注意してください。最適化の各ステップの後は、必ず再サンプリングしなければなりません。フレームグラフは特定の負荷の下のスナップショットであり、最適化はシステムのホットスポット分布を変えます——D 言語事例では、GC の負荷が下がって初めて、それまで GC 停止に「かさ上げ」されていた他のホットスポットのサンプリング比率が、本来の重みに戻りました。最初のフレームグラフに依拠してその後のすべてのステップを計画するのは、よくある誤りです。

根本原因を見つけるのは高くつくが、修正はしばしば安い——1 行の keepalive ディレクティブ、1 つのビルドパラメータ、1 か所のキャッシュのスイッチ。これこそが、先の診断の道筋が存在する理由です。

まとめ

本記事全体を 4 つの文に圧縮します。

  • P99 はテール体験のものさしであり、最も遅い 1% のリクエストが実際に被るものを定量化する。
  • 平均値はテールを何も証明しない。レイテンシー分布は下限があって上限がなく、スパイクは平均を動かさない。
  • テールの根本原因は一本の軸に沿って 2 つに分かれる。CPU が誤った処理で忙しいか、CPU が待っているか。
  • 診断は最適化に先立つ。まずフレームグラフやパケットキャプチャで根本原因を見て、それから見えたその一つを直す。

よくある質問

良い P99 レイテンシーとは?

あらゆる場面に当てはまる単一の数値はありません。P99 の目標値は、SLO とビジネス上の文脈によります。ユーザー向けの主要なトランザクションパス(決済ゲートウェイなど)は通常はるかに厳しく、社内のバッチ処理ははるかに緩やかです。正しい判断の枠組みはこうです——現在のトラフィックの下で、P99 はエンドユーザー体験や上流・下流の SLA に対する約束を満たしているか? 満たしており、かつ安定しているなら、それが「良い」P99 です。

平均レイテンシーは正常なのに P99 が高いのはなぜですか?

レイテンシー分布は下限があって上限がないためです——少数の遅いリクエストは平均を動かさないのに、P99 を押し上げます。前述のフィンテックゲートウェイがその典型です。P50 は 10ms 未満、監視は一面グリーンなのに、最も遅い 1% は 300ms を超えていました。平均値はこの種のスパイクを統計ノイズへと薄めてしまったのです。

P99 レイテンシーは高いのに CPU 使用率が低いのはなぜですか?

CPU が高くないのにレイテンシーが高いのは、時間が計算ではなく待機に費やされていることを意味します——ロック競合、ディスク/ネットワーク I/O のブロック、上流・下流の依存の応答遅延、スケジューリングのキュー待ちがこれに当たります。この種の時間は CPU フレームグラフには不可視です。スレッドはすでに CPU を離れており、on-CPU サンプリングでは当然捉えられません。この種の問題を診断するには off-CPU 分析——スレッドが CPU を離れている間の時間とコールスタックの測定——が必要です。具体的な手法は、弊社の off-CPU 分析ガイドをご覧ください。

P99 レイテンシーとテールレイテンシーの違いは?

テールレイテンシー(tail latency)は総称であり、レイテンシー分布のテール——最も遅い一群のリクエスト——を指します。P99 はテールレイテンシーを定量化する一つの方法であり、99 パーセンタイルをテールを表すものさしとして取ります。P99.9 や P99.99 はより極端なテールレイテンシー指標です。実務では「P99 レイテンシー」と「テールレイテンシー」はしばしば互換的に使われますが、厳密には P99 は確定した統計量であり、テールレイテンシーは分布の形状に対する定性的な記述です。

本番環境で P99 レイテンシーを測定するには?

一般的な方法は 2 つあります。1 つは、アプリケーションまたはゲートウェイの層で各リクエストの応答時間を収集し、パーセンタイルを算出する方法です(大半の APM や監視システムが対応しています)。もう 1 つは、非侵入型の動的トレーシングツール(OpenResty XRay など)で実行中のプロセスを直接サンプリングする方法で、コード変更も計装も不要です。重要な違いはこうです——前者は P99 が「いくつか」を教えてくれ、後者はさらに P99 が「なぜ高いか」を教えてくれます。後者はレイテンシーを関数レベルのフレームグラフへと還元し、遅いリクエストの時間がどの関数、どのコード行に費やされたのかを見せてくれます。

OpenResty XRay について

OpenResty XRay動的トレーシング製品であり、実行中のアプリケーションを自動的に分析して、パフォーマンスの問題、動作の問題、セキュリティの脆弱性を解決し、実行可能な提案を提供いたします。基盤となる実装において、OpenResty XRay は弊社の Y 言語によって駆動され、Stap+、eBPF+、GDB、ODB など、様々な環境下で複数の異なるランタイムをサポートしております。

本記事がお役に立ちましたら、ぜひ本ブログサイトおよび弊社の YouTube チャンネルをご購読ください。ありがとうございます!

著者について

章亦春(Zhang Yichun)は、オープンソースの OpenResty® プロジェクトの創始者であり、OpenResty Inc. の CEO および創業者です。

章亦春(GitHub ID: agentzh)は中国江蘇省生まれで、現在は米国ベイエリアに在住しております。彼は中国における初期のオープンソース技術と文化の提唱者およびリーダーの一人であり、Cloudflare、Yahoo!、Alibaba など、国際的に有名なハイテク企業に勤務した経験があります。「エッジコンピューティング」、「動的トレーシング」、「機械プログラミング」 の先駆者であり、22 年以上のプログラミング経験と 16 年以上のオープンソース経験を持っております。世界中で 4000 万以上のドメイン名を持つユーザーを抱えるオープンソースプロジェクトのリーダーとして、彼は OpenResty® オープンソースプロジェクトをベースに、米国シリコンバレーの中心部にハイテク企業 OpenResty Inc. を設立いたしました。同社の主力製品である OpenResty XRay動的トレーシング技術を利用した非侵入型の障害分析および排除ツール)と OpenResty Edge(マイクロサービスおよび分散トラフィックに最適化された多機能ゲートウェイソフトウェア)は、世界中の多くの上場企業および大企業から高い評価を得ております。OpenResty 以外にも、章亦春は Linux カーネル、Nginx、LuaJITGDBSystemTapLLVM、Perl など、複数のオープンソースプロジェクトに累計 100 万行以上のコードを寄与し、60 以上のオープンソースソフトウェアライブラリを執筆しております。

翻訳

英語版の原文と日本語訳版(本文)をご用意しております。読者の皆様による他の言語への翻訳版も歓迎いたします。全文翻訳で省略がなければ、採用を検討させていただきます。心より感謝申し上げます!