Pull to refresh
87.11
Nixys
DevOps, DevSecOps, MLOps — IT Solutions Integrator

Дом, милый дом: нюансы работы с ClickHouse. Часть 1

Level of difficultyMedium
Reading time12 min
Views9.1K

Всем привет, меня зовут Пётр, я инженер компании Nixys. На современных проектах используется огромное разнообразие баз данных: реляционные, ключ-значение, документоориентированные. Особое место среди них занимают колоночные БД, ярким представителем которых является ClickHouse. Это мощный инструмент, который способен обрабатывать миллиарды строк в секунду при минимальном времени ответа. Однако для максимальной эффективности ClickHouse необходимо понимать ряд фундаментальных моментов для того, чтобы использовать его по назначению. В этой серии статей мы разберём основы и особенности работы ClickHouse, которые помогут в выжимании максимума из этой СУБД. И сегодня начнём с фундаментальных теоретических моментов, чтобы составить максимально полное общее впечатление, которое поможет нам в дальнейшем.

Для начала определимся с тем, что такое ClickHouse. Самое лаконичное объяснение можно представить следующим образом:

ClickHouse — это open-source OLAP база данных, ориентированная на колонки.

Лаконично? Да. Понятно ли сходу невовлечённому человеку? Не очень. Давайте разобьём это определение на части и разберём каждую из них отдельно.

ClickHouse — это open-source OLAP база данных

Если с opensource базой данных все более-менее понятно, то что же такое OLAP? OLAP (online analytical processing) — это метод вычислений, который позволяет пользователям легко и выборочно извлекать и/или запрашивать данные для их анализа (иными словами, агрегировать данные) с разных точек зрения. По сути, мы хотим извлекать информацию из большого объёма разнообразных данных. Например, владелец магазина хранит данные о продаваемых товарах (цена, количество, их местоположение на полках) в одной таблице базы данных. В другой таблице хранятся данные о покупателях этих товаров, например пол, возраст и время покупки товаров. OLAP-технология позволяет объединить наборы этих данных, чтобы извлечь из них информацию и ответить на такие вопросы, как:

  • кто и когда покупает больше всего товаров;

  • что это за товары;

  • в какое время закупается та или иная группа покупателей.

На практике OLAP ориентирован на извлечение информации из большого объёма накопленных исторических данных для целей бизнес-аналитики, где данные редко изменяются после их вставки. Таким образом, подобные системы в первую очередь оптимизированы для обработки огромных объемов данных, необходимых для анализа, с нагрузкой в виде запросов, состоящих в основном из тяжелых операций чтения.

Так как нам необходимо агрегировать большой объем исторических данных, то для OLAP-системы выдвигается ряд важных условий:

  • система будет работать с большим объемом информации: наборы обрабатываемых данных могут исчисляться триллионами строк;

  • подавляющее большинство запросов — на чтение, так как главная задача - это анализ данных;

  • как правило, данные обновляются достаточно объемными пачками (> 10000 строк), а не по одной строке;

  • отсутствие транзакций и низкие требования к консистентности данных;

  • результат выполнения запроса существенно меньше исходных данных — то есть данные фильтруются и/или агрегируются.

Также в современных реалиях бизнеса выдвигаются требования к скорости работы OLAP-системы:

  • при выполнении запросов, результаты должны быть возвращены за секунды или менее;

  • высокая пропускная способность при обработке одного запроса;

  • сжатие хранимых данных внутри OLAP-системы;

  • возможность параллельной обработки запросов;

  • возможность распределенной обработки запросов на нескольких серверах;

  • хранение данных на диске, так как полностью хранить свойственный для OLAP системы объем данных в ОЗУ непозволительно дорого с точки зрения затрат на оборудование;

  • надежность, так как мы ни при каких обстоятельствах не хотим терять большой объем накопленной исторической информации;

Можно заметить, что под эти условия не подходят ACID базы данных (например, MySQL или PostgreSQL), так как транзакционная природа заметно влияет на скорости обработки запросов. Также здесь не подойдут in-memory решения (например, Redis) из-за невозможности параллельной и распределенной обработки запросов, а также из-за больших накладных расходах на ОЗУ, так как весь объем данных будет хранится в памяти.

Если задуматься концептуально, то классический строковый подход в целом неэффективен в OLAP-сценарии. Если мы возьмем классический MySQL и таблицу “Товары”:

Дата покупки

Наименование товара

Имя покупателя

10.12

Штаны

Биба

10.12

Футболка

Боба

10.12

Шорты

Абоба

То в памяти она будет храниться следующим образом:

10.12 - Штаны - Биба - 10.12 - Футболка - Боба - 10.12 - Шорты - Абоба

Подобный подход позволяет быстро записывать информацию, так как необходимо просто прикрепить новые данные к уже записанным. Однако, если мы захотим узнать, сколько было продано штанов за 10.12, то нам будет необходимо прочитать в оперативную память информацию из всей таблицы (то есть все 9 ячеек), хотя по сути для агрегации данных нам нужны только первые только 2 столбца (то есть, 6 ячеек), ведь имя покупателя в данном случае для нас не важно. То есть несмотря на быструю скорость записи (что особенно хорошо работает, если мы хотим одновременное выполнение нескольких транзакций) строковый подход к хранению данных крайне неэффективен при чтении большого объема информации. И, как следствие, не очень хорош для работы с ним, к примеру для построения типичного аналитического отчёта. Самую лучшую визуализацию разницы (и эффективности) в скорости получения одних и тех же объёмов данных из строковых и столбцовых баз предоставляет сама команда ClickHouse:

Колоночная база данных:

Гифка с официальной страницы ClickHouse
Гифка с официальной страницы ClickHouse

Строковая база данных:

Гифка с официальной страницы ClickHouse
Гифка с официальной страницы ClickHouse

На картинках наглядно видно, что пока строковая база построчно читает данные (большинство из которых избыточны) для построения одной агрегации, столбцовая база уже получила только необходимые для анализа данные и смогла за то же время получить гораздо больше информации. Для наглядности можно ознакомиться с принципом действия бенчмарка Percona. Несмотря на то, что основным противником ClickHouse в нём выступает clickhousedb_fdw, в нём также приведены результаты работы PostgreSQL, которые наглядно демонстрируют разницу в производительности баз при определенных сценариях. Тут мы подходим к последней части в нашем определении ClickHouse:

ориентированная на колонки.

Как нетрудно догадаться, колоночный подход заключается в том, чтобы хранить данные в столбцах, а не в строках. В таком случае, наша таблица “Товары” будет выглядеть следующим образом:

10.12 - 10.12 - 10.12 - Штаны - Футболка - Шорты - Биба - Боба - Абоба

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

CAP теорема — это теорема, согласно которой распределенная система может гарантировать не больше 2-х из 3-х свойств:

  • C (consistency) — согласованность. Каждое чтение из любого инстанса распределенной системы гарантирует вам самую последнюю актуальную запись. Соответственно, когда приложение записывает данные в систему, все копии данных в системе должны быть согласованы и не противоречить друг другу. 

  • A (availability) — доступность. Каждый живой инстанс распределенной системы всегда успешно выполняет запросы, однако не гарантируется, что ответы всех узлов системы совпадают.

  • P (partition tolerance) — устойчивость к распределению.  Разделение распределенной системы на несколько изолированных секций не приводит к некорректности отклика от каждой из секций. Также, если между узлами потеряно соединение, то они продолжают работать независимо друг от друга.

В текущих реалиях устойчивость к распределению является само собой разумеющимся условием для любой базы данных. А вот с согласованностью и доступностью все немного сложнее: мы можем гарантировать либо то, что в любой момент времени система ответит клиенту, либо то, что в любой момент времени данные во всей распределенной системе будут идентичны. Таким образом, нужно выбирать между указанными свойствами. Например, в рамках NoSQL большинство СУБД предпочитают жертвовать согласованностью данных, вместо этого выбирая конечную согласованность (eventual consistency). Это означает, что при обновлении данных в узлах системы, данные становятся несогласованными до того, как данные распространятся по всей системе и все узлы станут вновь согласованными. Во время этого процесса узлы могут представлять пользователям системы разные результаты, но в системных таблицах уже есть информация о том, что данные обновляются. Если в распределённой системе используется сильная согласованность (strong consistency), то все узлы всегда представляют одинаковые данные независимо от времени, но тогда уходит гарантия доступности, так системе необходимо блокировать доступ к узлам до завершения распространения информации.

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

Part и Partition

В ClickHouse есть два понятия, которые часто путают: Part и Partition. Несмотря на схожесть названий, они представляют из себя разные вещи и важно понимать различия. Part — это физический файл на диске, в котором хранится часть данных таблицы, при максимальном упрощении один Part — одна директория со столбцами. По умолчанию, физически они хранятся в директории «/var/lib/clickhouse», в самом ClickHouse —  в системной таблице «system.parts», в которой каждая строка описывает один кусок данных. Каждая операция вставки, приводит к тому, что ClickHouse немедленно создает в хранилище Part, содержащий данные из вставки вместе с другими метаданными, которые необходимо сохранить. Поэтому, сама команда ClickHouse рекомендует делать меньшее количество операций вставки, каждая из которых содержит больше данных вместо того, чтобы делать больше вставок, но каждая из которых содержит меньше данных, так как это уменьшит количество требуемых операций записи.

С другой стороны, Partition — это логическое разделение данных таблицы, созданное с использованием ключа партицирования. У него нет физического представления, но он позволяет объединять несколько Part по метаинформации. Проще говоря: партиция – это набор записей в таблице, объединенных по какому-либо критерию. Обычно партицирование используется для повышения производительности запросов, позволяя нам гибко объединять данные в подмножества и управлять ими. Например, можно напрямую запросить данные, которые относятся только к Partition, удалить их и т.д. Важный нюанс: при выполнении какой-либо операции на Partition, сами операции ничего не знают про ключи партицирования и Partition — вся необходимая информация хранится в специальном файле minmax_{PARTITIONING_KEY_COLUMN}.idx для каждого Part. Эти файлы содержат минимальные и максимальные значения столбцов в этом Part и позволяют логически собрать несколько Part в один Partition.

Табличные движки

ClickHouse предоставляет большое количество семейств табличных движков, однако самым популярным является MergeTree. Он является базовым и по совместительству одним из самым универсальных для высоконагруженных задач и заточен на быструю вставку данных с их последующей фоновой обработкой. Самые вкусные функции в MergeTree — это реплицирование и партицирование данных, которые обеспечивают нам отказоустойчивость. Она реализована за счёт мастер-мастер репликации и работает на уровне отдельной таблицы (но здесь стоит оговорится, что появился экспериментальный движок Replicated, который поддерживает репликацию метаданных через журнал DDL в ZooKeeper и выполняется на всех репликах для базы данных), а не всего сервера (то есть, в один момент времени на сервере одновременно могут быть как реплицируемые, так и не реплицируемые таблицы) и не зависит от шардинга: каждый шард имеет свою независимую репликацию, используемую для обеспечения отказоустойчивости. Все шарды используются для параллельного выполнения запроса. Сейчас мы не будем останавливаться на этом, а подробно рассмотрим этот аспект ClickHouse в следующей статье.

В самом семействе MergeTree самым популярным движком является ReplicatedMergeTree. Он лучше всего подходит для продакшена, так как помимо основных функций MergeTree он обладает функцией автоматической дедупликации данных при приёме данных, поэтому, например, можно безопасно повторить вставку, если во время первой попытки возникла какая-либо проблема. 

Все остальные движки как семейства MergeTree, так и других семейств, добавляют дополнительную функциональность и используются как правило для конкретных специфических случаев. Однако, это не значит, что всегда стоит бездумно использовать именно его: движки семейства MergeTree являются довольно тяжелыми, и в ряде случаев, например, если планируется несколько миллионов операций вставок в секунду, лучше использовать семейство Log engine, а в рамках MergeTree для частого обновления строк, рекомендуется использовать движок ReplacingMergeTree, так как обновление строк в нём вызывает мутацию, которая приводит к перезаписи и вы можете просто вставить обновленные строки, а старые будут заменены в соответствии с ключом сортировки таблицы.

Механика вставки данных

Механика вставки данных — ещё один важный аспект, который влияет на тюнинг ClickHouse для достижения устойчивости и скорости обработки больших объемов данных. Рассмотрим общий механизм вставки данных в таблицу ClickHouse семейства движков MergeTree.

На первом этапе сервер получает некоторый объём данных и формирует как минимум один блок вставки в память (на каждый ключ партицирования) из полученных данных. Блок — это контейнер, который представляет кусок таблицы в памяти, состоящий из:

  • IColumn — данные объекта;

  • IDataType — тип данных объекта, который сообщает, как необходимо обращаться с этим столбцом;

  • column name — имя столбца, которое может быть как исходным именем столбца из таблицы, так и искусственно сгенерированным именем, которое было присвоенное для получения временных результатов вычислений.

Блоки создаются для каждого обработанного Part. Когда мы вычисляем какую-то функцию по столбцам в блоке, то в блок добавляется ещё один столбец с её результатом, и столбцы для аргументов функции не затрагиваются, поскольку операции неизменяемы.

Вернёмся к вставке. Данные блока сортируются, применяются оптимизации, специфичные для табличного движка. Затем данные сжимаются и записываются в хранилище базы данных в виде новой части данных. Компрессия данных (data compression) — также очень важный аспект, который может показаться, на первый взгляд, очевидным, но данная операция не просто сокращает потребление дискового пространства: при использовании сжатия в базе данных увеличивается плотность информации в данных, а это значит, что нужно передавать меньше данных на диск и с диска, а также между клиентом и сервером, таким образом обменивая процессорное время на более высокую эффективную пропускную способность диска и сети. Более высокая пропускная способность, в свою очередь, повышает скорость обработки запросов. Важность компрессии в СУБД возрастает с ростом объёмов данных; даже небольшое сжатие может привести к разнице в сотни гигабайт, если входные данные достаточно велики, а, как упоминалось выше, ClickHouse заточен на использование с гигантскими объёмами данных.

В ClickHouse сжатие достигается за счёт кодеков сжатия столбцов. Важно понимать нюанс: кодирование и компрессия данных работают немного по-разному, но достигают одну и ту же цель — оптимизация, т. е. уменьшение размера рабочих данных. Кодировки применяют сопоставление к нашим данным и преобразуют значения на основе функции, используя свойства типа данных. И наоборот, сжатие использует общий алгоритм для компрессии данных на уровне байтов, и обычно перед использованием сжатия применяются кодировки. По умолчанию применяется алгоритм сжатия данных без потерь LZ4, который обеспечивает лучшую скорость сжатия и распаковки данных по сравнению с алгоритмами DEFLATE и LZO, однако проигрывает конкурентам в степени сжатия. Эти алгоритмы представляют собой кодеки общего назначения, которые являются достаточно универсальными. Однако, мы можем задать специализированные кодеки. Они предназначены для повышения эффективности сжатия за счёт использования определённых особенностей данных. Важно помнить, что некоторые из этих кодеков не сжимают данные самостоятельно, а лишь предварительно обрабатывают данных для последующего более эффективного сжатия данных. Самый используемым является DoubleDelta — кодек, который является производным от другого кодека Delta. В нем исходные значения данных заменяются разностью двух соседних значений, за исключением первого значения, которое остается неизменным и является отправной точкой. По сути, DoubleDelta вычисляется разницу от разниц Delta и сохраняет её в бинарном виде.

Настройка дефолтного сжатия для MergeTree происходит в конфигурационном файле ClickHouse:

<compression>

   <case>

     <min_part_size>...</min_part_size>

     <min_part_size_ratio>...</min_part_size_ratio>

     <method>...</method>

     <level>...</level>

   </case>

   ...

</compression>

  • min_part_size – Минимальный размер части данных.

  • min_part_size_ratio – Отношение размера части данных к размеру таблицы.

  • method – Алгоритм для сжатия, доступны lz4, lz4hc, zstd, deflate_qpl.

  • level – Уровень сжатия.

Также можно определить метод сжатия для каждого отдельного столбца и таблицы  через запрос CREATE TABLE:

CREATE TABLE codec_example

(

   dt Date CODEC(ZSTD),

   ts DateTime CODEC(LZ4HC),

   value Float32 CODEC(Delta, ZSTD)

)

ENGINE = <Engine>

В данном случае мы применили к данным сначала кодек Delta, а затем LZ4. Возможность определять метод сжатия для каждого отдельного столбца даёт очень мощную возможность: если вы хорошо понимаете структуру и содержимое вашей базы, то вы можете применить к каждой таблице и/или столбцу свой кодек сжатия, который подходит лучше для каждого конкретного случая. Однако, даже в самом базовом варианте (LZ4) при тесте базы с около 1 млрд строк, мы получим следующие значения в сжатом виде:

Name

Compressed Size

Uncompressed Size

Ratio

tempMax         

513.24 MiB

7.7 GiB 

15.53

tempMin         

507.67 MiB

7.7 GiB

15.53

На этом всё. В данной статье мы обзорно рассмотрели базовые аспекты колоночных баз данных, а также ключевые особенности ClickHouse. В следующих частях мы подробно рассмотрим процесс репликации данных в ClickHouse, разберём несколько боевых инцидентов с ее падением на продакшене, а также расскажем, как этого можно было избежать. Увидимся!

Подписывайтесь на наши соцсети: Telegram, vc.ru и YouTube. Мы всегда рады новым друзьям. 

Tags:
Hubs:
Total votes 11: ↑11 and ↓0+11
Comments2

Articles

Information

Website
nixys.io
Registered
Founded
Employees
51–100 employees
Location
Россия
Representative
Vlada Grishkina-Makareva