Apache + PHP+ FastCGI + Spawn FastCGI? Wydajność, migracja.

Mijający już weekend upłynął mi pod znakiem migracji serwera www.poema.art.pl na FastCgi. Sporo alternatyw, sporo, ale niepełnych informacji - trudno się w tym połapać komuś, kto nie jest zaznajomiony z tą technologią. Ale od początku. Dlaczego warto się przerzucić  na FastCgi? Otóż powody są co najmniej dwa:

  1. wydajność
  2. bezpieczeństwo

Rozwinięcie wątku wydajności należałoby zacząć od wyjaśnienia w jaki sposób FastCgi działa. Otóż dzięki FastCgi separujemy wykonanie kodu PHP od wątków Apacza. Apacz w modelu FastCgi służy wyłącznie do komunikacji z przeglądarką klienta. W momencie kiedy klient zażąda uruchomienia skryptu PHP - żądanie to przekazywane jest do 'serwera FastCGI' który używając interpretera PHP uruchamia skrypt i odsyła wynik do Apacza. Wygląda to mniej więcej tak:

+----------+  TCP 80  +----------+ Unix socket
|  Klient  | <----->  |  Apache  | <-----+
+----------+          +----------+       |
                       user: http        |
+----------+          +----------+       |      +-------------+        +--------------+
|  Klient  | <----->  |  Apache  | <-----+----> | PHP FastCgi | <----> | Server MySQL |
+----------+          +----------+       |      +-------------+        +--------------+
                       user: http        |        user: john
+----------+          +----------+       |
|  Klient  | <----->  |  Apache  | <-----+
+----------+          +----------+
                       user: http

Przy użyciu spawn-fastcgi uruchamiamy dla każdego z użytkowników własny serwer PHP FastCgi który nasłuchuje na Unixowym sockecie. Apache komunikuje się po sockecie z wybranym zgodnie z ustawieniami (per vhost) serwerem żądając wykonania skryptu. Dzięki temu otrzymujemy izolację wątków PHP od wątków Apache. Dodatkowo PHP FastCgi działając z uprawnieniami użytkownika, nie zaś serwera Apache ma możliwość zapisu do katalogów użytkownika, a także ograniczenia odczytu plików do których posiada uprawnienia.

Ok, ale co z wydajnością?

Po pierwsze Apache, dzięki temu że nie parsuje w każdym swoim wątku PHP zabiera o wiele mniej pamięci. Przy dużej ilości żądań jest  to sprawa kluczowa. Dzięki modelowi FastCgi można w prawidłowy sposób wykorzystać mechanizm database persistent connections (eg. w MySQL). W przypadku wielu wątków Apacza parsujących PHP nawiązywanych jest tyle połączeń ile wątków. Nowe wątki nie potrafią użyć ponownie połączeń utworzonych przez zabite wątki. Dzięki temu w parę minut osiągamy maksymalny limit połączeń z bazą.  W przypadku FastCgi nawiązanych zostaje tyle połączeń ile wątków i połączenia te są cały czas utrzymywane. To jest także sprawa kluczowa dla serwisów o wysokim obciążeniu.

Bezpieczeństwo?

Wspomniałem wcześniej - wykonanie kodu PHP z uprawnieniami użytkownika. Ale nie tylko - separacja cache APC to kolejny element bezpieczeństwa. Dalej: możliwość pozbycia się safe mode w PHP - aktualnie to mechanizm z którego PHP się wycofuje. W wersji 5 ma już go nie być.

fCgi, FastCgi, fCgid?

Jest sporo zamieszania jeśli chodzi o mechanizm FastCgi. Istnieje mnóstwo alternatyw. Osoba zaznajamiająca się z tematem za pomocą poldek search *cgi*   może czyć się nieco zagubiona. Dlatego opiszę co potrzebujemy aby to wszystko właściwie uruchomić.

Zacznijmy od końca, czyli od PHP. Potrzebna będzie paczka zawierająca interpreter fcgi dla PHP - jest to dodatkowy moduł SAPI (oprócz eg. zwyczajnego cli, lub modułu do Apacha) które można skompilować, tudzież zainstalować. Pod PLD jest to pakiet "php-fcgi". Dalej, przyda się coś co będzie potrafiło utworzyć nam serwery FastCgi dla użytkowników. Pakiet "spawn-fcgi" jest całkiem dobry do tego. I wreszcie musimy nauczyć Apache komunikować się z naszymi serwerami. Doinstalujmy moduł "apache-mod_fastcgi".

Wszystkie moduły można bez obaw zainstalować na serwerze który używa modułu "apache-mod_php"  do parsowania kodu PHP. Generalnie można używać obu modeli parsowania jednocześnie - część vhostów i aplikacji parsować modułem Apacza, najbardziej obciążone vhosty puścić przez FastCgi.

Konfiguracja PHP

Nieco problematyczną sprawą w modelu FastCgi jest  konfiguracja PHP per vhost. Nie możemy do tego użyć ani plików .htacess ani dyrektyw php_flag w konfiguracji vhosta. Należy dla każdego z vhostów wymagającego niestandardowego konfigu utworzyć osobny php.ini. Ma to jedną zasadniczą wadę. /etc/php/php.ini który zawiera nasze istawienia PHP nie będzie czytany. Zatem dla każdego z vhostów należy mieć kopię php.ini zawierającą wsztstkie opcje konfiguracyjne które chcemy zmienić. Jeśli którejś z opcji nie ujmiemy w indywidualnym .ini będzie ona miała wartość domyślną, taką, jaka jest wkompilowana w php. Czytany jest za to katalog /etc/php/conf.d, przy czym każda zdefiniowana tam opcja konfiguracyjna nadpisuje tę zdefiniowaną w indywidualnym .ini.

W PHP 5 ma być możliwość używania w globalnym php.ini wpisów [myhost.com], dzięki czemu będzie można nadpisać globalne ustawienia per vhost. Niestety póki co, trzeba robić kopie globalnego php.ini i zmieniać w niej wybrane opcje. Minimalna postać indywidualnego php.ini, tak, aby zachować najważniejsze spersonalizowane dla dystrybucji ustawienia to:

; Safe mode w binarce PHP domyślnie jest on, więc jeśli serwis
; pod nim nie działa trzeba wyłączyć
safe_mode = Off
; To warto podać, ograniczymy możliwość otwierania cudzych katalogów
open_basedir = "/home/webhosts/www.mychost.pl:/home/services/httpd:/tmp"
include_path = ${open_basedir}
error_reporting  =  E_ALL & ~E_NOTICE & ~E_STRICT
display_errors = Off
log_errors = On
; Błedy do sysloga, to php będzie działało z prawami usera, więc nie
; można zapisywać błędów do wspólnego pliku
error_log = syslog
ignore_repeated_errors = On
html_errors = Off
upload_tmp_dir = "/tmp"
expose_php = Off
memory_limit = 32M
date.timezone = "Europe/Warsaw"
browscap = /etc/php/browscap.ini

Konfiguracja spawn-fastcgi

Tu sprawa jest dość prosta, otóż spawn-fastcgi nie ma żadych konfigów, a wszystkie parametry przekazujemy w linii poleceń.

spawn-fcgi -s /var/run/fastcgi/fcgi.myhost.sock -M 0660 \
  -P /var/run/fastcgi/fcgi.myhost.pid -U http -G http \
  -u 10012 -g 10012 -C 2 -- /usr/sbin/php.fcgi -c /etc/php/webapps.d/myhost.ini

Po kolei: -s  /var/run/fastcgi/fcgi.myhost.sock mówi w którym miejscu tworzyć sockety serwerów FastCgi. Ja wybrałem sobie katalog /var/run/fastcgi. Należy go utworzyć nadając prawo do zapisu użytkownikowi, z którego prawami będzie działał serwer oraz prawa do odczytu dla serwera Apache (user http). -M 0660 każe zmienić uprawnienia do utworzonego socketa na 660, -P /var/run/fastcgi/fcgi.myhost.pid mówi gdzie zapisujemy plik z PIDem serwera. Parametry -U http -G http są ważne, i mówią że prawa dostępu do socketa mają być nadane dla użytkownika http. Jako że z socketa będzie korzystał serwer Apache zmiana uprawnień jest konieczna. Dalej -u 10012 -g 10012 - tutaj definiujemy id (bądź nazwę) użytkownika i grupy z uprawnieniami którego ma działać FastCgi. Z uprawnieniami tego użytkownika będzie wykonany kod PHP.  Można tu użyć ID'ów użytkowników wirtualnych. Nie muszą być dodani do /etc/passwd. Idealnie to się wpisuje w model hostingu gdzie użytkowników trzymamy w bazie danych bazie danych. Ważną sprawą jest jedynie, aby wirtualny użytkownik miał prawa do zapisu do katalogu na który wskazuje parametr -s. Można mu nadać prawa 777 lub trzymać sockety w katalogach domowych użytkowników.  -C 2 określa ile instancji FastCgi ma zostać uruchomionych. Wartość trzeba dobrać doświadczalnie. W przypadku zbyt małej ilości Apache będzie podawał klientom bład 500. I wreszcie pora na interpreter: -- /usr/sbin/php.fcgi -c /etc/php/webapps.d/myhost.ini - używamy zainstalowanego wcześniej php-fgi - podając mu parametr -c z indywidualnym plikiem konfiguracyjnym. Jeśli nie podamy parametru -c zostanie użyty domyślny /etc/php/php.ini.

Jeden serwer FastCgi może obsługiwać wiele hostów wirtualnych. W najprostszym przypadku  można uruchomić jedną instancję serwera na prawach użytkownika mającego prawa do odczyty dla plików PHP (może to być http) i uruchomić na nim wszystkie nasze vhosty. Przy czym w oczywisty sposób pozbywamy się w takim przypadku korzyści płynących z separacji uprawnień. Przy hostingu wirtualnym lepiej przyjąć strategię: jedna instancja per 1 użytkownik na której odpalamy wszystkie jego vhosty.

W ogóle fajnie jest napisać sobie jakiś rc.skrypt, który będzie uruchamiał wszystkie zdefiniowane instancje podczas startu, pozwoli nam je zresetować, zatrzymać, etc.

Konfiguracja Apache

Ok, i teraz Apacz. Wszystko co zostało zrobione do tej pory można bez obaw robić na serwerze który obsługuje PHP via moduł Apacza. Operacje poniżej wymagają już zmiany w konfiguracji serwera WWW, a co za tym idzie są ryzykowne. Główne ryzyko, przy pomyłce w konfiguracji to możliwość zaserwowania niesparsowanych skryptów PHP, tak więc na działających serwerach polecam blokadę dostępu dla virtualki via Order deny, allow Allow from my.ad.re.ss Deny from all. Dodatkowo jak wspomniałem wcześnej model parsowania PHP via moduł jak i FastCgi może współistnieć jednocześnie, tak więc możemy przerzucać na FastCgi pojedyncze virtualki.

Ok, zaczynamy od pliku "/etc/httpd/conf.d/90_mod_fastcgi.conf" . Domyślnie jest tam jedynie linijka ładująca moduł fastcgi. Dopiszmy do pliku:

<IfModule mod_fastcgi.c>
  # Domyślne ustawienia serwera FastCgi, -pass-header Authorization jest bardzo ważne
  FastCgiConfig -idle-timeout 20 -maxClassProcesses 1 -pass-header Authorization
  # Zarejestrujmy dwie akcje
  Action php5-fcgi /php-cgi
  Action application/x-fcgi-php /php-cgi
  # Załóżmy że nasz serwer FastCgi będzie znajdował pod wirtualnym adresem /php-cgi
  <Location "/php-cgi">
    php_flag engine off
    Order Deny,Allow
    Deny from All
    Allow from env=REDIRECT_STATUS
    Options ExecCGI FollowSymLinks
    SetHandler fastcgi-script
  </Location>
</IfModule>

Ok, to wystarczy, żeby móc przystąpić do konfigurowania vhostów. Do vhosta, dla którego uruchomiona została już instancja spawn-fcgi i mamy utworzony socket w katalogu /var/run/fastcgi/fcgi.myhost.sock dopiszmy:

<VirtualHost www.myhost.pl:80>
[...]
<IfModule mod_fastcgi.c>
# Tak! wyłączamy engine PHP, dzięki temu moduł PHP, który
# cały czas działa zostawi pliki PHP tej wirtualki w spokoju
php_flag engine off
# Magic! Nadpisujemy handler application/x-httpd-php każąc wszystkim
# plikom php zdefiniowany wcześniej dla modułu zostać sparsowanym via FastCgi
Action application/x-httpd-php /php-cgi
# Definicja serwera, ścieżka musi być taka sama jak doc_root vhosta + /php-cgi
# Nie należy się przejmować faktem, iż /php-cgi nie istnieje w tej ścieżce, tak ma być!
FastCgiExternalServer /home/webhosts/myhost/php-cgi -socket /var/run/fastcgi/fcgi.myhost.sock
</IfModule>
</VirtualHost>

Jeśli mamy jakiekolwiek opcje konfiguracyjne dla php (php_flag, php_admin_value) to należy je usunąć.

Uruchamiamy!

Checklist:

  • Sprawdzamy czy  instancja serwera FastCgi uruchomiona przez spawn-cgi działa: ps aux | grep cgi:
    10008   7832  0.0  1.6  71324 34212 ? SN   14:19   0:18 /usr/bin/php.fcgi -c /etc/php/webapps.d/myhost.ini
    10008   7833  0.0  1.4  67408 29276 ? SN   14:19   0:19 /usr/bin/php.fcgi -c /etc/php/webapps.d/myhost.ini
    http   15808  0.0  0.5  72288 11548 ? SN   20:11   0:00 /usr/sbin/fcgi-pm
  • Sprawdzamy czy jest socket i czy http ma możliwośc odczytu: ls -la /var/run/fastcgi:
    -rw-r--r--  1 root root    4 09-20 14:19 fcgi.myhost.pid
    srw-rw----  1 http http    0 09-20 14:19 fcgi.myhost.sock
  • Sprawdzamy, czy w naszym konfigu apacza nie ma błedów: service httpd configtest

Pora na restart Apache. Po tym kod PHP dla konfigurowanego vhosta powinien być parsowany via FastCgi, co będzie sygnalozowane w wyniku funkcji phpini() wpisem Server API CGI/FastCGI.

Dla kodu parsowanego via moduł Apache Server API  to Apache Handler.

Post a Comment

Security Code: