RoadRunner: A PHP nem arra készült, hogy meghaljon, vagy a Golang segít

RoadRunner: A PHP nem arra készült, hogy meghaljon, vagy a Golang segít

Szia Habr! Aktívak vagyunk a Badoo-nál dolgozik a PHP teljesítményén, mivel ezen a nyelven meglehetősen nagy rendszerünk van, és a teljesítmény kérdése pénzmegtakarítási kérdés. Több mint tíz éve ehhez készítettük el a PHP-FPM-et, amely eleinte egy PHP-foltcsomag volt, majd később a hivatalos terjesztésbe került.

Az elmúlt években a PHP nagyot fejlődött: javult a szemétgyűjtő, nőtt a stabilitás szintje - ma már gond nélkül lehet démonokat és hosszú életű szkripteket írni PHP-ben. Ez lehetővé tette a Spiral Scout számára, hogy tovább menjen: a RoadRunner a PHP-FPM-mel ellentétben nem tisztítja meg a memóriát a kérések között, ami további teljesítménynövekedést eredményez (bár ez a megközelítés bonyolítja a fejlesztési folyamatot). Jelenleg kísérletezünk ezzel az eszközzel, de még nincs megosztandó eredményünk. Hogy szórakoztatóbb legyen a várakozásuk, közzétesszük a Spiral Scout RoadRunner közleményének fordítását.

A cikkből fakadó megközelítés közel áll hozzánk: problémáink megoldása során legtöbbször egy csomó PHP-t és Go-t is használunk, kihasználva mindkét nyelv előnyeit, és nem hagyjuk el az egyiket a másik javára.

Enjoy!

Az elmúlt tíz évben cégeknek készítettünk pályázatokat a listáról Fortune 500, valamint az 500 felhasználót nem meghaladó közönséggel rendelkező vállalkozások számára. Mérnökeink mindvégig főként PHP-ben fejlesztik a háttérrendszert. De két évvel ezelőtt valami nagy hatással volt nemcsak termékeink teljesítményére, hanem méretezhetőségére is – bevezettük a Golangot (Go) technológiai halmazunkba.

Szinte azonnal felfedeztük, hogy a Go lehetővé teszi számunkra, hogy nagyobb alkalmazásokat készítsünk akár 40-szeres teljesítményjavítással. Ezzel ki tudtuk bővíteni meglévő PHP termékeinket, javítva azokat a két nyelv előnyeinek kombinálásával.

Elmondjuk, hogyan segít a Go és a PHP kombinációja valódi fejlesztési problémák megoldásában, és hogyan vált számunkra olyan eszközzé, amely képes megszabadulni néhány, a PHP haldokló modell.

Az Ön napi PHP fejlesztői környezete

Mielőtt arról beszélnénk, hogyan használhatod a Go-t a PHP haldokló modelljének újraélesztésére, vessünk egy pillantást az alapértelmezett PHP fejlesztői környezetre.

A legtöbb esetben az alkalmazást az nginx webszerver és a PHP-FPM szerver kombinációjával futtatja. Az előbbi statikus fájlokat szolgál ki, és konkrét kéréseket irányít át a PHP-FPM-re, míg a PHP-FPM maga hajtja végre a PHP kódot. Lehet, hogy az Apache és a mod_php kevésbé népszerű kombinációját használja. De bár ez egy kicsit másképp működik, az elvek ugyanazok.

Nézzük meg, hogyan hajtja végre a PHP-FPM az alkalmazáskódot. Amikor egy kérés érkezik, a PHP-FPM inicializál egy PHP gyermekfolyamatot, és átadja a kérés részleteit az állapot részeként (_GET, _POST, _SERVER stb.).

Az állapot nem változhat a PHP szkript végrehajtása során, így egyetlen mód van új bemeneti adatok beszerzésére: a folyamatmemória törlésével és újrainicializálásával.

Ennek a végrehajtási modellnek számos előnye van. Nem kell nagyon aggódni a memóriafelhasználás miatt, minden folyamat teljesen el van szigetelve, és ha valamelyik „elhal”, akkor az automatikusan újrateremtődik, és a többi folyamatra nincs hatással. Ennek a megközelítésnek azonban vannak hátrányai is, amelyek az alkalmazás méretezésekor jelentkeznek.

A rendszeres PHP-környezet hátrányai és hatástalanságai

Ha Ön professzionális PHP fejlesztő, akkor tudja, hol kezdjen új projektet – a keretrendszer kiválasztásával. Ez függőségi injekciós könyvtárakból, ORM-ekből, fordításokból és sablonokból áll. És természetesen minden felhasználói bevitel kényelmesen elhelyezhető egy objektumban (Symfony/HttpFoundation vagy PSR-7). Jók a keretek!

De mindennek megvan az ára. Bármely vállalati szintű keretrendszerben egy egyszerű felhasználói kérés feldolgozásához vagy egy adatbázishoz való hozzáféréshez legalább több tucat fájlt kell betöltenie, számos osztályt kell létrehoznia, és több konfigurációt kell elemeznie. De a legrosszabb az, hogy minden egyes feladat elvégzése után mindent alaphelyzetbe kell állítania, és újra kell kezdenie: az imént elindított kódok haszontalanná válnak, segítségével többé nem dolgoz fel újabb kérést. Mondja el ezt bármely programozónak, aki más nyelven ír, és megdöbbenést fog látni az arcán.

A PHP mérnökei évek óta keresik a megoldást a probléma megoldására, okos, lusta betöltési technikákkal, mikrokeretrendszerekkel, optimalizált könyvtárakkal, gyorsítótárral stb. De a végén még mindig vissza kell állítani az egész alkalmazást, és újra és újra kezdeni. (A fordító megjegyzése: ez a probléma részben megoldódik a preload PHP 7.4-ben)

A PHP a Go-val több kérést is túlélhet?

Lehetőség van néhány percnél tovább (akár órákig vagy napokig) élő PHP szkriptek írására: például cron feladatok, CSV-elemzők, sormegszakítók. Mindegyik ugyanazon forgatókönyv szerint működik: lekérnek egy feladatot, végrehajtják, és várják a következőt. A kód folyamatosan a memóriában van, értékes ezredmásodperceket takarítva meg, mivel számos további lépésre van szükség a keretrendszer és az alkalmazás betöltéséhez.

De hosszú életű szkripteket fejleszteni nem könnyű. Bármilyen hiba teljesen leállítja a folyamatot, a memóriaszivárgás diagnosztizálása dühítő, és az F5 hibakeresés már nem lehetséges.

A PHP 7 megjelenésével javult a helyzet: megjelent egy megbízható szemétgyűjtő, könnyebbé vált a hibák kezelése, és a kernelbővítmények is szivárgásmentesek. Igaz, a mérnököknek továbbra is óvatosnak kell lenniük a memóriával, és tisztában kell lenniük a kód állapotbeli problémáival (van olyan nyelv, amely figyelmen kívül hagyhatja ezeket a dolgokat?). Ennek ellenére a PHP 7 kevesebb meglepetést tartogat számunkra.

Lehetséges-e a hosszú élettartamú PHP-szkriptekkel való munka modelljét átvenni, adaptálni olyan triviális feladatokhoz, mint a HTTP-kérések feldolgozása, és ezáltal megszabadulni attól, hogy minden kéréssel mindent a nulláról kell betölteni?

A probléma megoldásához először olyan szerveralkalmazást kellett megvalósítanunk, amely képes fogadni a HTTP kéréseket, és egyenként átirányítani azokat a PHP-munkáshoz anélkül, hogy minden alkalommal megölné.

Tudtuk, hogy tudunk webszervert írni tiszta PHP-ben (PHP-PM) vagy C kiterjesztéssel (Swoole). És bár minden módszernek megvannak a maga előnyei, mindkét lehetőség nem felelt meg nekünk - valami többet akartunk. Többre volt szükségünk, mint pusztán egy webszerverre – azt vártuk, hogy olyan megoldást kapjunk, amely megkímél minket a PHP „kemény beindulásával” járó problémáktól, amely ugyanakkor könnyen adaptálható és kiterjeszthető konkrét alkalmazásokra. Vagyis szükségünk volt egy alkalmazásszerverre.

Tud ebben segíteni a Go? Tudtuk, hogy lehetséges, mert a nyelv az alkalmazásokat egyetlen bináris fájlba fordítja; ez többplatformos; saját, nagyon elegáns, párhuzamos feldolgozási modellt (concurrency) és könyvtárat használ a HTTP-vel való munkához; és végül több ezer nyílt forráskódú könyvtár és integráció válik elérhetővé számunkra.

Két programozási nyelv kombinálásának nehézségei

Először is meg kellett határozni, hogy két vagy több alkalmazás hogyan kommunikál egymással.

Például a használatával kiváló könyvtár Alex Palaestras, lehetővé vált a memória megosztása a PHP és a Go folyamatok között (hasonlóan az Apache mod_php-hez). Ennek a könyvtárnak azonban vannak olyan funkciói, amelyek korlátozzák a használatát a probléma megoldására.

Úgy döntöttünk, hogy egy másik, gyakoribb megközelítést alkalmazunk: a folyamatok közötti interakciót foglalatokon/csővezetékeken keresztül építjük ki. Ez a megközelítés megbízhatónak bizonyult az elmúlt évtizedekben, és jól optimalizálták az operációs rendszer szintjén.

Kezdetben létrehoztunk egy egyszerű bináris protokollt a folyamatok közötti adatcserére és az átviteli hibák kezelésére. A legegyszerűbb formájában ez a protokolltípus hasonló a netstring с rögzített méretű csomagfejléc (esetünkben 17 bájt), amely információkat tartalmaz a csomag típusáról, méretéről és egy bináris maszkot az adatok integritásának ellenőrzésére.

A PHP oldalon használtuk csomag funkció, a Go oldalon pedig a könyvtár kódolás/bináris.

Úgy tűnt számunkra, hogy egy protokoll nem elég - és hozzáadtuk a hívás lehetőségét net/rpc go szolgáltatások közvetlenül a PHP-ből. Később ez nagy segítségünkre volt a fejlesztésben, hiszen könnyedén tudtuk PHP alkalmazásokba integrálni a Go könyvtárakat. Ennek a munkának az eredménye látható például a másik nyílt forráskódú termékünkben Gorridge.

Feladatok elosztása több PHP dolgozó között

Az interakciós mechanizmus megvalósítása után elkezdtünk gondolkodni azon, hogy miként lehetne a leghatékonyabban átvinni a feladatokat a PHP folyamatokba. Amikor egy feladat megérkezik, az alkalmazáskiszolgálónak szabad dolgozót kell választania a végrehajtásához. Ha egy dolgozó/folyamat hibával lép ki vagy "meghal", megszabadulunk tőle, és újat készítünk a helyére. És ha a dolgozó/folyamat sikeresen befejeződött, visszaküldjük a feladatok elvégzésére rendelkezésre álló dolgozók körébe.

RoadRunner: A PHP nem arra készült, hogy meghaljon, vagy a Golang segít

Az aktív dolgozók készletének tárolására használtuk pufferelt csatorna, hogy eltávolítsuk a váratlanul „halott” dolgozókat a készletből, hozzáadtunk egy mechanizmust a hibák és a dolgozók állapotának nyomon követésére.

Ennek eredményeként egy működő PHP szervert kaptunk, amely képes bármilyen bináris formában benyújtott kérést feldolgozni.

Ahhoz, hogy alkalmazásunk webszerverként működjön, megbízható PHP-szabványt kellett választanunk a bejövő HTTP kérések megjelenítésére. A mi esetünkben csak átalakítani net/http kérés a Ugrás a formátumhoz oldaltól PSR-7így kompatibilis a legtöbb ma elérhető PHP keretrendszerrel.

Mivel a PSR-7 változatlannak tekinthető (egyesek szerint technikailag nem az), a fejlesztőknek olyan alkalmazásokat kell írniuk, amelyek elvileg nem kezelik a kérést globális entitásként. Ez jól illeszkedik a hosszú élettartamú PHP folyamatok koncepciójához. A végleges megvalósításunk, amelyet még nem neveztünk el, így nézett ki:

RoadRunner: A PHP nem arra készült, hogy meghaljon, vagy a Golang segít

A RoadRunner bemutatása - nagy teljesítményű PHP alkalmazásszerver

Az első tesztfeladatunk egy API háttérprogram volt, amely időnként (a szokásosnál jóval gyakrabban) megjósolhatatlanul felrobban. Bár az nginx a legtöbb esetben elegendő volt, rendszeresen találkoztunk 502 hibával, mert nem tudtuk elég gyorsan kiegyensúlyozni a rendszert a várható terhelésnövekedéshez.

A megoldás leváltására 2018 elején telepítettük első PHP/Go alkalmazásszerverünket. És azonnal hihetetlen hatást ért el! Nemcsak az 502-es hibától sikerült teljesen megszabadulnunk, de a szerverek számát kétharmadára csökkentettük, így rengeteg pénzt és fejfájást spóroltunk meg a mérnököknek és termékmenedzsereknek.

Az év közepére javítottuk a megoldásunkat, közzétettük a GitHubon MIT licenc alatt, és elneveztük RoadRunner, ezzel is hangsúlyozva hihetetlen gyorsaságát és hatékonyságát.

Hogyan javíthatja a RoadRunner a fejlesztési veremét

Alkalmazás RoadRunner lehetővé tette számunkra a Middleware net/http használatát a Go oldalon a JWT ellenőrzés végrehajtására, mielőtt a kérés elérné a PHP-t, valamint globálisan kezelhetjük a WebSocketeket és az összesített állapotot a Prometheusban.

A beépített RPC-nek köszönhetően bármely Go-könyvtár API-ját megnyithatja PHP-hez anélkül, hogy kiterjesztés-burkolókat írna. Ennél is fontosabb, hogy a RoadRunner segítségével új, nem HTTP-kiszolgálókat telepíthet. Ilyen például a kezelők futtatása PHP-ben AWS Lambda, megbízható sormegszakítók létrehozása, sőt hozzáadás is gRPC alkalmazásainkhoz.

A PHP és a Go közösségek segítségével javítottuk a megoldás stabilitását, egyes teszteknél akár 40-szeresére növeltük az alkalmazások teljesítményét, továbbfejlesztettük a hibakereső eszközöket, integráltuk a Symfony keretrendszert, és hozzáadtuk a HTTPS, HTTP/2 támogatást, bővítmények és a PSR-17.

Következtetés

Vannak, akiket még mindig elkapott a PHP elavult fogalma, mint egy lassú, nehézkes nyelv, amely csak a WordPress bővítményeinek írásához alkalmas. Ezek az emberek akár azt is mondhatnák, hogy a PHP-nek van egy ilyen korlátja: ha az alkalmazás elég nagy lesz, akkor válasszon egy „érettebb” nyelvet, és írja át a hosszú évek alatt felhalmozott kódbázist.

Minderre azt akarom válaszolni: gondold át újra. Úgy gondoljuk, hogy csak Ön állíthat be korlátozásokat a PHP számára. Egész életében áttérhet egyik nyelvről a másikra, és megpróbálhatja megtalálni az igényeinek megfelelőt, vagy elkezdhet a nyelvekre mint eszközökre gondolni. Egy olyan nyelv feltételezett hibái, mint a PHP, valójában a siker okai lehetnek. És ha kombinálja egy másik nyelvvel, például a Go-val, akkor sokkal hatékonyabb termékeket hoz létre, mintha egyetlen nyelv használatára korlátozódna.

Egy csomó Go-val és PHP-vel dolgozva elmondhatjuk, hogy szeretjük őket. Nem tervezzük, hogy egyiket a másikért feláldozzuk – éppen ellenkezőleg, keresni fogjuk a módokat, hogy még több értéket nyerjünk ebből a kettős stackből.

UPD: köszöntjük a RoadRunner készítőjét és az eredeti cikk társszerzőjét - Lachesis

Forrás: will.com

Hozzászólás