はじめに
こんにちは、 @hayatohanaoka です。
今回の記事は、私たちが何気なく使っているWebフレームワークが、
「どのようにリクエストを捌いているか」を書いていこうと思います。
みなさんは意識したことがありますか?
私は、開発職になって1年半ほどの人間なのですが、
恥ずかしながらこれまで深く意識していませんでした。
思い返してみても、この1年半で技術選定に関わる機会もあったものの、
フレームワークの比較で「書きやすさ」や「処理の速さ」は話に出ていましたが、
「どうリクエストを捌くか(並行処理のモデル)」まで踏み込んだ議論をした記憶がありません。
上記のことから、**「実は普段あまり意識しない部分なのかも?」**と感じたので、記事としてまとめて書いておくことにしました!
この記事が
- 私と同じように、フレームワークがリクエストをどう捌いているか意識したことがない方
- バックエンドの技術選定で、もう一歩深い基準を持ちたい方
- フレームワークが裏側でどう動いているか、ざっくり理解したい方
- 「ノンブロッキング」「軽量スレッド」などの言葉のイメージを掴みたい若手エンジニア
にとって何かいい情報となれば幸いです!
リクエストを捌く4つの基本アーキテクチャ
Webフレームワークがリクエストを捌くパターンは、
言語の特性や設計思想によって、大きく以下の4つに分類できます。
1. 「1リクエスト = 1プロセス/スレッド」モデル
Webの世界でも昔から使われている、最もオーソドックスなスタイル。
仕組み
リクエストが来るたびに、OSのプロセスやスレッドを1つ割り当てて処理する。
データベースへの問い合わせなど(I/O処理)が発生すると、結果が返ってくるまでそのスレッドは「待ち状態(ブロック)」になる。
メリット
コードが上から下へ順番に実行されるため、直感的で非常に開発しやすい。
デメリット
アクセスが集中した際、待ち状態のスレッドが大量に発生してしまい、
メモリを食いつぶしたり、順番待ちが発生する可能性がある
参考: C10K問題
代表例
- Ruby on Rails (Ruby)
- Django (Python)
- Laravel (PHP)
など
2. 「イベントループとノンブロッキングI/O」モデル
1の「スレッドが待機して無駄になる問題」を解決するために生まれた、
非同期処理メインのスタイル。
仕組み
メインの処理は「シングルスレッド(イベントループ)」で行う。
DBの読み書きなど時間のかかる処理が発生すると、裏側に処理を丸投げし、
処理の完了を待たずに、すぐ次のリクエストの処理に取り掛かる(ノンブロッキング)。
メリット
メモリ消費が少ない。
チャットアプリのような「1つ1つの処理は軽いが、大量の同時接続がある」といったユースケースに強い。
デメリット
重い処理を扱うのには不向き。
画像処理や複雑な計算など、CPUを多く使うような重い処理を走らせると、
メインのシングルスレッドごと止まってしまい、
他の全リクエストの処理を巻き込んで遅延させてしまうことがある。
代表例
- Express (Node.js)
- NestJS (Node.js)
など
3. 「軽量スレッド」モデル
OSの重いスレッドの代わりに、
言語側で超軽量なスレッドを作って並行処理するスタイル。
仕組み
リクエストごとに、言語のランタイムが管理する「軽量スレッド」を立ち上げ、そこで処理を行う。
この軽量スレッドは、OSのスレッドと比べてメモリ消費が数KBと圧倒的に少なく、起動も一瞬のもの。
そのため、数万〜数十万の同時起動も可能。
メリット
1のモデルのように「同期的で分かりやすいコード」を書きながら、
裏側では2のモデルのように「ノンブロッキングで効率よく」I/O待ちの処理ができる。
開発体験と高並行処理のいいとこ取りとなっている。
デメリット
スレッドの終了管理などを間違えると、メモリリーク(ゴルーチンリーク等)を起こす可能性がある。
言語のランタイムやスレッド管理に関連した特有の設計知識が必要。
代表例
- Gin (Go)
- Echo (Go)
※Goの「ゴルーチン」を利用
4. 「エンタープライズ進化系」モデル
Javaなどの歴史ある堅牢な言語が、時代のトラフィック要件に合わせて進化してきたスタイル。
仕組み
従来は「スレッドプール」を使った1のモデルでは対応できなかった高トラフィックに対応するため、2のような「リアクティブ(非同期)」モデルを取り入れて作成されている。
現在は、3に近い「仮想スレッド(Virtual Threads)」へと強力なパラダイムシフトが起きている。
メリット
大規模開発でも安定感のあるパフォーマンスが出せる。
長年培われた堅牢なエコシステムを保ちながら、最新の並行処理アーキテクチャを取り入れているため。
デメリット
バージョン違いによる書き方の違いが存在するため、学習コストやキャッチアップコストが比較的高い。
歴史が長い分、どの時代のアーキテクチャ(同期か、非同期か、仮想スレッドか)を採用しているかでコードの書き方が大きく変わってしまう。
代表例
- Spring Boot (Java、Kotlin)
など
モダンフレームワークはどう捌いている?
次に、モダンなフレームワークが先ほど書いた4つのパターンのどれに該当するかを簡単に書いていきます。
Ktor (Kotlin) : コルーチンによるスマートな軽量スレッド
「3. 『軽量スレッド』モデル」をKotlinの言語機能で実現したフレームワーク。
該当モデルと仕組み
【該当モデル:3. 軽量スレッドモデル】
Kotlinの非同期処理機能 「Coroutines(コルーチン)」 を全面的に採用し、
リクエストが来るたびに、この超軽量なコルーチンを立ち上げて処理を行う。
DBの返事待ちなどが発生すると、OSのスレッドは占有せずにコルーチンだけが一時停止(サスペンド)」状態になり、スレッドは別のリクエストの処理に移行する。
特徴
直感的で分かりやすい同期的なコードを書きながら、裏側ではノンブロッキングで非常に効率よく大量のトラフィックを捌くことができる。
Kotlinならではの表現力の高さと、並行処理の強さを両立したフレームワーク。
Helidon 4 (Java、Kotlin) : 仮想スレッドがもたらすパラダイムシフト
基本モデルの「4. エンタープライズ進化系モデル」の最先端。
該当モデルと仕組み
【該当モデル:4. エンタープライズ進化系モデル(Virtual Threads特化)】
Helidon 4は、Java 21で正式導入された 「Virtual Threads(仮想スレッド)」 をベースに、Webサーバーのコアからゼロベースで作り直された。
従来は「2. イベントループ」のようなリアクティブプログラミングで大量のリクエストを捌いていた結果、複雑なコードになりがちだったが、そこが解消され、開発体験も向上した。
特徴
仮想スレッドを使うことで、「1リクエスト = 1スレッド」の 昔ながらのシンプル(同期的)な書き方を維持しつつ、数百万の軽量スレッドを立てて捌くことができる。
リアクティブ特有の難解なコードを書かずとも、パフォーマンスの良いアプリケーションを作成できる。
Hono (TS/JS) : エッジ環境に最適化されたイベントループ
基本モデルの「2. イベントループ」をエッジコンピューティング向けに最適化したモデル。
該当モデルと仕組み
【該当モデル:2. イベントループ(のエッジ進化系)】
Node.jsだけでなく、Cloudflare WorkersやDeno、Bunといった「エッジランタイム」に最適化されている点が特徴的。
基本的には、言語がTypeScript/JavaScriptのため、Expressなどと同じ「シングルスレッド+イベントループ」となっている。
特徴
重たいサーバーを立ち上げなくてもリクエストを捌けるため、軽量かつ速いアプリケーションが作成できる。
エッジ環境(Cloudflare Workersなど)では、リクエストが来るたびにV8エンジンの**「Isolate(アイソレート)」**という超軽量なサンドボックス環境(数ミリ秒で起動)を割り当てて処理するため、サーバー立ち上げの遅延(コールドスタート)がほぼなく、世界中のサーバーで瞬時にリクエストを捌く。
おわりに(まとめ)
今回は、フレームワークごとのリクエスト処理モデル4つについてまとめました。
改めて、4つをまとめておきます。
- 「1プロセス/スレッド」モデル(Rails, Laravelなど)
- 「イベントループ」モデル(Express, Honoなど)
- 「軽量スレッド」モデル(Go, Ktorなど)
- 「エンタープライズ進化系」(Spring Boot, Helidonなど)
皆さんが普段使っているフレームワークは、裏側でどのようにリクエストを捌いているか知っていましたか?
次に技術選定をする時や、日々のコーディングの際に、この記事が少しでも役に立てば嬉しいです!
最後まで読んでいただき、ありがとうございました。
おまけ
これまでの開発であった以下の2つのエピソードも、今回調べた「フレームワークに適した書き方」に関する内容だったのだなと理解しなおしました。
- 過去に先輩が「 Ktor なのに runBlocking が書くなんて!信じられない!」と言っていたこと
- Ktorが非同期処理の良さを活かしたフレームワークなのに、ブロッキングしてたらKtorを使う意味が...
「1つ1つの処理が重たいから非同期処理したいのに、ブロッキングしてたら意味ないよな...」程度しか当時は理解できていなかった
- Ktorが非同期処理の良さを活かしたフレームワークなのに、ブロッキングしてたらKtorを使う意味が...
- 「hono には handler とか controller となる層は作らないのがベストプラクティス」とチームで話題になった
点と点が繋がるのに時間がかかってしまいましたが、なんだかすっきりしました!!