No Callbacks, No Threads & Ruby 1.9async & co-operative web-serversIlya Grigorik@igrigorik
The slides…TwitterMy blog
The state of art is not good enough.(we’ve been stuck in the same local minima for several years)
require"active_record”ActiveRecord::Base.establish_connection(  :adapter => "mysql",  :username => "root",  :database => "database",  :pool => 5)threads = []10.times do |n|   threads <<Thread.new {ActiveRecord::Base.connection_pool.with_connectiondo |conn|      res =conn.execute("select sleep(1)")end  }endthreads.each { |t| t.join }The “Experiment”vanilla everything…
require"active_record”ActiveRecord::Base.establish_connection(  :adapter => "mysql",  :username => "root",  :database => "database",  :pool => 5)threads = []10.times do |n|   threads <<Thread.new {ActiveRecord::Base.connection_pool.with_connectiondo |conn|      res =conn.execute("select sleep(1)")end  }endthreads.each { |t| t.join }5 shared connections# time ruby activerecord-pool.rb## real    0m10.663s# user    0m0.405s# sys     0m0.201s
BHP% power     lossWHP
> 50% power loss!?
MongoCouchMySQLPSQL…DriversThreadsRuby VMGILFibers…NetworkMongrelUnicornPassenger…
2MongoCouchMySQLPSQL…DriversThreadsRuby VM14GILFibers…NetworkMongrelUnicorn3We’re as fast as the slowest componentPassenger…
Global Interpreter Lock is a mutual exclusion lock held by a programming language interpreter thread to avoid sharing code that is not thread-safe with other threads. There is always one GIL for one interpreter process.Concurrency is a myth in Ruby(with a few caveats, of course)http://bit.ly/ruby-gil
N-M thread pool in Ruby 1.9…Better but still the same problem!Concurrency is a myth in Rubystill no concurrency in Ruby 1.9http://bit.ly/ruby-gil
Nick – tomorrow @ 11:45amConcurrency is a myth in Rubystill no concurrency in Ruby 1.9http://bit.ly/ruby-gil
Blocks entireRuby VMNot as bad, butavoid it still..Avoid locking interpreter threads at all costslet’s say you’re writing an extension…
Will fetch_xyz() block the VM?when was the last time you asked yourself this question?
require 'rubygems’require 'sequel'DB = Sequel.connect('mysql://root@localhost/test')while trueDB['select sleep(1)'].select.firstendBlocking 1s call!ltrace –ttTg -xmysql_real_query –p ./example.rbmysql.gem under the hood22:10:00.218438 mysql_real_query(0x02740000, "select sleep(1)", 15) = 0 <1.001100>22:10:01.241679 mysql_real_query(0x02740000, "select sleep(1)", 15) = 0 <1.000812>http://bit.ly/c3Pt3f
Blocking calls to mysql_real_querymysql_real_query requires an OS threadBlocking on mysql_real_query blocks the Ruby VMAka, “select sleep(1)” blocks the entire Ruby runtime for 1s(ouch)gem install mysqlwhat you didn’t know…
static VALUE async_query(intargc, VALUE* argv, VALUE obj) {  ...send_query( obj, sql );  ...schedule_query( obj, timeout);  ...returnget_result(obj); }staticvoidschedule_query(VALUEobj, VALUE timeout) {  ...structtimevaltv = { tv_sec: timeout, tv_usec: 0 };for(;;){FD_ZERO(&read);FD_SET(m->net.fd, &read);    ret = rb_thread_select(m->net.fd + 1, &read, NULL, NULL, &tv);    ...if (m->status == MYSQL_STATUS_READY)break; }}send query and blockRuby: select() = C: rb_thread_select()mysqlplus.gem under the hood
spinning in selectmysqlplus.gem + ruby select
Step 1: Fix the driversMany of our DB drivers don’t respect the underlying Ruby VM. Don’t blame the VM.
DriversRuby VMNetworkWEBRickMongrel made Rails viablestill powers a lot of Rails apps today
Rails rediscovers Apachethe worker/forker model…
*nix IPC is fast!  Woo!…Full Ruby VMAn exclusive Ruby VM for EACH requestam I the only one who thinks this is terrible?
“Does not care if your application is thread-safe or not, workers all run within their own isolated address space and only serve one client at a time for maximum robustness.”Robustness? That sounds like a bug.An exclusive Ruby VM for EACH requestam I the only one who thinks this is terrible?
Step 2: consider entire stackThe driver, the web-server, and the networkmust all work together.
Node imposes the full-stack requirementsNode imposes async driversNode imposes async frameworksSurprise: Node is “fast”
We can ignore the performanceissues at our own perilor, we can just fix the problem
>I’ll take Ruby over JSgem install eventmachine
p "Starting"EM.rundop"Running in EM reactor"endp”won’t get here"whiletruedo       timersnetwork_ioother_ioendEventMachine Reactorconcurrency without threadEventMachine: The Speed DemonWednesday @ 11:45am – Aman Gupta
Non-blocking IO requires non-blocking drivers:AMQP                        http://github.com/tmm1/amqpMySQLPlushttp://github.com/igrigorik/em-mysqlplusMemcachedhttp://github.com/astro/remcachedDNS                            http://github.com/astro/em-dnsRedishttp://github.com/madsimian/em-redisMongoDBhttp://github.com/tmm1/rmongoHTTPRequesthttp://github.com/igrigorik/em-http-requestWebSockethttp://github.com/igrigorik/em-websocketAmazon S3               http://github.com/peritor/happeningAnd many others: http://wiki.github.com/eventmachine/eventmachine/protocol-implementations
gem install em-mysqlplusEventMachine.rundoconn=EventMachine::MySQL.new(:host => 'localhost')  query =conn.query("select sleep(1)")query.callback { |res| pres.all_hashes }query.errback  { |res| pres.all_hashes }  puts ”executing…”end# > ruby em-mysql-test.rb## executing…# [{"sleep(1)"=>"0"}]callback fired 1s after “executing”em-mysqlplus: exampleasyncMySQL driver
non-blocking driverrequire'mysqlplus'defconnect(opts)conn=connect_socket(opts)EM.watch(conn.socket, EventMachine::MySQLConnection, conn, opts, self)enddefconnect_socket(opts)conn=Mysql.initconn.real_connect(host, user, pass, db, port, socket, ...)conn.reconnect=falseconnendEM.watch:  reactor will poll & notifyem-mysqlplus: under the hoodmysqlplus + reactor loop
Features: Maintains C-based mysql gem API
Deferrables for every query with callback & errback
 Connection query queue - pile 'em up!
 Auto-reconnect on disconnects
 Auto-retry on deadlockshttp://github.com/igrigorik/em-mysqlplusem-mysqlplusmysqlplus + reactor loop
and this callback goes to…
We can do better than node.jsall the benefits of evented code without the drawbacks
Ruby 1.9 Fibers are a means of creating code blocks which can be paused and resumed by our application (think lightweight threads, minus the thread scheduler and less overhead). f=Fiber.new {whiletruedoFiber.yield"Hi”end}pf.resume# => Hipf.resume# => Hipf.resume# => HiManual / cooperative scheduling!Ruby 1.9 Fibersand cooperative schedulinghttp://bit.ly/d2hYw0
Fibers vs Threads: creation time much lowerFibers vs Threads: memory usage is much lowerRuby 1.9 Fibersand cooperative schedulinghttp://bit.ly/aesXy5
defquery(sql)f = Fiber.currentconn=EventMachine::MySQL.new(:host => 'localhost')q = conn.query(sql)c.callback { f.resume(conn) }c.errback  { f.resume(conn) }return Fiber.yieldendEventMachine.rundoFiber.new {    res =query('select sleep(1)')    puts "Results: #{res.fetch_row.first}"}.resumeendException, async!Untangling Evented Code with Fibershttp://bit.ly/d2hYw0
defquery(sql)f = Fiber.currentconn=EventMachine::MySQL.new(:host => 'localhost')q = conn.query(sql)c.callback { f.resume(conn) }c.errback  { f.resume(conn) }  return Fiber.yieldendEventMachine.rundoFiber.new{    res =query('select sleep(1)')    puts "Results: #{res.fetch_row.first}"  }.resumeend1. Wrap into a continuationUntangling Evented Code with Fibershttp://bit.ly/d2hYw0
defquery(sql)f=Fiber.currentconn=EventMachine::MySQL.new(:host => 'localhost')q = conn.query(sql)c.callback { f.resume(conn) }c.errback  { f.resume(conn) }returnFiber.yieldendEventMachine.rundoFiber.new{    res =query('select sleep(1)')    puts "Results: #{res.fetch_row.first}"  }.resumeend2. Pause the continuationUntangling Evented Code with Fibershttp://bit.ly/d2hYw0
defquery(sql)f=Fiber.currentconn=EventMachine::MySQL.new(:host => 'localhost')q = conn.query(sql)c.callback { f.resume(conn) }c.errback  { f.resume(conn) }returnFiber.yieldendEventMachine.rundoFiber.new{    res =query('select sleep(1)')    puts "Results: #{res.fetch_row.first}"  }.resumeend3. Resume the continuationUntangling Evented Code with Fibershttp://bit.ly/d2hYw0
Good news, you don’t even have to muck around with Fibers!gem install em-synchronyhttp://github.com/igrigorik/em-synchrony Fiber aware connection pool with sync/async query support
 Multi request interface which accepts any callback enabled client
 Fibered iterator to allow concurrency control & mixing of sync / async
em-http-request: .get, etc are synchronous, while .aget, etc are async
em-mysqlplus: .query is synchronous, while .aquery is async

No Callbacks, No Threads - RailsConf 2010

  • 1.
    No Callbacks, NoThreads & Ruby 1.9async & co-operative web-serversIlya Grigorik@igrigorik
  • 2.
  • 3.
    The state ofart is not good enough.(we’ve been stuck in the same local minima for several years)
  • 4.
    require"active_record”ActiveRecord::Base.establish_connection( :adapter=> "mysql", :username => "root", :database => "database", :pool => 5)threads = []10.times do |n| threads <<Thread.new {ActiveRecord::Base.connection_pool.with_connectiondo |conn| res =conn.execute("select sleep(1)")end }endthreads.each { |t| t.join }The “Experiment”vanilla everything…
  • 5.
    require"active_record”ActiveRecord::Base.establish_connection( :adapter=> "mysql", :username => "root", :database => "database", :pool => 5)threads = []10.times do |n| threads <<Thread.new {ActiveRecord::Base.connection_pool.with_connectiondo |conn| res =conn.execute("select sleep(1)")end }endthreads.each { |t| t.join }5 shared connections# time ruby activerecord-pool.rb## real 0m10.663s# user 0m0.405s# sys 0m0.201s
  • 7.
  • 10.
  • 11.
  • 12.
  • 13.
    Global Interpreter Lockis a mutual exclusion lock held by a programming language interpreter thread to avoid sharing code that is not thread-safe with other threads. There is always one GIL for one interpreter process.Concurrency is a myth in Ruby(with a few caveats, of course)http://bit.ly/ruby-gil
  • 14.
    N-M thread poolin Ruby 1.9…Better but still the same problem!Concurrency is a myth in Rubystill no concurrency in Ruby 1.9http://bit.ly/ruby-gil
  • 15.
    Nick – tomorrow@ 11:45amConcurrency is a myth in Rubystill no concurrency in Ruby 1.9http://bit.ly/ruby-gil
  • 16.
    Blocks entireRuby VMNotas bad, butavoid it still..Avoid locking interpreter threads at all costslet’s say you’re writing an extension…
  • 17.
    Will fetch_xyz() blockthe VM?when was the last time you asked yourself this question?
  • 18.
    require 'rubygems’require 'sequel'DB= Sequel.connect('mysql://root@localhost/test')while trueDB['select sleep(1)'].select.firstendBlocking 1s call!ltrace –ttTg -xmysql_real_query –p ./example.rbmysql.gem under the hood22:10:00.218438 mysql_real_query(0x02740000, "select sleep(1)", 15) = 0 <1.001100>22:10:01.241679 mysql_real_query(0x02740000, "select sleep(1)", 15) = 0 <1.000812>http://bit.ly/c3Pt3f
  • 19.
    Blocking calls tomysql_real_querymysql_real_query requires an OS threadBlocking on mysql_real_query blocks the Ruby VMAka, “select sleep(1)” blocks the entire Ruby runtime for 1s(ouch)gem install mysqlwhat you didn’t know…
  • 20.
    static VALUE async_query(intargc,VALUE* argv, VALUE obj) { ...send_query( obj, sql ); ...schedule_query( obj, timeout); ...returnget_result(obj); }staticvoidschedule_query(VALUEobj, VALUE timeout) { ...structtimevaltv = { tv_sec: timeout, tv_usec: 0 };for(;;){FD_ZERO(&read);FD_SET(m->net.fd, &read); ret = rb_thread_select(m->net.fd + 1, &read, NULL, NULL, &tv); ...if (m->status == MYSQL_STATUS_READY)break; }}send query and blockRuby: select() = C: rb_thread_select()mysqlplus.gem under the hood
  • 21.
  • 22.
    Step 1: Fixthe driversMany of our DB drivers don’t respect the underlying Ruby VM. Don’t blame the VM.
  • 23.
    DriversRuby VMNetworkWEBRickMongrel madeRails viablestill powers a lot of Rails apps today
  • 24.
    Rails rediscovers Apachetheworker/forker model…
  • 25.
    *nix IPC isfast! Woo!…Full Ruby VMAn exclusive Ruby VM for EACH requestam I the only one who thinks this is terrible?
  • 26.
    “Does not careif your application is thread-safe or not, workers all run within their own isolated address space and only serve one client at a time for maximum robustness.”Robustness? That sounds like a bug.An exclusive Ruby VM for EACH requestam I the only one who thinks this is terrible?
  • 28.
    Step 2: considerentire stackThe driver, the web-server, and the networkmust all work together.
  • 29.
    Node imposes thefull-stack requirementsNode imposes async driversNode imposes async frameworksSurprise: Node is “fast”
  • 30.
    We can ignorethe performanceissues at our own perilor, we can just fix the problem
  • 31.
    >I’ll take Rubyover JSgem install eventmachine
  • 32.
    p "Starting"EM.rundop"Running inEM reactor"endp”won’t get here"whiletruedo timersnetwork_ioother_ioendEventMachine Reactorconcurrency without threadEventMachine: The Speed DemonWednesday @ 11:45am – Aman Gupta
  • 33.
    Non-blocking IO requiresnon-blocking drivers:AMQP http://github.com/tmm1/amqpMySQLPlushttp://github.com/igrigorik/em-mysqlplusMemcachedhttp://github.com/astro/remcachedDNS http://github.com/astro/em-dnsRedishttp://github.com/madsimian/em-redisMongoDBhttp://github.com/tmm1/rmongoHTTPRequesthttp://github.com/igrigorik/em-http-requestWebSockethttp://github.com/igrigorik/em-websocketAmazon S3 http://github.com/peritor/happeningAnd many others: http://wiki.github.com/eventmachine/eventmachine/protocol-implementations
  • 34.
    gem install em-mysqlplusEventMachine.rundoconn=EventMachine::MySQL.new(:host=> 'localhost') query =conn.query("select sleep(1)")query.callback { |res| pres.all_hashes }query.errback { |res| pres.all_hashes } puts ”executing…”end# > ruby em-mysql-test.rb## executing…# [{"sleep(1)"=>"0"}]callback fired 1s after “executing”em-mysqlplus: exampleasyncMySQL driver
  • 35.
    non-blocking driverrequire'mysqlplus'defconnect(opts)conn=connect_socket(opts)EM.watch(conn.socket, EventMachine::MySQLConnection,conn, opts, self)enddefconnect_socket(opts)conn=Mysql.initconn.real_connect(host, user, pass, db, port, socket, ...)conn.reconnect=falseconnendEM.watch: reactor will poll & notifyem-mysqlplus: under the hoodmysqlplus + reactor loop
  • 36.
  • 37.
    Deferrables for everyquery with callback & errback
  • 38.
    Connection queryqueue - pile 'em up!
  • 39.
  • 40.
    Auto-retry ondeadlockshttp://github.com/igrigorik/em-mysqlplusem-mysqlplusmysqlplus + reactor loop
  • 41.
  • 42.
    We can dobetter than node.jsall the benefits of evented code without the drawbacks
  • 43.
    Ruby 1.9 Fibersare a means of creating code blocks which can be paused and resumed by our application (think lightweight threads, minus the thread scheduler and less overhead). f=Fiber.new {whiletruedoFiber.yield"Hi”end}pf.resume# => Hipf.resume# => Hipf.resume# => HiManual / cooperative scheduling!Ruby 1.9 Fibersand cooperative schedulinghttp://bit.ly/d2hYw0
  • 44.
    Fibers vs Threads:creation time much lowerFibers vs Threads: memory usage is much lowerRuby 1.9 Fibersand cooperative schedulinghttp://bit.ly/aesXy5
  • 45.
    defquery(sql)f = Fiber.currentconn=EventMachine::MySQL.new(:host=> 'localhost')q = conn.query(sql)c.callback { f.resume(conn) }c.errback { f.resume(conn) }return Fiber.yieldendEventMachine.rundoFiber.new { res =query('select sleep(1)') puts "Results: #{res.fetch_row.first}"}.resumeendException, async!Untangling Evented Code with Fibershttp://bit.ly/d2hYw0
  • 46.
    defquery(sql)f = Fiber.currentconn=EventMachine::MySQL.new(:host=> 'localhost')q = conn.query(sql)c.callback { f.resume(conn) }c.errback { f.resume(conn) } return Fiber.yieldendEventMachine.rundoFiber.new{ res =query('select sleep(1)') puts "Results: #{res.fetch_row.first}" }.resumeend1. Wrap into a continuationUntangling Evented Code with Fibershttp://bit.ly/d2hYw0
  • 47.
    defquery(sql)f=Fiber.currentconn=EventMachine::MySQL.new(:host => 'localhost')q= conn.query(sql)c.callback { f.resume(conn) }c.errback { f.resume(conn) }returnFiber.yieldendEventMachine.rundoFiber.new{ res =query('select sleep(1)') puts "Results: #{res.fetch_row.first}" }.resumeend2. Pause the continuationUntangling Evented Code with Fibershttp://bit.ly/d2hYw0
  • 48.
    defquery(sql)f=Fiber.currentconn=EventMachine::MySQL.new(:host => 'localhost')q= conn.query(sql)c.callback { f.resume(conn) }c.errback { f.resume(conn) }returnFiber.yieldendEventMachine.rundoFiber.new{ res =query('select sleep(1)') puts "Results: #{res.fetch_row.first}" }.resumeend3. Resume the continuationUntangling Evented Code with Fibershttp://bit.ly/d2hYw0
  • 49.
    Good news, youdon’t even have to muck around with Fibers!gem install em-synchronyhttp://github.com/igrigorik/em-synchrony Fiber aware connection pool with sync/async query support
  • 50.
    Multi requestinterface which accepts any callback enabled client
  • 51.
    Fibered iteratorto allow concurrency control & mixing of sync / async
  • 52.
    em-http-request: .get, etcare synchronous, while .aget, etc are async
  • 53.
    em-mysqlplus: .query issynchronous, while .aquery is async
  • 54.
    remcached: .get, etc,and .multi_* methods are synchronousem-synchrony: simple evented programmingbest of both worlds…
  • 55.
    require"em-synchrony/em-mysqlplus"EventMachine.synchronydo db =EventMachine::MySQL.new(host:"localhost") res =db.query("select sleep(1)") puts resEventMachine.stopendAsync under the hoodUntangling Evented Code with Fibershttp://bit.ly/d2hYw0
  • 56.
    require "em-synchrony/em-mysqlplus"EventMachine.synchrony do db = EventMachine::MySQL.new(host: "localhost") res =db.query("select sleep(1)") puts resEventMachine.stopendUntangling Evented Code with Fibershttp://bit.ly/d2hYw0
  • 57.
    EM-HTTP, EM-MySQL, EM-Jack,etc.DriversAsync-rackRuby VMFibersNetworkGoliathThinOne VM, full concurrency, network-boundRuby 1.9, Fibers, Thin: in production!
  • 58.
    git clone git://github.com/igrigorik/em-mysqlplus.gitgitcheckout activerecordrake installdatabase.ymldevelopment:adapter:em_mysqlplusdatabase:widgetspool: 5timeout: 5000environment.rbrequire 'em-activerecord’require 'rack/fiber_pool'# Run each request in a Fiberconfig.middleware.useRack::FiberPoolconfig.threadsafe!Async Railswith EventMachine & MySQL
  • 59.
    classWidgetsController< ApplicationControllerdefindexWidget.find_by_sql("select sleep(1)")render:text=> "Oh hai”endendab –c 5 –n 10 http://127.0.0.1:3000/widgetsServer Software: thinServer Hostname: 127.0.0.1Server Port: 3000Document Path: /widgets/Document Length: 6 bytesConcurrency Level: 5Time taken for tests: 2.210 secondsComplete requests: 10Failed requests: 0Requests per second: 4.53 [#/sec] (mean)woot! Fiber DB pool at work.Async Railswith EventMachine & MySQL
  • 60.
    git clone git://…./igrigorik/mysqlplusgitcheckout activerecordrake installNot only is it doable… it already works.
  • 61.
    Ruby 1.9 +Rails 3 + new stack=Order of magnitude better performance(aka, enough of a reason to actually switch)
  • 62.
    The state ofart is not good enough, in fact, it’s terrible!Let’s fix it.Fibers & Cooperative Scheduling in Ruby:http://www.igvita.com/2009/05/13/fibers-cooperative-scheduling-in-ruby/Untangling Evented Code with Ruby Fibers:http://www.igvita.com/2010/03/22/untangling-evented-code-with-ruby-fibers/EM-Synchrony:http://github.com/igrigorik/em-synchronyWhat do you think?

Editor's Notes