為什麼我們需要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 10000Trying 127.0.0.1...Connected to localhost.Escape character is '^]'.hellohello
仔細看一下上面的程式: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.epollEM.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 endend#輸出:Tick...Tick...Tick...Tick...BOOMThe 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}" endendDefer #<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內建了兩鐘輕量級的並發處理機制:Deferrables和SpawnedProcesses。
EM::Deferrable
如果在一個類中include了EM::Deferrable,就可以把Callback和Errback關聯到這個類的執行個體。
一旦執行條件被觸發,Callback和Errback會按照與執行個體關聯的順序執行起來。
對應執行個體的#set_deferred_status方法就用來負責觸發機制:
當該方法的參數是:succeeded,則觸發callbacks;而如果參數是:failed,則觸發errbacks。觸發之後,這些回調將會在主線程立即得到執行。當然你還可以在回調中(callbacks和errbacks)再次調用#set_deferred_status,改變狀態。
require 'eventmachine'class MyDeferrable include EM::Deferrable def go(str) puts "Go #{str} go" endendEM.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" endend#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 endend#1s之後同時輸出前兩個,第二秒後輸出PeriodicPeriodicReceived helloPeriodic
注意這兩種機制的使用方式是不一樣的:一個是作為內部類include進去進而使用其定義的callback來執行的;而另一個是直接使用spawn執行個體通過notify來觸發代碼塊來執行的。
5、使用EM簡化網路編程
網路編程的簡單化是EM的一大特色。拿前面的Echo舉例子,我們通過十分簡潔的代碼就可以實現一個HttpServer的功能。
我們其實還有更簡潔的方式來完成這個工作,比如使用module:
require 'eventmachine'module Echo def receive_data(data) send_data(data) endendEM.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 endend
事實上,每次你建立一個串連,一個新的包含了你代碼的匿名類就會被建立。理論上,不同的串連不能互相交換資訊,這一點很重要。不過EM實際上在設計時以及解決這個問題:
require 'rubygems'require 'eventmachine'class Pass < EM::Connection attr_accessor :a, :b def receive_data(data) send_data "#{@a} #{data.chomp} #{b}" endendEM.run do EM.start_server("127.0.0.1", 10000, Pass) do |conn| conn.a = "Goodbye" conn.b = "world" endend
通過給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" endendEM.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 endendEventMachine::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) endendEventMachine::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 endendEventMachine::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
從測試結果我們可以看到,這個server在4s多的時間裡處理了40個請求(因為並發量是20,前面監聽8081的伺服器sleep是2s)。這就是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/