Обзор
Модуль WebAssembly — это скомпилированный бинарный файл, содержащий одну или несколько функций, которые можно вызывать из ClickHouse. Модуль можно рассматривать как библиотеку или разделяемый объект, который загружается один раз и затем многократно используется. Модуль WebAssembly с пользовательскими функциями (UDF) можно написать на любом языке, который компилируется в WebAssembly, например на Rust, C или C++. Код, скомпилированный в WebAssembly («гостевой» код) и выполняемый ClickHouse («хостом»), работает в изолированной песочнице и имеет доступ только к выделенной области памяти. Гостевой код экспортирует функции, которые может вызывать ClickHouse. К ним относятся как функции, реализующие вашу пользовательскую логику (используемую для определения UDF), так и вспомогательные функции, необходимые для управления памятью и обмена данными между ClickHouse и кодом WebAssembly. Ваш код должен быть скомпилирован в «автономный» WebAssembly (то естьwasm32-unknown-unknown) без каких-либо зависимостей от операционной системы или стандартной библиотеки. Кроме того, поддерживается только стандартная 32-битная целевая платформа WebAssembly (без расширения wasm64).
Модуль должен соответствовать одному из поддерживаемых протоколов взаимодействия (ABI) для работы с ClickHouse.
После компиляции бинарный код модуля загружается в ClickHouse путём вставки в таблицу system.webassembly_modules.
После этого вы можете создавать UDF, ссылающиеся на функции, экспортируемые модулем, с помощью оператора CREATE FUNCTION ... LANGUAGE WASM.
Предварительные требования
Включите поддержку WebAssembly в конфигурации ClickHouse:wasmtime(по умолчанию и на данный момент единственная) — использует Wasmtime
Быстрый старт
Этот пример демонстрирует полный процесс создания WebAssembly UDF на примере реализации вычисления гипотезы Коллатца. Мы напишем код в текстовом формате WebAssembly (WAT) — удобочитаемом представлении WebAssembly, поэтому на этом этапе не потребуется какой-либо язык программирования. ClickHouse требует, чтобы модуль был в бинарном формате, поэтому мы воспользуемся транспилятором, чтобы преобразовать WAT в WASM. Для этого преобразования можно использоватьwat2wasm из WebAssembly Binary Toolkit (WABT) или команду parse из wasm-tools.
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 бинарного файла модуля (ноль, если он есть на диске, но ещё не загружен).
Добавление модуля
Распределение модуля по cluster
system.webassembly_modules — это таблица уровня instance: INSERT выполняется только на той реплике, которая обрабатывает connection. Для оператора INSERT нет формы ON CLUSTER, поэтому последующий CREATE FUNCTION ... ON CLUSTER завершится ошибкой на репликах, где этого модуля нет:
insert на всех узлах, записывайте в табличную функцию 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' для удаления всех модулей, имя которых соответствует шаблону; другие формы не допускаются.
Создание 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сопоставляется с типом AssemblyScriptstring.
DETERMINISTIC: Объявляет функцию детерминированной — она всегда возвращает один и тот же результат для одних и тех же входных данных. Если указан этот параметр, ClickHouse может выполнить константную свёртку вызовов, в которых все аргументы являются константами: функция вычисляется один раз на этапе анализа запроса, а результат затем повторно используется для каждой строки.SHA256_HASH: Ожидаемый хеш модуля для проверки (заполняется автоматически, если опущен); может использоваться, чтобы гарантировать загрузку правильного WASM-модуля на разных репликах.SETTINGS: Настройки уровня функцииserialization_formatString — Формат, используемый для сериализации блоков аргументов, передаваемых модулю, и разбора возвращённого результата. Используется только сABI BUFFERED_V1. Поддерживаемые значения:MsgPack,JSONEachRow,CSV,TSV,TSVRaw,RowBinaryиBuffers. По умолчанию:MsgPack. Форматы с блочной обработкой, такие какBuffers, должны возвращать один столбец, тип которого соответствует объявленной сигнатуре функции.webassembly_udf_enable_fuelBool — Включает ограничение по fuel для функции. По умолчанию:true. Если указаноfalse, настройка уровня запросаwebassembly_udf_max_fuelдля этой функции игнорируется. Отключение ограничений fuel может повысить производительность. Однако для недоверенного или содержащего ошибки гостевого кода это может увеличить риск неконтролируемого выполнения.
Версии 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). - Модуль не обязан экспортировать какие-либо вспомогательные функции.
ABI BUFFERED_V1
Этот ABI экспериментальный и может измениться в будущих релизах.
u32 в порядке little-endian). Буферы передаются по дескриптору — указателю на эту структуру, а не на сами данные. Гостевой код должен экспортировать две функции для создания и уничтожения этих буферов. Если размер не равен нулю, указатель на данные также должен быть отличен от нуля: буфер, который объявляет положительный размер, но указывает на смещение 0 в линейной памяти, не является корректным буфером, и гостевой код не должен создавать такой буфер; ClickHouse отклоняет такой буфер и завершает запрос с ошибкой WASM_ERROR.
Для каждого входного блока ClickHouse:
- Сериализует столбцы аргументов, используя
serialization_formatфункции (MsgPackпо умолчанию). Построчные форматы записывают значения строка за строкой, располагая значения аргументов в порядке их объявления вARGUMENTS; форматы с именованными полями используют имена аргументов, поэтому при использовании таких форматов объявляйте их вARGUMENTS. - Вызывает экспортируемую модулем функцию
clickhouse_create_bufferи копирует сериализованные данные в память, на которую указывает возвращённый буфер. - Вызывает пользовательскую функцию с двумя аргументами
i32: дескриптором входного буфера (0, если у функции нет аргументов) и числом строк. Функция возвращает одинi32— дескриптор буфера результата, который гостевой код выделяет самостоятельно; возврат0приводит к завершению запроса с ошибкой. - Считывает буфер результата: он должен содержать ровно один столбец с точно таким же числом строк, сериализованный в том же формате. Для форматов с именованными полями, таких как
JSONEachRow, столбец результата должен называтьсяresult. - Вызывает
clickhouse_destroy_bufferдля дескрипторов входного буфера (если он есть) и буфера результата. Гостевой код не должен самостоятельно освобождать буфер результата и не должен возвращать дескриптор входного буфера в качестве результата — иначе он будет уничтожен дважды.
webassembly_udf_input_split_memory_ratio разбивает его динамически: ClickHouse измеряет сериализованные строки и начинает новый вызов до того, как входные данные превысят эту долю линейной памяти гостя, поэтому количество строк зависит от данных. Ненулевое значение webassembly_udf_max_input_block_size заменяет это фиксированным ограничением в указанное число строк на вызов, а значение отношения 0 передаёт весь блок за один вызов. Вызов никогда не уменьшается до менее чем одной строки, поэтому строка, которая сама по себе слишком велика для памяти модуля, всё равно приведёт к ошибке внутри гостя.
Точное содержимое буферов определяется выбранным форматом, и гостевой код должен самостоятельно декодировать и кодировать данные. В частности, для значений
String:MsgPack: ClickHouse записывает значенияString, используя семейство типовbin(0xc4/0xc5/0xc6), а не семействоstr. При разборе результата принимаются оба семейства.RowBinary: каждомуStringпредшествует его длина в байтах в виде беззнакового varint (LEB128), а не целого числа фиксированной ширины.JSONEachRow: каждая строка результата должна быть объектом с ключомresult, например{"result":"value"}.
str_reverse разворачивает байты каждой входной строки, используя serialization_format = 'RowBinary'. Экземпляры модуля переиспользуются между блоками, поэтому clickhouse_destroy_buffer должна действительно освобождать память — здесь аллокатор сбрасывается после того, как уничтожены все буферы.
wasm-ld (входит в состав LLVM/lld):
__attribute__((export_name("..."))), либо выполните компоновку с wasm-ld --export-all. -fno-builtin не позволяет clang заменять обычные циклы обработки байтов вызовами memcpy/memset, которые недоступны без стандартной библиотеки.
Загрузите модуль и создайте функцию:
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).
__new, __pin и __unpin. Стандартный механизм обработки входящих и выходных строк рассчитан на их наличие. Рекомендуемая команда:
env.abort для ловушек среды выполнения (нехватка памяти, проверка границ и т. д.). ClickHouse автоматически предоставляет этот импорт: при срабатывании abort активный запрос завершается с исключением WASM_ERROR, которое включает декодированное сообщение AssemblyScript и местоположение в исходном коде.
Пример:
asc и загрузки полученного файла .wasm в system.webassembly_modules объявите пользовательские функции (UDF) следующим образом:
Примечание о разработке пользовательских функций (UDF) на Rust
Для программ на Rust мы предоставляем вспомогательный крейт clickhouse-wasm-udf, который упрощает разработку пользовательских функций (UDF) WebAssembly для ClickHouse. Крейт предоставляет функции для управления памятью, поэтому вам не нужно вручную реализовыватьclickhouse_create_buffer и clickhouse_destroy_buffer — достаточно добавить этот крейт в зависимости. Также в нём есть макросы #[clickhouse_wasm_udf], которые оборачивают обычные функции Rust в требуемый ABI-формат.
С этим крейтом вы можете писать пользовательские функции (UDF) так:
serde.
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— Лимит fuel для каждого запуска экземпляра WebAssembly UDF. Каждая инструкция WebAssembly расходует некоторое количество fuel. Значение масштабируется в 1024 раза перед передачей в среду выполнения, поэтомуwebassembly_udf_max_fuel = 1соответствует примерно 1024 единицам fuel. Установите 0, чтобы убрать конечное ограничение. Применяется только к функциям, для которых настройка уровня функцииwebassembly_udf_enable_fuelимеет значение true, что используется по умолчанию. -
webassembly_udf_max_memory— Лимит памяти в байтах для каждого экземпляра WebAssembly UDF. -
webassembly_udf_max_input_block_size— Максимальное количество строк, передаваемых в WebAssembly UDF в одном блоке, для любого ABI. Установите 0, чтобы размер вызовов функции сABI BUFFERED_V1определялся настройкойwebassembly_udf_input_split_memory_ratio; остальные ABI в этом случае получают весь блок конвейера. -
webassembly_udf_input_split_memory_ratio— Доля линейной памяти, с которой module объявляет свой старт (от его предельного объёма памяти, если module стартует без страниц), которую могут занимать входные данные одного вызова. Используется для разбиения блоков функции сABI BUFFERED_V1, когдаwebassembly_udf_max_input_block_sizeравно 0; остальные ABI не передают сериализованный входной блок и игнорируют её. По умолчанию:0.5— остальная память остаётся под собственный working set гостя; значение, близкое к 1, всё равно может привести к сбою внутри allocator гостя. Установите 0 или задайте вcompatibilityверсию до 26.9, чтобы передавать весь блок конвейера одним вызовом. Должно быть не меньше 0 и не больше 1. -
webassembly_udf_max_instances— Максимальное количество экземпляров WebAssembly UDF, которые могут выполняться параллельно для одной функции.