Rack - niech aplikacje przemówią wspólnym językiem
12 komentarzy | Kategorie: Narzędzia, Ruby, Techblog | trackbackTagi: ebb http mongrel rack ruby thin
Bohaterem tego wpisu jest Rack. Jest to biblioteka, która staje się standardem jeśli chodzi o Rubiego. Standardem, z którego mogą być spore korzyści. Na początek chciałbym wyjaśnić jaki jest cel Rack'a, gdyż dobre zrozumienie tego może być pomocne w jego poznawaniu.
Na stronie domowej można przeczytać:
Rack provides an minimal interface between webservers supporting Ruby and Ruby frameworks.
Ale co to właściwie oznacza? Po co jakiś dodatkowy interfejs pomiędzy frameworkiem i serwerem aplikacji (webserwer)? Przykładowo Railsy świetnie działają na wszystkich serwerach, tj. mongrelu, thinie i ebb. Czy jest tu sens coś zmieniać? Otóż jest. Gdyby istniał tylko jeden framework nie byłoby problemu. Na całe szczęście od dłuższego czasu coraz głośniej słychać o innych framework, z czego z pewnością najciekawszy jest Merb. Dla każdego nowego frameworka musi zostać zaimplementowana obsługa wszystkich serwerów aplikacyjnych (chyba, że autorom nie zależy na jakimś). Liczba możliwych połączeń jest równa iloczynowi ilości frameworków i serwerów.
Rack: spróbujmy się jakoś dogadać
Na powyższy problem Rack odpowiada w bardzo prosty sposób: ustalmy wspólny interfejs pomiędzy serwerem a aplikacją. Dzięki temu serwer nie musi wiedzieć jaką aplikację obsługuje, a aplikacja nie musi wiedzieć w jakim serwerze została odpalona. Jeśli nawet w przyszłości pojawi się nowy framework (lub nowy serwer) to trzeba będzie napisać do niego implementację jednej klasy by obsłużyć wszystkie serwery (frameworki) zgodne z Rack.
Można by rzec, że Rack jest tym dla frameworków Rubiego czym specyfikacja CGI dla skryptów CGI. Specyfikacja ta pozwala na wspólną komunikację serwera www (apache, lighttp) oraz skryptu CGI (napisanego w php, perl czy nawet C).
W tym momencie możecie zapytać czy dla Was, jako programistów aplikacji, Rack ma jakiekolwiek znaczenie? Otóż ma, co za chwilę postaram się pokazać.
Rack pozwala na:
- wpięcie się w cykl przetwarzania żądania http
- pisanie middlewarów
- łączenie kilku aplikacji w jedną
Bliżej http
Być może nie każdy o tym wie, ale jeśli odpalamy swoją aplikację rails na mongrelu to możemy pisać tzw. handlery. Jest to kod, który jest odpalany poza kontekstem railsów, w którym mamy bezpośredni dostęp do obiektów Mongrel::HttpRequest i Mongrel::HttpResponse. Podstawowym zyskiem jest wydajność.
Dla przykładu porównałem najprostsze "Hello World" renderowane przez railsy (render :text => ...) oraz handlera mongrela. Poniżej znajduje się kod oraz wyniki pomiarów.
class StatusHandler < Mongrel::HttpHandler
def process(request, response)
puts request.params['REQUEST_URI']
response.start(200) do |head, out|
head["Content-Type"] = "text/html"
out.write "Hello World"
end
end
end
uri "/hello", :handler => StatusHandler.new, :in_front => true
class WelcomeController < ApplicationController
def index
render :text => "Hello World"
end
end
$ mongrel_rails start -e production -p 3000 -a localhost -S config/handler.rb
$ ab -c 1 -n 5000 http://localhost:3000/welcome
Requests per second: 311.26 [#/sec] (mean)
$ ab -c 1 -n 5000 http://localhost:3000/hello
Requests per second: 1672.43 [#/sec] (mean)
Jak widać handler jest 5 razy szybszy. Można go wykorzystywać np. do serwowania dynamicznych xmli, API lub tego typu rzeczy. Możemy być jednak w tarapatach gdy okaże się, że musimy zamienić mongrel na inny serwer. Nasze handlery przestaną działać i cały trud pójdzie na marne.
Rack daje nam podobne możliwości i dodatkowo (zupełnie gratis!) obiecuje działać niezależnie od serwera. Poniżej znajduje się przykładowa implementacja najprostszej aplikacji. Rack wymaga jedynie by obiekt aplikacji miał metodę call, która jako parametr dostanie hash (m.in. z parametrami żądania). Metoda ta powinna zwrócić 3 elementową tablicę w postaci [status, response_headers, body], gdzie status to numer zwracanego statusu http (np. 200), response_headers to hash z nagłowkami odpowiedzi, a body to obiekt, który odpowiada na metodę 'each' zwracając kolejne części ciała odpowiedzi.
require "rubygems"
require "rack"
class HelloApplication
def call(env)
return [200, {"Content-type" => "text/html"}, "Hello World"]
end
end
app = HelloApplication.new
case ARGV.first
when "mongrel"
require "mongrel"
Rack::Handler::Mongrel.run(app, :Port => 3000)
when "thin"
require "thin"
Rack::Handler::Thin.run(app, :Port => 3000)
when "ebb"
require "ebb"
Rack::Handler::Ebb.run(app, :port => 3000)
else
puts "usage: ruby hello.rb [mongrel|thin|ebb]"
end
Do naszego programu przekażę parametr określający jaki serwer chcę odpalić. Mógłbym jeszcze bardziej uprościć sprawę i posłużyć się narzędziem "rackup" (dostarczony razem z gemem), jednak handler ebb nie respektuje opcji :Port (oczekuje :port) dlatego tego nie zrobiłem.
Co powiecie na mały test wydajności?
$ ruby hello.rb mongrel
$ ab -c 1 -n 20000 http://localhost:3000/
Requests per second: 2446.64 [#/sec] (mean)
$ ruby hello.rb thin
$ ab -c 1 -n 20000 http://localhost:3000/
Requests per second: 3401.43 [#/sec] (mean)
$ ruby hello.rb ebb
$ ab -c 1 -n 20000 http://localhost:3000/
Requests per second: 5202.35 [#/sec] (mean)
Ponad 5000req/s, calkiem nieźle.
Kolejną dosyć ciekawą rzeczą są middlewary dostarczone wraz z Rackiem. Zadaniem dla middlewara jest opakowanie wywołania "call" aplikacji (wzorzec dekoratora). Możemy zatem zrobić coś przed lub po żądaniu. Przykładowo taka aplikacja w przypadku wystąpienia wyjątku mogłaby go przechwycić i wyświetlić stronę z opisem błędu oraz dodatkowymi informacjami (np. zrzutem stosu). Rack dostarcza nam do tego gotowy middleware o nazwie Rack::ShowExceptions. Oto przykład użycia i wynik w postaci zrzutu ekranu.
require "rubygems"
require "rack"
class HelloApplication
def call(env)
raise "Something went wrong!"
return [200, {"Content-type" => "text/html"}, "Hello World"]
end
end
app = HelloApplication.new
app = Rack::ShowExceptions.new(app)
case ARGV.first
when "mongrel"
require "mongrel"
Rack::Handler::Mongrel.run(app, :Port => 3000)
when "thin"
require "thin"
Rack::Handler::Thin.run(app, :Port => 3000)
when "ebb"
require "ebb"
Rack::Handler::Ebb.run(app, :port => 3000)
else
puts "usage: ruby hello.rb [mongrel|thin|ebb]"
end
Dodałem tylko dwie linijki. W jednej rzucam wyjątek (coś złego stało się w aplikacji), w drugiej opakowuję obiekt aplikacji za pomocą klasy Rack::ShowExceptions.

mod_rails? - mod_rack!(aka passenger)
Passenger początkowo został napisany z myślą o railsach. Na całe szczęście autorzy dodali wsparcie dla Racka, zatem moduł obsługuje wszystkie inne frameworki, które potrafią współpracować z Rackiem (z bardziej znanych: merb, camping, waves, sinatra, ramaze). Wszystkie aplikacje oparte na tych frameworkach możesz odpalać przy pomocy passengera.
Podsumowanie
Rack moim zdaniem jest jednym z ważniejszych projektów ostatnich miesięcy jeśli chodzi o Rubiego. Po pierwsze wprowadza standardowy sposób komunikacji serwer aplikacji - aplikacja webowa, dzięki czemu aplikacje możemy odpalać na różnych serwerach nie martwiąc się czy odpowiedni handler został zaimplementowany. Po drugie daje możliwość "zejścia do podziemi", tj. pracować z surowym żądaniem http, dzięki czemu zyskujemy sporą wydajność. Taki kod także jest przenośny pomiędzy serwerami (przeciwnie np do handlerów mongrela z oczywistych względów). Po trzecie daje możliwość dołączania warstw pośrednich (logowanie, raportowanie błędów), ale także możliwość łączenia dwóch aplikacji opartych o rack (czego nie pokazałem, ale nie jest trudno wyobrazić to sobie).
Jestem początkujący, uczę się PHP, HTML i CSS, jestem ciekaw ile zajęło ci czasu żeby nauczyć się i rozumieć to co napisałeś powyżej :)
@mateyko, a ja się zastanawiam czy mogłeś tak szybko przeczytać?:)
@radarek: nie trzeba. szybki „skan” – jak mawia dziadek Nielsen – wystarczy
Strasznie fajnie, że opisałeś RACK-a. Ja również uważam, że jest to jeden z ważniejszych projektów Rubiego.
Bardzo podoba mi się kombinacja RACK+ebb, czekam na rozwój i ustabilizowanie tego serwera. Tyle tysięcy requestów na sekundę jest bardzo pociągającą sprawą. Mając tak szybki adapter aż się prosi, by budować web services na mikroframeworkach opartych na RACK.
Widać, że nikt dokładnie nie przeczytał, bo nie zwrócił uwagi na „rządania http” :)
Skasuj komentarz, jak poprawisz.
Wreszcie cos (koniec sesji ?;-) bardzo ciekawy wpis.
PS > Eh, to byly czasy gdy byl tylko mod_ruby i fcgi :-)
@sharnik: ale wstyd, dzięki, poprawione :).
‘Po drugie daje możliwość „zejścia do podziemi”, tj. pracować z surowym żądaniem http, dzięki czemu zyskujemy sporą wydajność.’... no i proszę, wielbiciel prostego obiektowego opisu zjawisk cieszy się z możliwości grzebania w bebechach… jakby to było cudownie gdyby RoR zapewniał wydajność bez konieczności schodzenia do podziemia :), a tak… pozostaje CGI, C, albo handler.ashx. Ok… przeginam i ironizuję, ale od jakiegoś czasu zastanawiam się czy inwestować czas w poznawanie RoR, dojrzewam… ale straszy mnie wizja, że mój śliczny składniowo, prosty i czytelny kod poukładany w zgodzie z superelastycznym wzorcem projektowym będzie w działaniu jak stylowa bryczka, którą wyprzedzi byle
$rowerzysta:), ok, ok wydajność to nie wszystko… ale ma znaczenie, niemałe… warto inwestować?Nie warto, nie skaluje się.
@!$:), widzisz, pisząc ten wpis nie chciałem dać do zrozumienia, że dzięki rack’owi możemy pisać bardzo niskopoziomowo i wydajnie. Od razu zaczęłoby się opowiadanie, że rails jest wolny i w ogóle to najlepiej osadzać kod bezpośrednio w htmlu (czy też html bezpośrednio w kodzie). Widzę tutaj możliwość dodawania middleware lub ewentualnie napisanie sobie prostego frameworka (pytanie tylko czy to ma sens?). Rails to rails. Framework robi za nam sporo więc sporo zabiera (prędkości). Coś za coś. Jeśli Twoja aplikacja to takie moje przykładowe „hello world” to faktycznie możesz ją serwować bezpośrednio „rackiem”. Tylko, jaka aplikacja jest tak prosta?;)
Pisałem już (chyba nawet kilka razy), że nie znam się na ruby’m, ale mimo wszystko śledzę twojego bloga.
Jedna rzecz zwróciła moja uwagę: jak to jest, że mongrell+rack wypada szybciej niż te handlery mongrellowe o których na początku pisałeś?
@GiM, przepraszam za późną odpowiedź ale byłem w podróży :). Nie jestem w 100% pewien, ale wydaje mi się, że dzieje się tak ponieważ Rack używa handlera opartego na 1 handlerze mongrela. Natomiast w naszej aplikacji railsowej z dodatkowym handlerem musi istnieć jeszcze jakiś nadrzędny handler, który sprawdza że dla adresów zaczynających się od „/hello” ma być odpalony specjalny handler, zaś pozostałe requesty idą do railsów. Zatem są tutaj co najmniej 2 kroki (wpierw ten, główny niewidoczny dla nas handler, potem ten nasz).
EDIT:
ps. nie przeszkadza mi to, że pomimo iż na Rubym się nie znam to śledzisz mojego bloga ;-).