node.js と thread hog の話(2)
2012.10.15
[前回までの話へのリンク]
・node.js と thread hog の話(1)
最近になって「c10k 問題」が広く知られるようになったが、実際には、前回書いたように、thread を使いすぎるプログラム(thread hog なプログラム)はスケーラビリティが悪いということは、当時(90年代の終わりごろ)でもすでに「知る人は知る」問題になっていた。
複数のクライアントマシンとの間のソケットを開きっぱなしにしておく、Proxy Server、Chat Server、MMORPG に関わっている人達の間で、ソケット一つに thread を一つ割り当てるスタイルのプログラミングがスケーラビリティに欠けることが知られるようになったのもこのころである。
当時、Microsoft で MSN Messenger を作っている知り合いが「ついに1万人が同時接続しても大丈夫なアーキテクチャに到達した」と自慢しているので、詳しく聞いてみると、単に非同期な API である WinSock を使って single thread 化しただけだったと聞いて驚いたことを良く覚えている。それぐらい非同期 API を使いこなすことはまれだったのである。
thread はそもそも、process を使った並列処理よりも軽く並列処理をさせるために発明されたものだ。データベースやファイルシステムに頻繁にアクセスする必要があるプログラムの場合、すべてをシーケンシャルに実行するとディスクにアクセスしている時間CPUがアイドル状態になってしまうので、複数の thread にタスクを分散することにより、CPU をより有効に使おう、という発想である。
同じ process に属する thread たちはメモリー空間を共有しているため、データのやりとりはメモリを使って高速にできるし、仮想メモリー空間も1process 分(32-bit CPU の場合システム領域も含めて 4GB)で十分である。thread ごとに必要なのは、thread 切り替え時にレジスタを退避させておくメモリ(数十バイト)とスタック(OS の設定にもよるが 2MB〜8MB 程度)のみであり、process を複数走らせるよりは大幅に軽い。
数 MB のメモリなどは、仮想メモリー空間全体と比べれば遥かに小さく、thread を数個〜数十個走らせたところで、そのオーバーヘッドはたかが知れている、と考えられていたのだ。
proxy server や chat server の場合も、「開いたソケットから送られて来るデータを待つ」という処理を先の「ファイルにアクセスする」「データベースにアクセスする」と同じく「CPUがアイドル状態になってしまう時間がかかる処理」と見なせば、同じ様に複数の thread を使うことにより多重化する、というのは当然の発想である。初期の proxy server や chat server がそんなアーキテクチャで作られていたのもそれが理由だ。
しかし、実際にそんな server をネット上で運営すると、すぐに壁に突き当たる。たとえ thread 一つあたりに必要な仮想メモリのサイズが 2MB であっても、そんなサーバーに1000人が同時に接続すれば、2GB が必要となり、32-bit OS が1プロセスあたりに割り当てることのできる仮想メモリのサイズを超えてしまう。
そこで使われる様になったのが、非同期 Socket API を使った多重化というテクニックである。メインの thread 一つだけを走らせ、それがイベントループで、開いた socket からのイベントを順に処理して行く、というアーキテクチャである。
ただし、このアーキテクチャを採用した場合、イベント処理中にディスクやデータベースにアクセスすると、そこでCPUがアイドル状態に入ってしまうため、その手の時間がかかる処理すべてに非同期APIを使い、イベントループで、ファイルシステムやデータベースからのイベントを受けてイベントハンドラーに渡す形にしなければならない。データベースへのアクセスが同期型のものしかないシステムには適さないアーキテクチャだ。
当時、proxy server や chat server が非同期APIを使った single-thread なアーキテクチャに移行したにも関わらず http server がそちらに移行せずに mutli-thread なアーキテクチャのままでいたのにはいくつかの理由がある。
前回書いた様に、多くのプログラマーが非同期APIをアレルギーのように嫌っていたのも理由の一つである。特に、当時は非同期プログラムを連続的に書くことを許すクロージャが普及していなかったこともあり、非同期プログラミングはとても不便であった。私が IIS に対抗して作った single-thread アーキテクチャの micro-server も、非同期 API を呼び出す前には、イベントハンドラーに渡すべき情報(コンテキスト)を明示的に一所に集めて渡す必要があり、かつ、結果を処理するイベントハンドラーは呼び出し側とは別の場所に書いてある、という煩雑なプログラミングが必要であった。
もう一つの理由は、http server の場合、prozy server や chat server と違って、get や post を処理する間だけ thread を走らせれば良いため、例え同時に1000人の人がウェブページを閲覧していたとしても、実際の同時アクセス数はそれよりも遥かに少ない、という特徴があったからである。
また、当時はそれぞれの CPU の能力も、今のサーバー用の CPU と比べると2桁ぐらい性能が劣っていたため、thread の作り過ぎによる問題が出る前に、他の部分がボトルネックとなって、(ユーザーの増加に応じて)サーバーの台数を増やさざるを得ない状況になっていた、という面もあったのだと私は解釈している。
(つづく)
Comments