高性能Webサービスにおける並行処理モデルの詳細分析:Haskell、Node.js、Goの比較研究
Part I: 序論 - 高性能ルーティングに関する主張の解体
上長の仮説
本レポートは、「Haskellを使うとルーティングがマルチスレッドによって工夫されて、何かが嬉しいらしい」という、上長から提示された仮説の調査を目的とする。この仮説は、Haskellの並行処理アーキテクチャが、特に高負荷状況下において、Node.jsやGoといった他の主要な技術スタックと比較して、安定性やパフォーマンスの面で何らかの優位性を持つことを示唆している。本レポートでは、この仮説を技術的根拠に基づいて検証し、その真偽と、それが有効となる特定の文脈を明らかにすることを目的とする。
Webサービスにおける並行処理の重要性
現代のWebサービスにおいて、数万、数十万という同時クライアント接続を効率的に処理する能力は、その成否を分ける決定的な要因である。ユーザーからのリクエストは、データベースへの問い合わせ、外部APIの呼び出し、あるいは複雑な計算処理など、多岐にわたるタスクを伴う。これらのタスクをいかにして滞りなく、かつ迅速に処理するかは、アプリケーションの応答性、スケーラビリティ、そして最終的なユーザー体験に直結する。
この課題に対する最も根本的なアーキテクチャ上の決定が、「並行処理モデル」の選択である。並行処理モデルとは、複数のタスクを同時に、あるいは見かけ上同時に実行するための仕組みであり、その設計思想は言語や実行環境によって大きく異なる。このモデルの選択が、システムのパフォーマンス、安定性、そして開発の複雑性を規定する。
比較対象となる技術スタック
本レポートでは、それぞれが独自の哲学に基づいた並行処理モデルを採用する3つの技術スタックを詳細に分析する。
- Haskell: 洗練されたランタイムシステム(GHC RTS)によって管理される、プリエンプティブ(強制的割り込み型)な軽量スレッドモデルを採用。開発者から並行処理の複雑さを可能な限り隠蔽し、高いレベルの抽象化と安全性を実現する 1。
- Node.js: シングルスレッドで動作する、非同期ノンブロッキングI/Oモデル(イベントループ)を採用。I/O処理に特化した高い効率性を誇るが、そのアーキテクチャに起因する特有のボトルネックを持つ 3。
- Go: 軽量な並行実行単位である「ゴルーチン」と、それらの間の安全な通信を担う「チャネル」を言語機能として提供。ランタイムによるスケジューリングと、プログラマによる明示的な並行処理の記述を組み合わせた、実用的なアプローチを取る 5。
本レポートの構成
本レポートは、まず各技術スタックの並行処理モデルの理論的基盤を深く掘り下げ(Part II〜IV)、次にそれらのアーキテクチャ上のトレードオフと実際のパフォーマンスベンチマークを比較分析する(Part V)。最後に、これらの理論的な分析を実証するための、秒間10万リクエストを想定した高負荷テストの具体的な検証方法をステップバイステップで解説する(Part VI)。これにより、読者は上長の仮説に対する明確な結論を得るだけでなく、将来の技術選定において、より深い洞察に基づいた意思決定を行うための知識基盤を構築することができる。
Part II: Haskellパラダイム - ユーザースペースでのプリエンプティブ・マルチタスキング
Haskellの並行処理能力の核心は、その言語仕様そのものよりも、コンパイラであるGHC(Glasgow Haskell Compiler)が提供する高度なランタイムシステム(RTS)にある。このRTSが、上長の述べた「工夫されたマルチスレッド」の正体であり、高い安定性とパフォーマンスを両立させるための鍵となっている。
2.1. GHCランタイムシステム(RTS):見えざるエンジン
多くのプログラミング言語とは異なり、Haskellプログラムの実行時には、RTSと呼ばれる強力な支援システムが背後で動作している。これはC言語で記述された複雑なプログラムであり、メモリ管理(ガベージコレクション)、コードの実行、そして本題である並行処理の管理を一手に担っている 7。Haskellの並行処理モデルを理解することは、このRTSの働きを理解することとほぼ同義である。
2.2. 軽量並行処理:「グリーン・スレッド」
Haskellの並行処理の基本単位は「軽量スレッド」または「グリーン・スレッド」と呼ばれるものである。これは、オペレーティングシステム(OS)が直接管理する重量級の「OSスレッド」とは異なり、HaskellのRTSがユーザースペース内で完全に管理する、非常に軽量な実行単位である 1。
このアプローチの最大の利点は、スレッド生成と管理のコストが劇的に低いことにある。OSスレッドが1つあたり数メガバイトのメモリとカーネルリソースを消費するのに対し、Haskellの軽量スレッドは数百バイト程度のメモリで生成可能である 10。これにより、1つのOSプロセス内で、利用可能なメモリが許す限り、文字通り数万から数百万もの軽量スレッドを生成し、並行して実行することが可能になる 10。
アナロジー: OSスレッドを「大型貨物トラック」、Haskellの軽量スレッドを「小型配達ドローン」に例えることができる。同じコストで数台のトラックしか配備できないのに対し、ドローンであれば何千機も配備できる。これにより、非常にきめ細かく、多数の配送タスク(処理)を同時にさばくことが可能になる。
2.3. M:Nスレッドモデル:真の並列処理の実現
GHCのRTSは、「M:Nスレッドモデル」として知られる高度なスケジューリング方式を採用している。これは、M個の軽量スレッド(ドローン)を、N個のOSスレッド(トラック)に割り当てて実行するモデルである 9。
ここで重要なのは、N(OSスレッドの数)が、通常はマシンの持つCPUコア数と一致するように設定される点である(RTSオプション -N で指定) 12。これにより、Haskellアプリケーションは、利用可能な全てのCPUコアを最大限に活用し、複数の軽量スレッドを単なる「並行(concurrency)」ではなく、物理的に同時に実行する「並列(parallelism)」で動作させることができる。
2.4. GHCスケジューラ:安定性の鍵
GHCスケジューラはRTSの中核をなす「頭脳」であり、どの軽量スレッドを、どのOSスレッド上で、いつ実行するかを決定する役割を担う 14。このスケジューラの挙動こそが、Haskellの安定性の源泉である。
- ランキュー(Run Queue): 各OSスレッド(GHCの用語では「Capability」)は、それぞれが実行すべき軽量スレッドの待機行列(ランキュー)を持っている。これにより、スレッド間で共有リソースの競合が最小限に抑えられ、マルチコア環境での性能が向上する 14。
- プリエンプティブ・マルチタスキング(Preemptive Multitasking): これが上長の仮説を理解する上で最も重要な概念である。GHCのRTSは、ある軽量スレッドが一定時間(タイムスライス)以上実行を続けると、そのスレッドを強制的に中断させ、別の待機中スレッドにCPUの実行権を切り替えることができる 9。この「プリエンプション(横取り)」機能により、一つの計算負荷の高いリクエストがCPUコアを独占し、他の多数のリクエストを待たせてしまう(飢餓状態にする)事態を防ぐことができる。
- ワークスティーリング(Work Stealing): あるOSスレッドのランキューが空になった場合、そのOSスレッドは他の忙しいOSスレッドのランキューから仕事(軽量スレッド)を「盗んで」実行することができる 14。これにより、システム全体のCPU使用率が高く維持され、効率的な負荷分散が自動的に行われる。
2.5. ノンブロッキングI/Oの自動化:ランタイムの魔法
Webアプリケーションにおいて頻繁に発生するネットワーク通信やファイル読み書きといったI/O処理は、完了までに時間がかかり、ブロッキング(処理の待機)の原因となる。Haskellでは、この問題もRTSが巧みに解決する。
開発者がHaskellのコードでブロッキングI/O(例:ソケットからのデータ読み込み)を呼び出すと、RTSがその呼び出しを検知する 1。OSスレッドをブロックさせて待機させる代わりに、RTSは以下の動作を瞬時に行う。
- I/Oを要求した軽量スレッドを「スリープ」状態にする。
- RTS内部のノンブロッキングI/Oマネージャに、対象のI/O(ソケットなど)の監視を依頼する。
- OSスレッドを即座に解放し、ランキューで待機している別の軽量スレッドの実行を開始する。
- I/Oが完了すると、I/OマネージャはRTSに通知し、スリープしていた元の軽量スレッドが再びランキューに戻され、処理を再開する 1。
この一連のプロセスは、開発者に対して完全に透過的である。開発者は、非同期処理を意識した複雑なコールバックやasync/awaitのような構文を書く必要がなく、あたかも同期的に処理を記述しているかのように、シンプルで直線的なコードを書くだけでよい 2。その裏側で、RTSが自動的に全てをノンブロッキングかつ高効率な並行処理に変換してくれるのである。
2.6. Webサービスへの応用(Yesod, Servant)
この強力なランタイム基盤の上に、YesodやServantといったWebフレームワークが構築されている 2。YesodアプリケーションがWebリクエストを受け取ると、通常はそのリクエストを処理するために新しい軽量スレッドを一つ生成する。
これは、1万の同時接続があれば1万の軽量スレッドが生成され、それらがGHCスケジューラによって効率的にCPUコア群に割り当てられて並列処理されることを意味する。開発者はスレッドプールやコールバック地獄、非同期処理の複雑な状態管理から解放され、ビジネスロジックそのものに集中できる。並行処理は、Haskellプラットフォームに内在する、いわば「標準装備」の機能なのである 2。
上長の「マルチスレッドが工夫されている」という直感は、単に複数のスレッドを使うという話ではなく、GHCランタイムが提供するプリエンプティブ・スケジューラと透過的なノンブロッキングI/Oという、二つの強力な自動化機能の組み合わせを指している。この組み合わせが、Haskell製アプリケーションに、開発者の負担を増やすことなく、高い安定性とパフォーマンスをもたらすのである。
Part III: Node.jsパラダイム - 非同期イベントループ
Node.jsは、Haskellとは全く異なる設計思想で並行処理を実現している。その核心は「シングルスレッド」と「イベントループ」にあり、このアーキテクチャは特定の種類のワークロードにおいて驚異的な性能を発揮する一方で、Haskellが解決した問題を別の形で露呈させる。
3.1. シングルスレッド設計
Node.jsの最も基本的な原則は、開発者が記述したJavaScriptコードが、単一のメインスレッド上で実行されることである 3。この設計は、複数のスレッド間でデータを共有する際に発生する競合状態やデッドロックといった、並行処理における古典的で難解な問題を根本的に排除する。これにより、開発者は同期について深く悩むことなく、比較的シンプルにプログラムを記述できる。
3.2. イベントループとLibuv:非同期処理の心臓部
シングルスレッドでありながら、Node.jsが多数の同時接続を処理できる理由は「イベントループ」と呼ばれるメカニズムにある 3。イベントループは、実行すべきタスク(イベント)をキューで管理し、一つずつ順番に処理していくループである。
このモデルの鍵を握るのが、libuvというC言語で書かれたライブラリである。libuvはイベントループそのものを提供するだけでなく、時間のかかるI/O処理(ネットワーク通信、ファイルアクセスなど)をOSのカーネルや内部のワーカースレッドプールに委譲(オフロード)する機能を持つ 3。
Node.jsにおける非同期I/O処理の流れは以下のようになる。
- JavaScriptのメインスレッドが、I/O処理(例:ファイルの読み込み)を要求する。
- Node.jsは、この要求をlibuvに渡し、処理完了後に実行すべき「コールバック関数」を登録する。
- メインスレッドはI/Oの完了を待たずに、すぐに次のタスクの処理へと移る。これが「ノンブロッキング」である。
- libuv(またはOS)がバックグラウンドでI/O処理を完了すると、登録されたコールバック関数がイベントキューに追加される。
- イベントループは、メインスレッドが他のタスクを終えて手が空いたタイミングで、キューからコールバック関数を取り出して実行する 3。
3.3. 致命的なボトルネック:イベントループのブロッキング
このアーキテクチャの強みと弱みは表裏一体である。I/O処理はlibuvにオフロードされるためノンブロッキングだが、計算負荷の高い(CPUバウンドな)JavaScriptコードは、他の処理と同様にシングルスレッドのメインスレッド上で直接実行される。もしこの処理に時間がかかると、イベントループ全体がその処理の完了を待つことになり、完全に停止してしまう。これを「イベントループのブロッキング」と呼ぶ 4。
具体例: 以下のExpressサーバーは、この問題を明確に示す。
JavaScript
const express = require('express');
const app = express();
// CPUを長時間占有するブロッキングAPI
app.get('/blocking', (req, res) => {
// このループはメインスレッドを完全にブロックする
for (let i = 0; i < 5e9; i++) {
// 何もしない
}
res.send('Blocking task complete');
});
// すぐに応答を返すノンブロッキングAPI
app.get('/non-blocking', (req, res) => {
res.send('Non-blocking task complete');
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
このサーバーで起こる事象を順を追って解説する 20。
- あるクライアントが/blockingエンドポイントにリクエストを送信する。
- サーバーはリクエストを受け付け、巨大なforループの実行をメインスレッドで開始する。
- このループが実行されている間、イベントループは完全に「固まった」状態になる。新しいリクエストの受付、完了したI/O処理のコールバック実行、タイマーの発火など、他のすべてのタスクが停止する。
- この状態で、別のクライアントが高速なはずの/non-blockingエンドポイントにリクエストを送信しても、そのリクエストはOSのTCPキューで待たされるだけで、Node.jsプロセスは応答できない。
- サーバーは、数秒から数十秒かかるforループが完了するまで、すべてのクライアントに対して完全に無応答となる。
これこそが、上長が懸念していた「処理しきれないルーティング」の正体である。たった一つの重い処理が、システム全体の可用性を奪ってしまうのである。
3.4. Webサービスへの応用(Express, Fastify)
このアーキテクチャ特性から、Node.jsはI/Oバウンドなアプリケーション、例えばマイクロサービスアーキテクチャにおけるAPIゲートウェイや、主に外部のデータベースやAPIを呼び出すだけのサービスには非常に適している 21。メインスレッドはほとんどの時間をI/Oの待ち時間に費やすため、その間に多数の同時リクエストを効率的にさばくことができる。
しかし、リクエスト処理のどこか一部にでも、複雑なデータ変換、同期的な暗号化・圧縮、大規模なJSONの解析といったCPU負荷の高い処理が含まれている場合、それはシステム全体のアキレス腱となる 18。
Node.jsの「ノンブロッキング」という性質は、あくまでI/O処理に対する限定的なものである。計算処理に対しては、完全に「ブロッキング」である。この二面性が、Node.jsの最大の強み(I/Oスループット)と、最も危険な弱点を同時に生み出している。Haskellのランタイムがプリエンプションによって提供する「安全装置」とは対照的に、Node.js開発者は、自らのコードの計算量を常に意識し、イベントループをブロックしないよう細心の注意を払う責任を負う。これは、安定性と回復力における根本的な違いと言える。
Part IV: Goパラダイム - シンプルさとパフォーマンスの両立
Go言語は、Haskellのような学術的な洗練さと、Node.jsのような実用的なシンプルさの中間に位置し、両者の利点を組み合わせた強力な並行処理モデルを提供する。これにより、真の並列処理能力を、より多くの開発者にとってアクセスしやすい形で実現している。
4.1. ゴルーチン:OSスレッドより軽量な実行単位
Goの並行処理の基本単位は「ゴルーチン(Goroutine)」と呼ばれる。これは、他の関数と並行して実行される関数であり、Haskellの軽量スレッドと同様にGoのランタイムによって管理される 5。ゴルーチンもまた非常に軽量で、生成時のスタックサイズはわずか数キロバイトであり、必要に応じて動的に拡張される 6。
Haskellとの違いは、並行処理がプログラマによって明示的に開始される点である。関数呼び出しの前にgoキーワードを付与するだけで、その関数は新しいゴルーチンとして非同期に実行される 5。この明示的なアプローチは、コードのどこで並行処理が発生しているかを追いやすくする。
4.2. Goスケジューラ:GHCと同様のM:Nモデル
Goのランタイムもまた、GHCと同様のM:Nスケジューラを実装している。これにより、多数のゴルーチンを、CPUコア数に応じた少数のOSスレッドに多重化(multiplexing)して実行する 5。GOMAXPROCS環境変数によって、同時にGoのコードを実行できるOSスレッドの最大数を制御できる。
さらに重要なことに、Go 1.14以降、そのスケジューラはプリエンプティブになっている。関数呼び出しを含まないタイトなループに陥ったゴルーチンであっても、ランタイムが割り込みをかけて他のゴルーチンに実行機会を与えることができる 23。これにより、GoはHaskellと同様に、CPUバウンドなタスクが特定のコアを独占し、システム全体の応答性を損なうのを防ぐアーキテクチャ的な耐性を持っている。
4.3. チャネル:構造化された通信
Goの並行処理におけるもう一つの柱が「チャネル(Channel)」である。チャネルは、ゴルーチン間で安全にデータを送受信し、同期を取るための型付けされたパイプのようなものである 5。
Goでは、「メモリを共有して通信するな、通信によってメモリを共有せよ(Do not communicate by sharing memory; instead, share memory by communicating.)」という哲学が推奨されている 5。これは、複数のゴルーチンが同じメモリ領域を直接読み書きする(そしてロック機構で保護する)のではなく、チャネルを介してデータの所有権をゴルーチンからゴルーチンへと渡していくスタイルを意味する。この設計思想に従うことで、データ競合(race condition)といった並行処理特有のバグを設計レベルで回避しやすくなる。
4.4. Webサービスへの応用(net/http)
Goの標準ライブラリnet/httpは、この並行処理モデルを非常にうまく活用している。デフォルトで、net/httpサーバーは受け付けた各HTTPリクエストを、それぞれ新しいゴルーチン内で処理する 25。
このモデルは、I/OバウンドなタスクとCPUバウンドなタスクが混在するWebアプリケーションのワークロードに対して非常に堅牢である。
- リクエストがデータベース待ちなどのI/Oでブロックされると、Goのランタイムはそのゴルーチンをスリープさせ、OSスレッドを解放して他のゴルーチンを実行させる。
- リクエストが重い計算処理を行っていても、プリエンプティブ・スケジューラが適度に割り込みをかけるため、他のリクエストの処理が完全に停止することはない。
結果として、Goは特別な設定や複雑な非同期コードを記述することなく、標準機能だけで高効率かつ安定したWebサーバーを構築できる。
Goは、Haskellのような学術的言語で開拓された高度なM:Nプリエンプティブ・スケジューリングモデルを採用しつつ、それをgoキーワードやチャネルといった、よりシンプルで命令的なAPIを通じて開発者に提供している。これにより、GoはHaskellと同等のランタイムレベルの安定性を、関数型プログラミングの経験がない幅広い開発者にもたらすことに成功した。これは、高性能な並行処理の「民主化」とも言えるだろう。
Part V: 比較分析 - アーキテクチャとパフォーマンス
これまでの各技術スタックの理論的な分析を踏まえ、ここではアーキテクチャ上のトレードオフを整理し、定量的なベンチマークデータを用いてパフォーマンスを比較評価する。これにより、上長の仮説に対する最終的な結論を導き出す。
5.1. アーキテクチャのトレードオフ
Haskell、Node.js、Goの並行処理モデルは、それぞれ異なる設計思想に基づいている。その違いは、パフォーマンス、安定性、開発の容易さといった側面に直接的な影響を与える。以下の表は、その核心的な違いをまとめたものである。
表1: 並行処理モデルの比較
| 特徴 | Haskell (GHC) | Node.js | Go |
|---|---|---|---|
| スレッドモデル | M:N 軽量スレッド | シングルスレッド・イベントループ | M:N ゴルーチン |
| スケジューリング | プリエンプティブ(強制的) | 協調的(自主的) | プリエンプティブ(強制的) |
| タスク毎のメモリ | 極小(~1 KB) 10 | N/A | 軽量(~2 KB) 23 |
| I/O処理 | 透過的ノンブロッキング 1 | コールバックベース・ノンブロッキング 3 | ゴルーチン内でのブロッキング呼び出し |
| 並列性 | 全コアで自動的に利用 12 | JavaScriptコードでは不可 | 全コアで自動的に利用 23 |
| 主要なボトルネック | GC(ガベージコレクション)の一時停止 | イベントループのブロッキング 18 | チャネルの競合 / GC |
この表から、HaskellとGoがアーキテクチャ的に類似しており、共にプリエンプティブ・スケジューリングとM:Nモデルによって真の並列処理と安定性を実現していることがわかる。一方、Node.jsはシングルスレッドと協調的スケジューリングという全く異なるアプローチを取り、その結果として「イベントループのブロッキング」という固有の脆弱性を抱えている。
5.2. ベンチマークの解釈:TechEmpower Round 23
TechEmpower Framework Benchmarksは、Webフレームワークのパフォーマンスを比較するための業界標準的な指標の一つであるが、その結果は慎重に解釈する必要がある 27。ここでは、最も現実的なシナリオに近いとされる「Fortunes」テストの結果に注目する。このテストは、単純なテキスト応答だけでなく、データベースアクセス、HTMLテンプレートのレンダリング、そしてある程度の計算処理を含むため、総合的な性能評価に適している 29。
表2: TechEmpower "Fortunes" ベンチマーク結果 (Round 23)
| 順位 | フレームワーク | 言語 | 秒間リクエスト数 (RPS) |
|---|---|---|---|
| 15 | just-js | JavaScript (Node.js) | 982,024 27 |
| 17 | fasthttp-prefork | Go | 959,399 27 |
| 25 | gearbox | Go | 896,791 27 |
| N/A | yesod, wai | Haskell | (Round 23には未掲載) |
注: 2025年2月24日公開のRound 23のFortunesテストにおいて、Haskellフレームワーク(Yesod, waiなど)のエントリーは確認できなかった。過去のラウンドでは上位にランクインしていたが、近年は実装の更新が滞っており、最新のベンチマークからは除外されている可能性がある 30。
分析:
- Goの圧倒的なパフォーマンス: fasthttpのようなGoのフレームワークは、ベンチマークの上位に常に位置している 27。これは、Goがコンパイル言語であること、効率的なランタイムを持つこと、そしてベンチマーク向けに高度に最適化された低レベルなフレームワークが存在することに起因する 32。
- Node.jsの健闘: just-jsのような比較的新しいランタイムやfastifyのような高性能フレームワークは、Goに匹敵する非常に高いスループットを達成している 27。これは、V8エンジンのJITコンパイル技術と、I/Oバウンドなタスクに特化したイベントループモデルの効率性の高さを示している。
- Haskellの不在: Haskellが最新のベンチマークに不在であることは、コミュニティのベンチマーク最適化に対する関心の度合いを反映している可能性がある 30。しかし、これはHaskellの潜在的なパフォーマンスが低いことを直接意味するものではない。アーキテクチャの理論的優位性は、ベンチマークの数値とは別に評価されるべきである。
5.3. 上長の仮説の検証:生の速度よりも安定性
ここが本レポートの結論を導く上で最も重要な考察である。TechEmpowerのベンチマークは、均一な負荷の下での最大スループットを測定するものであり、「安定性」という指標を評価するには不向きである。
Part IIIで詳述したように、Node.jsの致命的な欠点であるイベントループのブロッキングは、Fortunesテストのような「すべてのリクエストが予測可能で、ほぼ同じ時間で完了する」という均一な負荷の下では顕在化しにくい。
上長の懸念する「安定性」とは、現実世界のアプリケーションで頻繁に発生する不均一な負荷、つまり、高速なリクエストと低速でCPU負荷の高いリクエストが混在する状況下で、システムがいかに安定して動作し続けるか、という点にある。
仮説に対する結論:
上長の直感は、技術的に正しい。Haskellの(そしてGoも同様の)プリエンプティブなM:Nスケジューリングモデルは、Node.jsの協調的なシングルスレッドモデルと比較して、不均一なワークロードに対するアーキテクチャ上の安定性と回復力において根本的に優れている。
Node.jsは均一なI/Oバウンドなタスクにおいては非常に高速だが、予測不能なCPUバウンドな処理が混入するとシステム全体が停止する脆弱性を抱えている。一方でHaskellは、ランタイムが「安全装置」として機能し、一つの重い処理がシステム全体に与える影響を局所化する。この意味で、Haskellはより「安全」で「安定的」な選択肢であると言える。生の速度(スループット)と、負荷変動に対する安定性は、分けて評価されるべき異なる性能指標なのである。
Part VI: 実践的検証ガイド - 秒間10万リクエストの負荷テスト
理論的な分析を実証するために、ここでは実際に高負荷を生成し、各技術スタックのアーキテクチャ的な挙動を観測するための具体的な手順を解説する。この実験を通じて、特にNode.jsのイベントループブロッキング現象を自身の目で確認することができる。
6.1. Step 1: テストアプリケーションの定義
まず、各言語で最小限の「Hello World」APIサーバーを準備する。これらのコードは、余分な機能を含まない、純粋なフレームワークの応答性能を測定するためのベースラインとなる。
Haskell (Yesod)
yesod-hello.hs というファイル名で保存する。
Haskell
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import Yesod
data App = App
mkYesod "App"
instance Yesod App
getHomeR :: Handler Html
getHomeR = pure "Hello from Haskell"
main :: IO ()
main = warp 3000 App
実行コマンド: stack runghc yesod-hello.hs 34
Node.js (Express)
express-hello.js というファイル名で保存する。
JavaScript
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello from Node.js');
});
// このブロッキングエンドポイントは後の実験で使用する
app.get('/blocking', (req, res) => {
// 意図的にCPUを数秒間占有する
const end = Date.now() + 5000; // 5秒間
while (Date.now() < end) {
// busy wait
}
res.send('Blocking task complete');
});
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
実行コマンド: node express-hello.js 35
Go (net/http)
go-hello.go というファイル名で保存する。
Go
package main
import (
"fmt"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello from Go")
}
func main() {
http.HandleFunc("/", helloHandler)
fmt.Println("Server listening on port 3000")
http.ListenAndServe(":3000", nil)
}
実行コマンド: go run go-hello.go 25
6.2. Step 2: テスト環境の準備
高負荷テストを正確に行うためには、環境の準備が不可欠である。
- ハードウェア: 理想的には、テスト対象のサーバーを動かすマシンと、負荷を生成するクライアントマシンを物理的に分ける。これにより、リソースの競合を防ぎ、純粋なサーバー性能を測定できる。両マシンともに、マルチコアCPUを搭載していることが望ましい。
-
OSのチューニング(重要): デフォルトのOS設定は、大量の同時接続を処理するには低すぎる制限が課せられている。特にクライアントマシンで以下の設定変更が必要となる。
-
ファイルディスクリプタ数の上限緩和: ネットワーク接続は、OSレベルではファイルディスクリプタとして扱われる。秒間10万リクエストを捌くには、この上限を大幅に引き上げる必要がある。
Bash
# 現在の上限を確認
ulimit -n
# 上限を一時的に引き上げる (例: 100000)
sudo ulimit -n 100000この設定は現在のシェルセッションでのみ有効である 37。
-
TCP/IPスタックのチューニング: より高度な設定として、カーネルのTCP/IPパラメータを調整することで、高負荷時の性能をさらに向上させることができる。net.core.somaxconn(接続待機キューの長さ)やnet.ipv4.tcp_tw_reuse(TIME_WAIT状態のソケット再利用)などが代表的な項目である。
-
6.3. Step 3: 負荷テストツールの選定と設定 (wrk)
負荷生成ツールとしてwrkを推奨する。wrkはマルチスレッド設計になっており、単一のマシンからでも非常に高い負荷を生成する能力があるため、今回の目的に適している 38。
-
インストール (Ubuntu/Debian系):
Bash
sudo apt-get update
sudo apt-get install build-essential libssl-dev git -y
git clone https://github.com/wg/wrk.git
cd wrk
make
sudo cp wrk /usr/local/bin/38
-
コマンドの構成: 秒間10万リクエストを目標とするコマンド例は以下の通り。
Bash
wrk -t12 -c1000 -d60s http://<server_ip>:3000/- -t12: 12個のスレッドを使用する。負荷生成クライアントマシンのCPUコア数に合わせるのが一般的。
- -c1000: 1000の同時接続を維持する。
- -d60s: テストを60秒間継続する。
- 最終的な秒間リクエスト数(RPS)は、サーバーの応答時間(レイテンシ)に依存する。目標の10万RPSに到達しない場合は、-c(接続数)の値を徐々に増やして調整する。
6.4. Step 4: テストの実行と分析 - 「なるほど!」の瞬間
ベースラインテスト
まず、Step 1で作成した3つの「Hello World」サーバーそれぞれに対して、wrkコマンドを実行し、結果を記録する。wrkの出力で注目すべきは以下の3点である 39。
- Requests/sec: 1秒あたりのリクエスト処理数。これがスループットの指標となる。
- Latency: 応答時間。特に99%の値は、ほとんどのユーザーが体験する最悪の応答時間を示し、性能の安定性を測る上で重要。
- Socket errors: 接続エラーの数。これが多数発生する場合、サーバーまたはクライアントが負荷に追いついていないことを示す。
ブロッキングテスト(本実験の核心)
次に、このレポートの核心的な主張を実証する実験を行う。
-
Node.js (Express) サーバーのみを起動する。このサーバーには、高速な/エンドポイントと、意図的に低速な/blockingエンドポイントが含まれている。
-
ターミナルを2つ開く。
-
ターミナル1で、ブロッキングエンドポイントに対してcurlコマンドを1回だけ実行する。コマンドはすぐには返ってこず、ハングしたように見える。
Bash
curl http://<server_ip>:3000/blocking -
curlを実行した直後に、ターミナル2で、高速なはずのルート(/)エンドポイントに対して、先ほどと同じ高負荷wrkテストを実行する。
Bash
wrk -t12 -c1000 -d60s http://<server_ip>:3000/
予測される結果と分析:
このwrkテストの結果は、ベースラインテストとは劇的に異なるものになるはずである。Requests/secはほぼゼロに近くなり、Latencyは極端に増大し、多数のSocket errors(タイムアウト)が報告されるだろう。
これは、ターミナル1からのたった一つの/blockingリクエストが、Node.jsのシングルスレッドであるイベントループを完全に占有してしまったために発生する現象である。その結果、イベントループはターミナル2からの大量のリクエストを一切処理できなくなり、サーバー全体が無応答状態に陥る。
この単純な実験は、たった一つのCPUバウンドなリクエストがシステム全体の可用性を破壊しうるという、Node.jsのアーキテクチャ上の脆弱性を明確に、そして否定しようのない形で証明する。これこそが、HaskellやGoのプリエンプティブなランタイムが解決する、安定性に関する根本的な問題なのである。この実験を自ら行うことで、理論的な知識は、深い直感的理解へと昇華されるだろう。
Part VII: 結論と戦略的提言
本レポートでは、Haskell、Node.js、Goという3つの主要な技術スタックの並行処理モデルを詳細に分析し、上長から提示された「Haskellのマルチスレッドは工夫されており、安定性に寄与する」という仮説を検証した。以下に、その結論と、将来の技術選定に資する戦略的な提言をまとめる。
調査結果の要約
- 上長の仮説の妥当性: 分析の結果、上長の仮説は技術的に正しいことが確認された。Haskell (GHC) の並行処理モデルは、プリエンプティブ(強制的割り込み型)なスケジューラとM:Nスレッドモデルを採用しており、これにより、一部の処理がCPUを長時間占有した場合でも、他の処理の実行を保証する。このアーキテクチャは、予測不能な、あるいはCPU負荷の高い処理が混在する現実的なワークロードにおいて、Node.jsの協調的(自主的)なシングルスレッドモデルよりも根本的に高い安定性と回復力を提供する。
- Goの位置づけ: Goは、Haskellと同様にプリエンプティブなM:Nスケジューラを持つ、回復力の高いアーキテクチャを採用している。これにより、Haskellと同等の安定性を実現しつつ、より主流の命令型プログラミングパラダイムに基づいた、シンプルで明示的な並行処理API(ゴルーチンとチャネル)を提供している。
- ベンチマークの教訓: TechEmpowerのような生のパフォーマンスベンチマークの数値は、システムの全体像の一部しか示さない。特に、均一な負荷の下での最大スループットは、不均一な負荷に対する「安定性」や「回復力」といった、アーキテクチャの本質的な堅牢性とは異なる指標である。技術選定においては、単一の数値に惑わされることなく、アーキテクチャの挙動とそのトレードオフを深く理解することが不可欠である。
戦略的ガイダンス
以上の分析に基づき、プロジェクトの要件やチームの特性に応じた技術選定の指針を以下に示す。
Haskellを選択すべき場合
- チームが関数型プログラミングの専門知識を有している、または習得に意欲的である場合。
- アプリケーションが、最大限の正当性、型安全性、そしてランタイムによって保証される堅牢性を必要とする場合。例えば、金融システム、複雑なビジネスロジックを持つ基幹システムなど、バグが許容されない領域に適している 2。
- 開発者が並行処理の低レベルな管理から解放され、宣言的な方法で複雑な問題を解決することに価値を見出す場合。
Node.jsを選択すべき場合
- アプリケーションがI/Oバウンドな処理に偏重している場合。例えば、他のマイクロサービスやデータベースへのリクエストを中継するだけのシンプルなAPIゲートウェイなど 41。
- 開発速度が最優先され、プロトタイピングやMVP(Minimum Viable Product)を迅速に構築する必要がある場合。
- チームがJavaScriptのエコシステムとスキルセットを最大限に活用したい場合。
- ただし、採用する際は、CPUバウンドなコードをメインスレッドで実行しないという厳格な規律をチーム全体で徹底する必要がある。重い処理はワーカースレッドにオフロードするなどの対策が必須となる 18。
Goを選択すべき場合
- 高い生のパフォーマンス、シンプルな並行処理、そして迅速なコンパイルによる容易なデプロイメントが主要な要件である場合 41。
- マイクロサービスアーキテクチャにおける汎用的なサービスを構築する場合に、パフォーマンスと開発者のアクセシビリティのバランスが取れた優れた選択肢となる 23。
- C言語のような低レベルな制御と、現代的な言語機能(ガベージコレクション、強力な標準ライブラリ)の両方を求める場合。
最終的な見解
本レポートは、一人のジュニアエンジニアが上長の漠然とした問いに答えるだけでなく、チーム全体の技術的な議論に、より深く、証拠に基づいた視点を提供する一助となることを目指した。最終的にどの技術を選択するかは、単一の「正解」があるわけではなく、プロジェクトの具体的な要件、チームのスキル、そして長期的な保守性といった多角的な視点からのトレードオフ分析によって決定されるべきである。このレポートが、その複雑な意思決定プロセスにおける、信頼できる羅針盤となることを期待する。
引用文献
- Green Threads are like Garbage Collection - FP Block Academy, 10月 27, 2025にアクセス、 https://academy.fpblock.com/blog/2017/01/green-threads-are-like-garbage-collection/
- Yesod Web Framework for Haskell, 10月 27, 2025にアクセス、 https://www.yesodweb.com/
- The Node.js Event Loop, 10月 27, 2025にアクセス、 https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick
- Overview of Blocking vs Non-Blocking - Node.js, 10月 27, 2025にアクセス、 https://nodejs.org/en/learn/asynchronous-work/overview-of-blocking-vs-non-blocking
- Chapter 8. Goroutines and Channels - Shichao's Notes, 10月 27, 2025にアクセス、 https://notes.shichao.io/gopl/ch8/
- How to Handle Concurrency with Goroutines and Channels in Go - freeCodeCamp, 10月 27, 2025にアクセス、 https://www.freecodecamp.org/news/how-to-handle-concurrency-in-go/
- The GHC Runtime System - Edward Z. Yang, 10月 27, 2025にアクセス、 http://ezyang.com/jfp-ghc-rts-draft.pdf
- The New GHC/Hugs Runtime System - Microsoft, 10月 27, 2025にアクセス、 https://www.microsoft.com/en-us/research/wp-content/uploads/1998/01/new-rts.pdf
- Green thread - Wikipedia, 10月 27, 2025にアクセス、 https://en.wikipedia.org/wiki/Green_thread
- why is GHC thread extremely light weight? - Stack Overflow, 10月 27, 2025にアクセス、 https://stackoverflow.com/questions/38683029/why-is-ghc-thread-extremely-light-weight
- Concurrent Haskell - Wikipedia, 10月 27, 2025にアクセス、 https://en.wikipedia.org/wiki/Concurrent_Haskell
- 9.30. Concurrent and Parallel Haskell — Glasgow Haskell Compiler
- 5.4. Using Concurrent Haskell — Glasgow Haskell Compiler 9.15 ..., 10月 27, 2025にアクセス、 https://ghc.gitlab.haskell.org/ghc/doc/users_guide/using-concurrent.html
- The GHC scheduler : ezyang's blog, 10月 27, 2025にアクセス、 http://blog.ezyang.com/2013/01/the-ghc-scheduler/
- Lightweight Concurrency Primitives for GHC - Semantic Scholar, 10月 27, 2025にアクセス、 https://pdfs.semanticscholar.org/67ad/e0320350d1fcf6dbadff29551c9f0f802c00.pdf
- Yesod for Haskellers :: Yesod Web Framework Book- Version 1.4, 10月 27, 2025にアクセス、 https://www.yesodweb.com/book-1.4/yesod-for-haskellers
- The Node.js Event Loop - Platformatic Blog, 10月 27, 2025にアクセス、 https://blog.platformatic.dev/the-nodejs-event-loop
- Don't Block the Event Loop (or the Worker Pool) - Node.js, 10月 27, 2025にアクセス、 https://nodejs.org/en/learn/asynchronous-work/dont-block-the-event-loop
- cpu bound task switches threads while executing NodeJS - Stack Overflow, 10月 27, 2025にアクセス、 https://stackoverflow.com/questions/75567428/cpu-bound-task-switches-threads-while-executing-nodejs
- How to Handle CPU Intensive Loads In Node JS ? - GeeksforGeeks, 10月 27, 2025にアクセス、 https://www.geeksforgeeks.org/node-js/how-to-handle-cpu-intensive-loads-in-node-js/
- Why is Node said to be not ideal for high CPU bounds tasks? - Reddit, 10月 27, 2025にアクセス、 https://www.reddit.com/r/node/comments/1ayl6dh/why_is_node_said_to_be_not_ideal_for_high_cpu/
- Goroutines in Go: A Practical Guide to Concurrency - GetStream.io, 10月 27, 2025にアクセス、 https://getstream.io/blog/goroutines-go-concurrency-guide/
- What are the differences when running goroutines on single thread using GO vs NODE.js : r/golang - Reddit, 10月 27, 2025にアクセス、 https://www.reddit.com/r/golang/comments/17ow9cp/what_are_the_differences_when_running_goroutines/
- Channels - Go by Example, 10月 27, 2025にアクセス、 https://gobyexample.com/channels
- Hello world HTTP server example · YourBasic Go, 10月 27, 2025にアクセス、 https://yourbasic.org/golang/http-server-example/
- Hello World - Go Web Examples, 10月 27, 2025にアクセス、 https://gowebexamples.com/hello-world/
- Round 23 results - TechEmpower Framework Benchmarks, 10月 27, 2025にアクセス、 https://www.techempower.com/benchmarks/
- NET says golang 6x times slower than .net and slower than nodejs - Reddit, 10月 27, 2025にアクセス、 https://www.reddit.com/r/golang/comments/1nxr0q2/net_says_golang_6x_times_slower_than_net_and/
- Best popular backend frameworks by performance of throughput benchmark comparison and ranking in 2025 - DEV Community, 10月 27, 2025にアクセス、 https://dev.to/tuananhpham/popular-backend-frameworks-performance-benchmark-1bkh
- What happened to Haskell on the web framework benchmark game? - Reddit, 10月 27, 2025にアクセス、 https://www.reddit.com/r/haskell/comments/3wb0zx/what_happened_to_haskell_on_the_web_framework/
- Go vs Node.js vs PHP: Which Framework Outperforms the Other in Speed and Efficiency?, 10月 27, 2025にアクセス、 https://leapcell.io/blog/go-vs-nodejs-performance-frameworks
- Performance Benchmark: Node.js vs Go | by Anton Kalik | ITNEXT, 10月 27, 2025にアクセス、 https://itnext.io/performance-benchmark-node-js-vs-go-9dbad158c3b0
- Benchmarks - Fastify, 10月 27, 2025にアクセス、 https://fastify.io/benchmarks/
- Basics :: Yesod Web Framework Book- Version 1.6, 10月 27, 2025にアクセス、 https://www.yesodweb.com/book/basics
- Introduction to Node.js, 10月 27, 2025にアクセス、 https://nodejs.org/en/learn/getting-started/introduction-to-nodejs
- Print hello world using Express JS - GeeksforGeeks, 10月 27, 2025にアクセス、 https://www.geeksforgeeks.org/node-js/print-hello-world-using-express-js/
- HOWTO: Use ulimit command to set soft limits | Ohio Supercomputer Center, 10月 27, 2025にアクセス、 https://www.osc.edu/resources/getting_started/howto/howto_use_ulimit_command_to_set_soft_limits
- wrk for benchmarking and testing - Mark Downie - PoppaString, 10月 27, 2025にアクセス、 https://www.poppastring.com/blog/wrk-for-benchmarking-and-testing
- wg/wrk: Modern HTTP benchmarking tool - GitHub, 10月 27, 2025にアクセス、 https://github.com/wg/wrk
- Writing APIs Without Servant vs. Using Servant - DEV Community, 10月 27, 2025にアクセス、 https://dev.to/surajvatsya/writing-apis-without-servant-vs-using-servant-34gc
- Golang vs Node: Complete Performance and Development Guide for 2025 - Netguru, 10月 27, 2025にアクセス、 https://www.netguru.com/blog/golang-vs-node