Сравнение производительности сжатия данных MySQL на Java 2023

Каков лучший способ хранения двоичных данных в MySQL? Это вопрос, на который есть несколько ответов, в зависимости от ваших целей. Например, если вам нужно оптимизировать размер хранилища, вам, вероятно, потребуется использовать какой-либо алгоритм сжатия, который эффективно сжимает ваши данные. В моём случае мне действительно нужна высокая производительность, то есть максимально быстрое время отклика для извлечения большого двоичного объекта из MySQL.
Давайте отложим в сторону вопрос о том, подходит ли MySQL для хранения двоичных данных. Вопрос здесь будет заключаться в том, как хранить двоичные данные, чтобы считывание из БД происходило как можно быстрее?
Решением может быть использование сжатия данных. Однако это то, что требует сравнительного анализа, поскольку существует компромисс между использованием процессора для распаковки и скоростью сети. В этом тесте я собираюсь сравнить производительность между различными алгоритмами сжатия, сжатия с использованием самого MySQL и без использования сжатия вообще.
Тест производительности
Этот тест производительности будет немного сложнее, чем обычно. Здесь у нас есть 3 основных параметра: сервер MySQL, сеть между MySQL и приложением и алгоритмы сжатия.
Наборы данных и алгоритмы сжатия
Существует 2 набора данных:
- Один основан на реальных пользовательских данных, содержит URL-адреса, UUID и т.д.
- Другой – рандомный, использующий разные размеры алфавита для достижения разных степеней сжатия.
Сжатие на уровне приложения будет происходить с использованием следующих алгоритмов: gzip, deflate, MySQL compression, zstd, snappy, brotli и lz4.
В дополнение к сжатию на уровне приложения мы также собираемся использовать внутреннее сжатие MySQL.
Коэффициенты сжатия для различных алгоритмов и наборов данных вы можете изучить в этой таблице.
BLOB в MySQL
MySQL имеет тип столбца BLOB, который позволяет хранить двоичные данные.
Другим аспектом является формат строки хранилища MySQL. В зависимости от формата строки, MySQL хранит данные по-разному:
DYNAMIC
формат строки для BLOB-объекта хранит 20-байтовый указатель на то, где он хранится. В отличие отREDUNDANT
иCOMPACT
форматов строк, которые хранят первые 768 байт в таблице, а остальные – на страницах переполнения. В этом тесте размеры данных начинаются с 1028 байт, поэтому такого рода оптимизация не имеет значения.COMPRESSED
формат строки позволяет выполнять сжатие на уровне базы данных, поэтому 20-байтовый указатель будет ссылаться на другое хранилище, которое будет сжато с использованием deflate с префиксом длины 4 байта. Для сравнения мы будем использовать тот же алгоритм на уровне приложения, чтобы увидеть накладные расходы MySQL на сжатие.
Итак, первая структура таблицы, которую мы собираемся использовать, такова:
CREATE TABLE `uncompressed_blobs` (
`id` INT NOT NULL PRIMARY KEY,
`data` MEDIUMBLOB NOT NULL
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;
В этой таблице мы будем хранить либо несжатые данные, либо данные, сжатые на прикладном уровне без использования сжатия MySQL (очевидно, мы хотим избежать двойного сжатия, поскольку это неэффективно).
И ещё одна таблица для сравнительного анализа сжатия MySQL:
CREATE TABLE `compressed_blobs` (
`id` INT NOT NULL PRIMARY KEY,
`data` MEDIUMBLOB NOT NULL
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED;
Сеть и аппаратное обеспечение
Для этого бенчмарка у меня получилось 3 различных конфигурации:
localhost
. Это самая простая конфигурация. И JMH benchmark, и MySQL были запущены на одном ноутбуке (i7–1165G7, 16 ГБ оперативной памяти, Ubuntu 22). Эта конфигурация обеспечивает наилучшие сетевые задержки и, предположительно, без особых разногласий, поскольку 2 потока использовались для бенчмарка JMH, а остальные – для MySQLAWS
. Я использовал фактический производственный кластер сервера MySQL с ведущими / ведомыми устройствами с proxysql перед ним. Тест JMH был запущен на отдельном сервере в том же регионе.1Gbit
– это так называемая конфигурация, которую я запустил в своей домашней сети. Сервер MySQL был установлен на старом компьютере с процессором Atom, а тест JMH был запущен с другого ноутбука (i7-U10610U, 32 ГБ оперативной памяти, Ubuntu 20): первый запуск с использованием Wi-Fi; и еще один с использованием Ethernet (у меня нет порта Ethernet на ноутбуке из конфигурацииlocalhost
).
Поскольку производительность сжатия на самом деле не зависит от версии JDK (как я показал в моём предыдущем тесте), я использовал разные версии JDK (openjdk-11 в `AWS` и openjdk-17 на моих ноутбуках).
Пропускные способности
Для этого теста наиболее важным показателем является пропускная способность: сколько байт несжатых данных мы можем получить за секунду. Время отклика не может показать значимый результат, поскольку оно не включает степень сжатия.
Контрольные показатели для настройки AWS
Я бы предположил, что наиболее интересные результаты будут получены для конфигурации AWS
, поскольку она наиболее близка к потенциальному варианту использования в производстве.
Здесь мы можем увидеть диаграмму пропускной способности для реального набора данных. Его размер варьируется от 600 КБ до 4 МБ:

Здесь вы можете увидеть лучшие результаты:

Вы можете видеть, что lz4 лидирует по сравнению с несжатым BLOB-объектом, а любой другой алгоритм работает медленнее. Чем больше BLOB, тем существеннее разница.
Давайте взглянем на меньший набор данных — от 34 КБ до 94 КБ:

Здесь картина более интересная, но это обосновывается тем, что степень сжатия lz4 для конкретных примеров значительно хуже. Однако важно отметить следующее — степень сжатия имеет большое значение. И чтобы проиллюстрировать этот момент, давайте проверим производительность для случайного набора данных при различных степенях сжатия:

Вы можете видеть следующий результат: больший размер и лучшая степень сжатия у lz4.
Бенчмарки для localhost
Что на самом деле показывает localhost
, так это случай, когда сеть работает максимально быстро:

Мы можем видеть, что производительность в несжатом виде значительно выше, чем у любого другого алгоритма, за исключением lz4.
Если вы помните, на графике из конфигурации AWS для больших двоичных объектов среднего размера lz4 показал результат похуже (из-за плохой степени сжатия). Однако, когда вы улучшаете пропускную способность MySQL (сети), lz4 продолжает сиять:

Тесты для локальной сети и слабого сервера
Несмотря на то, что в реальной производственной среде маловероятно столкнуться с такой конфигурацией, она может показать что-то интересное:

Здесь мы ясно видим, что несжатый большой двоичный объект выдаёт около 30 МБ / с, а для этого трудно показать какие-либо хорошие результаты для быстрых алгоритмов. И в таких обстоятельствах brotli блистает, поскольку у него лучшая степень сжатия среди всех алгоритмов, поэтому низкая пропускная способность MySQL хорошо компенсируется большой степенью сжатия и разумной производительностью декодирования (я хочу напомнить вам, что 11-й уровень brotli чрезвычайно медленный при кодировании).
Сравнения
Я также провёл пару интересных (на мой взгляд) сравнений.
Алгоритм сжатия MySQL
Первое сравнение проводится между ROW_FORMAT=COMPRESSED
и использованием функции UNCOMPRESS MySQL над сжатыми данными в ROW_FORMAT=DYNAMIC
. По сути, мы сравниваем организацию хранилища с простой декомпрессией.

Интересно, что выполнение SELECT UNCOMPRESS(data)
осуществляется немного быстрее, чем автоматическое распаковывание данных из самого хранилища.
Следующий шаг – сравнить получение несжатых данных из MySQL с помощью SELECT UNCOMPRESS(data)
и получение сжатых данных из MySQL и их распаковку в самом приложении.

Как и ожидалось, более низкое использование сети очень выгодно: распаковка в Java выполняется значительно быстрее. Но с идеальной сетью (а именно localhost
) это будет работать по-другому:

Мы можем видеть, что Java выигрывает только при самой высокой степени сжатия. В других случаях MySQL работает быстрее. Что, вероятно, означает, что реализация распаковки из MySQL выполняется быстрее (это имеет смысл, поскольку Java копирует много памяти и выделяет некоторые ресурсы).
lz4 vs Uncompressed
Давайте немного подробнее рассмотрим lz4 и uncompressed, поскольку они являются явными победителями в конфигурациях localhost
и AWS
. Вот небольшая анимация, чтобы увидеть lz4 против uncompressed во всех конфигурациях:

А вот lz4 против uncompressed для конфигурации AWS. Здесь очевидно, что lz4 явно выигрывает:

Заключение
Для нашего конкретного варианта использования lz4 выглядит очень многообещающе. Когда степень сжатия невелика, она немного проигрывает, но в целом превосходит все остальные варианты. Следующий вариант – вообще не использовать сжатие.
Но очень важно помнить, что сжатие на уровне приложения имеет некоторые недостатки:
- Данные не могут быть использованы внутри базы данных. Итак, если вы, например, храните JSON в большом двоичном объекте, вы не сможете использовать функции JSON.
- Это перемещает вычисления процессора из MySQL в ваше приложение (что также может быть преимуществом в некоторых случаях).
- Это требует больше усилий со стороны приложения.
Следующее, на что следует обратить внимание, – это степень сжатия. Знайте свои данные, проводите сравнительный анализ с вашими данными, поскольку производительность значительно зависит от фактической степени сжатия.
И последнее — конфигурация вашей среды также может сильно повлиять на решение, так как для более медленных серверов бенчмарк показывает совершенно противоположные результаты.
Исходный код находится на GitHub.