2010年12月31日金曜日

【翻訳】EventMachine入門

dan sinclairさんのEventMachineの入門記事(PDF)を翻訳しました。
原文はここからダウンロード可能です: http://everburning.com/news/eventmachine-introductions/
(翻訳の公開と画像の利用は本人より許諾済みです)

翻訳・内容の間違い等があればブログコメントやTwitterなどで遠慮無くご指摘ください。

EventMachine入門



Introduction


 うん、これから何を学ぶことになるのか、この導入のくだりがスタート地点として役に立つと思う。EventMachine とは何だろう。そしてそれは私たちのために何をしてくれるのだろう。さて、最初の部分は簡単だね。EventMachine は Reactor パターン(*1)の高性能な実装さ。

すげえ、いや、ちょっと待て、Reactor パターンって何だ? Wikipedia によると:

Reactor デザインパターンとは、一つ以上の入力による、サービスハンドラへ送られた同時並行なリクエストを扱うための並列プログラミングパターンです。サービスハンドラは入ってくるリクエスト群をより分け、関連するリクエストハンドラに同期的に送りつけます。

基本的に、EventMachine(EM) はソケットの受信待機、ネットワーク接続の確立、タイマー処理、そしていくつかの並列プリミティブを含んだ低レベルなもの全てを扱う。EM は高性能なAPIサーバ/サービスを作るのに必要な多くの中核機能を提供する。また EventMachine は Dan Kegel 他による C10K 問題(*2)の教訓を考慮に入れている。



ネットワーク処理能力と共に、EM は長時間かかるタスクをバックグラウンドなスレッドに回すことができるスレッドプールを提供する。これはアプリケーションを素早い・応答性の良いものにしてくれる。みんな応答性は好きだよね? もちろんスレッドプールを使うことに関してはトレードオフがあって、ほとんどは Ruby1.8 のグリーンスレッド(OS ではなく VM がスケジュールするスレッド)の実装に関係するんだ。

訳注: Ruby1.9ではネイティブスレッドになりましたが、Global Interpreter Lock により完全に並列実行されるわけではありません。ただしグリーンスレッドよりもスレッド切り替えコストは軽減されています)

最初に、簡単な例を見てみようか。EM におけるHello World、エコーサーバを例として使ってみよう。
緑色のテキストはサーバからの応答だから注意してね。

require 'eventmachine'

class Echo < EM::Connection
  def receive_data(data)
    send_data(data)
  end
end

EM.run do
  EM.start_server("0.0.0.0", 10000, Echo)
end
Rei:~ dj2$ telnet localhost 10000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
helo
helo
goodbye cruel world
goodbye cruel world

詳細は後に見ていくけど大雑把に言うと、ポート10000番上で開始されたサーバは接続を確立したときに、用意しておいた Echo クラスを使っている。ポートにデータが届いた時、EM は receive_data メソッドを実行し、send_data を呼ぶことによって、受け取ったデータをクライアントにエコーし返す。

本質的には、Echo クラスのインスタンスはファイルディスクリプタに結びつけられている。ファイルディスクリプタに動きがあればいつでも、君のクラスのインスタンスはそのアクションをハンドルするために呼ばれるのだ。これがほとんどの EventMachine プログラミングの基礎であり、reactor はファイルディスクリプタを監視し、君のクラスのインスタンスにあるコールバックを実行する。

注目すべき興味深いことは、我々の Echo クラスにはどんなネットワーク原理の概念もないってことだ。実装した Echo#receive_data にはパケットやヘッダの概念はないけれど、ネットワークのデータを取り出すことができる。


Getting Started


 導入はやりのけたので、EventMachine の楽園へのユカイな道のりを始められる。君が邁進するのに使えるいくつかの基本的タスクとコマンドがあるから、そこを原点として始めようか。

最初に、最も簡単で最もばかげた EM アプリケーションを作ってみよう。

require 'eventmachine'

EventMachine::run

ちっとも刺激的じゃない例だが、良い出発点だ。注意が2、3ある。まず最初に、EventMachine を利用するには、eventmachine を require しなくちゃならない。2番目に、抜け目ない人ならお気づきだろうが、最初の例では EM. を使っていたが今回は EventMachine:: を使った。これは EM:: でも EventMachine. でもいい。どれも同等の書き方だ。個人的には EM. が短くて簡潔なので好きだ。最後に、もしこの例を実行させたなら、決して終了しなかったことに気づくだろう。EM#run を呼ぶと、EventMachine reactor は開始され、止まれと言われるまではハッピーに走り続ける。そして実際に今は止まれと言わなかった。

どうやってこの野獣ちゃんを止めたらいい? とっても簡単。

require 'eventmachine'

EM.run do
  EM.add_timer(1) { EM.stop }
end

こっちでは EM#run にブロックを渡している。ブロックの中身は reactor が初期化されたあと、 reactor が走り始める前に実行される。必要な初期化処理があれば、なんでもこのブロックに入れることができる。この場合は、時が来たら EM#stop を実行してくれるタイマーを作成している。EM#stop の代わりに EM#stop_event_loop を呼ぶこともできる。この二つのメソッド呼び出しはどちらも同じもので、reactor を終了させる。reactor が終了すれば、EM#run ブロックのうしろに書かれたコードが実行される。

Timers

 ツアーの次の停留所は EventMachine タイマーだ。タイマーには二つのタイプが存在する。一回限りのタイマーと、周期的なタイマーだ。EventMachine にタイマーを加えるには二つの異なる、だが同等の効果の方法がある。一般的な方法としては、下記で見るように EM#add_timerEM#add_periodic_timer を使うやり方だ。

require 'eventmachine'

EM.run do
  EM.add_timer(5) do
    puts "BOOM"
    EM.stop_event_loop
  end
  EM.add_periodic_timer(1) do
    puts "Tick ... "
  end
end

titania:examples dj2$ ruby timer.rb
Tick...
Tick...
Tick...
Tick...
BOOM



この例では二つのタイマーをシステムに追加する。周期的なタイマーは多くとも1秒に一度実行され、一回限りのタイマーは少なくとも5秒後には実行されるだろう。この(「多くとも」とか「少なくとも」みたいな)言葉づかいは超重要だ。EventMachine はタイマーがいつ実行されるのか保証しない。指定したフレーム時間で実行されるかもしれないだけだ。

以下の同等の例では、メソッドの代わりにクラスを用いている。

require 'eventmachine'

EM.run do
  EM::Timer.new(5) do
    puts "BOOM"
    EM.stop
  end
  EM::PeriodicTimer.new(1) do
    puts "Tick ..."
  end
end

機能面ではこれらの二つの例は同一だ。私は最初に見た方、EM#add_timerEM#add_periodic_timer の方をより多く使う。

訳注:以下の問題は現在の EventMachine(0.12.10) では存在しません。
 EM#add_periodic_timer の返り値は EM::PeriodicTimer オブジェクトであり、
 EM#cancel_timer にそれを渡すと中で EM::PeriodicTimer#cancel メソッドを呼んでくれます)

メソッドではなくクラスを使用しなければならない場合も一つだけある。それは周期的なタイマーをキャンセルする場合だ。 EM#add_timer の返り値のシグネチャを EM#cancel_timer に渡して実行すると、一回限りのタイマーはキャンセル出来る。周期的なタイマーでは、タイマーが再スケジュールされる度に、毎回新しいシグネチャを受け取ってしまうという問題があるんだ。そして、シグネチャを知らなければタイマーをキャンセルことができない。

もし周期的なタイマーをキャンセルさせる必要があるなら、EM::PeriodicTimer を使う必要がある。これは EM::PeriodicTimer#cancel というメソッドを提供しているんだが、もうおわかりの通り、周期的なタイマーをキャンセルできる。

require 'eventmachine'

EM.run do
  p = EM::PeriodicTimer.new(1) do
    puts "Tick ..."
  end

  EM::Timer.new(5) do
    puts "BOOM"
    p.cancel
  end

  EM::Timer.new(8) do
    puts "The googles, they do nothing"
    EM.stop
  end
end
titania:examples dj2$ ruby timer_cancel.rb
Tick...
Tick...
Tick...
Tick...
BOOM
The googles, they do nothing



Deferring and Delaying Work

 Reactor パターンの定義を思い返してみると、 reactor はシングルスレッドだったのに気づくだろう。EventMachine もこのシングルスレッドなアプローチの reactor に当てはまる。reactor 自身はシングルスレッドであり、reactor で扱う EM のメソッドはスレッドセーフではない

これは2つの結果になる。まず第一に、おそらく私たちは長い時間かかるコードを持つってことだ。データベースクエリや、リモートHTTPリクエスト、その他もろもろ。効率のためには、これらのコードをバックグラウンドのスレッドにやらせる必要がある。第二に、コードをバックグラウンドのスレッドに追いやったら、それを動かしてくれるように reactor に命令できるようにする必要もある。

ここで EM#deferEM#next_tick のおでましだ。

EM#defer を使うと EventMachine のスレッドプールへ、実行するコードブロックを一つのスレッドとしてスケジュールすることができる。このスレッドプールは(デフォルトでは)最大で20個までのスレッドを提供する。やべぇ、俺らのコードをバックグラウンドで走らせたら、その結果をどうやってクライアントに伝えるんだ? もし必要なら、EM#defer は第二のパラメータをとる。callback だ。このコールバックはメイン reactor スレッド上で実行され、スレッドプールに回された操作の返り値を提供する。我々は、クライアントとコミュニケーションするために、このコールバックを使うことができる。

EM#defer には悪い面もあって、Ruby1.8 のスレッドの実装で動かさなきゃいけないということだ。 Ruby には本当のOSスレッドがない。グリーンスレッドと呼ばれるものならある。(一つのRubyプロセスの)全てをその上で動かすOSスレッドが一つあり、Ruby はそのOSレベルスレッド上で自分のスレッドを作成する。これは、 Ruby が全てのスレッドスケジューリングを扱っていることを意味していて、そしていずれにしてもあなたのプロセスがブロックするだろうOSレベルスレッドをつかむ。だから、あなたが延期処理を行うどのスレッドの中でも、Ruby をブロックしないと確信できるように注意しなくちゃならない。じゃないと全体をブロックしてしまう。

(訳注:先述の通り、Ruby1.9ではネイティブスレッドになっています。1.9ではOSがスレッドスケジューリングを扱いますが、GILにより一度に走るスレッドは一つだけです。よって注意すべき点は変わりません)

EM#next_tick を使うと、次の reactor ループイテレーションで発生するように、実行するコードブロックをスケジュールできる。実行はメイン reactor スレッドで起こる。事実上この遅延は瞬時に起っていると考えることができて、これは本来コードが異なるスレッドで動作することを意図してスケジュールしている。EM はスレッドセーフではないから、スレッドプールのとあるスレッドから EM#defer を使おうとすると、深刻な問題に陥ったりクラッシュする可能性がある。

例えば EM#defer なスレッドで何かを動かしていて、EM::HttpClient を使って新しい接続を確立する必要があるとしたら、それはメインスレッド上で行う必要がある。となると接続の作成は、EM#next_tick へ渡すコードブロックの中ですればいいわけだ。

EM#next_tick

 EM#next_tick を使うことは、EM#add_timer を動かすのにとてもよく似ている: EM#next_tick に渡した特定のコードブロックは、reactor ループの次のイテレーションで実行される。

require 'eventmachine'

EM.run do
  EM.add_periodic_timer(1) do
    puts "Hai"
  end
  EM.add_timer(5) do
    EM.next_tick do
      EM.stop_event_loop
    end
  end
end
titania:examples dj2$ ruby next_tick.rb
Hai
Hai
Hai
Hai



EM#next_tick でスケジュールされたものは、メインスレッドで同期的に実行される。EM#next_tick ブロック内のどんな長い時間かかるタスクも、それの実行を終えるまでプログラム全体をブロックしてしまう。通常、これはよくないことだ。

EM#defer

 同様に、EM#defer を使うことは、その実行がバックグラウンドスレッドで終了した後に、メインスレッドで実行されるコールバックを提供する能力を与えてくれる。また、コールバック関数は渡さなくてもよい。

require 'eventmachine'
require 'thread'

EM.run do
  EM.add_timer(2) do
    puts "Main #{Thread.current}"
    EM.stop_event_loop
  end
  EM.defer do
    puts "Defer #{Thread.current}"
  end
end
titania:examples dj2$ ruby defer.rb
Defer #<Thread:0x5637f4>
Main #<Thread:0x35700>




EM#defer に渡したブロックは、バックグラウンドスレッドで実行が開始され、ゴキゲンに走り続ける。注意だが、あなたのコードはこのスレッドを永遠に占有しないようにしなければならない。EventMachine はこの状態を検知しないので、スレッドが無期限に実行されてしまう。スレッドプールのサイズは決っているため、そうやってスレッドを失ったら、その分取り戻せなくなる。

また、コードが走り終わったあとにコールバック関数を実行するという EM#defer の機能を利用できる。コールバック関数に仮引数を指定していたならば、コードの返り値がそこに渡されてから実行される。

require 'eventmachine'

EM.run do
  op = proc do
    2+2
  end
  callback = proc do |count|
    puts "2 + 2 == #{count}"
    EM.stop
  end
  EM.defer(op, callback)
end

Rei:EventMachine dj2$ ruby defer_callback.rb
2 + 2 == 4



ここでは二つの数字を足す proc オブジェクトを作成している。この合計の結果は、EM#defer スレッドでの実行から返ってきて、コールバックへと渡される。そしてコールバックはメイン reactor スレッド上で実行される。このコールバックは結果を出力している。

Lightweight Concurrency

EventMachine は軽量な並行処理(Lightweight Concurrency)(*3)を扱うための2つのメカニズムを内蔵している。spawned process と deferrable だ。注意して欲しいのは、spawned process なんて呼ばれてはいるものの、OSのプロセスとは違うってことだ。この名前は Erlang から来ているのだが少し紛らわしい。

それら二つのメカニズムにひそむ主要なアイデアは、CPU やメモリにとってより軽く、そして標準のRubyスレッドであるってことだ。軽量の並列動作における多くの働きは、あなたのアプリケーション側でハンドルされる必要がある。deferrable と spawned process によるコードはあなたのアプリケーションから実行要求が来るまで、実行されない。

これらのメカニズムがどう動くのか、見て行こう。

EM::Deferrable

EM::Deferrable(*4) を見ていくところから始めよう。あなたのクラスに EM::Defferable をミックスインしたとき、そのクラスのインスタンスには callback と errback 関連の能力が提供される。あなたは実行される callback と errback をいくらでも定義できる。callback と errback は、インスタンスに追加された順番で実行される。

callback と errback を発動させるためには、インスタンスオブジェクトの #set_deferred_status を呼び出す。そのメソッドには :succeeded:failed を渡すことができて、:succeeded なら callback が、:failed なら errback が発動する。これらのブロックは、メインスレッド上ですぐさま実行されるだろう。また、deferrable オブジェクトに状態をセットするために、#succeed#fail というシンタックスシュガーも用意されている。

一旦オブジェクトの状態が指定されると、その後で追加されるどんな callback や errback も、追加された途端にその状態に応じてただちに実行される。

これが実際にどう動くか見てみよう。

require 'eventmachine'

class MyDeferrable
  include EM::Deferrable
  def go(str)
    puts "Go #{str} go"
  end
end

EM.run do
  df = MyDeferrable.new
  df.callback do |x|
    df.go(x)
    EM.stop
  end
  EM.add_timer(1) do
    df.set_deferred_status :succeeded, "SpeedRacer"
  end
end
titania:EventMachine dj2$ ruby deferrable.rb
Go SpeedRacer go



ここで何が起っているか注意深く見てみよう。最初に EM::Deferrable をクラスに include している。これで deferrable するのに必要なメソッドがミックスインされる。

普通に MyDeferrable クラスを作成して、そのインスタンスで #callback#errback を呼んでいる。callback と errback のブロックには、仮引数をいくらでも指定できる。処理が成功したか失敗したかが決まった時、インスタンスの #set_deferred_status を実行している。この場合では、:succeeded を渡し、callback には一つの仮引数を指定して定義したから、"SpeedRacer" が callback に渡される。

デフォルトでは、全ての callback が同じ引数で実行される。callback 内で #set_deferred_status をもう一度呼び、異なる引数を渡すことで、それ以降の callback には違う引数を渡すことも可能だ。これは deferrable のドキュメントでより詳しく解説される。

deferrable なオブジェクトを使うために、わざわざクラス全体を実装する必要がない場合もあるだろう。そういう場合のため、 EventMachine は EM::DefaultDeferrable というクラスを提供している。カスタムクラスでやる代わりに、EM::DefaultDeferrable.new をただするだけで、 EM::Deferrable をミックスインしたものと全く同じ動作をしてくれる。

EM::SpawnedProcess

EventMachine の spwened process は、Erlang のプロセスにインスパイアされている。OSのプロセスではないのにプロセスというネーミングには少し混乱させられるが。spawned process が持つアイデアは、プロセスを作成できて、それにコードを付加することができるということだ。未来のいつかの時点で、spawn されたオブジェクトの #notify メソッドを呼び出すことで、付加したブロックを実行することができる。

deferrable と異なりコードブロックはすぐ実行されないが、#notify 呼び出しがあったならいずれは実行される。

require 'eventmachine'

EM.run do
  s = EM.spawn do |val|
    puts "Received #{val}"
  end

  EM.add_timer(1) do
    s.notify "hello"
  end
  EM.add_periodic_timer(1) do
    puts "Periodic"
  end
  EM.add_timer(3) do
    EM.stop
  end
end
Rei:EventMachine dj2$ ruby spawn.rb
Periodic
Received hello
Periodic



Network Fun

さあて、イイモノの紹介に入ろう。EM は、ネットワークプログラミングを扱うように設計されている。どんなプロトコルだろうと、一連のベースプロトコル実装だろうが、その能力はAPI開発者やサーバ開発者に楽をさせる。

Servers

サーバーサイドから始めることにしよう。クライアント側のコードとはとてもよく似ているけど、それは次のセクションでやる。

イントロダクションで使ったエコーサーバの例に戻ってみよう。

require 'eventmachine'

class Echo < EM::Connection
  def receive_data(data)
    send_data(data)
  end
end

EM.run do
  EM.start_server("0.0.0.0", 10000, Echo)
end
Rei:~ dj2$ telnet localhost 10000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
helo
helo
goodbye cruel world
goodbye cruel world



君にも分かるとおり、EM::Connection クラスを継承して Echo クラスを作成している。そして EM#start_server メソッドを使い、さっき定義した Echo クラスを用いて、全てのインターフェースを監視する、ポート10000番上のサーバを作っている。

おもしろいことに、これと同じコードを書く方法がなんと二つ以上存在している。どれを使うかは好きにしていい。

モジュールを使うこともできる:

require 'eventmachine'

module Echo
  def receive_data(data)
    send_data(data)
  end
end

EM.run do
  EM.start_server("0.0.0.0", 10000, Echo)
end

またはブロックでも:

require 'eventmachine'

EM.run do
  EM.start_server("0.0.0.0", 10000) do |srv|
    def srv.receive_data(data)
      send_data(data)
    end
  end
end

3つの全ての例で、新しいコネクションが確立したとき、新しい匿名のクラスがあなたのコードを取り込んで作成される。これは大事なことで、コネクション毎にあなたのクラスの新しいインスタンスを持つことになるってことだ。インスタンスは次の接続の時には存在しないので、コネクション間でインスタンス内には何も保存することはできない。

サーバとして機能させるためには #receive_data(data) という一つのメソッドを実装する必要がある。もし #receive_data を実装しなかったら、"............>>>6" みたいなものがコンソールに吐出されるのを見るハメになる。もし君が私に似ているなら、それがどこから来ているのか調べるために30分費やすことになるだろう。

#receive_data のようにクライアントとの接続がある間に呼び出される、いくつかのメソッドが他にもある。

post_init
インスタンスの初期化時、コネクションが完全に確立する前に呼ばれる。
connection_completed
コネクションが完全に確立した後に呼ばれる。
receive_data(data)
クライアントからデータを受け取った時に呼ばれる。データはチャンクで受け取る。チャンクの組み立てはあなたの責任だ。
unbind
クライアントとの接続が完全に切れたときに呼ばれる。

私たちの例では、クライアントに返送するために #send_data(data) を使った。巨大なデータファイルを送りたいなら、巨大なデータのチャンクを送り出すのに便利なメソッドである #send_file_data(filename) を使うこともできる。

最後に、ここには示されていないが #close_connection#close_connection_after_writing という二つの便利なメソッドがある。これら2つのメソッドはとても似た操作を行う。両方ともクライアントが接続を閉じた時にシグナルを飛ばす。#close_connection_after_writing は接続が閉じられる前に #send_data で送った全てのデータを確実にクライアントに送るという点で異なる。

以前に言及したように、接続と接続の間で君のクラスのインスタンスはどんな情報も共有できない。幸運にも、いや、設計されたことだろうが、EventMachine はこれを処理するメカニズムを提供する。

require 'eventmachine'

class Pass < EM::Connection
  attr_accessor :a, :b
  def receive_data(data)
    send_data "#{@a} #{data.chomp} #{b}"
  end
end

EM.run do
  EM.start_server("127.0.0.1", 10000, Pass) do |conn|
    conn.a = "Goodbye"
    conn.b = "world"
  end
end
titania:~ dj2$ telnet localhost 10000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
mostly cruel
Goodbye mostly cruel world

EM#start_server へ渡されたブロックの中で、Pass インスタンスは自身が初期化された後、クライアントからデータを受け取る前に、EventMachine から値が渡されている。このどんな状態でもセットされたインスタンスを、必要ならば使うことができる。

Clients

ひとたびサーバを起動したならば、クライアントでそれに接続するのが有益だろう。幸い、サーバの動作を知った後なので、魔法を起こすのに何が必要か大部分は知っているよね。

require 'eventmachine'

class Connector < EM::Connection
  def post_init
    puts "Getting /"
    send_data "GET / HTTP/1.1\r\nHost: MagicBob\r\n\r\n"
  end

  def receive_data(data)
    puts "Received #{data.length} bytes"
  end
end

EM.run do
  EM.connect("www.postrank.com", 80, Connector)
end
titania:EventMachine dj2$ ruby connect.rb
Getting /
Received 1448 bytes
Received 1448 bytes
Received 1448 bytes
Received 1448 bytes
Received 2896 bytes
Received 1448 bytes
Received 1448 bytes
Received 935 bytes

EM#connect によりコネクションが作成される。このコード例は、上でサーバを作成したときに述べられた考えに従っている。コールバックメソッドは同じ名前を持ち、サーバにデータを送るメソッドも、サーバがクライアントにデータを送るときと同じ #send_data だ。


Conclusion


EventMachine の連続公演はこれにておしまい。私たちは、物事の成し遂げ方と、EM#runを使ってそれが走るのを見た。一回限りのタイマーと、周期的なタイマーを作成すること。Deferring と next_tick のコードブロック。deferrable と spawned process をクライアントとサーバで作成すること。

たぶん、全てを見たあとでは、あなたは Eventmachine がどう機能するか、そしてあなたのアプリケーションでどう使えばいいのか、より理解が深まったことだろう。

何か質問やコメントがあるならば、遠慮なく私に連絡を。: dan(at-mark)aiderss.com


Resources


http://rubyeventmachine.com/
http://github.com/eventmachine/eventmachine/tree/master
http://groups.google.com/group/eventmachine/topics
http://eventmachine.rubyforge.org/
http://www.igvita.com/2008/05/27/ruby-eventmachine-the-speed-demon/
http://20bits.com/articles/an-eventmachine-tutorial/
http://nutrun.com/weblog/distributed-programming-with-jabber-and-eventmachine/
http://www.infoq.com/news/2008/06/eventmachine
http://everburning.com/news/playing-with-eventmachine/
http://blog.nominet.org.uk/tech/2007/10/12/dnsruby-and-eventmachine/
http://devver.net/blog/2008/10/sending-files-with-eventmachine/
http://en.oreilly.com/rails2008/public/schedule/detail/1820
http://nhw.pl/wp/2007/12/07/eventmachine-how-to-get-clients-ip-address
http://simonwex.com/articles/2008/10/22/eventmachine-http-client
http://adrianhosey.blogspot.com/2009/01/eventmachine-tcp-server-module-for-ruby.htm


*1 http://en.wikipedia.org/wiki/Reactor_pattern
*2 http://www.kegel.com/c10k.html
*3 http://rubyeventmachine.com/browser/trunk/docs/LIGHTWEIGHT_CONCURRENCY
*4 http://rubyeventmachine.com/browser/trunk/docs/DEFERRABLES


0 件のコメント:

コメントを投稿

フォロワー