Aplikacja działająca na dwóch serwerach
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
.