LNMP – Część 3: PHP

W poprzednich dwóch wpisach dotyczących LNMP zająłem się odpowiednio wyjaśnieniemdlaczego wybrałem takie właśnie połączenie i wspomniałem o repozytoriach, a także poruszyłem temat serwera www nginx. Dzisiaj przyszedł czas na kolejny odcinek z tej serii, tym razem dotyczący PHP. Ta technologia została wybrana ze względu na jej niesamowitą popularność. Możliwości również nie są wcale małe, wprost przeciwnie, więc wybór był w zasadzie oczywisty. W tym odcinku zajmiemy się instalacją interpretera PHP i jego konfiguracją.

Instalujemy PHP

Jak już wspomniałem w pierwszym odcinku, repozytoria Dotdeb na chwilę obecną oferują domyślnie PHP w wersji 5.4. Nic jednak nie stoi na przeszkodzie (oprócz oczywiście możliwych problemów z kompatybilnością skryptów jakich używamy) aby użyć nowszego PHP 5.5, który również jest już stabilny, a na dodatek oferuje zintegrowane opcache. To, którą wersję wybierzecie jest waszym osobistym wyborem, ja zachęcam od nowszej, pamiętajmy wtedy o dodaniu odpowiednich wpisów do listy repozytoriów. Instalujemy natomiast „zestaw podstawowy”:

aptitude install php5-fpm php5-gd php5-curl php5-mcrypt php5-mysql

Dlaczego akurat tak, a nie inaczej?

  • FPM – to jest główny moduł do obsługi PHP poprzez FastCGI.
  • GD – biblioteka graficzna pozwalająca na manipulację obrazami, wykorzystywana przez wiele skryptów.
  • cURL – biblioteka pozwalająca w dużym skrócie na wysyłanie zapytań http, również jest często wykorzystywana przez inne skrypty.
  • Mcrypt – interfejs dla biblioteki o takiej samej nazwie, daje możliwość używania algorytmów kryptograficznych, nawet jeżeli nie wykorzystamy tego samodzielnie, w wielu przypadkach będzie po prostu konieczne.
  • MySQL/MySQLi – obsługa bazy danych MySQL/MariaDB.

Konfiguracja spawnera

Zaraz po zakończeniu instalacji PHP będzie już działać dla domyślnego użytkownika www-data. Założenia mojej serii są jednak takie, że będziemy tworzyć witryny per użytkownik – każdy z nich będzie miał swoje witryny, swoje katalogi i swoje procesy PHP. Pozwala to lepiej rozłożyć obciążenie, a także zabezpieczyć dane jeżeli posiadamy wiele witryn. Samego etapu tworzenia użytkownika nie będę opisywał – po prostu użyjmy np. adduser, następnie w katalogu domowym delikwenta utwórzmy katalog tmp oraz www (po co, będzie w dalszej części). Teraz możemy zająć się utworzeniem spawnera PHP dla tego użytkownika. Przechodzimy do /etc/php5/fpm/pool.d/ i kopiujemy lub edytujemy domyślny plik. Poniżej przedstawiam uproszczony „schemat” obcięty do minimum na potrzeby tego wpisu:

# Nazwa spawnera, użytkownika i grupy
[kowalski]

# Plik-serwer nasłuchujący do obsługi żądań dla PHP
listen = /tmp/php-fpm_$pool.sock 
listen.owner = $pool 
listen.group = $pool 
listen.mode = 0666 
user = $pool 
group = $pool 

# Ustawienia trybu działania, ilości procesów
pm = dynamic 
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 3
pm.max_spare_servers = 10
pm.max_requests = 100 

# Ustawienia glowne
php_admin_value[error_log] = /home/kowalski/php_error_log
;php_admin_value[open_basedir] = /home/kowalski/:/tmp/
;php_admin_value[upload_tmp_dir] = /home/kowalski/tmp
;php_admin_value[session.save_path] = /home/kowalski/tmp

# Dodatkowe ustawienia
;php_admin_value[memory_limit] = 256M
;request_terminate_timeout = 300
;php_admin_value[max_execution_time] = 300
;php_admin_value[upload_max_filesize] = 32M
;php_admin_value[post_max_size] = 32M


# Chroot i chdir
;chroot = 
chdir = /

Jeżeli ktoś chce poznać wszystkie zapisy oraz dokładniejsze wytłumaczenie opcji, zapraszam do przejrzenia domyślnego pliku, a także dokumentacji technicznej. Sam wyjaśnię tylko te miejsca, które mogą wydawać się niejasne.

  • pm – określa tryb pracy spawnera. Może on być statyczny, gdzie cały czas działa taka sama liczba spawnerów i czeka sobie na żądania, ale także dynamiczna, zmieniająca się wraz z obciążeniem. Ze względu na różnych ruch, zdecydowałem się na użycie dynamicznego.
  • pm.max_children – maksymalna ilość procesów, jakie mogą zostać utworzone w miarę wzrostu obciążenia. Gdy zostanie osiągnięta wartość maksymalna, a ciągle będą pojawiać się nowe żądania, serwer www będzie zwracał komunikat błędu 500.
  • pm.start_servers – ilość procesów tworzonych na samym początku startu spawnera.pm.min_spare_servers – minimalna ilość procesów w stanie oczekującym na żądania (nudzących się).
  • pm.max_spare_servers – maksymalna ilość procesów oczekujących na żądania – jeżeli nudzących się będzie więcej, będą one automatycznie ubijane. Po co to wprowadzono? Aby wykluczyć konieczność ciągłego tworzenia i zabijania procesów. Jeżeli ruch rośnie, wzrośnie liczba oczekujących procesów i utrzyma się na takim maksymalnym poziomie.
  • pm. max_requests – maksymalna ilość żądań dla każdego procesu, po którym powinien on zostać zabity i utworzony na nowo. Ma to na celu wyeliminowanie wycieków pamięci poprzez automatyczne „resetowanie”.

W dalszej części mamy nieco ustawień – pozwalają one na nadpisywanie ustawień pliku php.ini dla każdego z procesów. Dzięki temu możemy np. dla jednej witryny mocno ograniczyć maksymalny czas wykonywania skryptu, a dla drugiej (np. do obsługi phpMyAdmina) zwiększyć jeżeli zajdzie taka potrzeba. Celowo umieściłem tam również dyrektywę open_basedir, oraz ustawienia chroot i chdir. Pierwsze rozwiązanie jest „miękkie”, acz pozwala w pewien sposób ograniczyć widoczność skryptów.

Po jego ustawieniu PHP nie będzie miało dostępu do katalogów znajdujących się wyżej niż wymienione. Dlatego wspominałem o tworzeniu katalogu tmp, aby zapewnić użytkownikom ich własne katalogi tymczasowe (mimo wszystko zalecam dostęp do /tmp). Drugie rozwiązanie to już separacja na całego, ale wymaga o wiele więcej pracy – po prostu trzeba wiedzieć jak tworzyć jaila i mieć taką potrzebę, a to nie jest przedmiotem tej serii.

Konfiguracja głównych ustawień PHP

Teraz możemy przejść do edycji głównych, globalnych ustawień PHP, edytujemy plik /etc/php5/fpm/php.ini. Znajdziemy w nim bardzo dobry opis wszystkich dyrektyw (w razie wątpliwości do dyspozycji mamy jeszcze internetowy manual), ja chciałbym zwrócić uwagę tylko na kilka z nich:

  • disable_functions – wyłączenie tych funkcji, które są nam zbędne. W razie ataku na jakiś z serwisów i umieszczeniu na serwerze tzw. shella PHP, może to nas obronić przed zrobieniem przez użytkownika całkowitego spustoszenia. Wiedzieć trzeba, że domyślnie nie stoi na przeszkodzie nic, aby z poziomu PHP uruchamiać polecenia powłoki. Zalecany przeze mnie spis funkcji do zablokowania wygląda następująco:
    pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,exec,shell_exec,passthru,system,proc_open,popen,show_source
  • expose_php – wyłączamy „przedstawianie się” PHP w nagłówkach odpowiedzi HTTP. Dzięki temu nie damy znać ewentualnym atakującym, jakiej wersji PHP używamy. O takim przedstawianiu się różnych skryptów wspominał na swojej prezentacji Paszczak na tegorocznym Hot Zlocie, naprawdę warto przestawić to na „Off”.
  • max_execution_time – maksymalny czas wykonywania skryptu wyrażony w sekundach. Domyślnie ustawiony na aż 2 minuty, a naprawdę niewiele skryptów potrzebuje aż czegoś takiego. Aby nie blokować skryptów, zmniejszamy czas np. do 30 sekund. Jeżeli gdzieś mamy skrypty wymagające dłuższego czasu, realizujmy to przez ustawienia spawnera a nie globalne.
  • max_input_time – maksymalny czas „odbierania” i przetwarzania danych wejściowych, np. przesyłanego pliku. Również ograniczamy i regulujemy przez spawnera.
  • memory_limit – jak sama nazwa wskazuje, jest to limit pamięci dla skryptu. Domyślnie jest to kosmiczne 128 MB… Takich wartości wymagają tylko giganty typu Invision Power Board, podczas normalnego użytkowania jest to wartość rzadko spotykana, nawet WordPress z ogromem wtyczek może mieć problem z przebiciem limitu np. 32 MB.
  • display_errors – reguluje wyświetlanie błędów. Nawet jeżeli się zdarzą, nie powinniśmy ich wyświetlać użytkownikom, wśród których mogą znajdować się osoby szukające luk. Wyłączamy.
  • log_errors – zapisywanie błędów do wskazanego pliku, to włączamy aby wiedzieć, czy faktycznie zdarzają się jakieś błędy w naszych skryptach.
  • cgi.fix_pathinfo – domyślnie zakomentowane ustawienie, bardzo ważne w przypadku użycia serwera nginx ze względu na bezpieczeństwo. Tę opcję koniecznie odkomentowujemy, a następnie ustawiamy na 0.
  • upload_max_filesize – maksymalna wielkość uploadowanego poprzez formularze pliku. W przypadku np. forów dyskusyjnych, ale nawet witryn budowanych w oparciu o popularne CMSy, wartość na poziomie 4-8 MB powinna być wystarczająca. Ustawiamy oczywiście wedle naszych potrzeb.
  • max_file_uploads – maksymalna ilość jednocześnie wysyłanych plików w ramach jednego żądania. Ponownie, musimy sobie zadać pytanie, czy np. naprawdę potrzeba nam ustawiać tutaj aż 100 plików? Chyba nie.

Dodatkowe ustawienia znajdziemy w pliku /etc/php5/fpm/php-fpm.conf, jednakże tego zagadnienia nie będę tutaj poruszał, bo na proste potrzeby nie jest to wymagane. Znajdziemy tam ustawienia specyficzne dla samego FPM, regulujące np. czasy restartów procesów, przerwy i limity. Wszystkie opcje są dobrze opisane, więc jeżeli ktoś będzie widział potrzebę aby tam zaglądać, nie będzie żadnego problemu. Na koniec wspomnę o logach – warto obserwować plik (domyślnie) /var/log/php5-fpm.log aby dowiedzieć się z niego, czy nasza konfiguracja jest optymalna. Przykładowy zapis:

[15-Aug-2013 20:00:13] WARNING: [pool testuser] seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers), spawning 32 children, there are 7 idle, and 38 total children

Daje nam on jasno do zrozumienia, że spawner się męczy i powinniśmy zwiększyć wspomniane wcześniej parametry aby lepiej zapanować nad obciążeniem.

Dodajemy obsługę PHP do nginx

Teraz przyszedł czas, aby wreszcie „powiązać” nginx z PHP, a więc wszystko co tyczy się plików PHP przesyłać do spawnera nasłuchującego i wysyłającego odpowiedzi. W sekcji serwera dodajemy po prostu odpowiedni zapis, oczywiście nic nie stoi na przeszkodzie, aby wydzielić go do osobnego pliku i załączać przy pomocy include:

location ~ \.php$ 
{
    try_files $uri =404;
    include fastcgi_params;
    fastcgi_pass unix:/tmp/php-fpm_kowalski.sock;   
}

Kluczem jest pierwsza linijka – jak już wspominałem ostatnio, if to zło. Ta linijka sprawdza, czy plik do którego odwołuje się żądanie w ogóle istnieje. To musi znajdować się w ustawieniach, chyba że chcemy narobić sobie biedy. Wystarczy, że ktoś na serwerze wrzuci „obrazek” będący tak naprawdę plikiem php, a potem z poziomu http odwoła się do zasobu np. obrazek.jpg/.php. Domyślnie spowoduje to obsłużenie obrazka jako plik php, a to może mieć fatalne skutki…

Try_files zabezpiecza nas przed tym i wspólnie z cgi.fix_pathinfo uniemożliwia robienie nam „kuku” w taki sposób. Dalej znajdziemy linijkę odpowiadającą za dodanie zmiennych dla PHP. Nie musimy sami ich wymieniać, kompilacja nginx z dotdeba ma już gotową pokaźną listę w wymienionym pliku. Ostatnia linijka to oczywiście wskazanie spawnera. Wykorzystujemy sockety jako rozwiązanie szybsze od TCP/IP, co miało również miejsce w konfiguracji spawnera. Oczywiście adres musi pokrywać się z tym, jaki ustawiliśmy w konfiguracji PHP. Tutaj mała uwaga techniczna: jeżeli używamy dodatkowych lokalizacji z jakimiś specyficznymi opcjami, w nich również musimy dodać obsługę PHP (jeżeli jest potrzebna). Najprostszy z brzegu przykład to autoryzacja HTTP:

location ^~ /tajny-katalog/ {
    auth_basic            "Dostep zabroniony";
    auth_basic_user_file  /home/kowalski/.sterowanie_swiatem_auth;

    # PHP Wrapper
    include php-kowalski; 
}

# PHP Wrapper
include php-kowalski;

Podsumowanie

Po skonfigurowaniu wszystkiego restartujemy proces PHP (service restart php5-fpm), a następnie w katalogu dostępnym z poziomu www umieszczamy plik z zapisem phpinfo. Sprawdzamy, czy PHP działa prawidłowo. To już na tyle w tym odcinku. Jeżeli macie jakieś uwagi, pytania, lub własne opinie, piszcie śmiało. Jeżeli zrobiłem błędy, przepraszam. Z chęcią je poznam i poprawię, tak jak i wasze rozwiązania, bo przecież wymiana poglądów pozwala często na zbudowanie czegoś jeszcze lepszego niż dwa osobne twory. W następnym odcinku zajmiemy się instalacją i konfiguracją bazy danych MariaDB.