Railsy: Lighttpd czy Apache 2.2.x?

Opublikowane przez Jarosław Zabiełło Sun, 29 Oct 2006 14:53:00 GMT

Odnośnie użycia Railsów w celu produkcyjnym, dużo się zmieniło od czasu pojawienia się Mongrela – małego, ale dosyć szybkiego serwera napisanego w Ruby i C. Następuje generalne odejście od podpinania kilku procesów FastCGI do Rubiego z różnych powodów. W drugim wydaniu Agile Web Development in Rails autorzy przyznali, że próbowali różnych opcji użycia modułu FastCGI. Zawsze ostatecznie były jakieś problemy.

Aktualnie zalecanym podejściem jest użycie szybkiego serwera HTTP (Lighttpd, Apache lub LiteSpeed) na froncie do obsługi statycznych plików (głównie obrazki, style kaskadowe i pliki Javascript). Aplikację RoR należy uruchomić za pomocą kilku procesów Mongrela (nie jak wcześniej: kilka procesów FastCGI). A tym, co zepnie je z serwerem HTTP jest moduł rozkładający ruch (load balancing).

Niestety, okazało się że w wypadku Lighttpd ten moduł zawiera błędy. Z kolei nowy Apache 2.2 posiada dobrze działający moduł load balancingu. Spowodowało to zrezygnowanie przez wielu z Lighttpd na rzecz nowego Apache 2.2.x.

Z racji tego, że mam trochę swobody w doborze technologii w pracy, postawiłem serwer na nowym Apache’u. Aplikacje Railsów podpinamy za pomocą mongrela i wbudowanego w Apache modułu load_balancer. Działa faktycznie sprawnie.

Czy to znaczy, że Lighttpd nie ma sensu używać? Skąd! W wersji 1.5 mają już mieć naprawiony moduł load balancingu. Ale nawet i bez tego, istnieje bardzo dobry, mały i szybki load balancer – Pound. Skuszony jednak potrzebą eksperymentowania oraz nabytym niedawno serwerem dedykowanym (w miejsce wcześniejszego VPS’a) postanowiłem przenieść wszystkie swoje prywatne serwisy (a jest tego sporo) z Lighttpd do Apache 2.2.3. Musiałem przenieść dwie aplikacje Rails, dwie aplikacje Django, dwie aplikacje PHP 5.1 oraz jedną duża aplikację Zope/Plone. I jak wrażenia?

Otóż, po 2 tygodniach używania, stwierdziłem, że to nie ma sensu. Apache pożera za dużo pamięci. Dzieje się dokładnie tak, jak pisałem wcześniej. Nie mogłem użyć trybu wielowątkowego, nie tyle ze względu na PHP ale ze względy na Django! Napisali bowiem, że nie należy tego trybu używać w wypadku ich frameworka. Django nie jest wielowątkowy. Musiałem więc przełączyć Apache z trybu worker na prefork. Zgodnie z obawami, każdy fork musiał marnować pamięć na moduł mod_php i mod_python nawet jak to nie ma sensu (np. podczas obsługi obrazków). Na dodatek nie mogłem coś uruchomić jednej aplikacji Django. Kompletnie nie wiem dlaczego. Druga była prawie identycznie skonfigurowana i działała.

Wróciłem zatem z powrotem do Lighttpd , Pounda i Mongrela (dla RoR) i FastCGI (dla Django i PHP). Efekty były bardzo widoczne: zwolniło mi się ponad 200 MB pamięci. Może to nie ma znaczenia w pracy, gdzie mamy 2GB ale ja mam tylko 1GB i muszę jeszcze uruchomić zasobożerny Plone.

Dokładniej wszystkie sprawdzone i działające konfiguracje opiszę w powstającej książce o Railach. Moja ostateczna konlkluzja jest taka: jeśli zależy ci na oszczędzeniu pamięci – wybieraj Lighttpd. Jeśli nie jest to kluczowa sprawa, a potrzebujesz jakieś specjalne, dodatkowe moduły (których Apache ma pełno) nowy Apache 2.2 spełni twe oczekiwania.

Posted in  | Tagi , , , ,  | 9 comments

Polski kanał IRC dla RoR

Opublikowane przez Jarosław Zabiełło Thu, 12 Oct 2006 14:55:31 GMT

Poza polskim forum mamyod niedawna kanał #rubyonrails.pl na serwerze irc.freenode.net gdzie można bardziej interaktywnie wymieniać się doświadczeniem na temat Rubiego i/lub Railsów. Na razie nie ma wiele osób, ale może się bardziej rozkręci.

Posted in ,  | 5 comments

Subversion /etc/init.d script

Opublikowane przez Jarosław Zabiełło Sat, 07 Oct 2006 00:57:00 GMT

Postanowiłem troszkę poprawić napisany wcześniej skrypt startowy do Subversion. Dorzuciłem też wersję w Pythonie.

#!/usr/bin/env ruby

VERBOSE = true
USER = "nobody"
APP = "/usr/bin/svnserve"
DIR = "/home/svn-repository"
PID = "/var/run/svnserve.pid"

def script action
  def start
    unless File.exists?(PID)
      system %Q|su -c "#{APP} -d -r #{DIR}" #{USER}|
      system "echo `pidof -o %PPID #{APP}` > #{PID}"
    end
  end
  def stop
    return false unless File.exists?(PID)
    system "/bin/kill -TERM #{File.read(PID)}"
    system "/bin/rm -f #{PID}"
    true
  end
  puts "#{APP} #{action}"  if VERBOSE
  $defout.flush # unbuffered output
  case action
    when :start
      start
    when :stop
      puts "#{File.basename(APP)} cannot be stopped because it cannot find #{PID}" unless stop
    when :restart
      stop
      start
  end
end

if %w{start stop restart}.include? ARGV.first
  script ARGV.first.to_sym
else
  puts "Usage: sudo #{__FILE__} {start|stop|restart}"
end

Ten sam skrypt w Pythonie:

#!/usr/bin/env python

import os, sys

VERBOSE = True
USER = "nobody"
APP = "/usr/bin/svnserve"
DIR = "/home/svn-repository"
PID = "/var/run/svnserve.pid"

def script(action):
  def start():
    if not os.path.exists(PID):
      os.system('su -c "%s -d -r %s" %s' % (APP, DIR, USER))
      os.system('echo `pidof -o %%PPID %s` > %s' % (APP, PID))
  def stop():
    if not os.path.exists(PID): return False
    os.system('/bin/kill -TERM %s' % file(PID).read())
    return True
  if VERBOSE: print "%s %s" % (APP, action),
  if action == 'start':
    start()
  elif action == 'stop':
    if not stop():
      print "%s cannot be stopped because it cannot find %s" % (os.path.basename(APP), PID)
  elif action == 'restart':
    stop()
    start()

if len(sys.argv) == 2 and sys.argv[1] in ('start', 'stop', 'restart'):
  script(sys.argv[1])
else:
  if VERBOSE:  print "Usage: sudo %s {start|stop|restart}" % sys.argv[0]

Posted in ,  | Tagi , ,  | 2 comments

Duży polski serwis w Pylons

Opublikowane przez Jarosław Zabiełło Sat, 07 Oct 2006 00:50:00 GMT

Widzę, że Wydawnictwo Murator w końcu wypuściło oficjalną wersję vortalu PoradnikZdrowie wykorzystującego framework Pylons bo w Wiki na stronie Pylonsów widzę dopisaną informację.

Posted in  | Tagi  | 2 comments

Ruby i skrypt startowy do SVN

Opublikowane przez Jarosław Zabiełło Thu, 05 Oct 2006 14:37:00 GMT

Subversion to świetny, darmowy system wersjonowania kodu. Jest znacznie lepszy od starego CVS’a. Niestety, nigdzie nie mogłem się doszukać skryptu startującego do serwera. Co gorsze, skrytp svnserve w ogóle nie ma opcji zapisu numeru swojego procesu co utrudnia pisanie kodu który go “zabije”. Na szczęście Linux posiada komendę pidof. Poniżej gotowy skrypt napisany w Ruby. Należy go umieścić w /etc/init.d, dodać atrybuty wykonywalności (chmod a+x subversion.rb) i dodać linki symboliczne aby startowała utomatycznie razem ze startem serwera (pod Debianem/Ubuntu robi to komenda update-rc.d -f subversion.rb defaults, pod RedHatem/CentOS/Fedorą jest do tego komenda chkconfig)

#!/usr/bin/env ruby

$USER = "nobody"
$APP = "/usr/bin/svnserve"
$DIR = "/home/SVN"
$PID = "/var/run/svnserve.pid"

def script action
  def start 
    system(%Q|su -c "#{$APP} -d -r #{$DIR}" #{$USER}|)
    system(%Q|echo `pidof -o %PPID #{$APP}` > #{$PID}|)
  end
  def stop
    return false if not File.exists?($PID)
    system(%Q|/bin/kill -TERM #{File.read($PID)}|)
    true
  end
  print "#{$APP} #{action}..."
  $defout.flush # unbuffered output
  case action 
    when :start 
      start()
      puts "done"
    when :stop
      if not stop()
        puts "#{File.basename($APP)} cannot be stopped because it cannot find #{$PID}"
      else
        puts "done"
      end 
    when :restart
      stop()
      start()
      puts "done"
  end
end

case ARGV.first
  when "start": script :start
  when "stop": script :stop
  when "restart": script :restart
end

unless %w{start stop restart}.include? ARGV.first
  puts "Usage: sudo #{__FILE__} {start|stop|restart}"
  exit
end

Posted in  | Tagi , ,  | 2 comments

Elastyczność Railsów

Opublikowane przez Jarosław Zabiełło Wed, 04 Oct 2006 15:01:00 GMT

Większość osób, które zetknęły się z frameworkiem Ruby on Rails zauważyła, że domyślne ustawienia są tam tak dobrane, aby maksymalnie uprościć i zminimalizować ilość ręcznej pracy przy ustawieniach. Jeśli więc ktoś trzyma się flozofii i konwencji nazw (np. dla tabel i ich pól) będzie miał znacznie mniej dodatkowej roboty z konfiguracją frameworka do pracy. Typowa definicja modelu wyglądać może nawet tak prosto:

class User < ActiveRecord::Base
end

Powyższa definicja zakłada, że tabela w bazie nosi nazwę “users”, a jej primary key nazywa się “id”. W przeciwieństwie do innych ORM’ów, railsowy ActiveRecord nie wymaga żadnego wcześniejszego definiowania pól. Ruby bez problemu sam sobie wyciagnie wszystkie nazwy pól w sposób dynamiczny wtedy, kiedy będzie mu to potrzebne.

Nietypowe struktury

Niestety, nie zawsze jesteśmy w takiej komfortowej sytuacji, że mamy wpływ na nazewnictwo tabel i ich pól. Jednak to nie jest większym problemem, bo Rails sobie z tym radzi równie prosto.

class User < ActiveRecord::Base
  set_table_name 'uzytkownicy'
  set_primary_key 'IdUzytkownika'
end

Jak widać, dużo pracy nam nie przybyło. W tym wypadku musieliśmy tylko jawnie podać jak się nazywa nasza tabela i jak się nazywa jej klucz główny.

Korzystanie z wielu baz

W momencie tworzenia projektu aplikacji RoR generowany jest automatycznie plik konfiguracyjny zawierający połączenie do bazy danych. Przykładowy plik config/database.yml mogłby wyglądać np. tak:

default: &defaults
  adapter: mysql
  host: localhost
  encoding: utf8
  username: jakis_login
  password: jakies_haslo

development:
  database: bookstore_dev
  <<: *defaults

test:
  database: bookstore_test
  port: 3307
  <<: *defaults

production:
  database: bookstore
  socket: /var/run/mysqld/mysqld.sock
  <<: *defaults

Co robi powyższy kod? Definiuje 3 bazy: produkcyjną, roboczą i testową. (Ta ostatnia jest używana tylko do testów jednostkowych więc nie powinno się tam wstawiać żadnych istotnych danych) W w/w konfiguracji zdecydowaliśmy, że wszystkie bazy będą korzystać z MySQL >=4.1 z ustawionym wew. kodowaniem UTF-8 (zalecane). Baza produkcyjna działać będzie na Linuksie, więc zamiast portu TCP, podano uniksowy socket. Baza testowa działa na porcie 3307, a robocza na domyślnym (3306). Główna sekcja “defaults” jest wspólna dla wszystkich trzech baz zgodnie z zasadą DRY (Don’t Repeat Yourself).

Załóżmy jednak, że chcemy korzystać z dodatkowej bazy i chcemy aby z niej korzystały niektóre modele. Żaden problem. Do pliku dopisujemy tylko dodatkową definicję połączenia do innej bazy:

other:
  adapter: mysql
  host: 192.168.0.123
  database: customers
  <<: *defaults

Jak widać, możemy tą metodą tworzyć aplikację korzystającą z rozproszonych baz. (W tym wypadku baza customers leży na zupełnie innym serwerze, ale korzysta z tego samego usera i hasła.)

No dobrze, ale jak modele mają rozpoznać z jakiego połączenia korzystają? To też prosta sprawa. Domyślnym połączeniem jest to, co zdefiniowano w sekcji domyślnej. Jedynie modele, które korzystają z innego połączenia, muszą to mieć jawnie zadeklarowane w definicji, np.

class Client
  establish_connection :other
end

Model Client oraz wszystkie modele utworzone z niego na drodze dziedziczenia, będą korzystać z innego połączenia niż domyślne.

BTW, to jedna z bardziej brakujących mi cech we frameworku Django (Pylons nie ma z tym żadnego problemu). Mam nadzieję, że wkrótce to poprawią i w Django będzie można bez problemu korzystać z wielu baz.

Wspolne modele dla wielu aplikacji Rails

W tym scenariuszu mamy za zadanie stworzyć szereg aplikacji webowych korzystających z oddzielnych baz o tej samej strukturze tabel. Chcemy uniknąć powielania kodu niezbędnego dla modeli ORM. Nie mamy też czasu (ani ochoty) aby siedzieć i stworzyć definicje do wszystkich tabel wraz z wyposażeniem ich w nasze własne, dodatkowe metody uwzględniające każdą, możliwą potrzebę w przyszłości. Zamiast tego, wolimy tworzyć i rozbudowywać obiektowy opis naszej bazy dla wszystkich aplikacji w miarę ich rozwoju i wzrostu złożoności. Chcemy oby nasz model biznesowy, opisywany przez ActiveRecord, rósł stopniowo wraz ze wzrostem naszych aplikacji RoR.

Do takiego zadania, najlepiej aby wszystkie aplikacje RoR korzystały ze wspólnego repozytorium modeli. W wypadku systemów POSIX (Linux, czy MacOS-X) najprościej to rozwiązać za pomocą linków symbolicznych. Jednakże, jeśli chcemy aby nasz kod był bardziej przenośny i działał także pod Windowsami musimy inaczej podejść do tego problemu: wyniesiemy definicje naszych modeli do zewnętrznych modułów Rubiego aby domieszkowały one klasy poszczególnych modeli Rails.

Wpierw musimy dla każdej aplikacji RoR wskazać gdzie leży repozytorium z wpołdzielonymi modułami. Najlepiej wstawić do pliku conf/environment.rb zmienną globalną Rubiego o nazwie, np. $SHARED.

$SHARED = File.dirname(__FILE__)+'/../../shared/'

Konstrukcja File.dirname… jest zalecana, gdyż daje pewny i jednoznaczny dostęp do pliku bez względy na to gdzie przekopiujemy cały nasz projekt. (Identycznie należy robić pod PHP aby uniknąć problemów z ustawieniami zmiennej include_dirs. Większość pehapowego lamerstwa używa zapisu require(“plik.php”) i potem się dziwi że coś im nie działa)

Załóżmy że wcześniej mieliśmy model zdefiniowany następująco:

class Book
  set_table_name 'ksiazki'
  belongs_to :author
  def self.commented(user_id)
    self.find(:all, :conditions=>['user_id=?',user_id])
  end
end

Mamy tu więc nie tylko nazwę tabeli niezgodną z konwencją Railsów, ale także dodatkową metodę klasową. Wpierw zróbmy z tego moduł:

module BookModule
  def self.extended(c)
    c.set_table_name 'ksiazki'
    c.belongs_to :author
  end
  def commented(user_id)
    self.find(:all, :conditions=>['user_id=?',user_id])
  end
end

Kod korzystający z tego modułu przyjmie postać:

if RAILS_ENV == "development"
  load $SHARED + 'models/book.rb'
else
  require $SHARED + 'models/book.rb'
end

class Book< ActiveRecord::Base
  include BookModule
  extend BookModule
end

Górny fragment jest potrzebny jeśli chcemy aby Webrick/Mongrel w trybie development się przeładowywał podczas zmiany kodu w Ruby. “include” włącza metody instancji. Zaś “extend” włącza wszystkie emetody modułu _BookModule _do klasy jako jej metody klasowe. Zwróć uwagę, że w module nie definiowaliśmy ich jako z prefiksem self. Co prawda, efektem ubocznym tego przykładu jest to, że metoda commented() będzie możliwa do uruchomienia jako metoda zarówno instancji jak i klasy, ale to raczej nie ma żadnego znaczenia.

Posted in  | Tagi , ,  | 11 comments