By Ilya Grigorik on May 27, 2008
http://www.igvita.com/2008/05/27/ruby-eventmachine-the-speed-demon/
Ruby EventMachine,作为一个框架既有大批拥趸(Evented Mongrel,Analogger, Evented Starling)也饱受非议。一定程度上,人们对框架的恐惧、不安和疑虑(FUD)源于框架的实现语言(Ruby)和底层实现原理(Reactor pattern)的不匹配。Reactor pattern 是一个并发编程模型:接受服务请求,并发地分配到服务处理器。服务处理器根据不同的请求同步分发到相关联的请求处句柄(Request handler)
为什么从Reactorpattern开始?
由于web服务器一般由多进程/多线程实现的意识根深蒂固,当我在几年前加入到滑铁卢大学的一个研究项目之后感到相当吃惊:我们对不同的web服务器架构进行了测试,最出色的集中服务器都是事件驱动(event-driven)的架构。在和所有的人进行了无休止的探讨之后我很快意识到:在每秒有成千上万甚至十万百万个请求的并发环境中,创建进程或者线程切换开销过大。通过比较,一个轻量的经过高度优化事件循环(event-loop)架构在高负载下的表现确实让人眼前一亮。
EventMachine 和 Reactor pattern
在听取了一些不同的关于ruby应用服务器的Presentation之后,我偶然发现这样一种认识:事件驱动的服务器非常适合轻量级的请求,但对于长时间的请求,则性能不佳。从技术上讲是这样的,但是实践来看,未必是真。让我们从一个最简单的例子开始: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 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 fortests: 20.6246 seconds
# > Complete requests: 10
现在我们使用EventMachine框架构建了一个最简单的HTTP Web服务器。我们通过ab(Apache Bench)测试:并发数设置为5 (-c 5),请求数设置为10(-n 10)。耗时略大于20秒。正如所期望,Reactor同步处理每个请求,相当于并发数设置为1。因此,10个请求,每个请求耗时2秒,一个简单的数学题目。
EventMachine:轻量级并发的Reactor?
同步,是Reactor模式的特性,也是前个例子的瓶颈所在,这也是EventMachine背离纯粹模式的原因。特别地,这提供了一种机制:分配请求到一个线程池中(默认20个线程):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 long running request
resp.status = 200
resp.content = "Hello World!"
end
# Callback block to execute once therequest 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..."
}
# Benchmarking results:
#
# > ab -c 5 -n 10"http://127.0.0.1:8081/"
# > Concurrency Level: 5
# > Time taken for tests: 4.21405 seconds
# > Complete requests: 10
还是10个请求,并发数设置为5。总共耗时仅仅4秒有余。这意味着新的服务器在并行处理请求。Not quite the Reactor pattern EventMachine advertises, but a very powerful feature nonetheless - nowyou can see where all the FUD is coming from.
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 (maxconcurrency: 20)
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
没有线程,我们仍在不到5秒的时间内完成了40个请求—— 唯一的限制是我们在前面例子中构建的基于线程的服务器,并发量是20.真是魔法,这就是EventMachine之美:如果你的工作推迟或者阻塞在网络上,Reactor循环将继续处理其他的请求。当Deferred的工作完成之后,产生一个成功的信息并由reactor返回响应。无限潜能,没有线程,没有同步。Dnsruby是最好的例子。