RoadRunner: PHP er ikke bygget for å dø, eller Golang til unnsetning

RoadRunner: PHP er ikke bygget for å dø, eller Golang til unnsetning

Hei Habr! Vi er aktive på Badoo jobber med PHP-ytelse, siden vi har et ganske stort system på dette språket og ytelsesproblemet er et pengebesparende problem. For mer enn ti år siden opprettet vi PHP-FPM for dette, som først var et sett med patcher for PHP, og senere gikk inn i den offisielle distribusjonen.

De siste årene har PHP gjort store fremskritt: søppelsamleren har blitt bedre, stabilitetsnivået har økt – i dag kan du skrive demoner og langlivede skript i PHP uten problemer. Dette tillot Spiral Scout å gå lenger: RoadRunner, i motsetning til PHP-FPM, rydder ikke opp i minnet mellom forespørsler, noe som gir en ekstra ytelsesgevinst (selv om denne tilnærmingen kompliserer utviklingsprosessen). Vi eksperimenterer for tiden med dette verktøyet, men vi har ingen resultater å dele. For å gjøre det morsommere å vente på dem, vi publiserer oversettelsen av RoadRunner-kunngjøringen fra Spiral Scout.

Tilnærmingen fra artikkelen er nær oss: når vi løser problemene våre, bruker vi også oftest en haug med PHP og Go, for å få fordelene med begge språkene og ikke forlate det ene til fordel for det andre.

Nyt!

De siste ti årene har vi laget søknader for bedrifter fra listen Fortune 500, og for bedrifter med et publikum på ikke mer enn 500 brukere. Hele denne tiden har ingeniørene våre utviklet backend hovedsakelig i PHP. Men for to år siden hadde noe stor innvirkning, ikke bare på ytelsen til produktene våre, men også på deres skalerbarhet – vi introduserte Golang (Go) i teknologistabelen vår.

Nesten umiddelbart oppdaget vi at Go tillot oss å bygge større applikasjoner med opptil 40x ytelsesforbedringer. Med den var vi i stand til å utvide våre eksisterende PHP-produkter, og forbedre dem ved å kombinere fordelene med begge språk.

Vi vil fortelle deg hvordan kombinasjonen av Go og PHP bidrar til å løse reelle utviklingsproblemer og hvordan det har blitt et verktøy for oss som kan bli kvitt noen av problemene knyttet til PHP døende modell.

Ditt daglige PHP-utviklingsmiljø

Før vi snakker om hvordan du kan bruke Go for å gjenopplive PHP-modellen, la oss ta en titt på standard PHP-utviklingsmiljøet ditt.

I de fleste tilfeller kjører du applikasjonen din ved å bruke en kombinasjon av nginx-webserveren og PHP-FPM-serveren. Førstnevnte serverer statiske filer og omdirigerer spesifikke forespørsler til PHP-FPM, mens PHP-FPM selv kjører PHP-kode. Du bruker kanskje den mindre populære kombinasjonen av Apache og mod_php. Men selv om det fungerer litt annerledes, er prinsippene de samme.

La oss ta en titt på hvordan PHP-FPM kjører applikasjonskode. Når en forespørsel kommer inn, initialiserer PHP-FPM en PHP-underordnet prosess og sender detaljene for forespørselen som en del av dens tilstand (_GET, _POST, _SERVER, etc.).

Tilstanden kan ikke endres under kjøringen av et PHP-skript, så den eneste måten å få et nytt sett med inputdata på er å tømme prosessminnet og initialisere det på nytt.

Denne utførelsesmodellen har mange fordeler. Du trenger ikke bekymre deg for mye om minneforbruk, alle prosesser er fullstendig isolert, og hvis en av dem «dør», vil den automatisk gjenskapes og det vil ikke påvirke resten av prosessene. Men denne tilnærmingen har også ulemper som dukker opp når man prøver å skalere applikasjonen.

Ulemper og ineffektiviteter ved et vanlig PHP-miljø

Hvis du er en profesjonell PHP-utvikler, så vet du hvor du skal starte et nytt prosjekt – med valg av rammeverk. Den består av avhengighetsinjeksjonsbiblioteker, ORM-er, oversettelser og maler. Og selvfølgelig kan alle brukerinndata enkelt legges inn i ett objekt (Symfony/HttpFoundation eller PSR-7). Rammer er kule!

Men alt har sin pris. I ethvert rammeverk på bedriftsnivå, for å behandle en enkel brukerforespørsel eller tilgang til en database, må du laste inn minst dusinvis av filer, opprette en rekke klasser og analysere flere konfigurasjoner. Men det verste er at etter å ha fullført hver oppgave, må du tilbakestille alt og starte på nytt: all koden du nettopp startet blir ubrukelig, med dens hjelp vil du ikke lenger behandle en ny forespørsel. Fortell dette til enhver programmerer som skriver på et annet språk, og du vil se forvirring i ansiktet hans.

PHP-ingeniører har lett etter måter å løse dette problemet på i årevis, ved å bruke smarte lazy-loading-teknikker, mikrorammeverk, optimaliserte biblioteker, cache osv. Men til slutt må du fortsatt tilbakestille hele applikasjonen og starte på nytt, igjen og igjen . (Oversetterens merknad: dette problemet vil delvis løses med ankomsten av forspenning i PHP 7.4)

Kan PHP med Go overleve mer enn én forespørsel?

Det er mulig å skrive PHP-skript som lever lenger enn noen få minutter (opptil timer eller dager): for eksempel cron-oppgaver, CSV-parsere, købrytere. De jobber alle i henhold til samme scenario: de henter en oppgave, utfører den og venter på den neste. Koden ligger i minnet hele tiden, og sparer dyrebare millisekunder ettersom det er mange ekstra trinn som kreves for å laste rammeverket og applikasjonen.

Men det er ikke lett å utvikle manus med lang levetid. Enhver feil dreper prosessen fullstendig, diagnostisering av minnelekkasjer er irriterende, og F5-feilsøking er ikke lenger mulig.

Situasjonen har forbedret seg med utgivelsen av PHP 7: en pålitelig søppeloppsamler har dukket opp, det har blitt lettere å håndtere feil, og kjerneutvidelser er nå lekkasjesikre. Riktignok må ingeniører fortsatt være forsiktige med hukommelsen og være klar over tilstandsproblemer i kode (finnes det et språk som kan ignorere disse tingene?). Likevel har PHP 7 færre overraskelser i vente for oss.

Er det mulig å ta modellen med å jobbe med PHP-skript med lang levetid, tilpasse den til mer trivielle oppgaver som å behandle HTTP-forespørsler, og dermed bli kvitt behovet for å laste alt fra bunnen av med hver forespørsel?

For å løse dette problemet trengte vi først å implementere en serverapplikasjon som kunne akseptere HTTP-forespørsler og omdirigere dem én etter én til PHP-arbeideren uten å drepe den hver gang.

Vi visste at vi kunne skrive en webserver i ren PHP (PHP-PM) eller bruke en C-utvidelse (Swoole). Og selv om hver metode har sine egne fordeler, passet ikke begge alternativene oss - vi ville ha noe mer. Vi trengte mer enn bare en webserver - vi forventet å få en løsning som kunne redde oss fra problemene knyttet til en "hard start" i PHP, som samtidig enkelt kunne tilpasses og utvides for spesifikke applikasjoner. Det vil si at vi trengte en applikasjonsserver.

Kan Go hjelpe med dette? Vi visste at det kunne fordi språket kompilerer applikasjoner til enkle binære filer; det er på tvers av plattformer; bruker sin egen, veldig elegante, parallelle prosesseringsmodell (samtidig) og et bibliotek for arbeid med HTTP; og til slutt vil tusenvis av åpen kildekode-biblioteker og integrasjoner være tilgjengelige for oss.

Vanskelighetene med å kombinere to programmeringsspråk

Først av alt var det nødvendig å bestemme hvordan to eller flere applikasjoner vil kommunisere med hverandre.

For eksempel ved å bruke utmerket bibliotek Alex Palaestras, var det mulig å dele minne mellom PHP- og Go-prosesser (i likhet med mod_php i Apache). Men dette biblioteket har funksjoner som begrenser bruken for å løse problemet vårt.

Vi bestemte oss for å bruke en annen, mer vanlig tilnærming: å bygge interaksjon mellom prosesser gjennom sockets / pipelines. Denne tilnærmingen har vist seg å være pålitelig i løpet av de siste tiårene og har blitt godt optimalisert på operativsystemnivå.

Til å begynne med laget vi en enkel binær protokoll for utveksling av data mellom prosesser og håndtering av overføringsfeil. I sin enkleste form ligner denne typen protokoll på nettstreng с pakkehode med fast størrelse (i vårt tilfelle 17 byte), som inneholder informasjon om pakketypen, dens størrelse og en binær maske for å sjekke integriteten til dataene.

På PHP-siden brukte vi pakkefunksjon, og på Go-siden, biblioteket koding/binær.

Det virket for oss at én protokoll ikke var nok - og vi la til muligheten til å ringe net/rpc go-tjenester direkte fra PHP. Senere hjalp dette oss mye i utviklingen, siden vi enkelt kunne integrere Go-biblioteker i PHP-applikasjoner. Resultatet av dette arbeidet kan for eksempel sees i vårt andre åpen kildekode-produkt Goridge.

Fordeling av oppgaver på tvers av flere PHP-arbeidere

Etter å ha implementert interaksjonsmekanismen begynte vi å tenke på den mest effektive måten å overføre oppgaver til PHP-prosesser på. Når en oppgave kommer, må applikasjonsserveren velge en ledig arbeider til å utføre den. Hvis en arbeider/prosess går ut med en feil eller "dør", blir vi kvitt den og oppretter en ny for å erstatte den. Og hvis arbeideren/prosessen er fullført, returnerer vi den til utvalget av arbeidere som er tilgjengelige for å utføre oppgaver.

RoadRunner: PHP er ikke bygget for å dø, eller Golang til unnsetning

For å lagre bassenget av aktive arbeidere brukte vi bufret kanal, for å fjerne uventet "døde" arbeidere fra bassenget, la vi til en mekanisme for sporing av feil og arbeidernes tilstand.

Som et resultat fikk vi en fungerende PHP-server som er i stand til å behandle alle forespørsler presentert i binær form.

For at applikasjonen vår skulle begynne å fungere som en webserver, måtte vi velge en pålitelig PHP-standard for å representere alle innkommende HTTP-forespørsler. I vårt tilfelle, vi bare forvandle net/http-forespørsel fra Gå til format PSR-7slik at den er kompatibel med de fleste PHP-rammeverkene som er tilgjengelige i dag.

Fordi PSR-7 anses som uforanderlig (noen vil si at det teknisk sett ikke er det), må utviklere skrive applikasjoner som i prinsippet ikke behandler forespørselen som en global enhet. Dette passer fint med konseptet med langvarige PHP-prosesser. Vår endelige implementering, som ennå ikke er navngitt, så slik ut:

RoadRunner: PHP er ikke bygget for å dø, eller Golang til unnsetning

Vi introduserer RoadRunner - høy ytelse PHP-applikasjonsserver

Vår første testoppgave var en API-backend, som med jevne mellomrom sprenger inn uforutsigbare forespørsler (mye oftere enn vanlig). Selv om nginx var tilstrekkelig i de fleste tilfeller, møtte vi regelmessig 502 feil fordi vi ikke kunne balansere systemet raskt nok for den forventede økningen i belastning.

For å erstatte denne løsningen, distribuerte vi vår første PHP/Go-applikasjonsserver tidlig i 2018. Og fikk umiddelbart en utrolig effekt! Ikke bare ble vi kvitt 502-feilen helt, men vi klarte å redusere antall servere med to tredjedeler, og sparte mye penger og hodepinepiller for ingeniører og produktsjefer.

Ved midten av året hadde vi forbedret løsningen vår, publisert den på GitHub under MIT-lisensen og kalt den RoadRunner, og understreker dermed dens utrolige hastighet og effektivitet.

Hvordan RoadRunner kan forbedre utviklingsstabelen din

Søknad RoadRunner tillot oss å bruke Middleware net/http på Go-siden for å utføre JWT-verifisering før forespørselen når PHP, samt håndtere WebSockets og aggregert tilstand globalt i Prometheus.

Takket være den innebygde RPC-en kan du åpne API-en til alle Go-biblioteker for PHP uten å skrive utvidelsesomslag. Enda viktigere, med RoadRunner kan du distribuere nye ikke-HTTP-servere. Eksempler inkluderer kjørende behandlere i PHP AWS Lambda, skape pålitelige købrytere, og til og med legge til gRPC til våre applikasjoner.

Ved hjelp av PHP- og Go-samfunnene forbedret vi stabiliteten til løsningen, økte applikasjonsytelsen opptil 40 ganger i noen tester, forbedret feilsøkingsverktøy, implementerte integrasjon med Symfony-rammeverket, og la til støtte for HTTPS, HTTP/2, plugins og PSR-17.

Konklusjon

Noen mennesker er fortsatt fanget i den utdaterte forestillingen om PHP som et tregt, uhåndterlig språk som bare er bra for å skrive plugins for WordPress. Disse menneskene kan til og med si at PHP har en slik begrensning: når applikasjonen blir stor nok, må du velge et mer "modent" språk og omskrive kodebasen akkumulert over mange år.

Til alt dette vil jeg svare: tenk om igjen. Vi tror at bare du setter noen begrensninger for PHP. Du kan bruke hele livet på å gå fra ett språk til et annet, prøve å finne den perfekte matchen for dine behov, eller du kan begynne å tenke på språk som verktøy. De antatte feilene til et språk som PHP kan faktisk være årsaken til suksessen. Og hvis du kombinerer det med et annet språk som Go, vil du lage mye kraftigere produkter enn om du var begrenset til å bruke ett språk.

Etter å ha jobbet med en haug med Go og PHP, kan vi si at vi elsker dem. Vi planlegger ikke å ofre det ene for det andre - tvert imot vil vi se etter måter å få enda mer verdi fra denne doble stabelen.

UPD: vi ønsker velkommen til skaperen av RoadRunner og medforfatteren av den originale artikkelen - Lachesis

Kilde: www.habr.com

Legg til en kommentar