2010年06月17日
並列1000コネクションに耐える! Ruby のイベント駆動ライブラリ Rev と EventMachine の HTTPクライアント
こんにちは、takada-at です。
Rubyのイベント駆動型ネットワークプログラミングフレームワーク Rev と EventMachine で HTTPクライアントを動かしてみました。
イベント駆動型ネットワークプログラミングフレームワークとは何か説明しだすと難しいですが、一言で言うと、以下のようになります。
イベント駆動ライブラリのサンプルでは、サーバーを書くことが多いですが、クライアントの同時接続数の限界に挑戦したかったので、今回は HTTPクライアントを書いてみます。
Ruby のイベント駆動ライブラリとして Ruby Revと EventMachine の2つを使ってレスポンスタイムを計測しました。どちらも独自の HTTPクライアントをライブラリに組み込んでいます。
EventMachine
http://rubyeventmachine.com/
Rev
http://rubyforge.org/projects/rev/
Rev も EventMachine も Python の Twisted や Tornado、Perl の AnyEvent のように、ノンブロッキング I/O を利用することで並行性が高く高速なネットワークプログラムを組むためのフレームワークです。
どちらのライブラリもまだあまり資料がありませんが、内部の実装を見つつサンプルコードを作成してみました。
結論から言うと、Revはとても高速です。
サーバー側では単純に100ms待ってからレスポンスを返すだけのページを用意しておき、クライアントは並列に接続してレスポンスタイムの平均を取りました。要するに、並列数を増やしてもレスポンスタイムが100ms に近ければ近いほど、高速なクライアントということになります。
以下
なお、試験に利用したサーバーは、Python の Tornado で書かれており、十分に高速です。
(EventMachine にはなぜか HTTPクライアントが2つありますが、HttpClient という名前の方はまともに動作しなかったので試験対象から外しました)。
10コネクション
スレッドの方はすでに多少の遅延が派生してます。
100コネクション
EventMachine, Rev はほぼ問題の無い処理時間です。
500コネクション
EventMachine に多少の遅延が発生するようになりました。スレッドはほとんど使いものにならない遅さです。
1000コネクション
EventMachine もかなり高速ですが、1000を少し越えたあたりからまともに動作しなくなります。
一方 Rev は1000コネクション程度でも問題なく動作するようです。
なお注意事項として、この試験をする前に、ファイルディスクリプタの制限を増やしておく必要があります。
デフォルトでは1024以上のファイルディスクリプタを扱うことができず、ソケットの数にも制限がくわえられてしまいます。
Linux では、/etc/security/limits.conf を編集することでユーザーのファイルディスクリプタの上限を増やすことができます。
さらに、TIME_WAIT 状態のコネクションを使い回せるようにtcp_tw_reuse=1 を設定しておくとよいでしょう。
/etc/sysctl.conf に net.ipv4.tcp_tw_reuse = 1 を設定することで、TIME_OUT 状態のコネクションを再利用し、TCPコネクションの上限を引き上げることができます。
以下に今回使用したテストコードを掲載します。
イベント駆動を駆使してるため多少読みにくいですが、どのパターンも 1秒待ってからHTTPリクエストを並列に送信し、レスポンスの時間を計測しています。
(takada-at)
Rubyのイベント駆動型ネットワークプログラミングフレームワーク Rev と EventMachine で HTTPクライアントを動かしてみました。
イベント駆動型ネットワークプログラミングフレームワークとは何か説明しだすと難しいですが、一言で言うと、以下のようになります。
# ふつうのフロー駆動型プログラム Net::HTTP.start(host, port){|http| res = http.get(path) #この処理が終わってから } puts "done" #この次の処理が実行される
# イベント駆動型プログラム client = Rev::HttpClient::connect(host, port) client.get(path) #この処理が終わってないのに puts "not done" #すぐこの行が実行されてしまう一見するとプログラムの複雑さが増えるだけのように思えますが、処理をブロックしないということは無駄がないということでもあります。これによって、
- 複数のファイルディスクリプタを同時に開き
- 読み込み可能なものから処理していく
イベント駆動ライブラリのサンプルでは、サーバーを書くことが多いですが、クライアントの同時接続数の限界に挑戦したかったので、今回は HTTPクライアントを書いてみます。
Ruby のイベント駆動ライブラリとして Ruby Revと EventMachine の2つを使ってレスポンスタイムを計測しました。どちらも独自の HTTPクライアントをライブラリに組み込んでいます。
EventMachine
http://rubyeventmachine.com/
Rev
http://rubyforge.org/projects/rev/
Rev も EventMachine も Python の Twisted や Tornado、Perl の AnyEvent のように、ノンブロッキング I/O を利用することで並行性が高く高速なネットワークプログラムを組むためのフレームワークです。
どちらのライブラリもまだあまり資料がありませんが、内部の実装を見つつサンプルコードを作成してみました。
結論から言うと、Revはとても高速です。
サーバー側では単純に100ms待ってからレスポンスを返すだけのページを用意しておき、クライアントは並列に接続してレスポンスタイムの平均を取りました。要するに、並列数を増やしてもレスポンスタイムが100ms に近ければ近いほど、高速なクライアントということになります。
以下
- Rubyのスレッド + net/http ライブラリ
- Rev/HttpClient
- EventMachine/HttpClient2
なお、試験に利用したサーバーは、Python の Tornado で書かれており、十分に高速です。
(EventMachine にはなぜか HTTPクライアントが2つありますが、HttpClient という名前の方はまともに動作しなかったので試験対象から外しました)。
10コネクション
target: http://hornet.klab.org:8000/ concurrency: 10 net/http + thread avg: 140.5132 EventMachine/HttpClient2 avg: 101.3227 Rev/HttpClient avg: 101.4915
スレッドの方はすでに多少の遅延が派生してます。
100コネクション
target: http://hornet.klab.org:8000/ concurrency: 100 net/http + thread avg: 166.59175 EventMachine/HttpClient2 avg: 110.23086 Rev/HttpClient avg: 103.69137
EventMachine, Rev はほぼ問題の無い処理時間です。
500コネクション
target: http://hornet.klab.org:8000/ concurrency: 500 net/http + thread avg: 2720.349032 EventMachine/HttpClient2 avg: 132.847788 Rev/HttpClient avg: 113.132662
EventMachine に多少の遅延が発生するようになりました。スレッドはほとんど使いものにならない遅さです。
1000コネクション
target: http://hornet.klab.org:8000/ concurrency: 1000 net/http + thread avg: 10968.612545 EventMachine/HttpClient2 avg: 136.243228 Rev/HttpClient avg: 122.174841
EventMachine もかなり高速ですが、1000を少し越えたあたりからまともに動作しなくなります。
一方 Rev は1000コネクション程度でも問題なく動作するようです。
なお注意事項として、この試験をする前に、ファイルディスクリプタの制限を増やしておく必要があります。
デフォルトでは1024以上のファイルディスクリプタを扱うことができず、ソケットの数にも制限がくわえられてしまいます。
Linux では、/etc/security/limits.conf を編集することでユーザーのファイルディスクリプタの上限を増やすことができます。
さらに、TIME_WAIT 状態のコネクションを使い回せるようにtcp_tw_reuse=1 を設定しておくとよいでしょう。
/etc/sysctl.conf に net.ipv4.tcp_tw_reuse = 1 を設定することで、TIME_OUT 状態のコネクションを再利用し、TCPコネクションの上限を引き上げることができます。
以下に今回使用したテストコードを掲載します。
イベント駆動を駆使してるため多少読みにくいですが、どのパターンも 1秒待ってからHTTPリクエストを並列に送信し、レスポンスの時間を計測しています。
(takada-at)
require 'net/http' require 'benchmark' require 'rubygems' require 'rev' require 'eventmachine' require 'thread' class RevHttp < Rev::HttpClient def on_body_data data @content = data end def on_connect super end def request method, path @starttime = Time::now super end def on_request_complete end event_callback :on_request_complete end class EMHttp2 < EventMachine::Protocols::HttpClient2 def connection_completed super end def request args @starttime = Time::now super end attr_reader :starttime end class EventMachine::Protocols::HttpClient2::Request def starttime @conn.starttime end end def start_rev times, host, port, path, callback counter = times loop = Rev::Loop::default sumtime = 0 on_complete = lambda{ counter -= 1 sumtime += Time::now - @starttime if counter==0 #全リクエスト終了 loop.stop end } 1.upto(times) do m = RevHttp::connect(host, port) m.on_request_complete &on_complete #1秒待ってからリクエスト timer = Rev::TimerWatcher::new(1) timer.on_timer{ m.request("GET", path) } m.attach(loop) timer.attach(loop) end loop.run puts "Rev/HttpClient" puts " avg: #{sumtime*1000/times}" end def start_em2 times, host, port, path, callback counter = times sumtime = 0 on_complete = lambda{|res| counter -= 1 sumtime += Time::now - res.starttime if counter==0 #全リクエスト終了 callback.call EventMachine::stop end } EventMachine::run{ 1.upto(times) do m = EMHttp2::connect(host, port) #1秒待ってからリクエスト timer = EventMachine::Timer::new(1){ req = m.get(path) req.callback &on_complete } end } puts "EventMachine/HttpClient2" puts " avg: #{sumtime*1000/times}" end def start_nethttp times, host, port, path, callback Net::HTTP.version_1_2 counter = times sumtime = 0 1.upto(times) do Thread::start{ sleep 1 #1秒待つ Net::HTTP.start(host, port){|http| starttime = Time::now res = http.get(path) sumtime += Time::now - starttime } counter -= 1 if counter==0 puts "net/http + thread" puts " avg: #{sumtime*1000/times}" callback.call end } end end def wait &b cv = ConditionVariable::new mutex = Mutex::new f = lambda{ mutex.synchronize{ cv.signal } } th = Thread::start do mutex.synchronize do cv.wait(mutex) # fがcallされるまで待つ end end b.call(f) th.join end def main times=nil target = { :host => 'hornet.klab.org', :port => 8000, :path => '/' } url = "http://#{target[:host]}:#{target[:port]}#{target[:path]}" times = 10 if times.zero? puts "target: #{url}" puts "concurrency: #{times}" #以下スクリプト実行時の第2引数を見て、実行するコードを変更 wait{|f| start_nethttp(times, target[:host], target[:port], target[:path], f) } if ARGV[1].to_i == 0 e = lambda{} start_em2(times, target[:host], target[:port], target[:path], e) if ARGV[1].to_i == 1 start_rev(times, target[:host], target[:port], target[:path], e) if ARGV[1].to_i == 2 end main ARGV[0].to_i
トラックバックURL
この記事へのコメント
1. Posted by mad-p 2010年06月18日 12:17
せっかくイベント駆動のテストをしているのですから、wait()内でビジーループせずにConditionVariableを使った方がよいと思います。
2. Posted by takada-at 2010年06月18日 17:57
ご指摘ありがとうございます!
ConditionVariable を使用するように修正しました。
ConditionVariable を使用するように修正しました。