RoadRunner: PHP non è costruito per morire, o Golang per venire in soccorso

RoadRunner: PHP non è costruito per morire, o Golang per venire in soccorso

Ciao, Habr! Siamo attivi su Badoo lavorando sulle prestazioni di PHP, poiché disponiamo di un sistema abbastanza ampio in questa lingua e la questione delle prestazioni è una questione di risparmio di denaro. Più di dieci anni fa abbiamo creato per questo PHP-FPM, che inizialmente era un insieme di patch per PHP e in seguito è diventato parte della distribuzione ufficiale.

Negli ultimi anni PHP ha fatto grandi progressi: il garbage collector è migliorato, il livello di stabilità è aumentato: oggi puoi scrivere demoni e script di lunga durata in PHP senza problemi. Ciò ha permesso a Spiral Scout di andare oltre: RoadRunner, a differenza di PHP-FPM, non ripulisce la memoria tra le richieste, il che offre ulteriori vantaggi in termini di prestazioni (sebbene questo approccio complichi il processo di sviluppo). Stiamo attualmente sperimentando questo strumento, ma non abbiamo ancora risultati da condividere. Per rendere più divertente aspettarli, Stiamo pubblicando una traduzione dell'annuncio di RoadRunner da Spiral Scout.

L'approccio dell'articolo ci è vicino: quando risolviamo i nostri problemi, utilizziamo molto spesso anche una combinazione di PHP e Go, ottenendo i vantaggi di entrambi i linguaggi e non rinunciando all'uno a favore dell'altro.

Buon divertimento!

Negli ultimi dieci anni abbiamo creato applicazioni per le aziende dall'elenco Fortune 500e per le aziende con un pubblico non superiore a 500 utenti. Per tutto questo tempo, i nostri ingegneri hanno sviluppato il backend principalmente in PHP. Ma due anni fa, qualcosa ha avuto un grande impatto non solo sulle prestazioni dei nostri prodotti, ma anche sulla loro scalabilità: abbiamo introdotto Golang (Vai) nel nostro stack tecnologico.

Quasi immediatamente abbiamo scoperto che Go ci permetteva di creare applicazioni più grandi con prestazioni fino a 40 volte più veloci. Con esso siamo stati in grado di espandere i prodotti esistenti scritti in PHP, migliorandoli combinando i vantaggi di entrambi i linguaggi.

Ti racconteremo come una combinazione di Go e PHP aiuta a risolvere problemi reali di sviluppo e come si è trasformato per noi in uno strumento in grado di eliminare alcuni dei problemi associati a Modello PHP morente.

Il tuo ambiente di sviluppo PHP quotidiano

Prima di mostrarti come utilizzare Go per far rivivere il modello morente di PHP, diamo un'occhiata al tuo ambiente di sviluppo PHP standard.

Nella maggior parte dei casi, esegui l'applicazione utilizzando una combinazione di server web nginx e server PHP-FPM. Il primo serve file statici e reindirizza richieste specifiche a PHP-FPM, mentre PHP-FPM stesso esegue il codice PHP. Forse stai utilizzando una combinazione meno popolare di Apache e mod_php. Ma anche se funziona in modo leggermente diverso, i principi sono gli stessi.

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

Lo stato non può cambiare durante l'esecuzione di uno script PHP, quindi c'è solo un modo per ottenere un nuovo set di dati di input: cancellando la memoria del processo e reinizializzandola.

Questo modello di esecuzione presenta numerosi vantaggi. Non devi preoccuparti molto del consumo di memoria, tutti i processi sono completamente isolati e se uno di essi muore, verrà ricreato automaticamente senza influenzare il resto dei processi. Ma questo approccio presenta anche degli svantaggi che compaiono quando si tenta di ridimensionare l’applicazione.

Svantaggi e inefficienze di un normale ambiente PHP

Se sei impegnato nello sviluppo professionale in PHP, allora sai dove iniziare un nuovo progetto, scegliendo un framework. È costituito da librerie per l'inserimento delle dipendenze, ORM, traduzioni e modelli. E, naturalmente, tutti gli 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 aziendale, per elaborare una semplice richiesta di un utente o accedere a un database, dovrai caricare almeno decine di file, creare numerose classi e analizzare diverse configurazioni. Ma la cosa peggiore è che dopo aver completato ogni attività dovrai resettare tutto e ricominciare da capo: tutto il codice che hai appena avviato diventa inutile, con il suo aiuto non elaborerai più un'altra richiesta. Dillo a qualsiasi programmatore che scriva in qualsiasi altra lingua e vedrai lo sconcerto sul suo volto.

Gli ingegneri PHP hanno trascorso anni alla ricerca di modi per risolvere questo problema, utilizzando tecniche intelligenti di caricamento lento, microframework, librerie ottimizzate, cache, ecc. Ma alla fine, devi comunque reimpostare l'intera applicazione e ricominciare da capo, ancora e ancora. (Nota del traduttore: questo problema sarà parzialmente risolto con l'avvento di precarico in PHP 7.4)

PHP con Go può sopravvivere a più di una richiesta?

È possibile scrivere script PHP che dureranno più di qualche minuto (fino a ore o giorni): ad esempio, attività cron, parser CSV, code buster. Funzionano tutti secondo lo stesso scenario: recuperano un'attività, la eseguono e aspettano quella successiva. Il codice risiede in memoria, risparmiando preziosi millisecondi poiché sono necessari molti passaggi aggiuntivi per caricare il framework e l'applicazione.

Ma sviluppare script di lunga durata non è così facile. Qualsiasi errore uccide completamente il processo, diagnosticare perdite di memoria ti fa impazzire e non puoi più utilizzare il debug F5.

La situazione è migliorata con il rilascio di PHP 7: è apparso un garbage collector affidabile, è diventato più facile gestire gli errori e le estensioni del kernel sono ora protette dalle perdite. È vero, gli ingegneri devono comunque stare attenti con la memoria ed essere consapevoli dei problemi di stato nel codice (esiste un linguaggio in cui non dobbiamo preoccuparci di queste cose?). Eppure, in PHP 7 ci aspettano meno sorprese.

È possibile prendere il modello di lavoro con script PHP di lunga durata, adattarlo a compiti più banali come l'elaborazione delle richieste HTTP ed eliminare così la necessità di caricare tutto da zero per ogni richiesta?

Per risolvere questo problema, dovevamo prima implementare un'applicazione server in grado di accettare richieste HTTP e inoltrarle una per una al PHP Worker senza ucciderlo ogni volta.

Sapevamo che avremmo potuto scrivere un server web in puro PHP (PHP-PM) o utilizzando l'estensione C (Swoole). E sebbene ogni metodo abbia i suoi vantaggi, entrambe le opzioni non erano adatte a noi: volevamo qualcosa di più. Avevamo bisogno di più di un semplice server web: speravamo di ottenere una soluzione che potesse salvarci dai problemi associati all'"hard start" in PHP e che allo stesso tempo potesse essere facilmente adattata ed espansa per applicazioni specifiche. Cioè, avevamo bisogno di un server delle applicazioni.

Go può aiutarmi? Sapevamo che poteva farlo perché il linguaggio compila le applicazioni in singoli binari; è multipiattaforma; utilizza il proprio modello di elaborazione parallela (concorrenza) e una libreria propri, molto eleganti, per lavorare con HTTP; e infine, avremo a disposizione migliaia di librerie e integrazioni open source.

Difficoltà nel combinare due linguaggi di programmazione

Il primo passo è stato determinare come due o più applicazioni avrebbero comunicato tra loro.

Ad esempio, utilizzando meravigliosa biblioteca Alex Palaestras potrebbe implementare la condivisione della memoria tra i processi PHP e Go (simile a mod_php in Apache). Ma questa libreria ha caratteristiche che ne limitano l'utilizzo per risolvere il nostro problema.

Abbiamo deciso di utilizzare un altro approccio, più comune: creare interazione tra processi tramite socket/pipeline. Questo approccio ha dimostrato la sua affidabilità negli ultimi decenni ed è stato ben ottimizzato a livello di sistema operativo.

Per cominciare, abbiamo creato un semplice protocollo binario per lo scambio di dati tra processi e la gestione degli errori di trasmissione. Nella sua forma più semplice, questo tipo di protocollo è simile a stringa di rete с intestazione del pacchetto di dimensioni fisse (nel nostro caso 17 byte), che contiene informazioni sul tipo di pacchetto, sulla sua dimensione e una maschera binaria per verificare l'integrità dei dati.

Sul lato PHP abbiamo utilizzato funzione di confezionee, sul lato Go, una biblioteca codifica/binario.

Ci è sembrato che un protocollo non fosse sufficiente, quindi abbiamo aggiunto la possibilità di chiamare Vai a services net/rpc direttamente da PHP. Questo in seguito ci ha aiutato molto nello sviluppo, poiché abbiamo potuto facilmente integrare le librerie Go nelle applicazioni PHP. Il risultato di questo lavoro può essere visto, ad esempio, nel nostro altro prodotto open source Goridge.

Distribuire le attività tra più PHP Worker

Dopo aver implementato il meccanismo di interazione, abbiamo iniziato a pensare a come trasferire le attività ai processi PHP nel modo più efficiente. Quando arriva un'attività, il server delle applicazioni deve selezionare un lavoratore libero per completarla. Se un lavoratore/processo termina con un errore o “muore”, ce ne liberiamo e ne creiamo uno nuovo per sostituirlo. E se il lavoratore/processo è stato completato con successo, lo restituiamo al pool di lavoratori disponibili per eseguire le attività.

RoadRunner: PHP non è costruito per morire, o Golang per venire in soccorso

Per archiviare un pool di lavoratori attivi abbiamo utilizzato canale bufferizzato, per rimuovere i lavoratori inaspettatamente "morti" dal pool, abbiamo aggiunto un meccanismo per tenere traccia degli errori e degli stati dei lavoratori.

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

Affinché la nostra applicazione funzioni come server web, abbiamo dovuto scegliere uno standard PHP affidabile per rappresentare eventuali richieste HTTP in entrata. Nel nostro caso abbiamo semplicemente trasformare richiesta net/http da Vai al formato PSR-7in modo che sia compatibile con la maggior parte dei framework PHP oggi disponibili.

Poiché PSR-7 è considerata immutabile (alcuni direbbero che tecnicamente non lo è), gli sviluppatori devono scrivere applicazioni che fondamentalmente non trattino la richiesta come un'entità globale. Ciò si adatta perfettamente al concetto di processi PHP di lunga durata. La nostra implementazione finale, a cui non era ancora stato assegnato un nome, assomigliava a questa:

RoadRunner: PHP non è costruito per morire, o Golang per venire in soccorso

Presentazione di RoadRunner - server di applicazioni PHP ad alte prestazioni

La nostra prima attività di test è stata il backend dell'API, che periodicamente presentava picchi di richieste inaspettate (molto più spesso del solito). Sebbene nginx fosse sufficiente nella maggior parte dei casi, abbiamo riscontrato regolarmente errori 502 perché non siamo riusciti a bilanciare il sistema abbastanza velocemente per l'aumento di carico previsto.

Per sostituire questa soluzione, all'inizio del 2018 abbiamo implementato il nostro primo server applicativo PHP/Go. E subito abbiamo ottenuto un effetto incredibile! Non solo abbiamo eliminato completamente l'errore 502, ma siamo anche riusciti a ridurre il numero di server di due terzi, risparmiando un sacco di soldi e grattacapi a ingegneri e product manager.

A metà anno avevamo perfezionato la nostra soluzione, pubblicata su GitHub sotto la licenza MIT e chiamata RoadRunner, sottolineandone così l'incredibile velocità ed efficienza.

In che modo RoadRunner può migliorare il tuo stack di sviluppo

applicazione RoadRunner ci ha permesso di utilizzare Middleware net/http sul lato Go per eseguire la verifica JWT prima ancora che la richiesta raggiunga PHP, nonché per gestire WebSocket e aggregazione dello stato globale in Prometheus.

Grazie all'RPC integrato, puoi aprire l'API di qualsiasi libreria Go per PHP senza scrivere wrapper di estensione. Ancora più importante, RoadRunner può essere utilizzato per implementare nuovi server non HTTP. Gli esempi includono l'avvio di gestori in PHP AWS Lambda, creando code buster affidabili e persino aggiungendo gRPC alle nostre applicazioni.

Con l'aiuto delle comunità PHP e Go, abbiamo aumentato la stabilità della soluzione, aumentato le prestazioni dell'applicazione fino a 40 volte in alcuni test, migliorato gli strumenti di debug, implementato l'integrazione con il framework Symfony e aggiunto il supporto per HTTPS, HTTP/ 2, plugin e PSR-17.

conclusione

Alcune persone sono ancora intrappolate nella visione obsoleta di PHP come linguaggio lento e macchinoso, utile solo per scrivere plugin per WordPress. Queste persone potrebbero addirittura dire che PHP ha un limite: quando l’applicazione diventa abbastanza grande, bisogna scegliere un linguaggio più “maturo” e riscrivere la base di codice accumulata in tanti anni.

A tutto questo voglio rispondere: ripensarci. Riteniamo che solo tu possa impostare eventuali restrizioni per PHP. Puoi passare tutta la vita saltando da una lingua all'altra, cercando di trovare la corrispondenza perfetta per le tue esigenze, oppure puoi iniziare a pensare alle lingue come strumenti. Le carenze percepite di un linguaggio come PHP potrebbero in realtà essere le ragioni del suo successo. E se lo combini con un'altra lingua come Go, puoi creare prodotti molto più potenti che se fossi limitato a una sola lingua.

Avendo lavorato con una combinazione di Go e PHP, possiamo dire che li adoriamo. Non abbiamo intenzione di sacrificare l'uno per l'altro, ma piuttosto cerchiamo modi per ottenere ancora più valore da questo dual stack.

UPD: Diamo il benvenuto al creatore di RoadRunner e coautore dell'articolo originale - Lachesi

Fonte: habr.com

Aggiungi un commento