
Ciao, Habr! Noi di Badoo stiamo attivamente , poiché abbiamo un sistema abbastanza grande in questo linguaggio e la questione delle performance è una questione di risparmio. Più di dieci anni fa abbiamo creato PHP-FPM, che inizialmente era un insieme di patch per PHP, e successivamente è stato integrato nella versione ufficiale.
Negli ultimi anni PHP ha fatto grandi progressi: il garbage collector è migliorato, il livello di stabilità è aumentato: oggi è possibile scrivere demoni e script a lungo termine in PHP senza particolari problemi. Questo ha permesso a Spiral Scout di andare oltre: RoadRunner, a differenza di PHP-FPM, non libera la memoria tra le richieste, il che offre un ulteriore guadagno in termini di performance (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ù divertente, pubblichiamo la traduzione dell'annuncio di RoadRunner da Spiral Scout.
L'approccio dell'articolo ci è vicino: nella risoluzione delle nostre problematiche utilizziamo anch'esso la combinazione di PHP e Go, ottenendo i vantaggi di entrambi i linguaggi e non rinunciando a uno a favore dell'altro.
Buon divertimento!
Negli ultimi dieci anni abbiamo creato applicazioni sia per aziende della lista , sia per business con un pubblico di non più di 500 utenti. Durante tutto questo tempo i nostri ingegneri hanno sviluppato il backend prevalentemente in PHP. Ma due anni fa qualcosa ha influito notevolmente non solo sulle performance dei nostri prodotti, ma anche sulla loro scalabilità: abbiamo introdotto Golang (Go) nel nostro stack tecnologico.
Quasi immediatamente abbiamo scoperto che Go ci consente di creare applicazioni più grandi con un aumento delle performance fino a 40 volte. Grazie a questo, siamo stati in grado di espandere i prodotti esistenti scritti in PHP, migliorandoli grazie alla combinazione dei vantaggi di entrambi i linguaggi.
Vi racconteremo come la combinazione di Go e PHP aiuta a risolvere problemi reali di sviluppo e come è diventata per noi uno strumento capace di alleviare alcune problematiche legate alla .
Il vostro ambiente quotidiano di sviluppo PHP
Prima di raccontare come utilizzare Go per rivitalizzare il modello "morente" di PHP, diamo un'occhiata al vostro standard ambiente di sviluppo PHP.
Nella maggior parte dei casi, avvii l'applicazione utilizzando una combinazione di server web nginx e server PHP-FPM. Il primo gestisce i file statici e reindirizza a PHP-FPM le richieste specifiche, mentre PHP-FPM esegue il codice PHP. È possibile che tu stia utilizzando una combinazione meno popolare di Apache e mod_php. Anche se funziona in modo leggermente diverso, i principi rimangono gli stessi.
Consideriamo 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, quindi un nuovo set di dati in ingresso può essere ottenuto solo in un modo: liberando la memoria del processo e inizializzandolo nuovamente.
Questo modello di esecuzione ha molti vantaggi. Non devi preoccuparti eccessivamente del consumo di memoria, tutti i processi sono completamente isolati e se uno di essi "muore", verrà ricreato automaticamente e ciò non influenzerà gli altri processi. Ma ci sono anche svantaggi in questo approccio, che si manifestano quando si cerca di scalare l'applicazione.
Svantaggi e inefficienze dell'ambiente PHP tradizionale
Se ti occupi di sviluppo professionale in PHP, sai da dove iniziare un nuovo progetto: dalla scelta di un framework. Questo rappresenta librerie per l'iniezione delle dipendenze, ORM, traduzioni e modelli. E, naturalmente, tutti i dati di input dell'utente possono essere comodamente inseriti in un unico oggetto (Symfony/HttpFoundation o PSR-7). I framework sono fantastici!
Ma tutto ha il suo prezzo. In qualsiasi framework di livello enterprise, per gestire una semplice richiesta dell'utente o una chiamata al database, dovrai caricare almeno decine di file, creare numerose classi e analizzare diverse configurazioni. Ma la cosa peggiore è che dopo aver eseguito ogni compito, dovrai azzerare tutto e ricominciare: tutto il codice che hai appena inizializzato diventa inutile, poiché non potrai elaborare un'altra richiesta con esso. Dillo a qualsiasi programmatore che scrive in un altro linguaggio e vedrai un'espressione di incredulità sul suo volto.
Gli ingegneri PHP hanno cercato per anni modi per risolvere questo problema, utilizzando tecniche ben congegnate di "lazy loading", microframework, librerie ottimizzate, cache, ecc. Ma alla fine, si finisce comunque per riavviare l'intera applicazione e ricominciare da capo, ancora e ancora. (Nota del traduttore: questa problematica sarà parzialmente risolta con l'arrivo di PHP 7.4)
PHP può gestire più di una richiesta alla volta grazie a Go?
È possibile scrivere script PHP che durano più di qualche minuto (fino a ore o giorni): ad esempio, attività cron, parser CSV, gestori di queue. Tutti seguono lo stesso schema: estraggono un compito, lo eseguono, poi aspettano il successivo. Il codice rimane in memoria, risparmiando preziosi millisecondi, poiché richiede molte operazioni aggiuntive per caricare il framework e l'applicazione.
Tuttavia, sviluppare script a lungo termine non è così semplice. Qualsiasi errore interrompe completamente il processo, diagnosticare le perdite di memoria è frustrante e non si può più utilizzare il debug con F5.
La situazione è migliorata con l'uscita di PHP 7: è stato introdotto un affidabile garbage collector, la gestione degli errori è diventata più facile e le estensioni del core sono ora protette dalle perdite di memoria. Tuttavia, gli ingegneri devono ancora prestare attenzione alla memoria e tenere presente i problemi di stato nel codice (c'è un linguaggio in cui non si devono considerare questi aspetti?). Eppure, in PHP 7 ci sono meno sorprese.
È possibile prendere il modello di lavoro con script PHP a lungo termine, adattarlo a compiti più banali come la gestione delle richieste HTTP e così liberarsi dalla necessità di caricare tutto da zero ad ogni richiesta?
Per affrontare questa sfida, era necessario implementare un'applicazione server in grado di ricevere richieste HTTP e reindirizzarle una ad una a un worker PHP, senza interrompere il processo ogni volta.
Sapevamo di poter scrivere un web server in PHP puro (PHP-PM) oppure utilizzando un'estensione C (Swoole). Sebbene entrambi i metodi avessero i loro vantaggi, non ci soddisfacevano — volevamo qualcosa di più. Avevamo bisogno non solo di un web server, ma di una soluzione in grado di liberarci dai problemi legati al "lento avvio" in PHP, che potesse essere facilmente adattata ed espansa per applicazioni specifiche. In altre parole, avevamo bisogno di un server applicativo.
Il Go può aiutare in questo? Sapevamo di sì, perché questo linguaggio compila le applicazioni in singoli file binari; è multipiattaforma; utilizza un modello di concorrenza molto elegante e una libreria per la gestione di HTTP; e infine, avremo accesso a migliaia di librerie open-source e integrazioni.
Difficoltà nell'unire due linguaggi di programmazione
In primo luogo, era necessario definire come due o più applicazioni avrebbero comunicato tra loro.
Ad esempio, utilizzando di Alex Paléstras, era possibile realizzare la condivisione della memoria tra i processi PHP e Go (simile a mod_php in Apache). Tuttavia, questa libreria ha delle peculiarità che limitano il suo utilizzo per il nostro scopo.
Abbiamo deciso di adottare un approccio diverso e più comune: costruire l'interazione tra i processi tramite socket/pipeline. Questo approccio ha dimostrato la sua affidabilità negli ultimi decenni ed è stato ben ottimizzato a livello di sistema operativo.
Per iniziare, abbiamo creato un protocollo binario semplice per lo scambio di dati tra i processi e la gestione degli errori di trasmissione. Nella sua forma più semplice, un protocollo di questo tipo assomiglia a con (nel nostro caso 17 byte), che contiene informazioni sul tipo di pacchetto, la sua dimensione e una maschera binaria per il controllo dell'integrità dei dati.
Dal lato PHP abbiamo utilizzato , e dal lato Go — la libreria.
Ci siamo resi conto che un solo protocollo non bastava — e abbiamo aggiunto la possibilità di chiamare. In seguito, questo ci ha molto aiutato nello sviluppo, poiché potevamo integrare facilmente le librerie Go nelle applicazioni PHP. Il risultato di questo lavoro può essere visto, ad esempio, in un altro nostro prodotto open-source.
Distribuzione dei compiti tra più worker PHP
Dopo aver implementato il meccanismo di interazione, abbiamo iniziato a pensare a come trasmettere le attività ai processi PHP nel modo più efficiente possibile. Quando arriva un compito, il server delle applicazioni deve selezionare un worker libero per eseguirlo. Se il worker/processo ha terminato il lavoro con un errore o è "morto", lo eliminiamo e creiamo un nuovo worker in sostituzione. Se invece il worker/processo ha completato con successo il suo lavoro, lo restituiamo al pool di worker disponibili per l'esecuzione dei compiti.

Per memorizzare il pool di worker attivi abbiamo utilizzato, per rimuovere in maniera inattesa i worker "morti" dal pool, abbiamo aggiunto un meccanismo di monitoraggio degli errori e degli stati dei worker.
Di conseguenza, abbiamo ottenuto un server PHP operativo in grado di gestire qualsiasi richiesta presentata in forma binaria.
Affinché la nostra applicazione inizi a funzionare come un server web, è stato necessario scegliere uno standard PHP affidabile per rappresentare qualsiasi richiesta HTTP in ingresso. Nel nostro caso, noi semplicemente la richiesta net/http da Go nel formato, in modo che sia compatibile con la maggior parte dei framework PHP disponibili oggi.
Poiché PSR-7 è considerato immutabile (alcuni diranno che tecnicamente non è così), gli sviluppatori devono scrivere applicazioni che in linea di principio non trattano la richiesta come un'entità globale. Questo si sposa perfettamente con il concetto di processi PHP di lunga vita. La nostra implementazione finale, che non ha ancora ricevuto un nome, era così:

Presentiamo RoadRunner —
Il nostro primo compito di test è stato un backend API, dove si verificavano periodicamente picchi imprevisti di richieste (molto più frequentemente del normale). Anche se nella maggior parte dei casi le capacità di nginx erano sufficienti, ci siamo regolarmente imbattuti nell'errore 502, poiché non riuscivamo a bilanciare il sistema abbastanza velocemente per far fronte all'aumento previsto del carico.
Per sostituire questa soluzione, all'inizio del 2018 abbiamo lanciato il nostro primo server di applicazioni PHP/Go. E abbiamo subito ottenuto un effetto incredibile! Non solo abbiamo completamente eliminato l'errore 502, ma siamo anche riusciti a ridurre di due terzi il numero di server, risparmiando un sacco di soldi e alleviando il mal di testa per ingegneri e manager di prodotto.
A metà dell'anno abbiamo perfezionato la nostra soluzione, pubblicandola su GitHub con licenza MIT e chiamandola , sottolineando così la sua incredibile velocità ed efficienza.
Come RoadRunner può migliorare il tuo stack di sviluppo
Applicazione ci ha permesso di utilizzare Middleware net/http dalla parte di Go, per eseguire la verifica JWT prima che la richiesta arrivi a PHP, così come per gestire WebSocket e aggregare globalmente stati in Prometheus.
Grazie al RPC integrato, è possibile aprire API di qualsiasi libreria Go per PHP senza scrivere estensioni di wrapping. Ciò che è ancora più importante, con RoadRunner è possibile distribuire nuovi server che differiscono da HTTP. Esempi includono l'avvio di gestori in PHP, la creazione di robusti parser di code e persino l'aggiunta alle nostre applicazioni.
Con l'aiuto delle comunità PHP e Go, abbiamo aumentato la stabilità della soluzione, in alcuni test migliorando le prestazioni delle applicazioni fino a 40 volte, perfezionato gli strumenti di debug, implementato l'integrazione con il framework Symfony e aggiunto il supporto per HTTPS, HTTP/2, plugin e PSR-17.
Conclusione
Alcuni sono ancora prigionieri di un'idea obsoleta di PHP come un linguaggio lento e ingombrante, adatto solo per scrivere plugin per WordPress. Queste persone potrebbero persino dire che PHP ha un tale limite: quando l'applicazione diventa abbastanza grande, è necessario scegliere un linguaggio più "maturo" e riscrivere il codice accumulato in molti anni.
A tutto ciò vogliamo rispondere: ripensateci. Riteniamo che siano solo voi stessi a porre alcune limitazioni a PHP. Potete spendere tutta la vita a passare da un linguaggio all'altro, cercando di trovare la combinazione ideale con le vostre esigenze, oppure potete iniziare a considerare i linguaggi come strumenti. I presunti difetti di un linguaggio come PHP possono in realtà essere le ragioni del suo successo. E se lo unite a un altro linguaggio come Go, creerete prodotti molto più potenti che se vi limitaste a usare un solo linguaggio.
Dopo aver lavorato congiuntamente con Go e PHP, possiamo affermare di amarli. Non abbiamo intenzione di sacrificare uno a favore dell'altro: al contrario, cercheremo modi per estrarre ancora più benefici da questo doppio stack.
UPD: diamo il benvenuto al creatore di RoadRunner e co-autore dell'articolo originale —
Fonte: habr.com
