RoadRunner: PHP non è stato creato per morire, oppure Golang corre in aiuto

RoadRunner: PHP non è stato creato per morire, oppure Golang corre in aiuto

Ciao, Habr! In Badoo stiamo attivamente lavorando sulle performance di PHP, dato che abbiamo un sistema piuttosto grande su questo linguaggio e il tema delle performance è una questione di risparmio. Più di dieci anni fa abbiamo creato PHP-FPM, che inizialmente era un insieme di patch per PHP, ma successivamente è stato incluso nella distribuzione ufficiale.

Negli ultimi anni PHP ha fatto grandi progressi: il garbage collector è migliorato, la stabilità è aumentata — oggi è possibile scrivere demoni e script a lungo termine senza troppi problemi. Questo ha permesso a Spiral Scout di andare oltre: RoadRunner, a differenza di PHP-FPM, non libera la memoria tra le richieste, offrendo quindi un guadagno ulteriore in termini di prestazioni (anche se questo approccio complica il processo di sviluppo). Attualmente stiamo sperimentando con questo strumento, ma non abbiamo ancora risultati da condividere. Per rendere l'attesa più interessante, pubblichiamo la traduzione dell'annuncio di RoadRunner da Spiral Scout.

L'approccio descritto nell'articolo ci è vicino: per risolvere i nostri problemi utilizziamo spesso la combinazione di PHP e Go, ottenendo vantaggi da entrambe le lingue senza dover rinunciare a una in favore dell'altra.

Buon divertimento!

Negli ultimi dieci anni abbiamo sviluppato applicazioni sia per aziende della lista Fortune 500, sia per attività con un pubblico di non più di 500 utenti. Durante tutto questo tempo i nostri ingegneri hanno principalmente sviluppato il backend in PHP. Ma due anni fa qualcosa ha avuto un impatto significativo, non solo sulle performance dei nostri prodotti, ma anche sulla loro scalabilità: abbiamo introdotto Golang (Go) nel nostro stack tecnologico.

Quasi subito abbiamo scoperto che Go ci permette di creare applicazioni più grandi con un aumento delle performance fino a 40 volte. Grazie a esso, siamo riusciti a espandere i prodotti esistenti scritti in PHP, migliorandoli grazie alla combinazione dei vantaggi di entrambi i linguaggi.

Vi parleremo di come la combinazione di Go e PHP aiuta a risolvere problemi reali di sviluppo e come sia diventata per noi uno strumento in grado di alleviare alcune problematiche legate a un modello di "morte" di PHP.

Il vostro ambiente quotidiano di sviluppo PHP

Prima di discutere come Go può rivitalizzare il modello di "morte" di PHP, daremo un'occhiata al vostro standard ambiente di sviluppo PHP.

Nella maggior parte dei casi, eseguite l’applicazione utilizzando una combinazione di server web nginx e il server PHP-FPM. Il primo gestisce i file statici e reindirizza le richieste specifiche a PHP-FPM, mentre quest'ultimo esegue il codice PHP. Potreste utilizzare una combinazione meno popolare di Apache e mod_php. Anche se funziona in modo leggermente diverso, i principi sono gli stessi.

Esaminiamo come PHP-FPM esegue il codice dell'applicazione. Quando arriva una richiesta, PHP-FPM inizializza un processo PHP secondario e passa i dettagli della richiesta come parte del suo stato (_GET, _POST, _SERVER, ecc.).

Lo stato non può cambiare durante l'esecuzione dello script PHP, pertanto è possibile ottenere un nuovo set di dati di input solo in un modo: liberando la memoria del processo e inizializzandolo nuovamente.

Questo modello di esecuzione ha molti vantaggi. Non dovete preoccuparvi eccessivamente del consumo di memoria, tutti i processi sono completamente isolati e se uno di essi "muore", verrà ricreato automaticamente, senza influenzare gli altri processi. Tuttavia, questa metodologia ha anche degli svantaggi, che emergono nel tentativo di scalare l'applicazione.

Svantaggi e inefficienza dell'ambiente PHP standard

Se siete impegnati nello sviluppo professionale con PHP, saprete da dove iniziare un nuovo progetto: dalla scelta del framework. Questo rappresenta librerie per la gestione delle dipendenze, ORM, traduzioni e template. E naturalmente, tutti i dati di input degli utenti possono essere comodamente racchiusi in un singolo oggetto (Symfony/HttpFoundation o PSR-7). I framework sono fantastici!

Ma tutto ha un prezzo. In qualsiasi framework di livello enterprise, per gestire una semplice richiesta dell'utente o una chiamata al database, dovrete caricare almeno una dozzina di file, creare molteplici classi e analizzare varie configurazioni. Ma la cosa peggiore è che dopo ogni compito completato, dovrete azzerare tutto e ricominciare: tutto il codice che avete appena inizializzato diventa inutile, e non potete più gestire un'altra richiesta con esso. Racconta questo a qualsiasi programmatore che utilizza un altro linguaggio, e vedrete stupore sul suo volto.

Gli ingegneri PHP hanno cercato per anni di trovare soluzioni a questo problema, utilizzando metodologie ben ponderate di "lazy" loading, microframework, librerie ottimizzate, caching, ecc. Ma alla fine ci si trova sempre a dover azzerare l'intera applicazione e a ricominciare da capo, di nuovo e di nuovo. (Nota del traduttore: parte di questo problema sarà risolta con l'arrivo di preload PHP 7.4)

PHP può gestire più di una richiesta con Go?

È possibile scrivere script PHP che vivono più di pochi minuti (fino a ore o giorni): ad esempio, compiti cron, parser CSV, gestori di code. Tutti seguono uno stesso schema: estraggono il compito, lo eseguono, aspettano il successivo. Il codice rimane costantemente in memoria, risparmiando preziosi millisecondi poiché il caricamento del framework e dell'applicazione richiede numerosi passaggi aggiuntivi.

Tuttavia, sviluppare script a lungo termine non è così semplice. Qualsiasi errore uccide completamente il processo, diagnosticare perdite di memoria è frustrante e non è più possibile utilizzare il debug con F5.

La situazione è migliorata con l'uscita di PHP 7: è stato introdotto un collettore di rifiuti affidabile, è più facile gestire gli errori e le estensioni del core ora sono protette dalle perdite. Tuttavia, gli ingegneri devono ancora trattare la memoria con cautela e tenere a mente i problemi di stato nel codice (esiste un linguaggio dove non è necessario prestare attenzione a queste cose?). Eppure, in PHP 7 ci sono meno sorprese.

È possibile prendere il modello di funzionamento degli script PHP a lungo termine, adattarlo a compiti più banali come l'elaborazione di richieste HTTP e, in tal modo, eliminare la necessità di caricare tutto da zero ad ogni richiesta?

Per affrontare questo compito, era necessario implementare un'applicazione server in grado di ricevere richieste HTTP e reindirizzarle una dopo l'altra a un lavoratore PHP, senza ucciderlo ogni volta.

Sapevamo di poter scrivere un server web in puro PHP (PHP-PM) o utilizzando un'estensione C (Swoole). Sebbene entrambe le soluzioni avessero i loro vantaggi, nessuna delle due ci soddisfaceva completamente: volevamo qualcosa di più. Non avevamo bisogno solo di un server web, ma cercavamo una soluzione capace di liberaci dai problemi di 'caricamento pesante' in PHP, facilmente adattabile e scalabile per applicazioni specifiche. Quindi, avevamo bisogno di un server di applicazioni.

Go può aiutare in questo? Sapevamo di sì, perché questo linguaggio compila le applicazioni in singoli file binari; è multipiattaforma; utilizza un elegante modello di concorrenza e una libreria per lavorare con HTTP; e, infine, avremmo avuto accesso a migliaia di librerie open-source e integrazioni.

Difficoltà nell'integrare due linguaggi di programmazione

Prima di tutto, dovevamo definire come sarebbero comunicati due o più applicazioni tra loro.

Ad esempio, utilizzando una fantastica libreria di Alex Paleytras si potrebbe realizzare una condivisione di memoria tra processi PHP e Go (simile a mod_php in Apache). Ma questa libreria ha delle limitazioni che ne restringono l'applicabilità alla nostra soluzione.

Abbiamo deciso di utilizzare un approccio diverso, più comune: costruire l'interazione tra i processi attraverso socket/pipeline. Questo approccio ha dimostrato la sua affidabilità e ottimizzazione a livello di sistema operativo negli ultimi decenni.

Per iniziare, abbiamo creato un protocollo binario semplice per lo scambio di dati tra processi e la gestione degli errori di trasmissione. Nella sua forma più semplice, un protocollo di questo tipo assomiglia a netstring con un pacchetto di intestazione di dimensioni fisse (nel nostro caso 17 byte), che contiene informazioni sul tipo di pacchetto, le sue dimensioni e una maschera binaria per il controllo dell'integrità dei dati.

Dalla parte PHP abbiamo utilizzato la funzione pack, mentre dalla parte Go abbiamo utilizzato la libreria encoding/binary.

Un solo protocollo ci è sembrato troppo limitante — e abbiamo aggiunto la possibilità di invocare servizi Go net/rpc direttamente da PHP.In seguito, questo ci ha molto aiutato nello sviluppo poiché abbiamo potuto integrare facilmente le librerie Go nelle applicazioni PHP. Il risultato di questo lavoro è visibile, ad esempio, nel nostro altro prodotto open-source Goridge.

Distribuzione dei compiti tra diversi lavoratori PHP

Dopo aver implementato il meccanismo di interazione, abbiamo iniziato a pensare a come trasmettere compiti ai processi PHP in modo più efficiente. Quando arriva un compito, il server delle applicazioni deve scegliere un lavoratore libero per eseguirlo. Se il lavoratore/processo ha terminato il lavoro con un errore o è 'morto', lo eliminiamo e ne creiamo uno nuovo in cambio. E se il lavoratore/processo è stato completato con successo, lo restituiamo al pool di lavoratori disponibili per eseguire compiti.

RoadRunner: PHP non è stato creato per morire, oppure Golang corre in aiuto

Per memorizzare il pool di lavoratori attivi abbiamo utilizzato un canale bufferizzato, per rimuovere dal pool i lavoratori 'morti' inaspettatamente, abbiamo aggiunto un meccanismo per monitorare errori e stati dei lavoratori.

Di conseguenza, abbiamo ottenuto un server PHP funzionante in grado di gestire qualsiasi richiesta presentata in formato binario.

Affinché la nostra applicazione iniziasse a funzionare come server web, è stato necessario scegliere uno standard PHP affidabile per la rappresentazione di qualsiasi richiesta HTTP in entrata. Nel nostro caso, semplicemente convertiamo net/http request da Go in formato PSR-7, in modo che sia compatibile con la maggior parte dei framework PHP disponibili oggi.

Poiché PSR-7 è considerato immutabile (qualcuno dirà che tecnicamente non è così), agli sviluppatori è richiesto di scrivere applicazioni che in linea di principio non trattano la richiesta come un'entità globale. Questo si abbina perfettamente al concetto di processi PHP a lungo termine. La nostra implementazione finale, che non ha ancora ricevuto un nome, era così:

RoadRunner: PHP non è stato creato per morire, oppure Golang corre in aiuto

Presentiamo RoadRunner — un server PHP ad alte prestazioni

Il nostro primo compito di prova è stato un backend API, in cui si sono verificati picchi di richieste in modo imprevedibile (molto più frequentemente del solito). Anche se nella maggior parte dei casi le capacità di nginx erano sufficienti, ci siamo frequentemente imbattuti nell'errore 502, perché non riuscivamo a bilanciare il sistema abbastanza rapidamente per supportare l'aumento previsto del carico.

Per sostituire questa soluzione, all'inizio del 2018 abbiamo distribuito il nostro primo server PHP/Go per applicazioni. E subito abbiamo ottenuto un effetto straordinario! Non solo ci siamo liberati completamente dell'errore 502, ma siamo anche riusciti a ridurre il numero di server di due terzi, risparmiando un sacco di soldi e riducendo la necessità di antidolorifici per ingegneri e manager di prodotto.

Entro la metà dell'anno abbiamo perfezionato la nostra soluzione, l'abbiamo pubblicata su GitHub con licenza MIT e l'abbiamo chiamata RoadRunner, sottolineando così la sua incredibile velocità ed efficienza.

Come RoadRunner può migliorare il tuo stack di sviluppo

Applicazione RoadRunner ci ha permesso di utilizzare Middleware net/http sul lato Go per effettuare la verifica JWT prima che la richiesta raggiunga PHP, oltre a gestire i WebSocket e aggregare globalmente gli stati in Prometheus.

Grazie al RPC integrato, è possibile aprire API di qualsiasi libreria Go per PHP senza dover scrivere wrapper di estensione. Anche più importante, con RoadRunner è possibile distribuire nuovi server che differiscono da HTTP. Esempi di ciò includono l'esecuzione di handler in PHP, la creazione di parser di code affidabili e persino l'aggiunta AWS Lambdaalle nostre applicazioni. gRPC Con il supporto delle comunità PHP e Go, abbiamo aumentato la stabilità della soluzione, in alcuni test abbiamo aumentato le prestazioni delle applicazioni fino a 40 volte, abbiamo perfezionato gli strumenti di debug, implementato l'integrazione con il framework Symfony e aggiunto supporto per HTTPS, HTTP/2, plugin e PSR-17.

Alcuni sono ancora prigionieri di una visione obsoleta di PHP come un linguaggio lento e ingombrante, adatto solo per la scrittura di plugin per WordPress. Queste persone potrebbero persino dire che PHP ha un limite: quando l'applicazione diventa abbastanza grande, è necessario scegliere un linguaggio più 'maturo' e riscrivere il codice accumulato nel corso degli anni.

Conclusione

A tutto ciò vorrei rispondere: ripensaci. Crediamo che solo tu stessi imponendo limiti a PHP. Puoi trascorrere tutta la vita a passare da un linguaggio all'altro cercando la combinazione ideale per le tue esigenze, oppure puoi iniziare a considerare i linguaggi come strumenti. Apparenti svantaggi di un linguaggio come PHP possono in realtà essere le ragioni del suo successo. E se lo unisci a un altro linguaggio come Go, creerai prodotti molto più potenti di quanto faresti limitandoti a utilizzare un solo linguaggio.

Dopo aver lavorato con la combinazione di Go e PHP, possiamo affermare di amarli. Non abbiamo intenzione di sacrificare uno per l'altro: al contrario, cercheremo modi per trarre ancor più vantaggio da questo stack doppio.

UPD: diamo il benvenuto al creatore di RoadRunner e coautore dell'articolo originale —

Lachezis Ciao, Habr! Noi di Badoo lavoriamo attivamente per migliorare le prestazioni di PHP, poiché abbiamo un sistema abbastanza grande in questo linguaggio e la questione delle prestazioni è una questione di risparmio. Oltre dieci anni fa abbiamo creato PHP-FPM, che inizialmente era un insieme di patch per PHP, e in seguito è stato incluso nella distribuzione ufficiale. Negli ultimi anni, PHP è molto cambiato.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS 🔥 Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS | ProHoster