I/O と並行処理 — 待っている間に、何をするか
サムネイル生成を1本足しただけで、無関係な API まで全部遅くなった。待ち時間の処理と、計算そのものは、同じ場所に置いてはいけない。
この記事の進み方
What — 待っている時間と、計算している時間は違う
サーバーの処理時間は、性質の違う2つに分かれます。
- I/O 待ち — DB の応答、外部 API の応答、ファイルの読み書き。待っている間、CPU は空いている
- 計算(CPU) — 画像の変換、暗号の処理、大きな JSON の解析、複雑な正規表現。その間、CPU を占有する
普通の Web の処理は、ほとんどが I/O 待ちです。
GET /products/42 の内訳(合計 85ms)
DB のクエリ 5ms ← 待っている。CPU は空き
外部の在庫 API 70ms ← 待っている。CPU は空き
JSON の組み立て 3ms ← 計算
そのほか 7ms
85ms のうち、CPU を使っているのは 10ms ほどです。残りの 75ms は待っているだけなので、その間に別のリクエストを進められます。これが「1台で同時に何百ものリクエストを捌ける」理由です。
- I/O バウンド
- 処理時間の大半が待ち時間である状態。同時に走らせる数を増やすと、捌ける量が増える。
- CPU バウンド
- 処理時間の大半が計算である状態。同時に走らせる数を増やしても、捌ける量は増えない。
- スループット
- 単位時間あたりに捌ける件数。
- レイテンシ
- 1件あたりにかかる時間。
待ち方には、2つのやり方がある
待つ間、そのスレッドは眠る。OS が別のスレッドに切り替える
1リクエスト = 1スレッド(Java, Ruby, PHP など)- 待ち方
- スレッドが眠る。OS が切り替える
- 重い計算
- **そのスレッドだけが遅くなる**
- 同時に捌ける数
- スレッド数まで
- 1つあたりの費用
- メモリ(MB 単位)と切り替えの費用
- 費用
- —
- 応答
- —
- 利用者の体験
- —
- –コードは上から下へ素直に書ける。待つ処理も普通の呼び出しに見える
- –同時接続が数千になると、スレッドの数だけメモリと切り替えが増える
- –重い計算を書いても、他のリクエストのスレッドは動き続ける
どちらも「待つ間に別の仕事をする」ための仕組み。違うのは、何を単位に切り替えるか。得意な失敗の仕方も違う。
DB のクエリの応答を待っている 200ms の間、その処理はサーバーの CPU をどれだけ使っていますか?
Why — なぜ無関係な API まで遅くなったのか
冒頭のサービスはイベントループのモデルでした。1本のスレッドが、順番に仕事を進めます。
待つ処理なら、待ちに入った時点で次の仕事へ移れます。ところが計算している間は、移れません。その 0.4 秒のあいだ、他のリクエストは1件も進みません。
- 1普段待ちばかりなので、列が進む軽い
- 2サムネイルの生成が始まる0.4秒、スレッドを占有する重い
- 3後ろに積まれる無関係なリクエストも同じ列重い
- 4生成が重なる登録が集中する時間帯重い
- 5全体が4秒DB もネットワークも暇なまま重い
待ち中心の処理だけなら、同じ列に並んでいても互いを止めない。止めるのは計算している時間だけ。
サムネイルの生成そのものは 0.4 秒。問題は、その間に到着したリクエストが全部後ろに並ぶこと。商品一覧も検索もログインも、同じ列に並んでいる。
CPU バウンドか、I/O バウンドかの見分け方
どちらかで、打つ手がまったく変わります。同時に走らせる数を増やしてみると分かります。
| 増やしたときの変化 | 何が起きているか | 打つ手 |
|---|---|---|
| 捌ける量が増える | I/O バウンド。待ち時間が重なっている | 同時実行数を上げる。資源(接続など)の上限に注意 |
| 捌ける量が変わらず、1件あたりが遅くなる | CPU バウンド。計算が順番待ちしている | 計算を減らす、別の場所へ逃がす、CPU を増やす |
冒頭の事故は後者でした。同時実行数を増やしても、CPU は増えません。 押し込む量が増えるだけで、1件あたりが遅くなります(data/connection-pool の「DB は接続の数だけ速くならない」と同じ形)。
スレッドモデルなら安全、ではない
スレッドごとに処理するモデルでは、重い計算を書いても他のリクエストのスレッドは動き続けます。イベントループのように全員が止まることはありません。
代わりに、別の形で詰まります。
- スレッドの数が上限。 計算がスレッドを長く占有すると、空きスレッドが減り、新しいリクエストが待たされる
- CPU の取り合い。 コア数を超えるスレッドが同時に計算すると、切り替えの費用で全体が遅くなる
どちらのモデルでも、CPU の総量は増えません。 違うのは、詰まり方が「全員が同時に止まる」か「じわじわ遅くなる」かです。イベントループのほうが症状が派手で、気づきやすいとも言えます。
ある API の同時実行数を 10 から 20 に増やしたところ、1秒あたりに捌ける件数は変わらず、1件あたりの応答時間だけが約2倍になりました。この処理は何バウンドですか?
演習 — まず自分で判断する
解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。
サムネイルの生成を、どこで動かしますか?
- API はイベントループのモデル(1プロセス1スレッドで待ちを捌く)。4コアのサーバー2台
- 生成は1枚 0.4 秒の計算。1日に数百件だが、時間帯に集中する
- 商品の登録画面では、生成後のサムネイルをすぐ確認したい
- 同じサーバーが、商品一覧・検索・ログインの API も受けている
- 失敗したら、あとで作り直せる(元の画像は保存済み)
- · 受け口のスレッドを、計算で占有してよいか
- · すぐ確認したい、をどこまで満たす必要があるか
- · 集中したときに、何が伸びるか(応答か、待ち行列か)
「非同期にしたのに速くならない」と言う同僚に、何が起きているかを説明してください
- 同僚は画像の変換処理を async 関数に書き換えたが、応答時間が変わらなかった
- 非同期という言葉は知っているが、何が非同期になるかは曖昧
- 次に自分で判断できるようになってほしい
読み終わりましたか?
読了にすると、これを前提とする記事がロードマップで開放されます。