RoadRunner: A PHP nem arra készült, hogy meghaljon, vagy a Golang siet a segítségére

RoadRunner: A PHP nem arra készült, hogy meghaljon, vagy a Golang siet a segítségére

Helló, Habr! Mi a Badoo-nál aktívan dolgozunk a PHP teljesítményén, mivel elég nagy rendszerünk van ezen a nyelven, és a teljesítmény kérdése pénzmegtakarítást jelent. Több mint tíz éve létrehoztuk a PHP-FPM-et, ami először PHP patch-eként indult, később pedig bekerült a hivatalos terjesztésbe.

Az elmúlt években a PHP jelentős előrelépésen ment keresztül: javult a szemétgyűjtő, növekedett a stabilitás szintje — ma már a PHP-n könnyedén lehet demon-stílusú és hosszú életű szkripteket írni. Ez lehetővé tette a Spiral Scout számára, hogy tovább lépjen: a RoadRunner, ellentétben a PHP-FPM-mel, nem üríti ki a memóriát a kérések között, ami extra teljesítménynövekedést ad (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 nincsenek olyan eredményeink, amelyeket megoszthatnánk. Hogy a várakozás szórakoztatóbb legyen, közzé tesszük a Spiral Scout RoadRunner bejelentésének fordítását.

A cikk megközelítése közel áll hozzánk: feladataink megoldásához mi is gyakran használjuk a PHP és a Go kombinációját, megszerezve mindkét nyelv előnyeit anélkül, hogy lemondanánk az egyikről a másik javára.

Jó szórakozást!

Az utolsó tíz évben alkalmazásokat fejlesztettünk mind a Fortune 500, mind az 500-nál kevesebb felhasználóval rendelkező vállalkozások számára. Ez idő alatt mérnökeink túlnyomórészt PHP-ra fejlesztették a backendet. De két évvel ezelőtt valami jelentősen befolyásolta nemcsak termékeink teljesítményét, hanem azok skálázhatóságát is — bevezettük a Golang (Go) nyelvet a technológiai stackünkbe.

Szinte azonnal észrevettük, hogy a Go lehetővé teszi számunkra, hogy nagyobb alkalmazásokat hozzunk létre, 40-szeres teljesítménynövekedést elérve. Ezzel képesek voltunk bővíteni a meglévő PHP-ra írt termékeket, javítva őket a két nyelv előnyeinek kombinációja révén.

Elmondjuk, hogyan segít a Go és a PHP kombinációja a valós fejlesztési problémák megoldásában, és hogyan vált számunkra olyan eszközzé, amely képes megszabadítani a PHP »megfejtési« modelljével kapcsolatos problémák egy részétől..

A mindennapi PHP-fejlesztési környezeted

Mielőtt arról beszélnénk, hogyan lehet a Go segítségével életre kelteni a PHP »megfejtési« modelljét, nézzük meg a szokásos PHP-fejlesztési környezetedet.

A legtöbb esetben az alkalmazást az nginx webszerver és a PHP-FPM szerver kombinációjával indítja. Az első statikus fájlokat szolgál ki, és a PHP-FPM-nek speciális kéréseket irányít, míg a PHP-FPM végrehajtja a PHP-kódot. Előfordulhat, hogy egy kevésbé népszerű Apache és mod_php kombinációt használ. De bár ez kicsit másképp működik, az elvek ugyanazok.

Nézzük meg, hogyan hajtja végre a PHP-FPM az alkalmazás kódját. Amikor egy kérés érkezik, a PHP-FPM inicializál egy gyermeket PHP-folyamatot, és a kérés részleteit a folyamat állapotaként adja át (_GET, _POST, _SERVER stb. mintákban).

Az állapot nem változhat meg a PHP-szkript végrehajtása során, ezért új bemeneti adatokat csak egyetlen módon lehet beszerezni: a folyamat memória törlésével és annak újbóli inicializálásával.

Ennek a végrehajtási modellnek számos előnye van. Nem kell nagyon aggódni a memóriafogyasztás miatt, minden folyamat teljesen izolált, és ha az egyik „meghal”, automatikusan újra létrehozásra kerül, és ez nem érinti a többi folyamatot. De ennek a megközelítésnek hátrányai is vannak, amelyek a számítási alkalmazás méretezésének megkísérlésekor jelentkeznek.

A hagyományos PHP-környezet hátrányai és hatékonysági problémái

Ha professzionálisan fejleszt PHP-t, tudja, hogy új projekt indításának alapvető lépése a keretrendszer kiválasztása. Ez olyan könyvtárakat tartalmaz, amelyek a függőségek bevezetésére, ORM-ekre, fordításokra és sablonokra szolgálnak. És természetesen minden felhasználói bemeneti adatokat egy objektumba lehet kényelmesen csomagolni (Symfony/HttpFoundation vagy PSR-7). A keretrendszerek nagyszerűek!

De mindennek van ára. Bármely vállalati szintű keretrendszerben ahhoz, hogy egyszerű felhasználói kéréseket vagy adatbázis-hívásokat kezeljen, legalább tucatnyi fájlt kell betölteni, számos osztályt létrehozni, és több konfigurációt elemezni. De a legrosszabb, hogy minden egyes feladat végrehajtása után mindent újra kell kezdeni: az ön által éppen kezdeményezett kód haszontalanná válik, mivel azzal már nem tud újabb kérést kezelni. Ezt mondja el bármely programozónak, aki más nyelveken ír — és meg fogja látni az arcán a zavart.

A PHP-fejlesztők évek óta keresik a megoldást erre a problémára, kidolgozott "lustán" betöltési módszereket, mikrofframeworköket, optimalizált könyvtárakat, cache-t stb. De végül is mindig le kell állítani az egész alkalmazást, és újrakezdeni, ismételten. (A fordító megjegyzése: ez a probléma részben megoldódik a preload PHP 7.4) megjelenésével.

Túlélheti-e a PHP a Go segítségével több mint egy kérést?

Írhatunk PHP szkripteket, amelyek több percnél hosszabb ideig fennmaradhatnak (órákig vagy napokig): például cron-feladatok, CSV-elemzők, sorfeldolgozók. Mindegyik egy forgatókönyv szerint működik: felveszi a feladatot, végrehajtja, várja a következőt. A kód folyamatosan a memóriában van, ezzel értékes milliszekundumokat takarítva meg, mivel a keretrendszer és az alkalmazás betöltéséhez számos extra lépést kell végrehajtani.

De a hosszú élettartamú szkriptek fejlesztése nem olyan egyszerű. Bármilyen hiba teljesen megölheti a folyamatot, a memória szivárgások diagnosztizálása őrületesen nehéz, és már nem lehet az F5-öt használni a hibakereséshez.

A helyzet javult a PHP 7 megjelenésével: megbízható szemétgyűjtő került bevezetésre, könnyebb lett a hibák kezelése, és a magtárolók kibővítése most már védve van a szivárgásoktól. Igaz, hogy a fejlesztőknek még mindig óvatosan kell bánniuk a memóriával, és figyelembe kell venniük a kód állapotának problémáit (van olyan nyelv, amelynél ezeket figyelmen kívül lehet hagyni?). Mégis, a PHP 7-ben kevesebb meglepetés vár ránk.

Lehet-e a hosszú életű PHP-szkriptek működési modelljét alkalmazni a triviálisabb feladatokra, például a HTTP-kérések feldolgozására, és ezzel megszabadulni attól a szükségtől, hogy minden kérésnél minden újra kellene betölteni?

E feladat megoldásához először egy szerveralkalmazást kellett megvalósítani, amely képes HTTP-kéréseket fogadni, és azokat egymás után átirányítani a PHP-munkavállalónak anélkül, hogy minden alkalommal megölni kellene.

Tudtuk, hogy webkiszolgálót tudunk írni tiszta PHP-val (PHP-PM) vagy C-bővítménnyel (Swoole). Bár mindkét módszernek megvannak a maga előnyei, egyik sem elégítette ki az igényeinket — valami többre vágytunk. Nem csupán webkiszolgálóra volt szükségünk — olyan megoldást vártunk, amely segítene a PHP „nehezen indul” problémáival megbirkózni, és amelyet könnyen alkalmazhatunk és bővíthetünk konkrét alkalmazásokhoz. Tehát nekünk alkalmazásszerverre volt szükségünk.

Segíthet-e a Go ebben? Tudtuk, hogy igen, mert ez a nyelv egyesíti az alkalmazásokat egyetlen bináris fájlba; platformfüggetlen; saját, nagyon elegáns párhuzamos feldolgozási (concurrency) modellt és HTTP könyvtárat használ; végül pedig hozzáférhetünk ezrek open-source könyvtárhoz és integrációhoz.

A két programozási nyelv egyesítése során felmerülő nehézségek

Első lépésként meg kellett határoznunk, hogyan fognak kommunikálni egymással két vagy több alkalmazás.

Például az csodálatos könyvtár segítségével Alex Palasétrasz könyvtára lehetővé tette a PHP és a Go közötti memória megosztást (hasonlóan a mod_php-hoz az Apache-ban). Azonban ez a könyvtár bizonyos sajátosságokkal rendelkezik, amelyek korlátozzák a használatát a feladatunk megoldására.

Egy másik, elterjedtebb megközelítést választottunk: a folyamatok közötti interakciót socketekkel/csövekkel építjük fel. Ez a megközelítés az utóbbi évtizedekben bizonyítottan megbízható volt, és jól optimalizálták az operációs rendszerek szintjén.

Először is létrehoztunk egy egyszerű bináris protokollt az adatok folyamatok közötti cseréjére és a küldési hibák kezelésére. A legegyszerűbb formájában az ilyen típusú protokoll hasonlít a netstring a fix méretű csomagfejlécre (a mi esetünkben 17 byte), amely tartalmazza a csomag típusáról, a méretéről és a bináris maszkról szóló információt az adatok integritásának ellenőrzésére.

A PHP oldalon a packfüggvényt használtuk, a Go oldalon pedig az encoding/binary.

protokoll nem bizonyult elegendőnek — és hozzáadtuk a lehetőséget, hogy közvetlenül PHP-ból hívjunk Go-szolgáltatásokat net/rpc segítségével. Később ez nagymértékben segített minket a fejlesztés során, mivel könnyen integrálhattuk a Go könyvtárakat a PHP alkalmazásokba. E munkánk eredményét például egy másik open-source termékünkben, a Goridge-ben láthatjuk..

Feladatok elosztása több PHP-munkás között

A kommunikációs mechanizmus implementálása után azon kezdtünk gondolkodni, hogy miként tudnánk hatékonyabban átadni a feladatokat a PHP-folyamatoknak. Amikor egy feladat érkezik, az alkalmazásszervernek ki kell választania egy szabad munkást annak végrehajtására. Ha egy munkás/folyamat hibával zárult le vagy "meghalt", akkor eltávolítjuk, és helyette újat hozunk létre. Ha a munkás/folyamat sikeresen befejezte, akkor visszahelyezzük azt a szabad munkások közé, akik feladatok végrehajtására rendelkezésre állnak.

RoadRunner: A PHP nem arra készült, hogy meghaljon, vagy a Golang siet a segítségére

Az aktív munkások medencéjének tárolására a következőt használtuk pufferelt csatorna, a váratlanul "meghalt" munkások eltávolítására hibakövetési és állapotfigyelő mechanizmust adtunk hozzá.

Ennek eredményeként egy működő PHP-szervert kaptunk, amely képes bármilyen bináris formátumú kérés feldolgozására.

Ahhoz, hogy az alkalmazásunk webkiszolgálóként működjön, egy megbízható PHP-szabványt kellett választanunk bármilyen bejövő HTTP-kérés megjelenítésére. Esetünkben egyszerűen átalakítjuk a Go-beli net/http kérését PSR-7formátumba, hogy az kompatibilis legyen a ma elérhető legtöbb PHP-keretrendszerrel.

Mivel a PSR-7 változatlannak számít (valaki azt mondaná, hogy technikailag nem így van), a fejlesztőknek olyan alkalmazásokat kell írniuk, amelyek alapvetően nem kezelik a kérést globális entitásként. Ez remekül illeszkedik a hosszú életű PHP-folyamatok koncepciójához. Az utolsó megvalósításunk, amely még nem kapott nevet, így nézett ki:

RoadRunner: A PHP nem arra készült, hogy meghaljon, vagy a Golang siet a segítségére

Bemutatjuk a RoadRunner-t — nagy teljesítményű PHP-alkalmazás-kiszolgáló

Első tesztfeladatunk egy API-háttér volt, amelyen időnként kiszámíthatatlanul felmerültek a kéréskiugrások (sokkal gyakrabban, mint általában). Bár az nginx többségében elegendő volt, rendszeresen szembesültünk 502-es hibával, mert nem tudtuk elég gyorsan kiegyensúlyozni a rendszert a várható terhelésnövekedéshez.

Ennek a megoldásnak a helyettesítésére 2018 elején telepítettük az első PHP/Go alkalmazásszerverünket. Azonnal elképesztő hatást tapasztaltunk! Nemcsak hogy teljesen megszabadultunk a 502-es hibától, hanem a szerverek számát is egyharmadával csökkentettük, rengeteg pénzt és fájdalomcsillapítót spórolva meg a mérnökök és termékmenedzserek számára.

Év közepére továbbfejlesztettük megoldásunkat, közzétettük a GitHub-on MIT licenccel és elneveztük RoadRunner, ezzel hangsúlyozva hihetetlen sebességét és hatékonyságát.

Hogyan javíthatja a RoadRunner a fejlesztési stack-et

Alkalmazás RoadRunner lehetővé tette számunkra, hogy a Middleware net/http-t használjuk a Go oldalán, hogy a JWT-ellenőrzést még azelőtt elvégezzük, hogy a kérés PHP-ba érkezik, valamint WebSocketeket kezeljünk és globálisan aggregáljuk az állapotokat a Prometheusban.

A beépített RPC lehetővé teszi, hogy bármely Go könyvtár API-ját PHP számára elérhetővé tegyük anélkül, hogy wrapper-extensiókat kellene írni. Ami még fontosabb, a RoadRunner segítségével új, HTTP-tól eltérő szervereket telepíthetünk. Példaként említhetjük a PHP feldolgozók elindítását AWS Lambda, megbízható üzenetközvetítő elemzők létrehozását, és még gRPC alkalmazásainkhoz való hozzáadását.

A PHP és Go közösségekkel együttműködve növeltük a megoldás stabilitását, bizonyos tesztek során a teljesítményt akár 40-szer is fokozva, továbbfejlesztettük a hibakeresési eszközöket, integráltuk a Symfony keretrendszert, és támogatást adtunk a HTTPS, HTTP/2, pluginek és PSR-17 számára.

Zárás

Néhányan még mindig a PHP-t egy lassú, nehézkes nyelvként látják, amely csak a WordPress pluginok írására alkalmas. Ezek az emberek akár azt is mondhatják, hogy a PHP-nak van egy ilyen korlátja: amikor egy alkalmazás elég naggyá válik, 'érettebb' nyelvet kell választani, és újra kell írni az évek során felhalmozódott kódalapot.

Mindezekre azt szeretnénk mondani: gondolják át újra. Úgy érezzük, hogy csakis ti állítotok korlátokat a PHP számára. Az egész életedet eltöltheted azzal, hogy egyik nyelvről a másikra váltogatsz, próbálva megtalálni a tökéletes kombinációt az igényeiddel, vagy elkezdheted a nyelveket eszközként kezelni. A PHP-hoz hasonló nyelv látszólagos hátrányai valójában a sikerének okai is lehetnek. Ha pedig egy másik nyelvvel, például a Goval kombinálod, olyan sokkal erősebb termékeket hozhatsz létre, mint ha csak egyetlen nyelvet használnál.

A Go és PHP kombinációjával dolgozva azt mondhatjuk, hogy beleszerettünk. Nem tervezzük, hogy feláldozzuk az egyiket a másikért — ellenkezőleg, keresni fogunk módokat, hogy még több hasznot húzzunk ebből a párhuzamos stackből.

UPD: üdvözöljük a RoadRunner alkotóját és az eredeti cikk társszerzőjét — Lachezis

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster