EventMachine による I/O 多重化はナンボのものか?

はじめに

 こんにちは、インフラ統括本部の大山裕泰です。今回は、EventMachine によってイベント駆動型なサーバを記述した場合と、従来のフロー駆動型なサーバとで、高負荷環境にいおいてどれだけリソースの使い方に違いがあるのかを調べてみたという話になります。
 すっごい今更な内容ですが、この辺りの内容をキチンと押さえておくと、サーバサイドアプリケーションを記述する際、どのようにサーバを記述すれば(高負荷になった際に)どのマシンリソースが、どのように使われるかがわかるようになって嬉しいと思います。

EventMachine って何?

 I/O 多重化によるイベント駆動 I/O (Event Driven I/O) を実現するためのミドルウェアで Rails などで利用されています。RabbitMQ クライアントの一部 でも使われています。

イベント駆動 I/O って何?

 イベント駆動型プログラミング (Event Driven Programming) と呼ばれるプログラミングパラダイムを用いて I/O 処理を実現したものになります。 イベント駆動型プログラミングは、決められた手続きに沿って順に処理を行うフロー駆動型プログラミングの対抗概念になり、GUI プログラミングのようにユーザからのキー入力やマウス操作を “イベント” という形に抽象化し、これらを処理するコールバックルーチン “イベントハンドラ” をそれぞれ記述してゆきます。
 フロー駆動型プログラムにおける I/O 処理では、ソケットファイルディスクリプタを通してバッファからデータを取り出し、何か処理して、またデータを取り出し… というような感じで、データストリームに対して処理を行ってきました。例によって Echo サーバをフロー駆動な感じで記述すると以下のようになります。

 コネクションの確立、データストリーミング処理、切断処理といった一連の処理が一つの手続きによって記述されているのがわかります。
 これに対して、イベント駆動型プログラムにおける I/O 処理では、こうしたデータストリームをイベントという形に変換してやり、これをユーザが定義したイベントハンドラに処理をさせます。
 EventMachine によって Echo サーバをイベント駆動な感じで記述すると以下のようになります。

 コネクションの確立、データの取得、切断処理がそれぞれイベントハンドラで記述できています。

何が嬉しいの?

 イベント駆動 I/O についてグダグダと書いてきましたが、EventMachine によって得られるもう一つの恩恵 (というか今回はこっちがメイン) として I/O を多重化してくれます。I/O 多重化 (I/O Multiplexing) とは、複数の I/O ストリーム (ファイルディスクリプタ) を一元的に操作する手法で、select / poll / ppoll 等のシステムコールによって実現されます。つまり、一つのプロセス (あるいはスレッド) から複数のファイルディスクリプタを同時に取り扱うことができるようになります。これに対して、先ほどのフロー駆動 I/O による Echo サーバの例では、コネクションの度にスレッドを生成し、ソケットファイルディスクリプタはそれぞれのスレッドが独立して持っています。

 では何故 I/O 多重化なのでしょうか? それは C10K 問題 (C10K Problem) を解消する手法として期待されたためです (C10K 問題については、丁寧に解説してくれているサイトがいくつもあるので、併せて参照してください) 。
 ざっくりと説明すると、凄まじい数のコネクションを受け付けるサーバにおいて、フロー駆動 I/O の例で示したような実装をするとリソースを大量消費してしまい、処理が捌けなくなるよーという内容です。
 どういうことかというと、コネクション毎にプロセス (ないしはスレッド) を生成して、ビジネスロジックを走らせ、レスポンスを返すやり方だと、プロセスを生成する場合、プロセス毎にデータセグメントがコピーされるので大量にメモリが消費され、また大量に発生するコンテキストスイッチによる大量の TLB フラッシュによって大量の CPU 時間をにカーネルに持って行かれてしまいます。スレッドを生成する場合でも、プロセスのスタック領域のコピーや thread_info などカーネル内部に確保されるスレッド用のデータによって線形にメモリが消費され、またプロセスの場合ほどではないにしろ大量にコンテキストスイッチが発生するためコネクションが増えるに従って CPU 時間をロスします。
 多重 I/O では、単一スレッドのプロセスにおいて、複数のファイルディスクリプタを扱うことができるため、上記の問題が回避できます。
 尚、I/O 多重化をイベント駆動プログラミングと絡めて話していますが、イベント駆動 I/O を実現する上で I/O を多重化すると嬉しいことがあるので、たまたま同時に語られることが多いですが、これらはそれぞれ独立した概念で、I/O 多重化はイベント駆動でなければならないということではないです。

検証

 ここで、先ほど紹介しましたフロー駆動で実装した Echo サーバ (normal-echo) と、イベント駆動で実装した Echo サーバに (event-echo) 対してベンチマークを実施し、それぞれのリソースの使い方について確認します。尚、ベンチマークツールには methane さんの echoserver の client を用いました。測定したサーバ環境は以下のとおりです。

* CPU : Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz
* RAM : 4GB
* OS : Ubuntu 14.04
* Ruby : 2.0.0p384

 以下のように “同時実行数” と “スレッド当たりのコネクション数” をそれぞれ 100 から 400 までの条件で実行し、サーバのメモリ消費量を計測しました。

 実行結果は以下のとおりです。緑の棒グラフで示したものが normal-echo におけるメモリ消費量を表し、青色が event-echo におけるメモリ消費量を表しています。

 normal-echo の方はコネクション数が増えるに従ってメモリ消費量が増えているのに対して、EventMachine で実装した方はほぼ一定値で推移しています。
 また、スループット (処理したリクエスト数の秒平均値) を以下に示します。例によって、normal-echo の結果を緑、event-echo の結果を青で示しています。


 
 今回はまたま全ての結果が EventMachine で実装した方が優れていますが、あらゆるケースにおいて EventMachine が優れているわけではありません。
 normal-echo における各 CPU コアの利用率を表したものを以下に示します。

 全てのコアがまんべんなく利用できているのがわかります。これに対して event-echo における各 CPU コアの利用率を表したものが以下になります。


 
 特定のコアに処理が集中しているのがわかります。というのも、I/O 多重化によって一つのスレッドで全ての処理を捌いているので、マルチコアの恩恵が受けられていないためにこのようになっています。
 なので、先ほど示したスループットの結果は実行環境やワークロードによって変わってきます。今回利用したベンチマークツールでは、1 コネクション当り、6 byte のショートメッセージ “hello\n” をデフォルトで 100 回送るというもので、サーバも Echo サーバなのでコンテキストスイッチの CPU コストよりも Echo サーバのロジックが軽量だったため、上記のようになったと思われます。

終わりに

 今回は、イベント駆動プログラミング、I/O 多重化、そして I/O 多重化によって、どのリソースがどのように消費されるかについて見てきました。
 ここでの内容によって、効果的にリソースを消費するサーバ実装の手助けになれば幸いです。


PAGE TOP