RoadRunner: PHP nu este construit pentru a muri, sau Golang pentru salvare

RoadRunner: PHP nu este construit pentru a muri, sau Golang pentru salvare

Hei Habr! Suntem activi la Badoo lucrează la performanța PHP, deoarece avem un sistem destul de mare în această limbă, iar problema performanței este o problemă de economisire a banilor. În urmă cu mai bine de zece ani, am creat PHP-FPM pentru asta, care la început a fost un set de patch-uri pentru PHP, iar ulterior a intrat în distribuția oficială.

În ultimii ani, PHP a făcut progrese mari: colectorul de gunoi s-a îmbunătățit, nivelul de stabilitate a crescut - astăzi puteți scrie demoni și scripturi de lungă durată în PHP fără probleme. Acest lucru a permis Spiral Scout să meargă mai departe: RoadRunner, spre deosebire de PHP-FPM, nu curățește memoria între solicitări, ceea ce oferă un câștig suplimentar de performanță (deși această abordare complică procesul de dezvoltare). În prezent experimentăm acest instrument, dar nu avem încă niciun rezultat de distribuit. Pentru a face așteptarea lor mai distractivă, publicăm traducerea anunțului RoadRunner din Spiral Scout.

Abordarea din articol este aproape de noi: atunci când ne rezolvăm problemele, folosim cel mai adesea și o grămadă de PHP și Go, obținând beneficiile ambelor limbi și nu abandonând una în favoarea celeilalte.

Bucurați-vă!

În ultimii zece ani, am creat aplicații pentru companiile din listă Fortune 500, și pentru companii cu un public de cel mult 500 de utilizatori. În tot acest timp, inginerii noștri au dezvoltat backend-ul în principal în PHP. Dar acum doi ani, ceva a avut un impact mare nu numai asupra performanței produselor noastre, ci și asupra scalabilității acestora - am introdus Golang (Go) în tehnologia noastră.

Aproape imediat, am descoperit că Go ne-a permis să construim aplicații mai mari, cu îmbunătățiri de până la 40 de ori a performanței. Cu acesta, am reușit să extindem produsele existente scrise în PHP, îmbunătățindu-le prin combinarea avantajelor ambelor limbi.

Vă vom spune cum combinația dintre Go și PHP ajută la rezolvarea problemelor reale de dezvoltare și cum s-a transformat într-un instrument pentru noi care poate scăpa de unele dintre problemele asociate cu Model PHP pe moarte.

Mediul dumneavoastră zilnic de dezvoltare PHP

Înainte de a vorbi despre cum puteți folosi Go pentru a reînvia modelul PHP pe moarte, să aruncăm o privire la mediul dvs. implicit de dezvoltare PHP.

În cele mai multe cazuri, rulați aplicația folosind o combinație de server web nginx și server PHP-FPM. Primul servește fișiere statice și redirecționează anumite solicitări către PHP-FPM, în timp ce PHP-FPM în sine execută cod PHP. Este posibil să utilizați combinația mai puțin populară de Apache și mod_php. Dar deși funcționează puțin diferit, principiile sunt aceleași.

Să aruncăm o privire la modul în care PHP-FPM execută codul aplicației. Când vine o solicitare, PHP-FPM inițializează un proces copil PHP și transmite detaliile cererii ca parte a stării acesteia (_GET, _POST, _SERVER etc.).

Starea nu se poate schimba în timpul execuției scriptului PHP, așa că există o singură modalitate de a obține un nou set de date de intrare: ștergerea memoriei procesului și reinițializarea acesteia.

Acest model de execuție are multe avantaje. Nu trebuie să vă faceți prea multe griji cu privire la consumul de memorie, toate procesele sunt complet izolate, iar dacă unul dintre ele „moare”, acesta va fi recreat automat și nu va afecta restul proceselor. Dar această abordare are și dezavantaje care apar atunci când se încearcă scalarea aplicației.

Dezavantajele și ineficiența unui mediu PHP obișnuit

Dacă sunteți un dezvoltator PHP profesionist, atunci știți de unde să începeți un nou proiect - cu alegerea unui cadru. Constă din biblioteci de injectare a dependențelor, ORM-uri, traduceri și șabloane. Și, desigur, toate intrările utilizatorului pot fi plasate într-un singur obiect (Symfony/HttpFoundation sau PSR-7). Cadrele sunt misto!

Dar totul are prețul lui. În orice cadru la nivel de întreprindere, pentru a procesa o simplă cerere de utilizator sau acces la o bază de date, va trebui să încărcați cel puțin zeci de fișiere, să creați numeroase clase și să analizați mai multe configurații. Dar cel mai rău lucru este că după finalizarea fiecărei sarcini, va trebui să resetați totul și să o luați de la capăt: tot codul pe care tocmai l-ați inițiat devine inutil, cu ajutorul lui nu veți mai procesa o altă solicitare. Spune-i asta oricărui programator care scrie într-o altă limbă și vei vedea nedumerire pe fața lui.

Inginerii PHP caută de ani de zile modalități de a rezolva această problemă, folosind tehnici inteligente de încărcare leneră, microframeworks, biblioteci optimizate, cache etc. Dar, în cele din urmă, trebuie să resetați întreaga aplicație și să o luați de la capăt, iar și iar. . (Nota traducătorului: această problemă va fi parțial rezolvată odată cu apariția preîncărcare în PHP 7.4)

Poate PHP cu Go să supraviețuiască mai mult de o solicitare?

Este posibil să scrieți scripturi PHP care trăiesc mai mult de câteva minute (până la ore sau zile): de exemplu, sarcini cron, parsere CSV, întreruperi de coadă. Toate funcționează conform aceluiași scenariu: preiau o sarcină, o execută și așteaptă următoarea. Codul se află în memorie tot timpul, economisind milisecunde prețioase, deoarece sunt necesari mulți pași suplimentari pentru a încărca cadrul și aplicația.

Dar dezvoltarea unor scenarii de lungă durată nu este ușoară. Orice eroare distruge complet procesul, diagnosticarea scurgerilor de memorie este enervantă, iar depanarea F5 nu mai este posibilă.

Situația s-a îmbunătățit odată cu lansarea PHP 7: a apărut un colector de gunoi de încredere, a devenit mai ușor de gestionat erorile, iar extensiile de kernel sunt acum rezistente la scurgeri. Adevărat, inginerii trebuie să fie atenți la memorie și să fie conștienți de problemele de stare din cod (există un limbaj care poate ignora aceste lucruri?). Cu toate acestea, PHP 7 ne rezervă mai puține surprize.

Este posibil să luăm modelul de lucru cu scripturi PHP cu durată lungă de viață, să-l adaptăm la sarcini mai banale, cum ar fi procesarea solicitărilor HTTP, și astfel să scăpăm de necesitatea de a încărca totul de la zero cu fiecare solicitare?

Pentru a rezolva această problemă, trebuia mai întâi să implementăm o aplicație server care să accepte cereri HTTP și să le redirecționăm una câte una către lucrătorul PHP fără a o ucide de fiecare dată.

Știam că putem scrie un server web în PHP pur (PHP-PM) sau folosind o extensie C (Swoole). Și, deși fiecare metodă are propriile merite, ambele opțiuni nu ni s-au potrivit - ne doream ceva mai mult. Aveam nevoie de mai mult decât un simplu server web - ne așteptam să obținem o soluție care să ne salveze de problemele asociate cu un „pornire greu” în PHP, care în același timp ar putea fi ușor adaptat și extins pentru aplicații specifice. Adică aveam nevoie de un server de aplicații.

Go poate ajuta cu asta? Știam că se poate, deoarece limbajul compilează aplicațiile în binare unice; este multiplatformă; folosește propriul model de procesare paralelă, foarte elegant, (concurență) și o bibliotecă pentru lucrul cu HTTP; și, în sfârșit, mii de biblioteci și integrări open-source vor fi disponibile pentru noi.

Dificultățile combinării a două limbaje de programare

În primul rând, a fost necesar să se determine modul în care două sau mai multe aplicații vor comunica între ele.

De exemplu, folosind biblioteca excelenta Alex Palaestras, a fost posibilă partajarea memoriei între procesele PHP și Go (similar cu mod_php în Apache). Dar această bibliotecă are caracteristici care limitează utilizarea ei pentru rezolvarea problemei noastre.

Am decis să folosim o abordare diferită, mai comună: să construim interacțiunea între procese prin prize / conducte. Această abordare sa dovedit a fi fiabilă în ultimele decenii și a fost bine optimizată la nivel de sistem de operare.

Pentru început, am creat un protocol binar simplu pentru schimbul de date între procese și gestionarea erorilor de transmisie. În forma sa cea mai simplă, acest tip de protocol este similar cu netstring с antet pachet de dimensiune fixă (în cazul nostru 17 octeți), care conține informații despre tipul pachetului, dimensiunea acestuia și o mască binară pentru a verifica integritatea datelor.

Pe partea PHP am folosit funcția de pachet, iar pe partea Go, biblioteca codificare/binară.

Ni s-a părut că un singur protocol nu este suficient - și am adăugat posibilitatea de a apela net/rpc go servicii direct din PHP. Mai târziu, acest lucru ne-a ajutat foarte mult în dezvoltare, deoarece am putut integra cu ușurință bibliotecile Go în aplicațiile PHP. Rezultatul acestei lucrări poate fi văzut, de exemplu, în celălalt produs open-source al nostru Goridge.

Distribuirea sarcinilor între mai mulți lucrători PHP

După implementarea mecanismului de interacțiune, am început să ne gândim la cea mai eficientă modalitate de a transfera sarcini în procesele PHP. Când sosește o sarcină, serverul de aplicații trebuie să aleagă un lucrător liber care să o execute. Dacă un lucrător/proces iese cu o eroare sau „moare”, scăpăm de el și creăm unul nou pentru a-l înlocui. Și dacă lucrătorul/procesul s-a finalizat cu succes, îl returnăm grupului de lucrători disponibili pentru a îndeplini sarcini.

RoadRunner: PHP nu este construit pentru a muri, sau Golang pentru salvare

Pentru a stoca grupul de lucrători activi, am folosit canal tamponat, pentru a elimina lucrătorii „morți” în mod neașteptat din grup, am adăugat un mecanism de urmărire a erorilor și a stărilor lucrătorilor.

Ca rezultat, am obținut un server PHP funcțional, capabil să proceseze orice cereri prezentate în formă binară.

Pentru ca aplicația noastră să înceapă să funcționeze ca server web, a trebuit să alegem un standard PHP de încredere care să reprezinte orice solicitări HTTP primite. În cazul nostru, doar transforma net/http cerere de la Go to format PSR-7astfel încât să fie compatibil cu majoritatea cadrelor PHP disponibile astăzi.

Deoarece PSR-7 este considerat imuabil (unii ar spune că din punct de vedere tehnic nu este), dezvoltatorii trebuie să scrie aplicații care nu tratează cererea ca o entitate globală în principiu. Acest lucru se potrivește foarte bine cu conceptul de procese PHP cu durată lungă de viață. Implementarea noastră finală, care încă nu a fost numită, a arătat astfel:

RoadRunner: PHP nu este construit pentru a muri, sau Golang pentru salvare

Vă prezentăm RoadRunner - server de aplicații PHP de înaltă performanță

Prima noastră sarcină de testare a fost un backend API, care explodează periodic în mod imprevizibil (mult mai des decât de obicei). Deși nginx a fost suficient în majoritatea cazurilor, am întâlnit în mod regulat erori 502, deoarece nu am putut echilibra sistemul suficient de repede pentru creșterea așteptată a încărcării.

Pentru a înlocui această soluție, am implementat primul nostru server de aplicații PHP/Go la începutul anului 2018. Și a obținut imediat un efect incredibil! Nu numai că am scăpat complet de eroarea 502, dar am reușit să reducem numărul de servere cu două treimi, economisind o mulțime de bani și pastile pentru dureri de cap pentru ingineri și managerii de produs.

Până la jumătatea anului, ne-am îmbunătățit soluția, am publicat-o pe GitHub sub licența MIT și am numit-o RoadRunner, subliniindu-i astfel viteza si eficienta incredibila.

Cum vă poate îmbunătăți RoadRunner stiva de dezvoltare

cerere RoadRunner ne-a permis să folosim Middleware net/http pe partea Go pentru a efectua verificarea JWT înainte ca cererea să ajungă la PHP, precum și să gestionăm WebSockets și starea agregată la nivel global în Prometheus.

Datorită RPC-ului încorporat, puteți deschide API-ul oricărei biblioteci Go pentru PHP fără a scrie pachete de extensii. Mai important, cu RoadRunner puteți implementa noi servere non-HTTP. Exemplele includ rularea de handlere în PHP AWS Lambdas, creând întrerupătoare de cozi de încredere și chiar adăugând gRPC la aplicațiile noastre.

Cu ajutorul comunităților PHP și Go, am îmbunătățit stabilitatea soluției, am crescut performanța aplicației de până la 40 de ori în unele teste, am îmbunătățit instrumentele de depanare, am implementat integrarea cu framework-ul Symfony și am adăugat suport pentru HTTPS, HTTP/2, pluginuri și PSR-17.

Concluzie

Unii oameni sunt încă prinși în noțiunea depășită a PHP ca un limbaj lent și greu de utilizat, numai bun pentru a scrie plugin-uri pentru WordPress. Acești oameni ar putea spune chiar că PHP are o astfel de limitare: atunci când aplicația devine suficient de mare, trebuie să alegi un limbaj mai „matură” și să rescrii baza de cod acumulată de-a lungul multor ani.

La toate acestea vreau să răspund: mai gândește-te. Credem că numai dvs. setați orice restricții pentru PHP. Îți poți petrece întreaga viață trecând de la o limbă la alta, încercând să găsești potrivirea perfectă pentru nevoile tale sau poți începe să te gândești la limbi ca instrumente. Presupusele defecte ale unui limbaj precum PHP ar putea fi de fapt motivul succesului său. Și dacă îl combinați cu o altă limbă, cum ar fi Go, atunci veți crea produse mult mai puternice decât dacă ați fi limitat să utilizați orice limbă.

După ce am lucrat cu o mulțime de Go și PHP, putem spune că le iubim. Nu intenționăm să sacrificăm unul pentru celălalt - dimpotrivă, vom căuta modalități de a obține și mai multă valoare din acest dual stack.

UPD: salutăm creatorul RoadRunner și co-autorul articolului original - Lachesis

Sursa: www.habr.com

Adauga un comentariu