ボトルネックの再現と除去
このラボは本物のVM上で動きます
この箱はPodではなく、KubeVirtが起動した仮想マシンです。Linuxカーネルが別に動き、systemdが実際にサービスを管理し、dockerは模倣ではなく本物のDockerエンジンです。docker runで起動したコンテナは実際にプロセスになり、docker execもdocker logsもそのまま動作します。
以前は、このラボはPodの中で動いていました。カーネルの権限をすべて下ろした箱なので、コンテナを起動するステップが塞がれていて、そのためイメージのアーカイブを直接展開してみるという回り道で学んでいました。もう回り道は必要ありません。
知っておくことが2つあります。
- 最初の起動に1分ほどかかります。VMが起動してDockerをインストールするためです。Podのラボ(通常40秒)より遅くなります。
- ブラウザのプレビューはありません。VMに入ってくる接続は、採点用のポート1つだけが開いています。Webサーバーを起動したなら、VMの中で
curlで確認してください。
目標
ボトルネックをわざと作って再現し、階層ごとの仮説に絞り込み、取り除いたあと、改善幅を数字で確認して、目標のトラフィックを処理できるインスタンス数まで計算します。成果物は/root/lt3/の下に置きます。
なぜ重要なのか
ボトルネックを探すことは、勘ではなく、ラダーを順番にたどる作業です。ソケットバッファー → スレッドプールのキュー → コネクションプール待ち → DBのロック待ち → 処理。このラボが作るボトルネックは、そのうち最も手前の、つまり「一度に1つしか処理しない」という同時実行の制約です。このとき現れるサインが重要です。同時実行数を10倍に上げても、スループットはほぼそのままで、レイテンシだけが10倍に増えます。スループットが詰まると、代わりに待ち時間が増えるというこの関係を目で見たあとは、プロダクションでConnection is not available, request timed out after 30000msのようなログに出会ったとき、何を先に見るべきかがわかります。そして最後のステップで学ぶのは、測定値をそのまま使わないという原則です。測定された最大スループットの70%だけを使い、残りはヘッドルームとして残します。
準備
mkdir -p /root/lt3を実行しておいてください。使えるイメージは、あらかじめ取得してあるpython:3.12-alpine、nginx:1.27-alpine、alpine:3.20、busybox:1.36だけです。
ステップ
/root/lt3/slow.pyを作成します。条件は、標準ライブラリhttp.serverのHTTPServerを使う(一度に1つずつ処理する)、各リクエストでtime.sleep(0.05)により50msの遅延を与える、レスポンス本文にslow-appという文字列を含める、の3つです。このスクリプトをpython:3.12-alpineイメージで実行して、コンテナ名lt-slow、ホストポート127.0.0.1:8086で起動します。curl http://127.0.0.1:8086/のレスポンスに、slow-appが見える必要があります。- 同時実行数1で測定して、
/root/lt3/slow-c1.txtに保存します。正常なら、p95は0.04秒以上、スループットは40 RPS以下と出ます(50msの遅延を直列に処理するので、理論上は約20 RPS)。 - 同時実行数10で測定して、
/root/lt3/slow-c10.txtに保存します。確認する点: スループットが同時実行数1に対して2倍未満(ほぼそのまま)、p95は2倍以上に増加。スループットが詰まると、代わりに待ち時間が増えます。 /root/lt3/hypothesis.mdに、ボトルネックの候補を3つ以上、-または1.で始まる項目として書きます。候補は互いに異なる階層である必要があります(同時実行/スレッド/ワーカー、CPU、コネクションプール、ロック競合、IO・ネットワークのうち、最低3つの階層)。各候補について、どう確認するかも一緒に書いてください。/root/lt3/fast.pyを作成します。条件は、ThreadingHTTPServer(またはThreadingMixIn)で同時処理ができる、遅延50msはそのまま維持する、レスポンス本文にfast-appを含める、の3つです。コンテナ名lt-fast、ホストポート127.0.0.1:8087で起動します。同時実行数10で測定して/root/lt3/fast-c10.txtに保存し、スループットがslow-c10.txtに対して3倍以上である必要があります。- fastアプリを同時実行数1でも測定して
/root/lt3/fast-c1.txtを作成したあと、/root/lt3/compare.csvに4行をまとめます。形式はapp,concurrency,rps,p95で、行はslow,1、slow,10、fast,1、fast,10です。値は、各結果ファイルから読んだものである必要があります。 /root/lt3/capacity.txtに、4行を書きます。target_rps=300(目標のピークトラフィック)measured_rps=:compare.csvのfast/10行のrpssafe_rps=: 測定値の70%、小数点以下切り捨て(整数)instances=: ceil(300 ÷ safe_rps)、つまり切り上げた整数
/root/lt3/report.mdに、レポートを書きます。4つのセクションが必要です。ボトルネックの原因、証拠(測定値)、対処、容量の結論。本文に、改善前のスループット、改善後のスループット、必要なインスタンス数の3つの数字が、そのまま入っている必要があります。
参考
- コンテナの実行例:
docker run -d --name lt-slow -p 127.0.0.1:8086:8080 -v /root/lt3:/app python:3.12-alpine python /app/slow.py(スクリプトの中では、コンテナ内部の8080にバインドします)。 - サーバーが起動しなければ、
docker logs lt-slowで確認してください。PythonのハンドラーでContent-Lengthヘッダーを忘れると、クライアントが接続の終了を待って、測定が歪みます。 - 測定の例:
hey -n 100 -c 1 http://127.0.0.1:8086/ > /root/lt3/slow-c1.txt 2>&1、hey -n 200 -c 10 ...、fastは速いので、-n 1000 -c 10くらいがよいでしょう。 - 切り上げの計算:
awk -v t=300 -v s="$SAFE" 'BEGIN{n=t/s; r=int(n); if(n>r) r=r+1; print r}'。 - よくある間違い1: 改善を「レイテンシを減らすこと」と理解してしまうこと。レイテンシ50msはそのままにして、待ちをなくす必要があります。レイテンシはそのままで、スループットだけが大きく増えるのが正解です。
- よくある間違い2:
safe_rpsを四捨五入してしまうこと。切り捨てである必要があり、逆にinstancesは切り上げである必要があります。 - よくある間違い3: ステップ8のレポートに、「スループットが大きく増えた」のような文だけを書いてしまうこと。根拠は数字である必要があります。
遅いアプリケーションを起動する
/root/lt3/slow.pyを作成します。条件は、標準ライブラリhttp.serverのHTTPServerを使う(一度に1つずつ処理する)、各リクエストでtime.sleep(0.05)により50msの遅延を与える、レスポンス本文にslow-appという文字列を含める、の3つです。このスクリプトをpython:3.12-alpineイメージで実行して、コンテナ名lt-slow、ホストポート127.0.0.1:8086で起動します。curl http://127.0.0.1:8086/のレスポンスに、slow-appが見える必要があります。
/root/lt3/slow.pyは、リクエストごとに人為的な遅延を入れて、一度に1つずつしか処理しないサーバーである必要があります。レスポンス本文にslow-appという文字列が入っている必要があり、コンテナ名はlt-slow、ホストポートは8086です。
同時実行数1で基準を測定する
同時実行数1で測定して、/root/lt3/slow-c1.txtに保存します。正常なら、p95は0.04秒以上、スループットは40 RPS以下と出ます(50msの遅延を直列に処理するので、理論上は約20 RPS)。
/root/lt3/slow-c1.txtに保存します。50msの遅延を入れたなら、p95は最低でも40ms以上、スループットは1秒あたり20件前後が出れば正常です。値がおかしければ、対象のポートから確認してください。
同時実行数を10倍に上げてみる
同時実行数10で測定して、/root/lt3/slow-c10.txtに保存します。確認する点: スループットが同時実行数1に対して2倍未満(ほぼそのまま)、p95は2倍以上に増加。スループットが詰まると、代わりに待ち時間が増えます。
/root/lt3/slow-c10.txtに保存します。直列処理なら、スループットはほぼそのままで、代わりに待ち時間が増えます。この2つが同時に観察されれば、ボトルネックが同時実行にあるというサインです。
ボトルネックの仮説を立てる
/root/lt3/hypothesis.mdに、ボトルネックの候補を3つ以上、-または1.で始まる項目として書きます。候補は互いに異なる階層である必要があります(同時実行/スレッド/ワーカー、CPU、コネクションプール、ロック競合、IO・ネットワークのうち、最低3つの階層)。各候補について、どう確認するかも一緒に書いてください。
/root/lt3/hypothesis.mdに、互いに異なる階層の候補3つと、それぞれの確認方法を書きます。リクエストの処理経路のキューを順番にたどれば、候補が重なりません。確認方法のない仮説は、仮説ではありません。
同時処理に変える
/root/lt3/fast.pyを作成します。条件は、ThreadingHTTPServer(またはThreadingMixIn)で同時処理ができる、遅延50msはそのまま維持する、レスポンス本文にfast-appを含める、の3つです。コンテナ名lt-fast、ホストポート127.0.0.1:8087で起動します。同時実行数10で測定して/root/lt3/fast-c10.txtに保存し、スループットがslow-c10.txtに対して3倍以上である必要があります。
/root/lt3/fast.pyは、リクエストを同時に処理できる必要があります。遅延そのものは、そのままにしてください。遅延を減らすのではなく、待ちをなくすことが、今回の改善の核心です。コンテナ名はlt-fast、ポートは8087です。
改善前後の比較表
fastアプリを同時実行数1でも測定して/root/lt3/fast-c1.txtを作成したあと、/root/lt3/compare.csvに4行をまとめます。形式はapp,concurrency,rps,p95で、行はslow,1、slow,10、fast,1、fast,10です。値は、各結果ファイルから読んだものである必要があります。
fastアプリも、同時実行数1でもう1回測定したあと、/root/lt3/compare.csvに4行をまとめます。値は、各結果ファイルから読んだものである必要があります。
必要なインスタンス数を計算する
/root/lt3/capacity.txtに、4行を書きます。
target_rps=300(目標のピークトラフィック)measured_rps=:compare.csvのfast/10行のrpssafe_rps=: 測定値の70%、小数点以下切り捨て(整数)instances=: ceil(300 ÷ safe_rps)、つまり切り上げた整数
/root/lt3/capacity.txtに、目標・測定・安全なスループットと、インスタンス数を書きます。測定された最大値をそのまま使うとヘッドルームがなく、インスタンス数は切り上げる必要があります。
分析レポートを書く
/root/lt3/report.mdに、レポートを書きます。4つのセクションが必要です。ボトルネックの原因、証拠(測定値)、対処、容量の結論。本文に、改善前のスループット、改善後のスループット、必要なインスタンス数の3つの数字が、そのまま入っている必要があります。
/root/lt3/report.mdに、原因・証拠・対処・容量の結論を書きます。証拠は文章ではなく数字である必要があるので、前のステップで出た値を、本文にそのまま書いてください。