
Attualmente, nell'ecosistema PHP ci sono due connettori per lavorare con il server Tarantool: l'estensione ufficiale PECL , scritta in C, e , scritta in PHP. Sono l'autore di quest'ultima.
In questo articolo, vorrei condividere i risultati dei test sulle performance di entrambe le librerie e mostrare come, con piccole modifiche al codice, si possa ottenere un incremento delle performance del 3-5% (in test sintetici!)).
Cosa testeremo?
Testeremo i connettori suddetti synchroni lanciati in modalità asincrona, parallela e asincrona-parallela. 🙂 Inoltre, non vogliamo toccare il codice stesso dei connettori. Attualmente sono disponibili diverse estensioni che permettono di ottenere il risultato desiderato:
- è un framework asincrono ad alte prestazioni per PHP. Utilizzato da giganti di Internet come Alibaba e Baidu. Con la versione 4.1.0 è stato introdotto il metodo magico SwooleRuntime::enableCoroutine(), che consente "con una sola riga di codice di trasformare le librerie di rete sincrone di PHP in asincrone".
- Async ― fino a poco tempo fa era un'espansione promettente per il lavoro asincrono in PHP. Perché fino a poco tempo fa? Sfortunatamente, per ragioni a me sconosciute, l'autore ha rimosso il repository e il futuro del progetto è incerto. Dovremo utilizzare dei fork. Come Swoole, questa estensione consente di attivare l'asincronicità con un semplice comando, sostituendo l'implementazione standard dei flussi TCP e TLS con le loro versioni asincrone. Questo avviene tramite l'opzione «async.tcp = 1«.
- ― un'estensione abbastanza nuova dell'ormai noto Joe Watkins, autore di librerie come phpdbg, apcu, pthreads, pcov, uopz. Questa estensione fornisce un'API per il lavoro multithreading in PHP e si posiziona come un sostituto di pthreads. Un aspetto significativo di questa libreria è che funziona solo con la versione ZTS (Zend Thread Safe) di PHP.
Come testeremo?
Avvieremo un'istanza di Tarantool con il log della scrittura anticipata disattivato (wal_mode = none) e aumenteremo il buffer di rete (readahead = 1 * 1024 * 1024). La prima opzione escluderà l'uso del disco, la seconda permetterà di leggere un numero maggiore di richieste dal buffer del sistema operativo, riducendo così il numero di chiamate di sistema.
Per i benchmark che operano con i dati (inserimento, cancellazione, lettura, ecc.), prima dell'inizio del benchmark verrà (ri)creato uno spazio memtx, in cui i valori dell'indice primario vengono generati da un generatore di valori interi ordinati (sequence).
Il 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 di avviare il benchmark, lo spazio viene riempito con 10.000 tuple del tipo
{id, "tuplе_"}L'accesso alle tuple avviene tramite un valore chiave casuale.
Il benchmark consiste in una singola richiesta al server, che viene eseguita 10.000 volte (rivoluzioni), le quali, a loro volta, vengono eseguite in iterazioni. Le iterazioni vengono ripetute finché tutte le deviazioni di tempo tra 5 iterazioni non rientrano nella tolleranza consentita del 3%*. Dopodiché 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 di Lua è disattivato prima di ogni iterazione e avviato forzatamente dopo il suo completamento. Il processo PHP viene avviato solo con le estensioni necessarie per il benchmark, con la buffering dell'output attivata e il garbage collector disattivato.
* Il numero di rivoluzioni, iterazioni e il limite di tolleranza possono essere modificati nelle impostazioni del benchmark.
Ambiente di test
I risultati pubblicati di seguito sono stati ottenuti su un MacBook Pro (2015), sistema operativo – Fedora 30 (versione del kernel 5.3.8-200.fc30.x86_64). Tarantool è stato eseguito 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) (compilato: 22 Ott 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
* Purtroppo, 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 in profondità nella libreria e non è possibile influenzare il processo di codifica dal codice userland . Il connettore in puro PHP, al contrario, offre la possibilità di personalizzare il processo di codifica utilizzando il codificatore standard oppure implementando una propria soluzione. Sono disponibili due codificatori di default, uno basato su (estensione ufficiale MessagePack PECL), l'altro su (in puro PHP).
Prima di confrontare i connettori, misuriamo le prestazioni dei codificatori MessagePack per il connettore PHP e nei test successivi utilizzeremo quello che darà il miglior risultato:

Anche se la versione PHP (Pura) è inferiore in velocità rispetto all'estensione PECL, nei progetti reali consiglierei comunque di usare proprio questa , perché nel formato ufficiale di MessagePack la specifica è solo parzialmente implementata (ad esempio, non supporta i tipi di dati personalizzati, senza i quali non è possibile utilizzare Decimal — un nuovo tipo di dato introdotto in Tarantool 2.3) e presenta una serie di altri (inclusi problemi di compatibilità con PHP 7.4). Inoltre, nel complesso, il progetto appare abbandonato.
Quindi, misuriamo le prestazioni dei connettori in modalità sincrona:

Come si può vedere dal grafico, il connettore PECL (Tarantool) mostra prestazioni migliori rispetto al connettore in PHP (Client). Ma non è sorprendente, considerando che quest'ultimo, oltre a essere implementato in un linguaggio più lento, effettua, di fatto, più lavoro: ad ogni chiamata viene creato un nuovo oggetto Richiesta e Risposta (nel caso di Select — anche Criteria, e nel caso di Update/Upsert — Operations), le entità separate Connection, Packer e Handler aggiungono anche overhead. È ovvio che per la flessibilità si paga un prezzo. Tuttavia, nel complesso, l'interprete PHP mostra buone prestazioni, anche se c'è una differenza, ma è insignificante e, probabilmente, sarà ancora minore con l'uso del preloading in PHP 7.4, per non parlare di JIT in PHP 8.
Andiamo avanti. In Tarantool 2.0 è stata introdotta la supporto per SQL. Proviamo a eseguire operazioni Select, Insert, Update e Delete utilizzando il protocollo SQL e confrontiamo i risultati con gli equivalenti noSQL (binari):

I risultati di SQL non sono particolarmente impressionanti (ricordo che stiamo ancora testando la modalità sincrona). Tuttavia, non mi preoccuperei troppo di questo, poiché il supporto per SQL è ancora in fase di sviluppo attivo (di recente, ad esempio, è stata aggiunta la supporto per ) e, da quello che vediamo nella lista , il motore SQL subirà a breve una serie di ottimizzazioni.
Async
Bene, vediamo ora come l'estensione Async può aiutarci a migliorare i risultati sopra. Per scrivere programmi asincroni, l'estensione fornisce un'API basata su coroutine, che utilizzeremo. Attraverso esperimenti, abbiamo scoperto che il numero ottimale di coroutine per il nostro ambiente è 25:

Distribuiamo 10.000 operazioni su 25 coroutine e vediamo cosa ne è uscito:

Il numero di operazioni al secondo è aumentato di oltre 3 volte per !
È triste, ma il connettore PECL non si è avviato 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
Adesso stiamo valutando il numero ottimale di coroutine, questa volta per Swoole:

Fermiamoci a 25. Ripetiamo lo stesso trucco usato con l'estensione Async: distribuiremo 10.000 operazioni tra 25 coroutine. Inoltre, aggiungeremo un altro test, in cui divideremo tutto il lavoro in 2 processi (cioè ogni processo eseguirà 5.000 operazioni in 25 coroutine). I processi saranno creati utilizzando SwooleProcess.
Risultati:

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

Così come per Async, la differenza tra le operazioni binarie e SQL viene attenuata in modalità asincrona.
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 è addirittura migliore rispetto alle estensioni asincrone (escludendo Swoole avviato su 2 processi). Notate che per il connettore PECL non ci sono operazioni di Update e Upsert. Questo è dovuto al fatto che tali operazioni hanno restituito un errore — non so se sia colpa di ext-parallel, ext-tarantool o di entrambi.
Adesso confrontiamo le prestazioni SQL:

Notate la somiglianza con il grafico per i connettori eseguiti in modo sincrono?
Tutto insieme
E per finire, 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 effettuato — eseguiremo coroutine Async in parallelo utilizzando Parallel*. L'idea di integrare le estensioni sopraccitate è già dagli autori, tuttavia non è stato raggiunto un consenso, dovremo farlo da soli.
* Non sono riuscito a eseguire le coroutine Swoole con Parallel, sembra che queste estensioni non siano compatibili.
Quindi, i risultati finali:

In conclusion
Credo che i risultati siano decisamente soddisfacenti e sono sicuro che non sia ancora il limite! Se sia necessario per il vostro progetto reale, spetta solo a voi decidere; posso solo dire che per me è stato un esperimento interessante che ha permesso di valutare quanto si possa "estrarre" da un connettore TCP sincrono con il minimo sforzo. Se avete idee per migliorare i benchmark, esaminerò con piacere la vostra pull request. Tutto il codice con le istruzioni per l'esecuzione e i risultati è pubblicato in un separato .
Fonte: habr.com
