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:
- PREPARE – Prepara a instrução para execução.
- EXECUTAR – Enviar parâmetros
- 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 OUTSET @com_mysql_jdbc_outparam_p1 = 1
para enviar dados de acordo com os parâmetros IN / OUTCALL testj.inoutParam(@com_mysql_jdbc_outparam_p1)
procedimento de chamadaSELECT @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:
- com um banco de dados MariaDB 10.1.17 local, distante
- com um banco de dados MySQL Community Server 5.7.15 (compilação 5.7.15-0ubuntu0.16.04.1) local