现在的位置: 首页 > 综合 > 正文

Research on EventMachine

2013年08月28日 ⁄ 综合 ⁄ 共 9898字 ⁄ 字号 评论关闭

为什么我们需要EventMachine?

我们通常说的Ruby解释器里的Ruby线程是Green Thread:即程序里面的线程不会真正映射到操作系统的线程,而是由语言运行平台自身来调度,并且这种线程的调度不是并行的。

关于Ruby的并发问题这里有一个权威的解释:http://www.igvita.com/2008/11/13/concurrency-is-a-myth-in-ruby

这篇文章提到造成这种情况的原因主要是由于Ruby解释器和VM中GIL(Global Interpreter Lock,如下图所示)的存在,使得Ruby始终无法真正的享受多核带来的好处,尽管在Ruby1.9的解释器已经能够使用多个系统级别的线程,但是GIL为了保证我们代码的线程安全,只允许同一时刻运行一个单一的线程。当然,事情并不是绝对的,下图最右的JRuby则把线程调度的工作交给了JVM从而实现任务的并发执行。

所以,基于Ruby的复杂应用大多采用了这样的一种策略:使用推迟(defer)并发(parallelize)的方法来处理程序中的网络I/O部分,而不是引入线程到应用程序中

EventMachine 就是一个基于Reactor设计模式的、用于网络编程和并发编程的框架。Reactor模式描述了一种服务处理器——它接受事件并将其分发给已注册的事件处理。这种模式的好处就是清晰的分离了时间分发和处理事件的应用程序逻辑,而不需引入多线程来把代码复杂化。

EventMachine 本身提供了操作方便的网络套接字和隐藏底层操作的编程接口,这使得EM在CloudFoundry中被广泛的使用着。接下来,我们将对其机制进行一个简单的说明。

Reactor Pattern

“The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to the
associated request handlers.”——wiki

p.s. 这样的工作方式有些类似于Observer Pattern,但后者只监听一个固定主题的消息。

上述定义可以用下面的图示来描述,其中灰色的部分就是Reactor:

Demultiplexer:是单进程阻塞式的主事件循环(event loop)只要它没有被阻塞,它就能够将请求交给event dispatcher

Dispatcher:负责event handler的注册和取消注册,并将来自Demultiplexer的请求交给关联的event handler。

Event handler:是最终处理请求的部分。

1、一个最简单基于EM的HttpServer的例子

require 'rubygems'
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 

在另一个窗口,输入hello,服务器返回hello:

telnet localhost 10000

Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hello
hello

仔细看一下上面的程式:EM帮我们启动了一个server,监听端口10000,而Echo实例(继承了Connection,用来处理连接)则重写了receive_data方法来实现服务逻辑。

而EM.run实际上就是启动了Reactor,它会一直运行下去直到stop被调用之后EM#run之后的代码才会被执行到。Echo类的实例实际上是与一个File Descriptor注册在了一起(Linux把一切设备都视作文件,包括socket),一旦该fd上有事件发生,Echo的实例就会被调用来处理相应事件。

在CloudFoundry中,组件的启动大多是从EM.run开始的,并且出于在多路复用I/O操作时提高效率和资源利用率的考虑,CF组件往往会在EM.run之前先调用EM.epoll (EM默认使用的select调用),比如:

EM.epoll

EM.run {
  ...

  NATS.start(:uri => @nats_uri) do

    # do something

  end

  ...
 }

2、EM定时器(Timer)

EM中有两种定时器,add_timer添加的是一次性定时器,add_periodic_timer添加的是周期性定时器。

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

#输出:
Tick...
Tick...
Tick...
Tick...
BOOM
The googles, they do nothing

细节:我们在第一个EM::PeriodicTimer代码块中,传入了另外一个代码块:puts “Tick”。这里实际上告诉了 EM 每隔1秒触发一个事件,然后才调用puts代码块,这里的puts代码块就是回调


3、推迟和并发处理

EM#defer和EM#next_tick发挥作用的地方分别是:1、长任务应该放到后台运行;2、一旦这些任务被转移到后台,Reactor能够立刻回来工作。。

EM#defer方法

负责把一个代码块(block)调度到EM的线程池中执行(这里固定提供了20个线程),而defer的Callback参数指定的方法将会在主线程(即Reactor线程)中执行,并接收 后台线程的返回值作为Callback块的参数。

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


Defer #<Thread:0x7fa871e33e08> 
#两秒后
Main #<Thread:0x7fa87449b370> 

执行示意图如下:

EM#defer+Callback的用法:

require 'rubygems'
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

# the return value of op is passed to callback
#2 + 2 == 4

EM#next_tick方法

负责将一个代码块调度到Reactor的下一次迭代中执行,执行任务的是Reactor主线程。所以,next_tick部分的代码不会立刻执行到,具体的调度是由EM完成的。

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 

这里Reactor执行的过程用是同步的,所以太长的Reactor任务会长时间阻塞Reactor进程。EventMachine中有一个最基本原则我们必须记住:Never block the Reactor!

正是由于上述原因,next_tick的一个很常见的用法是递归的调用方式,将一个长的任务分配到Reactor的不同迭代周期去执行。

正常的循环代码:

 n = 0
  while n < 1000  
    do_something
    n += 1
  end

使用next_tick来处理:

require 'rubygems'
require 'eventmachine'

EM.run do
  n = 0  
  do_work = proc{  
    if n < 1000  
      do_something
      n += 1 
      EM.next_tick(do_work)
    else
      EM.stop
    end
  }
  
  EM.next_tick(do_work)  
end

next_tick中的block执行如红色的Task 1所示:

如上图所示那样,next_tick使单进程的Reactor给其他任务运行的机会——我们不想阻塞住Reactor,但我们也不愿引入Ruby线程,所以才有了这种方法。

next_tick在CloudFoundry中应用非常广泛,比如下面Router启动的一部分代码:

  # Setup a start sweeper to make sure we have a consistent view of the world.
  EM.next_tick do
    # Announce our existence
    NATS.publish('router.start', @hello_message)

    # Don't let the messages pile up if we are in a reconnecting state
    EM.add_periodic_timer(START_SWEEPER) do
      unless NATS.client.reconnecting?
        NATS.publish('router.start', @hello_message)
      end
    end
  end

与next_tick同样作用的方法还有EM.schedule,后者会不停地判断当前线程是不是Reactor线程。

next_tick还有个用处:当你通过defer方法把一个代码端调度到线程池中执行,然后又需要在主线程中使用EM::HttpClient来发一个出站连接,这时你就可以在前面的代码段里使用next_tick创建这个连接。

4、EM提供的轻量级的并发机制

EvenMachine内置了两钟轻量级的并发处理机制:DeferrablesSpawnedProcesses。

EM::Deferrable

如果在一个类中includeEM::Deferrable,就可以把CallbackErrback关联到这个类的实例。

一旦执行条件被触发,CallbackErrback会按照与实例关联的顺序执行起来。

对应实例的#set_deferred_status方法就用来负责触发机制:

当该方法的参数是:succeeded,则触发callbacks;而如果参数是:failed,则触发errbacks。触发之后,这些回调将会在主线程立即得到执行。当然你还可以在回调中(callbackserrbacks)再次调用#set_deferred_status,改变状态。

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


#1s 之后:
Go SpeedRacer go

EM::SpawnedProcess

这个方法的设计思想是:允许我们创建一个进程,把一个代码段绑定到这个进程上。然后我们就可以在某个时刻,让spawned实例被#notify方法触发,从而执行关联好的代码段。

它与Deferrable的不同之处就在于,这个block并不会立刻被执行到。

require 'rubygems'
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

#1s之后同时输出前两个,第二秒后输出Periodic
Periodic
Received hello
Periodic

注意这两种机制的使用方式是不一样的:一个是作为内部类include进去进而使用其定义的callback来执行的;而另一个是直接使用spawn实例通过notify来触发代码块来执行的。

5、使用EM简化网络编程

网络编程的简单化是EM的一大特色。拿前面的Echo举例子,我们通过十分简洁的代码就可以实现一个HttpServer的功能。

我们其实还有更简洁的方式来完成这个工作,比如使用module

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

以及直接使用block

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

事实上,每次你新建一个连接,一个新的包含了你代码的匿名类就会被创建。理论上,不同的连接不能互相交换信息,这一点很重要。不过EM实际上在设计时以及解决这个问题:

require 'rubygems'
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

通过给start_server添加一个块,EM会把把Pass的实例传进去(这个操作发生时实例已经被初始化但是客户端数据还没收到)。这样我们可以用这种方法为每个实例set值。

下面我们使用EventMachine建立一个客户端,这是非常简单的事情:

require 'rubygems'
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}"
    puts "Received #{data.length} bytes"
  end
end

EM.run do
  EM.connect('127.0.0.1', 10000, Connector)
end

除了使用EM#connect之外,客户端同服务器端的代码是一样的。其实EM::Connection类中还有很多有用的方法等着你实现:

post_init   当实例创建好,连接还没有完全建立的时候调用。一般用来做初始化
connection_completed   连接完全建立好的时候调用
receive_data(data)   当收到另一端的数据时调用。数据是成块接收的
unbind   当客户端断开连接的时候调用

此外,还有#close_connection#close_connection_after_writing这两个方法供用户断开连接。

下面给出一个更完整的例子,设置最大连接次数:

require 'rubygems'
require 'eventmachine'

 module LineCounter
   MaxLinesPerConnection = 10

   def post_init
     puts "Received a new connection"
     @data_received = ""
     @line_count = 0
   end

   def receive_data data
     @data_received << data
     while @data_received.slice!( /^[^\n]*[\n]/m )
       @line_count += 1
       send_data "received #{@line_count} lines so far\r\n"
       @line_count == MaxLinesPerConnection and close_connection_after_writing
     end
   end
 end

 EventMachine::run {
   host,port = "192.168.0.100", 8090
   EventMachine::start_server host, port, LineCounter
   puts "Now accepting connections on address #{host}, port #{port}..."
   EventMachine::add_periodic_timer( 10 ) { $stderr.write "*" }
 }

6、EventMachine的并发处理能力测试

基于ruby事件驱动的服务器非常适合轻量级的请求,但对于长时间的请求,则性能不佳”。我们下面的例子将告诉你这样的认识其实是不对的。(需要用eventmachine_httpserver来处理http请求和发送响应)

require 'rubygems'
require 'eventmachine'
require 'evma_httpserver'

class Handler  < EventMachine::Connection
  include EventMachine::HttpServer

  def process_http_request
    resp = EventMachine::DelegatedHttpResponse.new( self )

    sleep 2 # Simulate a 2s long running request

    resp.status = 200
    resp.content = "Hello World!"
    resp.send_response
  end
end

EventMachine::run {
  EventMachine::start_server("0.0.0.0", 8080, Handler)
  puts "Listening..."
}

# Benchmarking results:
#
# > ab -c 5 -n 10 "http://127.0.0.1:8080/"
# > Concurrency Level:      5
# > Time taken for tests:   20.6246 seconds
# > Complete requests:      10

这是一个最简单的HTTPserver,我们通过ab(ApacheBench)测试:并发数设置为5(-c 5),请求数设置为10(-n10)。耗时略大于20秒。正如上面所说

Reactor同步地处理每个请求,相当于并发数设置为1。因此,10个请求,每个请求耗时2

require 'rubygems'
require 'eventmachine'
require 'evma_httpserver'

class Handler  < EventMachine::Connection
  include EventMachine::HttpServer

  def process_http_request
    resp = EventMachine::DelegatedHttpResponse.new( self )

    # Block which fulfills the request
    operation = proc do
      sleep 2 # simulate a 2s long running request

      resp.status = 200
      resp.content = "Hello World!"
    end

    # Callback block to execute once the request is fulfilled
    callback = proc do |res|
        resp.send_response
    end

    # Let the thread pool (20 Ruby threads) handle request
    EM.defer(operation, callback)
  end
end

EventMachine::run {
  EventMachine::start_server("0.0.0.0", 8081, Handler)
  puts "Listening..."
}

好了,现在我们使用EM的线程池子来“并发”处理请求。结果还是10个请求,并发数设置为5。总共耗时仅仅4秒有余。就是这样,我们的这个server

发地处理了10个请求。我们还可以通过这个方法来验证下线程池中的线程数量。

前面讲过的Deferable机制其实是以一种没有线程开销的情况下实现并发处理的方法。这种机制的一个典型场景就是,你在一个server中需要去请求另外一个server

require 'rubygems'
require 'eventmachine'
require 'evma_httpserver'

class Handler  < EventMachine::Connection
  include EventMachine::HttpServer

  def process_http_request
    resp = EventMachine::DelegatedHttpResponse.new( self )

    # query our threaded server (max concurrency: 20). this part is deferable
    http = EM::Protocols::HttpClient.request(
              :host=>"localhost",
              :port=>8081,
              :request=>"/"
           )

    # once download is complete, send it to client
    http.callback do |r|
        resp.status = 200
        resp.content = r[:content]
        resp.send_response
    end

  end
end

EventMachine::run {
  EventMachine::start_server("0.0.0.0", 8082, Handler)
  puts "Listening..."
}

# Benchmarking results:
#
# > ab -c 20 -n 40 "http://127.0.0.1:8082/"
# > Concurrency Level:      20
# > Time taken for tests:   4.41321 seconds
# > Complete requests:      40

从测试结果我们可以看到,这个server4s多的时间里处理了40个请求(因为并发量是20,前面监听8081的服务器sleep2s)。这就是EM的魅力:

当你的工作推迟或者阻塞在socket上,Reactor循环将继续处理其他的请求。当Deferred的工作完成之后,产生一个成功的信息并由reactor返回响应。

参考资料:

EventMachine Introduction:http://everburning.com/news/eventmachine-introductions/

EM官方tutorials:https://github.com/eventmachine/eventmachine/wiki/Tutorials

以及这篇著名的博客:http://www.igvita.com/2008/05/27/ruby-eventmachine-the-speed-demon/

抱歉!评论已关闭.