
Nell'ecosistema PHP attualmente esistono due connettori per lavorare con il server Tarantool: si tratta dell'estensione ufficiale PECL , scritta in C, e , scritta in PHP. Io sono l'autore di quest'ultima.
In questo articolo vorrei condividere i risultati dei test di prestazione di entrambe le librerie e mostrare come, con minime modifiche al codice, sia possibile ottenere un incremento delle prestazioni del 3-5% (su test sintetici!).
Cosa testeremo?
Testeremo i connettori menzionati sopra sincronici , eseguiti in modo asincrono, parallelamente e asincrono-parallelamente. 🙂 Inoltre, non vogliamo toccare il codice dei connettori stessi. Attualmente sono disponibili diverse estensioni che consentono di raggiungere l'obiettivo desiderato:
- ― un framework asincrono ad alte prestazioni per PHP. Utilizzato da giganti del web come Alibaba e Baidu. A partire dalla versione 4.1.0 è stato introdotto il metodo magico SwooleRuntime::enableCoroutine(), che consente di "trasformare le librerie di rete PHP sincroniche in asincrone" con una sola riga di codice.
- Async ― fino a poco tempo fa, un'estensione molto promettente per il lavoro asincrono in PHP. Perché fino a poco tempo fa? Purtroppo, per una ragione a me sconosciuta, l'autore ha eliminato il repository e il futuro del progetto è incerto. Dovremo utilizzare dei fork. Come Swoole, questa estensione consente, con un semplice gesto, di trasformare le normali implementazioni TCP e TLS nei loro versioni asincroni, abilitando la modalità asincrona. Questo si fa tramite l'opzione "async.tcp = 1«.
- ― un'estensione relativamente nuova dell'illustre Joe Watkins, autore di librerie come phpdbg, apcu, pthreads, pcov, uopz. L'estensione fornisce API per il lavoro multithreading in PHP ed è posizionata come una sostituzione di pthreads. Un'importante limitazione della libreria è che funziona solo con la versione PHP ZTS (Zend Thread Safe).
Come testeremo?
Avvieremo un'istanza di Tarantool con il log di scrittura anticipata disattivato (wal_mode = none) e un buffer di rete aumentato (readahead = 1 * 1024 * 1024). La prima opzione escluderà l'accesso al disco, la seconda consentirà di recuperare più richieste dal buffer del sistema operativo, minimizzando così il numero di chiamate di sistema.
Per i benchmark che lavorano con i dati (inserimento, eliminazione, lettura, ecc.) prima dell'inizio del benchmark verrà (ri)creato uno spazio memtx, in cui i valori dell'indice primario sono generati da un generatore di valori interi ordinati (sequence).
DDL dello spazio appare così:
space = box.schema.space.create(config.space_name, {id = config.space_id, temporary = true})
space:create_index('primary', {type = 'tree', parts = {1, 'unsigned'}, sequence = true})
space:format({{name = 'id', type = 'unsigned'}, {name = 'name', type = 'string', is_nullable = false}})Se necessario, prima dell'avvio del benchmark, lo spazio viene riempito con 10.000 tuple del tipo
{id, "tuplе_<id>"}L'accesso alle tuple avviene tramite un valore di chiave casuale.
Il benchmark consiste in una singola richiesta al server, che viene eseguita 10.000 volte (rivoluzioni), che a loro volta vengono eseguite in iterazioni. Le iterazioni vengono ripetute finché tutte le deviazioni nel tempo tra 5 iterazioni non rientrano nei limiti di tolleranza dell'3 %*. Dopo di che viene preso il risultato medio. Tra le iterazioni c'è una pausa di 1 secondo, per evitare che il processore entri in throttling. Il garbage collector Lua è disattivato prima di ogni iterazione e forzato a partire dopo il suo completamento. Il processo PHP viene avviato solo con le estensioni necessarie per il benchmark, con il buffering dell'output abilitato e il garbage collector disabilitato.
* Il numero di rivoluzioni, iterazioni e soglia di errore può essere modificato nelle impostazioni del benchmark.
Ambiente di test
I risultati pubblicati di seguito sono stati ottenuti su MacBookPro (2015), sistema operativo — Fedora 30 (versione del kernel 5.3.8-200.fc30.x86_64). Tarantool è stato avviato in Docker con il parametro "--network host".
Versioni dei pacchetti:
Tarantool: 2.3.0-115-g5ba5ed37e
Docker: 19.03.3, build a872fc2f86
PHP: 7.3.11 (cli) (build: Oct 22 2019 08:11:04)
tarantool/client: 0.6.0
rybakit/msgpack: 0.6.1
ext-tarantool: 0.3.2 (+ patch per 7.3)*
ext-msgpack: 2.0.3
ext-async: 0.3.0-8c1da46
ext-swoole: 4.4.12
ext-parallel: 1.1.3
* Sfortunatamente, il connettore ufficiale non funziona con la versione PHP > 7.2. Per compilare e avviare l'estensione su PHP 7.3, è stato necessario utilizzare .
Risultati
Modalità sincrona
Il protocollo di Tarantool utilizza un formato binario per la serializzazione dei messaggi. Nel connettore PECL la serializzazione è nascosta nel profondo della libreria e non è possibile influenzare il processo di codifica dal codice userland. . Il connettore in PHP puro, al contrario, offre la possibilità di personalizzare il processo di codifica estendendo il codificatore standard oppure utilizzando una propria implementazione. Da subito sono disponibili due codificatori, uno basato su (estensione ufficiale MessagePack PECL), l'altro su (in PHP puro).
Prima di confrontare i connettori, misureremo le prestazioni dei codificatori MessagePack per il connettore PHP e nei successivi test utilizzeremo quello che mostrerà i migliori risultati:

Anche se la versione PHP (Pure) è più lenta rispetto all'estensione PECL, in progetti reali raccomanderei comunque di utilizzare proprio , poiché nell'estensione ufficiale MessagePack la specifica del formato è implementata solo parzialmente (ad esempio, non c'è supporto per i tipi di dati personalizzati, senza i quali non puoi utilizzare Decimal — un nuovo tipo di dati introdotto in Tarantool 2.3) e presenta vari altri (inclusi problemi di compatibilità con PHP 7.4). E in generale, il progetto sembra abbandonato.
Quindi, misuriamo le prestazioni dei connettori in modalità sincrona:

Come si può vedere dal grafico, il connettore PECL (Tarantool) mostra migliori prestazioni rispetto al connettore PHP (Client). Ma non è sorprendente, considerando che quest'ultimo, oltre a essere implementato in un linguaggio più lento, svolge, di fatto, un lavoro maggiore: ad ogni chiamata viene creato un nuovo oggetto Request e Response (nel caso di Select — anche Criteria, e nel caso di Update/Upsert — Operations), le entità separate Connection, Packer e Handler aggiungono anche sovraccarico. È evidente che per la flessibilità si deve pagare. Tuttavia, in generale, l'interprete PHP mostra buone prestazioni, anche se c'è una differenza, essa è insignificante e, forse, sarà ancora minore utilizzando il preloading in PHP 7.4, senza contare il JIT in PHP 8.
Procediamo. In Tarantool 2.0 è stato introdotto il supporto per SQL. Proviamo a eseguire operazioni Select, Insert, Update e Delete utilizzando il protocollo SQL e a confrontare i risultati con i relativi equivalenti noSQL (binari):

I risultati SQL non sono molto impressionanti (ricordo che stiamo ancora testando la modalità sincrona). Tuttavia, non mi preoccuperei eccessivamente di questo, il supporto per SQL è ancora in fase di sviluppo attivo (relativamente di recente, è stato aggiunto il supporto per ) e, a giudicare dalla lista , il motore SQL si aspetta una serie di ottimizzazioni in seguito.
Async
Bene, ora vediamo come l'estensione Async può aiutarci a migliorare i risultati sopra. Per la scrittura di programmi asincroni, l'estensione fornisce un'API basata su coroutine, che utilizzeremo. Dall'esperienza abbiamo scoperto che il numero ottimale di coroutine per il nostro ambiente è 25:

«Distribuiamo» 10.000 operazioni tra 25 coroutine e vediamo cosa abbiamo ottenuto:

Il numero di operazioni al secondo è aumentato di oltre 3 volte per !
Purtroppo, il connettore PECL non è partito con ext-async.
E per quanto riguarda SQL?

Come potete vedere, in modalità asincrona la differenza tra il protocollo binario e SQL è diventata trascurabile.
Swoole
Scopriamo di nuovo il numero ottimale di coroutine, questa volta per Swoole:

Fermiamoci su 25. Ripetiamo lo stesso trucco che con l'estensione Async ― distribuiamo 10.000 operazioni tra 25 coroutine. Inoltre, aggiungeremo un altro test in cui divideremo il lavoro in 2 processi (ciascun processo eseguirà 5.000 operazioni in 25 coroutine). I processi verranno creati tramite SwooleProcess.
Risultati:

Swoole mostra un risultato leggermente inferiore rispetto ad Async quando eseguito in un unico processo, ma con 2 processi la situazione cambia drasticamente (il numero 2 è stato scelto non a caso, sulla mia macchina 2 processi hanno mostrato i migliori risultati).
A proposito, anche nell'estensione Async c'è un'API per lavorare con i processi, tuttavia non ho notato alcuna differenza nell'esecuzione dei benchmark in un processo o in più processi (non è escluso che abbia fatto qualche errore).
SQL vs protocollo binario:

Come per Async, anche in modalità asincrona la differenza tra operazioni binarie e SQL si annulla.
Parallel
Poiché l'estensione Parallel non riguarda le coroutine, ma i thread, misuriamo il numero ottimale di thread paralleli:

È pari a 16 sulla mia macchina. Eseguiamo i benchmark dei connettori su 16 thread paralleli:

Come potete vedere, il risultato è persino migliore rispetto alle estensioni asincrone (escludendo Swoole eseguito su 2 processi). Notate che per il connettore PECL ci sono spazi vuoti per le operazioni di Update e Upsert. Questo è dovuto al fatto che queste operazioni sono fallite con un errore ― non so se è colpa di ext-parallel, ext-tarantool o di entrambi.
Ora confrontiamo le prestazioni di SQL:

Hai notato una somiglianza con il grafico per i connettori eseguiti in modo sincrono?
Tutto insieme
E infine, riassumiamo tutti i risultati in un unico grafico per vedere il quadro generale delle estensioni testate. Aggiungeremo al grafico solo un nuovo test che non abbiamo ancora eseguito: avvieremo le coroutine Async in parallelo utilizzando Parallel*. L'idea di integrare le suddette estensioni è già dagli autori, tuttavia non è stato raggiunto alcun consenso e dovremo farlo noi stessi.
* Non siamo riusciti a eseguire le coroutine Swoole con Parallel, sembra che queste estensioni non siano compatibili.
Quindi, i risultati finali:

In conclusione
Secondo me, i risultati sono stati piuttosto soddisfacenti e ho come l'impressione che questo non sia ancora il limite! Spetta solo a voi decidere se questo vi serve in un progetto reale; posso solo dire che per me è stato un esperimento interessante che ha permesso di capire quanto si può "estrarre" da un connettore TCP sincrono con il minimo sforzo. Se avete idee per migliorare i benchmark, sarò felice di considerare la vostra pull request. Tutto il codice con le istruzioni per l'esecuzione e i risultati è pubblicato in un .
Fonte: habr.com
