Zazwyczaj aplikacje, które piszemy, nie mają użytkowników i zbyt wiele ruchu. Czasem coś jednak pójdzie nie tak, pojawią się użytkownicy i jeden serwer na którym nasza aplikacja działa, przestaje wystarczać. Poniższy przewodnik opowiada jak skonfigurować dwa (lub więcej) serwerów do obsługiwania naszej aplikacji. Może się on przydać, również osobom, chcącym połączyć się z bazą danych umieszczoną na innym serwerze niż aplikacja.

Założenia

  • Zakładam, że korzystasz z Capistrano. Jeśli nie, to prawdopodobnie powinieneś.
  • Posiadasz Railsową aplikację zdeployowaną na jednym serwerze produkcyjnym.
  • Korzystasz z Nginxa, jak serwera WWW z zainstalowanym Phusion Passengerem. (Konfiguracja dla Apacha będzie podobna)
  • Jako baza danych wykorzystywany jest MySQL. Instrukcje udostępniania połączenia dla innych rodzajów baz danych mogą odbiegać od tego co jest opisane w tym przewodniku.
  • Serwer w tym przypadku to Debian, ale na wszystkich systemach *nixowych powinno to wyglądać podobnie.

Schemat konfiguracji

Master serwer – główny serwer, który będzie przyjmował żądania i je rozdzielał (część przekaże na inne serwery, część obsłuży sam). Na nim znajduje się baza i procesy chodzące w tle (backgroundrb, delayed_job, sphinx itp...)
Slave serwer – drugi serwer, służący wyłącznie jako serwer aplikacji, łączy się z bazą znajdującą się na master serwerze.
[caption id="attachment_295" align="aligncenter" width="340" caption="Schemat konfiguracji"][/caption]

Taka konfiguracja umożliwia dodanie więcej niż jednego równoległego slave serwera. W pewnym momencie warto również pomyśleć o przeniesieniu bazy na osobny serwer.

Ustawienia capistrano

Capistrano oparte jest o system ról, z których skorzystamy. Różnym serwerom możemy przypisać różne role, a poszczególnym rolom różne zadania. Dzięki temu możemy sprawić, by zadania związane z bazą i procesy chodzące w tle pracowały tylko na maszynie z rolą: :db. Jeśli mamy już skonfigurowane Capistrano, to powinniśmy do roli :app przypisać adres ip drugiego serwera.

role :app, "master_serwer_ip", "slave_serwer_ip"
role :db, "master_serwer_ip"

Zadania, które powinny być wykonywane wyłącznie na serwerze głównym należy oznaczyć :roles => :db.

task :restart, :roles => :db do
  run "cd #{release_path} && RAILS_ENV=production rake ts:rebuild &"
end

Tworzenie usera na slave

Na slave serwerze należy stworzyć użytkownika z takim samym hasłem i nazwą jak na serwerze master. Załóżmy, że posiadamy użytkownika capistrano z hasłem some_password. Logujemy się jako root na slave serwer:

useradd capistrano -p some_password
cd /home
mkdir capistrano
chown -R capistrano capistrano
chrgp -R capistrano capistrano

I mamy stworzonego użytkownika.

Przygotowanie deploya

Należy na serwerze slave stworzyć katalog do którego będziemy robić deploye. Najwygodniej nam będzie, jeśli struktura katalogów będzie identyczna jak na serwerze master. Jeśli nasza aplikacja nazywa się facebook_killer wykonujemy:

mdkir /var/www/facebook_killer 
chown capistrano /var/www/facebook_killer/
chgrp capistrano /var/www/facebook_killer/

Lokalnie capistrano powinno nam przygotować niezbędne katalogi. Wywołujemy więc

cap production deploy:setup

Robimy deploy i ewentualnie poprawiamy co trzeba (instalujemy gemy, linkujemy katalogi).

cap production deploy

Pliki konfiguracyjne

Pora przygotować pliki konfiguracyjne. Wszystkie pliki oprócz database.yml powinny być identyczne jak na masterze. Należy więc dostosować ich zawartość, po czym zajmujemy się plikiem database.yml:

vim /var/www/facebook_killer/shared/config/database.yml

Ponieważ chcemy się połączyć z bazą na masterze, należy ustawić wszystko tak jak na masterze i dodać dwie linijki w sekcji production (port i adres serwera z bazą danych):

adapter: mysql
encoding: utf8
database: facebook_killer_prd
username: database_user
password: database_password
host: master_serwer_ip
port: 3306

Odpalamy serwer ręcznie w trybie produkcyjnym, żeby zobaczyć czy działa. (W razie potrzeby należy doinstalować brakujące gemy):

cd /var/www/facebook_killer/current/
RAILS_ENV=production script/server production

Jeśli nie możemy teraz połączyć się bazą danych, znaczy to, że mamy odpowiednio zabezpieczony serwer produkcyjny. Wystarczy teraz tylko udostępnić bazę slave serwerowi. Jeśli jednak połączyliśmy się bez problemów, znaczy, to że zbyt łatwo każda osoba znająca hasło do bazy może się z nią zdalnie połączyć. Wnioski co to znaczy, należy wyciągnąć samemu.

Zdalny dostęp do bazy

Na serwerze z bazą (master serwer) należy ustawić dostęp do bazy danych. Poniższe podpunkt to ekstrakt ze świetnego przewodnika jak umożliwić zdalne łączenie się z serwerem bazy danych (w języku angielskim).

Logujemy się na master serwer i szukamy pliku configuracyjnego MySQL: my.cnf. (W zależności od dystrybucji linuxa może być w w różnych katalogach.)

vim /etc/mysql/my.cnf

Należy zmodyfikować sekcję [mysqld], upewaniając się, że linia skip-networking jest wykomentowana, a bind-address wskazuje na adres master serwera.

[mysqld]
user            = mysql
pid-file        = /var/run/mysqld/mysqld.pid
socket          = /var/run/mysqld/mysqld.sock
port            = 3306
basedir         = /usr
datadir         = /var/lib/mysql
tmpdir          = /tmp
bind-address    = master_serwer_ip
# skip-networking

Następnie należy zapisać plik i zrestartować serwer:

/etc/init.d/mysql restart

Kolejną rzeczą jest nadanie uprawnień użytkownikowi bazy danych łączącemu się z slave serwera. Łączymy się z bazą danych:

mysql -u root -p mysql

I nadajemy niezbędne uprawnienia.

GRANT ALL ON facebook_killer_prd.* TO database_user@'slave_server_ip'
IDENTIFIED BY 'database_password';
exit

Należy jeszcze otworzyć port 3306. Bezpieczniej jest to zrobić umożliwiając wejścia tylko spod adresu ip slave serwera.

/sbin/iptables -A INPUT -i eth0 -s slave_serwer_ip -p tcp --destination-port 3306 -j ACCEPT
service iptables save

Warto teraz odpalić serwer ręcznie i zobaczyć czy poprawnie łączymy się z bazą. Jeśli, występują jakieś problemy, polecam zajrzeć do oryginalnej instrukcji.

Ustawienie load balancingu

Pora na najważniejszą rzecz. Skorzystamy z dyrektywy upstream.

Na master serwerze ustawiamy:

# rozdziela żądania
 upstream www.facebook_killer.com {
     server 127.0.0.1:81;
     server slave_serwer_ip;
 }

 # nasłuchuje żądań i je przekazuje
 server {
     listen 80;
     server_name www.facebook_killer.com;
     location / {
       proxy_pass http://www.facebook_killer.com;
     }
}

 # uruchamia aplikację
 server {
     listen       81;
     server_name www.facebook_killer.com;
     root /var/www/facebook_killer/current/public;
     passenger_enabled on;
}

W skrócie chodzi o to, że pierwsza dyrektywa server nasłuchuje i przekazuje żądania do upstream, które rozdziela je między serwer zdalny (slave_serwer) oraz lokalny (zdefiniowany w drugiej dyrektywie) na porcie 81. Warto zapoznać się z dokumentacją dyrektywy upstream.

Przydatna uwaga: Upstream musi się nazywać tak samo jak domena (bez http), inaczej powyższa konfiguracja nie zadziała.

Na drugim serwerze konfiguracja jest dużo prostsza:

 server {
     listen 80:
     server_name www.facebook_killer.com;
     root /var/www/facebook_killer/current/public;
     passenger_enabled on;
}

Czyli tak naprawdę jest to zwykła konfiguracja nginx. Domena facebook_killer powinna wkazywać oczywiście na adres master serwera. Restartujemy nginxa na obu serwerach i gotowe. Możemy szykować się na wykop effect.

Uwagi końcowe

Domyślnie nginx, wysyła przychodzące żądania do serwerwów po równo, lub zgodnie z wagami, które można przypisać poszczególnym serwerom. Rozwiązanie to może spowodować, że jeden z serwerów dostanie za duże obciążenie, w czasie gdy inny będzie wolny. Warto zatem rozważyć użycie Global Queue. Działa ona w ten sposób, że tworzona jest jedna globalna kolejka żądań i są one przekazywane do serwerów dopiero wtedy, gdy serwery są w stanie obsłużyć kolejny request.

Oczywiście to jest najprostsza wersja takiej konfiguracji, która działa. Myślę, że powyższe wskazówki są dobrym punktem wyjścia do uruchomienia aplikacji działającej na kilku serwerach. Dodanie kolejnego serwera, po jego skonfigurowaniu to już tylko dodatkowa linjka w upstream.