
Pozdrav, Habr! Aktivno radimo u Badoo , s obzirom na to da imamo prilično veliki sistem na ovom jeziku i pitanje performansi je pitanje uštede novca. Prije više od deset godina stvorili smo PHP-FPM, koji je isprva bio skup zakrpa za PHP, a kasnije je postao deo službenog izdanja.
Tokom poslednjih godina PHP je značajno napredovao: poboljšan je sakupljač smeća, a nivo stabilnosti se povećao — danas je moguće pisati demone i dugotrajne skripte na PHP-u bez većih problema. To je omogućilo Spiral Scout-u da ide dalje: RoadRunner, za razliku od PHP-FPM-a, ne čisti memoriju između zahteva, što donosi dodatne performanse (iako ovaj pristup komplikuje proces razvoja). Trenutno eksperimentišemo s ovim alatom, ali zasad nemamo rezultata koje bismo mogli podeliti. Da čekanje bude zabavnije, publikujemo prevođenje najave RoadRunner-a od Spiral Scout-a.
Pristup iz članka nam je blizak: u rešavanju svojih zadataka najčešće koristimo kombinaciju PHP-a i Go-a, dobijajući prednosti oba jezika i ne odričući se jednog u korist drugog.
Uživajte!
Tokom poslednjih deset godina razvijali smo aplikacije i za kompanije sa liste , i za biznise sa manje od 500 korisnika. Tokom sveg ovog vremena naši inženjeri su uglavnom razvijali backend pretežno na PHP-u. Međutim, pre dve godine nešto je snažno uticalo ne samo na performanse naših proizvoda, već i na njihovu skalabilnost — uveli smo Golang (Go) u naš tehnološki stek.
Gotovo odmah smo otkrili da nam Go omogućava da kreiramo veće aplikacije uz povećanje performansi do 40 puta. Uz njegovu pomoć mogli smo da proširujemo postojeće proizvode napisane na PHP-u, unapređujući ih zahvaljujući kombinaciji prednosti oba jezika.
Ispričaćemo kako kombinacija Go-a i PHP-a pomaže u rešavanju stvarnih razvojnih zadataka i kako je postala alat koji može osloboditi deo problema povezanih sa .
Vaše svakodnevno okruženje PHP-razvoja
Pre nego što pričamo o tome kako Go može oživeti model "umiranja" PHP-a, hajde da razmotrimo vaše standardno okruženje PHP-razvoja.
U većini slučajeva pokrećete aplikaciju pomoću kombinacije web poslužitelja nginx i PHP-FPM poslužitelja. Prvi se brine o statičkim datotekama i prosljeđuje specifične zahtjeve PHP-FPM-u, dok sam PHP-FPM izvršava PHP kod. Možda koristite manje popularnu kombinaciju Apache i mod_php. No, iako radi malo drugačije, principi su isti.
Pogledajmo kako PHP-FPM izvršava kod aplikacije. Kada stigne zahtjev, PHP-FPM inicijalizira podproces PHP-a i prosljeđuje detalje zahtjeva kao dio njegovog stanja (_GET, _POST, _SERVER itd.).
Stanje se ne može mijenjati tijekom izvršavanja PHP skripte, stoga je moguće dobiti novi skup ulaznih podataka samo na jedan način: čišćenjem memorije procesa i ponovnim inicijaliziranjem.
Ovaj model izvršavanja ima mnogo prednosti. Ne morate previše brinuti o potrošnji memorije, svi procesi su potpuno izolirani, a ako jedan od njih „umre“, automatski će se obnoviti i to neće utjecati na ostale procese. No, ova metoda ima i nedostatke koji se pojavljuju prilikom pokušaja skaliranja aplikacije.
Nedostaci i neučinkovitost običnog PHP okruženja
Ako se bavite profesionalnim razvojem na PHP-u, znate s čega započeti novi projekt - od odabira frameworka. On predstavlja biblioteke za uvođenje ovisnosti, ORM-ove, prijevode i predloške. I, naravno, sve ulazne korisničke podatke možete lako staviti u jedan objekt (Symfony/HttpFoundation ili PSR-7). Frameworki su odlični!
No, sve to ima svoju cijenu. U svakom frameworku razine poduzeća za obradu jednostavnog korisničkog zahtjeva ili poziva na BDP morat ćete učitati najmanje desetke datoteka, stvoriti brojne klase i obraditi nekoliko konfiguracija. No, najgore od svega je to što nakon izvršavanja svake zadaće morate sve poništiti i započeti ispočetka: sav onaj kod koji ste upravo inicirali postaje beskoristan, s njim više ne možete obraditi još jedan zahtjev. Recite ovo bilo kojem programeru koji piše na nekom drugom jeziku - i vidjet ćete zbunjenost na njegovom licu.
PHP programeri su godinama tražili načine za rješavanje ovog problema, koristeći razrađene metode "lenjog" učitavanja, mikrorokframe, optimizirane biblioteke, cache itd. No, na kraju se ipak moraju resetirati cijela aplikacija i početi iznova, ponovno i ponovno. (Napomena prevoditelja: djelomično će ovaj problem biti riješen s pojavom u PHP 7.4)
Može li PHP uz pomoć Go preživjeti više od jednog zahtjeva?
Mogu se napisati PHP skripte koje će trajati dulje od nekoliko minuta (čak do sati ili dana): na primjer, cron zadaci, CSV parseri, razbijači redova. Svi oni rade po istom scenariju: izvlače zadatak, izvršavaju ga, čekaju sljedeće. Kod konstantno ostaje u memoriji, štedeći dragocjene milisekunde, jer učitavanje frameworka i aplikacije zahtijeva ostvarivanje mnoštva dodatnih radnji.
No, razvijati dugotrajne skripte nije tako jednostavno. Svaka greška potpuno ubija proces, dijagnosticiranje curenja memorije dovodi do ludila, a korištenje F5 za debagiranje više nije moguće.
Situacija se poboljšala izlaskom PHP 7: uveden je pouzdan sakupljač smeća, olakšano je upravljanje greškama, a proširenja jezgre sada su zaštićena od curenja. Međutim, inženjeri se još uvijek moraju pažljivo odnositi prema memoriji i imati na umu probleme stanja u kodu (postoji li jezik koji može ignorirati te probleme?). Ipak, u PHP 7 nas manje iznenađenja očekuje.
Može li se uzeti model rada s dugotrajnim PHP skriptama, prilagoditi ga za trivijalnije zadatke poput obrade HTTP zahtjeva i tako eliminirati potrebu za učitavanjem svega iz početka s svakim zahtjevom?
Za rješavanje ovog zadatka prvo je bilo potrebno realizirati serversku aplikaciju koja može primati HTTP zahtjeve i usmjeravati ih jedan po jedan PHP radniku, ne ubijajući ga svaki put.
Znali smo da možemo napisati web server u čistom PHP-u (PHP-PM) ili koristeći C-ekstenziju (Swoole). Iako svaki pristup ima svoje prednosti, ni jedna varijanta nam nije odgovarala - željeli smo nešto više. Trebao nam je ne samo web server - očekivali smo rješenje koje će nas osloboditi problema 'teškog starta' u PHP-u, a koje se lako može prilagoditi i proširiti za specifične aplikacije. Drugim riječima, trebao nam je aplikacijski server.
Može li Go pomoći u tome? Znali smo da može, jer ovaj jezik kompilira aplikacije u pojedinačne binarne datoteke; je višep платформen; koristi vlastiti, vrlo elegantan model paralelne obrade (concurrency) i biblioteku za rad s HTTP-om; i na kraju, bit će nam dostupne tisuće open-source biblioteka i integracija.
Teškoće kombiniranja dvaju programskih jezika
Prvo je trebalo odrediti kako će se dvije ili više aplikacija međusobno komunicirati.
Na primjer, kroz Alexa Paleastrasa, moglo se implementirati dijeljenje memorije između PHP i Go procesa (službeni mod_php u Apache-u). No, ova biblioteka ima karakteristike koje ograničavaju njezinu primjenu za rješavanje našeg zadatka.
Odlučili smo se za drugi, rašireniji pristup: izgraditi interakciju između procesa putem soketa/pipelinea. Ovaj je pristup u posljednjim desetljećima dokazao svoju pouzdanost i dobro je optimiziran na razini operacijskog sustava.
Na početku smo stvorili jednostavan binarni protokol za razmjenu podataka između procesa i obradu grešaka prijenosa. U svojoj najjednostavnijoj formi, protokol ovog tipa nalikuje s (u našem slučaju 17 bajtova), koje sadrži informacije o vrsti paketa, njegovoj veličini i binarnu masku za provjeru integriteta podataka.
Na strani PHP-a koristili smo , a na strani Go - biblioteku.
Jednog protokola nam se činilo premalo - dodali smo mogućnost pozivanja. Kasnije nam je to vrlo pomoglo u razvoju, jer smo lako mogli integrirati Go biblioteke u PHP aplikacije. Rezultat ovog rada može se vidjeti, na primjer, u našem drugom open-source proizvodu.
Raspodjela zadataka među više PHP radnika
Nakon implementacije mehanizma interakcije, počeli smo razmišljati o najefikasnijem načinu prijenosa zadataka PHP procesima. Kada stigne zadatak, aplikacijski poslužitelj mora odabrati slobodnog radnika za njegovo izvršenje. Ako radnik/proces završi s greškom ili se "sruši", oslobađamo ga i stvaramo novog u zamjenu. A ako je radnik/proces uspješno izvršen, vraćamo ga u bazen radnika dostupnih za izvršenje zadataka.

Za pohranu bazena aktivnih radnika koristili smo, kako bismo uklonili nepredviđeno "srušene" radnike, dodali smo mehanizam praćenja grešaka i stanja radnika.
Kao rezultat, dobili smo funkcionalni PHP poslužitelj sposoban obraditi sve zahtjeve predstavljene u binarnom formatu.
Da bi naša aplikacija počela raditi kao web poslužitelj, morali smo odabrati pouzdan PHP standard za predstavljanje bilo kojih dolaznih HTTP zahtjeva. U našem slučaju jednostavno net/http zahtjev iz Go-a u format, kako bi bio kompatibilan s većinom dostupnih PHP okvira danas.
Budući da se PSR-7 smatra nepromjenjivim (neki bi rekli da tehnički to nije tako), programerima je potrebno pisati aplikacije koje, u principu, ne tretiraju zahtjev kao globalnu entitet. To se odlično uklapa u koncept dugotrajnih PHP procesa. Naša konačna implementacija, koja još nema naziv, izgledala je ovako:

Predstavljamo RoadRunner —
Naš prvi testni zadatak bio je API backend, na kojem su povremeno nepredvidivo nastajali porasti zahtjeva (daleko češće nego obično). Iako su u većini slučajeva mogućnosti nginx-a bile dovoljne, redovito smo se susretali s greškom 502, jer nismo mogli dovoljno brzo balansirati sustav za očekivano povećanje opterećenja.
Za zamjenu ovog rješenja, početkom 2018. godine implementirali smo naš prvi PHP/Go aplikacijski poslužitelj. I odmah smo dobili nevjerojatan učinak! Ne samo da smo potpuno eliminirali grešku 502, već smo također uspjeli smanjiti broj poslužitelja za dvije trećine, uštedevši gomilu novca i tableta protiv glavobolje za inženjere i menadžere proizvoda.
Sredinom godine poboljšali smo naše rješenje, objavili ga na GitHubu pod MIT licencom i nazvali , ističući njegovu nevjerojatnu brzinu i učinkovitost.
Kako RoadRunner može poboljšati vaš razvojni stog
Primjena omogućilo nam je korištenje Middleware net/http na strani Go, kako bismo provodili JWT-verifikaciju prije nego što zahtjev dospije do PHP-a, kao i za obradu WebSocketa i globalno aggregeranje stanja u Prometheus.
Zahvaljujući ugrađenom RPC-u možemo otvoriti API bilo kojih Go-biblioteka za PHP bez pisanja wrapper ekstenzija. Što je još važnije, uz RoadRunner možemo implementirati nove servere, različite od HTTP-a. Primjeri uključuju pokretanje PHP obradnika, kreiranje pouzdanih raspoređivača i čak dodavanje u naše aplikacije.
Uz zajednice PHP-a i Go-a poboljšali smo stabilnost rješenja, u nekim testovima povećali performanse aplikacija do 40 puta, unaprijedili alate za otklanjanje grešaka, implementirali integraciju s Symfony okvirom i dodali podršku za HTTPS, HTTP/2, dodatke i PSR-17.
Zaključak
Neki još uvijek žive u zastarjelom uvjerenju da je PHP spor i nezgrapan jezik, pogodan samo za pisanje dodataka za WordPress. Ti ljudi mogu reći da PHP ima određeno ograničenje: kada aplikacija postane dovoljno velika, mora se odabrati „zreliji“ jezik i prepisati akumulirani kod koji traje godinama.
Na sve to želimo odgovoriti: razmislite ponovno. Smatramo da samo vi sami postavljate neka ograničenja za PHP. Možete potrošiti cijeli život prelazeći s jednog jezika na drugi, pokušavajući pronaći idealnu kombinaciju s vašim potrebama, ili možete početi gledati na jezike kao na alate. Izmišljeni nedostaci jezika poput PHP-a zapravo mogu biti uzroci njegovog uspjeha. A ako ga spojite s drugim jezikom poput Go-a, stvorit ćete mnogo snažnije proizvode nego da ste se ograničili na korištenje samo jednog jezika.
Radili smo sa spojem Go i PHP i možemo reći da smo ih zavoljeli. Ne planiramo žrtvovati jedan za drugi — naprotiv, tražit ćemo načine da izvučemo još više koristi iz ovog dvostrukog stoga.
UPD: pozdravljamo kreatora RoadRunner-a i co-autora originalnog članka —
Izvor: habr.com
