RoadRunner: PHP är inte byggd för att dö, eller Golang till undsättning

RoadRunner: PHP är inte byggd för att dö, eller Golang till undsättning

Hej, Habr! Vi är aktiva på Badoo arbetar med PHP-prestanda, eftersom vi har ett ganska stort system på detta språk och frågan om prestanda handlar om att spara pengar. För mer än tio år sedan skapade vi PHP-FPM för detta, som först var en uppsättning patchar för PHP, och senare blev en del av den officiella distributionen.

De senaste åren har PHP gjort stora framsteg: sopsamlaren har förbättrats, stabilitetsnivån har ökat - idag kan du skriva demoner och långlivade skript i PHP utan problem. Detta gjorde det möjligt för Spiral Scout att gå längre: RoadRunner, till skillnad från PHP-FPM, rensar inte upp minnet mellan förfrågningar, vilket ger ytterligare prestandafördelar (även om detta tillvägagångssätt komplicerar utvecklingsprocessen). Vi experimenterar för närvarande med det här verktyget, men vi har inga resultat att dela ännu. För att göra det roligare att vänta på dem, Vi publicerar en översättning av RoadRunner-meddelandet från Spiral Scout.

Tillvägagångssättet från artikeln ligger nära oss: när vi löser våra problem använder vi också oftast en kombination av PHP och Go, för att få fördelarna med båda språken och inte ge upp det ena till förmån för det andra.

Njut!

Under de senaste tio åren har vi skapat ansökningar för företag från listan Fortune 500, och för företag med en publik på högst 500 användare. Hela denna tid utvecklade våra ingenjörer backend främst i PHP. Men för två år sedan var det något som gjorde stor inverkan, inte bara på våra produkters prestanda, utan också på deras skalbarhet – vi introducerade Golang (Go) i vår teknologistack.

Nästan omedelbart upptäckte vi att Go tillät oss att bygga större applikationer med upp till 40 gånger snabbare prestanda. Med den kunde vi utöka befintliga produkter skrivna i PHP, förbättra dem genom att kombinera fördelarna med båda språken.

Vi kommer att berätta hur en kombination av Go och PHP hjälper till att lösa verkliga utvecklingsproblem och hur det har förvandlats till ett verktyg för oss som kan eliminera några av de problem som är förknippade med PHP döende modell.

Din vardagliga PHP-utvecklingsmiljö

Innan vi pratar om hur du kan använda Go för att återuppliva PHPs döende modell, låt oss ta en titt på din vanliga PHP-utvecklingsmiljö.

I de flesta fall kör du programmet med en kombination av nginx-webbserver och PHP-FPM-server. Den första serverar statiska filer och omdirigerar specifika förfrågningar till PHP-FPM, och PHP-FPM kör själv PHP-kod. Kanske använder du en mindre populär kombination från Apache och mod_php. Men även om det fungerar lite annorlunda är principerna desamma.

Låt oss titta på hur PHP-FPM kör programkod. När en förfrågan kommer, initierar PHP-FPM den underordnade PHP-processen och skickar begärandetaljen som en del av dess tillstånd (_GET, _POST, _SERVER, etc.).

Tillståndet kan inte ändras under exekveringen av ett PHP-skript, så det finns bara ett sätt att få en ny uppsättning indata: genom att rensa processminnet och återinitiera det.

Denna utförandemodell har många fördelar. Du behöver inte oroa dig så mycket för minnesförbrukningen, alla processer är helt isolerade och om en av dem dör kommer den automatiskt att återskapas utan att resten av processerna påverkas. Men detta tillvägagångssätt har också nackdelar som dyker upp när man försöker skala applikationen.

Nackdelar och ineffektivitet med en vanlig PHP-miljö

Om du är engagerad i professionell utveckling inom PHP, då vet du var du ska börja ett nytt projekt - genom att välja ett ramverk. Den består av bibliotek för beroendeinjektion, ORM, översättningar och mallar. Och naturligtvis kan all användarinmatning enkelt läggas i ett objekt (Symfony/HttpFoundation eller PSR-7). Ramar är coola!

Men allt har sitt pris. I alla ramverk på företagsnivå, för att behandla en enkel användarförfrågan eller komma åt en databas, måste du ladda minst dussintals filer, skapa många klasser och analysera flera konfigurationer. Men det värsta är att efter att ha slutfört varje uppgift måste du återställa allt och börja om: all kod du just initierade blir värdelös, med dess hjälp kommer du inte längre att behandla en annan begäran. Säg detta till vilken programmerare som helst som skriver på något annat språk, och du kommer att se förvirring i hans ansikte.

PHP-ingenjörer har ägnat år åt att leta efter sätt att lösa detta problem, med hjälp av smarta lazy loading-tekniker, mikroramar, optimerade bibliotek, cachar, etc. Men i slutändan måste du fortfarande återställa hela applikationen och börja om, igen och igen. (Översättarens anmärkning: detta problem kommer delvis att lösas med tillkomsten av förbelastning i PHP 7.4)

Kan PHP med Go överleva mer än en begäran?

Det är möjligt att skriva PHP-skript som varar längre än några minuter (upp till timmar eller dagar): till exempel cron-uppgifter, CSV-parsare, köbusters. De arbetar alla enligt samma scenario: de hämtar en uppgift, utför den och väntar på nästa. Koden finns i minnet och sparar värdefulla millisekunder eftersom många ytterligare steg krävs för att ladda ramverket och applikationen.

Men att utveckla långlivade manus är inte så lätt. Alla fel dödar hela processen, diagnostisering av minnesläckor gör dig galen och du kan inte längre använda F5-felsökning.

Situationen har förbättrats med lanseringen av PHP 7: en pålitlig sophämtare har dykt upp, det har blivit lättare att hantera fel och kärntillägg är nu skyddade från läckor. Det är sant att ingenjörer fortfarande måste vara försiktiga med minnet och vara medvetna om tillståndsproblem i koden (finns det ett språk där vi inte behöver oroa oss för dessa saker?). Och ändå, i PHP 7, väntar färre överraskningar på oss.

Är det möjligt att ta modellen att arbeta med långlivade PHP-skript, anpassa den till mer triviala uppgifter som att bearbeta HTTP-förfrågningar och därmed eliminera behovet av att ladda allt från början för varje begäran?

För att lösa detta problem behövde vi först implementera en serverapplikation som kunde acceptera HTTP-förfrågningar och vidarebefordra dem en efter en till PHP-arbetaren utan att döda den varje gång.

Vi visste att vi kunde skriva en webbserver i ren PHP (PHP-PM) eller med C-tillägget (Swoole). Och även om varje metod har sina egna fördelar, passade inte båda alternativen oss - vi ville ha något mer. Vi behövde mer än bara en webbserver – vi hoppades få en lösning som kunde rädda oss från problemen i samband med den ”hårda starten” i PHP, som samtidigt enkelt kunde anpassas och byggas ut för specifika applikationer. Det vill säga vi behövde en applikationsserver.

Kan Go hjälpa till med detta? Vi visste att det kunde det eftersom språket kompilerar applikationer till enstaka binärer; det är plattformsoberoende; använder sin egen, mycket eleganta, parallella bearbetningsmodell (samtidighet) och bibliotek för att arbeta med HTTP; och slutligen kommer tusentals bibliotek och integrationer med öppen källkod att vara tillgängliga för oss.

Svårigheter att kombinera två programmeringsspråk

Det första steget var att bestämma hur två eller flera applikationer skulle kommunicera med varandra.

Till exempel att använda underbart bibliotek Alex Palaestras kunde implementera minnesdelning mellan PHP- och Go-processer (liknande mod_php i Apache). Men det här biblioteket har funktioner som begränsar dess användning för att lösa vårt problem.

Vi bestämde oss för att använda ett annat, vanligare, tillvägagångssätt: att bygga interaktion mellan processer genom uttag/rörledningar. Detta tillvägagångssätt har bevisat sin tillförlitlighet under de senaste decennierna och har optimerats väl på operativsystemnivå.

Till att börja med skapade vi ett enkelt binärt protokoll för utbyte av data mellan processer och hantering av överföringsfel. I sin enklaste form liknar denna typ av protokoll nätsträng с pakethuvud med fast storlek (i vårt fall 17 byte), som innehåller information om typen av paket, dess storlek och en binär mask för att kontrollera dataintegriteten.

På PHP-sidan använde vi packfunktion, och på Go-sidan - ett bibliotek kodning/binär.

Det verkade för oss att ett protokoll inte räckte - så vi lade till möjligheten att ringa Go services net/rpc direkt från PHP. Detta hjälpte oss senare mycket i utvecklingen, eftersom vi enkelt kunde integrera Go-bibliotek i PHP-applikationer. Resultatet av detta arbete kan ses till exempel i vår andra produkt med öppen källkod Goridge.

Fördelning av uppgifter över flera PHP-arbetare

Efter att ha implementerat interaktionsmekanismen började vi fundera på hur vi mest effektivt överför uppgifter till PHP-processer. När en uppgift anländer måste applikationsservern välja en ledig arbetare för att slutföra den. Om en arbetare/process avslutas med ett fel eller "dör" blir vi av med det och skapar en ny för att ersätta den. Och om arbetaren/processen har slutförts framgångsrikt, returnerar vi den till poolen av arbetare som är tillgängliga för att utföra uppgifter.

RoadRunner: PHP är inte byggd för att dö, eller Golang till undsättning

För att lagra en pool av aktiva arbetare använde vi buffrad kanal, för att ta bort oväntat "döda" arbetare från poolen, lade vi till en mekanism för att spåra fel och arbetartillstånd.

Som ett resultat fick vi en fungerande PHP-server som kan behandla alla förfrågningar som presenteras i binär form.

För att vår applikation skulle fungera som en webbserver var vi tvungna att välja en pålitlig PHP-standard för att representera eventuella inkommande HTTP-förfrågningar. I vårt fall vi bara omvandla net/http-förfrågan från Gå till format PSR-7så att den är kompatibel med de flesta PHP-ramverk som finns tillgängliga idag.

Eftersom PSR-7 anses vara oföränderlig (vissa skulle säga att den tekniskt sett inte är det), måste utvecklare skriva applikationer som i grunden inte behandlar begäran som en global enhet. Detta passar bra med konceptet med långlivade PHP-processer. Vår slutliga implementering, som ännu inte hade fått ett namn, såg ut så här:

RoadRunner: PHP är inte byggd för att dö, eller Golang till undsättning

Vi presenterar RoadRunner - högpresterande PHP-applikationsserver

Vår första testuppgift var API-backend, som periodvis upplevde oväntade skurar av förfrågningar (mycket oftare än vanligt). Även om nginx var tillräckligt i de flesta fall, stötte vi regelbundet på 502 fel eftersom vi inte kunde balansera systemet tillräckligt snabbt för den förväntade ökningen av belastningen.

För att ersätta den här lösningen distribuerade vi vår första PHP/Go-applikationsserver i början av 2018. Och direkt fick vi en otrolig effekt! Inte nog med att vi helt blev av med 502-felet, utan vi kunde också minska antalet servrar med två tredjedelar, vilket sparade mycket pengar och huvudvärk för ingenjörer och produktchefer.

I mitten av året hade vi fulländat vår lösning, publicerat den på GitHub under MIT-licensen och kallat den RoadRunner, och framhäver därmed dess otroliga hastighet och effektivitet.

Hur RoadRunner kan förbättra din utvecklingsstack

Ansökan RoadRunner tillät oss att använda Middleware net/http på Go-sidan för att utföra JWT-verifiering innan begäran ens träffar PHP, samt för att hantera WebSockets och global tillståndsaggregation i Prometheus.

Tack vare den inbyggda RPC kan du öppna API:et för alla Go-bibliotek för PHP utan att skriva förlängningsomslag. Ännu viktigare är att RoadRunner kan användas för att distribuera nya icke-HTTP-servrar. Exempel inkluderar lansering av hanterare i PHP AWS Lambda, skapa pålitliga köbusters och till och med lägga till gRPC till våra applikationer.

Med hjälp av PHP- och Go-gemenskaperna har vi ökat stabiliteten i lösningen, ökat applikationsprestanda med upp till 40 gånger i vissa tester, förbättrat felsökningsverktyg, implementerat integration med Symfony-ramverket och lagt till stöd för HTTPS, HTTP/ 2, plugins och PSR-17.

Slutsats

Vissa människor är fortfarande fångade i den föråldrade synen på PHP som ett långsamt, besvärligt språk som bara är bra för att skriva WordPress-plugins. Dessa personer kan till och med säga att PHP har en begränsning: när applikationen blir tillräckligt stor måste du välja ett mer "moget" språk och skriva om kodbasen som har ackumulerats under många år.

På allt detta vill jag svara: tänk om. Vi tror att endast du kan sätta några begränsningar för PHP. Du kan spendera hela ditt liv med att hoppa från ett språk till ett annat, försöka hitta den perfekta matchningen för dina behov, eller så kan du börja tänka på språk som verktyg. De upplevda bristerna hos ett språk som PHP kan faktiskt vara orsakerna till dess framgång. Och om du kombinerar det med ett annat språk som Go kan du skapa mycket kraftfullare produkter än om du var begränsad till bara ett språk.

Efter att ha arbetat med en kombination av Go och PHP kan vi säga att vi älskar dem. Vi planerar inte att offra det ena för det andra, utan snarare letar efter sätt att få ut ännu mer värde ur denna dubbla stack.

UPD: Vi välkomnar skaparen av RoadRunner och medförfattare till originalartikeln - Lachesis

Källa: will.com

Lägg en kommentar