ClickHouse: управление миграциями и отправка запросов из PHP в кластер

Материал из Национальной библиотеки им. Н. Э. Баумана
Последнее изменение этой страницы: 22:24, 23 сентября 2018.
ClickHouse
Clickhouse logo.png
Разработчики: Яндекс
Выпущена: 15 June 2016 года; 3 years ago (2016-06-15)
Постоянный выпуск: v1.1.54383 / 21 May 2018 года; 18 months ago (2018-05-21)
Состояние разработки: Активное
Написана на: C++
Операционная система: Linux, macOS, FreeBSD
Тип ПО: Столбцовая СУБД
Лицензия: Apache License 2.0
Веб-сайт clickhouse.yandex

ClickHouse - столбцовая система управления базами данных (СУБД) с открытым кодом, предназначенная для онлайн обработки аналитических запросов (OLAP). Разрабатывается компанией Яндекс. ClickHouse использует собственный диалект SQL близкий к стандартному, но содержащий различные расширения: массивы и вложенные структуры данных, функции высшего порядка, вероятностные структуры, функции для работы с URI, возможность для работы с внешними key-value хранилищами («словарями»), специализированные агрегатные функции, функциональности для семплирования, приблизительных вычислений, возможность создания хранимых представлений с агрегацией, наполнения таблицы из потока сообщений Apache Kafka и т. д.

Обзор

ClickHouse изначально разрабатывался для обеспечения работы Яндекс.Метрики, второй крупнейшей в мире платформы для веб аналитики, и продолжает быть её ключевым компонентом. При более 13 триллионах записей в базе данных и более 20 миллиардах событий в сутки, ClickHouse позволяет генерировать индивидуально настроенные отчёты на лету напрямую из неагрегированных данных. В Яндекс.Метрике ClickHouse используется для нескольких задач. Основная задача - построение отчётов в режиме онлайн по неагрегированным данным. Для решения этой задачи используется кластер из 374 серверов, хранящий более 20,3 триллионов строк в базе данных. Объём сжатых данных, без учёта дублирования и репликации, составляет около 2 ПБ. Объём несжатых данных (в формате tsv) составил бы, приблизительно, 17 ПБ. [Источник 1]

Также ClickHouse используется:

  • для хранения данных Вебвизора;
  • для обработки промежуточных данных;
  • для построения глобальных отчётов Аналитиками;
  • для выполнения запросов в целях отладки движка Метрики;
  • для анализа логов работы API и пользовательского интерфейса.

Отличительные особенности

По-настоящему столбцовая СУБД

Cуществуют системы, которые могут хранить значения отдельных столбцов по отдельности, но не могут эффективно выполнять аналитические запросы в силу оптимизации под другой сценарий работы. Примеры: HBase, BigTable, Cassandra, HyperTable. В ClickHouse, по-настоящему столбцовой СУБД, рядом со значениями не хранится никаких лишних данных. Например, должны поддерживаться значения постоянной длины, чтобы не хранить рядом со значениями типа "число" их длины. Очень важно хранить данные компактно (без "мусора") в том числе в несжатом виде, так как скорость разжатия (использование CPU) зависит, в основном, от объёма несжатых данных.

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

Сжатие данных

Некоторые столбцовые СУБД (InfiniDB CE, MonetDB) не используют сжатие данных. Однако сжатие данных действительно играет одну из ключевых ролей в демонстрации отличной производительности.

Хранение данных на диске

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

Параллельная обработка запроса на многих процессорных ядрах

Большие запросы естественным образом распараллеливаются, используя все необходимые ресурсы из доступных на сервере.

Распределённая обработка запроса на многих серверах

В ClickHouse данные могут быть расположены на разных шардах. Каждый шард может представлять собой группу реплик, которые используются для отказоустойчивости. Запрос будет выполнен на всех шардах параллельно. Это делается прозрачно для пользователя.

Поддержка SQL

ClickHouse поддерживает декларативный язык запросов на основе SQL и во многих случаях совпадающий с SQL стандартом. Поддерживаются GROUP BY, ORDER BY, подзапросы в секциях FROM, IN, JOIN, а также скалярные подзапросы. Зависимые подзапросы и оконные функции не поддерживаются.

Векторный движок

Данные не только хранятся по столбцам, но и обрабатываются по векторам - частям столбцов. За счёт этого достигается высокая эффективность по CPU.

Обновление данных в реальном времени

ClickHouse поддерживает таблицы с первичным ключом. Для того, чтобы можно было быстро выполнять запросы по диапазону первичного ключа, данные инкрементально сортируются с помощью merge дерева. За счёт этого, поддерживается постоянное добавление данных в таблицу. Блокировки при добавлении данных отсутствуют.

Наличие индекса

Физическая сортировка данных по первичному ключу позволяет получать данные для конкретных его значений или их диапазонов с низкими задержками - менее десятков миллисекунд.

Подходит для онлайн запросов

Низкие задержки позволяют не откладывать выполнение запроса и не подготавливать ответ заранее, а выполнять его именно в момент загрузки страницы пользовательского интерфейса.

Поддержка приближённых вычислений

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

Репликация данных и поддержка целостности

Используется асинхронная multimaster репликация. После записи на любую доступную реплику, данные распространяются на все остальные реплики в фоне. Система поддерживает полную идентичность данных на разных репликах. Восстановление после большинства сбоев осуществляется автоматически, а в сложных случаях — полуавтоматически. При необходимости, можно включить кворумную запись данных.

Недостатки

  • Отсутствие полноценных транзакций.
  • Возможность изменять или удалять ранее записанные данные с низкими задержками и высокой частотой запросов не предоставляется. Есть массовое удаление данных для очистки более не нужного или соответствия GDPR. Массовое изменение данных находится в разработке (на момент июля 2018).[Источник 2]
  • Разреженный индекс делает ClickHouse плохо пригодным для точечных чтений одиночных строк по своим ключам.

Интерфейсы

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

Для работы из командной строки вы можете использовать clickhouse-client

$ clickhouse-client
ClickHouse client version 0.0.26176.
Connecting to localhost:9000.
Connected to ClickHouse server version 0.0.26176.

:)

Клиент может быть использован в интерактивном и неинтерактивном (batch) режиме. В batch режиме в качестве формата данных по умолчанию используется формат TabSeparated. Формат может быть указан в секции FORMAT запроса. По умолчанию, в batch режиме вы можете выполнить только один запрос. Чтобы выполнить несколько запросов из "скрипта", используйте параметр --multiquery. Это работает для всех запросов кроме INSERT. Результаты запросов выводятся подряд без дополнительных разделителей. Также, при необходимости выполнить много запросов, вы можете запускать clickhouse-client на каждый запрос. Заметим, что запуск программы clickhouse-client может занимать десятки миллисекунд. В интерактивном режиме, вы получите командную строку, в которую можно вводить запросы. При выполнении запроса, клиент показывает:

  • Прогресс выполнение запроса, который обновляется не чаще, чем 10 раз в секунду (по умолчанию). При быстрых запросах, прогресс может не успеть отобразиться.
  • Отформатированный запрос после его парсинга - для отладки.
  • Результат в заданном формате.
  • Количество строк результата, прошедшее время, а также среднюю скорость выполнения запроса.

HTTP-интерфейс

HTTP-интерфейс позволяет использовать ClickHouse на любой платформе, из любого языка программирования. По умолчанию, clickhouse-server слушает HTTP на порту 8123 (это можно изменить в конфиге). Если запросить GET / без параметров, то вернётся строка "Ok." (с переводом строки на конце). Это может быть использовано в скриптах проверки живости.

$ curl 'http://localhost:8123/'
Ok.

Запрос отправляется в виде параметра URL query или POST-запросом. Или начало запроса в параметре query, а продолжение POST-ом. Размер URL ограничен 16KB, это следует учитывать при отправке больших запросов. В случае успеха, вернётся код ответа 200 и результат обработки запроса в теле ответа. В случае ошибки, вернётся код ответа 500 и текст с описанием ошибки в теле ответа.

При использовании метода GET, выставляется настройка readonly. То есть, для запросов, модифицирующие данные, можно использовать только метод POST. Сам запрос при этом можно отправлять как в теле POST-а, так и в параметре URL.

Сторонние

Доступен ряд библиотек и визуальных интерфейсов от сторонних разработчиков.

Tabix - веб-интерфейс для ClickHouse в проекте Tabix. Основные возможности:

  • Работает с ClickHouse напрямую из браузера, без необходимости установки дополнительного ПО.
  • Редактор запросов с подсветкой синтаксиса.
  • Автодополнение команд.
  • Инструменты графического анализа выполнения запросов.
  • Цветовые схемы на выбор.

HouseOps - UI/IDE для OSX, Linux и Windows. Основные возможности:

  • Построение запросов с подсветкой синтаксиса. Просмотр ответа в табличном или JSON представлении.
  • Экспортирование результатов запроса в формате CSV или JSON.
  • Список процессов с описанием. Режим записи. Возможность остановки (KILL) процесса.
  • Граф базы данных. Показывает все таблицы и их столбцы с дополнительной информацией.
  • Быстрый просмотр размера столбца.
  • Конфигурирование сервера.

Форматы входных и выходных данных

ClickHouse может принимать INSERT и отдавать SELECT данные в различных форматах.

Поддерживаемые форматы и возможность использовать их в запросах INSERT и SELECT перечислены в таблице ниже.

Поддерживаемые форматы данных
Формат INSERT SELECT
TabSeparated
TabSeparatedRaw
TabSeparatedWithNames
TabSeparatedWithNamesAndTypes
CSV
CSVWithNames
Values
Vertical
VerticalRaw
JSON
JSONCompact
JSONEachRow
TSKV
Pretty
PrettyCompact
PrettyCompactMonoBlock
PrettyNoEscapes
PrettySpace
RowBinary
Native
Null
XML
CapnProto

Типы данных

UInt8, UInt16, UInt32, UInt64, Int8, Int16, Int32, Int64

Целые числа фиксированной длины, без знака или со знаком.

Диапазоны Int

  • Int8 - [ -128 : 127 ]
  • Int16 - [ -32768 : 32767 ]
  • Int32 - [ -2147483648 : 2147483647 ]
  • Int64 - [ -9223372036854775808 : 9223372036854775807 ]

Диапазоны Uint

  • UInt8 - [ 0 : 255 ]
  • UInt16 - [ 0 : 65535 ]
  • UInt32 - [ 0 : 4294967295 ]
  • UInt64 - [ 0 : 18446744073709551615 ]

Float32, Float64

Числа с плавающей запятой. Типы эквивалентны типам языка С:

  • Float32 - float;
  • Float64 - double.

Рекомендуется хранить данные в целочисленноми виде всегда, когда это возможно.

Булевы значения

Отдельного типа для булевых значений нет. Для них используется тип UInt8, в котором используются только значения 0 и 1.

String

Строки произвольной длины. Длина не ограничена. Значение может содержать произвольный набор байт, включая нулевые байты. Таким образом, тип String заменяет типы VARCHAR, BLOB, CLOB и т. п. В ClickHouse нет понятия кодировок. Строки могут содержать произвольный набор байт, который хранится и выводится, как есть. Если нужно хранить тексты, рекомендуется использовать кодировку UTF-8.

FixedString(N)

Строка фиксированной длины N байт (не символов, не кодовых точек). N должно быть строго положительным натуральным числом. При чтении сервером строки (например, при парсинге данных для INSERT), содержащей меньшее число байт, строка дополняется до N байт дописыванием нулевых байт справа. При чтении сервером строки, содержащей большее число байт, выдаётся сообщение об ошибке. При записи сервером строки (например, при выводе результата запроса SELECT), нулевые байты с конца строки не вырезаются, а выводятся. Обратите внимание, как это поведение отличается от поведения MySQL для типа CHAR (строки дополняются пробелами, пробелы перед выводом вырезаются).

С типом FixedString(N) умеет работать меньше функций, чем с типом String - то есть, он менее удобен в использовании.

Date

Дата. Хранится в двух байтах в виде (беззнакового) числа дней, прошедших от 1970-01-01. Позволяет хранить значения от чуть больше, чем начала unix-эпохи до верхнего порога, определяющегося константой на этапе компиляции (сейчас - до 2106 года, последний полностью поддерживаемый год - 2105). Минимальное значение выводится как 0000-00-00. Дата хранится без учёта часового пояса.

DateTime

Дата-с-временем. Хранится в 4 байтах, в виде (беззнакового) unix timestamp. Позволяет хранить значения в том же интервале, что и для типа Date. Минимальное значение выводится как 0000-00-00 00:00:00. Время хранится с точностью до одной секунды (без учёта секунд координации).

Часовые пояса

Дата-с-временем преобразуется из текстового (разбитого на составляющие) в бинарный вид и обратно, с использованием системного часового пояса на момент старта клиента или сервера. В текстовом виде, теряется информация о том, был ли произведён перевод стрелок. По умолчанию клиент переключается на часовой пояс сервера при подключении. Это поведение можно изменить, включив у клиента параметр командной строки --use_client_time_zone. Поддерживаются только часовые пояса, для которых для всего диапазона времён, с которым вы будете работать, не существовало моментов времени, в которые время отличалось от UTC на нецелое число часов (без учёта секунд координации).

Enum

Включает в себя типы Enum8 и Enum16. Enum сохраняет конечный набор пар 'строка' = целое число. Все операции с данными типа Enum ClickHouse выполняет как с числами, однако пользователь при этом работает со строковыми константами. Это более эффективно с точки зрения производительности, чем работа с типом данных String.

  • Enum8 описывается парами 'String' = Int8.
  • Enum16 описывается парами 'String' = Int16.

Array(T)

Массив из элементов типа T.

T может любым, в том числе, массивом. Таким образом поддержаны многомерные массивы.

AggregateFunction(name, types_of_arguments...)

Промежуточное состояние агрегатной функции. Чтобы его получить, используются агрегатные функции с суффиксом -State.

Tuple(T1, T2, ...)

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

Nullable(TypeName)

Позволяет работать как со значением типа TypeName так и с отсутствием этого значения (NULL) в одной и той же переменной, в том числе хранить NULL в таблицах вместе со значения типа TypeName. В качестве TypeName нельзя использовать составные типы данных Array и Tuple. Составные типы данных могут содержать значения типа Nullable. Поле типа Nullable нельзя включать в индексы. NULL — значение по умолчанию для типа Nullable, если в конфигурации сервера ClickHouse не указано иное.

Для хранения значения типа Nullable ClickHouse использует:

  • Отдельный файл с масками NULL (далее маска).
  • Непосредственно файл со значениями.

Маска определяет, что лежит в ячейке данных: NULL или значение. В случае, когда маска указывает, что в ячейке хранится NULL, в файле значений хранится значение по умолчанию для типа данных.

Почти всегда использование Nullable снижает производительность, это стоит учитывать при проектировании БД.

Nested(Name1 Type1, Name2 Type2, ...)

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

Служебные типы данных

Expression

Используется для представления лямбда-выражений в функциях высшего порядка.

Set

Используется для представления правой части выражения IN.

Nothing

Этот тип данных предназначен только для того, чтобы представлять NULL, т.е. отсутствие значения. Невозможно создать значение типа Nothing, поэтому он используется там, где значение не подразумевается. Например, NULL записывается как Nullable(Nothing). Также тип Nothing используется для обозначения пустых массивов.

Типы таблиц

Тип таблицы (движок) определяет:

  • Как и где хранятся данные, куда их писать и откуда читать.
  • Какие запросы поддерживаются и каким образом.
  • Конкурентный доступ к данным.
  • Использование индексов, если есть.
  • Возможно ли многопоточное выполнение запроса.
  • Параметры репликации данных.

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

MergeTree

Движок MergeTree, а также другие движки этого семейства (*MergeTree) — это наиболее функциональные движки таблиц ClickHousе.

Основные возможности:

  • Хранит данные, отсортированные по первичному ключу. Это позволяет создавать разреженный индекс небольшого объёма, который позволяет быстрее находить данные.
  • Позволяет оперировать партициями, если задан ключ партиционирования. ClickHouse поддерживает отдельные операции с партициями, которые работают эффективнее, чем общие операции с этим же результатом над этими же данными. Также, ClickHouse автоматически отсекает данные по партициям там, где ключ партиционирования указан в запросе. Это также увеличивает эффективность выполнения запросов.
  • Поддерживает репликацию данных. Для этого используется семейство таблиц ReplicatedMergeTree. Подробнее читайте в разделе Репликация данных.
  • Поддерживает сэмплирование данных. При необходимости можно задать способ сэмплирования данных в таблице.

Конфигурирование движка при создании таблицы ENGINE [=] MergeTree() [PARTITION BY expr] [ORDER BY expr] [SAMPLE BY expr] [SETTINGS name=value, ...]

  • ORDER BY — первичный ключ. Кортеж столбцов или произвольных выражений. Если используется ключ сэмплирования, то первичный ключ должен содержать его.
  • PARTITION BY — ключ партиционирования. Для партиционирования по месяцам используйте выражение toYYYYMM(date_column), где date_column — столбец с датой типа Date. В этом случае имена партиций имеют формат "YYYYMM".
  • SAMPLE BY — выражение для сэмплирования (не обязательно).
  • SETTINGS — дополнительные параметры, регулирующие поведение MergeTree (не обязательно).

Для конкурентного доступа к таблице используется мультиверсионность. То есть, при одновременном чтении и обновлении таблицы, данные будут читаться из набора кусочков, актуального на момент запроса. Длинных блокировок нет. Вставки никак не мешают чтениям. Чтения из таблицы автоматически распараллеливаются.

ReplacingMergeTree

Отличается от MergeTree тем, что выполняет удаление дублирующихся записей с одинаковым значением первичного ключа. Добавлен необязательный параметр движка таблицы — столбец с версией ver. При слиянии для всех строк с одинаковым значением первичного ключа оставляет только одну строку: если задан столбец версии — строку с максимальной версией, иначе — последнюю строку. Столбец с версией должен иметь тип из семейства UInt, или Date, или DateTime. Дедупликация данных производится лишь во время слияний. Слияние происходят в фоне в неизвестный момент времени, на который невозможно ориентироваться. Некоторая часть данных может так и остаться необработанной. Можно вызвать внеочередное слияние с помощью запроса OPTIMIZE, но на это не стоит рассчитывать, так как запрос OPTIMIZE приводит к чтению и записи большого объёма данных.

SummingMergeTree

Отличается от MergeTree тем, что суммирует данные при слиянии. Столбцы для суммирования могут быть заданы явно и неявно. Во втором случае, при слиянии, для всех строчек с одинаковым значением первичного ключа, производится суммирование значений в числовых столбцах, не входящих в первичный ключ. Чтобы задать столбцы для суммирования явно, нужно перечислить их через запятую в качестве дополнительного последнего параметра. В этом случае, при слиянии, для всех строчек с одинаковым значением первичного ключа, производится суммирование значений в указанных столбцах. Указанные столбцы также должны быть числовыми и не входить в первичный ключ. Если значения во всех таких столбцах оказались нулевыми, то строчка удаляется. Для остальных столбцов, не входящих в первичный ключ, при слиянии выбирается первое попавшееся значение. Но для столбцов типа AggregateFunction выполняется агрегация согласно заданной функции, так что этот движок фактически ведёт себя как AggregatingMergeTree. При чтении, суммирование не делается само по себе. Если оно необходимо - напишите соответствующий GROUP BY.

Дополнительно, таблица может иметь вложенные структуры данных, которые обрабатываются особым образом. Если название вложенной таблицы заканчивается на Map и она содержит не менее двух столбцов, удовлетворяющих следующим критериям:

  • первый столбец - числовой ((U)IntN, Date, DateTime), назовем его условно key
  • остальные столбцы - арифметические ((U)IntN, Float32/64), условно (values...)
  • то такая вложенная таблица воспринимается как отображение key => (values...) и при слиянии ее строк выполняется слияние элементов двух множеств по key со сложением соответствующих (values...)

AggregatingMergeTree

Отличается от MergeTree тем, что при слиянии, выполняет объединение состояний агрегатных функций, хранимых в таблице, для строчек с одинаковым значением первичного ключа. Чтобы это работало, используются: тип данных AggregateFunction, а также модификаторы -State и -Merge для агрегатных функций. В большинстве случаев, использование AggregatingMergeTree является неоправданным, так как можно достаточно эффективно выполнять запросы по неагрегированным данным.

CollapsingMergeTree

Позволяет автоматически удалять - "схлопывать" некоторые пары строк при слиянии. CollapsingMergeTree принимает дополнительный параметр - имя столбца типа Int8, содержащего "знак" строки. При слиянии, для каждой группы идущих подряд одинаковых значений первичного ключа (столбцов, по которым сортируются данные), остаётся не более одной строки со значением столбца sign_column = -1 ("отрицательной строки") и не более одной строки со значением столбца sign_column = 1 ("положительной строки"). То есть - производится схлопывание записей из лога изменений. Если количество положительных и отрицательных строк совпадает - то пишет первую отрицательную и последнюю положительную строку. Если положительных на 1 больше, чем отрицательных - то пишет только последнюю положительную строку. Если отрицательных на 1 больше, чем положительных - то пишет только первую отрицательную строку. Иначе - логическая ошибка, и ни одна из таких строк не пишется.

Сценарии применения данного движка довольно специфичны для Яндекс.Метрики.

GraphiteMergeTree

Движок предназначен для rollup (прореживания и агрегирования/усреднения) данных Graphite. Он может быть интересен разработчикам, которые хотят использовать ClickHouse как хранилище данных для Graphite. Graphite хранит в ClickHouse полные данные, а получать их может двумя способами - без прореживания, используя MergeTree, и с прореживанием, при помощи GraphiteMergeTree. Движок наследует свойства MergeTree. Настройки прореживания данных задаются параметром graphite_rollup в конфигурации сервера.

Репликация данных

Репликация поддерживается только для таблиц семейства MergeTree:

  • ReplicatedMergeTree
  • ReplicatedSummingMergeTree
  • ReplicatedReplacingMergeTree
  • ReplicatedAggregatingMergeTree
  • ReplicatedCollapsingMergeTree
  • ReplicatedGraphiteMergeTree

Репликация работает на уровне отдельных таблиц, а не всего сервера. То есть, на сервере могут быть расположены одновременно реплицируемые и не реплицируемые таблицы. Репликация не зависит от шардирования. На каждом шарде репликация работает независимо. Реплицируются сжатые данные запросов INSERT, ALTER (см. подробности в описании запроса ALTER). Запросы CREATE, DROP, ATTACH, DETACH, RENAME выполняются на одном сервере и не реплицируются:

  • CREATE TABLE создаёт новую реплицируемую таблицу на том сервере, где выполняется запрос, а если на других серверах такая таблица уже есть - добавляет новую реплику.
  • DROP TABLE удаляет реплику, расположенную на том сервере, где выполняется запрос.
  • RENAME переименовывает таблицу на одной из реплик - то есть, реплицируемые таблицы на разных репликах могут называться по разному.

Для обеспечения реплицирования требуется Apache ZooKeeper (рекомендуется использовать версию 3.4.5+). Чтобы использовать репликацию, укажите в конфигурационном файле адреса ZooKeeper кластера. Можно указать любой имеющийся ZooKeeper-кластер - система будет использовать в нём одну директорию для своих данных (директория указывается при создании реплицируемой таблицы). Репликация асинхронная, мульти-мастер. Запросы INSERT (а также ALTER) можно отправлять на любой доступный сервер. Данные вставятся на сервер, где выполнен запрос, а затем скопируются на остальные серверы. В связи с асинхронностью, только что вставленные данные появляются на остальных репликах с небольшой задержкой. Если часть реплик недоступна, данные на них запишутся тогда, когда они станут доступны. Если реплика доступна, то задержка составляет столько времени, сколько требуется для передачи блока сжатых данных по сети.

Блоки данных дедуплицируются. При многократной записи одного и того же блока данных (блоков данных одинакового размера, содержащих одни и те же строчки в одном и том же порядке), блок будет записан только один раз. Это сделано для того, чтобы в случае сбоя в сети, когда клиентское приложение не может понять, были ли данные записаны в БД, можно было просто повторить запрос INSERT. При этом не имеет значения, на какую реплику будут отправлены INSERT-ы с одинаковыми данными. То есть, обеспечивается идемпотентность INSERT.

Создание реплицируемых таблиц

В начало имени движка таблицы добавляется Replicated. Например, ReplicatedMergeTree. Также добавляются два параметра в начало списка параметров - путь к таблице в ZooKeeper, имя реплики в ZooKeeper. Пример: ReplicatedMergeTree('/clickhouse/tables/{layer}-{shard}/hits', '{replica}', EventDate, intHash32(UserID), (CounterID, EventDate, intHash32(UserID), EventTime), 8192)

Как видно в примере, эти параметры могут содержать подстановки в фигурных скобках. Подставляемые значения достаются из конфигурационного файла, из секции macros. Пример:
<macros>
<layer>05</layer>
<shard>02</shard>
<replica>example05-02-1.yandex.ru</replica>
</macros>

Путь к таблице в ZooKeeper должен быть разным для каждой реплицируемой таблицы. В том числе, для таблиц на разных шардах, должны быть разные пути. В данном случае, путь состоит из следующих частей:

  • /clickhouse/tables/ - общий префикс. Рекомендуется использовать именно его.
  • {layer}-{shard} - идентификатор шарда. В данном примере он состоит из двух частей, так как на кластере Яндекс.Метрики используется двухуровневое шардирование. Для большинства задач, оставьте только подстановку {shard}, которая будет раскрываться в идентификатор шарда.
  • hits - имя узла для таблицы в ZooKeeper. Разумно делать его таким же, как имя таблицы. Оно указывается явно, так как, в отличие от имени таблицы, оно не меняется после запроса RENAME.

Имя реплики - то, что идентифицирует разные реплики одной и той же таблицы. Можно использовать для него имя сервера, как показано в примере. Впрочем, достаточно, чтобы имя было уникально лишь в пределах каждого шарда. Можно не использовать подстановки, а указать соответствующие параметры явно. Это может быть удобным для тестирования и при настройке маленьких кластеров. Однако в этом случае нельзя пользоваться распределенными DDL-запросами (ON CLUSTER). При работе с большими кластерами мы рекомендуем использовать подстановки, они уменьшают вероятность ошибки. Выполните запрос CREATE TABLE на каждой реплике. Запрос создаёт новую реплицируемую таблицу, или добавляет новую реплику к имеющимся. Если вы добавляете новую реплику после того, как таблица на других репликах уже содержит некоторые данные, то после выполнения запроса, данные на новую реплику будут скачаны с других реплик. То есть, новая реплика синхронизирует себя с остальными. Для удаления реплики, выполните запрос DROP TABLE. При этом, удаляется только одна реплика - расположенная на том сервере, где вы выполняете запрос.

Преобразование из MergeTree в ReplicatedMergeTree

Здесь и далее, под MergeTree подразумеваются все движки таблиц семейства MergeTree, так же для ReplicatedMergeTree.

Если у вас была таблица типа MergeTree, репликация которой делалась вручную, вы можете преобразовать её в реплицируемую таблицу. Это может понадобиться лишь в случаях, когда вы уже успели накопить большое количество данных в таблице типа MergeTree, а сейчас хотите включить репликацию. Если на разных репликах данные отличаются, то сначала синхронизируйте их, либо удалите эти данные на всех репликах кроме одной. Переименуйте имеющуюся MergeTree таблицу, затем создайте со старым именем таблицу типа ReplicatedMergeTree. Перенесите данные из старой таблицы в поддиректорию detached в директории с данными новой таблицы (/var/lib/clickhouse/data/db_name/table_name/). Затем добавьте эти куски данных в рабочий набор с помощью выполнения запросов ALTER TABLE ATTACH PARTITION на одной из реплик.

Преобразование из ReplicatedMergeTree в MergeTree

Создайте таблицу типа MergeTree с другим именем. Перенесите в её директорию с данными все данные из директории с данными таблицы типа ReplicatedMergeTree. Затем удалите таблицу типа ReplicatedMergeTree и перезапустите сервер. Если вы хотите избавиться от таблицы ReplicatedMergeTree, не запуская сервер, то

  1. удалите соответствующий файл .sql в директории с метаданными (/var/lib/clickhouse/metadata/);
  2. удалите соответствующий путь в ZooKeeper (/path_to_table/replica_name);

После этого, вы можете запустить сервер, создать таблицу типа MergeTree, перенести данные в её директорию, и перезапустить сервер.

Для небольших объёмов данных

TinyLog

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

  • если вы одновременно читаете из таблицы и в другом запросе пишете в неё, то чтение будет завершено с ошибкой
  • если вы одновременно пишете в таблицу в нескольких запросах, то данные будут битыми

Log

Отличается от TinyLog тем, что вместе с файлами столбцов лежит небольшой файл "засечек". Засечки пишутся на каждый блок данных и содержат смещение - с какого места нужно читать файл, чтобы пропустить заданное количество строк. Это позволяет читать данные из таблицы в несколько потоков. При конкуррентном доступе к данным, чтения могут выполняться одновременно, а записи блокируют чтения и друг друга. Движок Log не поддерживает индексы. Также, если при записи в таблицу произошёл сбой, то таблица станет битой, и чтения из неё будут возвращать ошибку. Движок Log подходит для временных данных, write-once таблиц, а также для тестовых и демонстрационных целей.

Memory

Хранит данные в оперативной памяти, в несжатом виде. Данные хранятся именно в таком виде, в каком они были получены при чтении. Таким образом, само чтение из этой таблицы полностью бесплатно. Конкурентный доступ к данным синхронизируется. Блокировки короткие: чтения и записи не блокируют друг друга. Индексы не поддерживаются. Чтение распараллеливается. За счёт отсутствия чтения с диска, разжатия и десериализации данных удаётся достичь максимальной производительности (выше 10 ГБ/сек.) на простых запросах. При перезапуске сервера данные из таблицы исчезают и таблица становится пустой. Обычно, использование этого движка таблиц является неоправданным. Тем не менее, он может использоваться для тестов, а также в задачах, где важно достичь максимальной скорости на не очень большом количестве строк (примерно до 100 000 000).

Buffer

Буферизует записываемые данные в оперативной памяти, периодически сбрасывая их в другую таблицу. При чтении, производится чтение данных одновременно из буфера и из другой таблицы.

Buffer(database, table, num_layers, min_time, max_time, min_rows, max_rows, min_bytes, max_bytes)

Параметры движка: database, table - таблица, в которую сбрасывать данные. Вместо имени базы данных может использоваться константное выражение, возвращающее строку. num_layers - уровень параллелизма. Физически таблица будет представлена в виде num_layers независимых буферов. Рекомендуемое значение - 16. min_time, max_time, min_rows, max_rows, min_bytes, max_bytes - условия для сброса данных из буфера. Данные сбрасываются из буфера и записываются в таблицу назначения, если выполнены все min-условия или хотя бы одно max-условие. min_time, max_time - условие на время в секундах от момента первой записи в буфер; min_rows, max_rows - условие на количество строк в буфере; min_bytes, max_bytes - условие на количество байт в буфере. При записи, данные вставляются в случайный из num_layers буферов. Или, если размер куска вставляемых данных достаточно большой (больше max_rows или max_bytes), то он записывается в таблицу назначения минуя буфер.

Distributed

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

Distributed(logs, default, hits[, sharding_key])

Данные будут читаться со всех серверов кластера logs (имя кластера в конфигурационном файле сервера), из таблицы default.hits, расположенной на каждом сервере кластера. Данные не только читаются, но и частично (настолько, насколько это возможно) обрабатываются на удалённых серверах. Например, при запросе с GROUP BY, данные будут агрегированы на удалённых серверах, промежуточные состояния агрегатных функций будут отправлены на запросивший сервер; затем данные будут доагрегированы. Вместо имени базы данных может использоваться константное выражение, возвращающее строку. Например, currentDatabase().

Кластеры задаются следующим образом:

<remote_servers>
    <logs>
        <shard>
            <!-- Не обязательно. Вес шарда при записи данных. По умолчанию, 1. -->
            <weight>1</weight>
            <!-- Не обязательно. Записывать ли данные только на одну, любую из реплик. По умолчанию, false - записывать данные на все реплики. -->
            <internal_replication>false</internal_replication>
            <replica>
                <host>example01-01-1</host>
                <port>9000</port>
            </replica>
            <replica>
                <host>example01-01-2</host>
                <port>9000</port>
            </replica>
        </shard>
        <shard>
            <weight>2</weight>
            <internal_replication>false</internal_replication>
            <replica>
                <host>example01-02-1</host>
                <port>9000</port>
            </replica>
            <replica>
                <host>example01-02-2</host>
                <port>9000</port>
            </replica>
        </shard>
    </logs>
</remote_servers>

Здесь задан кластер с именем logs, состоящий из двух шардов, каждый из которых состоит из двух реплик.

В качестве параметров для каждого сервера указываются host, port и, не обязательно, user, password:

  • host - адрес удалённого сервера. Может быть указан домен, или IPv4 или IPv6 адрес. В случае указания домена, при старте сервера делается DNS запрос, и результат запоминается на всё время работы сервера. Если DNS запрос неуспешен, то сервер не запускается. Если вы изменяете DNS-запись, перезапустите сервер.
  • port - TCP-порт для межсерверного взаимодействия (в конфиге - tcp_port, обычно 9000). Не перепутайте с http_port.
  • user - имя пользователя для соединения с удалённым сервером. по умолчанию - default. Этот пользователь должен иметь доступ для соединения с указанным сервером. Доступы настраиваются в файле users.xml, подробнее смотрите в разделе "Права доступа".
  • password - пароль для соединения с удалённым сервером, в открытом виде. по умолчанию - пустая строка.

При указании реплик, для каждого из шардов, при чтении, будет выбрана одна из доступных реплик. Можно настроить алгоритм балансировки нагрузки (то есть, предпочтения, на какую из реплик идти) - см. настройку load_balancing. Если соединение с сервером не установлено, то будет произведена попытка соединения с небольшим таймаутом. Если соединиться не удалось, то будет выбрана следующая реплика, и так для всех реплик. Если попытка соединения для всех реплик не удалась, то будут снова произведены попытки соединения по кругу, и так несколько раз. Это работает в пользу отказоустойчивости, хотя и не обеспечивает полную отказоустойчивость: удалённый сервер может принять соединение, но не работать, или плохо работать.

Можно указать от одного шарда (в таком случае, обработку запроса стоит называть удалённой, а не распределённой) до произвольного количества шардов. В каждом шарде можно указать от одной до произвольного числа реплик. Можно указать разное число реплик для каждого шарда.

Вы можете прописать сколько угодно кластеров в конфигурации. Для просмотра имеющихся кластеров, вы можете использовать системную таблицу system.clusters. Движок Distributed позволяет работать с кластером, как с локальным сервером. При этом, кластер является неэластичным: вы должны прописать его конфигурацию в конфигурационный файл сервера (лучше всех серверов кластера).

Есть два способа записывать данные на кластер:

  • Выполнять запись непосредственно на каждый шард. То есть, делать INSERT в те таблицы, на которые "смотрит" распределённая таблица. Это наиболее гибкое решение - можно использовать любую схему шардирования, которая может быть нетривиальной из-за требований предметной области. Также это является наиболее оптимальным решением, так как данные могут записываться на разные шарды полностью независимо.
  • Делать INSERT в Distributed таблицу. В этом случае, таблица будет сама распределять вставляемые данные по серверам. Для того, чтобы писать в Distributed таблицу, у неё должен быть задан ключ шардирования (последний параметр). Также, если шард лишь один, то запись работает и без указания ключа шардирования (так как в этом случае он не имеет смысла). Запись данных осуществляется полностью асинхронно. Блок данных всего лишь записывается в локальную файловую систему и отправляtтся на удалённые серверы в фоне, при первой возможности. Следует проверять, успешно ли отправляются данные, проверяя список файлов (данные, ожидающие отправки) в директории таблицы: /var/lib/clickhouse/data/database/table/.

phpClickHouse

Командой разработчиков новостного аггрегатора СМИ2, в процессе внедрения ClickHouse в свои проекты, был обнаружен ряд неудобств и отсутсвие некоторых функций. Поэтому было принято решение разработать PHP-wrapper для удобной работы с БД, реализующий возможности ClickHouse. Так появился phpClickHouse.

Особенности и основные возможности

  • Отсутствие зависимостей, требуются только модули curl и json
  • Работа с кластером ClickHouse, автоматическое определение необходимых нод при разных конфигурациях
  • Выполнение запроса на каждой ноде в кластере (см. наш отдельный проект, посвященный миграциям на ClickHouse)
  • Асинхронное выполнение запросов на чтение данных и вставку данных
  • Поддержка сжатия на лету при записи данных в ClickHouse из локального файла без создания временных файлов
  • Поддержка запросов на чтение с использованием локального CSV-файла для выполнения запроса вида select * from X where id in (local_csv_file)
  • Работа с партициями таблиц
  • Вставка массива в колонку
  • Запись результата запроса напрямую в файл с поддержкой сжатия без создания временных файлов
  • Получение размера таблицы, базы и списка процессов на каждой ноде
  • Получение статистики выполнения запроса SELECT

Установка

Установку стабильной сборки драйвера можно выполнить через composer:

composer require smi2/phpclickhouse

Или клонировать драйвер из основной (master) ветки Git-репозитория:

git clone https://github.com/smi2/phpClickHouse.git

Пример использования

Создадим подключение к серверу ClickHouse, базу данных и таблицу с помощью phpClickHouse. Выполняем запрос на подключение к серверу, создание БД и таблицы:

<?php
// Конфигурация
$config=['host'=>'192.168.1.20','port'=>'8123','username'=>'default','password'=>''];
// Создаем клиента
$client=new \ClickHouseDB\Client($config);
// Проверяем соединение с базой
$client->ping();
// Отправляем запрос на создание 
$client->write('CREATE DATABASE IF NOT EXISTS articles');
$client->write("CREATE TABLE IF NOT EXISTS articles.events (
    event_date  Date,
    event_time  DateTime,
    event_type  Enum8('VIEWS' = 1, 'CLICKS' = 2),
    site_id     Int32,
    article_id  Int32,
    ip          String,
    city        String,
    user_uuid   String,
    referer     String,
    utm         String
    ) 
    engine=MergeTree(event_date, (site_id, event_type, article_id), 8192)
");

// Выбираем default базу
$client->database('articles');
// Получаем список таблиц
print_r($client->showTables());

Запросы в драйвере разделены на следующие:

  • запись
  • вставку данных
  • чтение

Операции вставки и чтения данных могут выполняться параллельно.[Источник 3] Запросы на запись и вставку данных не содержат ответа, выполняется только проверка, что ответ сервера был положительным. Запросы на чтение ответ содержат (исключением является прямая запись ответа в файл).

Вставка данных:

$client->insert('events',
[
    [date('Y-m-d'), time(), 'CLICKS', 1, 1234, '192.168.1.1', 'Moscow', 'xcvfdsazxc', '', ''],
    [date('Y-m-d'), time(), 'CLICKS', 1, 1235, '192.168.1.1', 'Moscow', 'xcvfdsazxc', 'http://yandex.ru', ''],
    [date('Y-m-d'), time(), 'CLICKS', 1, 1236, '192.168.1.1', 'Moscow', 'xcvfdsazxc', '', ''],
    [date('Y-m-d'), time(), 'CLICKS', 1, 1237, '192.168.1.1', 'Moscow', 'xcvfdsazxc', '', ''],
],
[
    'event_date', 'event_time', 'event_type', 'site_id', 'article_id', 'ip', 'city', 'user_uuid', 'referer', 'utm'
]
);

Такой метод вставки подходит только для маленьких таблиц или таблиц справочников, так как в этом случае будет выполняться преобразование массива в строку.

Для вставки большего количества строк воспользуемся прямой загрузкой TSV-файла, который будет генерироваться при событии. Для этого будем записывать TSV-файл на сервере, где происходят события, и для упрощения отправлять его в ClickHouse. Допустим, что у нас есть некий класс UserEvent, который позволяет получить все необходимые данные для вставки, данные проверены на валидность внутри класса:

$row = [
            'event_date' => $userEvent->getDate(),
            'event_time' => $userEvent->getTime(),
            'event_type' => $userEvent->getType(),
            'site_id'    => $userEvent->getSiteId(),
            'article_id' => $userEvent->getArticleId(),
            'ip'         => $userEvent->getIp(),
            'city'       => $userEvent->getCity(),
            'user_uuid'  => $userEvent->getUserUuid(),
            'referer'    => $userEvent->getReferer(),
            'utm'        => $userEvent->getUtm(),
        ];

Запись будем производить в файл, ротируемый ежеминутно следующим способом (допускаем все недостатки — ошибки записи, блокировки, и т. д. — строка всегда записывается):

// Имя файла 
$filename='/tmp/articles.events_version1_'.date("YmdHi").'.TSV';
// Преобразование массива в строку TabSeparated
$text=\ClickHouseDB\FormatLine::TSV($row)."\n";

// Также можно преобразовать массив в строку CSV  
// $text=\ClickHouseDB\FormatLine::CSV($row)."\n";

file_put_contents($filename,$text,FILE_APPEND);

Допустим, что у нас накопилось 5—10 таких файлов, и мы готовы их отправить в базу:

$file_data_names=
[
    '/tmp/articles.events_version1_201612121201.TSV',
    '/tmp/articles.events_version1_201612121301.TSV',
    '/tmp/articles.events_version1_201612121401.TSV'
]
// Включаем сжатие
$client->enableHttpCompression(true);
// Отправляем TSV-файлы в ClickHouse
$result_insert = $client->insertBatchTSVFiles('events', [$file_data_names], [
        'event_date',
        'event_time',
        'event_type',
        'site_id',
        'article_id',
        'ip',
        'city',
        'user_uuid',
        'referer',
        'utm'
    ]);
// Получаем время, за которое данные были доставлены 
foreach ($file_data_names as $fileName) {
    echo $fileName . " : " . $result_insert[$fileName]->totalTimeRequest() . "\n";
}

Стоит отметить, что работа с CSV-файлами также поддерживается. Для них нужно использовать функцию insertBatchFiles(), аналогичную функции insertBatchTSVFiles(). Однако при использовании TSV-файлов появляется дополнительная возможность вставлять в поле DateTime дату и время в формате unix timestamp. Подробнее о поддержке формата TabSeparated см. в документации ClickHouse. ClickHouse использует формат CSV, соответствующий RFC4180. При этом стандартные средства PHP, а именно функция fputcsv(), не полностью соответствует требованиям формата. Для полноценной поддержки форматов TSV и CSV-файлов были реализованы преобразователи массива в строку: FormatLine::CSV() и FormatLine::TSV(), которые используют возможность ClickHouse хранить в колонках данные в виде массивов.

Чтение данных

Допустим, нам нужно посчитать, сколько уникальных пользователей просмотрело статьи по дням:

       SELECT
            event_date,
            uniqCombined(user_uuid) AS count_users
        FROM
            events
        WHERE
            site_id=1
        GROUP BY
            event_date
        ORDER BY
            event_date
        LIMIT 4

Сколько пользователей, которые просматривали статьи, совершили клики:

       SELECT
            user_uuid,
            count() AS clicks
        FROM
            articles.events
        WHERE
            event_type IN ( 'CLICKS' )
            AND site_id = 1
            AND user_uuid IN  (
                SELECT
                    user_uuid
                FROM
                    articles.events
                WHERE
                    event_type IN ( 'VIEWS' ) AND site_id = 1
                GROUP BY
                    user_uuid
            )
        GROUP BY user_uuid
        LIMIT 5

Какие UTM-метки давали наибольшее количество просмотров и кликов:

       SELECT
            utm,
            countIf(event_type IN('VIEWS')) AS views,
            countIf(event_type IN('CLICKS')) AS clicks
        FROM
            events
        WHERE
            event_date = today()
            AND site_id = 1
        GROUP BY
            utm
        ORDER BY
            views DESC
        LIMIT 15

Работа с кластером

Для подключения к кластеру используется класс ClickHouseDB\Cluster:

$cl = new ClickHouseDB\Cluster(
 ['host'=>'allclickhouse.smi2','port'=>'8123','username'=>'x','password'=>'x']
);

В DNS-записи allclickhouse.smi2 перечислены IP-адреса всех узлов: ch63.smi2, ch64.smi2, ch65.smi2, ch66.smi2, что позволяет использовать механизм Round-robin DNS. Драйвер выполняет подключение к кластеру и отправляет ping-запросы на каждый узел, перечисленный в DNS-записи.

Установка максимального времени подключения ко всем узлам кластера настраивается следующим образом:

$cl->setScanTimeOut(2.5); // 2500 ms

Проверка состояния реплик кластера выполняется так:

if (!$cl->isReplicasIsOk())
{
   throw new Exception('Replica state is bad , error='.$cl->getError());
}

Состояние ClickHouse-кластера проверяется следующим образом:

  • Проверяются соединения со всеми узлами кластера, перечисленными в DNS-записи.
  • На каждый узел отправляется SQL-запрос, который позволяет определить состояние всех реплик ClickHouse-кластера.

Скорость выполнения запроса может быть увеличена, если не вычитывать значения столбцов log_max_index, log_pointer, total_replicas, active_replicas, при получении данных из которых выполняются запросы на ZK-кластер.

Для облегченной проверки в драйвере необходимо установить специальный флаг:

$cl->setSoftCheck(true);

Получение списка всех доступных кластеров делается следующим образом:

print_r($cl->getClusterList());
// result
//    [0] => pulse
//    [1] => repikator
//    [2] => sharovara

Например, получить конфигурацию кластеров, которые были описаны выше, можно так:

foreach (['pulse','repikator','sharovara'] as $name)
{
   print_r($cl->getClusterNodes($name));
   echo "> $name , count shard   = ".$cl->getClusterCountShard($name)." ; count replica = ".$cl->getClusterCountReplica($name)."\n";
}

//Результат:
//>  pulse , count shard = 2 ; count replica = 2
//>  repikator , count shard = 1 ; count replica = 4
//>  sharovara , count shard = 4 ; count replica = 1

Получение списка узлов по названию кластера или из шардированных таблиц:

$nodes=$cl->getNodesByTable('sharovara.body_views_sharded');

$nodes=$cl->getClusterNodes('sharovara');

Получение размера таблицы или размеров всех таблиц через отправку запроса на каждый узел кластера:

foreach ($nodes as $node)
{
   echo "$node > \n";
   print_r($cl->client($node)->tableSize('test_sharded'));
   print_r($cl->client($node)->tablesSize());
}

// Упрощенный вариант использования
$cl->getSizeTable('dbName.tableName');

Получение списка таблиц кластера:

$cl->getTables()

Определение лидера в кластере:

$cl->getMasterNodeForTable('dbName.tableName') // Лидер имеет установленный флаг is_leader=1

Запросы, связанные, например, с удалением или изменением структуры, отправляются на узел с установленным флагом is_leader.

Очистка данных в таблице в кластере:

$cl->truncateTable('dbName.tableName')`

Управление миграциями: phpMigrationsClickHouse

Вышеупомянутой командой СМИ2 был также создан инструмент для миграции DDL-запросов в кластер - phpMigrationsClickHouse.

Установка и запуск

Установка:

git clone https://github.com/smi2/phpMigrationsClickhouse.git
cd phpMigrationsClickhouse

git submodule init
git submodule update --init --recursive

# copy example config
cp config.default.php config.php

mcedit config.php

Запуск:

./migration.sh help
# php _migration.php help

./migration.sh run 

./migration.sh run [--config=/path/cnf.php --server=config_id]

Откроет выбор сервера/конфигурации и далее выбор миграции. Если вызвать execone, производит миграцию одного указанного файла

./migration.sh execone --file=020_test_db.sql [--config=/path/cnf.php --server=config_id]

config.php

<?php
return
[
    'clickhouseProduction'=>
        [
            'clickhouse'=>['host' => 'prod.clickhouse.host.smi2.ru', 'port' => '8123', 'username' => 'UUU',  'password' => 'PPP'],
            'repository'=>__DIR__ . '/../ClickhouseMigrations/',
            'path'=>'ch2.production',
            'split'=>['query'=>';;'],
        ],
    'clickhouseDEVServer'=>
        [
            'clickhouse'=>['host' => '192.168.1.20', 'port' => '8123', 'username' => 'UUU',  'password' => 'PPP'],
            'repository'=>__DIR__ . '/../ClickhouseMigrations/',
            'path'=>'ch.develop',
            'split'=>['query'=>';;'],
        ]
];

Использование

Формат шаблонов PHP [Источник 4]

  • setAutoSplitQuery - разделитель запросов
  • setTimeout - время выполнение каждого запроса
  • addSqlUpdate - что накатываем
  • addSqlDowngrade - что откатываем

Пример

Создадим PHP-файл, содержащий следующий код:

$cluster_name = 'pulse'; 
$mclq = new \ClickHouseDB\Cluster\Migration($cluster_name);
$mclq->setTimeout(100);

Добавим SQL-запросы, которые нужно накатить:

$mclq->addSqlUpdate(" CREATE DATABASE IF NOT EXISTS dbpulse  "); 
$mclq->addSqlUpdate(" 

 CREATE TABLE IF NOT EXISTS dbpulse.normal_summing_sharded (
     event_date Date DEFAULT toDate(event_time),
     event_time DateTime DEFAULT now(),
     body_id Int32,
     views Int32
 ) ENGINE = ReplicatedSummingMergeTree('/clickhouse/tables/{pulse_replica}/pulse/normal_summing_sharded', '{replica}', event_date, (event_date, event_time, body_id), 8192)
 "); 

Добавим SQL-запросы для выполнения отката в случае ошибки:

$mclq->addSqlDowngrade(' DROP TABLE IF EXISTS dbpulse.normal_summing_sharded '); 

$mclq->addSqlDowngrade(' DROP DATABASE IF EXISTS dbpulse  '); 

Существует 2 стратегии накатывания миграций:

  • отправка каждого отдельного SQL-запроса на один сервер с переходом к следующему SQL-запросу;
  • отправка всех SQL-запросов на один сервер с переходом к следующему серверу.

При возникновении ошибки возможны следующие варианты: [Источник 5]

  • выполнение downgrade-запроса на все узлы, на которых уже были произведены upgrade-запросы;
  • ожидание перед отправкой upgrade-запросов на другие сервера;
  • выполнение downgrade-запроса на всех серверах в случае возникновения ошибки.

Отдельное место занимают ошибки, когда не известно состояние кластера:

  • ошибка timeout соединения;
  • ошибка связи с сервером.

Принцип работы PHP-кода при выполнении миграции следующий:

// Получение списка IP-адресов узлов кластера
$node_hosts=$this->getClusterNodes($migration->getClusterName());
// Получение downgrade-запроса
$sql_down=$migration->getSqlDowngrade();
// Получение upgrade-запроса
$sql_up=$migration->getSqlUpdate();

// Выполнение upgrade-запроса на каждый узел и, в случае ошибки, выполнение downgrade-запроса

$need_undo=false;
$undo_ip=[];

foreach ($sql_up as $s_u) {
    foreach ($node_hosts as $node) {
        // Выполнение upgrade-запроса
        $state=$this->client($node)->write($s_u);

        if ($state->isError()) {
            $need_undo = true;
        } else {
            // OK
        }

        if ($need_undo) {
            // Фиксация узлов кластера, где произошла ошибка  
            $undo_ip[$node]=1;
            break;
        }
    }
}

// Проверка успешности выполнения upgrade-запросов на всех узлах кластера
if (!$need_undo)
{
    return true; // OK
}

В случае ошибки выполняется отправка на все узлы кластера downgrade-запроса:

foreach ($node_hosts as $node) {
    foreach ($sql_down as $s_u) {
        try{
            $st=$this->client($node)->write($s_u);
        } catch (Exception $E) {
            // Оповещение пользователя об ошибке при выполнении downgrade-запроса

        }
    }
}


Источники

  1. Wikipedia [Электронный ресурс] ClickHouse / Дата обращения: 17.08.2018. Режим доступа: https://ru.wikipedia.org/wiki/ClickHouse
  2. clickhouse.yandex [Электронный ресурс] Документация ClickHouse / Дата обращения: 18.08.2018. Режим доступа: https://clickhouse.yandex/docs/ru/
  3. habr.com [Электронный ресурс] Как запустить ClickHouse своими силами и выиграть джекпот / Дата обращения 22.08.2018. Режим доступа: https://habr.com/company/smi2/blog/314558/
  4. github.com [Электронный ресурс] Документация phpMigrationsClickhouse / Дата обращения 23.08.2018. Режим доступа: https://github.com/smi2/phpMigrationsClickhouse
  5. habr.com [Электронный ресурс] Масштабирование ClickHouse, управление миграциями и отправка запросов из PHP в кластер / Дата обращения 22.08.2018. Режим доступа: https://habr.com/company/smi2/blog/317682/