フェルミ推定で解く Performance Tuning

実際の Web プログラマーの現場では、よく「なぜかパフォーマンスが遅い」という現象に悩まされる。その原因は様々である。

例えば、アプリケーション層の実装が起因でメモリを線形的 or 指数関数的に喰っているのが原因かもしれない。

ミドルウェア層の設定ミスが原因で大量の Disk I/O が発生していることが原因かもしれない。

TCP 層の設定が原因で、Linux Kernel のソケット通信の設定値がデフォルトのママであったことによって TCP Syn backlog があふれているのが原因かもしれない。

それとも、単にインフラチームによる Log Rotation の設定が漏れているだけかもしれない。

もしくは、Auto Scaling の設定に Maximum があることを知らずに設定したアプリケーションエンジニアのミスかも知れない。偶然にも、利用しているクラウドベンダーのサービスの一時的な障害が遠因となっていることもあるだろう。

そう思いたくはないが、別チームが取得している PV ログに暗黙的に依存したビジネスロジックがあり、そのログのパターンが変わったことにる集計ロジックの不整合が原因かもしれない。

はたまたサービス自体の成長が原因で、ある特定の地域からのリクエストが急激に増えたことによって、アプリケーションサーバーの Auto Scaling が間に合っていないのかもしれない。

そう、「なぜかパフォーマンスが遅い」というたった一つの現象が発現するのも、アプリケーション層からインターネット層から物理層のあらゆるレイヤーで、様々な原因が相互に・複雑に影響した結果なのである。

現実というのは複雑だ。

であるからこそ、その課題に対して、効率的に根本原因の把握及び妥当な解決策の実施を行わなくてはいけない。誰も一週間から一ヶ月もその調査に時間を割きたくはないだろう。それがビジネス上クリティカルな影響があるならなおさらだ。

「経験」が唯一の答えではない

あらゆる障害を体験してきた経験豊富なソフトウェアエンジニアであれば、少しは過去の知識を灯台のように道を照らしながら進んでいけるかもしれない。

しかし、この「過去の知識」というのもやっかいだ。

過去の "成功体験"(例:「Disk I/O がこのトレンドで跳ねているときは、確かデータベースエンジンが XXX だったときのあの設定値のミスによるものだなぁ」)があると、自然とその知識に優先順位を決定してしまい、調査に時間を費やしてしまう。

しかし、必ずしもそうではないのが現実だ。新たな課題が生じるたびに、まっさらな気持ちで原因特定に当たらないと、自分のみならず部下や同僚の調査時間まで無駄に使ってしまうかもしれない。

したがって、必要なのは「経験」ではなく、適切な「課題解決の手法」なのだ。

それがあれば、ジュニアエンジニアだろうがシニアエンジニアだろうが、金融業界だろうが不動産業界だろうが、Web アプリだろうが Linux Kernel エンジニアだろうが、効率的に課題を解決することができる。

逆に言うと、経験にしか頼れないようであるなら、自分のキャリアを狭めてしまう可能性がある。継続的な自己学習による柔軟性と弾力性のあるキャリアに必要なのものの資質のうちの一つは、基本となる課題解決力であろう。

フェルミ推定

フェルミ推定とは、論理的思考力のフレームワークの一種だ。もちろん、Performance Tuning 問題に対しても応用できる。

フェルミ推定で必要な、仮説思考力・フレームワーク思考力・抽象化思考力という3つの思考力を活かして、効率的な調査を実現するための手法を体得すること。

具体例

Web アプリケーションの Latency (p95) が何故か普段より数百 ms 遅くなっている とする。その原因を探りたい。

なお、ある程度現状が見えない環境でも、雑でもいいから仮設を立てて、落とし所を探しながら調査していくことが必須である。したがって、この思考実験では、その Web アプリケーションの実装言語が Ruby or Go なのかとか、フレームワークが Rails or Sinatra なのかとか、クラウドベンダーが AWS os GCP or Azure なのかといった詳細は、課題解決の手法を学ぶ上では本質ではない。

戦略

ここでは、『地頭力を鍛える 問題解決に活かす「フェルミ推定」』 で紹介されている以下の「3つの思考力」の切り口から、「Web アプリケーションの Latency (p95) が何故か普段より数百 ms 遅い」という課題を分解しながら、適切な調査軸や解決策を提案していく。

  • 4.1. 仮説思考力(結論から考える)
  • 4.2. フレームワーク思考力(全体から考える)
  • 4.3. 抽象化思考力(単純に考える)

これらの基本思考力を活かし、フェルミ推定のプロセスに当てはめていく。

Fermi Estimation

仮説思考力

仮説思考力とは、一言で言うなら「結論から考える」思考パターンのことである。

具体的には、以下のプロセスに分割できる。

  • 限られた情報の中から、確度の高い仮説を選定する
  • 仮説の検証を繰り返すながら、仮説の修正や情報の精度を向上していく
  • 上記のプロセスを繰り返しながら、最終結論に至る

フェルミ推定において仮説思考力を適用するにあたって、2つの注意点がある。

  • すべての情報を集めることを諦める(= 限られた情報からでも仮説を無理矢理でも導き出す)
  • 時間を決めてとにかく結論を出す

まず、「すべての情報を集めることを諦める」に関して、実際に稼働する Web アプリケーションのログや時系列メトリクスというのは、膨大だ。すべての情報を集めようとすると、調査時間もかかるし、それらを保存するインフラコストも跳ね上がる。更に悪いことに、情報を集めていくと「情報を集めて整理する」という手段自体が目的になってしまい、当初調査していた目的を忘れてしまったり、集めた情報の山を見て自己満足を感じて息が切れてしまう。

また、「時間を決めてとにかく結論を出す」という割り切りも必要だ。「まずは Google で検索してから」では、刻一刻と赤字を垂れ流す Web アプリケーションによる損失を最大化してしまう。時間をかければいつかは原因特定できるのは当たり前である。限られた時間の中で成果を出すからこそ難しいのである。そして、難しいからこそ、適切な手法を用いて再現性高く解決できるスキルのあるエンジニアの市場価値が高まるのは、至極当然である。

フレームワーク思考力

フレームワーク思考力とは、一言で言うなら「全体から考える」思考パターンのことである。

具体的には、以下のプロセスに分割できる。

  • まずは課題の全体像を俯瞰し、規模感や全体像を把握する
  • 次に、適切な切り口で全体像を分断する
    • 全体をもれなくダブり無く分類するために、3C/4P といった MECE なフレームワークを用いると良い
    • 良い切り口がわからない場合は、二分探索で大きい切り口から再帰的に調査していく優先順位付け手法も有用である
  • 各切り口を調査・検証可能な最小単位に分解する(=「因数分解」する)
  • 分解した最小単位それぞれについて、必要な精度で調査・計算・検証を行う
    • 有効数字の桁数を必要以上の精度で求めないことも大切。無駄な計算コストは割くべきではない。

抽象化思考力

抽象化思考力とは、一言で言うなら「単純に考える」思考パターンのことである。

具体的には、以下のプロセスに分割できる。

  • まずは、課題の本質的な資質・特徴を抽出して、「モデル化」する
  • 次に、抽象的なモデルに対する一般解を求める
  • 最後に、再び具体的な課題に落とし込み、個別解を求める

例えば、以下のような具体例があげられるであろう。

  • 「CTR の広告クリエイティブごとの最適配信比率を求める」という課題が合った時に、「線形計画問題 3」というモデルに抽象化した上で、前提条件・制約条件に適切な値をはてはめて公式を解く
  • 「禁止ワードを放送中動画のコメントリストから省く」という課題が合った時に、「ある特定の String 型をトライ木に存在するかどうかを Boolean フラグで判定する」というモデルに抽象化した上で、禁止ワードのブラックリストに載っていたコメントを除外するビジネスロジックを実装する
  • 「木からりんごが落ちるのはなぜか」という課題が合った時に、「一般相対性理論」というモデルに抽象化することで、木から落ちるりんごにかかる重力(のみならず、太陽系の惑星の軌道まで)を解くことができる

少し話は反れるが、いわゆるデータ構造やアルゴリズムが Computer Science の基礎として求められているのも、現実の課題をいかに抽象化して、基本的なデータ構造やアルゴリズムで解くことができるからであろう。

逆に言うならば、この「抽象化思考力」が不足している場合、どれだけデータ構造やアルゴリズムを記憶しても、実際の課題に適用できないとも言える。探索アルゴリズムの最小・最大・平均の Time Complexity および Memory Complexity を覚えるだけでは、どう実際の課題に適用していいかわからないのだ。

ユースケース例

では、実際に3つの思考力を活かして、「Web アプリケーションの Latency (p95) が何故か普段より数百 ms 遅くなっている 」という課題を分解していく。

アンチパターン

アプローチを開始する前に、陥っては危険なアンチパターンについて紹介しよう。

  • 5.1.1. いきなりデータを集め始める
  • 5.1.2. 分析のための分析をする
  • 5.1.3. 課題のインパクトを意識していない

いきなりデータを集め始める

もちろん、最低限のデータ(例:エラーが発生している、CPU がサチっている、Latency p95 が跳ねている)は見てもよいが、その後でいきなり "why CPU get saturated Ubuntu" とググり始めないようにしよう。

また、Brendan Gregg の提唱する USE Method 4 のような自分が信頼しているものであったり、チームで決めていたりする「まずはこれを埋めようチェックリスト」がある場合は、まずは最低限そこを埋めよう。逆に言うと、こういったチェックリストは「暗黙に考えていた優先順位に引っ張られて偏りのある調査をしてしまう」というリスクを避けるためのものであり、そのチェックリストの内容が有効である限り、その手法には従うべきである。

分析のための分析をする

全体のストーリーや調査のイメージがないまま、分析をするための分析をし始めていくのも危険信号だ。最終目的を忘れて、情報を集めること自体が目的になってしまったり、根本原因にたどり着き得る重要な切り口をあげないまま、別の切り口の調査に時間を使ってしまったりする。調査している内に、「自分が何をしているかわからなくなった」という状態になったら、このアンチパターンに陥っている証拠だ。

課題のインパクトを意識していない

課題が出てきた時に、まずはいきなり調査に入っていないだろうか?例えば、今週末リリースを控える、ビジネス上最重要な機能の実装の終盤を迎えていたとする。あなたがそのタスクにアサインされているとしよう。そこでその課題が発生した。当事者意識を発揮して調査を始めるのが良いが、半日使ってしまったことによってリリースが遅れてしまうかもしれない。しかも、結局課題を解決できなかった場合、自責の念も残ってしまう。そして、最悪なことに、実はその課題はビジネス上大したインパクトもなく、来週移行の修正でも間に合うものかもしれない。

あなたの所属する現場で、優先順位に決定権を持つのは誰だろうか?テックリードなどの現場のリーダーだろうか。ディレクターやプロダクトオーナーなどビジネス上重要なレポートラインにいる同僚だろうか。その新機能はあまりにも重要なので、部長や CTO 自身に情報を集めるプロジェクト型組織をとっているだろうか。自分自身にその決定権がない限り、当事者意識を発揮 "しすぎる" リスクも把握しておこう。

これは、言い換えるなら「調査・解決の期限をチームで把握する」ことにもつながる。障害発生時のエスカレーションフローや連絡手段が体系だっていれば救えるかもしれないが、可能な限り今解決しようとしている課題が、真に "課題" なのかどうか、見極める癖は忘れてはいけない。

アプローチ設定

まずは、「Web アプリケーションの Latency (p95) が何故か普段より数百 ms 遅くなっている 」という課題に対してどうアプローチしていけばいいのかを設定していこう。

仮説思考力を活用したアプローチ設定

この例では、仮説思考力をもとに、いくつかの仮説を洗い出していく。ここで重要なのは、「少ない情報からでも仮説を立てていく」ことである。情報収集をしないと仮設を立てられない、と考えるのではなく、実はすでに持っている何らかの情報を使おう、と発想を変えるところにポイントがある。

「Latency (p95) が遅くなっている」という情報のほか、記憶を探ってみると、実は以下のような事実が浮かび上がることがある。

  • [事実] Latency (p95) の増加を伝える Alerting が発火した
  • [事実] お昼前に、メンバーの一人が何らかのデプロイをしていた
  • [事実] 先週後半に、クライアント側でログ周りの変更がリリースされており、今週に入って新バージョンの市場浸透率があがってきている
  • [事実] Docker で動いているアプリケーションコンテナの base image に security patch があたっていた
  • [事実] 午前中に業務チームが「アプリの動作がもっさりしていることがいつもよりある気がする」と言っていた

ここから、例えば以下の仮説が思いつくであろう。

  • [仮説] お昼前のメンバーのデプロイの差分に、Latency 増加につながる何らかのビジネスロジックの変更が入っていた
  • [仮説] お昼前のメンバーのデプロイの差分に、Latency 増加につながるライブラリや 3rd party モジュールの変更が入っていた
  • [仮説] クライアント側のログ変更の実装にバグが有り、ログの流量が増えシステムが全体的に遅くなっている
  • [仮説] base image の security patch の差分に Latency 増加につながる何らかの変更が入っていた

また、「実は課題と思っていたものは課題ではなかった」という仮設の検証も行おう。可能性は低いかもしれないが、「猪突猛進」にならないために必要な視点でもある。

  • [仮説] グラフの time range をデフォルトの 1 hours から 24 hours や 2 days で見てみると、トレンドは実は特に変わっていない。つまり Latency の増加は普段のトレンド通り。
  • [仮説] Latency (p95) の増加を伝えるための Alerting System のしきい値の設定自体にミスが有り、気にするしきい値ではない
  • [仮説] Latency (p95) の増加を伝えるための Alerting System に何らかの不具合があり、時系列メトリクスが発生していないのに Alert が誤報してしまった

いきなり調査をし始めると、自分の仮説を裏付けるような事実ばかりを集めてしまう、認知心理学における「確証バイアス5」という状態に陥っている可能性もある。いきなり調査を始めずに、まずは仮説をあげるのも、こういったバイアスによる判断リスクを最小限に抑える目的もある。

フレームワーク思考力を活用したアプローチ設定

また、フレームワーク思考力を活かして、調査しようとしている Web アプリケーションの全体像について把握しておくのも忘れないでおこう。全体俯瞰の威力は、「思考の癖を取り払い、思い込みをなくす」ことにある。例えば、先程仮設思考力を利用して仮説を立てた時に、いくつかの事実を参考に仮設を立てた。しかし、必ず誰かしら思考の癖があるし、思い込みを完全になくすことはできない。

例えば、Alerting system が導入したばかりかもしれない。その事実があると、「実は Alerting system の不具合では?」という思い込みを無意識の内に持ってしまい、自然とその仮設の優先順位をあげて調査してしまう。

この場合、調査しようとしている Web アプリケーションの持つ性質や、利用している middleware からあげられる一般的な特性などを上げて、システム全体のアーキテクチャごとにボトルネックとなりうる箇所をリストアップするのが良いであろう。

例えば、典型的な Web Application として以下の構成をイメージしてみよう。

  • single-region で稼働する Web Application
  • RDBMS を主要なデータソースとし、ビジネスロジックを構築する際にほぼ毎回アクセスしている(e.g. MySQL)
  • in-memory KVS をRDBMS に対する aside-cache 型の cache store として利用している(e.g. memcached, Redis)
  • prefork 型の HTTP server を前段に利用している(e.g. Unicorn, Apache prefork mode)
  • 静的コンテンツは CDN を通じて配信している(e.g. Fastly, AWS Cloudfront, Akamai)

これらから、以下のような仮説が洗い出せるはずだ。

  • RDBMS を主要なデータソースとし、ビジネスロジックを構築する際にほぼ毎回アクセスしている(e.g. MySQL)
    • [仮説] JOIN を多用する SQL Query が大量に発行されている
    • [仮説] データ数の増加に伴って RDBMS の CPU が飽和している
    • [仮説] INDEX が貼られていない、データベースエンジンの特性を生かした設定が行われていない
  • in-memory KVS をRDBMS に対する aside-cache 型の cache store として利用している(e.g. memcached, Redis)
    • [仮説] 新しく追加した機能で RDBMS への結果をキャッシュするべきなのにキャッシュしていない
    • [仮説] Write-through 型の cache store なのに、考慮が足りずに書き込みがボトルネックになっている
    • [仮説] 単発の GET query は速いものの、操作数が大幅に増えた結果、トータルの Latency の増加の起因となっている
  • prefork 型の HTTP server を前段に利用している(e.g. Unicorn, Apache prefork mode)
    • [仮説] worker 数の設定値自体がデフォルトのままで、worker が枯渇している
    • [仮説] worker に割り当てる memory が足りていない

Performance Tuning の場合、USE method やチームで決めているチェックリストに沿って現状を確認する、全体像を掴むのもありだろう。これらのチェックリストも、思い込みに縛られず、CPU や memory の使用状況から Disk I/O、ネットワーク転送量など、幅広く全体像を捉えるための方法論である。

モデル分解

「アプローチ設定」のフェーズを通して、当たるべき仮説は洗い出せたはずだ。では次に、モデル分解を通じて、検証可能な最小単位への因数分解を行っていこう。

と、その前に、以下の観点について、再度確認しておこう。以下で述べた観点は、すでに今までの文章の中で提示してきた観点である。

  • その仮設は、本当に全体像を意識して仮説を洗い出せているだろうか?一歩引いて考えてみよう。
  • この時点で何らかの調査をしてしまってはいないだろうか?してしまったとして、その調査結果を裏付けるような仮説に肩入れしたり、情報を集めること自体が目的になっていないだろうか?
  • 仮説を洗い出す中で把握した影響インパクトを、タスクの優先順位に決定権を持つ適切な人物にレポートしているだろうか?当事者意識を発揮しすぎていないだろうか?

...

...

...

OK. では、これらの質問全てに「Yes」と答えられたなら、次のステップに進んでいこう。といっても、ここまでのフェーズで確かに仮説を洗い出せているのであれば、半分解決していることも多い。

例えば、最初に上げた以下の仮説を考えてみる。

[仮説] お昼前のメンバーのデプロイの差分に、Latency 増加につながる何らかのビジネスロジックの変更が入っていた

この仮説がなぜ妥当な検証候補に成りうるのか。まず、そもそも Latency が何故増加するのかを考えてみる。ここで、クライアントからリクエストが来たときの一連の流れを考えてみる。

client (iOS/Android/Web)
---> CDN
---> DNS
---> Reverse Proxy
---> HTTP servers (preforked)
---> in-memory KVS / RDBMS

この仮説は、ビジネスロジックに着目している。すなわち、「HTTP servers」の箇所がボトルネックであるとしているのだ。HTTP server が処理を返すまでの一連の流れを、更にざっくりと掘り下げてみよう。この過程が、すなわち「因数分解」と呼べる。

accept HTTP request GET
---> parse GET request parameters
---> query model A to RDBMS (via cache-store) and save them to an in-memory array
---> query model B JOINing C to RDBMS (via cache-store) and save them to an in-memory array
---> calculate some numbers by iterating those arrays using CPU
---> ...etc.

この処理のフェーズをどこまで深堀りするか・できるかは、調査者のそのアプリケーションへの理解度による。ここで重要なのは、ビジネスロジックの一行一行を見る必要はないし、むしろ見る時間を使ってはいけないことだ。あくまで情報を積み上げるのではなく、「仮説に仮説を重ねる」ように進めていく。

それらの一連の処理の中で、一般的な Web application であれば、以下の観点で怪しいところがないかという観点で見ていくことになるだろう。

  • CPU
  • memory, swapping
  • Disk I/O
  • deadlock
  • sleep
  • timeout

計算実行 / 現実性検証

アプローチを設定し、モデルを分解し、調査すべき最小単位まで因数分解できたら、後は実際に計算を実行するフェーズである。

例えば、先に上げた以下の仮説であれば、commit や Pull Requests の diff をベースに、因数分解したそれぞれの項目を念頭に置きながらコードを読んでいけばよいだろう。

[仮説] お昼前のメンバーのデプロイの差分に、Latency 増加につながる何らかのビジネスロジックの変更が入っていた

以下の仮説であれば、実際に time range を変更して確認するだけで、現実性検証はできる。注意としては、トレンドから判断する場合は、平常時のトレンドやアプリケーションの特性を理解していないと、気にするべきではないスパイクに引っかかってしまったり、見逃してはいけない波形を見逃してしまうことになる。できれば、グラフの診断については、そのアプリケーションのオーナーや、そのオーナーのメンターやリーダーであるようなシニアエンジニアにも意見を仰いでみるのが良いだろう。

[仮説] グラフの time range をデフォルトの 1 hours から 24 hours や 2 days で見てみると、トレンドは実は特に変わっていない。つまり Latency の増加は普段のトレンド通り。

以下の仮説であれば、SQL Query が Latency 増加につながるまでのモデルが因数分解できていることがまず前提だ。それがあれば、例えば Query Plan を見てみたり、JOIN しているレコード数を見てみたり、実際のデータを見に行ったりすることで、仮説を検証していく。

[仮説] JOIN を多用する SQL Query が大量に発行されている

繰り返しになるが、このフェーズでは、「初期の仮説に縛られすぎない」ことも重要である。また、現実性検証のフェーズでは、スキル不足や思い込みによる判断ミスのリスクも当然あるので、検証過程をこまめに記録することは可能な限り心がけると良い。

最後に

本記事で述べた一連の手法は、現場で成果を出しているエンジニアであれば、大なり小なり、または意識・無意識の内に適用している手法であろう。また、本記事を超えた概念や、トピックごとのローカルな最適解も数多く存在するであろう。更に、一般的な「課題解決」の手法として、Performance Tuning に限らず、プロジェクトマネジメントの場面、要件定義や基本設計の場面、外部とのコミュニケーションや部下育成の場面などにも、応用できるであろう。であるからにして、一連の手法自体完璧なものでもない。銀の弾丸では全く無い。

ここで、この課題解決の手法の見方を変えてみて見ると、「自分の過去の成功や事例を一回 Unlearn して、ゼロベースで考える力、課題に向き合う力」 とも言いかえられるだろう。この見方を再帰的に手法自体に適用して考えてみると、この手法自体を継続的に自己学習しながら、アップデートしていく必要がある。今回の記事で書いた時点の手法に "固執" することなく、常に他人や書籍からアップデートするヒントがないか、というスタンスで磨き込んでいく姿勢が、何よりも大事なのだ。

2022-07-23