sql >> Base de Datos >  >> RDS >> MariaDB

Rendimiento del controlador del conector MariaDB Java

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:

  1. PREPARAR:prepara la declaración para su ejecución.
  2. EJECUTAR:enviar parámetros
  3. 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 OUT
  • SET @com_mysql_jdbc_outparam_p1 = 1 para enviar datos de acuerdo con los parámetros IN / OUT
  • CALL testj.inoutParam(@com_mysql_jdbc_outparam_p1) procedimiento de llamada
  • SELECT @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:

  1. con una base de datos MariaDB 10.1.17 local, distante
  2. con una base de datos MySQL Community Server 5.7.15 (compilación 5.7.15-0ubuntu0.16.04.1) local