Пользовательские функции на WebAssembly
ClickHouse поддерживает создание пользовательских функций (UDF), реализованных на WebAssembly. Это позволяет выполнять пользовательскую логику, написанную на таких языках, как Rust, C, C++ и другие, компилируя их в модули WebAssembly.
Обзор
Модуль WebAssembly — это скомпилированный двоичный файл, который содержит одну или несколько функций, вызываемых из ClickHouse. Модуль можно рассматривать как библиотеку или разделяемый объект, который загружается один раз и многократно переиспользуется.
Модуль WebAssembly, содержащий UDF, может быть написан на любом языке, компилируемом в WebAssembly, например Rust, C или C++.
Код, скомпилированный в WebAssembly («гостевой» код) и выполняемый ClickHouse («хост»), запускается в изолированном окружении (sandbox), имеющем доступ только к выделенному участку памяти.
Гостевой код экспортирует функции, которые ClickHouse может вызывать; сюда входят функции, реализующие вашу прикладную логику (используемые для определения UDF), а также вспомогательные функции, необходимые для управления памятью и обмена данными между ClickHouse и кодом WebAssembly.
Ваш код должен быть скомпилирован в «автономный» WebAssembly (также известный как wasm32-unknown-unknown) без каких-либо зависимостей от операционной системы или стандартной библиотеки. Также поддерживается только стандартная 32-битная цель WebAssembly (расширение wasm64 не поддерживается).
Модуль должен следовать одному из поддерживаемых протоколов взаимодействия (ABI) для интеграции с ClickHouse.
После компиляции двоичный код модуля загружается в ClickHouse путём вставки его в таблицу system.webassembly_modules.
После этого вы можете создавать UDF, которые ссылаются на функции, экспортируемые модулем, с помощью выражения CREATE FUNCTION ... LANGUAGE WASM.
Предварительные требования
Включите поддержку WebAssembly в конфигурации ClickHouse:
Доступные реализации движка:
Быстрый старт
В этом примере демонстрируется полный рабочий процесс создания WebAssembly UDF путём реализации калькулятора гипотезы Коллатца.
Мы напишем код в текстовом формате WebAssembly (WAT), который является человекочитаемым представлением WebAssembly, поэтому на этом этапе знание какого-либо языка программирования не требуется.
ClickHouse требует, чтобы модуль был в двоичном формате, поэтому мы воспользуемся транспайлером для преобразования WAT в WASM.
Для выполнения этого преобразования вы можете использовать wat2wasm из WebAssembly Binary Toolkit (WABT) или команду parse из wasm-tools.
В приведённом выше фрагменте мы передаём бинарный код WASM непосредственно в клиент ClickHouse, используя FORMAT RawBlob, чтобы вставить его в таблицу system.webassembly_modules.
Затем мы определяем UDF, который ссылается на функцию steps, экспортируемую из модуля:
Обратите внимание, что мы указываем имя функции из модуля после ::, так как оно отличается от имени UDF.
Теперь мы можем использовать функцию collatz_steps в наших запросах:
Столбец number явно приводится к типу UInt32, потому что функции WebAssembly ожидают точного соответствия типов сигнатуре, указанной в операторе CREATE FUNCTION.
В результате мы получили последовательность шагов Коллатца для чисел от 1 до 100, соответствующую последовательности A006577 из OEIS.
Управление модулями WASM через системную таблицу
Модули WebAssembly хранятся в таблице system.webassembly_modules, имеющей следующую структуру:
- Столбцы
nameString — Имя модуля. Непустое, допускаются только буквенно-цифровые символы и подчёркивания.codeString — Сырые двоичные данные WASM. Только для записи, при чтении возвращается пустая строка.hashUInt256 — SHA256 бинарного файла модуля (ноль, если модуль присутствует на диске, но ещё не загружен).
Управление модулями осуществляется с помощью стандартных SQL-операций над этой таблицей:
Добавить модуль
При необходимости укажите контрольную сумму:
Если указанный хэш не совпадает с вычисленным SHA256‑хэшем кода модуля, вставка завершается с ошибкой. Это может быть полезно при загрузке модулей из внешних источников, таких как S3 или HTTP.
Распределение модуля по кластеру
system.webassembly_modules — локальная таблица каждого экземпляра: INSERT выполняется только на реплике, обрабатывающей соединение. Формы ON CLUSTER для оператора INSERT не существует, поэтому последующий CREATE FUNCTION ... ON CLUSTER завершится ошибкой на репликах, где модуль отсутствует:
Чтобы выполнить вставку на всех узлах, записывайте в табличную функцию cluster, а не в локальную таблицу system.webassembly_modules:
Этот подход опирается на то, что нижележащий механизм распределённой записи проходит через каждую реплику в каждом сегменте, а это происходит только когда кластер настроен с internal_replication=false. При internal_replication=true (значение по умолчанию для кластеров, использующих ReplicatedMergeTree для управления репликацией), вставка доставляется только одной работоспособной реплике в каждом сегменте, а system.webassembly_modules по этому пути не реплицируется — поэтому на части реплик модуль по-прежнему будет отсутствовать. В такой конфигурации нужно выполнять вставку в каждую реплику отдельно, например перебирая system.clusters и записывая через remote(...) для каждого хоста, либо копируя бинарный файл в user_scripts/wasm/ на каждый хост.
Проверить значение internal_replication для кластера можно с помощью SELECT cluster, shard_num, internal_replication FROM system.clusters.
После такой распределённой вставки модуль присутствует на каждой реплике, и CREATE FUNCTION ... ON CLUSTER выполняется успешно:
Вы можете проверить, что модуль загружен на всех репликах, с помощью clusterAllReplicas:
Операции вставки в system.webassembly_modules идемпотентны для одной и той же пары (name, hash), поэтому повторный запуск распределённой вставки безопасен и вполне подходит для восстановления состояния после замены реплики. Обратите внимание, что вновь добавленные серверы не получают существующие модули задним числом — необходимо повторно выполнить вставку для обновлённого кластера или поместить бинарный файл в каталог user_scripts/wasm/ на новом хосте.
Список модулей
Удаление модуля
Удаление выполняется оператором DELETE FROM system.webassembly_modules WHERE name = '...'.
Предикат должен иметь вид либо name = 'literal' для точного совпадения, либо name LIKE 'pattern' для удаления всех модулей, имена которых соответствуют шаблону; другие формы не допускаются.
Если какие-либо существующие пользовательские функции (UDF) ссылаются на один из найденных модулей, удалить его не удастся, поэтому сначала необходимо удалить эти UDF.
Создайте WebAssembly UDF
Синтаксис:
Параметры:
function_name: Имя функции в ClickHouse. Может отличаться от имени экспортируемой функции в модуле.FROM 'module_name' :: 'source_function_name': Имя загруженного модуля WASM и имя функции в модуле WASM, которое следует использовать (по умолчанию —function_name).ARGUMENTS: Список имён и типов аргументов (имена необязательны и используются для форматов сериализации, которые поддерживают именованные поля).ABI: Версия Application Binary InterfaceROW_DIRECT: Прямое сопоставление типов, построчная обработкаBUFFERED_V1: Блочная обработка с сериализациейASSEMBLYSCRIPT: Построчная обработка для модулей, созданных компилятором AssemblyScript. Числовые типы сопоставляются с примитивами AssemblyScript; ClickHouseStringсопоставляется со строковым типомstringв AssemblyScript.
DETERMINISTIC: Объявляет функцию детерминированной — она всегда возвращает один и тот же результат для одних и тех же входных данных. Если указано, ClickHouse может выполнить свёртку в константу для вызовов, где все аргументы являются константами: функция вычисляется один раз на этапе анализа запроса, и результат повторно используется для каждой строки.SHA256_HASH: Ожидаемый хэш модуля для проверки (автоматически заполняется, если опущен); может использоваться для обеспечения загрузки корректного модуля WASM на разных репликах.SETTINGS: Настройки для отдельной функцииserialization_formatString — Формат сериализации для ABI, где это требуется. Поддерживаемые значения:MsgPack,JSONEachRow,CSV,TSV,TSVRaw,RowBinaryиBuffers. Значение по умолчанию:MsgPack. Блочные форматы, такие какBuffers, должны возвращать один столбец, тип которого соответствует объявленной сигнатуре функции.webassembly_udf_enable_fuelBool — Включает ограничение fuel для функции. Значение по умолчанию:true. Если указаноfalse, настройка уровня запросаwebassembly_udf_max_fuelигнорируется для этой функции. Отключение ограничений fuel может повысить производительность при использовании движкаwasmtime. Однако для недоверенного или содержащего ошибки гостевого кода это может увеличить риск бесконтрольного выполнения.
Версии ABI
Для взаимодействия с ClickHouse модули WebAssembly должны соответствовать одному из поддерживаемых ABI (Application Binary Interface).
ROW_DIRECT: Прямое отображение типов (только примитивные типыInt32,UInt32,Int64,UInt64,Float32,Float64)BUFFERED_V1: Сложные типы с сериализациейASSEMBLYSCRIPT: Построчное взаимодействие с модулями AssemblyScript; поддерживаются числовые типы иString.
ABI ROW_DIRECT
Вызывает экспортируемую функцию WASM напрямую для каждой строки.
- Аргументы и возвращаемые значения должны иметь числовые типы
Int32/UInt32/Int64/UInt64/Float32/Float64/Int128/UInt128. - Строки не поддерживаются в этом ABI.
- Сигнатуры должны соответствовать экспорту WASM (
i32/i64/f32/f64/v128). - Модуль не обязан экспортировать какие-либо вспомогательные функции.
Например, функция с сигнатурой:
Его можно создать следующим образом:
WebAssembly не различает знаковые и беззнаковые аргументы, а использует разные инструкции для интерпретации значений. Поэтому размер аргумента должен строго совпадать, тогда как его знаковость определяется операциями внутри функции.
ABI BUFFERED_V1
Этот ABI является экспериментальным и может измениться в будущих релизах.
Обрабатывает целые блоки целиком, используя (де)сериализацию через память WASM. Поддерживает любые типы аргументов и возвращаемых значений.
Сериализованные данные копируются в память WASM, передаваемую как указатель на буфер (состоящий из указателя на данные и размера этих данных) в функцию UDF вместе с числом строк во входных данных. Таким образом, функция, определяемая пользователем, на стороне WASM всегда принимает два аргумента i32 и возвращает одно значение i32. Гостевой код обрабатывает данные и возвращает указатель на буфер результата с сериализованными данными результата.
Гостевой код должен реализовать две функции для создания и уничтожения этих буферов.
Примеры определений на C:
ABI ASSEMBLYSCRIPT
Предназначен для модулей, скомпилированных компилятором AssemblyScript. Каждая строка инициирует один вызов экспортируемой функции, сопоставляя значения ClickHouse с примитивными типами и строковыми объектами AssemblyScript.
Поддерживаемые типы:
-
Числовые:
Int8/UInt8,Int16/UInt16(на границе расширяются доi32),Int32/UInt32,Int64/UInt64,Float32,Float64 -
String— сопоставляется со строковым типом AssemblyScriptstring(UTF-16 в памяти WASM). ClickHouse автоматически выполняет преобразование UTF-8 ↔ UTF-16. -
Пользовательские классы AssemblyScript не поддерживаются в качестве типов аргументов или возвращаемых значений — их идентификаторы классов во время выполнения не являются стабильными между компиляциями (см. AssemblyScript#2982).
Требования к модулю:
Модуль должен быть скомпилирован с управляемой средой выполнения AssemblyScript, чтобы экспортировались __new, __pin и __unpin. Стандартная обработка входящих и исходящих строк рассчитана именно на это. Рекомендуемая команда вызова:
AssemblyScript также импортирует env.abort для ловушек среды выполнения (нехватка памяти, проверка границ и т. д.). ClickHouse автоматически предоставляет этот импорт: при срабатывании abort активный запрос завершается с исключением WASM_ERROR, содержащим декодированное сообщение AssemblyScript и расположение в исходном коде.
Пример:
После компиляции с помощью asc и загрузки полученного файла .wasm в system.webassembly_modules объявите пользовательские функции (UDF) следующим образом:
Примечание по разработке UDF на Rust
Для программ на Rust мы предоставляем вспомогательный crate clickhouse-wasm-udf, который упрощает разработку UDF на WebAssembly для ClickHouse. Этот crate содержит функции для управления памятью, поэтому вам не нужно вручную реализовывать функции clickhouse_create_buffer и clickhouse_destroy_buffer — достаточно добавить crate как зависимость. Также доступны макросы #[clickhouse_wasm_udf], которые оборачивают ваши обычные функции Rust в требуемый формат ABI.
С этим crate вы можете писать UDF следующим образом:
Макросы сгенерируют обёрточную функцию, принимающую и возвращающую структуры буферов, и автоматически обработают (де)сериализацию с использованием serde.
Host API, доступный модулям
Следующие функции хоста могут быть импортированы и использованы в модулях:
clickhouse_server_version() -> i64— возвращает версию сервера ClickHouse в виде целого числа (например, 25011001 для v25.11.1.1).clickhouse_throw(ptr: i32, size: i32)— вызывает ошибку с переданным сообщением. Принимает указатель на область памяти, содержащую строку сообщения об ошибке, и размер строки.clickhouse_log(ptr: i32, size: i32)— записывает сообщение в текстовый лог сервера ClickHouse.clickhouse_random(ptr: i32, size: i32)— заполняет память случайными байтами.env.abort(message: i32, fileName: i32, line: i32, column: i32)— предоставляется для модулей, совместимых с AssemblyScript. Его вызов (или срабатывание ловушки среды выполнения AssemblyScript, которая его вызывает) завершает UDF исключениемWASM_ERROR, содержащим декодированное сообщение и расположение в исходном коде. На модули, которые не импортируютenv.abort, это не влияет.
Настройки
Следующие настройки на уровне запроса управляют выполнением WebAssembly UDF:
-
webassembly_udf_max_fuel— лимит топлива на выполнение экземпляра WebAssembly UDF. Каждая инструкция WebAssembly потребляет некоторое количество топлива. Перед передачей в среду выполнения значение умножается на 1024, поэтомуwebassembly_udf_max_fuel = 1соответствует примерно 1024 единицам топлива. Установите значение 0, чтобы снять ограничение. Применяется только к функциям, для которых настройка на уровне функцииwebassembly_udf_enable_fuelимеет значение true, что используется по умолчанию. -
webassembly_udf_max_memory— лимит памяти в байтах на экземпляр WebAssembly UDF. -
webassembly_udf_max_input_block_size— максимальное количество строк, передаваемых в WebAssembly UDF в одном блоке. Установите значение 0, чтобы обрабатывать все строки сразу. -
webassembly_udf_max_instances— максимальное количество экземпляров WebAssembly UDF, которые могут выполняться одновременно для одной функции.
Пример использования: