仕組みから学ぶ Web
応用読了目安 16#I/O#並行処理#イベントループ#スレッド#CPU バウンド

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つのやり方がある

Compare待っている間、どうするか

待つ間、そのスレッドは眠る。OS が別のスレッドに切り替える

1リクエスト = 1スレッド(Java, Ruby, PHP など)
待ち方
スレッドが眠る。OS が切り替える
重い計算
**そのスレッドだけが遅くなる**
同時に捌ける数
スレッド数まで
1つあたりの費用
メモリ(MB 単位)と切り替えの費用
費用
応答
利用者の体験
  • コードは上から下へ素直に書ける。待つ処理も普通の呼び出しに見える
  • 同時接続が数千になると、スレッドの数だけメモリと切り替えが増える
  • 重い計算を書いても、他のリクエストのスレッドは動き続ける

どちらも「待つ間に別の仕事をする」ための仕組み。違うのは、何を単位に切り替えるか。得意な失敗の仕方も違う。

確認 — ここまで読めたか

DB のクエリの応答を待っている 200ms の間、その処理はサーバーの CPU をどれだけ使っていますか?

まず選ぶ(解答例は a〜d の記号で説明します)

Why — なぜ無関係な API まで遅くなったのか

冒頭のサービスはイベントループのモデルでした。1本のスレッドが、順番に仕事を進めます

待つ処理なら、待ちに入った時点で次の仕事へ移れます。ところが計算している間は、移れません。その 0.4 秒のあいだ、他のリクエストは1件も進みません

Figure0.4秒の計算が、待ち行列に何をするか
  1. 1普段待ちばかりなので、列が進む軽い
  2. 2サムネイルの生成が始まる0.4秒、スレッドを占有する重い
  3. 3後ろに積まれる無関係なリクエストも同じ列重い
  4. 4生成が重なる登録が集中する時間帯重い
  5. 5全体が4秒DB もネットワークも暇なまま重い

待ち中心の処理だけなら、同じ列に並んでいても互いを止めない。止めるのは計算している時間だけ。

1/5
普段1件あたり CPU を使うのは 10ms ほど。残りは待ちなので、その間に他の仕事が進む。80ms で返せる。

サムネイルの生成そのものは 0.4 秒。問題は、その間に到着したリクエストが全部後ろに並ぶこと。商品一覧も検索もログインも、同じ列に並んでいる。

CPU バウンドか、I/O バウンドかの見分け方

どちらかで、打つ手がまったく変わります。同時に走らせる数を増やしてみると分かります。

増やしたときの変化何が起きているか打つ手
捌ける量が増えるI/O バウンド。待ち時間が重なっている同時実行数を上げる。資源(接続など)の上限に注意
捌ける量が変わらず、1件あたりが遅くなるCPU バウンド。計算が順番待ちしている計算を減らす、別の場所へ逃がす、CPU を増やす

冒頭の事故は後者でした。同時実行数を増やしても、CPU は増えません。 押し込む量が増えるだけで、1件あたりが遅くなります(data/connection-pool の「DB は接続の数だけ速くならない」と同じ形)。

スレッドモデルなら安全、ではない

スレッドごとに処理するモデルでは、重い計算を書いても他のリクエストのスレッドは動き続けます。イベントループのように全員が止まることはありません。

代わりに、別の形で詰まります。

  • スレッドの数が上限。 計算がスレッドを長く占有すると、空きスレッドが減り、新しいリクエストが待たされる
  • CPU の取り合い。 コア数を超えるスレッドが同時に計算すると、切り替えの費用で全体が遅くなる

どちらのモデルでも、CPU の総量は増えません。 違うのは、詰まり方が「全員が同時に止まる」か「じわじわ遅くなる」かです。イベントループのほうが症状が派手で、気づきやすいとも言えます。

確認 — ここまで読めたか

ある API の同時実行数を 10 から 20 に増やしたところ、1秒あたりに捌ける件数は変わらず、1件あたりの応答時間だけが約2倍になりました。この処理は何バウンドですか?

まず選ぶ(解答例は a〜d の記号で説明します)

演習 — まず自分で判断する

解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。

演習 — 設計判断を問う

サムネイルの生成を、どこで動かしますか?

与えられた条件
  • API はイベントループのモデル(1プロセス1スレッドで待ちを捌く)。4コアのサーバー2台
  • 生成は1枚 0.4 秒の計算。1日に数百件だが、時間帯に集中する
  • 商品の登録画面では、生成後のサムネイルをすぐ確認したい
  • 同じサーバーが、商品一覧・検索・ログインの API も受けている
  • 失敗したら、あとで作り直せる(元の画像は保存済み)
この軸で考える
  • · 受け口のスレッドを、計算で占有してよいか
  • · すぐ確認したい、をどこまで満たす必要があるか
  • · 集中したときに、何が伸びるか(応答か、待ち行列か)
まず選ぶ(解答例は a〜d の記号で説明します)

演習 — 説明できるか

「非同期にしたのに速くならない」と言う同僚に、何が起きているかを説明してください

与えられた条件
  • 同僚は画像の変換処理を async 関数に書き換えたが、応答時間が変わらなかった
  • 非同期という言葉は知っているが、何が非同期になるかは曖昧
  • 次に自分で判断できるようになってほしい

読み終わりましたか?

読了にすると、これを前提とする記事がロードマップで開放されます。