Nginx és PHP

Az előző cikkben alapszinten megismerkedtünk az Nginx-szel, valamint megtudtuk, mitől és miben gyorsabb, mint a kihívójának szánt Apache.

Ebben, és a következő cikkben azt vizsgáljuk, hogy hogyan tudunk egy PHP alapú site-t futtatni vele, például egy Drupal alapú portált.

PHP kicsit másként

A legtöbben tisztában vannak azzal, hogy a PHP-t alapvetően kétféleképpen lehet futtatni: Apache modulként és CGI-ként. A két módszer közül az előbbi elterjedtebb, mert a CGI-s futtatás nagy terhet ró a kiszolgáló webszerverre, hiszne minden egyes lekéréshez külön PHP interpretert kell forkolni.

Az Apache modulos futtatásnak viszont van egy pár hátránya, amiről nem mindig esik szó:

  • Problémákhoz vezethet, ha szálkezeléses (threading) Apache-on belül használjuk
  • Ugyanazzal az userrel fut, mint az őt hostoló Apache folyamat
  • Az életciklusa szorosan össze van kötve az őt hostoló Apache folyamattal

Ebből a leginkább problematikus a második pont. Az Apache-t ugyanis alapvetően kétféleképp szokás futtatni:

  • Minden site ugyanazzal a felhasználóval fut (www-data, wwwrun, apache, httpd, …). Ennek hátrányai nyilvánvalóak: bármely site kódja képes lehet írni/olvasni bármely másik site könyvtárát
  • Minden site különböző felhasználóval fut, de a fő Apache folyamat root userként (ITK modul).

A második probléma megértéséhez kicsit bele kell túrni az Apache lelkivilágába. A fő Apache folyamat ugyanis az, amelyik a 80-as (443-as, stb) porton figyel, majd bejövő kérésnél a kérés fejlécét megvizsgálva dönti el, hogy milyen felhasználó nevében indítsa el a gyermekfolyamatot. Azonban - és ez nagyon fontos - amennyiben bármilyen sérülékenység van a HTTP kéréseket feldolgozó kódrészletben, a támadó ezt kihasználva egy root felhasználói jogokkal futó folyamat belsejében találja magát - ami teljes hozzáférést biztosít számára a rendszer minden komponenséhez és adatához. A fő Apache folyamatnak viszont muszáj root userként futnia, mert csak így tud adott felhasználóval gyermekfolyamatot indítani.

Mi hát a megoldás? Egy teljesen új futtatási mód: a FastCGI! A FastCGI ötvözi a CGI és a modulos PHP előnyeit: bármilyen felhasználóval elindulhat, viszont egynél több lekérést is ki tud szolgálni. Ezen felül teljesen elkülönítetten fut a webszervertől, egy többé-kevésbé standard protokollon kommunikálva vele, így képes lehet akár bebörtönözve is futni (chroot, jail), de akár egy teljesen különálló gépen is futhat.

Működését tekintve lényegesen egyszerübb folyamatról van szó: itt is egy darab, root felhasználóként futó felügyelő folyamat (control process) fut, ám ő nem foglalkozik semmilyen szinten a bejövő kérések feldolgozásával, hanem amint bejön egy, rögtön elkezd egy szabad feldolgozó (worker) szálat keresni, ha nem talál, akkor pedig indít egy újat. A feldolgozó szál az, ami először beleolvas a kérés fejlécébe. Ennek megfelelően a kéréseket nem a site-ok neve alapján osztja el, hanem az alapján, hogy mely portjára érkezett a kérés (a feldolgozó folyamatokat is ennek megfelelően lehet konfigurálni). Ez azt jelenti, hogy futtató felhasználónként egy portra (vagy unix socketre) van szükségünk a futtatáshoz.

PHP esetében a FastCGI futtatási mód egyébként nem annyira újdonság, már a 4-es verzióhoz is készült egy olyan patchkészlet, mely lehetővé tette a webalkalmazások ilyen módon való futtatását, azóta pedig minden PHP főverzióhoz elkészült, illetve az 5.3-as verziótól már szerves része a fő kódbázisnak is a FastCGI futtatási lehetőség. A továbbiakban ezzel foglalkozunk.

Tekintve, hogy az Nginx-hez nem készült PHP modul, és CGI-ket sem tud futtatni, a PHP integrációjának egyetlen újta-módja a FastCGI-s futtatás.

Egy FastCGI worker születése

A FastCGI használatba vételéhez fel kell telepíteni az ehhez szükséges csomagot, mely a legtöbbször php-fpm, php5-fpm vagy php53-fpm néven található meg a rendszerben. Van néhány olyan Linux disztribúció, mely alapból nem szállítja ezt a futtatási módot, de szinte minden ilyenhez létezik külső csomagfejlesztők által biztosított csomag.

Az FPM konfigja alapvetően két részből áll: a globális beállításokból, és az egyes ún. worker poolok konfigurálásából. A globális részben általában nincs túl sok dolgunk, ezt a csomagfejlesztők már jól beállították, hiszen itt olyasmiket lehet csak állítani, mint a naplófájlok útvonala, vagy a folyamatazonosítót tartalmazó fájl útvonala.

Ami számunkra sokkal-sokkal érdekesebb, az az egyes workerek konfigurálása. Három fő dolgot kell beállítani:

  • Milyen felhasználó nevében fusson
  • Hol figyeljen
  • Milyen stratégiát használjon a gyermekfolyamatok menedzselése során

Lássunk egy tipikus worker konfigot (csak a cikkben érdekes részeket hagytam meg, ennél több lehetőség is van):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
[www]
; The address on which to accept FastCGI requests.
; Valid syntaxes are:
;   'ip.add.re.ss:port'    - to listen on a TCP socket to a specific address on
;                            a specific port;
;   'port'                 - to listen on a TCP socket to all addresses on a
;                            specific port;
;   '/path/to/unix/socket' - to listen on a unix socket.
; Note: This value is mandatory.
listen = 127.0.0.1:9000
;listen = /var/run/php-fpm/www.sock

; Unix user/group of processes
; Note: The user is mandatory. If the group is not set, the default user's group
;       will be used.
user = nobody
group = www

; Choose how the process manager will control the number of child processes.
; Possible Values:
;   static  - a fixed number (pm.max_children) of child processes;
;   dynamic - the number of child processes are set dynamically based on the
;             following directives:
;             pm.max_children      - the maximum number of children that can
;                                    be alive at the same time.
;             pm.start_servers     - the number of children created on startup.
;             pm.min_spare_servers - the minimum number of children in 'idle'
;                                    state (waiting to process). If the number
;                                    of 'idle' processes is less than this
;                                    number then some children will be created.
;             pm.max_spare_servers - the maximum number of children in 'idle'
;                                    state (waiting to process). If the number
;                                    of 'idle' processes is greater than this
;                                    number then some children will be killed.
;  ondemand - no children are created at startup. Children will be forked when
;             new requests will connect. The following parameter are used:
;             pm.max_children           - the maximum number of children that
;                                         can be alive at the same time.
;             pm.process_idle_timeout   - The number of seconds after which
;                                         an idle process will be killed.
; Note: This value is mandatory.
pm = ondemand

; The number of child processes to be created when pm is set to 'static' and the
; maximum number of child processes to be created when pm is set to 'dynamic'.
; This value sets the limit on the number of simultaneous requests that will be
; served. Equivalent to the ApacheMaxClients directive with mpm_prefork.
; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP
; CGI.
; Note: Used when pm is set to either 'static' or 'dynamic'
; Note: This value is mandatory.
pm.max_children = 20
pm.process_idle_timeout = 10s

; The number of child processes created on startup.
; Note: Used only when pm is set to 'dynamic'
; Default Value: min_spare_servers + (max_spare_servers - min_spare_servers) / 2
;pm.start_servers = 20

; The desired minimum number of idle server processes.
; Note: Used only when pm is set to 'dynamic'
; Note: Mandatory when pm is set to 'dynamic'
; pm.min_spare_servers = 5

; The desired maximum number of idle server processes.
; Note: Used only when pm is set to 'dynamic'
; Note: Mandatory when pm is set to 'dynamic'
; pm.max_spare_servers = 20

; The number of requests each child process should execute before respawning.
; This can be useful to work around memory leaks in 3rd party libraries. For
; endless request processing specify '0'. Equivalent to PHP_FCGI_MAX_REQUESTS.
; Default Value: 0
;pm.max_requests = 500

; Chroot to this directory at the start. This value must be defined as an
; absolute path. When this value is not set, chroot is not used.
; Note: you can prefix with '$prefix' to chroot to the pool prefix or one
; of its subdirectories. If the pool prefix is not set, the global prefix
; will be used instead.
; Note: chrooting is a great security feature and should be used whenever 
;       possible. However, all PHP paths will be relative to the chroot
;       (error_log, sessions.save_path, ...).
; Default Value: not set
;chroot = 
; Pass environment variables like LD_LIBRARY_PATH. All $VARIABLEs are taken from
; the current environment.
; Default Value: clean env
;env[HOSTNAME] = $HOSTNAME
;env[PATH] = /usr/local/bin:/usr/bin:/bin

; Additional php.ini defines, specific to this pool of workers. These settings
; overwrite the values previously defined in the php.ini. The directives are the
; same as the PHP SAPI:
;   php_value/php_flag             - you can set classic ini defines which can
;                                    be overwritten from PHP call 'ini_set'. 
;   php_admin_value/php_admin_flag - these directives won't be overwritten by
;                                     PHP call 'ini_set'
; For php_*flag, valid values are on, off, 1, 0, true, false, yes or no.

; Default Value: nothing is defined by default except the values in php.ini and
;                specified at startup with the -d argument
php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f [email protected]
;php_flag[display_errors] = off
;php_admin_value[error_log] = /var/log/fpm-php.www.log
;php_admin_flag[log_errors] = on
;php_admin_value[memory_limit] = 32M

Fuh, kicsit hosszú lett, de nem akartam kivenni a magyarázó megjegyzéseket, mert rettentő hasznosak - noha a végleges konfig fájlból ezeket ki lehet gyomlálni

Szóval, ahogy látjuk, a konfig a port megadásával indul (listen, 10. sor), hogy hol akarjuk majd elérni a workereket. Ez lehet TCP port is, vagy egy ún. Unix socket, ami valójában nem más, mint egy speciális fájl, amit ugyan közvetlenül nem lehet sem írni sem olvasni, viszont kiválóan használható folyamatok közti kommunikációra. Ez utóbbi gyetlen korlátja, hogy kialakítása miatt csak egy gépen belül működik.

A következő két opció (user és group, 11-12. sorok) adja meg, hogy milyen felhasználó és csoport nevében fussanak a worker folyamatok. Ez határozza meg, hogy a PHP feldolgozó milyen fájlokat/mappákat írhat és olvashat, tehát minden jogosultságnak eszerint kell alakulnia a webalkalmazás fájljain/mappáin.

Ezután következik a worker folyamatok menedzselésének szabályozása (19-74. sorok). A konfig fájl kommentjei elég részletesen bemutatnak minden opciót, így én arra fókuszálnék, hogy melyik főbb stratégiát mikor érdemes használni:

  • ondemand: Fejlesztéshez ez a legjobb. Nem terheli a gépet feleslegesen kihasználatlan worker folyamatokkal, illetve nincs esélye hogy “beleragad” valami régebbi módosítás. Nagyon gyér látogatottságú, gyors válaszidőt nem igénylő oldalak esetében lehet még hasznos, például valami statisztika oldalnál.
  • static: Nevéből adódóan mindig egy fix worker készlettel dolgozik. Olyan oldalaknál hasznos, aminek előre tervezhető a terhelése, pl. intranetes oldalaknál, ahol mindig körülbelül ugyanannyi felhasználóra kell felkészülni
  • dynamic: Ez az, amit általánosan használunk publikus website-ok esetén. Konfigurációs opciói nagyjából hasonlóképpen alakulnak, mint a Prefork modullal futtatott Apache hasonló opciói, bár itt-ott van némi eltérés.

Sorban ezután nyílik lehetőség a chroot beállítására (85. sor). Fontos tudni, hogy a FastCGI-s PHP csak a worker folyamat elindulása után vált be chroot-ba, így a worker elindulásához szükséges programkönyvtáraknak nem kell a chroot részét képezniük, csak a különböző beépülő modulok által igényelt dolgoknak kell ott lenniük (pl. az XML modulokhoz a libxml2-nek). Ez a tizedére csökkenti a chroot méretét.

Végül két külön szekcióban a környezeti változókat (env) adhatjuk meg, illetve a közös php.ini opciókat bírálhatjuk felül, hasonlóan, mint az Apache modulként futtatott PHP esetében.

Nagyjából ennyiből áll egy PHP worker beállítása. A következő részben összekötjük az Nginx-et és a PHP-t, illetve ismételten szó kerül a teljesítményről is.

Folytatjuk

Comments