
Hello, Habr! We at Badoo are actively , as we have a fairly large system in this language and the issue of performance is a matter of saving money. More than ten years ago, we created PHP-FPM for this purpose, which initially was a set of patches for PHP and later became part of the official distribution.
In recent years, PHP has made significant strides: the garbage collector has improved, and stability has increased – today, you can write daemons and long-running scripts in PHP without much difficulty. This has allowed Spiral Scout to go further: unlike PHP-FPM, RoadRunner does not clean up memory between requests, providing an additional performance boost (although this approach complicates the development process). We are currently experimenting with this tool, but we have no results to share yet. To make the wait more enjoyable, we publish the translation of the RoadRunner announcement from Spiral Scout.
The approach from the article resonates with us: when solving our tasks, we also most often use the combination of PHP and Go, gaining benefits from both languages without rejecting one for the other.
Geniet!
Over the past ten years, we have created applications for companies on the , and for businesses with an audience of no more than 500 users. All this time, our engineers developed the backend primarily in PHP. But two years ago, something significantly impacted not only the performance of our products but also their scalability – we introduced Golang (Go) into our technology stack.
Almost immediately, we found that Go allows us to build larger applications with performance increases of up to 40 times. With it, we were able to expand existing products written in PHP, enhancing them by combining the strengths of both languages.
We will discuss how the combination of Go and PHP helps solve real development tasks and how it has become a tool for us capable of alleviating some of the issues related to .
Your daily PHP development environment
Before discussing how to revitalize the PHP “dying” model with Go, let’s take a look at your standard PHP development environment.
In de meeste gevallen start u de applicatie met een combinatie van de webserver nginx en de PHP-FPM-server. De eerste bedient statische bestanden en leidt specifieke aanvragen door naar PHP-FPM, dat de PHP-code uitvoert. Misschien gebruikt u een minder populaire combinatie van Apache en mod_php. Maar hoewel deze iets anders werkt, zijn de principes hetzelfde.
Laten we bekijken hoe PHP-FPM de code van de applicatie uitvoert. Wanneer een aanvraag binnenkomt, initialiseert PHP-FPM een dochter-PHP-proces en geeft de details van de aanvraag door als onderdeel van zijn status (_GET, _POST, _SERVER, enz.).
De status kan niet veranderen tijdens de uitvoering van het PHP-script, dus een nieuwe set invoergegevens kan op slechts één manier worden verkregen: door het geheugen van het proces te wissen en het 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 er een van hen 'sterft', wordt deze automatisch opnieuw gemaakt en beïnvloedt dit de andere processen niet. Maar deze aanpak heeft ook nadelen die zich voordoen bij het proberen te schalen van de applicatie.
Nadelen en inefficiëntie van de gebruikelijke PHP-omgeving
Als u professioneel met PHP ontwikkelt, weet u wat u moet doen om een nieuw project te starten: het kiezen van een framework. Dit bestaat uit bibliotheken voor afhankelijkheidsinjectie, ORM's, vertalingen en sjablonen. En natuurlijk kan alle binnenkomende gebruikersinvoer handig in één object worden geplaatst (Symfony/HttpFoundation of PSR-7). Frameworks zijn geweldig!
Maar alles heeft zijn prijs. In elk enterprise-level framework moet u voor het verwerken van een eenvoudige gebruikersaanvraag of databaseverzoek ten minste tientallen bestanden laden, talloze klassen maken en verschillende configuraties parseren. Maar het ergste is dat u na elke taak alles moet resetten en opnieuw moet beginnen: al uw net geïnitieerde code wordt nutteloos, want daarmee kunt u geen nieuwe aanvraag meer verwerken. Vertel dit aan elke programmeur die in een andere taal schrijft, en u zult de verbazing op zijn gezicht zien.
PHP-ingenieurs hebben jarenlang naar oplossingen voor dit probleem gezocht. Ze maakten gebruik van doordachte technieken zoals "lazy loading", microframeworks, geoptimaliseerde bibliotheken, caching, enzovoorts. Maar uiteindelijk moeten ze toch weer de hele applicatie opnieuw opstarten, keer op keer. (Opmerking van de vertaler: gedeeltelijk zal dit probleem opgelost zijn met de komst van PHP 7.4)
Kan PHP, met behulp van Go, meer dan één verzoek tegelijk verwerken?
Het is mogelijk om PHP-scripts te schrijven die langer dan enkele minuten meegaan (zelfs uren of dagen): bijvoorbeeld cron-taken, CSV-parsers en queue-processors. Ze werken allemaal volgens hetzelfde principe: een taak wordt opgehaald, uitgevoerd en vervolgens wordt op de volgende gewacht. De code blijft constant in het geheugen, wat kostbare milliseconden bespaart, aangezien het laden van het framework en de applicatie veel extra stappen vereist.
Maar het is niet eenvoudig om langdurige scripts te ontwikkelen. Elke fout kan het proces volledig beëindigen, het diagnosticeren van geheugenlekken kan je tot waanzin drijven, en het is niet meer mogelijk om de F5-debugmodus te gebruiken.
De situatie is verbeterd met de komst van PHP 7: er is een betrouwbare garbage collector gekomen, het verwerken van fouten is eenvoudiger geworden, en de kernel-extensies zijn nu beschermd tegen lekken. Ingenieurs moeten echter nog steeds voorzichtig omgaan met geheugen en zich bewust zijn van eventuele problemen met de staat in de code (is er een programmeertaal waarbij je hier geen aandacht aan hoeft te besteden?). Desondanks zijn er in PHP 7 minder verrassingen.
Kunnen we het model voor langdurige PHP-scripts toepassen en aanpassen voor meer triviale taken, zoals het verwerken van HTTP-verzoeken, en zo de noodzaak vermijden om bij elk verzoek alles opnieuw te laden?
Om deze taak op te lossen, moest er eerst een serverapplicatie worden geïmplementeerd die in staat is om HTTP-verzoeken te ontvangen en deze één voor één door te sturen naar de PHP-werknemer, zonder deze elke keer opnieuw te doden.
We wisten dat we een webserver konden schrijven in pure PHP (PHP-PM) of met behulp van een C-extensie (Swoole). En hoewel elk van de methoden zijn eigen voordelen heeft, voldeden beide opties niet aan onze verwachtingen — we wilden iets meer. We hadden niet alleen een webserver nodig — we verwachtten een oplossing die ons zou bevrijden van de problemen die gepaard gaan met de 'zware opstart' in PHP, en die bovendien gemakkelijk aanpasbaar en uitbreidbaar was voor specifieke applicaties. Wij hadden een applicatieserver nodig.
Kan Go hierbij helpen? We wisten dat het kon, omdat deze taal applicaties compileert naar enkele binaire bestanden; het is cross-platform; het gebruikt zijn eigen, zeer elegante concurrency-model en een bibliotheek voor HTTP-werking; en tenslotte hebben we toegang tot duizenden open-source bibliotheken en integraties.
De moeilijkheden van het combineren van twee programmeertalen
In de eerste plaats moest worden bepaald hoe twee of meer applicaties met elkaar zouden communiceren.
Bijvoorbeeld, met behulp van van Alex Palæstras konden geheugen delen tussen PHP- en Go-processen worden gerealiseerd (vergelijkbaar met mod_php in Apache). Maar deze bibliotheek heeft kenmerken die het gebruik voor onze taak beperken.
We besloten een andere, meer gangbare aanpak te gebruiken: het opbouwen van interactie tussen processen via sockets/pipes. Deze aanpak heeft zijn betrouwbaarheid in de afgelopen decennia bewezen en is goed geoptimaliseerd op het niveau van het besturingssysteem.
Als eerste hebben we een eenvoudige binaire protocol gecreëerd voor gegevensuitwisseling tussen processen en foutverwerking tijdens verzending. In zijn eenvoudigste vorm lijkt dit soort protocol op met (in ons geval 17 bytes), die informatie bevat over het type pakket, de grootte ervan en een binaire mask voor gegevensintegriteit.
Aan de PHP-kant gebruikten we , en aan de Go-kant de bibliotheek.
We vonden één protocol niet genoeg — en we voegden de mogelijkheid toe om. Dit hielp ons later enorm in de ontwikkeling, omdat we Go-bibliotheken gemakkelijk in PHP-applicaties konden integreren. Het resultaat van dit werk is bijvoorbeeld zichtbaar in een ander open-source product van ons,.
Taakverdeling onder meerdere PHP-werkers
Na de implementatie van het interactiemechanisme gingen we nadenken over de meest efficiënte manier om taken aan PHP-processen door te geven. Wanneer er een taak binnenkomt, moet de applicatieserver een beschikbare werker selecteren voor de uitvoering. Als een werker/proces zijn werk met een fout heeft beëindigd of 'is overleden', verwijderen we deze en creëren we een nieuwe ter vervanging. En als de werker/proces succesvol heeft gewerkt, sturen we deze terug naar de pool van beschikbare werkers voor taakuitvoering.

Voor het opslaan van de pool van actieve werkers hebben we gebruikgemaakt van, om onverwacht 'overleden' werkers uit de pool te verwijderen, hebben we een mechanisme voor fout- en statusbewaking toegevoegd.
Als resultaat hebben we een werkende PHP-server gekregen die in staat is om elke aanvraag in binaire vorm te verwerken.
Om onze applicatie als webserver te laten werken, moesten we een betrouwbare PHP-standaard kiezen voor de weergave van alle inkomende HTTP-aanvragen. In ons geval de net/http-aanvraag van Go naar het formaat, zodat deze compatibel is met de meeste beschikbare PHP-frameworks vandaag de dag.
Aangezien PSR-7 als onveranderlijk wordt beschouwd (sommigen zouden zeggen dat dit technisch gezien niet klopt), moeten ontwikkelaars applicaties schrijven die in principe niet omgaan met aanvragen als globale entiteit. Dit sluit prachtig aan bij het concept van langlevende PHP-processen. Onze uiteindelijke implementatie, die nog geen naam heeft gekregen, zag er als volgt uit:

Maak kennis met RoadRunner —
Onze eerste testtaak was een API-backend, waar sporadisch onvoorspelbare spikes in aanvragen ontstonden (veel vaker dan normaal). Hoewel de mogelijkheden van nginx in de meeste gevallen voldoende waren, kwamen we regelmatig de fout 502 tegen omdat we het systeem niet snel genoeg konden balanceren voor de verwachte toename van de belasting.
Ter vervanging van deze oplossing hebben we begin 2018 onze eerste PHP/Go-applicatieserver uitgerold. En we kregen onmiddellijk een ongelooflijk effect! We hebben niet alleen de fout 502 volledig kunnen elimineren, maar konden ook het aantal servers met twee derden verminderen, wat veel geld en hoofdpijnspillen voor ingenieurs en productmanagers heeft bespaard.
Halverwege het jaar hebben we onze oplossing verbeterd, deze op GitHub gepubliceerd onder de MIT-licentie en deze genoemd , waarmee we zijn ongelooflijke snelheid en efficiëntie benadrukken.
Hoe RoadRunner uw ontwikkelstack kan verbeteren
Toepassing stelde ons in staat om Middleware net/http aan de Go-kant te gebruiken om JWT-verificatie uit te voeren nog voordat de aanvraag in PHP aankomt, en om WebSockets te verwerken en global states in Prometheus te aggregeren.
Dankzij de ingebouwde RPC kunnen we API's van elke Go-bibliotheek voor PHP openen zonder wrapper-extensies te schrijven. Wat nog belangrijker is, met RoadRunner kunnen we nieuwe servers implementeren die verschillen van HTTP. Voorbeelden zijn het starten van PHP-handlers, het creëren van betrouwbare queue parsetools en zelfs het toevoegen aan onze applicaties.
Met de gemeenschappen van PHP en Go hebben we de stabiliteit van de oplossing verbeterd, in sommige tests de prestaties van applicaties met 40 keer verhoogd, debuggingtools verbeterd, integratie met het Symfony-framework gerealiseerd en ondersteuning toegevoegd voor HTTPS, HTTP/2, plugins en PSR-17.
Conclusie
Sommigen zijn nog steeds gevangen in de verouderde opvatting van PHP als een traag, log taal die alleen geschikt is voor het schrijven van plugins voor WordPress. Deze mensen kunnen zelfs zeggen dat PHP zo'n beperking heeft: wanneer een applicatie groot genoeg wordt, moet je kiezen voor een 'volwassenere' taal en de in de loop der jaren opgebouwde codebase herschrijven.
Daarop willen we antwoorden: denk nog eens na. Wij geloven dat alleen u zelf beperkingen aan PHP oplegt. U kunt uw leven besteden aan het overstappen van de ene naar de andere taal in een poging de perfecte combinatie met uw behoeften te vinden, of u kunt beginnen talen als gereedschappen te beschouwen. De vermeende tekortkomingen van een taal zoals PHP kunnen in werkelijkheid redenen zijn voor zijn succes. En als u het combineert met een andere taal zoals Go, kunt u veel krachtigere producten creëren dan wanneer u zich beperkt tot het gebruik van slechts één taal.
Na Samengewerkt te hebben met de combinatie van Go en PHP, kunnen we stellen dat we van hen houden. We zijn niet van plan om het een voor het ander op te geven - integendeel, we zullen manieren blijven zoeken om nog meer voordelen uit deze dubbele stack te halen.
UPD: we verwelkomen de maker van RoadRunner en coauteur van het originele artikel -
Bron: habr.com
