
En el ecosistema PHP actualmente existen dos conectores para trabajar con el servidor Tarantool: la extensión oficial PECL , escrita en C, y , escrita en PHP. Yo soy el autor de esta última.
En este artículo me gustaría compartir los resultados de las pruebas de rendimiento de ambas bibliotecas y mostrar cómo, con cambios mínimos en el código, se puede lograr un aumento del 3-5 de rendimiento (¡en pruebas sintéticas!).
¿Qué vamos a probar?
Vamos a probar los mencionados anteriormente conectores sincrónicos, ejecutados de manera asíncrona, en paralelo y asíncrono-paralelamente. 🙂 También no queremos tocar el código de los propios conectores. Actualmente hay varias extensiones disponibles que permiten lograr lo deseado:
- ― un marco de trabajo asíncrono de alto rendimiento para PHP. Utilizado por gigantes de internet como Alibaba y Baidu. A partir de la versión 4.1.0, se ha introducido el método mágico SwooleRuntime::enableCoroutine(), que permite "con una línea de código transformar bibliotecas de red sincrónicas en asíncronas".
- Async ― hasta hace poco una extensión prometedora para trabajo asíncrono en PHP. ¿Por qué hasta hace poco? Desafortunadamente, por razones que desconozco, el autor eliminó el repositorio y el futuro del proyecto es incierto. Tendremos que utilizar de las bifurcaciones. Al igual que Swoole, esta extensión permite, con un simple gesto, introducir la asincronía reemplazando la implementación estándar de los flujos TCP y TLS por sus versiones asíncronas. Esto se hace a través de la opción "async.tcp = 1«.
- ― una extensión bastante nueva del conocido Joe Watkins, autor de bibliotecas como phpdbg, apcu, pthreads, pcov, uopz. La extensión proporciona una API para trabajo multihilo en PHP y se posiciona como un reemplazo de pthreads. Una limitación importante de la biblioteca es que solo funciona con versiones ZTS (Zend Thread Safe) de PHP.
¿Cómo vamos a probar?
Iniciaremos una instancia de Tarantool sin el registro de escritura anticipada (wal_mode = none) y con un aumento del búfer de red (readahead = 1 * 1024 * 1024). La primera opción excluirá el trabajo con disco, la segunda permitirá leer más consultas del búfer del sistema operativo y así minimizar el número de llamadas al sistema.
Para los benchmarks que trabajan con datos (inserción, eliminación, lectura, etc.), antes de iniciar el benchmark se (re)creará el espacio memtx, donde los valores del índice primario son generados por un generador de valores enteros ordenados (secuencia).
El aspecto del espacio DDL es el siguiente:
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}})De ser necesario, antes de ejecutar el benchmark, el espacio se llena con 10,000 tuplas del tipo
{id, "tuplе_"}El acceso a las tuplas se realiza mediante un valor de clave aleatorio.
El benchmark en sí es una sola consulta al servidor, que se ejecuta 10,000 veces (revoluciones), las cuales, a su vez, se ejecutan en iteraciones. Las iteraciones se repiten hasta que todas las desviaciones en el tiempo entre 5 iteraciones se encuentren dentro de un margen de error aceptable del 3 %*. Después de eso, se toma el resultado promedio. Entre iteraciones hay una pausa de 1 segundo para evitar que el procesador entre en throttling. El recolector de basura de Lua se desactiva antes de cada iteración y se inicia obligatoriamente después de que se completa. El proceso de PHP se inicia solo con las extensiones necesarias para el benchmark, con la buffering de salida activada y desactivando el recolector de basura.
* La cantidad de revoluciones, iteraciones y el umbral de error se pueden cambiar en la configuración del benchmark.
Entorno de prueba
Los resultados publicados a continuación se realizaron en un MacBookPro (2015), el sistema operativo es Fedora 30 (versión del núcleo 5.3.8-200.fc30.x86_64). Tarantool se ejecutó en Docker con el parámetro "--network host".
Versiones de paquetes:
Tarantool: 2.3.0-115-g5ba5ed37e
Docker: 19.03.3, build a872fc2f86
PHP: 7.3.11 (cli) (compilado: 22 de octubre de 2019 08:11:04)
tarantool/client: 0.6.0
rybakit/msgpack: 0.6.1
ext-tarantool: 0.3.2 (+ parche para 7.3)*
ext-msgpack: 2.0.3
ext-async: 0.3.0-8c1da46
ext-swoole: 4.4.12
ext-parallel: 1.1.3
* Desafortunadamente, el conector oficial no funciona con la versión de PHP > 7.2. Para compilar y ejecutar la extensión en PHP 7.3, fue necesario utilizar .
Resultados
Modo sincrónico
El protocolo de Tarantool utiliza un formato binario para la serialización de mensajes. En el conector PECL, la serialización está oculta profundamente en las entrañas de la biblioteca y no es posible influir en el proceso de codificación desde el código userland . El conector en PHP puro, por otro lado, ofrece la posibilidad de personalizar el proceso de codificación ampliando el codificador estándar o utilizando su propia implementación. De forma predeterminada, hay dos codificadores disponibles, uno basado en (la extensión oficial de MessagePack PECL), el otro en (en PHP puro).
Antes de comparar los conectores, evaluemos el rendimiento de los codificadores MessagePack para el conector PHP y en las siguientes pruebas utilizaremos aquel que muestre el mejor resultado:

Aunque la versión PHP (Pure) es más lenta que la extensión PECL, en proyectos reales, aún recomendaría usar , porque en la extensión oficial de MessagePack la especificación del formato está implementada solo parcialmente (por ejemplo, no hay soporte para tipos de datos personalizados, sin los cuales no podrás utilizar Decimal, un nuevo tipo de dato introducido en Tarantool 2.3) y presenta varios otros (incluyendo problemas de compatibilidad con PHP 7.4). En general, el proyecto parece estar abandonado.
Así que, midamos el rendimiento de los conectores en modo sincronizado:

Como se puede ver en el gráfico, el conector PECL (Tarantool) muestra un mejor rendimiento en comparación con el conector de PHP (Client). Pero esto no es sorprendente, dado que el último, además de estar implementado en un lenguaje más lento, realiza, en esencia, más trabajo: se crea un nuevo objeto en cada llamada Request y Response (en el caso de Select ― también Criteria, y en el caso de Update/Upsert ― Operations), las entidades separadas Connection, Packer y Handler también añaden sobrecarga. Es evidente que se paga un precio por la flexibilidad. Sin embargo, en general, el intérprete PHP muestra un buen rendimiento; aunque hay diferencias, estas son poco significativas y, posiblemente, sean aún menores al utilizar preloading en PHP 7.4, sin mencionar JIT en PHP 8.
Continuemos. En Tarantool 2.0 se introdujo soporte para SQL. Intentemos realizar operaciones Select, Insert, Update y Delete utilizando el protocolo SQL y comparar los resultados con sus equivalentes noSQL (binarios):

Los resultados de SQL no son muy impresionantes (recuerden que aún estamos probando en modo sincronizado). Sin embargo, no me preocuparía demasiado por esto en este momento, el soporte para SQL todavía está en desarrollo activo (recientemente, por ejemplo, se agregó soporte para ) y, según la lista , se esperan varias optimizaciones para el motor SQL en el futuro.
Async
Veamos ahora cómo la extensión Async puede ayudarnos a mejorar los resultados anteriores. Para la escritura de programas asíncronos, la extensión proporciona una API basada en corutinas, que es lo que utilizaremos. A través de la experimentación, descubrimos que el número óptimo de corutinas para nuestro entorno es 25:

Distribuimos 10,000 operaciones entre 25 corutinas y vemos qué resulta:

El número de operaciones por segundo ha crecido más de 3 veces para !
Es una pena, pero el conector PECL no se ejecutó con ext-async.
¿Y con SQL?

Como pueden ver, en modo asíncrono la diferencia entre el protocolo binario y SQL queda dentro del margen de error.
Swoole
De nuevo determinamos el número óptimo de corutinas, esta vez para Swoole:

Nos quedaremos en 25. Repitamos el mismo truco que con la extensión Async: distribuiremos 10,000 operaciones entre 25 corutinas. Además, añadiremos otra prueba en la que dividiremos todo el trabajo en 2 procesos (es decir, cada proceso realizará 5,000 operaciones en 25 corutinas). Los procesos se crearán utilizando SwooleProcess.
Resultados:

Swoole muestra un resultado ligeramente inferior en comparación con Async al ejecutarse en un solo proceso, pero con 2 procesos la situación cambia drásticamente (el número 2 se eligió no al azar, en mi máquina justamente 2 procesos mostraron el mejor resultado).
Por cierto, la extensión Async también tiene API para trabajar con procesos, sin embargo, allí no noté ninguna diferencia al ejecutar los benchmarks en uno o varios procesos (no se puede descartar que cometí un error en algún lugar).
SQL vs protocolo binario:

Al igual que con Async, la diferencia entre las operaciones binarias y SQL se nivela en modo asíncrono.
Parallel
Dado que la extensión Parallel no trata sobre corutinas, sino sobre hilos, midamos el número óptimo de hilos paralelos:

Es de 16 en mi máquina. Ejecutemos los benchmarks de los conectores en 16 hilos paralelos:

Como pueden ver, el resultado es incluso mejor que con las extensiones asíncronas (sin contar Swoole ejecutado en 2 procesos). Tengan en cuenta que para el conector PECL no hay operaciones en lugar de Update y Upsert. Esto se debe a que esas operaciones fallaron con un error; no sé si es culpa de ext-parallel, ext-tarantool o de ambos.
Ahora comparemos el rendimiento de SQL:

¿Notaron la similitud con el gráfico de los conectores ejecutados de manera síncrona?
Todo junto
Y para finalizar, resumamos todos los resultados en un solo gráfico para ver el panorama general de las extensiones probadas. Solo agregaremos una nueva prueba al gráfico, que aún no hemos realizado: ejecutaremos corrutinas Async en paralelo utilizando Parallel*. La idea de integrar las extensiones mencionadas anteriormente ya por los autores, sin embargo, no se llegó a un consenso, así que tendremos que hacerlo nosotros mismos.
* No se pudo ejecutar corrutinas Swoole con Parallel; parece que estas extensiones son incompatibles.
Entonces, los resultados finales:

En conclusión
En mi opinión, los resultados son bastante impresionantes y tengo la extraña sensación de que ¡esto aún no es el límite! Si esto es necesario para usted en un proyecto real, debe decidirlo exclusivamente usted mismo; solo diré que para mí fue un experimento interesante que me permitió evaluar cuánto se puede ‘exprimir’ de un conector TCP sincrónico con mínimos esfuerzos. Si tiene ideas para mejorar los benchmarks, estaré encantado de considerar su pull request. Todo el código con instrucciones para la ejecución y resultados está publicado de forma separada .
Fuente: habr.com
