Русский
Русский
English
Статистика
Реклама

ClickHouse как устроен MergeTree

Моя команда использует ClickHouse как хранилище для 100 млрд записей страфиком по300 млн всутки ипоиском потаблице. Ярасскажу обустройстве движка таблиц MergeTree. Рассказ буду вести, показывая физические данные, анеабстрактные схемы.



MergeTree это сердце ClickHouse, остальные движки скорее вспомогательные. Название отсылает кLSM-дереву, которое давно используется вдругих СУБД.



Последовательный доступ решает, даже для SSD


ClickHouse задумывался для аналитики, под большую пропускную способность записи ичтения. Чтобы получить максимальную пропускную способность записи можно просто писать вконец файла, без поддержания всяких индексов ипроизвольных seekов, нокак без них обеспечить скорость чтения? Ведь поиск попроизвольным данным без индекса быстрым быть неможет. Получается, нужно либо упорядочивать вмомент вставки, либо фуллсканить вмомент чтения. Оба варианта гробят пропускную способность. ClickHouse решил упорядочивать посередине: вфоне.


Так ипоявился компромисс ввиде MergeTree: пишем надиск небольшие куски отсортированных данных, которые ClickHouse сливает вфоне для поддержания упорядоченности.


Концептуальная идея, надеюсь, ясна, приступим крассмотрению работы MergeTree подмикроскопом.


Лабораторная работа


Достанем из-под парты ClickHouse версии 20.8.9.6 исоздадим таблицу снебольшим числом колонок. Вкачестве первичного ключа выберем user_id. ClickHouse создал папку /var/lib/clickhouse/data/default/clicks.


Про index_granularity чуть позже.


create table clicks(    date      DateTime,    user_id   Int64,    banner_id String) engine = MergeTree() order by user_id settings index_granularity = 2;

Вставим 10строчек:


+-------------------+-------+---------+|date               |user_id|banner_id|+-------------------+-------+---------+|2021-01-01 15:43:58|157    |lqe(|    ||2021-01-01 15:45:38|289    |freed    ||2021-01-01 15:47:18|421    |08&N0    ||2021-01-01 15:48:58|553    |n5UD$    ||2021-01-01 15:50:38|685    |1?!Up    ||2021-01-01 15:52:18|817    |caHy6    ||2021-01-01 15:53:58|949    |maXZD    ||2021-01-01 15:55:38|1081   |Fx:BO    ||2021-01-01 15:57:18|1213   |\v8j+    ||2021-01-01 15:58:58|1345   |szEG)    |+-------------------+-------+---------+

Надиске появился один кусок данных (data part) all_1_1_0. Посмотрим, изчего онсостоит:


  • all название партиции. Поскольку выражения партиционирования вCREATE TABLE мынезадали, вся таблица будет водной партиции.
  • 1_1 это срез блоков, который хранится впарте.
  • 0 это уровень вдереве слияний. Нулевой уровень упервых протопартов, если два парта слить, тоихуровень увеличится на1.

all_1_1_0/ banner_id.bin banner_id.mrk2 checksums.txt columns.txt count.txt date.bin date.mrk2 primary.idx user_id.bin user_id.mrk2

Появился первичный индекс primary.idx иподва файла накаждую колонку: mrk2 иbin.


  • columns.txt информация околонках.
  • count.txt число строк вкуске.

Первичный индекс primary.idx, онже разрежённый


Вprimary.idx лежат засечки: отсортированные значения выражения первичного ключа, заданного вCREATE TABLE, для каждой index_granularity строки: для строки0, index_granularity, index_granularity*2 ит.д. Размер гранулы index_granularity это степень разреженности индекса primary.idx. Для каждого запроса ClickHouse читает сдиска целое количество гранул. Если задать большой размер гранулы, будет прочитано много лишних строк, если маленький увеличится размер первичного индекса, который хранится воперативке для быстродействия.


Последняя засечка (здесь 1345) нужна, чтобы знать, начём заканчивается таблица.
od -i просто отображает байты как целые положительные четырёхбайтные числа.


od -i all_1_1_0/primary.idx0000000               157               0             421               00000020               685               0             949               00000040              1213               0            1345               00000060

Файлы данных .bin


Файлы bin содержат значения колонок, отсортированные повыражению первичного ключа, внашем случае user_id. Данные хранятся всжатом виде, единица сжатия блок. N-ое значение принадлежит кN-ой строке.


banner_id.bin:


cat  all_1_1_0/banner_id.bin | clickhouse-compressor -d | od -a0000000  enq   l   q   e   (   | enq   f   r   e   e   d enq   0   8   &0000020    N   0 enq   n   5   U   D   $ enq   1   ?   !   U   p enq   c0000040    a   H   y   6 enq   m   a   X   Z   D enq   F   x   :   B   O0000060  enq   \   v   8   j   + enq   s   z   E   G   )

user_id.bin:


cat  all_1_1_0/user_id.bin | clickhouse-compressor -d | od -i0000000               157               0             289               00000020               421               0             553               00000040               685               0             817               00000060               949               0            1081               00000100              1213               0            1345               0

Ну и date.bin, в виде epoch-time:


cat  all_1_1_0/date.bin | clickhouse-compressor -d | od -i0000000        1609515838      1609515938      1609516038      16095161380000020        1609516238      1609516338      1609516438      16095165380000040        1609516638      16095167380000050#от пятница,  1 января 2021 г. 15:43:58 (UTC) #до пятница,  1 января 2021 г. 15:58:58 (UTC)

Стоп, апочему данные отсортированы? Ведь мывставляли строки впроизвольном порядке? Дело втом, что ClickHouse сортирует вставляемые строки воперативке сиспользованием красно-чёрного дерева, ивремя отвремени сбрасывает его надиск ввиде immutable дата-парта.


Есть DELETE иUPDATE, ноэти команды недля рутинного использования, они работают вфоне, инеменяют старые парты, асоздают новые.


Благодаря тому, что столбцы хранятся всвоих файлах, можно читать только тестолбцы, которые указаны вSELECT, также эффективнее сжатие засчет однотипности данных. Поэтой причине ClickHouse иназывается колончатой СУБД.


Файлы засечек .mrk2


Для каждой засечки изprimary.idx mrk2знает, где именно вbin-файле начинаются значения соответствующей колонки.


Засечка вmrk2 состоит из3положительных 8-битных чисел, всего 24байта: смещение блока вbin-файле, смещение вразжатом блоке иколичество строк вгрануле. Третье число пока неважно.


Читаем третью засечку:


#читаем 24 байта как числа long начиная с 48 байт смещенияod -l -j 48 -N 24 all_1_1_0/user_id.mrk20000060                                 0                              320000100                                 20000110

Пройдём поэтому указателю, пропустив 32байта внулевом блоке:


cat  all_1_1_0/user_id.bin | clickhouse-compressor -d |od -j 32 -i -N 40000040               6850000044

Видим значение первичного ключа изчетвёртой строки. Это иесть начало третьей засечки вprimary.idx!


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


Поиск поMergeTree


Рассмотрим, как выполняется запрос сусловием напервичный ключ. ClickHouse спомощью бинарного поиска поprimary.idx вычисляет, скакой строки нужно читать данные. Тоесть попервичному индексу вычисляются области строк таблицы, которые могут удовлетворять запросу.


Видно, что 982 лежит между гранулами 949 и1213, поэтому можно прочесть только одну гранулу:


--включаем логи в clickhouse-clientset send_logs_level = 'trace';SELECT *FROM clicksWHERE user_id = 982Selected 1 parts by date, 1 parts by key, 1 marks by primary key, 1 marks to read from 1 rangesReading approx. 2 rows with 1 streams

Авот если сделать поиск поколонке вне первичного ключа, придётся делать фуллскан:


SELECT *FROM clicksWHERE banner_id = 'genbykj[';Selected 1 parts by date, 1 parts by key, 5 marks by primary key, 5 marks to read from 1 rangesReading approx. 10 rows with 1 streams

Это делает ClickHouse СУБД плохо справляющейся сточечными запросами, ведь приходится читать много лишних строк. Если размер гранулы поумолчанию 8192, аврезультате только одна строка, тоэффективность чтения 1/8192 = 0.0001.


Партиции


Теперь разберемся, что такое партиции. Добавим выражение партиционирования вCREATE TABLE, обрезая дату додня:


create table clicks(    date      DateTime,    user_id   Int64,    banner_id String) engine = MergeTree() order by user_id partition by toYYYYMMDD(date);+-------------------+-------+---------+|date               |user_id|banner_id|+-------------------+-------+---------+|2021-01-16 13:34:29|157    ||^/g~    ||2021-01-16 18:51:09|289    |/y;ny    ||2021-01-17 00:07:49|421    |@7bbc    ||2021-01-17 05:24:29|553    |.[e/{    ||2021-01-17 10:41:09|685    |0Wj)m    ||2021-01-17 15:57:49|817    |W6@Oo    ||2021-01-17 21:14:29|949    |tvQZ&    ||2021-01-18 02:31:09|1081   |ZPeCE    ||2021-01-18 07:47:49|1213   |H|$PI    ||2021-01-18 13:04:29|1345   |a'0^J    |+-------------------+-------+---------+

После вставки 10строк, надиске появились три парта вместо одного: на16, 17, 18января 2021года:


clicks 20210116_1_1_0 20210117_2_2_0 20210118_3_3_0

Внутри парты такиеже, как ибез партиционирования, нодобавился файл, хранящий партицию, ккоторой относится парт:


cat clicks/20210118_3_3_0/partition.dat | od -i0000000    202101180000004

Иminmax индекс подате, вкотором хранятся минимальное имаксимальное значение даты впарте:


od -i 20210116_1_1_0/minmax_date.idx0000000  1610804069  16108230690000010date --utc -d @1610804069Sat Jan 16 13:34:29 UTC 2021date --utc -d @1610823069Sat Jan 16 18:51:09 UTC 2021

Теперь посмотрим, как партиции помогают впоиске:


--запрос по выражению, участвующего в партиционированииSELECT *FROM clicksWHERE (date >= toUnixTimestamp('2021-01-17 00:00:00', 'UTC')) AND (date < toUnixTimestamp('2021-01-17 16:00:00', 'UTC'))--зная границы дат каждого парта, легко узнать, какие парты читать не нужноMinMax index condition: (column 0 in [1610841600, +inf)), (column 0 in (-inf, 1610899199]), andNot using primary index on part 20210117_2_2_0Selected 1 parts by date, 1 parts by key, 1 marks by primary key, 1 marks to read from 1 rangesReading approx. 8192 rows with 1 streamsdateuser_idbanner_id 2021-01-17 03:07:49      421  @7bbc      2021-01-17 08:24:29      553  .[e/{      2021-01-17 13:41:09      685  0Wj)m      2021-01-17 18:57:49      817  W6@Oo     Read 4 rows, 104.00 B in 0.002051518 sec., 1949 rows/sec., 49.51 KiB/sec.--запрос без участия партицийSELECT *FROM clicksWHERE banner_id = 'genbykj['--приходится читать все парты, в три параллельных потокаNot using primary index on part 20210117_2_2_0Not using primary index on part 20210116_1_1_0Not using primary index on part 20210118_3_3_0Selected 3 parts by date, 3 parts by key, 3 marks by primary key, 3 marks to read from 3 rangesReading approx. 24576 rows with 3 streamsRead 10 rows, 140.00 B in 0.001798808 sec., 5559 rows/sec., 76.01 KiB/sec.

Дата-парты также удобно смотреть через системную таблицу:


SELECT name, rows, min_time, max_timeFROM system.partsWHERE table = 'clicks'namerowsmin_timemax_time 20210116_1_1_0     2  2021-01-16 16:34:29  2021-01-16 21:51:09  20210117_2_2_0     4  2021-01-17 03:07:49  2021-01-17 18:57:49  20210118_3_3_0     4  2021-01-18 00:14:29  2021-01-18 16:04:29 

Итого мыувидели, что каждый дата-парт принадлежит кодной партиции, ипри поиске ClickHouse старается читать только нужные партиции. Ещё партиции можно отдельно удалять, отключать ипроизводить над ними другие операции.


Слияние дата-партов


Для того, чтобы количество партов неразрасталось, ClickHouse производит фоновое слияние кусков. При слиянии также срабатывает логика вReplacing, Summing, Collapsing идругих вариациях движка MergeTree. При слиянии двух отсортированных партов появляется один отсортированный.


--остановим процесс слияния и вставим строкsystem stop merges clicks;insert into clicks(date, user_id, banner_id) select now() , number * 132 + 157, randomPrintableASCII(5)from system.numbers limit 50;Ok.--появился 1 парт+--------------+------+|name          |active|+--------------+------+|20210116_1_1_0|1     |+--------------+------+--вставим ещё строкinsert into clicks(date, user_id, banner_id)  select now() , number * 132 + 157, randomPrintableASCII(5) from system.numbers limit 50;--уже 2 парта+--------------+------+|name          |active|+--------------+------+|20210116_1_1_0|1     ||20210116_2_2_0|1     |+--------------+------+--возобновим процесс слиянияsystem start merges clicks;-- и попросим ClickHouse запустить слияниеoptimize table clicks final;--через некоторое время видим, что два парта слились в один 20210116_1_2_1, у которого увеличился уровень. --Неактивные парты будут удалены со временем.+--------------+------+----+|name          |active|rows|+--------------+------+----+|20210116_1_1_0|0     |50  ||20210116_1_2_1|1     |100 ||20210116_2_2_0|0     |50  |+--------------+------+----+

Выводы


Яосветил базовые конструкции, как видим, никакой магии нет. Идея MergeTree стара ипроста как сам LSM. Всем рекомендую пользоваться!

Источник: habr.com
К списку статей
Опубликовано: 11.02.2021 16:05:53
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Администрирование баз данных

Clickhouse

Mergetree

Базы данных

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru