55 KiB
machine_translated | machine_translated_rev |
---|---|
true | 1cd5f0028d |
Обзор архитектуры ClickHouse
ClickHouse-это настоящая СУБД, ориентированная на столбцы. Данные хранятся столбцами и во время выполнения массивов (векторов или кусков столбцов). Когда это возможно, операции отправляются на массивы, а не на отдельные значения. Это называется «vectorized query execution,» и это помогает снизить стоимость фактической обработки данных.
В этой идее нет ничего нового. Она восходит к тому времени, когда
APL
язык программирования и его потомки:A +
,J
,K
, иQ
. Массивное программирование используется в научной обработке данных. Эта идея также не является чем-то новым в реляционных базах данных: например, она используется вVectorwise
система.
Существует два различных подхода для ускорения обработки запросов: векторизованное выполнение запросов и генерация кода во время выполнения. Последнее устраняет все косвенные действия и динамическую диспетчеризацию. Ни один из этих подходов не является строго лучшим, чем другой. Генерация кода во время выполнения может быть лучше, когда он объединяет множество операций, таким образом полностью используя исполнительные блоки процессора и конвейер. Векторизованное выполнение запроса может быть менее практичным, поскольку оно включает временные векторы, которые должны быть записаны в кэш и считаны обратно. Если временные данные не помещаются в кэш L2, это становится проблемой. Но векторизованное выполнение запросов более легко использует возможности SIMD центрального процессора. Один научная статья написанное нашими друзьями показывает, что лучше сочетать оба подхода. ClickHouse использует векторизованное выполнение запросов и имеет ограниченную начальную поддержку для генерации кода во время выполнения.
Столбцы
IColumn
интерфейс используется для представления столбцов в памяти (собственно, кусков столбцов). Этот интерфейс предоставляет вспомогательные методы для реализации различных реляционных операторов. Почти все операции неизменяемы: они не изменяют исходный столбец, а создают новый измененный. Например, в IColumn :: filter
метод принимает маску байта фильтра. Он используется для WHERE
и HAVING
реляционный оператор. Дополнительные примеры: IColumn :: permute
способ поддержки ORDER BY
, этот IColumn :: cut
способ поддержки LIMIT
.
Различный IColumn
реализации (ColumnUInt8
, ColumnString
, и так далее) отвечают за расположение столбцов в памяти. Расположение памяти обычно представляет собой непрерывный массив. Для целочисленного типа столбцов это всего лишь один непрерывный массив, например std :: vector
. Для String
и Array
столбцы, это два вектора: один для всех элементов массива, расположенных последовательно, и второй для смещений к началу каждого массива. Существует также ColumnConst
это сохраняет только одно значение в памяти, но выглядит как столбец.
Поле
Тем не менее, можно работать и с индивидуальными ценностями. Чтобы представить индивидуальную ценность, то Field
предназначенный. Field
это просто дискриминированный Союз UInt64
, Int64
, Float64
, String
и Array
. IColumn
имеет operator[]
метод получения n-го значения в виде a Field
и insert
способ, чтобы добавить Field
до самого конца колонны. Эти методы не очень эффективны, потому что они требуют решения временных проблем Field
объекты, представляющие индивидуальную ценность. Существуют и более эффективные методы, такие как insertFrom
, insertRangeFrom
и так далее.
Field
у него нет достаточной информации о конкретном типе данных для таблицы. Например, UInt8
, UInt16
, UInt32
, и UInt64
все они представлены в виде UInt64
в Field
.
Дырявые абстракции
IColumn
есть методы для общих реляционных преобразований данных, но они не удовлетворяют всем потребностям. Например, ColumnUInt64
не имеет метода для вычисления суммы двух столбцов, и ColumnString
у него нет метода для запуска поиска по подстрокам. Эти бесчисленные процедуры реализуются за пределами IColumn
.
Различные функции на столбцах могут быть реализованы общим, неэффективным способом с использованием IColumn
способы извлечения Field
значения, или специализированным способом, использующим знание внутренней компоновки памяти данных в определенном месте. IColumn
реализация. Он реализуется путем приведения функций к определенному виду IColumn
тип и дело с внутренним представлением непосредственно. Например, ColumnUInt64
имеет getData
метод, который возвращает ссылку на внутренний массив, а затем отдельная процедура считывает или заполняет этот массив непосредственно. У нас есть «leaky abstractions» чтобы обеспечить эффективную специализацию различных процедур.
Тип данных
IDataType
отвечает за сериализацию и десериализацию: чтение и запись фрагментов столбцов или отдельных значений в двоичной или текстовой форме. IDataType
непосредственно соответствует типам данных в таблицах. Например, существуют DataTypeUInt32
, DataTypeDateTime
, DataTypeString
и так далее.
IDataType
и IColumn
они лишь слабо связаны друг с другом. Различные типы данных могут быть представлены в памяти одним и тем же именем IColumn
реализации. Например, DataTypeUInt32
и DataTypeDateTime
оба они представлены следующим образом ColumnUInt32
или ColumnConstUInt32
. Кроме того, один и тот же тип данных может быть представлен разными IColumn
реализации. Например, DataTypeUInt8
может быть представлен следующим образом ColumnUInt8
или ColumnConstUInt8
.
IDataType
хранит только метаданные. Например, DataTypeUInt8
не хранит вообще ничего (кроме vptr) и DataTypeFixedString
магазины просто N
(размер строк фиксированного размера).
IDataType
имеет вспомогательные методы для различных форматов данных. Примерами являются методы сериализации значения с возможным цитированием, сериализации значения для JSON и сериализации значения в формате XML. Прямого соответствия форматам данных не существует. Например, различные форматы данных Pretty
и TabSeparated
можно использовать то же самое serializeTextEscaped
вспомогательный метод от IDataType
интерфейс.
Блок
A Block
это контейнер, представляющий подмножество (фрагмент) таблицы в памяти. Это всего лишь набор троек: (IColumn, IDataType, column name)
. Во время выполнения запроса данные обрабатываются с помощью Block
s. Если у нас есть Block
, у нас есть данные (в IColumn
объект), у нас есть информация о его типе (в IDataType
) это говорит нам, как обращаться с этим столбцом, и у нас есть имя столбца. Это может быть либо исходное имя столбца из таблицы, либо какое-то искусственное имя, назначенное для получения временных результатов вычислений.
Когда мы вычисляем некоторую функцию по столбцам в блоке, мы добавляем другой столбец с его результатом в блок, и мы не касаемся столбцов для аргументов функции, потому что операции неизменяемы. Позже ненужные столбцы могут быть удалены из блока, но не изменены. Это удобно для исключения общих подвыражений.
Блоки создаются для каждого обработанного фрагмента данных. Обратите внимание, что для одного и того же типа вычисления имена столбцов и типы остаются одинаковыми для разных блоков, и изменяются только данные столбцов. Лучше разделить данные блока из заголовка блока, потому что небольшие размеры блока имеют высокую нагрузку временных строк для копирования shared_ptrs и имен столбцов.
Блокировать Потоки
Блочные потоки предназначены для обработки данных. Мы используем потоки блоков для чтения данных откуда-то, выполнения преобразований данных или записи данных куда-то. IBlockInputStream
имеет read
метод для извлечения следующего блока, пока он доступен. IBlockOutputStream
имеет write
метод, чтобы подтолкнуть блок куда-то.
Потоки отвечают за:
- Чтение или письмо за столом. Таблица просто возвращает поток для чтения или записи блоков.
- Реализация форматов данных. Например, если вы хотите вывести данные на терминал в
Pretty
форматирование, вы создаете поток вывода блока, где вы толкаете блоки, и он форматирует их. - Выполнение преобразований данных. Скажем так у вас есть
IBlockInputStream
и хотите создать отфильтрованный поток. Вы создаетеFilterBlockInputStream
и инициализируйте его с помощью своего потока. Затем, когда вы вытащите блок изFilterBlockInputStream
, он извлекает блок из вашего потока, фильтрует его и возвращает отфильтрованный блок вам. Конвейеры выполнения запросов представлены таким образом.
Есть и более сложные трансформации. Например, когда вы тянете из AggregatingBlockInputStream
, он считывает все данные из своего источника, агрегирует их, а затем возвращает поток агрегированных данных для вас. Еще пример: UnionBlockInputStream
принимает множество источников ввода в конструкторе, а также ряд потоков. Он запускает несколько потоков и читает из нескольких источников параллельно.
Потоки блокируют использовать «pull» подход к управлению потоком: когда вы вытягиваете блок из первого потока, он, следовательно, вытягивает необходимые блоки из вложенных потоков, и весь конвейер выполнения будет работать. Ни «pull» ни «push» это лучшее решение, потому что поток управления является неявным, и это ограничивает реализацию различных функций, таких как одновременное выполнение нескольких запросов (объединение многих конвейеров вместе). Это ограничение может быть преодолено с помощью сопрограмм или просто запуском дополнительных потоков, которые ждут друг друга. У нас может быть больше возможностей, если мы сделаем поток управления явным: если мы найдем логику для передачи данных из одной расчетной единицы в другую вне этих расчетных единиц. Читать это статья для новых мыслей.
Следует отметить, что конвейер выполнения запроса создает временные данные на каждом шаге. Мы стараемся держать размер блока достаточно маленьким, чтобы временные данные помещались в кэш процессора. При таком допущении запись и чтение временных данных практически бесплатны по сравнению с другими расчетами. Мы могли бы рассмотреть альтернативу, которая заключается в том, чтобы объединить многие операции в трубопроводе вместе. Это может сделать конвейер как можно короче и удалить большую часть временных данных, что может быть преимуществом, но у него также есть недостатки. Например, разделенный конвейер позволяет легко реализовать кэширование промежуточных данных, кражу промежуточных данных из аналогичных запросов, выполняемых одновременно, и объединение конвейеров для аналогичных запросов.
Форматы
Форматы данных реализуются с помощью блочных потоков. Есть «presentational» форматы, пригодные только для вывода данных клиенту, такие как Pretty
формат, который предоставляет только IBlockOutputStream
. И есть форматы ввода/вывода, такие как TabSeparated
или JSONEachRow
.
Существуют также потоки подряд : IRowInputStream
и IRowOutputStream
. Они позволяют вытягивать / выталкивать данные отдельными строками, а не блоками. И они нужны только для упрощения реализации ориентированных на строки форматов. Обертка BlockInputStreamFromRowInputStream
и BlockOutputStreamFromRowOutputStream
позволяет конвертировать потоки, ориентированные на строки, в обычные потоки, ориентированные на блоки.
I/O
Для байт-ориентированных входов / выходов существуют ReadBuffer
и WriteBuffer
абстрактный класс. Они используются вместо C++ iostream
s. Не волнуйтесь: каждый зрелый проект C++ использует что-то другое, чем iostream
s по уважительным причинам.
ReadBuffer
и WriteBuffer
это просто непрерывный буфер и курсор, указывающий на позицию в этом буфере. Реализации могут владеть или не владеть памятью для буфера. Существует виртуальный метод заполнения буфера следующими данными (для ReadBuffer
) или смыть буфер куда-нибудь (например WriteBuffer
). Виртуальные методы редко вызываются.
Реализация следующих принципов: ReadBuffer
/WriteBuffer
используются для работы с файлами и файловыми дескрипторами, а также сетевыми сокетами, для реализации сжатия (CompressedWriteBuffer
is initialized with another WriteBuffer and performs compression before writing data to it), and for other purposes – the names ConcatReadBuffer
, LimitReadBuffer
, и HashingWriteBuffer
за себя говорить.
Буферы чтения/записи имеют дело только с байтами. Есть функции от ReadHelpers
и WriteHelpers
заголовочные файлы, чтобы помочь с форматированием ввода / вывода. Например, есть помощники для записи числа в десятичном формате.
Давайте посмотрим, что происходит, когда вы хотите написать результирующий набор в JSON
форматирование в stdout. У вас есть результирующий набор, готовый к извлечению из него IBlockInputStream
. Вы создаете WriteBufferFromFileDescriptor(STDOUT_FILENO)
чтобы записать байты в stdout. Вы создаете JSONRowOutputStream
, инициализируется с помощью этого WriteBuffer
, чтобы записать строки в JSON
в stdout. Вы создаете BlockOutputStreamFromRowOutputStream
кроме того, чтобы представить его как IBlockOutputStream
. А потом ты позвонишь copyData
для передачи данных из IBlockInputStream
к IBlockOutputStream
и все это работает. Внутренне, JSONRowOutputStream
буду писать в формате JSON различные разделители и вызвать IDataType::serializeTextJSON
метод со ссылкой на IColumn
и номер строки в качестве аргументов. Следовательно, IDataType::serializeTextJSON
вызовет метод из WriteHelpers.h
: например, writeText
для числовых типов и writeJSONString
для DataTypeString
.
Таблицы
То IStorage
интерфейс представляет собой таблицы. Различные реализации этого интерфейса являются различными движками таблиц. Примеры StorageMergeTree
, StorageMemory
и так далее. Экземпляры этих классов являются просто таблицами.
Ключ IStorage
методы read
и write
. Есть и другие варианты alter
, rename
, drop
и так далее. То read
метод принимает следующие аргументы: набор столбцов для чтения из таблицы, набор столбцов для чтения из таблицы. AST
запрос для рассмотрения и желаемое количество потоков для возврата. Он возвращает один или несколько IBlockInputStream
объекты и информация о стадии обработки данных, которая была завершена внутри табличного движка во время выполнения запроса.
В большинстве случаев метод read отвечает только за чтение указанных столбцов из таблицы, а не за дальнейшую обработку данных. Вся дальнейшая обработка данных осуществляется интерпретатором запросов и не входит в сферу ответственности компании IStorage
.
Но есть и заметные исключения:
- Запрос AST передается на сервер
read
метод, и механизм таблиц может использовать его для получения использования индекса и считывания меньшего количества данных из таблицы. - Иногда механизм таблиц может сам обрабатывать данные до определенного этапа. Например,
StorageDistributed
можно отправить запрос на удаленные серверы, попросить их обработать данные на этапе, когда данные с разных удаленных серверов могут быть объединены, и вернуть эти предварительно обработанные данные. Затем интерпретатор запросов завершает обработку данных.
Стол read
метод может возвращать несколько значений IBlockInputStream
объекты, позволяющие осуществлять параллельную обработку данных. Эти несколько блочных входных потоков могут считываться из таблицы параллельно. Затем вы можете обернуть эти потоки с помощью различных преобразований (таких как вычисление выражений или фильтрация), которые могут быть вычислены независимо, и создать UnionBlockInputStream
поверх них, чтобы читать из нескольких потоков параллельно.
Есть и другие варианты TableFunction
s. Это функции, которые возвращают временное значение IStorage
объект для использования в FROM
предложение запроса.
Чтобы получить быстрое представление о том, как реализовать свой движок таблиц, посмотрите на что-то простое, например StorageMemory
или StorageTinyLog
.
В результате этого
read
метод,IStorage
возвращаетсяQueryProcessingStage
– information about what parts of the query were already calculated inside storage.
Синтаксический анализатор
Написанный от руки рекурсивный парсер спуска анализирует запрос. Например, ParserSelectQuery
просто рекурсивно вызывает базовые Парсеры для различных частей запроса. Парсеры создают AST
. То AST
представлен узлами, которые являются экземплярами IAST
.
Генераторы парсеров не используются по историческим причинам.
Переводчики
Интерпретаторы отвечают за создание конвейера выполнения запроса из AST
. Есть простые переводчики, такие как InterpreterExistsQuery
и InterpreterDropQuery
или более изощренные InterpreterSelectQuery
. Конвейер выполнения запроса представляет собой комбинацию блочных входных и выходных потоков. Например, результат интерпретации SELECT
запросов IBlockInputStream
для чтения результирующего набора из; результат запроса INSERT - это IBlockOutputStream
чтобы записать данные для вставки в, и результат интерпретации INSERT SELECT
запросов IBlockInputStream
это возвращает пустой результирующий набор при первом чтении, но копирует данные из него SELECT
к INSERT
в то же время.
InterpreterSelectQuery
использует ExpressionAnalyzer
и ExpressionActions
машины для анализа запросов и преобразований. Именно здесь выполняется большинство оптимизаций запросов на основе правил. ExpressionAnalyzer
это довольно грязно и должно быть переписано: различные преобразования запросов и оптимизации должны быть извлечены в отдельные классы, чтобы позволить модульные преобразования или запрос.
Функции
Существуют обычные функции и агрегатные функции. Агрегатные функции см. В следующем разделе.
Ordinary functions don’t change the number of rows – they work as if they are processing each row independently. In fact, functions are not called for individual rows, but for Block
’s данных для реализации векторизованного выполнения запросов.
Есть некоторые другие функции, такие как размер блока, роунумберинблок, и runningAccumulate, которые эксплуатируют обработку блоков и нарушают независимость строк.
ClickHouse имеет сильную типизацию, поэтому нет никакого неявного преобразования типов. Если функция не поддерживает определенную комбинацию типов, она создает исключение. Но функции могут работать (перегружаться) для многих различных комбинаций типов. Например, в plus
функция (для реализации +
оператор) работает для любой комбинации числовых типов: UInt8
+ Float32
, UInt16
+ Int8
и так далее. Кроме того, некоторые вариадические функции могут принимать любое количество аргументов, например concat
функция.
Реализация функции может быть немного неудобной, поскольку функция явно отправляет поддерживаемые типы данных и поддерживается IColumns
. Например, в plus
функция имеет код, генерируемый экземпляром шаблона C++ для каждой комбинации числовых типов, а также постоянные или непостоянные левые и правые аргументы.
Это отличное место для реализации генерации кода во время выполнения, чтобы избежать раздувания кода шаблона. Кроме того, он позволяет добавлять слитые функции, такие как fused multiply-add или выполнять несколько сравнений в одной итерации цикла.
Из-за векторизованного выполнения запроса функции не закорачиваются. Например, если вы пишете WHERE f(x) AND g(y)
, обе стороны вычисляются, даже для строк, когда f(x)
равно нулю (за исключением тех случаев, когда f(x)
является нулевым постоянным выражением). Но если избирательность самого f(x)
состояние является высоким, и расчет f(x)
это гораздо дешевле, чем g(y)
, лучше всего реализовать многоходовой расчет. Это будет первый расчет f(x)
, затем отфильтруйте столбцы по результату, а затем вычислите g(y)
только для небольших отфильтрованных фрагментов данных.
Статистическая функция
Агрегатные функции - это функции, определяющие состояние. Они накапливают переданные значения в некотором состоянии и позволяют получать результаты из этого состояния. Они управляются с помощью IAggregateFunction
интерфейс. Состояния могут быть довольно простыми (состояние для AggregateFunctionCount
это всего лишь один человек UInt64
значение) или довольно сложное (состояние AggregateFunctionUniqCombined
представляет собой комбинацию линейного массива, хэш-таблицы и HyperLogLog
вероятностная структура данных).
Государства распределяются в Arena
(пул памяти) для работы с несколькими состояниями при выполнении высокой мощности GROUP BY
запрос. Состояния могут иметь нетривиальный конструктор и деструктор: например, сложные агрегатные состояния могут сами выделять дополнительную память. Это требует некоторого внимания к созданию и уничтожению государств и правильной передаче их права собственности и порядка уничтожения.
Агрегатные состояния могут быть сериализованы и десериализованы для передачи по сети во время выполнения распределенного запроса или для записи их на диск, где недостаточно оперативной памяти. Они даже могут храниться в таблице с DataTypeAggregateFunction
чтобы разрешить инкрементное агрегирование данных.
Сериализованный формат данных для состояний агрегатных функций в настоящее время не является версионным. Это нормально, если агрегатные состояния хранятся только временно. Но у нас есть такая возможность
AggregatingMergeTree
механизм таблиц для инкрементного агрегирования, и люди уже используют его в производстве. Именно по этой причине обратная совместимость требуется при изменении сериализованного формата для любой агрегатной функции в будущем.
Сервер
Сервер реализует несколько различных интерфейсов:
- Интерфейс HTTP для любых иностранных клиентов.
- TCP-интерфейс для собственного клиента ClickHouse и для межсерверной связи во время выполнения распределенного запроса.
- Интерфейс для передачи данных для репликации.
Внутренне это просто примитивный многопоточный сервер без сопрограмм или волокон. Поскольку сервер предназначен не для обработки высокой скорости простых запросов, а для обработки относительно низкой скорости сложных запросов, каждый из них может обрабатывать огромное количество данных для аналитики.
Сервер инициализирует программу Context
класс с необходимой средой для выполнения запроса: список доступных баз данных, пользователей и прав доступа, настройки, кластеры, список процессов, журнал запросов и так далее. Переводчики используют эту среду.
Мы поддерживаем полную обратную и прямую совместимость для протокола TCP сервера: старые клиенты могут разговаривать с новыми серверами, а новые клиенты-со старыми серверами. Но мы не хотим поддерживать его вечно, и мы удаляем поддержку старых версий примерно через год.
!!! note "Примечание" Для большинства внешних приложений мы рекомендуем использовать интерфейс HTTP, поскольку он прост и удобен в использовании. Протокол TCP более тесно связан с внутренними структурами данных: он использует внутренний формат для передачи блоков данных, а также использует пользовательское обрамление для сжатых данных. Мы не выпустили библиотеку C для этого протокола, потому что она требует связывания большей части кодовой базы ClickHouse, что нецелесообразно.
Выполнение Распределенных Запросов
Серверы в кластерной установке в основном независимы. Вы можете создать Distributed
таблица на одном или всех серверах кластера. То Distributed
table does not store data itself – it only provides a «view» ко всем локальным таблицам на нескольких узлах кластера. Когда вы выберите из Distributed
таблица, он переписывает этот запрос, выбирает удаленные узлы в соответствии с настройками балансировки нагрузки и отправляет запрос к ним. То Distributed
таблица запрашивает удаленные серверы для обработки запроса только до стадии, когда промежуточные результаты с разных серверов могут быть объединены. Затем он получает промежуточные результаты и сливает их. Распределенная таблица пытается распределить как можно больше работы на удаленные серверы и не отправляет много промежуточных данных по сети.
Все становится сложнее, когда у вас есть подзапросы в предложениях IN или JOIN, и каждый из них использует a Distributed
стол. У нас есть разные стратегии выполнения этих запросов.
Глобального плана запросов для выполнения распределенных запросов не существует. Каждый узел имеет свой локальный план запроса для своей части задания. У нас есть только простое однопроходное распределенное выполнение запросов: мы отправляем запросы на удаленные узлы, а затем объединяем результаты. Но это неосуществимо для сложных запросов с высокой мощностью группы BYs или с большим количеством временных данных для соединения. В таких случаях нам необходимо: «reshuffle» данные между серверами, что требует дополнительной координации. ClickHouse не поддерживает такого рода выполнение запросов, и мы должны работать над этим.
Дерево Слияния
MergeTree
это семейство механизмов хранения данных, поддерживающих индексацию по первичному ключу. Первичный ключ может быть произвольным кортежем столбцов или выражений. Данные в a MergeTree
таблица хранится в «parts». Каждая часть хранит данные в порядке первичного ключа, поэтому данные лексикографически упорядочиваются кортежем первичного ключа. Все столбцы таблицы хранятся отдельно column.bin
файлы в этих краях. Файлы состоят из сжатых блоков. Каждый блок обычно содержит от 64 КБ до 1 МБ несжатых данных, в зависимости от среднего размера значения. Блоки состоят из значений столбцов, расположенных последовательно друг за другом. Значения столбцов находятся в одном и том же порядке для каждого столбца (первичный ключ определяет порядок), поэтому при итерации по многим столбцам вы получаете значения для соответствующих строк.
Сам первичный ключ является «sparse». Он адресует не каждую отдельную строку, а только некоторые диапазоны данных. Разделение primary.idx
файл имеет значение первичного ключа для каждой N-й строки, где N называется index_granularity
(обычно N = 8192). Кроме того, для каждой колонки у нас есть column.mrk
файлы с «marks,» которые являются смещениями для каждой N-й строки в файле данных. Каждая метка представляет собой пару: смещение в файле к началу сжатого блока и смещение в распакованном блоке к началу данных. Обычно сжатые блоки выравниваются по меткам, а смещение в распакованном блоке равно нулю. Данные для primary.idx
всегда находится в памяти, а данные для column.mrk
файлы кэшируются.
Когда мы собираемся прочитать что-то из части в MergeTree
, мы смотрим на primary.idx
данные и найдите диапазоны, которые могут содержать запрошенные данные, а затем посмотрите на column.mrk
данные и рассчитать смещения для того, чтобы начать чтение этих диапазонов. Из-за разреженности могут быть прочитаны избыточные данные. ClickHouse не подходит для высокой загрузки простых точечных запросов, так как весь диапазон с index_granularity
строки должны быть прочитаны для каждого ключа, и весь сжатый блок должен быть распакован для каждого столбца. Мы сделали индекс разреженным, потому что мы должны быть в состоянии поддерживать триллионы строк на одном сервере без заметного потребления памяти для индекса. Кроме того, поскольку первичный ключ разрежен, он не является уникальным: он не может проверить существование ключа в таблице во время вставки. В таблице может быть много строк с одним и тем же ключом.
Когда вы INSERT
куча данных в MergeTree
, эта связка сортируется по порядку первичного ключа и образует новую часть. Существуют фоновые потоки, которые периодически выделяют некоторые детали и объединяют их в одну сортированную деталь, чтобы сохранить количество деталей относительно низким. Вот почему он так называется MergeTree
. Конечно, слияние приводит к тому, что «write amplification». Все части неизменны: они только создаются и удаляются, но не изменяются. Когда SELECT выполняется, он содержит снимок таблицы (набор деталей). После слияния мы также сохраняем старые детали в течение некоторого времени, чтобы облегчить восстановление после сбоя, поэтому, если мы видим, что какая-то объединенная деталь, вероятно, сломана, мы можем заменить ее исходными частями.
MergeTree
это не дерево LSM, потому что оно не содержит «memtable» и «log»: inserted data is written directly to the filesystem. This makes it suitable only to INSERT data in batches, not by individual row and not very frequently – about once per second is ok, but a thousand times a second is not. We did it this way for simplicity’s sake, and because we are already inserting data in batches in our applications.
Таблицы MergeTree могут иметь только один (первичный) индекс: вторичных индексов не существует. Было бы неплохо разрешить несколько физических представлений в одной логической таблице, например, хранить данные в более чем одном физическом порядке или даже разрешить представления с предварительно агрегированными данными наряду с исходными данными.
Есть движки MergeTree, которые выполняют дополнительную работу во время фоновых слияний. Примеры CollapsingMergeTree
и AggregatingMergeTree
. Это можно рассматривать как специальную поддержку обновлений. Имейте в виду, что это не настоящие обновления, поскольку пользователи обычно не имеют никакого контроля над временем выполнения фоновых слияний, а данные в MergeTree
таблица почти всегда хранится в нескольких частях, а не в полностью объединенном виде.
Копирование
Репликация в ClickHouse может быть настроена на основе каждой таблицы. Вы можете иметь некоторые реплицированные и некоторые нереплицированные таблицы на одном сервере. Вы также можете иметь таблицы, реплицируемые различными способами,например, одна таблица с двухфакторной репликацией, а другая-с трехфакторной.
Репликация осуществляется в виде ReplicatedMergeTree
подсистема хранилища. Путь в ZooKeeper
указывается в качестве параметра для механизма хранения данных. Все таблицы с одинаковым путем внутри ZooKeeper
становятся репликами друг друга: они синхронизируют свои данные и поддерживают согласованность. Реплики можно добавлять и удалять динамически, просто создавая или удаляя таблицу.
Репликация использует асинхронную многомастерную схему. Вы можете вставить данные в любую реплику, которая имеет сеанс с ZooKeeper
, и данные реплицируются во все остальные реплики асинхронно. Поскольку ClickHouse не поддерживает обновления, репликация является бесконфликтной. Поскольку нет подтверждения кворума вставок, только что вставленные данные могут быть потеряны, если один узел выйдет из строя.
Метаданные для репликации хранятся в ZooKeeper. Существует журнал репликации, в котором перечислены необходимые действия. Действия таковы: получить часть; объединить части; удалить раздел и так далее. Каждая реплика копирует журнал репликации в свою очередь, а затем выполняет действия из этой очереди. Например, при вставке «get the part» действие создается в журнале, и каждая реплика загружает эту часть. Слияния координируются между репликами для получения идентичных байтам результатов. Все части объединяются одинаково на всех репликах. Это достигается путем выбора одной реплики в качестве лидера, и эта реплика инициирует слияние и запись «merge parts» действия по ведению журнала.
Репликация является физической: между узлами передаются только сжатые части, а не запросы. Слияния обрабатываются на каждой реплике независимо в большинстве случаев, чтобы снизить затраты на сеть, избегая усиления сети. Большие объединенные части передаются по сети только в случаях значительного запаздывания репликации.
Кроме того, каждая реплика хранит свое состояние в ZooKeeper как набор деталей и их контрольные суммы. Когда состояние локальной файловой системы отличается от эталонного состояния в ZooKeeper, реплика восстанавливает свою согласованность, загружая недостающие и сломанные части из других реплик. Когда в локальной файловой системе появляются неожиданные или неработающие данные, ClickHouse не удаляет их, а перемещает в отдельный каталог и забывает.
!!! note "Примечание" Кластер ClickHouse состоит из независимых сегментов, и каждый сегмент состоит из реплик. Кластер таков неупругий, поэтому после добавления нового осколка данные не будут автоматически перебалансированы между осколками. Вместо этого предполагается, что нагрузка на кластер будет регулироваться неравномерно. Эта реализация дает вам больше контроля, и это нормально для относительно небольших кластеров, таких как десятки узлов. Но для кластеров с сотнями узлов, которые мы используем в производстве, этот подход становится существенным недостатком. Мы должны реализовать механизм таблиц, который охватывает весь кластер с динамически реплицируемыми областями, которые могут быть разделены и сбалансированы между кластерами автоматически.
{## Оригинальная статья ##}