- С помощью SQL
- С помощью консоли и собственного кода
- С помощью Cloud API (бета)
- С помощью Terraform (бета)
Пользовательские функции SQL
Пользовательские функции SQL можно создавать с помощью оператораCREATE FUNCTION на основе лямбда-выражения.
В этом примере мы создадим простую исполняемую пользовательскую функцию isBusinessHours.
Функция будет проверять, попадает ли указанная временная метка в стандартные рабочие часы, и возвращать true, если да, и false — если нет.
- Войдите в Cloud Console и откройте консоль SQL
- Напишите следующий SQL-запрос, чтобы создать функцию
isBusinessHours:
- Чтобы протестировать только что созданную UDF, выполните следующую команду:
- Вы можете использовать команду
DROP FUNCTION, чтобы удалить только что созданную UDF:
- Настройки уровня сеанса (заданные через оператор
SET) не передаются в контекст выполнения UDF - Настройки профиля пользователя не наследуются UDF
- Настройки уровня запроса не применяются при выполнении UDF
Исполняемые пользовательские функции
Исполняемые UDF запускают ваш код в изолированном процессе (песочнице) рядом с каждой репликой вашего сервиса. Можно загрузить скрипт на Python, который выполняется в средеpython3.11, или заранее скомпилированный бинарный файл, который выполняется в нативной среде выполнения.
В этом примере мы создадим ту же простую исполняемую пользовательскую функцию isBusinessHours, которая проверяет, попадает ли заданная временная метка в обычные рабочие часы.
Ранее мы создавали её с помощью SQL, а на этот раз создадим её с помощью Python и настроим через консоль.
1
Создайте файл Python
Создайте локально новый файл Среда выполнения Python —
main.py:python3.11. Если ваш Python-скрипт импортирует сторонние пакеты, перечислите их в файле requirements.txt, и ClickHouse Cloud установит их автоматически для обеих архитектур CPU, на которых работает ClickHouse Cloud (x86-64 и ARM64). Зависимости можно также упаковать непосредственно в ZIP-архив, но в этом случае придётся включить в него кэшированные пакеты для обеих архитектур CPU, поэтому вариант с requirements.txt проще. Например:ClickHouse Cloud ищет файл
main.py в zip-архиве, который вы загрузите через консоль на следующем шаге.
Если назвать файл иначе, возникнет ошибка.2
Упакуйте зависимости и локальные файлы
В этом руководстве нет зависимостей и дополнительных файлов, поэтому в архив достаточно поместить только Чтобы добавить пакеты зависимостей и другие локальные файлы (например, Чтобы сослаться на базовый каталог локального пакета в коде на Python, используйте Это полезно, когда необходимо:
main.py:requirements.txt, wheel-пакеты, файлы конфигурации или файлы данных), поместите их в тот же каталог, где находится main.py, а затем выполните zip из этого каталога, чтобы main.py оказался в корне архива:os.path.dirname(os.path.abspath(__file__)). Это выражение возвращает абсолютный путь к каталогу внутри ZIP-архива, в котором находится ваш main.py, что позволяет обращаться к другим файлам пакета:- Обращаться к файлам конфигурации, поставляемым вместе с вашей UDF
- Загружать wheel-пакеты для пользовательских зависимостей
- Использовать дополнительные скрипты или файлы данных
3
Создайте UDF в консоли
- На главной странице Cloud console нажмите на название своей организации в меню в левом нижнем углу.
- Выберите в меню пункт User-defined functions.
- На странице пользовательских функций нажмите Set up a UDF. В правой части экрана откроется панель конфигурации.
- Введите имя функции. В этом примере используйте
isBusinessHours. - Выберите тип функции — Executable pool или Executable:
- Executable pool: пул долгоживущих процессов запускается один раз и повторно используется для разных блоков и запросов. Рекомендуется практически для любой рабочей нагрузки.
- Executable: для каждого обрабатываемого блока данных запускается новый процесс. Используйте этот вариант, только если процесс не должен сохранять состояние между вызовами.
- Выберите Runtime type. В этом примере оставьте
python3.11. Чтобы загрузить предварительно скомпилированный бинарный файл, выберите Native; подробнее см. в разделе Среда выполнения Native. - В этом примере оставьте настройки по умолчанию. Полный список параметров конфигурации и их значений по умолчанию в Cloud см. в разделе Справочник по конфигурации.
- Нажмите Browse File и загрузите файл
.zip, созданный в начале этого руководства. - Добавьте новый аргумент. В этом примере добавьте аргумент
timestampс типомDateTime. - Выберите тип возвращаемого значения. В этом примере выберите
Bool. - Нажмите Create UDF. В диалоговом окне отобразится текущий статус сборки.
- Если возникнут проблемы, статус изменится на error.
- В противном случае статус сменится с building на provisioning. Чтобы подготовка завершилась, сервис должен быть активен. Если сервис находится в режиме простоя, нажмите Wake Up Service на панели UDF details рядом с названием сервиса.
- По завершении статус изменится на deployed.
4
Протестируйте свою UDF
- вернитесь на главную страницу SQL Console, нажав Settings - return to your service view в левом верхнем углу страницы
- выберите SQL Console в меню слева
- введите следующий запрос:
5
Создать новую версию
Чтобы изменить код или настройки UDF, создайте новую версию: существующие версии изменить нельзя. Панель Edit позволяет управлять только тем, каким сервисам назначена UDF; загрузка файла через эту панель не заменит развернутый код.
- На главной странице Cloud console нажмите на название вашей организации в меню в левом нижнем углу.
- Выберите в меню User-defined functions.
- Нажмите на три точки в столбце Actions для UDF
isBusinessHoursи выберите Create new version - Загрузите zip-архив с измененным кодом или измените настройки, выберите сервисы, которые должны получить новую версию, и нажмите Create new version
executable_pool долгоживущие процессы пула продолжают выполнять предыдущую версию, пока пул не будет перезагружен. Воспользуйтесь действием Reload UDF рядом с сервисом на панели UDF. Оно выполняет на этом сервисе команду SYSTEM RELOAD FUNCTION ON CLUSTER default isBusinessHours.Вы успешно добавили свою первую пользовательскую функцию через консоль, убедились, что она работает, и узнали, как при необходимости создать ее новую версию.Нативная среда выполнения (скомпилированные бинарные файлы)
Нативная среда выполнения позволяет загрузить предварительно скомпилированный бинарный файл вместо скрипта на Python. При создании UDF выберите для параметра тип среды выполнения значение Native (runtime: native в API). В песочнице ничего не устанавливается автоматически: всё, что нужно бинарному файлу, должно быть встроено в него при компиляции.
На данный момент поддерживаются Rust, Go, C++, а также JavaScript, скомпилированный в бинарный файл с помощью Bun. Работа с Bun проверена на версиях 1.4.0 и 1.4.2; с другими версиями возможны проблемы.
Архив должен содержать по одному исполняемому файлу для каждой архитектуры CPU, каждый в отдельной папке:
main и запускается без аргументов; режим или подкоманду передавайте как обычный аргумент функции.
Настоятельно рекомендуется использовать статически скомпонованные бинарные файлы: они не зависят от системных библиотек хоста, системные вызовы которых могут различаться в разных версиях сервера, а политика системных вызовов песочницы настроена именно под них. Динамически скомпонованные бинарные файлы работают, только если они компонуются исключительно с glibc и собраны с glibc версии 2.35 или более ранней. JavaScript, скомпилированный с помощью Bun, всегда компонуется динамически.
Нативным UDF нельзя предоставить доступ к сети: исходящие сетевые подключения доступны только в среде выполнения Python. См. раздел Сетевой доступ.
Соберите бинарный файл для обеих архитектур. В Rust один раз добавьте обе целевые платформы, а затем выполните сборку для каждой из них:
amd64/main и arm64/main.
Протокол обмена данными такой же, как у среды выполнения Python: процесс считывает аргументы из stdin и записывает результаты в stdout в заданном в конфигурации формате. См. раздел Форматы и заголовки фрагментов. В репозитории llm-token-udf есть примеры нативных UDF на Rust и Go со скриптами сборки.
Справочник по конфигурации
Перечисленные ниже настройки доступны в консоли и через API. Указаны значения по умолчанию для ClickHouse Cloud; в некоторых случаях они отличаются от значений по умолчанию в версии с открытым исходным кодом.
Следующими настройками версии с открытым исходным кодом управляет ClickHouse Cloud, и изменить их для отдельной UDF нельзя:
command_termination_timeout, lifetime, stderr_reaction, check_exit_code и execute_direct. По умолчанию pool_size в версии с открытым исходным кодом равен 16, а в Cloud — 3. Полный список настроек версии с открытым исходным кодом приведён в разделе Исполняемые пользовательские функции.
Имена функций должны соответствовать шаблону ^[A-Za-z][A-Za-z0-9_]*$, быть уникальными в пределах организации и не совпадать с именами встроенных функций ClickHouse; зарезервированные имена API отклоняет. У каждого аргумента должно быть имя, соответствующее тому же шаблону; API и Terraform требуют его при любом формате. Архивы с символическими ссылками отклоняются.
Форматы и заголовки фрагментов
TabSeparated — формат по умолчанию и самый простой. При его использовании символы табуляции, переводы строк и обратные слеши необходимо экранировать с обеих сторон. Формат RowBinary безопасен для двоичных данных и избавляет ClickHouse от форматирования и разбора текста; для длинных строк лучше использовать именно его.
При включённом параметре Send chunk header ClickHouse перед каждым фрагментом записывает количество строк в виде десятичного числа и символ \n, а затем сами строки в заданном формате. Процесс должен прочитать ровно столько строк, записать столько же результатов и один раз сбросить вывод. Без этого параметра процесс, который читает строчно-ориентированный формат, например TabSeparated, RowBinary или JSONEachRow, и буферизует вывод, не может определить, где заканчивается блок, и запрос зависает до истечения тайм-аута чтения. Включайте этот параметр для любой такой UDF, которая не сбрасывает вывод после каждой строки. Формат Native уже содержит границы блоков, поэтому UDF с форматом Native может прочитать один блок, записать один блок и сбросить вывод без заголовка.
Сетевой доступ
Сетевой доступ поддерживается только для среды выполнения Python; для нативных UDF включить его нельзя. Нативные UDF Python, созданные в консоли, собираются с включённым сетевым доступом, и отдельной настройки для него в консоли нет. В API параметрsandboxType позволяет выбрать между netenable, который разрешает исходящие подключения, и basic, который их запрещает; в Terraform-провайдере тот же выбор доступен через параметр sandbox_type.
Сетевой доступ разрешает исходящие подключения к конечным точкам в публичном интернете. Подключения к частным сетям, link-local-адресам и внутренним адресам блокируются. У песочницы нет облачной идентичности: нет роли инстанса, нет конечной точки метаданных и нет наследуемых переменных окружения, поэтому UDF располагает только теми учётными данными, которые вы ей передадите. Сетевой доступ применяется при сборке версии UDF, поэтому, если существующая версия была создана без него, нужно создать новую версию.
Хранилища секретов для UDF пока нет. Включите файл конфигурации в zip-архив и считывайте его по пути относительно __file__; никогда не указывайте ключи в SQL.
Лимит памяти
Лимит памяти задаёт максимальный объём памяти (в МиБ), доступный каждому процессу песочницы. Если параметр не задан, действует значение песочницы по умолчанию — 4 ГиБ. Ограничение распространяется на виртуальное адресное пространство, а не на резидентную память, поэтому средам выполнения, которые резервируют большие диапазоны адресов, нужен запас, намного превышающий их фактическое потребление. Так, Go при запуске резервирует заметно больше 1 ГиБ адресного пространства. Если процесс превышает ограничение, выделение памяти завершается неудачей внутри самого процесса: UDF Python завершается с трассировкойMemoryError, а запрос — ошибкой, в сообщении которой приводится содержимое stderr.
Детерминированные функции и кэш запросов
По умолчанию UDF считается недетерминированной. Запрос, вызывающий такую функцию приuse_query_cache = 1, завершается ошибкой QUERY_CACHE_USED_WITH_NONDETERMINISTIC_FUNCTIONS, если не изменено значение настройки query_cache_nondeterministic_function_handling.
Включайте детерминированность только в том случае, если функция при одних и тех же аргументах всегда возвращает один и тот же результат. Тогда кэш запросов сможет сохранять и отдавать результаты запросов, которые её вызывают. Другие недетерминированные функции в том же запросе, например now, today или rand, по-прежнему препятствуют кэшированию при обработке по умолчанию (throw). При query_cache_nondeterministic_function_handling = 'save' такие запросы всё равно кэшируются, а при 'ignore' выполняются без кэширования.
Перезапуск сервиса
Мониторинг
Начиная с версии 26.6, ClickHouse фиксирует использование ресурсов исполняемыми UDF в восьми счётчиках, имена которых начинаются с префиксаExecutableUserDefinedFunction. На уровне отдельного запроса они хранятся как записи в столбце типа Map ProfileEvents таблицы system.query_log. На уровне всего сервера они доступны как строки таблицы system.events (накопительные значения с момента запуска сервера), а также как столбцы ProfileEvent_<name> таблицы system.metric_log (значения, периодически собираемые во времени). Суффиксы счётчиков:
Таблица
system.asynchronous_metrics содержит метрики ExecutableUserDefinedFunctionProcesses и ExecutableUserDefinedFunctionMemoryResidentBytes. Вторая из них — это сумма VmRSS по всем работающим процессам UDF. Она даёт лишь верхнюю оценку, поскольку общие страницы памяти учитываются в каждом процессе отдельно.
system.query_log.memory_usage учитывает только процесс сервера ClickHouse и не включает память процессов UDF.Рекомендации по производительности
Используйтеexecutable_pool. В режиме executable для каждого блока данных запускается новый процесс, поэтому накладные расходы на запуск приходятся на каждый блок.
У каждого вызова есть фиксированная стоимость. Если значение ExecutableUserDefinedFunctionInvocations очень велико по сравнению с количеством обработанных строк, увеличьте preferred_block_size_bytes для этого запроса, чтобы каждый блок содержал больше строк.
Один запрос одновременно использует не более max_threads процессов пула, поэтому пул размером больше max_threads не ускорит этот запрос. Начните с небольшого пула и увеличивайте его размер, если значение ExecutableUserDefinedFunctionPoolWaitMicroseconds становится заметным.
Каждая строка пересекает границу процесса. Пользовательские функции (UDF) предназначены для логики, которую невозможно выразить на SQL, а не для тривиальной построчной обработки, с которой справится встроенная функция или SQL UDF.
Устранение неполадок
Если UDF завершается сбоем, запрос завершается с ошибкой с кодом754 (UDF_EXECUTION_FAILED). Содержимое stderr дочернего процесса включается в сообщение об ошибке, поэтому выводите диагностическую информацию туда и завершайте процесс с ненулевым кодом возврата:
- Запрос зависает, а затем завершается с ошибкой по тайм-ауту: процесс буферизовал вывод в строково-ориентированном формате без Send chunk header. Включите этот параметр и сбрасывайте вывод после каждого фрагмента, а при использовании формата
Native— после каждого блока. - Код возврата 1 и трассировка стека
MemoryError(Python): превышен лимит памяти. Увеличьте значение лимита памяти или сократите потребление памяти каждым процессом. - Периодические сбои при типе
executableпод высокой нагрузкой: переключитесь наexecutable_pool. - Статус сборки — error: в корне zip-архива отсутствует
main.py(Python) илиamd64/mainиarm64/main(Native), либо архив содержит символические ссылки. - Не удаётся создать функцию: её имя совпадает с именем встроенной функции.
Управление пользовательскими функциями (UDF) через Cloud API
Операции жизненного цикла, доступные в консоли, также можно выполнять программно через ClickHouse Cloud API. Конечные точки UDF позволяют автоматизировать загрузку исходных архивов, создание функций и версий, их подключение к сервисам и отключение от них, а также удаление версий и функций. В API нет операции перезагрузки: после подключения новой версии UDF типаexecutable_pool к сервису выполните в этом сервисе SQL-команду SYSTEM RELOAD FUNCTION ON CLUSTER default <name>, чтобы процессы пула начали использовать новую версию.
Эти конечные точки находятся на стадии бета-тестирования, и контракт API может измениться.
- Создайте URL для загрузки, чтобы получить предварительно подписанный URL для загрузки
application/zip, а затем загрузите по нему ZIP-архив. Каждый ID загрузки можно использовать только для одной попытки создания UDF или её версии; при повторной попытке запросите новый URL для загрузки. - Создайте UDF из загруженного архива, указав имя функции, среду выполнения, аргументы и возвращаемый тип.
- Подключите UDF к сервису. Если версия не указана, подключается последняя готовая версия. Сервис должен быть запущен; бездействующие сервисы можно предварительно активировать.
Полный список конечных точек:
См. справочник API UDF со схемами запросов и ответов.
Управление пользовательскими функциями (UDF) с помощью Terraform
Официальный Terraform-провайдер ClickHouse включает два ресурса для управления пользовательскими функциями (UDF) в рамках подхода «инфраструктура как код»:clickhouse_udfуправляет самой функцией. Он принимает ZIP-архив с исходным кодом функции и публикует новую версию при изменении хеша архива, дожидаясь завершения сборки.clickhouse_udf_attachmentподключает версию UDF к сервису. К сервису можно одновременно подключить не более одной версии функции. Можно закрепить конкретный номер версии или сослаться наclickhouse_udf.<name>.version, чтобы автоматически обновлять сервисы до последней версии.
Эти ресурсы доступны начиная с версии провайдера 3.24.0. Они имеют статус бета, и их поведение может измениться в будущих версиях провайдера.
isBusinessHours из предыдущего примера:
clickhouse_udf принимает format, pool_size, max_command_execution_time, command_read_timeout, command_write_timeout, send_chunk_header, sandbox_type, sandbox_version, return_name и fail_on_build_error; полное описание схемы см. в документации ресурса.
Чтобы протестировать новую версию на сервисе для разработки до того, как она попадёт в продакшн, настройте одно подключение на отслеживание последней версии, а для другого закрепите версию с помощью переменной. Подключение на сервисе для разработки автоматически переходит на каждую новую сборку, а подключение в продакшн обновляется только при изменении var.prod_udf_version:
Провайдер пока не поддерживает параметр
deterministic и ограничение памяти. Оба параметра задаются при создании версии, поэтому у версий, опубликованных провайдером, их быть не может. Чтобы использовать эти параметры, создавайте такие версии через консоль или API.executable_pool после смены версии в подключении выполните на сервисе SYSTEM RELOAD FUNCTION ON CLUSTER default <name>. При удалении ресурса clickhouse_udf удаляются все версии функции, и она отключается от всех сервисов.