Александр Зайцев

Современные аналитические базы данных не существовали бы без эффективного сжатия данных. Хранилище становится дешевле и производительнее, но объемы данных обычно растут еще быстрее. Закон Мура для больших данных превосходит аналогичный для аппаратного обеспечения. В нашем блоге мы уже писали о сжатии ClickHouse (https://www.altinity.com/blog/2017/11/21/compression-in-clickhouse) и обертке типа данных Low Cardinality (https: // www. altinity.com/blog/2019/3/27/low-cardinality ). В этой статье мы опишем и протестируем самые продвинутые кодировки ClickHouse, которые особенно хорошо подходят для данных временных рядов. Мы гордимся тем, что некоторые из этих кодировок были добавлены в ClickHouse компанией Altinity.

В этой статье представлен предварительный предварительный просмотр новых функций кодирования для ClickHouse версии 19.11. На момент написания выпуск 19.11 еще не доступен. Для тестирования новых кодировок ClickHouse может быть собран из исходного кода или может быть установлена ​​тестовая сборка. Мы ожидаем, что релиз 19.11 ClickHouse должен стать общедоступным через несколько недель.

Обзор кодирования и сжатия

Кодирование и сжатие используются с той же целью - мы используем их для более компактного хранения данных. Однако они по-разному преобразуют данные. Кодирование отображает исходные данные в другую (закодированную) форму, элемент за элементом. Вы можете думать об этом как о непрерывном математическом отображении. Например, словарная кодировка отображает строки в словарные индексы. Кодирование учитывает кодируемый тип данных. С другой стороны, сжатие предполагает использование некоего универсального алгоритма, который сжимает байты, а не типы данных.

В течение многих лет ClickHouse поддерживал два алгоритма сжатия: LZ4 (используется по умолчанию) и ZSTD. У этих алгоритмов иногда есть логика для применения кодировок внутри, но они не знают о типах данных ClickHouse. Знание типа данных и природы данных позволяет нам более эффективно кодировать данные. За последние несколько месяцев синтаксис ClickHouse был расширен с целью включения кодировок на уровне столбцов, и в ClickHouse были добавлены новые кодировки:

  • Дельта. Дельта-кодирование сохраняет разницу между последовательными значениями. Разница обычно имеет меньший размер байта и количество элементов, особенно для последовательностей. Позже его можно эффективно сжать с помощью LZ4 или ZSTD.
  • DoubleDelta. В этой кодировке ClickHouse сохраняет разницу между последовательными дельтами. Это дает даже лучшие результаты для медленно меняющихся последовательностей. Если использовать аналогию из физики, Delta кодирует скорость, а DoubleDelta кодирует ускорение.
  • Горилла. Этот алгоритм вдохновлен статьей в Facebook некоторое время назад [http://www.vldb.org/pvldb/vol8/p1816-teller.pdf], и никто больше не помнит академическое название алгоритма. Кодирование Gorilla очень эффективно для значений, которые не часто меняются. Это применимо как к типам данных с плавающей запятой, так и к целочисленным.
  • T64. Эта кодировка уникальна для ClickHouse. Он вычисляет максимальные и минимальные значения для закодированного диапазона, а затем удаляет старшие биты, транспонируя 64-битную матрицу (отсюда и происходит название T64). В итоге мы получаем более компактное битовое представление тех же данных. Кодировка универсальна для целочисленных типов данных и не требует от данных каких-либо особых свойств, кроме местоположения значений.

Кодировки можно указать как часть определения столбца с помощью ключевого слова «Кодек». В приведенном ниже примере определяется кодировка DoubleDelta для столбца. Обратите внимание, что он отключает сжатие по умолчанию.

ts DateTime Codec(DoubleDelta) -- encoded but NOT compressed

Чтобы обеспечить как кодирование, так и сжатие, кодеки можно объединить в цепочку, как показано ниже:

ts DateTime Codec(DoubleDelta, LZ4) -- encoded AND compressed

Кодировки Delta, DoubleDelta и Gorilla можно найти во многих базах данных временных рядов. Delta и DoubleDelta традиционно используются для меток времени, а кодировка Gorilla используется для значений. InfluxDB и хранилище Prometheus - хорошие примеры. Как мы продемонстрировали несколько месяцев назад [https://www.altinity.com/blog/clickhouse-for-time-series], ClickHouse хорошо конкурирует с InfluxDB с точки зрения производительности, но уступает по эффективности сжатия. . Использование новых кодировок устранит пробел. Давай проверим их!

Методология испытаний

Мы собираемся протестировать кодировки по нескольким критериям:

  • Эффективность кодирования без сжатия для разных типов данных
  • Эффективность кодирования с применением сжатия LZ4 и ZSTD
  • Эффективность сжатия LZ4 и ZSTD применительно к закодированным данным

Мы использовали 4 разных сгенерированных набора данных по 1000000 строк в каждом, которые мы храним в разных таблицах:

  • Монотонная последовательность чисел с постоянным приращением (codec_test1_seq).
    Примеры данных: 0,1000,2000,3000,4000,5000,6000,7000,8000,9000,…
  • Монотонная последовательность со случайным приращением (codec_test2_mon).
    Примеры данных: 22,1072,2034,3076,4007,5094,6061,7074,8061,9058,…
  • Постоянная последовательность со случайными выбросами (codec_test3_var).
    Примеры данных: 1825,1000,1000,1000,1000,1000,1000,1703,1000,1000,…
  • Рандомизированный набор значений ниже 1B (codec_test4_rand).
    Примеры данных: 608745218, 167444263, 620949842, 956686395, 66062863,…

Первые три набора данных типичны для приложений временных рядов. Рандомизированный набор - это наихудший сценарий. Каждая таблица набора данных имеет несколько столбцов, которые представляют различные комбинации типа данных, кодировки и сжатия. Мы также сохраняем незашифрованный столбец в качестве ссылки. Это позволяет нам сравнивать различные комбинации внутри наборов данных и между наборами данных. Подводя итог, мы тестируем следующие комбинации:

  • Типы данных Int32, Int64 *
  • Нет, кодировки Delta, DoubleDelta, Gorilla, T64
  • Нет, сжатие LZ4, ZSTD **

* Изначально мы планировали протестировать Float32 и Float64 с кодировкой Gorilla, но удалили ее, чтобы сделать статью более лаконичной. Вы по-прежнему можете найти типы данных с плавающей запятой и комбинации кодирования в скриптах в Приложении.
** Можно настроить уровень сжатия, но использовались уровни сжатия по умолчанию.

Эффективность сжатия и кодирования можно увидеть из таблицы ClickHouse system.columns, которая отображает сжатый и несжатый размер для каждого столбца, а также кодировки и сжатия, примененные к столбцу.

Для тех, кто интересуется подробными примерами SQL, вы можете найти скрипты в Приложении в конце статьи.

Результаты эффективности кодирования и сжатия: целочисленные типы

При просмотре результатов ниже помните о размере исходных данных: некодированные и несжатые. Это 4 000 000 байт для Int32 и 8 000 000 байт для Int64. Мы также включаем размер незашифрованных данных в качестве справки.

Во-первых, давайте посмотрим на несжатые данные.

Как видите, Delta вообще не сжимает данные, поскольку размер дельты по умолчанию совпадает с типом данных. Мы увидим, что с LZ4 он будет работать лучше. DoubleDelta лучше всего подходит для однородных и монотонных последовательностей, но не так хорош для случайных данных.

Также очень интересно посмотреть, как работают Gorilla и T64. Случайные данные - лучший пример. Поскольку мы сгенерировали случайные значения ниже 1B, данные умещаются в 30 бит. T64 может обнаружить это и отлично удалить лишние биты. Вы можете отчетливо видеть это как для Int32, так и для Int64 - размер данных примерно одинаков и близок к 3 750 000 байт (1 000 000 * 30/8).

Gorilla немного хуже работает с набором случайных данных (32 бита), но отлично подходит для набора данных var. «Калибровочные» данные являются основным вариантом использования такого кодирования, хотя мы были удивлены производительностью Gorilla для целочисленных типов данных в целом.

Теперь давайте посмотрим на сжатие LZ4.

При сжатии LZ4 дельта-кодирование уже подходит для всего, кроме случайного набора данных. DoubleDelta, очевидно, лучше всего подходит для случаев с временными рядами.

А теперь посмотрим на ZSTD.

Интересно, что ZSTD может сжимать Delta для последовательностей более эффективно, чем DoubleDelta. Степень сжатия последовательностей впечатляет 1: 800+ как с дельта-кодированием, так и с ZSTD. T64 снова лучше всего подходит для случайных данных.

Давайте посмотрим на это под другим углом. Для отчета ниже мы объединили все наборы данных временных рядов, чтобы сравнить эффекты сжатия и кодирования вместе.

Подводя итог, можно сказать, что Delta и DoubleDelta хорошо работают для данных, специфичных для временных рядов. DoubleDelta очень эффективен с LZ4, поэтому добавление ZSTD не улучшает его. T64 и Gorilla - отличные кодеки общего назначения, которые можно рекомендовать для всех случаев, когда шаблон данных неизвестен. Gorilla лучше подходит для временных рядов, но T64 более удобен для сжатия. Даже если Gorilla может быть более эффективным на начальном этапе, сжатие более эффективно с T64. Но разница не такая уж и большая. Обратите внимание, однако, что Gorilla может кодировать числа с плавающей запятой с той же эффективностью, поэтому для значений с плавающей запятой это не проблема.

Вывод

Кодеки ClickHouse очень помогают улучшить общее сжатие, уменьшить объем хранилища и повысить производительность за счет меньшего количества операций ввода-вывода. Важно понимать природу данных и выбирать правильный кодек. Дельта-кодирование лучше всего подходит для хранения столбцов времени, DoubleDelta должна очень хорошо сжиматься для увеличения счетчиков, а Gorilla лучше всего для датчиков. T64 можно использовать для целочисленных данных, если вы не храните случайные хэши. Использование кодов также позволяет придерживаться быстрого LZ4 и снизить нагрузку на ЦП при распаковке данных.

Однако есть возможности для улучшения. В частности, похоже, что DoubleDelta может быть улучшена для Int32, на прошлой неделе была добавлена ​​новая версия кодека T64, и есть статья VictoriaMetrics, в которой говорится о значительном улучшении с использованием модифицированного алгоритма Gorilla [https://medium.com / faun / victoriametrics-достижения-лучшего-сжатия-данных-временных-рядов-данных-чем-горилла-317bc1f95932 ]. Мы ожидаем, что в ближайшие несколько месяцев в ClickHouse будут доступны еще более эффективные кодеки.

Можно пожаловаться, что в этой статье отсутствуют тесты производительности. Тесты производительности на синтетических данных могут ввести в заблуждение. Мы собираемся протестировать реальный эффект с помощью тестов TSBS. Быть в курсе.

Приложение: Подготовка тестовых данных

Ниже приведены сценарии для повторения наших тестов.

DROP TABLE IF EXISTS codec_test1_seq;
CREATE TABLE codec_test1_seq (
 n Int32,
 /* No compression */
 n32               Int32 default n Codec(NONE),
 n32_delta         Int32 default n Codec(Delta),
 n32_doubledelta   Int32 default n Codec(DoubleDelta),
 n32_t64           Int32 default n Codec(T64),
 n32_gorilla       Int32 default n Codec(Gorilla),
 n64               Int64 default n Codec(NONE),
 n64_delta         Int64 default n Codec(Delta),
 n64_doubledelta   Int64 default n Codec(DoubleDelta),
 n64_t64           Int64 default n Codec(T64),
 n64_gorilla       Int64 default n Codec(Gorilla),
 f32               Float32 default n Codec(NONE),
 f64               Float64 default n Codec(NONE),
 f32_gorilla       Float32 default n Codec(Gorilla),
 f64_gorilla       Float64 default n Codec(Gorilla),
 /* LZ4 compression */
 l_n32             Int32 default n Codec(LZ4),
 l_n32_delta       Int32 default n Codec(Delta, LZ4),
 l_n32_doubledelta Int32 default n Codec(DoubleDelta, LZ4),
 l_n32_t64         Int32 default n Codec(T64, LZ4),
 l_n32_gorilla     Int32 default n Codec(Gorilla, LZ4),
 l_n64             Int64 default n Codec(LZ4),
 l_n64_delta       Int64 default n Codec(Delta, LZ4),
 l_n64_doubledelta Int64 default n Codec(DoubleDelta, LZ4),
 l_n64_t64         Int64 default n Codec(T64, LZ4),
 l_n64_gorilla     Int64 default n Codec(Gorilla, LZ4),
 l_f32             Float32 default n Codec(LZ4),
 l_f64             Float64 default n Codec(LZ4),
 l_f32_gorilla     Float32 default n Codec(Gorilla, LZ4),
 l_f64_gorilla     Float64 default n Codec(Gorilla, LZ4),
 /* ZSTD compression */
 z_n32             Int32 default n Codec(ZSTD),
 z_n32_delta       Int32 default n Codec(Delta, ZSTD),
 z_n32_doubledelta Int32 default n Codec(DoubleDelta, ZSTD),
 z_n32_t64         Int32 default n Codec(T64, ZSTD),
 z_n32_gorilla     Int32 default n Codec(Gorilla, ZSTD),
 z_n64             Int64 default n Codec(ZSTD),
 z_n64_delta       Int64 default n Codec(Delta, ZSTD),
 z_n64_doubledelta Int64 default n Codec(DoubleDelta, ZSTD),
 z_n64_t64         Int64 default n Codec(T64, ZSTD),
 z_n64_gorilla     Int64 default n Codec(Gorilla, ZSTD),
 z_f32             Float32 default n Codec(ZSTD),
 z_f64             Float64 default n Codec(ZSTD),
 z_f32_gorilla     Float32 default n Codec(Gorilla, ZSTD),
 z_f64_gorilla     Float64 default n Codec(Gorilla, ZSTD)
) Engine = MergeTree 
PARTITION BY tuple() ORDER BY tuple();
DROP TABLE IF EXISTS codec_test2_mon;
CREATE TABLE codec_test2_mon AS codec_test1_seq;
DROP TABLE IF EXISTS codec_test3_var;
CREATE TABLE codec_test3_var AS codec_test1_seq;
DROP TABLE IF EXISTS codec_test4_rand;
CREATE TABLE codec_test4_rand AS codec_test1_seq;
insert into codec_test1_seq (n)
select number*1000 from numbers(1000000) settings max_block_size=1000000;
insert into codec_test2_mon (n)
select number*1000+(rand()%100) from numbers(1000000) settings max_block_size=1000000;
insert into codec_test3_var (n)
select 1000 + (rand(1)%1000) * (rand(2)%10 = 0) from numbers(1000000) settings max_block_size=1000000;
insert into codec_test4_rand (n)
select rand()%(1000000*1000) from numbers(1000000) settings max_block_size=1000000;
/* sample rows */
select table, groupArray(n) samples from (
select 'codec_test1_seq' table, n from codec_test1_seq limit 10
union all
select 'codec_test2_mon' table, n from codec_test2_mon limit 10
union all
select 'codec_test3_var' table, n from codec_test3_var limit 10
union all
select 'codec_test4_rand' table, n from codec_test4_rand limit 10
)
group by table order by table;
/* validation. It is complicated because of mix of Int and Float types */
select arrayDistinct(avgForEach(CAST(replaceOne(replaceOne(toString(tuple(*)), '(', '['), ')', ']'), 'Array(Int64)'))) avg from codec_test1_seq
union all
select arrayDistinct(avgForEach(CAST(replaceOne(replaceOne(toString(tuple(*)), '(', '['), ')', ']'), 'Array(Int64)'))) avg from codec_test2_mon
union all
select arrayDistinct(avgForEach(CAST(replaceOne(replaceOne(toString(tuple(*)), '(', '['), ')', ']'), 'Array(Int64)'))) avg from codec_test3_var
union all
select arrayDistinct(avgForEach(CAST(replaceOne(replaceOne(toString(tuple(*)), '(', '['), ')', ']'), 'Array(Int64)'))) avg from codec_test4_rand;
/* Encoding benchmarks results */
select multiIf(table like '%rand', 'Random', 'Time-Series') dataset_type,
       table, type as data_type, 
       multiIf(compression_codec like '%ZSTD%', 'ZSTD', compression_codec like '%LZ4%', 'LZ4', ' None') compression,
       multiIf(compression_codec like '%DoubleDelta%', 'DoubleDelta', 
               compression_codec like '%Delta%', 'Delta', 
               compression_codec like '%T64%', 'T64',
               compression_codec like '%Gorilla%', 'Gorilla', 
               ' None') encoding,
compression_codec codec, 
sum(data_uncompressed_bytes) uncompressed, 
sum(data_compressed_bytes) compressed, 
round(uncompressed/compressed,1) ratio 
from system.columns 
where table like 'codec_test%' and name != 'n' 
group by dataset_type, table, data_type, compression, encoding, codec
order by dataset_type, table, data_type, compression, encoding, codec
FORMAT CSVWithNames;

Первоначально опубликовано на https://www.altinity.com 10 июля 2019 г.