- 使用 SQL
- 使用控制台和你自己的代码
- 使用 Cloud API (Beta)
- 使用 Terraform (Beta)
SQL 用户自定义函数
可以使用CREATE FUNCTION 语句,基于 lambda 表达式创建 SQL UDF。
在此示例中,我们将创建一个简单的可执行用户自定义函数 isBusinessHours。
该函数会检查某个时间戳是否处于正常营业时间内;如果是则返回 true,否则返回 false。
- 登录 Cloud Console 并打开 SQL 控制台
- 编写以下 SQL 查询以创建
isBusinessHours函数:
- 运行以下内容,测试你刚创建的 UDF:
- 您可以使用
DROP FUNCTION命令删除刚刚创建的 UDF:
- 会话级设置 (通过
SET语句设置) 不会传递到 UDF 的执行上下文中 - 用户 profile 中的设置不会被 UDFs 继承
- 查询级设置在 UDF 执行期间不生效
可执行用户自定义函数
可执行 UDF 会在沙箱进程中运行您的代码,该进程与服务的每个副本一同运行。您可以上传 Python 脚本 (在python3.11 运行时上运行) ,也可以上传预编译的二进制文件 (在 Native 运行时上运行) 。
在本示例中,我们将创建与前文相同的简单可执行用户自定义函数 isBusinessHours,用于检查某个时间戳是否落在正常工作时间内。
此前我们是通过 SQL 创建它的,这次则改用 Python 创建,并通过控制台进行配置。
1
创建 Python 文件
在本地新建文件 Python 运行时为
main.py:python3.11。如果您的 Python 脚本导入了第三方包,请将它们列在 requirements.txt 文件中,ClickHouse Cloud 会为其运行所在的两种 CPU 架构 (x86-64 和 ARM64) 自动安装这些包。您也可以直接将依赖项打包到 ZIP 文件中,但这样就必须同时包含适用于这两种 CPU 架构的缓存包,因此使用 requirements.txt 更为简便。例如:ClickHouse Cloud 会在您下一步通过控制台上传的 zip 文件中查找
main.py。
如果该文件使用其他名称,将会报错。2
打包依赖项和本地文件
本教程不涉及任何依赖项或额外文件,因此归档中只需包含 如需包含依赖包及其他本地文件 (例如 你可以在 Python 代码中通过 此功能适用于以下场景:
main.py:requirements.txt、wheel 文件、配置文件或数据文件) ,请将它们放在 main.py 所在的目录中,然后在该目录下运行 zip,确保 main.py 位于归档文件的根目录:os.path.dirname(os.path.abspath(__file__)) 引用本地打包内容的基础目录路径。该表达式会返回 main.py 在 ZIP 归档中所在目录的绝对路径,以便你访问其他一同打包的文件:- 访问与 UDF 一同打包的配置文件
- 加载 wheel 包以引入自定义依赖项
- 引用其他脚本或数据文件
3
通过控制台创建 UDF
- 在 Cloud 控制台主页上,点击左下角菜单中您的组织名称。
- 从菜单中选择 User-defined functions。
- 在用户自定义函数页面上,点击 Set up a UDF,屏幕右侧会打开一个配置面板。
- 输入函数名称。本示例使用
isBusinessHours。 - 选择函数类型:Executable pool 或 Executable:
- Executable pool:只启动一次由长期运行进程组成的进程池,并在多个块和查询之间复用。几乎所有工作负载都推荐使用此类型。
- Executable:每处理一个数据块都会启动一个新进程。仅当进程不得在多次调用之间保留状态时才使用此类型。
- 选择 Runtime type。本示例保持
python3.11不变。如需改为上传预编译的二进制文件,请选择 Native;参见 Native 运行时。 - 本示例使用默认设置。有关全部配置参数及其在 Cloud 中的默认值,请参见配置参考文档。
- 点击 Browse File,上传在本教程开头创建的
.zip文件。 - 添加一个新参数。本示例添加一个类型为
DateTime的参数timestamp。 - 选择返回类型。本示例选择
Bool。 - 点击 Create UDF,对话框中会显示当前的构建状态。
- 如果出现任何问题,状态会变为 error。
- 否则,状态会从 building 依次变为 provisioning。服务必须处于唤醒状态才能完成预配。如果服务处于空闲状态,请在 UDF details 面板中点击服务名称旁的 Wake Up Service。
- 完成后,状态会变为 deployed。
4
测试您的 UDF
- 点击页面左上角的 Settings - return to your service view,返回 SQL 控制台主页
- 点击左侧菜单中的 SQL Console
- 编写以下查询:
5
创建新版本
如需修改 UDF 的代码或设置,请创建新版本。版本一经创建便不可更改。Edit 面板仅用于管理 UDF 分配到哪些服务;在该面板中上传文件不会替换已部署的代码。
- 在 Cloud 控制台首页,点击左下角菜单中您的组织名称。
- 从菜单中选择 User-defined functions。
- 在
isBusinessHoursUDF 对应的 Actions 列下点击三个点图标,然后点击 Create new version - 上传包含修改后代码的 zip 文件,或修改设置,选择要接收新版本的服务,然后点击 Create new version
executable_pool 类型的 UDF,长期运行的池进程会继续运行上一版本,直到该池被重新加载。请使用 UDF 面板中服务旁的 Reload UDF 操作,它会在该服务上执行 SYSTEM RELOAD FUNCTION ON CLUSTER default isBusinessHours。至此,您已通过控制台成功添加了第一个用户自定义函数,确认其可正常运行,并了解了如何在需要时为其创建新版本。Native 运行时 (已编译的二进制文件)
借助 Native 运行时,您可以上传预编译的二进制文件,而不是 Python 脚本。创建 UDF 时,将 运行时类型 设置为 Native (在 API 中为runtime: native) 。沙箱不会为您预装任何依赖;二进制文件所需的一切都必须在编译时打包进去。
目前已支持:Rust、Go、C++,以及使用 Bun 编译为二进制文件的 JavaScript。Bun 已在 1.4.0 和 1.4.2 版本上通过测试,其他版本可能无法正常运行。
归档文件中必须为每种 CPU 架构各提供一个可执行文件,并分别放在各自的文件夹中:
main,且运行时不带任何参数;如需指定模式或子命令,请将其作为普通函数参数传入。
强烈建议使用静态链接的二进制文件:它们不依赖主机的系统库 (这些系统库的系统调用可能因服务器版本而异) ,而且沙箱的系统调用策略也是专门针对静态链接二进制文件调优的。动态链接的二进制文件只有在仅链接 glibc 且基于 glibc 2.35 或更早版本构建时才能正常运行。使用 Bun 编译的 JavaScript 始终是动态链接的。
Native UDF 无法启用网络访问;出站网络访问仅适用于 Python 运行时。参见网络访问。
为两种架构分别构建二进制文件。在 Rust 中,先一次性添加两个目标平台,然后分别构建:
amd64/main 和 arm64/main。
其通信协议与 Python 运行时完全相同:进程从 stdin 读取参数,并按所配置的格式将结果写入 stdout。请参阅格式与块头。llm-token-udf 仓库提供了用 Rust 和 Go 编写的 Native UDF 及相应的构建脚本。
配置参考
以下设置可在控制台和 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 格式本身已携带块边界信息,因此 Native UDF 无需块头即可读取一个块、写出一个块并执行刷写。
网络访问
网络访问仅适用于 Python 运行时;Native UDF 无法启用网络访问。在控制台中创建的 Python UDF 在构建时会默认启用网络访问,控制台中也没有相应的设置项。在 API 中,可通过sandboxType 在 netenable (允许出站连接) 和 basic (不允许出站连接) 之间进行选择;Terraform provider 则通过 sandbox_type 提供相同的选项。
启用网络访问后,可以向公网端点发起出站连接,但发往私有网络、链路本地地址和内部地址的连接会被阻止。沙箱不具备云身份:既没有实例角色和元数据端点,也不会继承任何环境变量,因此 UDF 只能使用您显式提供的凭证。网络访问在构建 UDF 版本时生效,因此如果某个现有版本在创建时未启用网络访问,则需要创建新版本。
目前 UDF 尚不支持机密信息存储。请将 config 文件打包到 zip 中,并通过相对于 __file__ 的路径读取;切勿在 SQL 中写入密钥。
内存限制
内存限制用于设置每个沙箱进程可用的最大内存 (单位为 MiB) 。未设置时,使用沙箱的默认值 4 GiB。 该限制针对的是虚拟地址空间,而非常驻内存。因此,对于会预留大段地址范围的运行时,需要在其实际用量之上留出充足的余量。例如,Go 在启动时就会预留远超 1 GiB 的地址空间。当进程超出限制时,内存分配会在进程内部失败:Python UDF 会输出MemoryError traceback 并退出,查询随之失败,错误消息中会包含相应的 stderr 输出。
确定性函数与查询缓存
默认情况下,UDF 被视为非确定性函数。如果在use_query_cache = 1 的情况下执行调用此类函数的查询,查询会失败并报错 QUERY_CACHE_USED_WITH_NONDETERMINISTIC_FUNCTIONS,除非更改了 query_cache_nondeterministic_function_handling 设置。
仅当函数对相同参数始终返回相同结果时,才应启用 确定性。启用后,查询缓存 即可缓存调用该函数的查询结果,并直接返回缓存结果。在默认的 throw 处理方式下,如果同一查询中还包含其他非确定性函数 (例如 now、today 或 rand) ,该查询仍无法被缓存;若设置 query_cache_nondeterministic_function_handling = 'save',则此类查询仍会被缓存;若设置为 'ignore',则会正常执行这些查询,但不缓存结果。
服务重启
监控
ClickHouse 26.6 及更高版本使用八个计数器记录可执行 UDF 的资源用量,这些计数器均以ExecutableUserDefinedFunction 为前缀。按查询维度,它们以条目形式记录在 system.query_log 的 ProfileEvents Map 列中。按服务器全局维度,它们既是 system.events 中的行 (自服务器启动起累计) ,也是 system.metric_log 中的 ProfileEvent_<name> 列 (随时间采样) 。各计数器后缀如下:
system.asynchronous_metrics 包含 ExecutableUserDefinedFunctionProcesses 和 ExecutableUserDefinedFunctionMemoryResidentBytes。后者是所有存活 UDF 进程的 VmRSS 之和。由于共享内存页会在每个进程中重复计入,该值只是一个上界。
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 输出会包含在错误消息中,因此请将诊断信息写入 stderr,并以非零状态码退出:
- 查询卡住,随后因超时而失败:进程以行式格式缓冲了输出,但未启用 Send chunk header。请启用该选项,并每输出一个块 (chunk) 刷写一次;若使用
Native格式,则每输出一个块 (block) 刷写一次。 - 返回码为 1 并出现
MemoryError回溯信息 (Python) :超出了内存限制。请调高 内存限制,或降低每个进程的内存占用。 - 高负载下类型为
executable的函数间歇性失败:请改用executable_pool。 - 构建状态为 error:zip 根目录中缺少
main.py(Python) 或amd64/main和arm64/main(Native) ,或者压缩包中包含符号链接。 - 无法创建函数:函数名称与内置函数冲突。
使用 Cloud API 管理 UDF
控制台中的生命周期操作也可通过 ClickHouse Cloud API 以编程方式执行。 UDF 端点可让您通过脚本完成以下操作:上传源代码归档文件、创建函数和版本、将其附加到服务或从服务中分离,以及删除版本和函数。该 API 不提供重新加载操作:将executable_pool UDF 的新版本附加到服务后,请在该服务上通过 SQL 运行 SYSTEM RELOAD FUNCTION ON CLUSTER default <name>,使池中的进程加载新版本。
这些端点目前处于 Beta 阶段,API 合约可能会变更。
- 创建上传 URL 以获取预签名的
application/zip上传 URL,然后将 ZIP 归档文件上传至该 URL。每个上传 ID 只能用于一次创建 UDF 或创建版本的尝试;重试时请申请新的上传 URL。 - 从上传的归档文件创建 UDF,并指定函数名称、运行时、参数和返回类型。
- 将 UDF 附加到服务。如果省略版本,则会附加最新的就绪版本。服务必须处于运行状态;可先唤醒空闲服务。
完整端点列表如下:
有关请求和响应 schema,请参阅 UDF API 参考文档。
使用 Terraform 管理 UDF
官方 ClickHouse Terraform provider 提供了两个资源,可用于以基础设施即代码的方式管理 UDF:clickhouse_udf用于管理函数本身。它接收包含函数源代码的 ZIP 归档文件,并在归档文件的哈希发生变化时发布新版本,同时等待构建完成。clickhouse_udf_attachment用于将 UDF 版本附加到服务。一个服务在同一时间最多只能使用一个函数版本。您可以固定指定版本号,也可以引用clickhouse_udf.<name>.version,以自动将服务升级到最新版本。
这些资源适用于 provider 3.24.0 及更高版本。它们目前处于 Beta 阶段,行为可能会在未来的 provider 版本中发生变化。
isBusinessHours UDF:
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;完整 schema 请参阅资源文档。
如需在新版本进入生产环境之前,先在开发服务上进行测试,可以让一个附加资源跟踪最新版本,并将另一个附加资源固定到某个变量。开发环境的附加资源会自动跟进每个新构建版本,而生产环境的附加资源仅在您修改 var.prod_udf_version 时才会更新:
该 provider 尚未支持
deterministic 和内存限制。这两项均需在创建版本时设置,因此通过该 provider 发布的版本无法使用它们。如需使用,请改为通过控制台或 API 创建这些版本。executable_pool UDF,在附加的版本发生变更后,请在该服务上运行 SYSTEM RELOAD FUNCTION ON CLUSTER default <name>。删除 clickhouse_udf 资源会移除该函数的所有版本,并将其从所有服务中分离。