Any fool can make things bigger, more complex, and more violent. It takes a touch of genius and a lot of courage to move in the opposite direction. Albert Einstein

o Ruby

Rack - niech aplikacje przemówią wspólnym językiem

12 komentarzy | Kategorie: Narzędzia, Ruby, Techblog | trackback
Tagi:

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).

Jeśli spodobał Ci się wpis to może umieścisz ten blog w swoim czytniku RSS?

Komentarze

1. avatar icon mateyko napisał(a) 27 Cze 2008 o godz. 22:42:

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 :)

2. avatar icon Radarek napisał(a) 27 Cze 2008 o godz. 22:43:

@mateyko, a ja się zastanawiam czy mogłeś tak szybko przeczytać?:)

3. avatar icon Jacek Becela napisał(a) 28 Cze 2008 o godz. 00:26:

@radarek: nie trzeba. szybki „skan” – jak mawia dziadek Nielsen – wystarczy

4. avatar icon RazorJack napisał(a) 28 Cze 2008 o godz. 01:05:

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.

5. avatar icon sharnik napisał(a) 28 Cze 2008 o godz. 08:54:

Widać, że nikt dokładnie nie przeczytał, bo nie zwrócił uwagi na „rządania http” :)

Skasuj komentarz, jak poprawisz.

6. avatar icon Paweł Kondzior napisał(a) 28 Cze 2008 o godz. 10:13:

Wreszcie cos (koniec sesji ?;-) bardzo ciekawy wpis.

PS > Eh, to byly czasy gdy byl tylko mod_ruby i fcgi :-)

7. avatar icon Radarek napisał(a) 28 Cze 2008 o godz. 10:46:

@sharnik: ale wstyd, dzięki, poprawione :).

8. avatar icon !$:) napisał(a) 10 Lip 2008 o godz. 18:36:

‘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ć?

9. avatar icon ajatollah sharnik napisał(a) 10 Lip 2008 o godz. 19:26:

Nie warto, nie skaluje się.

10. avatar icon Radarek napisał(a) 12 Lip 2008 o godz. 00:31:

@!$:), 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?;)

11. avatar icon GiM napisał(a) 22 Lip 2008 o godz. 01:38:

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ś?

12. avatar icon Radarek napisał(a) 31 Lip 2008 o godz. 21:32:

@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 ;-).

Dodaj coś od siebie

Możesz korzystać ze składni Textile.

Pola oznaczone * są wymagane.

Proszę o dodawanie komentarzy związanych z tematem postu, sprawy osobiste proszę załatwiać przez maila bądź gg.

Zastrzegam sobie prawo do moderacji komentarzy (edycja, usuwanie).