パフォーマンス工学とプロファイリング
翻訳についての注記: 本ドキュメントは英語原文
11-observability/08-performance-engineering-and-profiling.mdを日本語に翻訳したものです。コードブロックおよびMermaidダイアグラムは原文のまま維持しています。
TL;DR
パフォーマンスの仕事は計測と算数であり、ライブラリコンテストではない。計測可能な主張は常に同じ形をしている: ワークロードW・到着率lambdaにおいて、エンドポイントEのレイテンシ分布は、利用率Uで、ハードウェアH上で、コストC(百万リクエストあたり)ならば予算Bの下に留まる。この形に言い直せないものは、コードに関する意見であり結果ではない。価値のほとんどは2つの規律が担う。誰かがコードを書く前に目標p99をホップ別の許容量へ変えるバジェット分解と、「遅い気がする」を名前付きのフレーム・システムコール・ロック・キューに割合付きで変えるサンプリングによる帰属だ。どちらも安価で、どちらも急いでいるときにはスキップされる。
手順は道具より重要だ。まず定常状態の証拠を確立する(今日動くシステムのログではなくプロファイルと、余裕がどれだけあるかを語る待ち行列の算数)、次に並列度を足すのではなく仕事を減らす、そしてレビューに耐える方法で再計測する。Amdahlの限界は、その最適化をそもそも予定する価値があるかを決める: サービス時間の8%を占める成分をどんなに巧みに書き直しても、8%以上は買えない。推論: 大半の本番サービスで、最も大きい勝ち筋はホットループではなく、境界をまたいだ重複の作業(バッチ化されていない呼び出し、インデックスなしの参照、再シリアライズ、回避可能な失敗のリトライ)にある。そしてそのすべては、コードに関する直感ではなく計測で見つける。
ワークロードと計測の契約
ワークロードの説明がないパフォーマンスの記事は逸話集だ。道具を選ぶ前に契約を書き留める。
| 項目 | 何を固定するか | なぜ答えを変えるか |
|---|---|---|
| ワークロード構成 | 読取/書込比、ペイロードサイズの分布、エンドポイントシェア | 書込パスのプロファイルは読取のファンアウトについて何も語らない |
| 到着プロセス | 平均レート、バースト性(変動係数)、季節性 | 固定した平均負荷でもレイテンシはバースト性の関数になる |
| 並列度の目標 | インスタンスごと・クライアントごとの実行中リクエスト数 | CPU律速か、キュー律速か、ロック律速かを決める |
| 目標分布 | p50、p99、そしてハードデッドライン。「平均」ではない | 平均は尾を隠す。ユーザーが離脱しタイムアウトが発火するのは尾だ |
| コストの許容範囲 | 百万リクエストあたりのUSD、または千RPSあたりのコア数 | すべての最適化をマネージャーが読める予算行に変える |
| 環境 | インスタンス種別、カーネル、ランタイム版、キャッシュ状態、リージョン | 環境を名乗らない結果は再現可能ではない |
議論を正直に保つ3つの定義がある。
- サービス時間はリクエストが発生させる仕事。応答時間はサービス時間に、その前にあるすべての待ちを加えたものだ。「コードが遅くなった」報告の大半は待ちであり、オンCPU時間だけをサンプリングするプロファイラには見えない
- 利用率は資源の容量を消費した割合で、飽和ではない。完璧なスケジューリング下のCPU使用率95%のノードにも待ち行列遅延はあり、ホットコア1つを持つ40%のノードにはない
- スループットの上限はレイテンシ目標を満たしながら持続できる値で、一度だけ生き延えたピークではない
本記事が扱うのは計測・帰属・バジェットの算数・ベンチマーク手法であり、目標設定(SLOの記事)、サービス間のリクエスト単位の因果帰属(トレーシングの記事)、フリートのサイズ設定(キャパシティプランニング)は扱わない。この3つの隣接の記事が、本記事が使う数字を供給する。
レイテンシ分布の解剖
リクエストのレイテンシは名前付き区間の総和であり、各区間が自分の分布と自分の障害挙動を持つ。
client think time
+ DNS resolution and connection setup (amortized by keep-alive)
+ TLS handshake (0-RTT or session resumption changes it)
+ edge/LB queueing (accept queue, worker pool wait)
+ application queue (Little's law decides how long)
+ dependency calls in series (each with its own tail)
+ fan-out to N dependencies (tail amplification: max of N draws)
+ serialization and compression (allocation-heavy, easy to shave)
+ garbage collection or runtime pauses (concurrent, but not free)尾のほとんどは3つの機構が生む。
ファンアウト増幅。 リクエストがN個の独立サービスに触れ、それぞれがp99にある確率1%なら、少なくとも1つが遅い確率は1 - (1 - 0.01)^Nで、N = 100なら約63%。「テイル・アット・スケール」の背後にある文書化された算数これであり、ファンアウトは多くの小さな尾を共通ケースの問題に変える。だから大きなサーバではなくヘッジとホップ別デッドラインが存在する。
飽和近傍の待ち行列。 利用率rho・平均サービス時間E[S]のM/M/1キューの期待遅延はE[S] / (1 - rho)。rho = 0.5でアイドル時の2倍、0.8で5倍、0.9で10倍。到着がバースト的或服务時間に重尾がある実サービスはM/M/1より悪い。Kingmanの近似はこの項に変動係数による(c_a^2 + c_s^2) / 2を乗じる。工学的な帰結は2つ: 到着を滑らかにする(バッチ化、リクエスト結合、ジッタ付きリトライ)ことは容量なしでレイテンシを買う。そしてサービス時間の平均を減らすことは、分散を減らすことと同等の価値がある。
割り込み。 ガベージコレクション、CPUスロットル、ページキャッシュミス、透明巨大ページの圧縮、ディスクライトバックは、すべてオンCPUプロファイルに見えない待ちを加える。Goのガベージコレクタは並行に動くがミューテータへアロケーション支援のコストを加える。JVMの既定の最大ポーズ目標(-XX:MaxGCPauseMillis)は200msで、100ms予算のエンドポイントでは災難だが、発火するまで誰もその既定値を読まない。文書化済み事実: LinuxのCFS帯域制御は固定周期(既定100ms)でクォータを補充するため、周期内にCPUクォータを使い切ったコンテナは次の周期までスケジューリングされない。だからCPU limitは小幅なスループット課税ではなくp99スパイクとして現れる。
測るのは総量ではなく区間だ。エンドツーエンドのヒストグラムは「問題がある」とだけ語る。キュー時間・依存先時間・シリアライズ時間・ポーズ時間の5つのヒストグラムが、どれかを教えてくれる。
サービシング予算のための待ち行列の数学
4つの公式で設計レビューの大半をカバーできる。すべてコードを書く前にスプレッドシートでできる算数だ。
Littleの法則: 並列数 = 到着率 x レイテンシ。 すべてのサイズを決める。2,000 RPS・40msなら実行中のリクエストはどこかに80本。1ワーカーが1本ずつ扱いサービス時間が8msなら、最低16人の稼働ワーカーにバースト性の余裕を足す。障害中にレイテンシが200msへ上がると、同じ2,000 RPSで400本の実行中を要求する — これが遅い依存先がプール枯渇になる経路だ。
利用率からレプリカ数。 replicas = ceil(arrival_rate x service_time / (concurrency_per_instance x target_utilization))。目標値は待ち行列曲線を踏まえて選ぶ: 0.7がバースト的なHTTPファンインでよく使われるのは、0.85だと2分の不均衡を吸収できないからだ。
Amdahlの限界。 speedup <= 1 / (1 - p)。pは除去できる総サービス時間のシェア。レイテンシの20%を占める成分の30%を削ると1 / 0.94 = 6.4%がエンドツーエンドの改善だ。この1行はどんなレビュー会議よりも多くの悪い提案を殺す。
デッドラインの伝播。 各ホップの許容量は呼び出し元の残り予算より小さくなければならず、設定したタイムアウトは依存先が計画に使うデッドラインになる。ゲートウェイの予算が150msで自身に20msを使うなら、下流には合計130msが配られる。そのうち3つが並列で各々ファンアウトを持つなら、それぞれにサブバジェットとヘッジ余裕が必要だ。呼び出し元が諦めた後も動き続けるリクエストは、誰も読まないレスポンスのために容量を消費する。明示的に課金し、デッドラインを超えたら破棄せよ。
p99目標120msの検索APIの実例:
| 区間 | 現在のp99 | シェア | 対処 | 改善後の想定p99 |
|---|---|---|---|---|
| エッジキューとTLS | 6ms | 5% | keep-alive再利用、コネクションプール | 6ms |
| アプリケーションキュー | 34ms | 28% | インスタンス並列度を上げる、rho > 0.7でレプリカ追加 | 12ms |
| ランキングサービス(ファンアウト12) | 41ms | 34% | p90でヘッジ、ホットクエリをキャッシュ | 26ms |
| ドキュメント取得 | 18ms | 15% | N+1をmulti-get 1本にバッチ化 | 9ms |
| シリアライズと圧縮 | 14ms | 12% | 未使用フィールドをスキーマで落とす | 7ms |
| ランタイムポーズ(GC) | 7ms | 6% | リクエストあたりのアロケーション率を下げる | 5ms |
参照設計: 合計は約120msから約65msへ下がり、その半分弱が容量の変更ではなく2箇所の重複した仕事を消すことで生まれる。きみのシステムで最初に確認すべき行はキューの行だ。成長とともにそれ自体が悪くなる唯一の行だからだ。
質問別プロファイリング道具箱
道具は入手可能性ではなく質問で選ぶ。各行が、そのプロファイルが答える障害モードと答えられない障害モードを名乗る。
| 質問 | 収集するもの | 見逃しうるもの |
|---|---|---|
| CPU時間はどこへ行く? | 100Hz以上のサンプリングCPUプロファイラ。読むためのフレイムグラフ、回帰のための差分プロファイル | オフCPUのすべて: 待ち、ロック、スロットル |
| リクエストは何にブロックされている? | ウォールクロック/レイテンシプロファイル、オフCPUとスケジューリング遅延の分析 | ブロックしないが遅い仕事 |
| 誰がメモリを確保している? | ヒープのアロケーションサイト・サンプリング。リークにはライブヒープ(in-use)プロファイル | 単一の長命参照に保持されたオブジェクトは、所有者でなく確保元のようになる |
| 競合はある? | ミュテックス/ロック競合のプロファイリング、ランキュー遅延 | より高い並列度でしか出ない競合 |
| メモリやキャッシュの無駄動き? | ハードカウンタ: IPC、LLCミス、TLBミス、stalled-cycles | カウンタが使えない共有/仮想化ホスト |
| システムコールパスのコストは? | 回数のヒストグラムと遅延、block-IOとネットワーク再送カウンタ | それらのアプリレベルの意味 |
| 特定のリクエストが遅いのは? | まれなトレースが残るレートでサンプリングしたトレース | 集約コスト。トレーシングは算数には疎すぎる |
| フリートで何が変わった? | サービスとバージョン単位の常時プロファイル。週単位で保持 | 根本的な見逃しはない。雑に運用すると高い |
文書化済み事実: Googleは本番ジョブ全体の常時プロファイリングをデータセンター総資源の2%未満で実施したと報告している(SIGMETRICS 2021、"Profiling a Datacenter")。プロファイリングは障害中の活動でなければならないという議論を終らせた数字だ。常時プロファイルこそが、「誰も見ていなかった火曜日のデプロイでフリートの4%のコアを奪ったサービス」を見つける方法である。
サンプリングのセマンティクスは、賢い人を誤誘導するので1段落分の価値がある。解像度10msのCPUプロファイルはサンプルをスタックの最上部のフレームに割り当てるため、インライン化された関数とテールコールは隣へ仕事を移す。だからこそプロファイラはフレームポインタかアンワインド情報を必要とする。文書化済み事実: Goは1.24でlinux/amd64とlinux/arm64のフレームポインタを既定有効にし、DWARFデコードなしにカーネルベース(eBPF)でのGoスタック追跡を実用にした。アロケーションプロファイルは補正しない限り確保サイトへ偏る: Kバイトごとに1回サンプリングするのは「どこで作られたか」を語り、「いま生きている何」ではない。リークの所有者を特定するのは、支配木か保持解析を伴うライブヒープ・プロファイリングだけだ。
プロファイルを自分に嘘をつかず読む
間違った結論の大半は6つの過ちから生まれる。
- フレイムグラフをタイムラインとして読む。 幅はサンプルのシェアで、横位置は時系列ではなくソート順。広いフレームは「このサブツリーが無数のサンプルでCPUを保持していた」を意味するが、それは1回の遅い呼び出しでも百万回の安い呼び出しでもある。関数で分割して見分める
- ワークロードが違うプロファイルを比較する。 キャッシュフラッシュ後のプロファイルは別のプログラムだ。同じ混合・同じ並列度・同じキャッシュ状態で比べる
- コストを呼び出し元に帰属させる。
Serialize()がサンプルの30%で、その半分の呼び出しが誤った二重エンコードによるものなら、修正は見ているフレームより上流にある。割合だけでなく呼び出し数を数える - 系を攪乱するプロファイラのプロファイルを信じる。 一部ランタイムのロックプロファイリングはアロケーション挙動を変え、デバッグビルドはインライン化を変える。すべての数字の隣にビルドフラグを書く
- マシンを無視する。 CPU律速に見えるサービスは共有依存先を待っているかもしれず、1コア100%のノードはノードレベルではアイドルに見える。見える資源ではなく疑った資源をプロファイルする
- p50パスの1本のプロファイルから結論する。 遅いリクエストはたいてい別のコードパス(キャッシュミス、リトライ、コールドなプランキャッシュ)を通る。プロファイラを遅いコホートに絞り込む。リクエストクラス別のレイテンシプロファイリングこそが、99%の説明ではなく1%の発見の方法だ
回帰には差分プロファイルを使う。バージョンAのサンプルからBのサンプルを引き、差分プロットを読む。絶対値のプロファイルはコードについて議論する。差分は出荷された変更を指す。
レビューに耐えるベンチマーク手法
ベンチマークは仮説・対照・環境の明記を持つ実験である。結果を信じられるものにするチェックリスト:
- コンパイラに仕事を消させない。 最適化コンパイラは不要な代入を削り定数畳み込みする。計測された仕事が実際に走るよう、ランタイムには不透明なシンク(
Blackhole風のバリア)が必要だ - 意図的にウォームアップする。 JITのティアリング、プランキャッシュ、コネクションプール、ページキャッシュはすべて落ち着くまで数秒かかる。結果だけでなくウォームアップ方針を報告する
- コアを隔離するかランダム化する。 周波数スケーリング、ターボバジェット、騒がしい隣接で10-30%の振れが出る。スレッドをピンし、単一スレッドの仕事ならSMTを無効にするか、広い信頼区間を受け入れる
- レイテンシは分布、スループット上限は最小値と中央値で報告する。 平均は尾を隠す。閉ループ負荷テストの中央値はその以上に尾を過小評価する
- 協調的省略(coordinated omission)を避ける。 負荷生成器がレスポンスを待ってから次を送るなら、システムが遅いまさにその瞬間に自らの到着率を落とす。最も悪いサンプルがヒストグラムから消えるのだ。到着は計画どおりに保ち、キュー時間をレイテンシとして数える
- 目視でなく統計的検定で比較する。 同じハードウェアで交互にA/B/Aを実行し、順位ベースかブートストラップで分布を比べれば、1組の実行では隠れる2%の回帰を検出できる
- 主張する前に本番で確認する。 エンドツーエンドの主張は実トラフィック下の同一ハードウェアのカナリアを必要とする。ステージングはキャッシュ状態・コネクション再利用・依存先の尾をまず再現しない
最適化の順序と各層のコスト
最適化には自然なコスト順があり、リストを飛ばすのはほぼ常に誤りだ。
- 仕事を消す。 結果をキャッシュし、呼び出しをバッチ化し、書込を結合し、未使用フィールドを落とし、永続的エラーでも発火するリトライを外す。レバレッジ最高・リスク最低・新しい機構ゼロ
- 計算量と局所性を改善する。 スキャンではなくインデックス、working setの縮小、連続走査、内部ループでのハッシュ参照回避、プリフェッチャに仕事を与えるポインタグラフより配列
- データの動きを減らす。 コピー削減、バッファリングでなくストリーミング、ペイロード別の圧縮調整、システムコールが支配的な場所でのゼロコピー(
sendfile、splice、io_uring) - 高価なオブジェクトに境界を設けて再利用する。 コネクションプール、バッファ、プリペアドステートメント、コンパイル済みプラン。プールのサイズ設定は待ち行列の問題: 小さすぎれば待ちが生まれ、大きすぎればキューは依存先の中へ引っ越す
- 慎重に並列度を足す。 並列化は共有の限界(ロック、単一書込シェード、メモリ帯域)にぶつかるまでスループットを掛算し、その後は競合を掛算する。ユニバーサルスケーリング則の変曲点はNUMAマシンと共有キャッシュで予想より早くやってくる
- ランタイムかハードウェアを変える。 SIMD、圧縮アクセラレータ、バッチ推論のGPU、数値カーネルのためのコンパイル言語。実在し、高価で、最後: 移行コストは四半期単位で測られ、1〜4段階が今四半期の勝ちの大半を返すことが多い
5段階を正直に保つルールが2つ。計測していないロックを共有する仕事を並列化しないこと。下流の依存先がすでに飽和近いシステムへ並列度を足さないこと — 相手のレイテンシを自分の障害に変換するだけだ(バックプレッシャの記事を参照)。
CIと本番での回帰防御
パフォーマンスは退屈な理由で劣化する: 追加されたフィールド、新しいミドルウェア層、マイグレーションで落ちたインデックス、アロケーションを開始した依存先。コストに見合う防御は以下。
- 狭く決定論的なベンチマークスイート。サービス時間を支配する上位3エンドポイントと2つのデータ構造をカバーし、固定ハードウェアと固定入力コーパスで実行。中央値とスループットをブートストラップ信頼区間でゲートし、単発でなくトレンドでアラートする
- リリースごとの差分フレイムグラフ。標準的な負荷テストからパイプラインで生成する。レビュアーが尋ねずに「どの幅が増えたか」見えるようにする
- テストとしてのアロケーション予算。 リクエストあたりのアロケーションが倍になったパスは、今日のレイテンシテストを通り、次四半期のコストモデルで落ちる。安定で計測が安いリクエストあたりバイト数をアサートする
- バージョンごとに保持される常時プロファイル。 障害調査が「先週同時刻と比較する」から始まり、ゼロから始まらないようにする
- リポジトリ内のレイテンシ予算ツリー。CIが検証できるチェックインされたファイルとして保ち、各スパンの実測p99が許容量に収まることを確かめる。超えたらテスト失敗と同じくビルドを落とす
容量とコストのモデル
パフォーマンスの仕事は意思決定を変えるときだけ価値を持つ。だから価格をつける。1リージョン2,000 RPS・平均サービス時間40ms・インスタンスあたり8 vCPU・目標利用率0.7のサービスをとる。
- Littleの法則から実行中の並列数:
2,000 x 0.040 = 80リクエスト - 1インスタンスがレイテンシ目標下で50の本をさばけるなら、必要レプリカは
ceil(80 / (50 x 0.7)) = 3。不均衡かAZ喪失にもう1台足す - vCPU時USD 0.10での百万リクエストあたりコスト:
(4 x 8 vCPU x USD 0.10) / (2,000 x 3,600)は約USD 0.00044/リクエスト、つまり百万件あたり約USD 444 - 平均サービス時間を25%削って30msにすると並列数は60へ、レプリカは余裕込みで3台へ、百万件あたりコストは約4分の1下がる — ただし削った仕事が実CPUでありキュー時間ではないなら
この最後の仮定こそがパフォーマンス会計の誤りやすい箇所だ。推論: サービス時間の削減は、利用率が制約要因だったときにだけレプリカ減少へ変わる。待ち時間の削減は同じフリートサイズでのより良いレイテンシへ変わる。どちらを買ったのか明示すること。買い物は違い、片方だけが請求書に現れる。
セキュリティ、プライバシー、マルチリージョンの注意
- プロファイルは機微な成果物だ。ヒープダンプにはトークン・鍵・顧客行が入る。アロケーションのスタックと値を捕まえる常時プロファイラは運用詳細ではなくデータ保持の決定である。本番データと同じアクセス制御で保存し、定期削除する
- サンプリングは機密性の表面を変える: プロファイル内のシンボル名は内部エンドポイントや機能フラグを暴く。プロファイルファイルは社内限定の成果物として扱う
- 「実コストを測る」ために認証・認可チェックを無効化しない。チェックを有効にした状態で計測し、チェックを最適化する(トークンキャッシュ、対称検証、決定のキャッシュ)。セキュリティ制御は計測対象のパス上に残す
- マルチリージョンは最適化できない下限を加える: リージョン間ラウンドトリップは最低でも数十ミリ秒であり、リージョン間で同期ファンアウトするアーキテクチャのレイテンシ予算は物理で決まる。コードを調整するより往復を減らす(マルチリージョンアーキテクチャを参照)
失敗トレース
ワークロードではなくlimitがp99を決めた。 Javaサービスのp99が100ms周期で揺れ、CPUは35%。コンテナのCPUクォータは平均需要サイズで、最も忙しいスレッドは周期境界ごとにスロットルされていた。limitを外す(バースト必要量より上に上げる)と尾は平坦になり、利用率はほとんど動かない。教訓: 周期的な時計を持つp99スパイクはコードの話ではなくスケジューリングの話だ。
最適化は本物で、そして無関係だった。 JSONシリアライズの書き直しが12msから3msへ下がり、エンドツーエンドのp99は2ミリ秒しか動かなかった。シリアライズはパスの12%で、ランキングのファンアウトが支配的だった。教訓: 作業を予定する前にAmdahlの限界を計算し、成分ではなくエンドツーエンドの分布を測る。
協調的省略がバグを隠した。 負荷テストはp99 80ms・エラーゼロと報告し、本番は同じビルドで1,400ms。負荷生成器は閉ループで、遅い依存先に詰まったワーカーはリクエストを出すのをやめていた — まさに停滞を示すサンプルを消していたのだ。教訓: 開ループの到着生成とキュー時間の会計。それができなければ尾の主張をするな。
アロケーション回帰はレビューを通過した。 新しい観測性ミドルウェアがリクエストごとにマップを1つ確保する。現在の負荷ではレイテンシは無変化、メモリ帯域は40%増、そして2四半期後、サービスは通常のトラフィックスパイクをOOM killなしに吸収できない。教訓: リクエストあたりバイト数のCIアサートが、レイテンシゲートが見逃すものを捕まえる。
障害中のプロファイリングは悪化させる。 エンジニアが飽和したサービスの全レプリカへロックプロファイリングエージェントをアタッチする。追加オーバーヘッドとフルヒープスキャンで再起動の嵐となりSEV1になる。教訓: 1インスタンスだけ低解像度でプロファイルするか、先週保持しておいた常時プロファイルを読む。
検証戦略
最適化は「それを書いていない懐疑論者を説得できる証拠」が揃ったときに終わる。
- Before: 現バージョンの常時プロファイルと、動かすつもり予算ツリーの行。そこに効果予言する算数(Amdahlのシェア、待ち行列項、アロケーション差分)を添える
- Reproduce: 最適化する指標で本番の形状を10%以内に再現するベンチマーク。環境は固定しPRに記録する
- Compare: 宣言した効果量で「差なし」を棄却できるまで交互A/B/Aを実行し、順位ベースの検定にかける。p50、p99、スループット、リクエストあたりCPU、リクエストあたり確保バイト数を報告する
- Canary: エラーバジェット・ゲートの下、実トラフィックで同一ハードウェアのカナリア。予算ツリーの行が動き、他の行が悪化していないことを確認する(よくある失敗はコストを共有依存先へ移すこと)
- Soak: 最低1つの完全なトラフィックサイクル。GCの断片化、コネクションプールのドリフト、キャッシュウォームアップは数時間スケールの効果を隠す
- Attach: 差分フレイムグラフ、前後のヒストグラム、更新した予算ファイル。次のエンジニアは結論だけでなく方法も引き継ぐ
判断の枠組み
| 状況 | 最初の一手 | 次の手 | やめる条件 |
|---|---|---|---|
| ユーザーが遅いと訴え、指標は違反せず | リクエストクラス別の遅いコホートのレイテンシプロファイル | キュー時間と依存先の尾を確認 | 動いた区間に名前を付けられたら |
| p99が今SLOを割っている | 余分な仕事を止める: ロードシェディング、任意のファンアウト切断、安全限界内で並列度引き上げ | その後にプロファイル。ページ対応中に最適化しない | エラーバジェットの燃焼が止まったら |
| リクエストあたりコストが高すぎる | フリート規模でアロケーションとCPUプロファイルをとり、サービスコストシェア順に並べる | リクエストあたりコア数が最悪の仕事を消す | 百万リクエストあたりコア数が測定可能に下がったら |
| 機能が予算に収まらない | 予算ツリー: どの行が吸収できるか | 追加呼び出しをバッチ化またはキャッシュ | すべての行が余裕を持って収まる |
| デプロイ後の回帰 | 先週同時刻のプロファイルとの差分プロファイル | 推測でなく設定フラグでビセクト | 1つの変更が差分を説明したら |
| ベンチは絶好調、本番は平坦 | ベンチが本番のパスを実行しているか(キャッシュ状態、ペイロードサイズ、並列度) | 実際のパスに計装を入れる | 両者が10%以内で一致したら |
主要な要点
- プロファイルなく最適化なし。分布と環境のないパフォーマンスの主張は、コードについての好みである
- 容量を買う前に仕事を消す。そして予定する前にAmdahlの限界を確認する — 賢い書き直しの天井はサービス時間に占めるシェアだ
- 尾はファンアウト・飽和近傍の待ち行列・割り込みから来る。だからCPUだけでなくキュー時間とポーズを測る
- Littleの法則はレイテンシ回帰を容量障害に変える。プールとレプリカは
lambda x Wにバーストの余裕を足してでサイズし、レイテンシを変えるたびに再確認する - 低一桁%のオーバーヘッドでの常時プロファイリングは十分に安くなった。「再現できなかった」は今や限界ではなくプロセスの欠陥だ
- 閉ループの負荷テストは最も悪いサンプルを消す。到着を計画どおりに保てないならp99を出すな
- すべての結果をコア数と百万リクエストあたりバイト数で価格付けし、サービス時間(金)を買ったのか待ち時間(レイテンシ)を買ったのか述べよ
- 予算ツリーと、予算が破れたとき失敗するベンチマークを残す。それでwinは次の6ヶ月のコミットを生き延える
参考文献
- Dean, G., and Barroso, L. A. "The Tail at Scale." Communications of the ACM 56(2), 2013 — ファンアウトによる尾の増幅、ヘッジドリクエスト、デッドライン伝播
- Cummins, J., et al. "Profiling a Datacenter." SIGMETRICS 2021 — データセンター総資源の2%未満で実施されたフリート規模の常時プロファイリング
- Gregg, B. Systems Performance: Enterprise and the Cloud, 2nd ed. Addison-Wesley, 2020 — サービス時間と待ち時間、オフCPU解析、USEメソッド
- Tene, G. "How NOT to Measure Latency." Strange Loop 2015 — 協調的省略、パーセンタイルの誤用、HDRヒストグラム
- Kingman, J. F. C. "On Queues in Heavy Traffic." 1962 — 分散でスケールする待ち行列遅延の背後の拡散近似
- Goプロジェクトドキュメント:
runtime/pprof、GCチューニングガイド、フレームポインタ既定有効化のGo 1.24リリースノート - Linuxカーネルドキュメント: Control Group v2(cpu.maxの帯域制御周期とクォータ)、block/IO会計