RENDIMIENTO DEL CONECTOR JAVA MARIADB
Siempre hablamos de rendimiento. Pero la cosa siempre es "¡Mide, no adivines!".
Últimamente se han realizado muchas mejoras de rendimiento en MariaDB Java Connector. Entonces, ¿cuál es el rendimiento actual del controlador?
Permítanme compartir un resultado de referencia de 3 controladores jdbc que permiten el acceso a una base de datos MySQL/MariaDB: DrizzleJDBC, MySQL Connector/J y MariaDB java connector.
Las versiones del controlador son la última versión disponible de GA al momento de escribir este blog:
- MariaDB 1.5.3
- MySQL 5.1.39
- Llovizna 1.4
EL REFERENTE
JMH es una herramienta de marco de micro-benchmarking de Oracle desarrollada por Oracle, entregada como herramientas openJDK, que será la suite de microbenchmark oficial de java 9. Su ventaja distintiva sobre otros marcos es que está desarrollado por los mismos chicos de Oracle que implementan JIT (compilación Just In Time) y permiten evitar la mayoría de los escollos de micro-benchmark.
Fuente de referencia: https://github.com/rusher/mariadb-java-driver-benchmark.
Las pruebas son bastante sencillas si está familiarizado con Java.
Ejemplo:
public class BenchmarkSelect1RowPrepareText extends BenchmarkSelect1RowPrepareAbstract { @Benchmark public String mysql(MyState state) throws Throwable { return select1RowPrepare(state.mysqlConnectionText, state); } @Benchmark public String mariadb(MyState state) throws Throwable { return select1RowPrepare(state.mariadbConnectionText, state); } @Benchmark public String drizzle(MyState state) throws Throwable { return select1RowPrepare(state.drizzleConnectionText, state); } } public abstract class BenchmarkSelect1RowPrepareAbstract extends BenchmarkInit { private String request = "SELECT CAST(? as char character set utf8)"; public String select1RowPrepare(Connection connection, MyState state) throws SQLException { try (PreparedStatement preparedStatement = connection.prepareStatement(request)) { preparedStatement.setString(1, state.insertData[state.counter++]); try (ResultSet rs = preparedStatement.executeQuery()) { rs.next(); return rs.getString(1); } } } }
Las pruebas que utilizan las consultas de INSERT se envían a un motor BLACKHOLE con el registro binario deshabilitado, para evitar la E/S y la dependencia del rendimiento del almacenamiento. Esto permite tener resultados más estables.
(Sin usar el motor blackhole y deshabilitar el registro binario, los tiempos de ejecución variarían hasta en un 10%).
Benchmark se ejecutó en las bases de datos MariaDB Server 10.1.17 y MySQL Community Server 5.7.13. El siguiente documento muestra los resultados utilizando los 3 controladores con MariaDB Server 10.1.17. Para obtener los resultados completos, incluidos los de MySQL Server 5.7.13, consulte el enlace en la parte inferior del documento.
MEDIO AMBIENTE
La ejecución (cliente y servidor) se realiza en un único droplet de servidor en digitalocean.com utilizando los siguientes parámetros:
- Java(TM) SE Runtime Environment (compilación 1.8.0_101-b13) 64 bits (última versión real cuando se ejecuta este punto de referencia)
- Ubuntu 16.04 64 bits
- 512 MB de memoria
- 1 CPU
- base de datos MariaDB "10.1.17-MariaDB", MySQL Community Server build "5.7.15-0ubuntu0.16.04.1"
utilizando archivos de configuración predeterminados y estas opciones adicionales:- max_allowed_packet =40M #paquete de intercambio puede ser de hasta 40mb
- character-set-server =utf8 #para usar UTF-8 como predeterminado
- collation-server =utf8_unicode_ci #para usar UTF-8 como predeterminado
Cuando se indica "distante", los puntos de referencia se ejecutan con un cliente y un servidor separados en 2 hosts idénticos en el mismo centro de datos con un ping promedio de 0,350 ms.
EXPLICACIONES DE MUESTRA DE RESULTADOS
Benchmark Score Error Units BenchmarkSelect1RowPrepareText.mariadb 62.715 ± 2.402 µs/op BenchmarkSelect1RowPrepareText.mysql 88.670 ± 3.505 µs/op BenchmarkSelect1RowPrepareText.drizzle 78.672 ± 2.971 µs/op
Esto significa que esta simple consulta tomará un tiempo promedio de 62.715 microsegundos usando el controlador MariaDB con una variación de ± 2.402 microsegundos para el 99.9% de las consultas.
La misma ejecución usando el controlador drizzle tomará un tiempo promedio de 88.670 microsegundos, y 78.672 microsegundos usando el conector MySQL (menor tiempo de ejecución, mejor).
Los porcentajes mostrados se establecen de acuerdo con el primer resultado de mariadb como referencia (100 %), lo que permite comparar fácilmente otros resultados.
COMPARACIONES DE RENDIMIENTO
El punto de referencia probará el rendimiento de los 3 comportamientos diferentes principales utilizando una misma base de datos local (mismo servidor) y una base de datos distante (otro servidor idéntico) en el mismo centro de datos con un ping promedio de 0,450 ms
Diferentes comportamientos:
Protocolo de texto
Esto corresponde a la opción useServerPrepStmts inhabilitada.
Las consultas se envían directamente al servidor con el reemplazo de parámetros desinfectados realizado en el lado del cliente.
Los datos se envían como texto. Ejemplo:Se enviará una marca de tiempo como el texto "1970-01-01 00:00:00.000500" usando 26 bytes
Protocolo binario
Esto corresponde a la opción useServerPrepStmts habilitada (implementación predeterminada en el controlador MariaDB).
Los datos se envían en binario. La marca de tiempo de ejemplo "1970-01-01 00:00:00.000500" se enviará usando 11 bytes.
Hay hasta 3 intercambios con el servidor para una consulta:
- PREPARAR:prepara la declaración para su ejecución.
- EJECUTAR:enviar parámetros
- DEALLOCATE PREPARE:libera una declaración preparada.
Consulte Documentación de preparación del servidor para obtener más información.
Los resultados de PREPARE se almacenan en caché en el lado del controlador (tamaño predeterminado 250). Si Prepare ya está en caché, PREPARE no se ejecutará, DEALLOCATE se ejecutará solo cuando PREPARE ya no se use y no esté en caché. Eso significa que algunas ejecuciones de consultas tendrán 3 viajes de ida y vuelta, pero otras solo tendrán un viaje de ida y vuelta, enviando un identificador y parámetros PREPARE.
Reescribir
Esto corresponde a la opción rewriteBatchedStatements habilitada.
Rewrite usa el protocolo de texto y se refiere solo a lotes. El controlador reescribirá la consulta para obtener resultados más rápidos.
Ejemplo:
Insertar en valores ab (i) (?) con los valores del primer lote [1] y [2] se reescribirá en
Insertar en valores ab (i) (1), (2).
Si la consulta no se puede reescribir en "valores múltiples", la reescritura utilizará consultas múltiples:
Insertar en la tabla (col1) valores (?) en la actualización de clave duplicada col2=? con los valores [1,2] y [2,3] se reescribirán para
Insertar en la tabla (col1) valores (1) en la actualización de clave duplicada col2=2;Insertar en la tabla (col1) valores (3) en actualización de clave duplicada col2=4
Las desventajas de esta opción son:
- Los ID de incremento automático no se pueden recuperar usandoStatement.html#getGeneratedKeys().
- Se habilitan consultas múltiples en una sola ejecución. Eso no es un problema para PreparedStatement, pero si la aplicación usa Statement eso puede ser una degradación de la seguridad (inyección SQL).
* MariaDB y MySQL tienen implementados esos 3 comportamientos, rocíe solo el protocolo de texto.
RESULTADOS DE REFERENCIA
Resultados del controlador MariaDB
CONSULTA DE SELECCIÓN ÚNICA
private String request = "SELECT CAST(? as char character set utf8)"; public String select1RowPrepare(Connection connection, MyState state) throws SQLException { try (PreparedStatement preparedStatement = connection.prepareStatement(request)) { preparedStatement.setString(1, state.insertData[state.counter++]); //a random 100 bytes. try (ResultSet rs = preparedStatement.executeQuery()) { rs.next(); return rs.getString(1); } } }
LOCAL DATABASE: BenchmarkSelect1RowPrepareHit.mariadb 58.267 ± 2.270 µs/op BenchmarkSelect1RowPrepareMiss.mariadb 118.896 ± 5.500 µs/op BenchmarkSelect1RowPrepareText.mariadb 62.715 ± 2.402 µs/op
DISTANT DATABASE: BenchmarkSelect1RowPrepareHit.mariadb 394.354 ± 13.102 µs/op BenchmarkSelect1RowPrepareMiss.mariadb 709.843 ± 31.090 µs/op BenchmarkSelect1RowPrepareText.mariadb 422.215 ± 15.858 µs/op
Cuando el resultado de PREPARAR para esta consulta exacta ya está en caché (coincidencia de caché), la consulta será más rápida (7,1 % en este ejemplo) que usar el protocolo de texto. Debido a los intercambios PREPARE y DEALLOCATE de solicitudes adicionales, el error de caché es un 68,1 % más lento.
Este énfasis en las ventajas e inconvenientes de utilizar un protocolo binario. El caché HIT es importante.
CONSULTA DE INSERCIÓN ÚNICA
private String request = "INSERT INTO blackholeTable (charValue) values (?)"; public boolean executeOneInsertPrepare(Connection connection, String[] datas) throws SQLException { try (PreparedStatement preparedStatement = connection.prepareStatement(request)) { preparedStatement.setString(1, datas[0]); //a random 100 byte data return preparedStatement.execute(); } }
LOCAL DATABASE: BenchmarkOneInsertPrepareHit.mariadb 61.298 ± 1.940 µs/op BenchmarkOneInsertPrepareMiss.mariadb 130.896 ± 6.362 µs/op BenchmarkOneInsertPrepareText.mariadb 68.363 ± 2.686 µs/op
DISTANT DATABASE: BenchmarkOneInsertPrepareHit.mariadb 379.295 ± 17.351 µs/op BenchmarkOneInsertPrepareMiss.mariadb 802.287 ± 24.825 µs/op BenchmarkOneInsertPrepareText.mariadb 415.125 ± 14.547 µs/op
Los resultados de INSERT son similares a los resultados de SELECT.
LOTE:1000 INSERTAR CONSULTA
private String request = "INSERT INTO blackholeTable (charValue) values (?)"; public int[] executeBatch(Connection connection, String[] data) throws SQLException { try (PreparedStatement preparedStatement = connection.prepareStatement(request)) { for (int i = 0; i < 1000; i++) { preparedStatement.setString(1, data[i]); //a random 100 byte data preparedStatement.addBatch(); } return preparedStatement.executeBatch(); } }
LOCAL DATABASE: PrepareStatementBatch100InsertPrepareHit.mariadb 5.290 ± 0.232 ms/op PrepareStatementBatch100InsertRewrite.mariadb 0.404 ± 0.014 ms/op PrepareStatementBatch100InsertText.mariadb 6.081 ± 0.254 ms/op
DISTANT DATABASE: PrepareStatementBatch100InsertPrepareHit.mariadb 7.639 ± 0.476 ms/op PrepareStatementBatch100InsertRewrite.mariadb 1.164 ± 0.037 ms/op PrepareStatementBatch100InsertText.mariadb 8.148 ± 0.563 ms/op
El uso del protocolo binario es aquí más significativo, con resultados un 13 % más rápidos que el uso del protocolo de texto.
Las inserciones se envían de forma masiva y los resultados se leen de forma asíncrona (que corresponde a la opción useBatchMultiSend). Esto permite tener resultados lejanos con un rendimiento no muy alejado de los locales.
Rewrite tiene un rendimiento sorprendentemente bueno, pero no tendrá identificadores de incremento automático. Si no necesita identificaciones de inmediato y no usa ORM, esta solución será la más rápida. Algunos ORM permiten la configuración para manejar la secuencia internamente para proporcionar identificadores de incremento, pero esas secuencias no se distribuyen, por lo que no funcionarán en clústeres.
COMPARACIÓN CON OTROS IMPULSORES
Consulta SELECT con resultado de una fila
BenchmarkSelect1RowPrepareHit.mariadb 58.267 ± 2.270 µs/op BenchmarkSelect1RowPrepareHit.mysql 73.789 ± 1.863 µs/op BenchmarkSelect1RowPrepareMiss.mariadb 118.896 ± 5.500 µs/op BenchmarkSelect1RowPrepareMiss.mysql 150.679 ± 4.791 µs/op BenchmarkSelect1RowPrepareText.mariadb 62.715 ± 2.402 µs/op BenchmarkSelect1RowPrepareText.mysql 88.670 ± 3.505 µs/op BenchmarkSelect1RowPrepareText.drizzle 78.672 ± 2.971 µs/op BenchmarkSelect1RowPrepareTextHA.mariadb 64.676 ± 2.192 µs/op BenchmarkSelect1RowPrepareTextHA.mysql 137.289 ± 4.872 µs/op
HA significa "Alta disponibilidad" usando la configuración Maestro-Esclavo
(la URL de conexión es "jdbc:mysql:replication://localhost:3306,localhost:3306/testj").
Estos resultados se deben a muchas opciones de implementación diferentes. Aquí hay algunas razones que explican las diferencias horarias:
- El controlador MariaDB está optimizado para UTF-8, lo que permite menos creación de matriz de bytes, evitando la copia de matriz y el consumo de memoria.
- Implementación de alta disponibilidad:los controladores MariaDB y MySQL utilizan una clase Proxy dinámica de java situada entre los objetos de declaración y los sockets, lo que permite agregar un comportamiento de conmutación por error. Esas adiciones costarán una sobrecarga de 2 microsegundos por consulta (62.715 sin convertirse en 64.676 microsegundos).
En la implementación de MySQL, casi todos los métodos internos son proxy, agregando una sobrecarga para muchos métodos que no tienen nada que ver con la conmutación por error, agregando una sobrecarga total de 50 microsegundos para cada consulta.
(Drizzle no tiene PREPARE, ni funcionalidad HA)
“Seleccionar 1000 filas”
private String request = "select * from seq_1_to_1000"; //using the sequence storage engine private ResultSet select1000Row(Connection connection) throws SQLException { try (Statement statement = connection.createStatement()) { try (ResultSet rs = statement.executeQuery(request)) { while (rs.next()) { rs.getString(1); } return rs; } }
BenchmarkSelect1000Rows.mariadb 244.228 ± 7.686 µs/op BenchmarkSelect1000Rows.mysql 298.814 ± 12.143 µs/op BenchmarkSelect1000Rows.drizzle 406.877 ± 16.585 µs/op
Cuando se utilizan muchos datos, el tiempo se dedica principalmente a leer desde el socket y almacenar el resultado en la memoria para enviarlo de vuelta al cliente. Si el punto de referencia solo ejecutara SELECT sin leer los resultados, el tiempo de ejecución de MySQL y MariaDB sería equivalente. Dado que el objetivo de una consulta SELECT es obtener resultados, el controlador MariaDB está optimizado para devolver resultados (evitando la creación de matrices de bytes).
“Insertar 1000 filas”
LOCAL DATABASE: PrepareStatementBatch100InsertPrepareHit.mariadb 5.290 ± 0.232 ms/op PrepareStatementBatch100InsertPrepareHit.mysql 9.015 ± 0.440 ms/op PrepareStatementBatch100InsertRewrite.mariadb 0.404 ± 0.014 ms/op PrepareStatementBatch100InsertRewrite.mysql 0.592 ± 0.016 ms/op PrepareStatementBatch100InsertText.mariadb 6.081 ± 0.254 ms/op PrepareStatementBatch100InsertText.mysql 7.932 ± 0.293 ms/op PrepareStatementBatch100InsertText.drizzle 7.314 ± 0.205 ms/op
DISTANT DATABASE: PrepareStatementBatch100InsertPrepareHit.mariadb 7.639 ± 0.476 ms/op PrepareStatementBatch100InsertPrepareHit.mysql 43.636 ± 1.408 ms/op PrepareStatementBatch100InsertRewrite.mariadb 1.164 ± 0.037 ms/op PrepareStatementBatch100InsertRewrite.mysql 1.432 ± 0.050 ms/op PrepareStatementBatch100InsertText.mariadb 8.148 ± 0.563 ms/op PrepareStatementBatch100InsertText.mysql 43.804 ± 1.417 ms/op PrepareStatementBatch100InsertText.drizzle 38.735 ± 1.731 ms/op
Las inserciones masivas de MySQL y Drizzle son como X INSERT:el controlador envía 1 INSERT, espera el resultado de la inserción y envía la siguiente inserción. La latencia de la red entre cada inserción ralentizará las inserciones.
Trámites de tienda
LLAMADA DE PROCEDIMIENTO
//CREATE PROCEDURE inoutParam(INOUT p1 INT) begin set p1 = p1 + 1; end private String request = "{call inOutParam(?)}"; private String callableStatementWithOutParameter(Connection connection, MyState state) throws SQLException { try (CallableStatement storedProc = connection.prepareCall(request)) { storedProc.setInt(1, state.functionVar1); //2 storedProc.registerOutParameter(1, Types.INTEGER); storedProc.execute(); return storedProc.getString(1); } }
BenchmarkCallableStatementWithOutParameter.mariadb 88.572 ± 4.263 µs/op BenchmarkCallableStatementWithOutParameter.mysql 714.108 ± 44.390 µs/op
Las implementaciones de MySQL y MariaDB difieren completamente. El controlador Mysql utilizará muchas consultas ocultas para obtener el resultado de salida:
SHOW CREATE PROCEDURE testj.inoutParam
para identificar los parámetros IN y OUTSET @com_mysql_jdbc_outparam_p1 = 1
para enviar datos de acuerdo con los parámetros IN / OUTCALL testj.inoutParam(@com_mysql_jdbc_outparam_p1)
procedimiento de llamadaSELECT @com_mysql_jdbc_outparam_p1
para leer el resultado de salida
La implementación de MariaDB es sencilla utilizando la capacidad de tener el parámetro OUT en la respuesta del servidor sin consultas adicionales. (Esa es la razón principal por la que el controlador MariaDB requiere la versión 5.5.3 o posterior del servidor MariaDB/MySQL).
CONCLUSIÓN
¡El controlador de MariaDB es genial!
El protocolo binario tiene diferentes ventajas, pero se basa en tener los resultados de PREPARE ya en caché. Si las aplicaciones tienen muchos tipos diferentes de consultas y la base de datos está distante, es posible que esa no sea la mejor solución.
Rewrite tiene resultados sorprendentes para escribir datos por lotes
El conductor se mantiene bien frente a otros conductores. Y hay mucho por venir, pero esa es otra historia.
Resultados sin procesar:
- con una base de datos MariaDB 10.1.17 local, distante
- con una base de datos MySQL Community Server 5.7.15 (compilación 5.7.15-0ubuntu0.16.04.1) local