
Salut, Habr! Noi, la Badoo, lucrăm activ , deoarece avem un sistem destul de mare construit pe acest limbaj, iar problema performanței este o chestiune de economisire a banilor. Cu peste zece ani în urmă, am creat PHP-FPM, care a fost la început un set de patch-uri pentru PHP, dar care mai târziu a fost inclus în livrarea oficială.
În ultimii ani, PHP a avansat semnificativ: colectorul de gunoi s-a îmbunătățit, stabilitatea a crescut — astăzi poți scrie demoni și scripturi de lungă durată pe PHP fără probleme mari. Acest lucru a permis lui Spiral Scout să meargă mai departe: RoadRunner, spre deosebire de PHP-FPM, nu curăță memoria între cereri, ceea ce oferă un câștig suplimentar în performanță (deși acest abordare complică procesul de dezvoltare). Acum experimentăm cu acest instrument, dar nu am obținut rezultate de împărtășit. Pentru a face așteptarea mai plăcută, publicăm traducerea anunțului RoadRunner de la Spiral Scout.
Abordarea din articol ne este familiară: atunci când ne rezolvăm sarcinile, folosim adesea combinația PHP și Go, obținând avantajele ambelor limbaje fără a renunța la unul în favoarea altuia.
Bucură-te!
În ultimii zece ani, am creat aplicații și pentru companii din lista , și pentru afaceri cu o audiență de maximum 500 de utilizatori. În tot acest timp, inginerii noștri au dezvoltat backend-ul predominant în PHP. Dar acum doi ani, un lucru a avut un impact semnificativ nu doar asupra performanței produselor noastre, ci și asupra scalabilității acestora — am introdus Golang (Go) în stiva noastră de tehnologii.
Aproape imediat, am observat că Go ne permite să creăm aplicații mai mari cu o creștere a performanței de până la 40 de ori. Cu ajutorul său, am reușit să extindem produsele existente, scrise în PHP, îmbunătățindu-le prin combinația avantajelor ambelor limbaje.
Vă vom povesti cum combinația Go și PHP ajută la rezolvarea problemelor reale de dezvoltare și cum a devenit pentru noi un instrument capabil să elimine o parte din problemele legate de .
Mediul dumneavoastră de dezvoltare PHP cotidiana
Înainte de a explica cum Go poate revitaliza modelul de «moarte» al PHP, să examinăm mediul dumneavoastră standard de dezvoltare PHP.
În cele mai multe cazuri, aplicația este rulată folosind o combinație de server web nginx și server PHP-FPM. Primul servește fișiere statice și redirecționează cererile specifice la PHP-FPM, care execută codul PHP. S-ar putea să folosiți o combinație mai puțin populară din Apache și mod_php. Însă, deși funcționează puțin diferit, principiile sunt aceleași.
Să vedem cum execută PHP-FPM codul aplicației. Când vine o cerere, PHP-FPM inițiază un proces PHP fiu, iar detaliile cererii sunt transmise ca parte din starea sa (_GET, _POST, _SERVER etc.).
Starea nu poate fi schimbată în timpul execuției scriptului PHP, așa că un nou set de date de intrare poate fi obținut doar printr-un singur mod: curățând memoria procesului și inițiindu-l din nou.
Această 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 celelalte procese. Dar acest lucru are și dezavantaje, care devin evidente atunci când se încearcă scalarea aplicației.
Dezavantajele și ineficiența unei medii PHP obișnuite
Dacă sunteți implicat în dezvoltarea profesională în PHP, știți cu ce trebuie să începeți un nou proiect - cu alegerea unui cadru. Acesta constă în biblioteci pentru injectarea dependențelor, ORM-uri, traduceri și șabloane. Și, desigur, toate datele de intrare ale utilizatorului pot fi ușor plasate într-un singur obiect (Symfony/HttpFoundation sau PSR-7). Cadrele sunt grozave!
Dar orice are un cost. În orice cadru de nivel enterprise, pentru a gestiona o cerere simplă de utilizator sau pentru a accesa baza 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 este că, după ce cada sarcină este finalizată, totul trebuie resetat și început din nou: tot codul pe care tocmai l-ați inițiat devine inutil, iar cu acesta nu veți putea procesa o altă cerere. Spuneți aceasta oricărui programator care scrie într-o altă limbă și veți vedea confuzia pe chipul lui.
Inginerii PHP au căutat ani de zile soluții pentru această problemă, utilizând metodologii gândite pentru „încărcarea leneșă”, micro-framework-uri, biblioteci optimizate, cache etc. Dar, în cele din urmă, tot trebuie să reseteze întreaga aplicație și să înceapă din nou, din nou și din nou. (Nota traducătorului: parțial, această problemă va fi rezolvată odată cu apariția în PHP 7.4)
Poate PHP să gestioneze mai mult de o cerere cu ajutorul Go?
Se pot scrie scripturi PHP care durează mai mult de câteva minute (chiar și ore sau zile): de exemplu, sarcini cron, parsere CSV, analizatori de cozi. Toate funcționează după același scenariu: extrag o sarcină, o execută, așteaptă următoarea. Codul rămâne constant în memorie, economisind prețioase milisecunde, deoarece pentru a încărca framework-ul și aplicația sunt necesare multe acțiuni suplimentare.
Dar a dezvolta scripturi care durează mult nu este ușor. Orice eroare distruge complet procesul, diagnosticarea scurgerilor de memorie este frustrantă, iar utilizarea depanării cu F5 nu mai este posibilă.
Situația s-a îmbunătățit odată cu lansarea PHP 7: a apărut un colector de gunoi fiabil, a devenit mai ușor să se gestioneze erorile, iar extensiile nucleului sunt acum protejate împotriva scurgerilor. Totuși, inginerii trebuie să fie încă precauți cu privire la memorie și să fie conștienți de problemele de stare din cod (există oare o limbă în care să nu fie nevoie să se acorde atenție acestor lucruri?). Și totuși, în PHP 7 ne așteaptă mai puține surprize.
Se poate lua modelul de lucru cu scripturi PHP de lungă durată, să fie adaptat pentru sarcini mai triviale, cum ar fi gestionarea cererilor HTTP și astfel să evităm necesitatea de a încărca totul de la zero la fiecare cerere?
Pentru a rezolva această problemă, a fost necesară implementarea unei aplicații server care să poată accepta cereri HTTP și să le redirecționeze una câte una către un worker PHP, fără a-l omorî de fiecare dată.
Știam că putem scrie un server web folosind PHP pur (PHP-PM) sau cu ajutorul unei extensii C (Swoole). Deși fiecare metodă are avantajele sale, ambele opțiuni nu ne-au satisfăcut – ne doream ceva mai mult. Aveam nevoie nu doar de un server web – ne așteptam să obținem o soluție capabilă să ne scape de problemele legate de „încărcarea grea” în PHP, care, în același timp, să fie ușor adaptabilă și extensibilă pentru aplicații specifice. Cu alte cuvinte, aveam nevoie de un server de aplicații.
Poate Go să ajute la asta? Știam că poate, pentru că acest limbaj compilează aplicațiile în fișiere binare unice; este multiplatformă; utilizează un model de procesare concurentă foarte elegant și o bibliotecă pentru lucrul cu HTTP; și, în cele din urmă, ne vor fi disponibile mii de biblioteci open-source și integrări.
Dificultăți în combinația a două limbi de programare
În primul rând, a trebuit să definim cum vor comunica între ele două sau mai multe aplicații.
De exemplu, prin a lui Alex Palæstra, era posibil să implementăm partajarea memoriei între procesele PHP și Go (analog cu mod_php în Apache). Dar această bibliotecă are particularități care îi limitează utilizarea pentru a rezolva problema noastră.
Am decis să folosim o altă abordare, mai răspândită: să construim interacțiunea între procese prin socket-uri / pipe-uri. Această abordare și-a dovedit fiabilitatea în ultimele decenii și a fost bine optimizată la nivelul sistemului de operare.
La început, am creat un protocol binar simplu pentru schimbul de date între procese și pentru gestionarea erorilor de transmitere. În forma sa cea mai simplă, un protocol de acest tip semănă cu de (în cazul nostru 17 octeți), care conține informații despre tipul pachetului, dimensiunea acestuia și o mască binară pentru verificarea integrității datelor.
Pe partea PHP, am folosit , iar pe partea Go – biblioteca.
Ne-a părut insuficient un singur protocol – și am adăugat posibilitatea de a apela. Asta ne-a ajutat 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 alt produs open-source de-al nostru.
Distribuția sarcinilor între mai mulți lucrători PHP
După implementarea mecanismului de interacțiune, ne-am gândit cum să transmită cel mai eficient sarcinile către procesele PHP. Când vine o sarcină, serverul de aplicații trebuie să aleagă un lucrător liber pentru a o executa. Dacă lucrătorul/procesul a terminat munca cu o eroare sau a „murit”, ne debarasăm de el și creăm unul nou în loc. Iar dacă lucrătorul/procesul a funcționat cu succes, îl returnăm în pool-ul lucrătorilor disponibili pentru executarea sarcinilor.

Pentru stocarea pool-ului de lucrători activi am folosit, pentru a elimina lucrarea „muribundă” din pool, am adăugat un mecanism de monitorizare a erorilor și a stării lucrătorilor.
Ca rezultat, am obținut un server PHP funcțional, capabil să proceseze orice cereri prezentate în format binar.
Pentru ca aplicația noastră să înceapă să funcționeze ca un server web, a fost necesar să alegem un standard PHP fiabil pentru reprezentarea tuturor cererilor HTTP de intrare. În cazul nostru, pur și simplu cererea net/http din Go în formatul, pentru a fi compatibil cu majoritatea framework-urilor PHP disponibile astăzi.
Deoarece PSR-7 este considerat nemodificabil (deși unii ar spune că tehnic nu este așa), dezvoltatorii trebuie să scrie aplicații care în principiu nu tratează cererea ca pe o entitate globală. Aceasta se potrivește perfect cu conceptul de procese PHP de lungă durată. Implementarea noastră finală, care nu a primit încă un nume, arăta astfel:

Vă prezentăm RoadRunner —
Prima noastră sarcină de testare a fost un backend API, care experimenta periodic vârfuri de cereri imprevizibile (de multe ori mai frecvente decât de obicei). Deși în cele mai multe cazuri capacitățile nginx erau suficiente, ne confruntam regulat cu eroarea 502, deoarece nu puteam echilibra sistemul suficient de repede pentru a face față creșterii așteptate a sarcinilor.
Pentru a înlocui această soluție, la începutul anului 2018 am desfășurat primul nostru server de aplicații PHP/Go. Și am obținut imediat un efect incredibil! Nu numai că ne-am debarasat complet de eroarea 502, dar am reușit să reducem numărul serverelor cu două treimi, economisind o grămadă de bani și pastile împotriva durerilor de cap pentru ingineri și manageri de produse.
La mijlocul anului, ne-am îmbunătățit soluția, am publicat-o pe GitHub sub licența MIT și am numit-o , subliniind astfel viteza și eficiența sa incredibilă.
Cum poate RoadRunner să îmbunătățească stiva dumneavoastră de dezvoltare
Aplicare ne-a permis să utilizăm Middleware net/http pe partea de Go, pentru a efectua verificarea JWT înainte ca solicitarea să ajungă în PHP, precum și pentru a gestiona WebSockets și a agrega global stările în Prometheus.
Datorită RPC-ului integrat, putem expune API-urile oricăror biblioteci Go pentru PHP fără a scrie extensii-wrapper. Mai important, cu ajutorul RoadRunner, putem desfășura noi servere diferite de HTTP. Exemplele includ lansarea handler-elor PHP, crearea de parseri de cozi fiabili și chiar adăugarea în aplicațiile noastre.
Cu ajutorul comunităților PHP și Go, am crescut stabilitatea soluției, în unele teste am crescut performanța aplicațiilor de până la 40 de ori, am îmbunătățit instrumentele de depanare, am realizat integrarea cu cadrul Symfony și am adăugat suport pentru HTTPS, HTTP/2, pluginuri și PSR-17.
Concluzie
Unii încă sunt prizonieri ai viziunii învechite despre PHP ca fiind un limbaj lent și bulky, potrivit doar pentru scrierea pluginurilor în WordPress. Acești oameni pot chiar să spună 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 anilor.
La toate acestea se impune un răspuns: gândește-te din nou. Credem că doar tu îți stabilești limitările pentru PHP. Poți petrece toată viața făcând tranziții de la un limbaj la altul, încercând să găsești combinația perfectă pentru nevoile tale, sau poți începe să percepi limbajele ca pe instrumente. Aparentele dezavantaje ale limbajului, precum PHP, pot fi de fapt cauzele succesului său. Iar dacă îl combini cu un alt limbaj, cum ar fi Go, vei crea produse mult mai puternice decât dacă te-ai limita la utilizarea unui singur limbaj.
După ce am lucrat cu combinația Go și PHP, putem spune că le-am îndrăgit. Nu intenționăm să sacrificăm unul în favoarea celuilalt - dimpotrivă, vom căuta modalități de a extrage și mai multe beneficii din această duplă stivă.
UPD: salutăm creatorul RoadRunner și coautorul articolului original -
Sursa: habr.com
