Współbieżność w Ruby

Trójmiejska Grupa Użytkowników Ruby
Gdańsk, 15 Maja 2012

Paweł Gościcki

pawelgoscicki.com
@pawelgoscicki
github.com/pjg


Exvo.com

Wspołbieżność?

Przetwarzanie współbieżne (ang. concurrent computing) – przetwarzanie oparte na współistnieniu wielu wątków lub procesów, operujących na współdzielonych danych. Wątki uruchomione na tym samym procesorze są przełączane w krótkich przedziałach czasu, co sprawia wrażenie, że wykonują się równolegle.

-- http://pl.wikipedia.org/wiki/Przetwarzanie_współbieżne

Czemu jest to ważne? (I)

Czemu jest to ważne? (II)

Free Lunch is over

-- Herb Sutter, http://www.gotw.ca/publications/concurrency-ddj.htm

W 2003 roku zatrzymał się wzrost GHz, ale liczba tranzystorów ciągle rośnie z roku na rok.

Powód? Wprowadzenie wielordzeniowych procesorów.

Nowy miernik

Wydajność w przeliczeniu na 1W (TDP)

(tutaj prawo Moore'a nadal znajduje zastosowanie)

Co to znaczy dla nas?

Dla nas, twórców aplikacji webowych.

Może znaczyć "nic"

Serwery aplikacji są właściwie bez stanu (stan przechowywany jest w sesji u klienta, każdy request może być obsługiwany przez inny proces/maszynę).

Możemy się skalować horyzontalnie dodając nowe serwery aplikacji.

Tylko pojawiają się problemy...

Na początek trochę teorii... (I)


PROCES

Na początek trochę teorii... (II)


Passenger/Unicorn

I na tym właściwie mógłbym zakończyć swoją prezentację

Na początek trochę teorii... (III)


WĄTEK (thread)

Na początek trochę teorii... (IV)


WADY WĄTKÓW

A jak to się robi w Ruby?

Zielone wątki (ang. green threads)

Natywne wątki (ang. native threads)

Fibers/continuations

GIL - Global Interpreter Lock

To może usuńmy GIL?

Analogia do kaloszy

Jeżeli nie pada, to czemu mamy je codziennie nosić?

The main concern is safeness. We cannot remove the GIL because then it would allow the C Ruby code to crash when you use threads. I don't want Ruby to be like that.

-- Matz @ RubyConf '2011-10, New Orleans

To jak radzą sobie JRuby/Rubinius?

Ale...

Współbieżność jest trudna.

Wydaje się, że już ją umiesz, że wszystko się kuma, ale napotykasz na wyścig (ang. race condition) -- sytuacja, że kod wykonuje się wcześniej niż zakładaliśmy -- np. przed zakończeniem innego kodu), który teoretycznie nie jest możliwy, ale jednak ma miejsce i wszystko wywraca się do góry nogami.

Przykład

@array, threads = [], []
4.times do
  threads << Thread.new { (1..100_000).each { |n| @array << n } }
end
threads.each { |t| t.join }
puts @array.size
          
  • MRI: 400000
  • JRuby: 335467, 342397, 341080
  • MRI: 294278, 285755, 280704

A co z Rails?

I z MRI?

Mamy coś "za darmo" (dokładniej: współbieżność przy wykonywaniu I/O)

Gemfile:

gem "mysql2"

config/database.yml

development:
  adapter: mysql2
  pool: 20
  ...

routes.rb

root :to => 'site#index'
class SiteController < ApplicationController
  def index
    threads = []
    10.times do |n|
      threads << Thread.new do
        ActiveRecord::Base.connection_pool.with_connection do |conn|
          res = conn.execute("SELECT SLEEP(1)")
        end
      end
    end
    threads.each { |t| t.join }
  end
end
          
☺ rails s
=> Booting WEBrick
=> Rails 3.2.3 application starting in development on http://0.0.0.0:3000
=> Call with -d to detach
☺ ab -c 1 -n 1 http://localhost:3000/
...
Requests per second:    0.99 [#/sec] (mean)
Time per request:       1013.341 [ms] (mean)
Time per request:       1013.341 [ms] (mean, across all concurrent requests)
Started GET "/" for 127.0.0.1 at 2012-05-13 20:05:19 +0200
Processing by SiteController#index as */*
   (1007.0ms)  SELECT SLEEP(1)
   (1003.1ms)  SELECT SLEEP(1)
   (1005.1ms)  SELECT SLEEP(1)
   (1001.4ms)  SELECT SLEEP(1)
   (1000.2ms)  SELECT SLEEP(1)
   (1000.2ms)  SELECT SLEEP(1)
   (1000.2ms)  SELECT SLEEP(1)
   (1000.2ms)  SELECT SLEEP(1)
   (1000.2ms)  SELECT SLEEP(1)
   (1000.2ms)  SELECT SLEEP(1)
  Rendered site/index.html.erb within layouts/application (1.3ms)
Completed 200 OK in 1066ms (Views: 39.8ms | ActiveRecord: 0.0ms)
          

config.threadsafe!


Rails 2.2+

config/database.yml

development:
  adapter: mysql2
  pool: 20
  ...

config/environments/development.rb

config.threadsafe!
class SiteController < ApplicationController
  def index
    ActiveRecord::Base.connection_pool.with_connection do |conn|
      res = conn.execute("SELECT SLEEP(1)")
    end
  end
end
☺ r s
=> Booting WEBrick
=> Rails 3.2.3 application starting in development on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server
[2012-05-13 21:25:46] INFO  WEBrick 1.3.1
[2012-05-13 21:25:46] INFO  ruby 1.9.3 (2012-04-20) [x86_64-linux]
[2012-05-13 21:25:46] INFO  WEBrick::HTTPServer#start: pid=28954 port=3000
          
☺ ab -c 10 -n 10 http://localhost:3000/
Benchmarking localhost (be patient).....done
...

Requests per second:    9.37 [#/sec] (mean)
Time per request:       1067.630 [ms] (mean)
Time per request:       106.763 [ms] (mean, across all concurrent requests)
Transfer rate:          13.02 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:  1015 1049  20.2   1057    1067
Waiting:     1015 1048  20.1   1057    1067
Total:       1015 1049  20.2   1058    1067

Percentage of the requests served within a certain time (ms)
  50%   1058
  66%   1065
  75%   1065
  80%   1067
  90%   1067
  95%   1067
  98%   1067
  99%   1067
 100%   1067 (longest request)
          

Wnioski

Dziękuję za uwagę

Pytania?

Materiały uzupełniające