WYDAJNOŚĆ ZŁĄCZA MARIADB JAVA
Zawsze mówimy o wydajności. Ale zawsze chodzi o „Zmierz, nie zgaduj!”.
Ostatnio w MariaDB Java Connector wprowadzono wiele ulepszeń wydajności. Więc jaka jest aktualna wydajność sterownika?
Pozwólcie, że podzielę się wynikiem testu porównawczego 3 sterowników jdbc umożliwiających dostęp do bazy danych MySQL/MariaDB:DrizzleJDBC, MySQL Connector/J i MariaDB java connector.
Wersje sterowników to najnowsza dostępna wersja GA w momencie pisania tego bloga:
- MariaDB 1.5.3
- MySQL 5.1.39
- Mżawka 1.4
WSKAŹNIK
JMH to narzędzie Oracle micro-benchmarking opracowane przez Oracle, dostarczane jako narzędzia openJDK, które będzie oficjalnym pakietem microbenchmark java 9. Jego wyróżniającą przewagą nad innymi frameworkami jest to, że jest rozwijany przez tych samych facetów w Oracle, którzy wdrażają JIT (kompilacja Just In Time) i pozwalają uniknąć większości pułapek mikro-benchmarkowych.
Źródło testu porównawczego:https://github.com/rusher/mariadb-java-driver-benchmark.
Testy są całkiem proste, jeśli znasz java.
Przykład:
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); } } } }
Testy wykorzystujące zapytania INSERT są wysyłane do mechanizmu BLACKHOLE z wyłączonym dziennikiem binarnym, aby uniknąć operacji we/wy i zależności od wydajności pamięci. Pozwala to na uzyskanie bardziej stabilnych wyników.
(Bez korzystania z silnika czarnej dziury i wyłączenia dziennika binarnego czasy wykonania mogą się różnić do 10%).
Testy porównawcze zostały wykonane na bazach danych MariaDB Server 10.1.17 i MySQL Community Server 5.7.13. Poniższy dokument przedstawia wyniki przy użyciu 3 sterowników z MariaDB Server 10.1.17. Aby uzyskać pełne wyniki, w tym te z MySQL Server 5.7.13, zobacz link na dole dokumentu.
ŚRODOWISKO
Wykonywanie (klient i serwer) odbywa się na pojedynczym serwerze droplet na digitalocean.com przy użyciu następujących parametrów:
- Java(TM) SE Runtime Environment (kompilacja 1.8.0_101-b13) 64-bitowe (rzeczywista ostatnia wersja podczas uruchamiania tego testu porównawczego)
- Ubuntu 16.04 64 bity
- 512 MB pamięci
- 1 procesor
- baza danych MariaDB „10.1.17-MariaDB”, kompilacja MySQL Community Server „5.7.15-0ubuntu0.16.04.1”
przy użyciu domyślnych plików konfiguracyjnych i tych dodatkowych opcji:- max_allowed_packet =40M #pakiet wymiany może mieć do 40mb
- serwer-zestawu znaków =utf8 #aby używać UTF-8 jako domyślnego
- sortowanie-serwer =utf8_unicode_ci #aby używać UTF-8 jako domyślnego
Gdy wskazano „odległe”, testy porównawcze są uruchamiane z oddzielnym klientem i serwerem na 2 identycznych hostach w tym samym centrum danych ze średnim pingiem 0,350 ms.
WYNIKI PRZYKŁADOWE WYJAŚNIENIA
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
Oznacza to, że to proste zapytanie zajmie średni czas 62,715 mikrosekund przy użyciu sterownika MariaDB z różnicą ± 2,402 mikrosekund dla 99,9% zapytań.
To samo wykonanie przy użyciu sterownika drizzle zajmie średni czas 88,670 mikrosekund, a 78,672 mikrosekund przy użyciu złącza MySQL (mniejszy czas wykonania, tym lepiej).
Wyświetlane wartości procentowe są ustawione zgodnie z pierwszym wynikiem mariadb jako odniesieniem (100%), co pozwala na łatwe porównanie innych wyników.
PORÓWNANIA WYDAJNOŚCI
Test porównawczy przetestuje wydajność 3 głównych różnych zachowań przy użyciu tej samej lokalnej bazy danych (ten sam serwer) i odległej bazy danych (inny identyczny serwer) w tym samym centrum danych ze średnim pingiem 0,450 ms
Różne zachowania:
Protokół tekstowy
Odpowiada to wyłączonej opcji useServerPrepStmts.
Kwerendy są wysyłane bezpośrednio do serwera, a zamiana parametrów oczyszczonych odbywa się po stronie klienta.
Dane są wysyłane jako tekst. Przykład:znacznik czasu zostanie wysłany w postaci tekstu „1970-01-01 00:00:00.000500” przy użyciu 26 bajtów
Protokół binarny
Odpowiada to włączonej opcji useServerPrepStmts (domyślna implementacja w sterowniku MariaDB).
Dane są wysyłane w postaci binarnej. Przykładowy znacznik czasu „1970-01-01 00:00:00.000500” zostanie wysłany z użyciem 11 bajtów.
Istnieje do 3 wymian z serwerem dla jednego zapytania :
- PREPARE – Przygotowuje oświadczenie do wykonania.
- WYKONAJ – Wyślij parametry
- ZDEJMIJ PRZYGOTOWANIE — publikuje przygotowane oświadczenie.
Zobacz Dokumentację przygotowania serwera, aby uzyskać więcej informacji.
Wyniki PREPARE są przechowywane w pamięci podręcznej po stronie sterownika (domyślny rozmiar 250). Jeśli Przygotuj jest już w pamięci podręcznej, PREPARE nie zostanie wykonane, DEALLOCATE zostanie wykonane tylko wtedy, gdy PREPARE nie jest już używane i nie znajduje się w pamięci podręcznej. Oznacza to, że wykonanie niektórych zapytań będzie miało 3 podróże w obie strony, ale niektóre będą miały tylko jedną podróż w obie strony, wysyłając identyfikator i parametry PREPARE.
Przepisz
Odpowiada to opcji rewriteBatchedStatements enabled.
Rewrite używa protokołu tekstowego i dotyczy tylko partii. Sterownik przepisze zapytanie, aby uzyskać szybsze wyniki.
Przykład:
Wstaw do ab (i) wartości (?) z pierwszymi wartościami partii [1] i [2] zostaną przepisane na
Wstaw do ab (i) wartości (1), (2).
Jeśli zapytanie nie może zostać przepisane w „wielowartościowych”, przepisanie użyje wielu zapytań :
Wstaw do tabeli(col1) wartości (?) przy aktualizacji zduplikowanego klucza col2=? z wartościami [1,2] i [2,3] zostaną przepisane na
Wstaw do table(col1) wartości (1) przy zduplikowanym kluczu aktualizacja col2=2;Wstaw do table(col1) wartości (3) na zduplikowana aktualizacja klucza col2=4
Wady tej opcji to:
- Identyfikatorów automatycznego przyrostu nie można pobrać za pomocąStatement.html#getGeneratedKeys().
- Włączono wiele zapytań w jednym wykonaniu. Nie stanowi to problemu dlaPreparedStatement, ale jeśli aplikacja używa instrukcji, może to spowodować pogorszenie bezpieczeństwa (wstrzyknięcie SQL).
* MariaDB i MySQL mają zaimplementowane te 3 zachowania, Drizzle tylko protokół Text.
WYNIKI BENCHMARK
Wyniki sterownika MariaDB
POJEDYNCZE ZAPYTANIE
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
Gdy wynik PREPARE dla tego dokładnego zapytania znajduje się już w pamięci podręcznej (trafienie w pamięci podręcznej), zapytanie będzie szybsze (7,1% w tym przykładzie) niż przy użyciu protokołu tekstowego. Ze względu na dodatkowe żądanie wymian PREPARE i DEALLOCATE, brak pamięci podręcznej jest o 68,1% wolniejszy.
Podkreśla to zalety i niedogodności korzystania z protokołu binarnego. HIT pamięci podręcznej jest ważny.
ZAPYTANIE Z POJEDYNCZYM WSTAWIENIEM
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
Wyniki dla INSERT są podobne do wyników SELECT.
PARTIA:1000 WSTAW ZAPYTANIE
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
Korzystanie z protokołu binarnego jest tutaj ważniejsze, dając wyniki o 13% szybsze niż przy użyciu protokołu tekstowego.
Inserty są wysyłane masowo, a wyniki odczytywane asynchronicznie (co odpowiada opcjiuseBatchMultiSend). Pozwala to na uzyskanie odległych wyników z wydajnością niedaleko od lokalnych.
Rewrite ma niesamowicie dobrą wydajność, ale nie będzie miał identyfikatorów automatycznego zwiększania. Jeśli nie potrzebujesz identyfikatorów od razu i nie korzystasz z ORM, to rozwiązanie będzie najszybsze. Niektóre ORM pozwalają konfiguracji na obsługę sekwencji wewnętrznie w celu zapewnienia identyfikatorów przyrostu, ale te sekwencje nie są rozproszone, więc nie będą działać w klastrach.
PORÓWNANIE Z INNYMI KIEROWCAMI
WYBIERZ zapytanie z wynikiem jednego wiersza
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 oznacza „High Availability” przy użyciu konfiguracji Master-Slave
(adres URL połączenia to „jdbc:mysql:replication://localhost:3306,localhost:3306/testj”).
Wyniki te wynikają z wielu różnych wyborów wdrożeniowych. Oto kilka powodów, które wyjaśniają różnice czasowe:
- Sterownik MariaDB jest zoptymalizowany pod kątem UTF-8, pozwalając na mniej tworzenia tablicy bajtów, unikając kopiowania tablicy i zużycia pamięci.
- Implementacja HA:sterowniki MariaDB i MySQL używają dynamicznej klasy Proxy Java znajdującej się między obiektami Statement i gniazdami, co pozwala na dodanie zachowania awaryjnego. Te dodanie będą kosztować 2 mikrosekundy na zapytanie (62.715 bez przekształcenia się w 64,676 mikrosekund).
W implementacji MySQL prawie wszystkie metody wewnętrzne są udostępniane za pośrednictwem proxy, dodając obciążenie dla wielu metod, które nie mają nic wspólnego z przełączaniem awaryjnym, dodając łączny narzut 50 mikrosekund na każde zapytanie.
(Drizzle nie ma funkcji PREPARE, ani funkcji HA)
„Wybierz 1000 wierszy”
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
W przypadku korzystania z dużej ilości danych, czas spędzany jest głównie na odczytywaniu z gniazda i przechowywaniu wyników w pamięci w celu wysłania ich z powrotem do klienta. Gdyby benchmark wykonywał tylko SELECT bez odczytywania wyników, czas wykonania MySQL i MariaDB byłby równoważny. Ponieważ celem zapytania SELECT jest uzyskanie wyników, sterownik MariaDB jest zoptymalizowany pod kątem zwracania wyników (unikając tworzenia tablic bajtów).
„Wstaw 1000 wierszy”
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
Wstawianie zbiorcze MySQL i Drizzle jest jak X INSERT:Sterownik wysyła 1 INSERT, czeka na wynik wstawienia i wysyła następną wstawkę. Opóźnienie sieci między poszczególnymi wstawkami spowolni wstawianie.
Procedury sklepowe
PROCEDURA POŁĄCZENIA
//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
Implementacje MySQL i MariaDB całkowicie się różnią. Sterownik Mysql użyje wielu ukrytych zapytań, aby uzyskać wynik wyjściowy:
SHOW CREATE PROCEDURE testj.inoutParam
do identyfikacji parametrów IN i OUTSET @com_mysql_jdbc_outparam_p1 = 1
do wysyłania danych zgodnie z parametrami IN / OUTCALL testj.inoutParam(@com_mysql_jdbc_outparam_p1)
procedura połączeniaSELECT @com_mysql_jdbc_outparam_p1
odczytywanie wyniku wyjściowego
Implementacja MariaDB jest prosta dzięki możliwości posiadania parametru OUT w odpowiedzi serwera bez żadnych dodatkowych zapytań. (To główny powód, dla którego sterownik MariaDB wymaga serwera MariaDB/MySQL w wersji 5.5.3 lub nowszej).
WNIOSEK
Sterownik MariaDB buja!
Protokół binarny ma różne zalety, ale opiera się na tym, że wyniki PREPARE znajdują się już w pamięci podręcznej. Jeśli aplikacje mają wiele różnego rodzaju zapytań, a baza danych jest odległa, może to nie być lepszym rozwiązaniem.
Rewrite ma niesamowite wyniki, aby zapisywać dane w partii
Kierowca trzyma się dobrze w porównaniu z innymi kierowcami. A przed nami wiele, ale to już inna historia.
Surowe wyniki:
- z lokalną, odległą bazą danych MariaDB 10.1.17
- z bazą danych MySQL Community Server 5.7.15 (kompilacja 5.7.15-0ubuntu0.16.04.1) lokalna