Passenger bliżej - Rails, Rack i WSGI

Opublikowane przez Jarosław Zabiełło Sat, 07 Jun 2008 13:24:00 GMT

Stworzony pierwotnie na użytek Rails, aktualnie mod_passenger już obsługuje nie tylko Rails ale także masę innych frameworków używających Rack’a. W nowej dokumentacji wymienione są frameworki: Camping, Halcyon, Mack, Merb, Ramaze i Sinatra. W dokumentacji nie wymieniono jeszcze “drugiej listy”, zawierającej frameworki korzystające z WSGI i Pythona (np. Pylons, Django, TurboGears itp.). Chcąc sprawdzić plotki wokół tej sprawy, sprawdziłem, czy faktycznie mod_passenger pracuje nie tylko z Ruby, ale także z Pythonem. Sprawdziłem także jak to jest faktycznie z obługą Rails i frameworków na Rack’u (tu sprawdziłem tylko Merba). Sprawdziłem też JRuby dla Rails i Merba.

Wszystkie testy były wykonywane na laptopie MacBook Pro Core 2 Duo, 2.16GHZ, 4GB RAM (OSX 10.5.3 dla tego modelu widzi tylko 3GB) i dyskiem 200GB kręcącym się z szybkością 7200 rpm. Ruby 1.8.6, Python 2.5.2, Apache 2.2.8 (mpm-prefork) były instalowane z MacPortów. Passenger, mimo że instalowany ze źródeł w Apache’u był wyświetlany jako “Phusion_Passenger/1.1.0” (być może więc to nie jest jeszcze ta nowa wersja 2.0 o której pisałem wcześniej) Nie sprawdzałem Linuksa, być może wyniki i wnioski będą wtedy inne.

Dla tych co chcieliby sami popróbować podaję wpierw konfigurację serwerów wirtualnych dla Apache’a potrafiącą unieść razem: PHP (2.5.6), Rails (2.1), Merb (0.9.4 edge), i Django (edge).

Konfiguracja Apache’a 2.2.8


# Ruby (Rails):
<VirtualHost *:80>     
  DocumentRoot "/opt/local/apache2/vhosts/rails_app/public"  
  ServerName rails_app
</VirtualHost>

# Ruby (Merb):        
<VirtualHost *:80>  
  RailsAutoDetect off  
  DocumentRoot "/opt/local/apache2/vhosts/merb_app/public"    
  ServerName merb_app       
</VirtualHost>

# Python (WSGI):
<VirtualHost *:80>          
  DocumentRoot "/opt/local/apache2/vhosts/wsgi_app/public"    
  ServerName wsgi_app
</VirtualHost>      

# Django (mod_python)
<VirtualHost *:80>         
  DocumentRoot "/opt/local/apache2/vhosts/djangus/public"    
  ServerName django_modpython 
  <Location "/">
    SetHandler python-program
    PythonHandler django.core.handlers.modpython
    SetEnv DJANGO_SETTINGS_MODULE djangus.settings
    PythonDebug On        
    PythonPath "['/opt/local/apache2/vhosts'] + sys.path"    
  </Location>       
</VirtualHost>   

# PHP:
<VirtualHost *:80>  
  RailsAutoDetect off  
  DocumentRoot "/opt/local/apache2/vhosts/php_app/public"        
  ServerName php_app   
</VirtualHost>    

Wszystkie aplikacje korzystające z mod_passengera muszą posiadać folder public i tmp. Gdy do katalogu tmp dorzucimy jakikolwiek (może być pusty) plik o nazwie restart.txt, to przy następnym przeładowaniu przeglądarki nastąpi restart aplikacji.

We wszystkich przypadkach użyłem dosyć banalnego kodu polegającego na wyświetleniu “Hello World!”. Z wyników programu ab wyciąłem nieistotne informacje.

Rails (2.1)

Rails 2.1 + mod_passenger 1.1.0

Rails obsługiwane są w Passengerze praktycznie bezobsługowo. Wystarczy wkopiować pliki na serwer i to wszystko.

ab -n 1000 -c 1 http://rails/ 
...
Concurrency Level:      1
Failed requests:        0
Write errors:           0
Total transferred:      582000 bytes
HTML transferred:       12000 bytes
Requests per second:    430.20 [#/sec] (mean)
Transfer rate:          244.35 [Kbytes/sec] received

ab -n 1000 -c 4 http://rails/
...
Concurrency Level:      4
Failed requests:        239
   (Connect: 0, Length: 239, Exceptions: 0)
Write errors:           0
Total transferred:      488073 bytes
HTML transferred:       9132 bytes
Requests per second:    520.75 [#/sec] (mean)
Transfer rate:          247.88 [Kbytes/sec] received

Szybkość nie jest najgorsza, ale niepokojąca jest duża lista błędnych requestów w wypadku zapytań równoległych. Jak się dalej okazuje, ten problem dotyczy także obługi Rack jak i WSGI. Albo to jakaś specyfika OSX, albo wcale nie jest tak dobrze ze stabilnością mod_passengera dla równoległych zapytań. Trzeba by było też zbadać, czy ten efekt występuje także dla Linuksa. Zdziwiłbym się gdyby tam było podobnie skoro Dreamhost już oferuje hosting z mod_rails…

Dla porównania Rails używający Mongrela, Thin oraz Ebb. Można by pokusić aby odpalić Ebb i Thina na uniksowych socketach (Ebb też to już potrafi!), ale wtedy musiałbym zestawiać klaster i uruchamiać to przez proxy. Jak ktoś chce to niech się sam pobawi. Dla prostoty użyłem portów TCP. Sprawdziłem też JRuby.

Rails 2.1 + Ebb 0.2.0

ebb_rails -e production start

ab -n 1000 -c 1 http://127.0.0.1:3000/
...
Concurrency Level:      1
Failed requests:        0
Write errors:           0
Requests per second:    488.95 [#/sec] (mean)

ab -n 1000 -c 4 http://127.0.0.1:3000/
..
Concurrency Level:      4
Failed requests:        0
Write errors:           0
Requests per second:    533.22 [#/sec] (mean)

Szybkość większa od mod_passengera i co ważniejsze, zero jakichkolwiek błędów przy pracy równoległej.

Rails 2.1 + Thin 0.8.1

thin start -e production              

ab -n 1000 -c 1 http://127.0.0.1:3000/
...
Concurrency Level:      1
Failed requests:        0
Write errors:           0
Requests per second:    485.95 [#/sec] (mean)

ab -n 1000 -c 4 http://127.0.0.1:3000/
...              
Concurrency Level:      4
Failed requests:        0
Write errors:           0
Requests per second:    506.21 [#/sec] (mean)

Szybkość trochę mniejsza od Ebb, ale stabilność b. dobra.

Rails 2.1 + Mongrel 1.1.5

script/server -e production

ab -n 1000 -c 1 http://127.0.0.1:3000/
...
Concurrency Level:      1
Failed requests:        0
Write errors:           0
Requests per second:    373.74 [#/sec] (mean)

ab -n 1000 -c 4 http://127.0.0.1:3000/
...
Concurrency Level:      4
Failed requests:        0
Write errors:           0
Requests per second:    360.46 [#/sec] (mean)

Stabilność bez zarzutu, wydajność jednak mniejsza od serwerów pracujących asynchronicznie.

Rails 2.1 + JRuby 1.1.2

W wypadku JRuby trzeba go trochę “rozgrzać”. Pełna wydajność Javy pojawia się po jakimś czasie pracy. Wynika to ze specyfiki i możliwośći JVM która dokonuje dynamicznych optymalizacji kodu w trakcie jego działania (z tego powodu Java potrafi przewyższyć wydajnością C++). “Dla rozgrzewki” przepuściłem Rails przez 30 tys. requestów co spowodowało że początkowych 137 req/s zrobiło się 257 req/s.

jruby script/server -e production    

ab -n 1000 -c 1 http://127.0.0.1:3000/
...
Concurrency Level:      1
Failed requests:        0
Write errors:           0
Requests per second:    200.48 [#/sec] (mean)

ab -n 1000 -c 4 http://127.0.0.1:3000/
...
Concurrency Level:      4
Failed requests:        0
Write errors:           0
Requests per second:    257.05 [#/sec] (mean)

JRuby jest stabilny ale z wydajnością dla Rails jeszcze jest trochę do poprawienia. Dorzucenie opcji optymalizacyjnych, czyli odpalenie Rails przez

  
jruby -J-server -J-Djruby.compile.frameless=true script/server -e production

trochę poprawiło wynik: 273 req/s, ale generalnie nie jest to wielka różnica. JRuby 1.1.x już ma domyślnie powłączane optymalizacje.

Rails – wnioski

mod_passenger dla Rails faktycznie jest wydajny, choć trzeba jeszcze by zbadać dlaczego zwraca tyle błędnych requestów dla równoległych zapytań i czy ten problem występuje też na Linuksie. Dlatego na razie najwydajniejszym i najstabilniejszym rozwiązaniem dla Rails wciąż pozostaje kombinacja nginx + proxy do ebb lub thin. mod_passenger kusi głównie prostotą konfiguracji (właściwie brakiem konfiguracji). No i chyba ja to testowałem dla mod_passengera w wersji 1.1, a nie 2.0 (przynajmniej taka się wyświetla w Apache).

Merb 0.9.4 edge

Merb 0.9.4 edge + mod_passenger 1.1.0

Aby użyć frameworka Rack z Passengerem trzeba w katalogu projektu tworzyć plik config.ru zawierający konfigurację Rack’a. W wypadku Merba będzie to

require 'rubygems'
require 'merb-core'

Merb::Config.setup(:merb_root   => ".",
                   :environment => ENV['RACK_ENV'])
Merb.environment = Merb::Config[:environment]
Merb.root = Merb::Config[:merb_root]
Merb::BootLoader.run

run Merb::Rack::Application.new  
Concurrency Level:      1
Failed requests:        0
Write errors:           0
Requests per second:    762.62 [#/sec] (mean)

Concurrency Level:      4
Failed requests:        247
   (Connect: 0, Length: 247, Exceptions: 0)
Write errors:           0
Requests per second:    985.15 [#/sec] (mean) 

Merb jest wciąż dużo szybszy od Rails. Podobnie jak dla Rails, wiele równoległych zapytań nie zostało poprawnie wykonanych. Na produkcyjne użycie Passengera dla Merba jest jeszcze trochę za wcześnie.

Merb 0.9.4 edge + ebb 0.2.0

merb -e production -a ebb  
...                              
Concurrency Level:      4
Failed requests:        0
Write errors:           0
Requests per second:    1463.69 [#/sec] (mean)

Concurrency Level:      10
Failed requests:        0
Write errors:           0
Requests per second:    1560.60 [#/sec] (mean)

Merb na Ebb deklasuje wydajnością wszystkie inne rozwiązania. Tylko czysty PHP jest w stanie mu dorównać, ale porównywanie PHP z całym, złożonym frameworkiem Rubiego jest trochę bez sensu. Z tego co mówił ostatnio (na kanale IRC) Ezra Zygmuntowicz, podpięcie Ebb na czystym Rack’u, daje nawet 7 tys req/s i deklasuje (rzekomo) szybkiego PHP. Gadanie o wyższej wydajności PHP jest po prostu głupie. Można się założyć, że jakiekolwiek porównanie frameworka naprzeciw frameworka, tj. Merba z Symfony czy Cake PHP, obnaży bezlitośnie słabości PHP tak, jak to zrobił Django w jednym ze starszych testów.

Merb 0.9.4 edge + thin 0.8.1

merb -e production -a thin 
...
Concurrency Level:      1
Failed requests:        0
Write errors:           0
Requests per second:    1147.78 [#/sec] (mean)

Concurrency Level:      4
Failed requests:        0
Write errors:           0
Requests per second:    1234.82 [#/sec] (mean)

Wydajność równie dobra, choć trochę słabsza od Ebb. Sporo więcej od Rails i zero problemów ze stabilnością.

Merb 0.9.4 edge + mongrel 1.1.5

merb -e production -a mongrel
...
Concurrency Level:      1
Failed requests:        0
Write errors:           0
Requests per second:    868.10 [#/sec] (mean)

Concurrency Level:      4
Failed requests:        0
Write errors:           0
Requests per second:    837.01 [#/sec] (mean)

Mongrel jest wolniejszy od serwerów asynchronicznych ale i tak wyraźnie szybszy od Rails.

Merb 0.9.4 edge + JRuby 1.1.2

 
jruby -S merb -e production 
...
Concurrency Level:      1
Failed requests:        0
Write errors:           0
Requests per second:    449.52 [#/sec] (mean)

Concurrency Level:      4
Failed requests:        0
Write errors:           0
Requests per second:    505.52 [#/sec] (mean)

Merb na “rozgrzanym” (30 tys. req) JRuby osiąga wydajność taką jak najszybsze rozwiązania dla Rails z użyciem asynchronicznego Ebb! To znaczy, że użycie produkcyjne Merba w systemach Javy ma jak najbardziej sens.

Passenger & WSGI

Pora na przyjrzenie się temu jak mod_passenger daje sobie radę z Pythonem. W katalogu ze źródłami Passengera leży gotowa, prosta aplikacja WSGI. Dzieki dobrej dokumentacji Django, udało mi się uruchomić ten framework pod Passengerem. Gorzej było z Pylons. Nie udało mi się stworzyć poprawnego pliku passenger_wsgi.py, niezbędnego do tego aby Passenger uruchomił aplikację WSGI.

Django edge

Django, mimo swych zalet, stosuje bardzo głupią politykę nie wypuszczania kolejnych wersji kodu. Dostępna na stronie wersa 0.96 jest stara i generalnie zaleca się aby używać nowszą wersję, która istnieje tylko w repozytorium Subversion. Oparcie kodu produkcyjnego o wersję edge jest trochę ryzykowne i szybko sprowadza się do złej praktyki ciągłego łatania kodu.

W wypadku Django plik passenger_wsgi.py zawiera kod:

import os, sys
sys.path.append('/bezwl/sciezka/do/nazwaprojektu') 
os.environ['DJANGO_SETTINGS_MODULE'] = 'nazwaprojektu.settings'
import django.core.handlers.wsgi
application = django.core.handlers.wsgi.WSGIHandler()
  ab -n 10000 -c 10            
  ...   
  apr_poll: The timeout specified has expired (70007)

Test padł po 9tys requestach. Dalej nie można było uruchomić Django. Dopiero restart calego Apache’a pomógł. Dla niższej ilości równoległych zapytań (ab -n 1000 -c 4) Django nie padło, ale było dużo błędnych requestów. Okazało się, że problem wynikał z równoczesnej obecności modułu mod_python i mod_passenger. Nie można ich razem włączać.

Usunięcie mod_passengera i zostawienie samego mod_pythona pomogło. (Albo jest jakiś konflikt między nimi, albo (co bardziej jest prawdopodobne) trzeba poczekać na opcję wyłączającą passengera dla serwera wirtualnego używającego mod_pythona. Dla Rails i Rack są takie opcje (RailsAutoDetect, RackAutoDetect), dla WSGI nie mogłem nic takiego znaleźć w kodzie źródłowym. Modułu mod_wsgi nie sprawdzałem, bo nie było go dostępnego w portach OSX.)

Po zablokowaniu mod_passenger’a tym razem mod_python nie zwracał żadnego błędnego requestu.

Concurrency Level:      1
Failed requests:        0
Write errors:           0
Requests per second:    934.87 [#/sec] (mean)

Concurrency Level:      4
Failed requests:        0
Write errors:           0
Requests per second:    1240.26 [#/sec] (mean)

Nie jest źle. Django jest tu wyraźnie szybsze od Rails (co nikogo nie dziwi), ale jest wolniejsze od Merba, co dla miłośników Pythona może być przykrą niespodzianką. Większość krytyki Rubiego oparta jest na krytyce (słabszej) wydajności Railsów. Okazuje się, że winny nie jest Ruby, ale słabo zoptymalizowany Rails. Merb jest dowodem na to, że można napisać w Rubim framework, który nie tylko będzie partnerem dla rozwiązan Pythona, ale nawet potrafi je przewyższać wydajnościowo.

Zobaczmy jak wypadnie mod_passenger.

Concurrency Level:      1
Failed requests:        0
Write errors:           0
Requests per second:    904.60 [#/sec] (mean)

Concurrency Level:      4
Failed requests:        240
   (Connect: 0, Length: 240, Exceptions: 0)
Write errors:           0
Requests per second:    497.51 [#/sec] (mean)

W pracy jednoprocesowej nie można mieć zastrzeżeń. Szybkość jest niezła. Niestety praca równoległa to porażka. Dużo requestów nie zostało obsłużonych. Gorzej, za drugim razem (tylko dla 4 rownoległych zapytań) Django kompletnie się załamało i zwróciło wyjątek “apr_poll: The timeout specified has expired (70007)”. Nie można było go dłużej używać. Wymagany był restart Apache’a.

Aby sprawdzić czy to tylko problem Django czy ogólnie implementacji WSGI dla mod_passengera, uruchomiłem test na prostej aplikacji WSGI. Niestety sytuacja jest ta sama.

Wniosek: mod_passenger dla Pythona wymaga dopracowania pracy równoległej. Jeszcze za wcześnie aby go używać z Pythonem. Ale z drugiej strony uruchomiłem mod_passengera z WSGI trochę przedwcześnie. Nie ma przecież w manualu ani jednego zdania o tym, że Passenger działa z WSGI. Oparłem się tylko na pogłoskach i wygrzebałem tą opcję ze źródeł. Więc jeszcze wszystko może się tu zmienić. To samo dotyczy Rack’a. Twórcy muszą przyjrzeć się problemom związanym z obsługą równoległych zapytań.

Update

Jednak dobrze się domyślałem, Apache nie oszukiwał. W tekście testowałem starszego Passengera 1.1. Także nie był to Ruby Enterprise. NOWY Passenger 2.0RC1 oraz Ruby Enterprise został dopiero niedawno opublikowany. Co ciekawe, dodano obsługę Apache MPM Worker a nie MPM Prefork, co dodatkowo zmniejsza zużycie pamięci. Niestety Passenger 2.0RC1 jest na razie dostępny tylko w wersji na Linuksa.

Tagi , , , , , , ,  | 6 comments

Comments

  1. Avatar Tomash powiedział about 5 hours later:

    Bardzo ładny test. Nie myślałeś o przetłumaczeniu tego na angielski? Brakuje w railsowej blogosferze sensownego benchmarka porównawczego dla różnych metod odpalania aplikacji, który uwzględniałby Passengera i problem równoległych żądań.

  2. Avatar Jarosław Zabiełło powiedział about 6 hours later:

    @Tomash: Dzięki, ale nie mam czasu na tłumaczenie tekstu. Moim testom daleko też do “sensownego benchmarka”, ale nie o to mi chodziło. Chciałem głównie pokazać jak to wszystko odpalać i tylko tak z grubsza sprawdzić wydajność. Przy okazji wyszły te problemy z równoległością dla Passengera. Nie miałem czasu aby sprawdzić na Linuksie.

  3. Avatar Drogomir powiedział about 8 hours later:

    A z jakimi opcjami odpalasz mod_passengera? Bo domyślnie uruchomi się 6 instancji dla aplikacji, więc trochę niesprawiedliwe jest uruchamianie innych serwerów w pojedynkę.

    Zrobiłem kilka własnych benchmarków. Nie mam teraz czasu wszystkiego opisywać po kolei, ale na pewno przy ab dla mod_passengera było 0 nieprawidłowych zapytań (Gentoo, Apache 2.2.8 z portage), więc to chyba specyficzne dla OSXa. Jeżeli chodzi o ebb, to dostaję tylko “accept(): Invalid argument”, więc nie mam jak sprawdzić….

    Na szybko mogę jeszcze dodać, że dla mnie ogromną zaletą mod_passengera jest dynamiczne przydzielanie wolnych zasobów. Jeżeli na serwerze jest więcej aplikacji, to trzeba pomyśleć, które potrzebują więcej mongreli, które mniej, zrobić proxy. W wypadku mod_passengera wszystko dzieje się automagicznie – naprawdę duże ułatwienie.

  4. Avatar Drogomir powiedział about 8 hours later:

    I jeszcze jedna bardzo ważna rzecz. Do utrzymywania aplikacji railsowych używałem dotychczas goda. Bardzo fajnie działa, ale po dłuższym czasie żre dużo ramu i trzeba go restartować, aplikacje można restartować tylko jako root i musiałem napisać swój własny config, żeby wszystko było zautomatyzowane. A z mod_passengerem? :) Apache chodzi i to wszystko ;-)

  5. Avatar Tomash powiedział about 20 hours later:

    Tak jak Drogus napisał – ja też uważam, że Passenger to nowy król odpalania aplikacji RoR :) Jak tylko skończymy testy obciążeniowe i stabilnościowe (blog.aenima.pl), przenosimy na passengera pozostałe firmowe projekty.

  6. Avatar piter powiedział 3 days later:

    Na FreeBSD 7.0 test przy concurency level 4 zwraca 0 nieprawidłowych zapytań.

(leave url/email »)

   Pomoc języka formatowania Obejrzyj komentarz