tavis nörttimaailmassa

EksisONE - artikkeleita ja ohjeita nörttimaailmasta

PHP-FPM: Asetukset ja hienosäätö

Joka kerta kun kävijä avaa sivun, niin palvelimelle lähtetään useita pyyntöjä, englanniksi request.  Pyyntöjä tehdään käytännössä aina vähintään kaksi, mutta kun sivu, vaikka WordPressin julkaisu, avataan, niin jokainen sivun elementti vaatii aina pyynnön. Yksnkertaisesti tehty sivu pärjää parillakymmenellä pyynnöllä, mutta tyypillinen nykyaikaisempi sivu vaatii helposti pari sataa pyyntöä. Suurin osa pyynnöistä liittyy PHP-koodeihin dynaamisilla sivuilla ja niiden käsittely ottaa aikansa. Tuota aikaa yritetään saada mahdollisimman lyhyeksi, koska se on osa sitä mikä koetaan sivuston nopeudeksi tai hitaudeksi. PHP-FPM on yksi sellainen työkalu.

Jos sinulla on käytössä webhotelli, niin ohjeista ei ole sinulle suurtakaan hyötyä, ellei cPanelista löydy vastaavaa kohtaa. Ylipäätään kaikki järkevät ja hyödylliset säädöt koskevat vain ja ainoastaan virtuaalipalvelimia.

PHP-FPM on laajennos, joka tehostaa websivujen pyytämien PHP-scriptien suoritusta. Jos sinulla ei ole sitä vielä käytössä, niin asennusohjeet löydät täältä.

PHP-FPM:n oletusasetukset ovat mikroskooppisen pienille servereille, joilla ei ole oikeastaan yhtä käyttäjää enempää kuormaa. Jos ei ennakkoon tiedä tiedä mitä on tekemässä, niin useimmiten PHP-FPM:n asetuksia aletaan murehtimaan siinä vaiheessa, kun ollaan syystä tai toisesta vilkaistu logeja, ja törmätty huolestuttaviin warning-ilmoituksiin.

PHP-FPM:n logit löydät täältä (ainakin Ubuntussa ja muista vaihtaa versio oikeaksi):

cat /var/log/php7.3-fpm.log

Virheet ovat aina huono asia, mutta se mikä yleensä herättää, on monta riviä tätä:

[19-Apr-2020 22:35:07] WARNING: [pool www] server reached pm.max_children setting (5), consider raising it

Se ei ole virhe, vaan varoitus, joka aika ajoin kertoo oikeastakin ongelmasta, mutta useimmiten se tarkoittaa yhtä asiaa: olet jättänyt oletusasetukset päälle, jonka takia PHP-FPM:ltä loppuu työtila. Kyseessä on siis eräällä tavalla aivan sama asia kuin PHP:n kanssa, jossa WordPress ei anna siirtää yli 2 MB tiedostoja, koska asetukset ovat liian tiukalla php.ini tiedostossa.

Aina kun PHP-koodia kutsutaan ja se ajetaan, niin PHP käynnistää prosessin, jota kutsutaan työntekijksi, workeriksi, lapsiprosessiksi… nimiä on vaikka kuinka ja aika usein termin käyttö riippuu kirjoittajasta asian poysyessä kuitenkin samana. Minä puhun useimmiten työntekijästä, joka on vain käännös workerista.

Yksi prosessi tekee yhden asian ja sitten se sammuu. Kun perään tulee uusi pyyntö, niin taas käynnistetään uusi prosessi, joka valimistuttuaan sammutetaan. Tuo jatkuva käynnistäminen ja sammuttaminen vie aikaa sekä tehoja. Kun paikalla on vaikka 100 ihmistä, joista jokainen tekee 200 yhden sivun vaatimaa pyyntöä, niin se tarkoittaa joka sivulatauksella 20 000 pyyntöä, joissa jokaisessa pyynnössä PHP on käynnistänyt ja sammuttanut tekemistään. Ja kun periaatteessa tehdään yhtä asiaa kerrallaan, niin jono pitenee ja jono tarkoittaa odotusaikaa.

Esimerkki ei ole aivan pätevä, mutta jos aikaa kuluu käynnistämisiin ja sammuttamisiin 10 ms, niin 200 pyynnöllä, eli yhdelle ihmisille yhdellä sivulla, se tarkoittaa 2 sekunnin odottamista pelkästään prosessin käynnistämisen ja sammuttamisen takia.

Tuon tehostamiseen on tehty PHP-FPM. Se periaatteessa tekee asioita rinnakkain ja pitää kutsuja käsittelevät prosessit aktiivisina. Silloin saadaan enemmän kutsuja käsiteltyä useamman samanaikaisen prosessin avulla lyhyemmässä ajassa. Ei olekaan enää yhtä työntekijää, joka tulee töihin, tekee yhden työn, ja kävelee takaisin tauolle lähteäkseen samantien takaisin työpisteelleen. Onkin useampi työntekijä, jotka kaikki tekevät koko ajan töitä.

Kun työntekijät loppuvat kesken, niin saadaan em. ilmoitus. Se johtuu siitä, että PHP-FPM joutuu komentamaan kesken töidensä yhden tai useamman workerin vaihtamaan muihin hommiin, jolloin tekemättömien töiden jono kasvaa. Ja se johtuu asetuksista, joissa on varattu liian vähän työvoimaa. Ei se yleensä sivustoja kaada, mutta hidastaa.

Kuten työvoiman kanssa aina, niin mitään ei saada ilmaiseksi. PHP-FPM:n lasten/työntekijöiden tapauksessa hinta on muistin kulutus. Jokainen hengissä pidettävä prosessityöläinen vie muistia – jossainhan sen työpisteen on oltava. Siksi asetuksissa joudutaan hakemaan tasapainoa muistin kulutuksen ja saadun hyödyn välillä.

Lähtöasetelma

Minulla on DigitalOceanin VPS, jossa on 4 CPU ja 8 GB RAM. Käyttöjärjestelmä on Ubuntu 19.10. Koko serveripaketti on rakennettu niin, että frontendinä on Nginx (SSL ja HTTP/2), välissä on Varnish (reverse proxy eli cache) ja backendinä on Apache2 (www-palvelin).  Tietokannan hoitaa MariaDB ja objektien välimuistina on Redis.

Tätä kirjoitettaessa käytössä on PHP7.3, joten muuta, jos sinulla on jokin muu (tuoreempi, toivottavasti) versio. PHP:n versioilla 5.x asiat tehdään paikoin eri tavalla, mutta eihän kukaan enää käytä noin vanhaa. Jos käytät, niin päivitä. Jos et pysty vaihtamaan uudempaan PHP-versioon, niin vaihda välittömästi palveluntarjoajaa!

Teen kaiken SSH:ssa aina rootina, joten muista sudo tai su sopiviin paikoihin, jos kirjaudut user-oikeuksilla.

PHP-FPM:n  asetukset

PHP-FPM:n asetukset löytyvät kahdesta paikasta:

  • hakemistossa /etc/php/7.3/fpm/ on php-fpm.conf sille miten PHP-FPM ylipäätään toimii sekä php.ini PHP:n asetuksille
  • hakemistossa /etc/php/7.3/fpm/pool.d/ on poolien asetukset eli miten PHP-FPM toimii työntekijöiden kanssa ja oletuksena on www.conf

Jos käytät vain yhtä conf-tiedostoa, niin käytännössä muokkaat aina www.conf tiedostoa:

nano /etc/php/7.3/fpm/pool.d/www.conf

Aina muokkauksen jälkeen käynnistetään PHP-FPM uudestaan:

systemctl restart php7.3-fpm

Logi löytyy sieltä mistä muutkin: /var/log/php7.3-fpm.log.

pm.max.children

Arvo, joka pm.max.children kohtaan annetaan tarkoittaa yksinkertaistettuna kuinka paljon yhtäaikaisia pyyntöjä serveri voi käsitellä. Children, lapset, ovat kuin työntekijöitä, joista jokainen tekee yhden ja vain yhden työn. Tehtyään sen se jää joko odottamaan (on idle-tilassa) tai se lopetetaan. riippuen mikä moodi on käytössä.

Lyhyesti: pm.max_children on maksimimäärä työntekijöitä, jotka PHP-FPM voi käynnistää.

Moodit ovat PHP-FPM:n tapoja säädellä työntekijöiden käyttäytymistä ja ne esitellään alempana.

Kyse on kuin kaupasta, jossa max_children tarkoittaa kassojen lukumäärää yhteensä. Jos pm.max_children = 1 niin talossa on vain yksi kassapiste. Jos pm.max_children = 20 niin kassoja on kaikkiaan saatavilla 20. Asiakkaiden lukumäärä (eli paljonko pyyntöjä, niitä requesteja) ratkaisee riittävätkö kassat vai tuleeko jonoa. Ja aivan kuin kaupassa, niin mitä enemmän työntekijöistä on istuu kassoilla, niin sitä vähemmän on muuta henkilökuntaa tekemässä jotain toisia töitä. Joten tarkoitus ei ole, että kassoja on tolkuttomasti auki, vaan että kassoja on juuri sen verran auki, että saadaan jonot estettyä tai ainakin hoidettua nopeasti.

Jos kassoja on kaupassa auki enemmän kuin on asiakkaita, niin vaikka olisi hyllytyksessä työvoimapulaa, silti kassat istuvat peukaloitaan pyörittäen. Kun PHP-FPM asetetaan moodiin dynamic, niin päätetään mikä on minimimäärä kassoja auki ja samalla lopuille kerrotaan, että yhtä monta kuin on kassapistettä, niin on niihin koulutettua henkilökuntaa. Kun tulee kiire, niin he lopettavat hyllyttämisen ja siirtyvät kassoille. Sen aikaa mitä vie saapua kuulutuksesta kassalle, jonot kasvavat, mutta sitten ne alkavat purkautumaan. Kun ruuhka on purettu, niin työntekijä sulkee kassan ja palaa takaisin muihin töihin.

Kassaesimerkki on siitä huono, että PHP-FPM:n suhteen kyse on kuin yhden tuotteen pikakassasta, jos on jo ehtinyt mieltää ostoskorin sisällön samaksi kuin haettava sivusisältö. Yksi PHP-FPM:n työntekijä tekee yhden työn ja jää sitten odottamaan eli siirtyy idle-tilaan. Kun ollaan oltu idle-tilassa riittävän kauan, niin riippuen käytettävästä moodista työntekijä joko sammuttaa itsensä tai sitten ei.

Jos kutsuja tulee enemmän kuin on tekijöitä, niin PHP-FPM sammuttaa vanhemmasta päästä ja aloittaa uuden. Se vie aikaa ja näkyy hidasteluna. Kun työntekijöitä on riittävästi ja osa on jopa valmiina tarttumaan töihin ilman, että täytyy käydä ensin kotoa hakemassa, niin työt valmistuvat nopeammin.

Mutta kääntäen, jokainen työntekijä varaa itselleen myös muistia. Jos max_children on liian suuri, niin muistia hukataan tyhjänpanttina seisoskeleviin työntekijöihin. Ei sekään tehokasta ole, varsinkin jos muistia tarvittaisiin muualla.

Siksi on hieman laskettava, että tiedetään mikä olisi mahdollisesti sopiva arvo ja sitä varten on hahmotettava paljonko muut toiminnot kaipaavat muistia.

Tarvittavan määrän laskeminen

Yksi työkalu on Monit kuorrutettuna omalla muistikapasiteetilla: nyt pitäisi muistaa kuinka paljon RAM:ia lupasi aikoinaan joillekin tehtäville.

Minulla on yhteensä 8 GB muistia dropletin käytössä. Asetuksissa olen luvannut

  • Varnishille 3 GB (käytössä 2 GB nyt)
  • Redisille 256 MB (käytössä vain 17 MB)

Monit kertoi, että muut toiminnot, ilman nyt jo pyörivää PHP-FPM:ää, haluavat puolisen gigaa

Nuo yhdessä ovat noin 4 gigaa. Pelivaraa jää vähimmillään toinen samanmoinen ilman Linuxin itsensä käyttämää. Tätä kirjoitettaessa PHP-FPM varaa 2,3 GB, joten tiedän jo nyt, että jos saan kävijäryntäyksen, niin muisti menee vähille. Mutta jos ryntäys kohdistuu yhdelle ja samalle sivulle, niin minulla ei ole cachen takia hätäpäivää. Sen sijaan jos nautin yleistä surffausta, niin PHP ja Varnish haukkaavat niin ison osan muistista, että sivustot ovat vaarassa kaatua. Tai jos eivät kaadu, niin alkavat hidastelemaan rajusti ja osa antaa satunnaisia error 5xx virheitä.

Saa tiedon vapaasta muistusta hieman nopeamminkin, tai ainakin suuntaa sille. Näet tällä olevan tilanteen:

free -hl

Sen mukaan minulla on yhteensä vapaana 5 gigaa, tällä hetkellä. Edeltävä lasku perustui siihen, että Varnishilta ja Redisiltä ei lopu muisti kesken, joten jos vähennän erotukset, niin samoihin menee.

Se, että tällä hetkellä 8 gigan muistista on kaikkiaan käytössä 4,7 gigaa ja serverin edellisestä buuttauksesta on aikaa vain pari päivää eli Varnishkin on vielä vajaakäytöllä, on selvä syy alkaa miettimään virtuaaliserverin upgreidausta.

Lasketaan paljonko PHP-FPM voi saada työväkeä.

ps -ylC php-fpm7.3 --sort:rss

Näytetään mitä on ajettu ja minkä kokoisia:

ps --no-headers -o "rss,cmd" -C php-fpm7.3 | awk '{ sum+=$1 } END { printf ("%d%s\n", sum/NR/1024,"M") }'
  • Tuo näyttää keskimäärin koon megoina. Minulla se vaihteli välillä 150 – 200 MB.

Nyt tiedetään paljonko suurinpiirtein yksi prosessi vie keskimäärin (minulla 155 MB)  ja paljonko niille voidaan tarjota RAM:ia (2,5 GB x 1024 = 2560 MB), joten yksinkertainen jakolasku kertoo mikä on max_children arvo:

2560 MB/150 MB = 16.5

Pyöristetään se lähimpään parilliseen lukuun ylöspäin, joten pm.max_children = 18.

Se, että pyöristetään ylöspäin lähimpään parilliseen lukuun johtunee vain siitä, että muita arvoja lasketaan tuosta jakamalla. Silloin saadaan aina kauniita tasalukuja.

Serverit

PHP-FPM:ssä joudutaan asettamaan kaksi servers arvoa. Aidosti ne eivät olet servereitä, vaan ainoastaan typerästi nimetty children, workerit, työntekijät tai miksi niitä sitten haluat kutsuakaan.

  • pm.start_servers on se määrä työntekijöitä (children), jotka laitetaan odottamaan töitä, kun PHP-FPM käynnistyy
  • pm.min_spare_servers on se määrä työntekijöitä, jotka ovat vähintään koko ajan varalla aloittamaan työnteon välittömästi. Se kannattaa laittaa samanksi kuin pm.start_servers.
  • pm.max_spare_servers on enimmillään sama kuin max_childrenmutta usein arvoksi laitetaan 4 x start_servers . Se on se määrä työtekijöitä, jotka jäävät pidemmäksi aikaa idle-tilaan odottamaan, ennenkuin niitä sammutetaan. Jos PHP-FPM on käynnistänyt työntekijöitä enemmän kuin mitä max_spare_servers on, niin ylittävä osuus sammutetaan heti, kun työt loppuvat.

pm.start_servers on se määrä, josta lähdetään liikkeelle. Ne ovat siis aina odottamassa sivustolle saapuvia. Jos arvon laittaa liian korkeaksi, niin muistia varataan enemmän ja pyyntöihin päästään vastaamaan välittömästi. Jos se on liian matala, niin muistia on enemmän vapaana hiljaisena aikana, mutta työntekijöiden käynnistäminen ensimmäisellä kerralla vie hieman aikaa,

Sille on olemassa parikin laskukaavaa.

  • min_spare_servers + (max_spare_servers - min_spare_servers) / 2 on yleisimmin tarjottu. Ihan kiva, jos tietää mitä minimi ja maksimi täytyisi olla, ja sitä ei koskaan kerrota missään vinkissä ja neuvossa. Aina käsketään laittamaan sopivat, mutta sopivuuden valitseminen voi olla melkoisen vaikeaa.
  • CPU-ytimien määrä x 4 on paljon helpompi, koska lähtötieto löytyy.

CPU-ytimien määrä ei ole automaattisesti sama kuin CPU:n määrä. Prosessorissa voi olla useampi ydin, kuten vaikka 4 tai 8, jopa enemmän. Se täytyy selvittää.

Saat tiedon virtuaalipalvelimesi tiedoista, tai sitten et, kuten DigitalOceanilla. Ilmeisesti kyseessä on tieto, joka pitäisi olla yleisesti tiedossa – mutta minulla ei ole.

Virtuaaliserverit ovat nimensä mukaisesti virtuaalisia, eli serveristä on jaettu resursseja kaikkien serverillä olevien kesken. Oikeastaan hieman samalla tavalla kuin webhotelleissa. Kun VPS:ää mainostetaan vaikka neljällä prosessorilla, niin aidosti ei ole ostanut käyttöä serveriltä, jolla on neljä todellista prosessoria, vaan vastaavan tehon. Toki on mahdollista ostaa serveripala, jossa on oikeasti prosessorivoimaa ja -määrää, mutta silloin ollaan hintaluokassa, jossa ihmettelen miksi olet täällä – sinulla on Testarossa ja luet Datsuninin viritysopasta.

Prosessorien ytimien määrä

Käytännössä VPS-asiakkailla ytimien määrä on sama kuin CPU:n määrä, mutta se kannattaa tarkistaa:

nproc --all

Komennolla lscpu saat enemmän tietoa prosessorikannastasi. Prosessorien lukumäärä on selvä, ja ytimien määrän per prosessori (eli socket) löydät kohdasta core.

Koska minulla on 4 CPU:ta, joka on sama kuin ytimien määrä, niin kertolaskun ytimet x 4 mukaan pm.start_servers = 16.

Vähintään ja enintään idle-tilassa

Muut servers -arvot ovat helpompia.

  • min_spare_servers on se määrä työntekijöitä, jotka PHP-FPM pitää aina varalla idle-tilanssa. Arvoksi vinkataan usein kaksi kertaa ytimien määrä eli minulla olisi pm.min_spare_servers = 8 tai sen voi asettaa 20 prosenttiin max.children arvosta, jolloin minulla se voisi olla 4, jos työntekijöitä on maksimissaan 18.
  • max_spare_servers on se määrä työntekijöitä, jotka jätetään pidemmäksi aikaa idle-tilaan odottamaan ennen sammuttamistaan. Se on usein sama kuin start_servers, joten minulla pm.max_spare_servers = 16

max_children on siis suurin määrä työntekijöitä, jotka PHP-FPM pystyy käskemään töihin. Jos niitä käynnistetään enemmän kuin max_spare_servers on, niin kiirepiikin jälkeen ylinmääräiset sammutetaan ja jäljelle jää sen verran työntekijöitä mitä max_spare_servers määrä. Kun nekin ovat olleet tarpeeksi kauan idle-tilassa tekemättä mitään, niin niitä aletaan sammuttamaan, kunnes hereillä on min_spare_servers mukainen määrä, jonka alle ei mennä.

En löytänyt mistään kuinka pitkä idle-aika on. Osa Googlen löytämistä osumista väittää kiven kovaan, että se on sama kuin process_idle_timeout, mutta PHP-FPM:n dokumenttien mukaan tuo ei pidä paikkaansa. Eikä se toiminut myöskään testeissä.

Käytännössä tilanne ei välttämättä ole edellä olevan laskukaavan mukainen ja PHP-FPM ei näyttäisi koskaan laskevan idle-tilassa olevien työntekijöiden määrää alle start_servers arvon aivan riippumatta paljonko on laitettu min_spare_servers arvoksi. Siksi start_servers ja min_spare_servers kannattaa ehkä olla samat ja määrä alin, joka kannattaa pitää käynnisssä, vaikka työntekijät eivät tekisi mitään.

Koska minulla sivut tulevat cachen kautta, niin kävijät eivät normaalisti puske aktiiviseksi kuin enintään kaksi työntekijää. Julkaisupiikit ovat noin sadalla samanaikaisella kävijällä käynnistäneet hetkeksi 6 prosessia. Siksi olen käyttänytdynamic ja ondemand-moodissa aika usein arvoa 2. Mutta koska jokainen kolkuttaja ja aukkojen etsijä käynnistelee prosesseja, niin olen käyttänyt myös arvoa 4. Tuo hieman riippuu fiiliksistä ja tilanteesta, koska ei ole olemassa mitään absoluuttista totuutta oikeiden tai edes sopivien arvojen suhteen.

pm.max_requests

max_requests on se määrä kuinka monta kertaa työntekijä saa olla töissä, jos sitä ei erikseen sammuteta. Kun laskuri tulee täyteen, niin prosessi lopetetaan ja aloitetaan uudestaan. Oletusarvo on 0 eli jos PHP-FPM ei erikseen vaadi, niin työntekijä pysyy käynnissä koko ajan eli näkyy vähintään idle-tilassa. Sitä ei ole pakko asettaa, mutta max_requests saattaa olla hyödyllinen, jos jokin PHP-scripti vuotaa muistia. Silloin RAM:n kulutus kasvaa hallitsemattomasti, kunnes loppuu kokonaan.

Minulla lähti kerran muistinkulutus karkuteille. Cron kehui vievänsä 3 gigaa muistia, kun sen pitäisi normaalisti tyytyä muutaman megaan. Kerran minuutissa cronin vievä muisti pyörähti siuuremmaksi ja samalla lisääntyi tietysti PHP-FPM:n muistinkäyttö. Aivan sama kuinka paljon nostin max_children määrää, niin hetken kuluttua logista löytyi ilmoitus niiden loppumisesta.

Joku cronin ajama PHP-scripti oli jäänyt kesken, sitä ei ehditty ajamaan tai se vuoti muistiin (memory leaking on termi, jonka ehkä haluat googlettaa). Joka kierroksella ajettiin samaa scriptiä itsensä päällä tai muistia ei onnistuttu vapauttamaan. Silloin en ollut asettanut max_requests arvoa, joten mikään ei rajoittanut yritysten määrää. Kun laitoin siihen hetkeksi matalan arvon, niin tilanne vakiintui. Muisti ei kylläkään vapautunut ilman serverin reboottia, mutta palvelin ei enää kaatunut RAM:n loppumiseen.

Saatat törmätä PHP-FPM:n logeissa tällaisiin ilmoituksiin:

[23-Apr-2020 00:52:33] NOTICE: [pool www] child 5801 exited with code 0 after 13188.332120 seconds from start

Se ei ole virhe, kuten NOTICE merkinnästä huomaa. Se tulee max_request asetuksen myötä. PHP-FPM kertoo kuinka monessa sekunnissa esimerkiksi 500 pyyntöä on tullut täyteen. Esimerkissä olisi mennyt hieman päälle 3,5 tuntia  Jos aika on huomattavan pitkä,  niin voi harkita arvon laskemista. Yleensä PHP-koodien roikottaminen pitkän aikaa on sarjaa huonot ideat, koska niillä on taipumusta mennä rikki ja se näkyy muistin kasvaneena kulutuksena. Tai sitä voi jopa nostaa, jos sivustolla on niin paljon liikennettä, että asetettu suorituskerta tulee täyteen huomattavan nopeasti ja pakottaa PHP-FPM:n käynnistämään childin uudestaan.

Tämä on taas niitä inhottavia tilanteita, jossa pitäisi kyetä päättämään koska paljon on paljon tai jopa liikaa tai peräti liian vähän. Kuitenkin, 500 on max_request kohdassa kommentoituna oletusarvona, ja tyypillisesti oletukset ovat matalia. Hieman riippuen päivästä, niin minulla tulee 500 täyteen noin neljässä tunnissa. Olen nostanut sitä hissukseen ylöspäin ja tällä hetkellä 800 pyörähtää kahdeksassa tunnissa. Tätä kirjoitettaessa arvo on 3600. Luultavasti pyrin etsimään ajan, jossa päästään 24 tuntiin – mutta sekään ei sinällään perustu mihinkään muuhun kuin logien lukemisen helppouteen, sillä ilmoitukset työntekijän sammuttamisesta ja uudelleenkäynnistämisestä täyttävät login äkkiä.

Mitä pidempi aika kestää max_request arvon täyttymiseen, niin sitä enemmän muistia varataan. Kun aika on lyhyempi, niin joka kerta kun child nollataan ja käynnistetään uudestaan, niin muistia vapautetaan hieman. Joten jos olet hilkuilla muistin kanssa, niin arvo kannattaa ehkä laittaa hivenen matalammaksi. Jos muistia riittää suuremman kuormituksenkin aikana, niin sen voi jättää suuremmaksi ja yrittää jollain tapaa miettiä kuinka kauan yhtä työntekijää kannattaa muutoin roikuttaa muistissa odottamassa kävijöitä.

Aidosti muistinkulutuksen säätäminen max_request arvolla on turhaa säätämistä millitason asioilla ja jos se on pakko tehdä, niin on aika päivittää suurempaan ja tehokkaampaan.

process_idle_timeout

Asetus toimii vain jos pm = ondemand, muissa moodeissa se ei vaikuta.

process_idle_timeout on aika, jonka jälkeen työntekijä lopettaa itse itsensä, jos mitään ei tapahdu eli vapauttaa käyttämänsä muistin. Oletus on 10 sekuntia.

10 sekuntia voi tuntua lyhyeltä ajalta, mutta ei se PHP-skriptien kohdalla ole. Niitä ajetaan nopeasti ja jos 10 sekunnin aikana yksikään käyttäjistä ei ole sitä tarvinnut, niin moista ei kannata pitää odottamassa. On parempi vapauttaa se mahdolliseen muuhun käyttöön.

slowlog

Joskus voi olla tarve selvittää mikä PHP-skripti hidastaa toimintaa. Sekin onnistuu PHP-FPM:llä huomattavan helposti. Laita www.conf asetuksiin:


slowlog = /var/log/php7.3-fpm-$pool.log.slow
request_slowlog_timeout = 3
request_slowlog_trace_depth = 20

Nyt logiin ilmestyvät kaikki ne PHP-skriptit, joiden suorittaminen kestää kauemmin kuin 3 sekuntia. Voit myös säätää kuinka syvälle koodista toiseen kaivaudutaan asetuksella trace_depth, mutta minä en osaa sitä hyödyntää – joten olkoot kaikkien esimerkkien tarjoamassa oletuksessa 20.

Minulla hitaita olivat esimerkiksi

  • WP Rocket, kun sen spider haki sisältöä curlilla cacheen – tuo saattoi johtua itse työstä tai oli seuraus siitä, että sivulla joku muu hidasteli
  • Embed Plus for YouTube
  • Query Monitor, ilmeisesti jonkun tietokanta-asian kanssa
  • Kallyas teeman dashboard
  • Kallyas teeman font manager
  • FAQ-rakenteen tekevän Basepressin hakutoiminto
  • Woocommercen action scheduler
  • EWWW:n kuvien optimointi, ja tulee ihan suoraan työstä
  • WordPressin site health
  • Dropbox Plus backup (älä käytä sitä, se on muutenkin ihan surkea, UpdraftPlus toimii paljon paremmin)
  • Amazon S2 and CloudFront Pro, joka hoitaa CDN:ää, ja aikaa kului ehkä paikallisen ja Amazonilla olevien tiedostojen synkkauksessa

Oli niitä muitakin, jotka eivät selvinneet kolmessa sekunnissa. Mutta logien perusteella voi hieman miettiä, että onko asia korjattavissa  esimerkiksi lisäosan vaihdolla tai kannattaako sille ylipäätään tehdä yhtään mitään.

Fiksattu log

Pakasta vedettynä PHP-FPM:n logi /var/log/php7.3-fpm.log antaa aika minimaalisesti tietoja. Logi kertoo pääsääntöisesti näin huimaavasti, jos mitään ihmeellistä ei ole tapahtunut:


[01-May-2020 13:53:04] NOTICE: Terminating ...
[01-May-2020 13:53:04] NOTICE: exiting, bye-bye!
[01-May-2020 13:53:04] NOTICE: fpm is running, pid 18765
[01-May-2020 13:53:04] NOTICE: ready to handle connections
[01-May-2020 13:53:04] NOTICE: systemd monitor interval set to 10000ms

Sen voi muuttaa enemmän webserverien access.log tyyppiseksi. Lisää nämä haluamaasi pooliin, esimerkiksi www.conf tiedostoon:


access.log = /var/log/php7.3-fpm-access.$pool.log
access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%"

  • %q lisää kyselytiedot (query string) HTTP-pyyntöön
  • %fnäyttää aidon suoritetun skriptin
  • %{mili}d on pyynnön suorittamiseen mennyt aika millisekunteina
  • %{kilo}M esittää muistin käytön huippuarvon kilotavuina, kun PHP on suorittanut scriptin
  • %C on CPU:n käyttämä kuorma.

Nyt logimerkintä muuttuu muotoon php7.3-php-access.www.log ja antaa enemmän informaatiota:


- - 02/May/2020:10:23:44 +0300 "POST /wp-admin/admin-ajax.php" 200 //var/www/html/wp-admin/admin-ajax.php 251.302 32768 95.50%
- - 02/May/2020:10:23:47 +0300 "POST /wp-admin/admin-ajax.php?_fs_blog_admin=true" 200 //var/www/eksis.one/public_html/wp-admin/admin-ajax.php 840.939 71680 98.70%
- - 02/May/2020:10:23:49 +0300 "HEAD /index.php" 200 //var/www/html/index.php 0.937 2048 0.00%
- - 02/May/2020:10:23:49 +0300 "HEAD /index.php" 200 //var/www/html/index.php 0.872 2048 1146.79%

Kun otetaan käyttöön poolin oma access.log, niin aiemmat ilmoitukset PHP-FPM:n käynnistymisestä siirtyvät php7.3-php-error.log tiedostoon.

On ihan omista tarpeista kiinni, että kannattaako logia säätää. Jos ei koskaan informaatiota tarvitse, niin turha moista vaivaa on nähdä. Toisaalta, kun asiat menevät pieleen, niin hankala logeja jälkeenpäin on rakentaa.

Kolme moodia

PHP-FPM tarjoaa kolme erilaista tapaa buustata PHP-scriptien suorittamista:

  • static: tekijöitä on vakiomäärä, jotka ovat heti saatavilla ja joita ei vapauteta, jos tarve loppuu; kiireisille sivustoille tai jos muistia on turhan panttina vapaana
  • dynamic: tekijöitä on odottamassa asetettu minimimäärä, mutta niitä voidaan lisätä tarpeen mukaan; välimalli, joka sopii useimmille
  • ondemand: resursseja otetaan käyttöön vain tarvittaessa ja kun tarve loppuu, niin ne vapautetaan; vähäiselle liikenteelle ja jos muistin vähyys tekee kiusaa

Niiden välillä on vain yksi ero: kuinka nopeasti kävijän pyyntöön voidaan vastata ja kuinka paljon muistia nopeus maksaa.

Uskon, että useimmat kaltaiseni käyttävät vain yhtä tapaa, dynaamista. Se johtuu siitä, että käytännössä kaikki ohjeet (joista 80 % on samoja kirjoitusvirheitä myöten; intialaispakistanilainen tapa tuottaa IT-sisältöä ja tehdä SEO-optimointia) selittävät aina dynamicin asetukset.

Toki dynamic on eräällä tavalla turvallinen välimuoto, mutta silti valtaosalle ondemand olisi ehkä järkevämpi. Mutta toki maailmassa on tuhottomasti katiska.info tyyppisiä sivustoja, joiden palvelimilla dynaaminen toiminta saattaa olla perusteltua kävijämäärien takia. Mistään ei koskaan löydy paljonko on paljon eli koska kyseessä on niin kiireinen sivusto, että staattinen ratkaisu olisi järkevä. Tai koska sivusto on niin pieni, että kannattaa mennä ondemandilla.

  • 2 GB muistilla, tai alle, ei ylipäätään kannata asentaa PHP-FPM:ää. Sille ei kuitenkaan jää muistia riittävästi ja kun se alkaa sitä vaatimaan itselleen, niin muut jäävät ilman. Muisti loppuu ja järjestelmä joko hidastuu tolkuttomasti tai kaatuu.

Itse uskon, että kyse ei ole niinkään kävijämääristä, vaan virtuaalipalvelimen kokoluokasta. Jos muistia on vähän, ehkä 4 – 8 GB, niin ondemand sallii muistia muuhunkin. Toisaalta, jos kävijöitä ei ole, niin mihin muistia ylipäätään tarvitsisi säästää. Kyse on kuitenkin siitä, että kun tulee kävijäpiikki, niin hidastetaanko sivuston toimintaa siksi, että PHP jää jalkoihin, vaikka ylimääräistä muistia olisi siirrettävissä.

Jos ollaan RAM:n suhteen kokoluokassa 8 – 16 GB, niin on varaa pitää hieman muistia varattuna vastaamassa heti kävijöille ja kun ryntäys kasvaa, niin sitten voidaan ottaa mahdollisesti vapaana olevasta muistista, resurssien rajoissa, dynaamisesti lisää työntekijöitä painimaan PHP-kutsujen kanssa.

32 GB ja enemmän menevät taatusti staattiselle puolelle. Silloin webserveri käsittelee niin isoja kävijämääriä, että kannattaa lohkaista saman tien reippaampi kakku huolehtimaan siitä, että hitaus ei ainakaan PHP:n käsittelystä serverin puolella johdu.

Moodi on ilmoitettava esimerkiksi www.conf tiedostossa pm = kohdassa, esimerkiksi pm = dynamic.

Jokainen moodi vaatii hieman erilaiset asetukset children ja servers kohtiin, eivätkä piittaa itseensä kuulumattomista kohdista, vaikka ne olisikin asetettu. Joten periaatteessa kaikki voidaan asettaa ja jättää jopa kommentoimatta, jolloin voi eräällä tavalla lennossa vaihtaa moodista toiseen vaihtamalla vain pm = arvo.

Dynamic

Dynamic vaatii hieman ynnäilyä, koska siinä asetetaan eniten asetuksia. Asetuksille on omat edellä selitetyt laskukaavansa, mutta silti sopivia arvoja joutuu hieman kokeilemaan. Tai ei pakko ole. Kyse on vain siitä millaista tasapainoa etsii käytössä olevan muistin ja PHP-FPM:n välille. Jos muistia riittää kaikille, eikä PHP-FPM valita lapsukaistensa kuolemasta, niin systeemin voi huoletta jättää silleen laittamillaan laskennallisilla oletusarvoilla tekemään töitään.

Virittäminen virittämisen takia menee harrastustoiminnan puolelle. Ei siinäkään mitään vikaa ole, mutta jos yrittää tehdä tuottavaa työtä, niin ollaan äkkiä vaarassa hukata työpäivä poikineen, kun yrittää säätää millisekuntiasioita. Toki uuden oppiminen vie aina aikaa.

dynamic vaatii käyttöön asetukset

  • pm.max_children on suurin mahdollinen työntekijämäärä, joka voidaan tarpeen mukaan käynnistää
  • pm_start_servers on se määrä työntekijöitä, joka otetaan käynnistyessä käyttöön, jos/kun PHP-FPM sammuu jossain vaiheessa
  • pm.min_spare_servers on vähimmäismäärä työntekijöitä, jotka pidetään aina muistissa, vaikka niillä ei olisikaan töitä (ovat idle-tilassa)
  • pm.max_spare_servers on maksimimäärä työntekijöitä, jotka voidaan tarpeen vaatiessa pitää kauemmin varalla idle-tilassa, ennen kuin niitä aletaan sammuttelemaan

Ondemand

Kauppavertauksessa on koko ajan auki vain yksi kassa. Jos tulee kiirettä, niin aukaistaan lisää kassoja siihen asti, että kaikki kassat ovat auki. Heti kun jono loppuu, niin kassa poistuu muualle töihin.

ondemand eräällä tavalla tasaa kävijäpiikkien synnyttämää painetta, mutta ei syö hiljaisena aikana muistia. Ajatus on kaunis, mutta sitä käytetään ymmärtääkseni usemmiten silloin kun ollaan vähällä muistikapasiteetilla, Jos vähästä muistista suurin osa onkin jo valmiiksi käytössä, niin ei ondemand miltään pelasta, koska ruuhkatilanteessa kuitenkin pyöräytetään max_children astuksen mukainen määrä työntekijöitä ja niille on oltava tilaa. Jos ei ole, niin muisti tulee täyteen ja aletaan swappaamaan, eikä kellään ole enää kivaa.

Jos max_childeren joudutaan laittamaan matalaksi, eikä se riitä kävijäpiikissä, niin nopeusetua ei saada, koska PHP-FPM joutuu sammuttamaan ja käynnistämään työntekijöitä koko ajan purkaakseen syntyvää jonoa.

Joten vaikka on käytössä ondemand, niin silti olisi oltava vapaana riittävästi muistia ja siltä osin olisi ihan sama varata se samantien käyttöön static-asetuksella. Asia tietysti hieman muuttuu. jos palvelimella tehdään aika ajoin jotain muuta muistia syövää. Oma näkemykseni on, että ondemand laitetaan matalilla arvoilla pienelle serverille ylläpitämään arkikäyttöä ja kun rasituspiikki tulee, niin se sitten kärvistellään hidastuneen serverin kanssa. ondemand on silloin vain minimiasetus, joka ylipäätään mahdollistaa PHP-FPM:n käytön.

ondemand vaatii kaksi asetusta:

  • pm.max_children on käynnistettävien työntekijöiden maksimimäärä
  • pm.process_idle_timeout joka on se toimeton aika, jonka jälkeen työntekijä sammutetaan

Static

Jos mietitään vertausta kaupan kassoihin, niin static on  nimensä mukaisesti staattinen, muuttumaton. Silloin henkilökuntaa on niin paljon, että on varaa pitää määrätty määrä kassoja auki. Jos on hiljaista, niin kassat odottavat, Mutta heti kun tulee enemmän asiakkaita, vaikka työmatkaliikennettä, niin jonoja ei pääse syntymään, koska jokainen asiakas voi mennä kassalle, jossa ei ole jonoa tai jonoa on mahdollisen vähän. Kun ruuhka purkautuu, niin kassat jäävät odottelemaan.

Ongelmia tulee silloin kun henkilökuntaa on kaikkiaan liian vähän (eli muistia ei ole riittävästi) ja silti lohkaistaan kassoille tarpeettoman paljon henkilökuntaa. Toinen ongelmakohta tulee siitä, että kun henkilökuntaa ei ole muutenkaan riittävästi, niin silti on kiinteä määrä kassoja. Kassoja on kuitenkin niin vähän, että ne eivät selviä ruuhkapiikeistä, vaan jonoja syntyy. Ja hiljaisena aikana ne puuhattomat kassat katselevat vierestä, kun muut yrittävät hiki hatussa selvitä kaupan muista töistä liian pienellä henkilökunnalla.

Suurin osa ohjeista ehdottaa käyttämään joka puolella aina dynamicia. Osa toteaa, että itseasiassa suurimmalle osalle sopisi ondemand ja kiireisille aina static, ja dynamic on aika turha välimuoto. Minä taasen olen sitä mieltä, että aivan kaikille tutantokäytössä oleville palvelimille kannattaa aina käyttää static moodia ja ainoa jota mietitään, on maksimiosuus muistista, joka voidaan PHP-FPM:lle lohkaista. Joka tapauksessa pyrkimys on, että sivusto ei hidastu PHP:n suorittamisen takia missään tilanteessa ja tuotantopalvelin täytyy mitoittaa sen mukaan, että muisti riittää niin PHP:lle kuin muillekin palveluille, cache mukaanlukien.

Jos virtuaalipalvelin on harrastus, niin silloin voi miettiä ondemandin ja dynamicin välillä.

Static vaatii yhden asetuksen:

  • pm.max_children joka on se määrä työntekijöitä, joka käynnistetään ja pidetään elossa, vaikka olisivatkin idle-tilassa

Yksilölliset asetukset

PHP-FPM:n asetusta, jossa säädetään mikä kolmesta moodista on käytössä, kutsutaan pooliksi. Yleinen, oletuksena oleva, on yleensä nimeltään www, mutta nimen voi toki muuttaa haluamakseen. Jos kyselijälle ei ole muuta kerrottu, niin käytetään sitä poolia, joka käyttää sama sock-tiedostoa, joka on määrätty virtual hostin conf-tiedostossa. Jos mitään ei ole asetettu, niin se virtual host tai muu webserverin kautta liikenteensä saava palvelu ei osaa PHP-FPM:ää käyttää.

Pooleja voi olla käytössä rajaton määrä sen mukaan mitä tarvitsee mihinkin. Isoilla tekijöillä on omat systeeminsä, mutta jos olet kuten minä pyörittäessäsi kourallista sivustoja yhdellä virtuaalipalvelimella, niin täytyy valita kahdesta vaihtoehdosta:

  • ovatko kaikki sivustot samoilla asetuksilla
  • tarvitaanko eri sivustoille erilaisia asetuksia

Kaikki keskittyy taas kerran yhteen ja vain yhteen asiaan: kuinka paljon on muistia vapaana.

Käyttämätön muisti on sinällään hyödytöntä. Sitä ei kannata säästää vain säästämisen takia. Mutta se, että sinulla on hiljaisena aikana muistia vapaana, ei kuitenkaan tarkoita sitä, että kävijäpiikin aikana RAM:ia löytyisi käyttämättömänä. Tuon asian selvittäminen vaatisi ymmärrystä miten ja milloin kävijät käyvät sivustoilla ja kuinka paljon kuormaa niin sivustot kuin itse palvelinkin kestää. Joten stressi- ja kuormitustestejä pitäisi tehdä.

Ollaan realistisia. Pienkäytäjä ei suuremmin testaa. Useimmiten ei ole tarvekaan, ennen kuin jokin hajoaa. Toki, jos sinulla on verkkokauppoja, niin taloudellisesti ottaen odottaa siihen asti, että kauppa hidastelee, antaa satunnaisia error 5xx ilmoituksia tai kaatuu kokonaan, ei ole ehkä järkevää. Mutta ei työajan tuhlaaminen liiketoimintaa kasvattamattomiinkaan tehtäviin ole kannattavaa ja serverin asetukset ovat tyypillisiä laskuttamattomia työntunteja. Kannattaa pitää mielessä kiehertämässä, että kaatuneen tai kiukuttelevat webkaupan korjaaminen vasta kalliita laskuttamattomia työtunteja on. Hieman on siis mietittävä ja ennakoitava.

Jos virtuaaliserverillä on muistia 8 gigaa, ja kävijämäärät ovat päivätasolla alle 5000 (hieman hatusta heitetty määrä), niin PHP-FPM:n syvällisempiä asetuksia ei kannata montaa tuntia pohtia ja testailla, vaan kannattaa panostaa nekin tunnit asentamalla Varnish sisältöpuolen palvelemiseen ja esimerkiksi Woocommercessa ottaa jokin sivutason cache-lisäosa käyttöön, kuten WP Rocket.

Itse kuitenkin otin kohtuullisen nopeastikin käyttöön useamman poolin. Rehellisyyden nimissä… siksi että pystyin. Ja aivan yhtä nopeasti luovuin per sivusto asetetuista pooleista, koska en saanut mitään silminnähtävää etua. Monimutkaisuus monimutkaisuuden takia ei sekään saa olla itseisarvo. Asiat saa ja täytyy pitää yksinkertaisena.

Poolien teko

Poolit löytyvät kaikki yhdestä hakemistosta conf-päätteisinä:

cd /etc/php/7.3/fpm/pool.d/

www.conf on oletuksena, ja nimen voi halutessaan vaihtaa.

Kun teet uuden poolin, niin helpoin tapa on tehdä ne lohkoina www.conf tiedostoon, mutta tallennetaan ensin originaali turvaan:

mv /etc/php/7.3/fpm/pool.d/www.conf /etc/php/7.3/fpm/pool.d/www.conf.original
nano /etc/php/7.3/fpm/pool.d/www.conf

Koska oma systeemi on vaatimattoman pieni, niin käytin vain kolmea:

  • [dynamic] – oletus, koska en muuttanut socketin nimeä, ja pm = dynamic
  • [static] ja pm = static
  • [ondemand] ja pm = ondemand

Voit tehdä poolit myös erillisinä tiedostoina pool.d hakemistoon. Nimeät ne vain haluamiksesi conf-päätteellä. Silloin saat helposti sammutettua poolin vain muuttamalla tiedostotunnistetta, vaikka lisäämällä loppuun .stop. Muista kuitenkin, että jos sammutat sellaisen, jossa on erikseen käynnistettävä socket ja joku virtual host käyttää samaa sock-tiedostoa, niin sen sivuston toiminta loppuu myös.

Jos sinulla on jokaisessa asetettuna sama socket, niin silloinhan voit sammutella ja käynnistellä miten haluat, kunhan vain yksi on kerrallaan käytössä. Kohtuullisen helppo työtapa, kun testaat kuormituksia.

Kun otat käyttöön yksilölliset poolit, niin jokaiseen tehdään erilaiset pm.-asetukset sekä jokaiselle täytyy tehdä myös oma sock-tiedosto.

Käytössäni ollut setup oli tämä:

Kun olet tehnyt muutokset ja säädöt, niin ensimmäiseksi tarkistetaan, että syntaksi on oikein:

php-fpm7.3 -t
  • Huomaa, että tässä käytetään prosessin nimeä eli versio laitetaankin loppuun.

Aina kun PHP-FPM:n asetuksia on muutettu, niin se on käynnistettävä uudestaan (PHP:n kohdallahan käynnistettäisiin Apache, Nginx kuulemma selviää ilmankin)

systemctl restart php7.3-fpm

Virtual hostit

Poolien teko ei vielä sinällään muuta serverin ja sivustojen käyttäytymistä. Jos asiat jätetään silleen, niin kaikki noudattavat www poolia – oikeammin sitä, jossa on sama socket kuin virtual hostin asetuksissa.

Jos sinulla on käytössä reverse proxy, niin muutat backendin virtual hostin asetuksia. Et sitä, joka on keulilla hoitamassa SSL-sertifikaatin ja ohjaamassa liikennettä Varnishille tai mitä sinulla sitten onkin reverse proxynä. Jos hoidat kaiken tuon pelkästään Nginxillä, niin en tiedä mitä kohtaa sinun tulisi muokata – yritä hahmottaa.

Jos serverin backend on Apache2

Avaa virtual hostin conf:

nano /etc/apache2/sites-available/example.tld.conf

Laita tämä virtual hostin <VirtualHost> osaan ja muuta socketin nimi oikeaksi:


<FilesMatch \.php$>
   SetHandler "proxy:unix:/var/run/php/php7.3-fpm.sock|fcgi://localhost/"
</FilesMatch>

Tehdään vielä pieni hienosäätö. Paitsi, että asetettaisiin pooli per sivusto, niin asetetaan niitä myös erikseen per sivuston backend ja frontend.

Sivustoilla on usein ns. frontend ja backend. Se mikä näkyy julkisena kävijöille ja joka mielletään sivustoksi. Lisäksi on hallinta, tausta, jossa tehdään ylläpito. WordPressissä se olisi wp-admin. Ne rasittavat eri tavalla sivustoa ja ehdottomasti suurin osa painolastista pitäisi tulla käyttäjien toimesta frontendin puolelle. Valitettavasti WordPresseissä tilanne on useinkin päinvastoin ja hidastukset sekä kuorma tulevatkin hallinnan puolelta. Lisäosat, page builderit ja varsinkin Jetpack ovat suurimpia syyllisiä siihen.

Ollaan hassussa tilanteessa. Jos serverillä on käytössä esimerkiksi Varnish, ja/tai WordPress käyttää sivutason välimuistia, kuten WP Rocketia, niin itseasiassa PHP-FPM:n hyödyn hakemista ja asetuksia ei kannattaisikaan panostaa frontendin palvelemiseen, vaan backendille. Tietenkään asia ei ole aivan näin yksinkertainen, mutta kun seurasin sivustojen vaikutusta PHP-FPM:ään, niin

  • Varnishin kanssa poolin koolla ei ollut mitään merkitystä ja kuorma tuli omista tekemisistäni backendeistä
  • ilman Varnishia pooli merkitsi hyvinkin paljon

Tuossa vaiheessa ei ollut paljoakaan liikennettä, noin 20 samanaikaista kävijää, joista noin 15 luki aina samaa sivua. Tilanne muuttuu varmasti kun jokainen kävijä selailee enemmän tai vähemmän eri sivuja, jolloin Varnish ei pääse töihin. Mutta, tuossakin tehojen suhteen saattaisi olla järkevämpää uhrata muistia Varnishille lämmittämällä cache kuin säätämällä useita eri pooleja tai varaamalla isompi kakku RAM:ia staattiselle poolille.

Joten taas kerran, ei ole olemassa oikeaa ja väärää tapaa, vaan kaikki pitäisi säätää tarpeen mukaan. Eikä tarvetta tiedä, jos ei seuraa ja tutki.

Käytän esimerkkinä WordPressiä, mutta sama idea toimii vastaavasti kaikilla, joilla backend on jonkun yhtenäisen osoitteen kautta.

Muutetaan hieman edellistä:

 
<FilesMatch \.php$> 
   SetHandler "proxy:unix:/var/run/php/php7.3-fpm-dynamic.sock|fcgi://localhost/" 
</FilesMatch> 

Tee edellä olevan lisäksi <Directory> osio:


<Directory /var/www/example.tld/public_html/wp-admin/>
   <FilesMatch \.php$>
      SetHandler "proxy:unix:/var/run/php/php7.3-fpm-ondemand.sock|fcgi://localhost/"
   </FilesMatch>
</Directory>

  • Yksinkertaistettuna: ensin kerrotaan koko sivustoa koskeva PHP-FPM:n socket ja sen jälkeen muutetaan se hakemiston mukaan toiseksi sopivammaksi

Socketien nimet täytyvät silloin olla samat kuin mitkä olet laittanut eri pooleihin. Esimerkissä frontend, eli tavalliset kävijät, saisi PHP-FPM:n työntekijämäärät dynamic-poolin mukaan ja kirjautuneet WordPressin hallinnan puolella menisivät poolin ondemand arvoilla.

Tarkistetaan vielä, että syntaksi on oikein:

apache2ctl -t

Käynnistetään Apache uudestaan:

systemctl restart apache2

Serverin backend on Nginx

Avaa virtual hostin conf:

nano /etc/nginx/sites-available/example.tld

Laita tämä virtual hostin server lohkoon ja muuta socketin nimi sopivaksi:

set $fpm_socket "unix:/run/php/php7.3-fpm.sock";

Jos halutaan Ngibnxille front- ja backendille erilaiset poolit samalla tavalla kuin mitä Apachen kohdalla neuvottiin, niin muutetaan hieman:


set $fpm_socket "unix:/run/php/php7.3-fpm-dynamic.sock"; 

if ($uri ~* "^/wp-admin/") { 
   set $fpm_socket "unix:/run/php/php7.3-fpm-ondemand.sock";
}

Tarkasta syntaksi kirjoitusvirheiden varalta ja lataa Nginx uudestaan:

nginx -t
systemctl reload nginx

Nginxin PHP-FPM-statussivu

Jos olet ottanut käyttöön PHP-FPM:n yksinkertaiseen seurantaan sen oman status-sivun, niin useammalla poolilla se hajoaa. Tai ei hajoa, mutta silloin näytetään tiedot vain yhden socketin kautta. Tuon voi kiertää ottamalla käyttöön useamman sivun per käytetty sock, mutta onhan se jo hieman purkkaviritys. Muutakaan tapaa ei ole.

Stackissä Nginx/Varnish/Apache2 Nginxillä näytettävä status-sivu tietenkin kertoo FPM-PHP:n tilan, vaikka socket asetetaankin vain Apachessa, koska status hakee tiedon socketin kautta. Itseasiassa tuossa tapauksessa status-sivu onkin haettava Nginxin kautta – tai ainakaan en koskaan saanut vastaavaa Apachen kautta näkyviin, koska keulilla oleva Nginx antoi sinnikkäästi error 404 ilmoituksen.

Miten lopuksi kävikään?

Pyöritin koko serveriä hetken aikaa tällä asetuksella:

 
pm = dynamic 
pm.max_children = 20 
pm.start_servers = 2 
pm.min_spare_servers = 2 
pm.max_spare_servers = 10 
pm.max_requests = 500 

Kaikki toimi sinällään ihan hienosti ja kuormituksen lisääntyessä käyntipiikkien myötä otettiin lisätyövoimaa käyttöön aktiivisten prosessien määrän noustessa hetkeksi ja palautessa melkoisen nopeasti takaisin yhteen. Idle-tilassa olevien määrä oli hetken kuluttua sama kuin max_spare_servers – tietenkin yhden alle, koska yksi prosessi on aina käytössä.

Muistin kulutus kuitenkin kasvoi koko ajan. Joten muistia ei kuitenkaan palautettu käyttöön, joten dynamic käyttäytyisi hetken kuluttua siltä osin aivan kuten static. Tai sitten olin ymmärtänyt jotain aivan väärin.

Tarkistin asian.

Kuormitustesti

Testasin muistin käyttöä komennolla

ps --no-headers -o "rss,cmd" -C php-fpm7.3 | awk '{ sum+=$1 } END { printf ("%d%s\n", sum/1024,"Mb") }'

Kokeilin sen ensin jokaisella moodilla PHP-FPM:n käynnistyksen jälkeen ja uudestaan kuormitustestin jälkeen. Kuormituksen tein Siegellä ja mittasin uudestaan muistin käytön:

siege -c50 -i -b -t1m -f urls.txt

Tein kolmannen mittauksen noin 15 minuutin paussin jälkeen.

Käytetyt moodit olivat:

Static


pm = static
pm.max_children = 20

Dynamic


pm = dynamic
pm.max_children = 20
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 10

Ondemand


pm = ondemand
pm.max_children = 20
pm.process_idle_timeout = 10s;

Tulokset

[table id=2 /]

Idlet eivät vapautuneet ondemandissa asetetun 10 sekunnin jälkeen. Tein muutaman kuormitustestin lisää dropletilla ja totesin, että PHP-FPM kasvatti itseään nopeasti ja kuorman loputtua myös palautti osan muistista takaisin. Silti toista gigaa jäi varatuksi. Haiskahti siltä, että dynamic ei toimikaan idlen suhteen ihan niin kuin lupaillaan, vaan kerran käyttöön otettu jää käyttöön. Tuo tarkoittaisi sitä, että jos on hieman vähänlaisesti muistia, niin joko on syytä kikkailla asetuksilla tai kylmän rauhallisesti käynnistää PHP-FPM cronilla aina uudestaan aamuyön hiljaisina tunteina.

Poiketaan hieman offtopikkiin.

Samassa systeemissä http-testaukset antavat huomattavan ylioptimistisia tuloksia nopeuksista, koska internetin viiveet jäävät pois, mutta myös kuormittavat enemmän. Numeroihin ei saisikaan suhtautua oikeina arvoina, vaan käyttää niitä ainoastaan mittarina kertomaan ovatko muutokset nopeuttaneet vai hidastaneet. Siksi kokeilin samoja testejä oman Windows 10-läppärin kautta käyttämällä Ubuntun appsia.

Käytin testaamiseen tätä:

ab -k -n 10000 -c 50 https://www.katiska.info/

Jos sinulla ei ole ApacheBenchiä, niin saat asennettua sen tällä:

apt install apache-utils

Ehkä simuloituja yhtäaikaisia käyttäjiä olisi saanut olla enemmän kuin 50, kuin myös 10000 pyyntöä, sillä se vastaisi 200 pyyntöä per käyttäjä, joka on aika normaali ensimmäistä kertaa sivulla käyvälle, jolla selaimen välimuisti ei tule mukaan.

Kuormaa alkoi rajoittamaan nettiyhteyden hitaus, jonka takia tulokset antavat hieman luotettavamman kuvan aidosta tilanteesta.

Hitaus toi mukanaan yhden elementin lisää. Servereitä ja työntekijöitä ei tarvittukaan enää yhtä montaa, koska nettiyhteyden lagaamisen takia ne ehtivät suoriutumaan töistään paremmin. Toki tuo on hieman vaarallinen tapa ajatella asiaa, mutta silti. Tipautin PHP-FPM:n children ja servers määrää sekä siirsin kaiken takaisin tyyppiin dynamic. Lopputuloksena muistin kulutus laski, koska ei varattu muistia peukaloitaan pyörittäville työntekijöille ja silti mikään tunnusluku ei heikentynyt.

Takaisin alkuperäiseen testiin.

Minulla meni sillä hetkellä Varnishille jonkun verrankin liian vähän muistia, koska olin joutunut sen muiden asetusten takia käynnistämään uudelleen. Päätin katsoa mitä dropletin 8 gigan muistille tapahtuu, jos Varnish on cachettanut reippaammin sisältöä. Lämmitin cachen käyttämällä wget komentoa sivustolle, jossa on päälle 2000 sivua. Varnishille varaamani 3 gigaa olikin aivan liian vähän, jonka takia osa sisällöstä tipahti cachesta. Parin kokeilun jälkeen selvisi, että maksimitilanteessa Varnish vaatii 5 gigaa muistia ja siinä vaiheessa 8 gigan RAM alkaa olla ahdas.

Jos asetin PHP-FPM:lle dynamicin ohjeellisten laskutapojen edellyttämät children– ja servers-määrät 18, niin minulle jäi muistia yli enää vain gigan verran ja se on pelivarana aika vähän.

Jätin Varnishille 5 gigaa, koska se on sivustojen toiminnalle merkityksellisempi kuin muut, PHP-FPM mukaanlukien. Seurailin hetken aikaa PHP-FPM:n käyttäytymistä ns. tyhjäkäyntitilanteessa, eli koko liikenne on orgaanista ja samanaikaisia käyttäjiä oli 20 – 30. Totesin, että voin olla piittaamatta laskuohjeista, palautin kaikki sivustot – niin front- kuin backenditkin, static-asetukselle ja laitoin työntekijöiden maksimiksi 18.

Mitä en tajunnut

Minua häiritse koko ajan selvä epäsuhta muistin kulutuksen kanssa, oikeammin miten se ilmoitetaan. Olin ottanut serverillä käyttöön pm = static ja pm.max_children = 18. Vuorokauden käynnissäolon jälkeen Vapun käyntiromahduksen aikana:

  • Monit kertoi: php7.3-fpm 3,1 GB, kun yhteensä on muistia käytössä 1,7 GB
  • ps_mem taasen kertoi prosessille php-fpm7.3 muistin kulutukseksi 732 MB

Ilmeisesti Monit kertoo paljonko muistia olisi käytössä, jos kaikki työtekijät olisivat aktiivisina – tai sitten se on jokin muu arvaus. Sen sijaan ps_mem kertoo aidon käytön, ja se nousi sekä laski, hieman, joka ajokerta. Jotain siis tapahtui.

Silti en ymmärtänyt miksi minulla eivät aiemmin testeissä workereiden määrät laskeneet takaisin min_spare_servers määriin, vaan ne pysyivät koko ajan vähintään puolessa tusinassa niin ondemand kuin dynamic asetuksillakin. Useamman tunnin googlettelu ei tuonut vastausta, joten aloin ihmettelemään PHP-FPM:n status-sivua, jota en sitäkään ymmärtänyt.

Enimmäinen, johon silmä tarttui, oli että ilmoitetut PHP-scriptit olivat koko ajan samoja. Kun jotain liikennettä syntyi, niin

  • WordPressin frontend näkyy pelkästään GET/POST kutsuina /index.php tiedostolle,
  • WordPressin hallinta on aina /wp-admin/admin-ajax.php riippumatta mitä tekee
  • Moodle antoi useamman scriptin, riippuen mitä tein
  • Woocommerce näkyi index.php kutsuina, mutta ?-määritykset joskus muuttuivat
  • Matomon käyntitilastointi näkyi aina omanaan

Olin aiemmin luullut, että tuo on jokin asetusvika ja siinä pitäisi näkyä kaikki PHP-skriptit, joita käsitellään. Tai sitten se tulee Varnishin välimuistin toiminnasta. Mutta ilmeisesti olen vain törmännyt siihen miten WordPress ja muut ylipäätään toimivat ja/tai miten PHP-FPM toimii. Tai miksi hitaita skriptejä logitettaessa laitetaan syvyys. Status näyttää vain sen eräällä tavalla lähtöskriptin, ei kaikkia ajettavia. Pointti on siinä, että se on normaalia.

Minulla kaikki Worpdressit tekevät eräällä tavalla paikallisen välimuistituksen WP Rocketilla. Se tekee sivukohtaisen cachen, joka leikkaa pyyntöjä aika railakkaastikin. Sen jälkeen Varnish tekee omia taikojaan leikaten vielä lisää pyyntömääriä. Molemmat yhdessä aiheuttavat sen, että dynaaminen sisältö muuttuukin staattiseksi. Tietysti poikkeuksen tekevät aidosti dynaaminen sisältö, kuten vaikka käyttäjätilastointi Matamolla tai pari widgettiä, mutta suurin osa ei tarvitse muuta näkyvää PHP-pyyntöä kuin index.php.

Testaillessani huomasin myös, että melkoisen nopealla aikataululla – joissain sekunneissa – jokainen synnyttämäni työntekijä hävisi, ja korvautui jollekin serverillä olevalle sivustolle, käytännössä aina WordPressejä, kohdistuvalle GET/HEAD pyynnölle juuritiedostoon index.php. Jopa silloin kun liikennettä ei pitänyt olla.

Aloin ymmärtämään miksi idle-workerit eivät koskaan poistu. Syynä oli se miksi olen uhrannut tunteja erilaisten bottiestojen tekemiseen, niin webserverillä kuin Varnishin puolella. En ollut tajunnut, että se mitä aiemmin pidin ylipäätään palvelimen hidastumisena ja kuormituksen kasvuna, kulki käsi kädessä

  • tolkuttoman määrän kirjautumisyritysten,
  • xmlrpc-kokeilujen,
  • haittallisten crawlereiden kulkemiseen sivulta toiselle tai
  • ylipäätään aukkojen koputtelun

aiheuttamalle kuormalle PHP-FPM:lle.

Kun olen sanonut, että tavallisen sivuston kuormasta tulee helposti 80 % haitallisesta liikenteestä, ja se täytyy tappaa ennenkuin ostaa isomman webhotellin tai virtuaaliserverin, niin olen yrittänyt sanoa, että haittaliikenne laittaa myös PHP-FPM:n polvilleen tai ainakin kyykkyyn tehdessään nopealla vauhdilla loputtomasti kutsuja toisensa perään eri PHP-scripteille.

Minulla ei bottiliikenteen takia missään vaiheessa idle-tilassa olevat työntekijät ehtineet olla tarpeeksi kauan toimettomina sammuakseen kokonaan. Jossain vaiheessa joku botti potki sen takaisin hereille.

Kauppavertauksessa tilanne olisi sama kuin että kassalla ei ole jonoa ja sieltä voisi siirtyä muihin töihim – mutta ei voi, koska juoksukaljaihmiset tai muutoin vaan hintoja kyselevät renkaanpotkijat pitävät työn puuhassa ilman, että siitä työstä olisi kaupalle mitään hyötyä.

Olen sanonut ennenkin ja sanon taas: koputtelijat ja SEO-spiderit eivät ole harmittomia, vaan ne varastavat resursseja, joista sinä maksat ja jotka olisi tarkoitettu aidoille ihmiskävijöille. Ihmisillä puhutaan matala-asteisen, sinällään näkymättömän, tulehduksen aiheuttamista terveysriskeistä. Kolkuttajat ja varsinkin SEO-palveluita myyvien yritysten crawlerit aiheuttavat eräänlainen matala-asteisen palvelunestohyökkäyksen (DDoS) jatkuvalla tulvallaan. Tai sitten pitäisi puhua pavelutasoa heikehtävästä hyökkäyksestä – ja sinä maksat laskun.

Lopuksi

Aloin penkomaan alunperin PHP-FPM:n asetuksia, kun jokin cronin ajama PHP-scripti oli rikki. Perusongelma oli, mahdollisesti vikaantuneen PHP-koodin lisäksi, siinä, että cron käynnisti aivan kaiken samalla sekunnilla. Kun porrastin niitä hieman, kuten että tyhjäkäynnillä olevien wordpressien töitä ei enää tehdäkään joka minuutti, vaan joka kolmas minuutti, niin päällekkäisiä scriptejä ei enää ole samaa määrää ja alemmatkin children arvot riittävät. Silti täydellinen backup kerran vuorokaudessa vaati enemmän resursseja kuin mikään kävijäpiikki. Mutta ei se pitkään koskaan kestä.

Voisin jopa väittää, että jos päivittäiset kävijämääräsi ovat tasolla maksimissaan muutama tuhat, niin ondemand arvoilla pm.max_children = 18 riittää kävijäpiikkeihin hyvin – tietysti oletuksella, että sinulla on RAM:ia varalla luokkaa 2 gigaa. Mutta

  • samoilla kävijämäärillä,
  • ja jos Varnish hoitaa reverse proxyn,
  • ja jos koko Varnishin takana oleva sisältö mahtuu hieman alle 5 gigaan,
  • ja jos sinulla on muistia 8 gigaa,

niin pm = static ja pm.max_children = 18 toimii ihan yhtä hyvin (aidosti se saisi olla korkeampikin mitä RAM:n kokonaismäärä sallisi, vaikka reserse proxy olisikin käytössä, ja tuo johtuu siitä, että linuxin joutuu kuitenkin reboottaamaan noin kahden viikon välein ytimen pävitysten takia, jolloin cachet nollaantuvat)

Kupletin juoni on siinä, että ilman Monitin (tai jonkun muun) tekemää seurantaa sekä virhelogien vilkaisua ihmettelisin edelleenkin sivuston satunnaista hidastelua, joka johtui aidosti kahdesta syystä: cronista ja hieman liian matalista PHP-FPM:n arvoista.

On toinenkin oppi: ole aina varovainen Googlen antamien lähteiden kanssa, koska ne useimmiten tarjoavat kaikki samaa ratkaisumallia selittämättä miksi, eikä mallia läheskään aina ole sinulle sopiva. Ja tapa botit, talossa sekä puutarhassa.