RoadRunner: PHP is niet gebouwd om te sterven, of Golang om te redden

RoadRunner: PHP is niet gebouwd om te sterven, of Golang om te redden

Hallo, Habr! Wij zijn actief bij Badoo werken aan PHP-prestaties, aangezien we een vrij groot systeem in deze taal hebben en de prestatiekwestie een kwestie is van geld besparen. Ruim tien jaar geleden hebben we hiervoor PHP-FPM gemaakt, wat eerst een set patches voor PHP was, en later onderdeel werd van de officiële distributie.

De afgelopen jaren heeft PHP grote vooruitgang geboekt: de garbage collector is verbeterd, het stabiliteitsniveau is toegenomen - tegenwoordig kun je zonder problemen daemons en langlevende scripts in PHP schrijven. Hierdoor kon Spiral Scout verder gaan: RoadRunner ruimt, in tegenstelling tot PHP-FPM, geen geheugen op tussen verzoeken, wat extra prestatievoordelen oplevert (hoewel deze aanpak het ontwikkelingsproces compliceert). We experimenteren momenteel met deze tool, maar we hebben nog geen resultaten om te delen. Om het wachten leuker te maken, We publiceren een vertaling van de RoadRunner-aankondiging van Spiral Scout.

De aanpak uit het artikel ligt dicht bij ons: bij het oplossen van onze problemen gebruiken we ook meestal een combinatie van PHP en Go, waarbij we de voordelen van beide talen benutten en de een niet opgeven ten gunste van de ander.

Geniet!

De afgelopen tien jaar hebben we applicaties voor bedrijven uit de lijst gemaakt Fortune 500en voor bedrijven met een publiek van niet meer dan 500 gebruikers. Al die tijd hebben onze engineers de backend voornamelijk in PHP ontwikkeld. Maar twee jaar geleden had iets niet alleen een grote impact op de prestaties van onze producten, maar ook op hun schaalbaarheid: we introduceerden Golang (Go) in onze technologiestack.

Vrijwel onmiddellijk ontdekten we dat we met Go grotere applicaties konden bouwen met tot 40x snellere prestaties. Hiermee konden we bestaande producten die in PHP waren geschreven uitbreiden en verbeteren door de voordelen van beide talen te combineren.

We zullen u vertellen hoe een combinatie van Go en PHP echte ontwikkelingsproblemen helpt oplossen en hoe het voor ons een hulpmiddel is geworden dat enkele van de problemen die gepaard gaan met PHP stervend model.

Uw dagelijkse PHP-ontwikkelomgeving

Voordat we het hebben over hoe je Go kunt gebruiken om het stervende PHP-model nieuw leven in te blazen, laten we eerst eens kijken naar je standaard PHP-ontwikkelomgeving.

In de meeste gevallen voer je de applicatie uit met een combinatie van een nginx-webserver en een PHP-FPM-server. De eerste serveert statische bestanden en stuurt specifieke verzoeken door naar PHP-FPM, en PHP-FPM voert zelf PHP-code uit. Misschien gebruik je een minder populaire combinatie van Apache en mod_php. Maar hoewel het iets anders werkt, zijn de principes hetzelfde.

Laten we eens kijken hoe PHP-FPM applicatiecode uitvoert. Wanneer een verzoek binnenkomt, initialiseert PHP-FPM het onderliggende PHP-proces en geeft de verzoekdetails door als onderdeel van de status ervan (_GET, _POST, _SERVER, enz.).

De status kan niet veranderen tijdens de uitvoering van een PHP-script, dus er is maar één manier om een ​​nieuwe set invoergegevens te verkrijgen: door het procesgeheugen leeg te maken en opnieuw te initialiseren.

Dit uitvoeringsmodel heeft veel voordelen. U hoeft zich niet veel zorgen te maken over het geheugengebruik, alle processen zijn volledig geïsoleerd en als een van hen sterft, wordt deze automatisch opnieuw aangemaakt zonder de rest van de processen te beïnvloeden. Maar deze aanpak heeft ook nadelen die optreden bij het schalen van de applicatie.

Nadelen en inefficiënties van een reguliere PHP-omgeving

Als je je bezighoudt met professionele ontwikkeling in PHP, dan weet je waar je een nieuw project moet beginnen - door een raamwerk te kiezen. Het bestaat uit bibliotheken voor afhankelijkheidsinjectie, ORM's, vertalingen en sjablonen. En natuurlijk kan alle gebruikersinvoer gemakkelijk in één object worden geplaatst (Symfony/HttpFoundation of PSR-7). Kaders zijn cool!

Maar alles heeft zijn prijs. In elk raamwerk op ondernemingsniveau moet je, om een ​​eenvoudig gebruikersverzoek te verwerken of toegang te krijgen tot een database, minstens tientallen bestanden laden, talloze klassen maken en verschillende configuraties parseren. Maar het ergste is dat je na het voltooien van elke taak alles moet resetten en opnieuw moet beginnen: alle code die je zojuist hebt geïnitieerd, wordt nutteloos, met zijn hulp zul je niet langer een ander verzoek verwerken. Vertel dit aan elke programmeur die in een andere taal schrijft, en je zult verbijstering op zijn gezicht zien.

PHP-ingenieurs hebben jarenlang gezocht naar manieren om dit probleem op te lossen, met behulp van slimme lazyload-technieken, microframeworks, geoptimaliseerde bibliotheken, caches, enz. Maar uiteindelijk moet je nog steeds de hele applicatie resetten en opnieuw en opnieuw beginnen. (Noot van de vertaler: dit probleem zal gedeeltelijk opgelost worden met de komst van preload in PHP 7.4)

Kan PHP met Go meer dan één verzoek overleven?

Het is mogelijk om PHP-scripts te schrijven die langer dan een paar minuten duren (tot uren of dagen): bijvoorbeeld cron-taken, CSV-parsers, wachtrijbusters. Ze werken allemaal volgens hetzelfde scenario: ze halen een taak op, voeren deze uit en wachten op de volgende. De code bevindt zich in het geheugen, waardoor kostbare milliseconden worden bespaard, omdat er veel extra stappen nodig zijn om het raamwerk en de applicatie te laden.

Maar het ontwikkelen van scripts met een lange levensduur is niet zo eenvoudig. Elke fout doodt het proces volledig, het diagnosticeren van geheugenlekken maakt je gek en je kunt F5-foutopsporing niet langer gebruiken.

De situatie is verbeterd met de release van PHP 7: er is een betrouwbare garbage collector verschenen, het is gemakkelijker geworden om fouten af ​​te handelen en kernelextensies zijn nu beschermd tegen lekken. Het is waar dat ingenieurs nog steeds voorzichtig moeten zijn met het geheugen en zich bewust moeten zijn van statusproblemen in de code (bestaat er een taal waarin we ons hier geen zorgen over hoeven te maken?). En toch staan ​​ons in PHP 7 minder verrassingen te wachten.

Is het mogelijk om het model van het werken met langlevende PHP-scripts over te nemen en dit aan te passen aan meer triviale taken zoals het verwerken van HTTP-verzoeken, en daarmee de noodzaak te elimineren om alles voor elk verzoek opnieuw te laden?

Om dit probleem op te lossen, moesten we eerst een serverapplicatie implementeren die HTTP-verzoeken kon accepteren en deze één voor één kon doorsturen naar de PHP-werker zonder deze elke keer te doden.

We wisten dat we een webserver in puur PHP (PHP-PM) of met de C-extensie (Swoole) konden schrijven. En hoewel elke methode zijn eigen voordelen heeft, pasten beide opties niet bij ons - we wilden iets meer. We hadden meer nodig dan alleen een webserver - we hoopten een oplossing te krijgen die ons zou kunnen redden van de problemen die gepaard gaan met de "harde start" in PHP, die tegelijkertijd gemakkelijk zou kunnen worden aangepast en uitgebreid voor specifieke toepassingen. Dat wil zeggen, we hadden een applicatieserver nodig.

Kan Go hierbij helpen? We wisten dat dit mogelijk was, omdat de taal applicaties in afzonderlijke binaire bestanden compileert; het is platformonafhankelijk; gebruikt zijn eigen, zeer elegante, parallelle verwerkingsmodel (concurrency) en bibliotheek voor het werken met HTTP; en ten slotte zullen duizenden open-sourcebibliotheken en integraties voor ons beschikbaar zijn.

Moeilijkheden bij het combineren van twee programmeertalen

De eerste stap was om te bepalen hoe twee of meer applicaties met elkaar zouden communiceren.

Gebruik bijvoorbeeld prachtige bibliotheek Alex Palaestras zou het delen van geheugen tussen PHP- en Go-processen kunnen implementeren (vergelijkbaar met mod_php in Apache). Maar deze bibliotheek heeft functies die het gebruik ervan voor het oplossen van ons probleem beperken.

We besloten een andere, meer gebruikelijke aanpak te gebruiken: interactie tussen processen opbouwen via sockets/pipelines. Deze aanpak heeft de afgelopen decennia zijn betrouwbaarheid bewezen en is goed geoptimaliseerd op besturingssysteemniveau.

Om te beginnen hebben we een eenvoudig binair protocol gemaakt voor het uitwisselen van gegevens tussen processen en het afhandelen van transmissiefouten. In zijn eenvoudigste vorm is dit type protocol vergelijkbaar met netstring с pakketheader met vaste grootte (in ons geval 17 bytes), dat informatie bevat over het type pakket, de grootte ervan en een binair masker om de gegevensintegriteit te controleren.

Aan de PHP-kant gebruikten we pak functieen aan de Go-kant - een bibliotheek codering/binair.

Het leek ons ​​dat één protocol niet genoeg was, dus hebben we de mogelijkheid toegevoegd om te bellen Ga naar services net/rpc rechtstreeks vanuit PHP. Dit heeft ons later veel geholpen bij de ontwikkeling, omdat we Go-bibliotheken gemakkelijk konden integreren in PHP-applicaties. Het resultaat van dit werk is bijvoorbeeld te zien in ons andere open source-product Gorge.

Taken verdelen over meerdere PHP-workers

Na de implementatie van het interactiemechanisme begonnen we na te denken over de manier waarop we taken het meest efficiënt naar PHP-processen konden overbrengen. Wanneer er een taak binnenkomt, moet de applicatieserver een vrije werknemer selecteren om deze te voltooien. Als een werker/proces eindigt met een fout of ‘sterft’, verwijderen we deze en creëren we een nieuwe om deze te vervangen. En als de werker/het proces met succes is voltooid, sturen we het terug naar de pool van medewerkers die beschikbaar zijn om taken uit te voeren.

RoadRunner: PHP is niet gebouwd om te sterven, of Golang om te redden

Om een ​​pool van actieve werknemers op te slaan die we gebruikten gebufferd kanaalOm onverwacht “dode” werknemers uit de pool te verwijderen, hebben we een mechanisme toegevoegd voor het opsporen van fouten en werknemersstatussen.

Als gevolg hiervan hebben we een werkende PHP-server ontvangen die alle verzoeken in binaire vorm kan verwerken.

Om onze applicatie als webserver te laten functioneren, moesten we een betrouwbare PHP-standaard kiezen om alle inkomende HTTP-verzoeken weer te geven. In ons geval gewoon transformeren net/http-verzoek van Ga naar formaat PSR-7zodat het compatibel is met de meeste PHP-frameworks die vandaag de dag beschikbaar zijn.

Omdat PSR-7 als onveranderlijk wordt beschouwd (sommigen zouden zeggen dat dit technisch gezien niet het geval is), moeten ontwikkelaars applicaties schrijven die het verzoek niet fundamenteel als een mondiale entiteit behandelen. Dit past mooi bij het concept van langlevende PHP-processen. Onze uiteindelijke implementatie, die nog geen naam had gekregen, zag er als volgt uit:

RoadRunner: PHP is niet gebouwd om te sterven, of Golang om te redden

Maak kennis met RoadRunner - krachtige PHP-applicatieserver

Onze eerste testtaak was de API-backend, die af en toe onverwachte verzoeken (veel vaker dan normaal) ondervond. Hoewel nginx in de meeste gevallen voldoende was, kwamen we regelmatig 502-fouten tegen omdat we het systeem niet snel genoeg in evenwicht konden brengen voor de verwachte toename van de belasting.

Ter vervanging van deze oplossing hebben we begin 2018 onze eerste PHP/Go-applicatieserver geïmplementeerd. En meteen kregen we een ongelooflijk effect! We zijn niet alleen volledig van de 502-fout afgekomen, maar we hebben ook het aantal servers met tweederde kunnen verminderen, wat veel geld en kopzorgen heeft bespaard voor engineers en productmanagers.

Halverwege het jaar hadden we onze oplossing geperfectioneerd, gepubliceerd op GitHub onder de MIT-licentie en gebeld RoadRunner, waardoor de ongelooflijke snelheid en efficiëntie worden benadrukt.

Hoe RoadRunner uw ontwikkelingsstapel kan verbeteren

Toepassing RoadRunner stelde ons in staat om Middleware net/http aan de Go-kant te gebruiken om JWT-verificatie uit te voeren voordat het verzoek zelfs maar PHP bereikt, en om WebSockets en globale statusaggregatie in Prometheus af te handelen.

Dankzij de ingebouwde RPC kun je de API van elke Go-bibliotheek voor PHP openen zonder extensie-wrappers te schrijven. Belangrijker nog is dat RoadRunner kan worden gebruikt om nieuwe niet-HTTP-servers te implementeren. Voorbeelden hiervan zijn het starten van handlers in PHP AWS Lambda, betrouwbare wachtrijbrekers creëren en zelfs toevoegen gRPC voor onze toepassingen.

Met de hulp van de PHP- en Go-gemeenschappen hebben we de stabiliteit van de oplossing vergroot, de applicatieprestaties in sommige tests tot wel 40 keer verhoogd, de foutopsporingstools verbeterd, integratie met het Symfony-framework geïmplementeerd en ondersteuning toegevoegd voor HTTPS, HTTP/ 2, plug-ins en PSR-17.

Conclusie

Sommige mensen zitten nog steeds gevangen in de verouderde opvatting dat PHP een langzame, omslachtige taal is die alleen goed is voor het schrijven van WordPress-plug-ins. Deze mensen zouden zelfs kunnen zeggen dat PHP een beperking heeft: wanneer de applicatie groot genoeg wordt, moet je een meer ‘volwassen’ taal kiezen en de codebasis herschrijven die zich in de loop der jaren heeft opgebouwd.

Op dit alles wil ik antwoorden: denk nog eens na. Wij zijn van mening dat alleen jij beperkingen voor PHP kunt instellen. Je kunt je hele leven van de ene taal naar de andere springen, in een poging de perfecte match voor je behoeften te vinden, of je kunt talen als hulpmiddelen gaan beschouwen. De waargenomen tekortkomingen van een taal als PHP kunnen feitelijk de redenen zijn voor het succes ervan. En als je het combineert met een andere taal zoals Go, kun je veel krachtigere producten maken dan wanneer je beperkt zou zijn tot slechts één taal.

Omdat we met een combinatie van Go en PHP hebben gewerkt, kunnen we zeggen dat we er dol op zijn. We zijn niet van plan het één op te offeren voor het ander, maar zoeken liever naar manieren om nog meer waarde uit deze dual stack te halen.

UPD: We verwelkomen de maker van RoadRunner en co-auteur van het originele artikel - Lachesis

Bron: www.habr.com

Voeg een reactie