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是最好的例子。