RoadRunner: PHP er ikke skabt til at dø, eller Golang skynder sig til undsætning

RoadRunner: PHP er ikke skabt til at dø, eller Golang skynder sig til undsætning

Hej, Habr! Vi i Badoo arbejder aktivt på PHP-ydeevnen, da vi har et ret stort system på dette sprog, og spørgsmålet om ydeevne er et spørgsmål om penge. For over ti år siden skabte vi PHP-FPM, som i starten var et sæt patches til PHP, men senere blev en del af den officielle distribution.

I de seneste år har PHP gjort store fremskridt: affaldsindsamleren er blevet forbedret, stabiliteten er øget - i dag kan man skrive dæmoner og langvarige scripts på PHP uden særlige problemer. Dette har givet Spiral Scout mulighed for at gå videre: RoadRunner, i modsætning til PHP-FPM, rydder ikke hukommelsen mellem anmodninger, hvilket giver en ekstra ydeevnegevinst (selvom denne tilgang komplicerer udviklingsprocessen). Vi eksperimenterer i øjeblikket med dette værktøj, men vi har stadig ikke resultater at dele. For at gøre ventetiden sjovere, offentliggør vi oversættelsen af RoadRunner-annoncen fra Spiral Scout.

Tilgangen i artiklen er nær vores: når vi løser vores opgaver, bruger vi også oftest kombinationen af PHP og Go, hvilket giver os fordele fra begge sprog uden at give afkald på det ene til fordel for det andet.

God fornøjelse!

I de seneste ti år har vi skabt applikationer til både virksomheder på listen Fortune 500, og til virksomheder med en brugergruppe på højst 500. I hele denne tid har vores ingeniører primært udviklet backend i PHP. Men for to år siden påvirkede noget stærkt ikke kun ydeevnen af vores produkter, men også deres skalérbarhed - vi introducerede Golang (Go) i vores teknologistak.

Vi opdagede straks, at Go giver os mulighed for at skabe større applikationer med en ydeevneforøgelse på op til 40 gange. Med det kunne vi udvide eksisterende produkter skrevet i PHP, hvilket forbedrede dem ved hjælp af en kombination af fordele fra begge sprog.

Vi vil fortælle, hvordan kombinationen af Go og PHP hjælper med at løse reelle udviklingsopgaver, og hvordan det er blevet et værktøj for os, der kan hjælpe med at afhjælpe nogle af problemerne relateret til modellen 'døende' PHP.

Din daglige PHP-udviklingsmiljø

Før vi fortæller, hvordan man kan genoplive modellen 'døende' PHP med Go, lad os se på dit standard PHP-udviklingsmiljø.

I de fleste tilfælde kører du en applikation ved hjælp af en kombination af webserveren nginx og PHP-FPM-serveren. Den første serverer statiske filer og omdirigerer specifikke anmodninger til PHP-FPM, som selv udfører PHP-koden. Måske bruger du en mindre populær kombination af Apache og mod_php. Men selvom den fungerer lidt anderledes, er principperne de samme.

Lad os se på, hvordan PHP-FPM udfører applikationskoden. Når en anmodning modtages, initialiserer PHP-FPM en underliggende PHP-proces og sender detaljerne i anmodningen som en del af dens tilstand (_GET, _POST, _SERVER osv.).

Tilstanden kan ikke ændres under udførelsen af PHP-scriptet, så for at få et nyt sæt inputdata skal du gøre det på én måde: rydde hukommelsen i processen og initialisere den igen.

Denne udførelsesmodel har mange fordele. Du behøver ikke at bekymre dig meget om hukommelsesforbruget, alle processer er fuldstændigt isolerede, og hvis en af dem "dør", vil den automatisk blive genskabt, og dette vil ikke påvirke de andre processer. Men denne tilgang har også ulemper, der viser sig, når du forsøger at skalere applikationen.

Ulemperne og ineffektiviteten i den almindelige PHP-miljø

Hvis du beskæftiger dig med professionel PHP-udvikling, ved du, hvor du skal starte et nyt projekt - med valget af et framework. Det er biblioteker til afhængighedsinjektion, ORM'er, oversættelser og skabeloner. Og selvfølgelig kan alle indgående brugerdata nemt placeres i et objekt (Symfony/HttpFoundation eller PSR-7). Frameworks er fantastiske!

Men alt har sin pris. I ethvert enterprise-niveau framework kræves det, at du indlæser mindst dusinvis af filer, opretter adskillige klasser og parser flere konfigurationer for at behandle en simpel brugerforespørgsel eller databaseanrop. Men det værste er, at efter hver opgave skal alt nulstilles, og du skal starte forfra: al den kode, du lige har initialiseret, bliver ubrugelig, og du kan ikke behandle endnu en anmodning med den. Fortæl dette til enhver programmør, der skriver i et andet sprog, og du vil se forundring i deres ansigt.

PHP-ingeniører har i årevis forsøgt at finde løsninger på dette problem, anvendt velovervejede metoder til 'lazy' indlæsning, mikrorahmen, optimerede biblioteker, cache osv. Men i sidste ende er man stadig nødt til at nulstille hele applikationen og starte forfra, gang på gang. (Oversætterens bemærkning: denne problem bliver delvist løst med fremkomsten af preload i PHP 7.4)

Kan PHP overleve mere end én anmodning ved hjælp af Go?

Man kan skrive PHP-scripts, der lever længere end et par minutter (snarere timer eller dage): for eksempel cron-jobs, CSV-parsere, queue-browsere. Alle arbejder efter det samme mønster: de henter en opgave, udfører den, venter på den næste. Koden er konstant i hukommelsen, hvilket sparer dyrebare millisekunder, da det tager tid at indlæse frameworket og applikationen ellers.

Men det er ikke så let at udvikle langsigtede scripts. Enhver fejl dræber helt processen, diagnose af hukommelseslækager driver en til vanvid, og debugging med F5 kan ikke længere bruges.

Situationen er forbedret med udgivelsen af PHP 7: der er kommet en pålidelig garbage collector, det er blevet lettere at håndtere fejl, og kernen udvidelser er nu beskyttet mod lækager. Ingeniører skal stadig være forsigtige med hukommelsen og huske på tilstande i koden (er der et sprog, hvor man kan ignorere disse ting?). Men alligevel er der færre overraskelser i PHP 7.

Kan man tage modellen for arbejde med langsigtede PHP-scripts, tilpasse den til mere trivielle opgaver som behandling af HTTP-anmodninger, og dermed undgå at skulle indlæse alt fra bunden for hver anmodning?

For at løse denne opgave skulle man først implementere en serverapplikation, der kunne modtage HTTP-anmodninger og videresende dem en ad gangen til PHP-arbejderen uden at dræbe det hver gang.

Vi vidste, at vi kunne skrive en webserver i ren PHP (PHP-PM) eller med C-udvidelse (Swoole). Og selv om hver metode har sine fordele, var ingen af dem tilfredsstillende for os — vi ønskede noget mere. Vi havde brug for ikke bare en webserver — vi ønskede en løsning, der kunne befri os fra de problemer, der er forbundet med "tung opstart" i PHP, og som samtidig kunne tilpasses og udvides til specifikke applikationer. Med andre ord havde vi brug for en applikationsserver.

Kan Go hjælpe med dette? Vi vidste, at det kunne, fordi dette sprog kompilerer applikationer til enkeltstående binære filer; det er tværplatformet; det bruger sin egen, meget elegante model for samtidighed (concurrency) og et bibliotek til at arbejde med HTTP; og endelig vil vi have adgang til tusinder af open-source-biblioteker og integrationer.

Udfordringer ved at kombinere to programmeringssprog

Først og fremmest skulle vi definere, hvordan to eller flere applikationer ville kommunikere med hinanden.

For eksempel ved hjælp af et fremragende bibliotek af Alex Palaeastris, der kunne implementere delt hukommelse mellem PHP og Go-processer (svarende til mod_php i Apache). Men dette bibliotek har nogle begrænsninger, der begrænser dets anvendelse til vores opgave.

Vi besluttede at bruge en anden, mere udbredt tilgang: at opbygge interaktionen mellem processer via sockets/pipe. Denne tilgang har i de seneste årtier bevist sin pålidelighed og er godt optimeret på systemniveau.

Først oprettede vi et simpelt binært protokol til dataudveksling mellem processer og fejlbehandling af overførsel. I sin enkleste form ligner protokollen denne type netstring med med header af fast størrelse (i vores tilfælde 17 byte), der indeholder oplysninger om pakkeformat, dens størrelse og en binær maske til at kontrollere dataintegritet.

På PHP-siden brugte vi funktionen pack, og på Go-siden brugte vi biblioteket encoding/binary.

Vi syntes, at ét protokol var for lidt — så vi tilføjede muligheden for at kalde Go-tjenester net/rpc direkte fra PHP. Senere hjalp dette os meget under udviklingen, da vi nemt kunne integrere Go-biblioteker i PHP-applikationer. Resultatet af dette arbejde kan ses, for eksempel i et andet af vores open-source-produkter, Goridge.

Opdeling af opgaver mellem flere PHP-arbejdere

Efter implementeringen af interaktionsmekanismen begyndte vi at overveje, hvordan vi mest effektivt kunne overføre opgaver til PHP-processerne. Når en opgave kommer, skal applikationsserveren vælge en ledig arbejder til at udføre den. Hvis en arbejder/proces stopper med en fejl eller "dør", slipper vi af med den og opretter en ny i stedet. Og hvis en arbejder/proces har udført arbejdet succesfuldt, returnerer vi den til puljen af arbejdere, der er tilgængelige til at udføre opgaver.

RoadRunner: PHP er ikke skabt til at dø, eller Golang skynder sig til undsætning

Til opbevaring af puljen af aktive arbejdere brugte vi en buffercanal, for at fjerne uventet "døde" arbejdere fra puljen tilføjede vi en mekanisme til overvågning af fejl og tilstande for arbejderne.

Som resultat har vi fået en fungerende PHP-server, der kan håndtere enhver forespørgsel præsenteret i binær form.

For at få vores applikation til at fungere som webserver, måtte vi vælge en pålidelig PHP-standard til repræsentation af enhver indkommende HTTP-forespørgsel. I vores tilfælde konverterer vi net/http-forespørgslen fra Go til formatet PSR-7, så den er kompatibel med de fleste tilgængelige PHP-rammer i dag.

Da PSR-7 betragtes som uforanderligt (nogen vil sige, at det teknisk set ikke er helt korrekt), må udviklerne skrive applikationer, der i princippet ikke behandler forespørgslen som en global enhed. Dette passer perfekt ind i konceptet med langlivede PHP-processer. Vores endelige implementering, som endnu ikke har fået et navn, så således ud:

RoadRunner: PHP er ikke skabt til at dø, eller Golang skynder sig til undsætning

Vi præsenterer RoadRunner — en højtydende server til PHP-applikationer

Vores første testopgave var en API-backend, hvor uforudsigelige spikes i forespørgsler opstod periodisk (meget oftere end normalt). Selvom nginx i de fleste tilfælde var tilstrækkeligt, stødte vi regelmæssigt på fejl 502, fordi vi ikke kunne balancere systemet hurtigt nok til at imødekomme det forventede stigning i belastningen.

Til erstatning for denne løsning implementerede vi i begyndelsen af 2018 vores første PHP/Go-applikationsserver. Og straks fik vi en utrolig effekt! Vi eliminerede ikke kun fejl 502 fuldstændigt, men vi formåede også at reducere antallet af servere med to tredjedele, hvilket sparede en masse penge og hovedpinepiller til ingeniører og produktchefer.

I midten af året forbedrede vi vores løsning, offentliggjorde den på GitHub under MIT-licensen og kaldte den RoadRunner, hvilket understregede dens utrolige hastighed og effektivitet.

Hvordan RoadRunner kan forbedre din udviklingsstak

Anvendelse RoadRunner gjorde det muligt for os at bruge Middleware net/http på Go-siden for at udføre JWT-verifikationer, inden anmodningen når PHP, samt for at behandle WebSockets og globalt aggregere tilstande i Prometheus.

Med den indbyggede RPC kan man åbne API'er for enhver Go-bibliotek til PHP uden at skrive wrapper-extensions. Endnu vigtigere er det, at man med RoadRunner kan implementere nye servere, der adskiller sig fra HTTP. Eksempler inkluderer lancering af PHP-handlere AWS Lambda, oprettelse af pålidelige kø-parsers og endda tilføjelse gRPC til vores applikationer.

Ved hjælp af PHP- og Go-samfundene har vi øget stabiliteten i løsningen og i nogle tests øget applikationernes ydeevne med op til 40 gange, forbedret debuggingsværktøjer, implementeret integration med Symfony-rammen og tilføjet støtte til HTTPS, HTTP/2, plugins og PSR-17.

Konklusion

Nogle sidder stadig fast i det forældede syn på PHP som et langsomt og klodset sprog, der kun er egnet til at skrive plugins til WordPress. Disse mennesker kan endda sige, at PHP har en sådan begrænsning: når en applikation bliver stor nok, skal man vælge et mere 'modent' sprog og omskrive den opbyggede kodebase, som har været samlet i mange år.

Alt dette vil vi sige: tænk igen. Vi mener, at det kun er dig selv, der sætter nogle begrænsninger for PHP. Du kan bruge hele dit liv på at skifte fra det ene sprog til det andet, i et forsøg på at finde den perfekte løsning til dine behov, eller du kan begynde at betragte sprog som værktøjer. De indbildte mangler ved sprog som PHP kan faktisk være grunden til dets succes. Og hvis du kombinerer det med et andet sprog som Go, kan du skabe langt mere magtfulde produkter, end hvis du kun brugte ét sprog.

Efter at have arbejdet med kombinationen af Go og PHP, kan vi bekræfte, at vi elsker dem. Vi planlægger ikke at ofre det ene for det andet — tværtimod vil vi lede efter måder at få endnu mere gavn af denne dobbelte stak.

UPD: vi byder velkommen til skaberen af RoadRunner og medforfatteren af den oprindelige artikel — Lachezis

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster