MariaDB
 sql >> Base de Dados >  >> RDS >> MariaDB

Desempenho do driver do conector Java do MariaDB

DESEMPENHO DO CONECTOR MARIADB JAVA


Sempre falamos de desempenho. Mas a coisa é sempre “Meça, não adivinhe!”.

Recentemente, muitas melhorias de desempenho foram feitas no MariaDB Java Connector. Então, qual é o desempenho atual do driver?

Deixe-me compartilhar um resultado de comparação de 3 drivers jdbc que permitem acesso a um banco de dados MySQL/MariaDB: DrizzleJDBC, MySQL Connector/J e MariaDB java connector.

As versões do driver são a versão GA mais recente disponível no momento da redação deste blog:
  • MariaDB 1.5.3
  • MySQL 5.1.39
  • Regue 1.4

O REFERENCIAL


JMH é uma ferramenta de estrutura de microbenchmarking da Oracle desenvolvida pela Oracle, fornecida como ferramentas openJDK, que será o pacote oficial de microbenchmark java 9. Sua vantagem distintiva sobre outros frameworks é que ele é desenvolvido pelos mesmos caras da Oracle que implementam JIT (compilação Just In Time) e permitem evitar a maioria das armadilhas de micro-benchmark.

Fonte do comparativo de mercado: https://github.com/rusher/mariadb-java-driver-benchmark.

Os testes são bem diretos se você estiver familiarizado com java.
Exemplo:
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);
            }
        }
    }
}

Testes usando as consultas de INSERT são enviados para um mecanismo BLACKHOLE com o log binário desabilitado, para evitar E/S e dependência do desempenho do armazenamento. Isso permite ter resultados mais estáveis.
(Sem usar o mecanismo blackhole e desabilitar o log binário, os tempos de execução podem variar até 10%).

Benchmark foram executados nos bancos de dados MariaDB Server 10.1.17 e MySQL Community Server 5.7.13. O documento a seguir mostra os resultados usando os 3 drivers com MariaDB Server 10.1.17. Para os resultados completos, incluindo os do MySQL Server 5.7.13, consulte o link na parte inferior do documento.

AMBIENTE


A execução (cliente e servidor) é feita em um único droplet de servidor no digitalocean.com usando os seguintes parâmetros:
  • Java(TM) SE Runtime Environment (build 1.8.0_101-b13) 64 bits (última versão real ao executar este benchmark)
  • Ubuntu 16.04 64 bits
  • 512 MB de memória
  • 1 processador
  • banco de dados MariaDB “10.1.17-MariaDB”, MySQL Community Server build “5.7.15-0ubuntu0.16.04.1”
    usando arquivos de configuração padrão e estas opções adicionais:
    • max_allowed_packet =40 milhões de pacotes #exchange podem ter até 40 MB
    • character-set-server =utf8 #para usar UTF-8 como padrão
    • collation-server =utf8_unicode_ci #para usar UTF-8 como padrão

Quando indicado “distante”, os benchmarks são executados com cliente e servidor separados em 2 hosts idênticos no mesmo datacenter com um ping médio de 0,350ms.

AMOSTRAS DE EXPLICAÇÕES 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



Isso significa que essa consulta simples levará um tempo médio de 62,715 microssegundos usando o driver MariaDB com uma variação de ± 2,402 microssegundos para 99,9% das consultas.
A mesma execução usando o driver drizzle levará um tempo médio de 88,670 microssegundos, e 78,672 microssegundos usando o conector MySQL (menor tempo de execução, melhor).

As porcentagens exibidas são definidas de acordo com o primeiro resultado do mariadb como referência (100%), permitindo comparar facilmente outros resultados.

COMPARAÇÕES DE DESEMPENHO


O benchmark testará o desempenho dos 3 principais comportamentos diferentes usando um mesmo banco de dados local (mesmo servidor) e um banco de dados distante (outro servidor idêntico) no mesmo datacenter com um ping médio de 0,450ms

Comportamentos diferentes:
Protocolo de texto

Isso corresponde à opção useServerPrepStmts desativada.
As consultas são enviadas diretamente para o servidor com substituição de parâmetros higienizados feita no lado do cliente.
Os dados são enviados como texto. Exemplo:Um carimbo de data/hora será enviado como o texto “1970-01-01 00:00:00.000500” usando 26 bytes
Protocolo binário

Isso corresponde à opção useServerPrepStmts habilitada (implementação padrão no driver MariaDB).
Os dados são enviados em binário. Exemplo de carimbo de data/hora “1970-01-01 00:00:00.000500” será enviado usando 11 bytes.

Existem até 3 trocas com o servidor para uma consulta:
  1. PREPARE – Prepara a instrução para execução.
  2. EXECUTAR – Enviar parâmetros
  3. DEALLOCATE PREPARE – Libera uma declaração preparada.

Consulte a documentação de preparação do servidor para mais informações.

Os resultados do PREPARE são armazenados em cache no lado do driver (tamanho padrão 250). Se Prepare já estiver em cache, PREPARE não será executado, DEALLOCATE será executado somente quando PREPARE não for mais usado e não estiver em cache. Isso significa que algumas execuções de consultas terão 3 viagens de ida e volta, mas algumas terão apenas uma viagem de ida e volta, enviando um identificador e parâmetros PREPARE.
Reescrever

Isso corresponde à opção rewriteBatchedStatements ativada.
Rewrite usa o protocolo de texto e diz respeito apenas a lotes. O driver reescreverá a consulta para obter resultados mais rápidos.

Exemplo:
Inserir em ab (i) valores (?) com os valores do primeiro lote [1] e [2] serão reescritos para
Inserir em ab (i) valores (1), (2).

Se a consulta não puder ser reescrita em “multi-values”, a reescrita usará multi-queries:
Insert into table(col1) values ​​(?) na atualização de chave duplicada col2=? com valores [1,2] e [2,3] serão reescritos para
Inserir na tabela(col1) valores (1) na atualização de chave duplicada col2=2;Inserir na tabela(col1) valores (3) em atualização de chave duplicada col2=4

As desvantagens desta opção são:
  • Os IDs de incremento automático não podem ser recuperados usandoStatement.html#getGeneratedKeys().
  • Multi-consultas em uma execução são habilitadas. Isso não é um problema paraPreparedStatement, mas se o aplicativo usar Statement isso pode ser uma degradação da segurança (injeção de SQL).

* MariaDB e MySQL tem esses 3 comportamentos implementados, Drizzle apenas o protocolo Text.

RESULTADOS DE REFERÊNCIA

Resultados do driver MariaDB

CONSULTA DE SELEÇÃO Ú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




Quando o resultado PREPARE para esta consulta exata já estiver em cache (cache hit), a consulta será mais rápida (7,1% neste exemplo) do que usando o protocolo de texto. Devido às trocas PREPARE e DEALLOCATE de solicitação adicional, o cache miss é 68,1% mais lento.

Isso enfatiza as vantagens e inconvenientes de usar um protocolo binário. Cache HIT é importante.


CONSULTA DE INSERÇÃO Ú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




Os resultados para INSERTs são semelhantes aos resultados de SELECTs.

LOTE:1.000 INSERIR 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





O uso do protocolo binário é aqui mais significativo, tendo resultados 13% mais rápidos do que o uso do protocolo de texto.

As inserções são enviadas em massa e os resultados são lidos de forma assíncrona (que corresponde a optionuseBatchMultiSend). Isso permite ter resultados distantes com desempenho não muito distante dos locais.

Rewrite tem um desempenho incrível, mas não terá ids de incremento automático. Se você não precisa de ids imediatamente e não usa ORM, esta solução será a mais rápida. Alguns ORM permitem que a configuração lide com a sequência internamente para fornecer IDs de incremento, mas essas sequências não são distribuídas, portanto, não funcionarão em clusters.

COMPARAÇÃO COM OUTROS MOTORISTAS

SELECT consulta com resultado de uma linha

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 “High Availability” usando a configuração Master-Slave
(URL de conexão é “jdbc:mysql:replication://localhost:3306,localhost:3306/testj”).

Esses resultados são devidos a muitas opções de implementação diferentes. Aqui estão algumas razões que explicam as diferenças de tempo:

  • O driver MariaDB é otimizado para UTF-8, permitindo menos criação de array de bytes, evitando cópia de array e consumo de memória.
  • Implementação de alta disponibilidade:os drivers MariaDB e MySQL usam uma classe proxy dinâmica Java localizada entre objetos Statement e soquetes, permitindo adicionar comportamento de failover. Essa adição custará uma sobrecarga de 2 microssegundos por consulta (62,715 sem se tornar 64,676 microssegundos).
    Na implementação do MySQL, quase todos os métodos internos são proxy, adicionando uma sobrecarga para muitos métodos que não têm nada a ver com failover, adicionando uma sobrecarga total de 50 microssegundos para cada consulta.

(Drizzle não tem PREPARE, nem funcionalidade HA)

"Selecionar 1.000 linhas"

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



Ao usar muitos dados, o tempo é gasto principalmente na leitura do soquete e no armazenamento do resultado na memória para enviá-lo de volta ao cliente. Se o benchmark estivesse apenas executando o SELECT sem ler os resultados, o tempo de execução do MySQL e do MariaDB seria equivalente. Como o objetivo de uma consulta SELECT é ter resultados, o driver MariaDB é otimizado para retornar resultados (evitando a criação de arrays de bytes).

"Inserir 1.000 linhas"

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




A inserção em massa do MySQL e do Drizzle é como a do X INSERT:Driver envia 1 INSERT, espera o resultado da inserção e envia a próxima inserção. A latência de rede entre cada inserção diminuirá a velocidade das inserções.

Procedimentos da loja

LIGAÇÃO DE PROCEDIMENTO

//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



As implementações do MySQL e do MariaDB diferem completamente. O driver MySQL usará muitas consultas ocultas para obter o resultado de saída:

  • SHOW CREATE PROCEDURE testj.inoutParam para identificar os parâmetros IN e OUT
  • SET @com_mysql_jdbc_outparam_p1 = 1 para enviar dados de acordo com os parâmetros IN / OUT
  • CALL testj.inoutParam(@com_mysql_jdbc_outparam_p1) procedimento de chamada
  • SELECT @com_mysql_jdbc_outparam_p1 para ler o resultado de saída

A implementação do MariaDB é direta usando a capacidade de ter o parâmetro OUT na resposta do servidor sem quaisquer consultas adicionais. (Essa é a principal razão pela qual o driver MariaDB requer o servidor MariaDB/MySQL versão 5.5.3 ou posterior).




CONCLUSÃO


O driver do MariaDB é demais!

O protocolo binário tem vantagens diferentes, mas depende de ter os resultados do PREPARE já no cache. Se os aplicativos tiverem muitos tipos diferentes de consultas e o banco de dados estiver distante, essa pode não ser a melhor solução.

Rewrite tem resultados incríveis para gravar dados em lote

Driver mantém bem contra outros drivers. E há muito por vir, mas isso é outra história.



Resultados brutos:
  1. com um banco de dados MariaDB 10.1.17 local, distante
  2. com um banco de dados MySQL Community Server 5.7.15 (compilação 5.7.15-0ubuntu0.16.04.1) local