WebAssembly ユーザー定義関数
ClickHouse は、WebAssembly で実装されたユーザー定義関数 (UDF) の作成をサポートしています。これにより、Rust や C、C++ などの言語で実装したカスタムロジックを WebAssembly モジュールにコンパイルし、実行できます。
概要
WebAssembly モジュールは、ClickHouse から呼び出すことができる 1 つ以上の関数を含むコンパイル済みバイナリファイルです。 モジュールは、一度ロードして何度も再利用するライブラリまたは共有オブジェクトと考えることができます。
UDF を含む WebAssembly モジュールは、Rust、C、C++ など、WebAssembly へコンパイル可能な任意の言語で記述できます。
WebAssembly にコンパイルされたコード (「ゲスト」コード) と、それを実行する ClickHouse (「ホスト」) は、専用のメモリ空間にのみアクセス可能なサンドボックス環境内で実行されます。
ゲストコードは ClickHouse が呼び出せる関数をエクスポートします。これには、カスタムロジックを実装する関数 (UDF を定義するために使用) に加えて、メモリ管理や、ClickHouse と WebAssembly コード間のデータ交換に必要なサポート関数が含まれます。
コードは、オペレーティングシステムや標準ライブラリへの依存を持たない「freestanding」WebAssembly (wasm32-unknown-unknown) としてコンパイルする必要があります。また、サポートされるのはデフォルトの 32 ビット WebAssembly ターゲットのみです (wasm64 拡張は使用できません) 。モジュールは、ClickHouse と連携するためにサポートされているいずれかの通信プロトコル (ABI) に従わなければなりません。
コンパイル後、モジュールのバイナリコードは system.webassembly_modules テーブルに挿入することで ClickHouse にロードされます。
その後、CREATE FUNCTION ... LANGUAGE WASM ステートメントを使用して、モジュールによってエクスポートされる関数を参照する UDF を作成できます。
前提条件
ClickHouse の設定で WebAssembly サポートを有効化します。
利用可能なエンジン実装:
クイックスタート
この例では、Collatz conjecture (コラッツ予想) の計算機を実装することで、WebAssembly UDF を作成するまでの一連のワークフローを示します。
ここでは WebAssembly Text 形式 (WAT) でコードを記述します。WAT は WebAssembly の人間が読みやすい表現であり、この段階では特定のプログラミング言語は必要ありません。
ClickHouse ではモジュールはバイナリ形式である必要があるため、トランスパイラを使って WAT を WASM に変換します。
この変換を行うには、WebAssembly Binary Toolkit (WABT) の wat2wasm か、wasm-tools の parse コマンドを使用できます。
上記のスニペットでは、バイナリの WASM コードを FORMAT RawBlob を用いてパイプし、直接 ClickHouse クライアントに渡して system.webassembly_modules テーブルに挿入しています。
次に、そのモジュールからエクスポートされている steps 関数を参照する UDF を定義します。
:: の後ろには、UDF 名とは異なるモジュール内の関数名を指定している点に注意してください。
これで、クエリ内で collatz_steps 関数を使用できるようになりました。
number カラムは UInt32 に明示的にキャストされています。これは、WebAssembly 関数が CREATE FUNCTION ステートメントで指定されたシグネチャと型が完全に一致していることを要求するためです。
結果として、1 から 100 までの数値に対する Collatz 手順の数列が得られ、これは OEIS の A006577 に対応します。
system テーブル経由で WASM モジュールを管理する
WebAssembly モジュールは、次の構造を持つ system.webassembly_modules テーブルに保存されます。
- Columns
nameString — モジュール名。空でないこと、かつ英数字およびアンダースコアのみ。codeString — 生のバイナリ形式の WASM コード。書き込み専用で、読み取り時は空文字列を返します。hashUInt256 — モジュールバイナリの SHA256 (ディスク上には存在するがまだロードされていない場合はゼロ) 。
モジュールの管理は、このテーブルに対する標準的な SQL 操作によって行います。
モジュールを追加する
必要に応じて、インテグリティハッシュを指定します:
指定されたハッシュ値がモジュールコードから計算された SHA256 と一致しない場合、挿入は行われません。これは、S3 や HTTP などの外部ソースからモジュールを読み込む際に役立ちます。
モジュールをクラスター全体に配布する
system.webassembly_modules はインスタンスごとのテーブルであり、INSERT で書き込まれるのは接続を処理しているレプリカだけです。INSERT ステートメントには ON CLUSTER 形式がないため、続けて CREATE FUNCTION ... ON CLUSTER を実行すると、モジュールを持たないレプリカでは失敗します:
insert をすべてのノードに展開するには、ローカルの system.webassembly_modules テーブルではなく、cluster テーブル関数に書き込みます:
このパターンは、基盤となる分散書き込みパスが各分片内のすべてのレプリカを経由することを前提としています。これは、クラスターが internal_replication=false に設定されている場合にのみ発生します。internal_replication=true の場合 (ReplicatedMergeTree を使用してレプリケーションを行うクラスターのデフォルト設定) 、insert は各分片ごとに正常なレプリカ 1 つにのみ送られ、system.webassembly_modules はその経路ではレプリケーションされません。そのため、一部のレプリカでは引き続きモジュールが存在しないままになります。この構成では、各レプリカに対して個別に insert する必要があります。たとえば、system.clusters を反復してホストごとに remote(...) 経由で書き込むか、すべてのホストの user_scripts/wasm/ にバイナリをコピーします。
クラスターの internal_replication は、SELECT cluster, shard_num, internal_replication FROM system.clusters で確認できます。
このように展開して insert した後は、すべてのレプリカにモジュールが配置され、CREATE FUNCTION ... ON CLUSTER が成功します。
clusterAllReplicas を使って、すべてのレプリカでモジュールが読み込まれていることを確認できます:
system.webassembly_modules への insert は、同じ (name, hash) の組み合わせに対しては冪等です。そのため、分散された insert を再実行しても安全であり、レプリカの置き換え後に状態を修復する現実的な方法です。なお、新たに追加したサーバーに既存のモジュールがさかのぼって配布されることはありません。更新後のクラスターに対して insert を再実行するか、新しいホストの 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 Interface のバージョンROW_DIRECT: 型を直接マッピングし、行単位で処理BUFFERED_V1: シリアライゼーションを伴うブロック単位の処理ASSEMBLYSCRIPT: AssemblyScript コンパイラで生成されたモジュール向けの行単位処理。数値型は AssemblyScript のプリミティブ型にマッピングされ、ClickHouse のStringは AssemblyScript のstringにマッピングされます。
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 実行時のユーザー定義関数は常に 2 つの i32 引数を受け取り、単一の i32 値を返します。
ゲストコードはこのデータを処理し、シリアライズされた結果データを含む結果バッファへのポインタを返します。
ゲストコードは、これらのバッファを作成および破棄するための 2 つの関数を提供しなければなりません。
C による定義例:
ABI ASSEMBLYSCRIPT
AssemblyScript コンパイラで生成されたモジュールを対象とします。各行で、エクスポートされた関数が 1 回トリガーされ、ClickHouse の値が AssemblyScript のプリミティブ型および文字列オブジェクトにマッピングされます。
サポートされる型:
-
数値:
Int8/UInt8、Int16/UInt16(境界ではi32に拡張) 、Int32/UInt32、Int64/UInt64、Float32、Float64 -
String— AssemblyScript のstring(WASM メモリ内では UTF-16) にマッピングされます。ClickHouse は UTF-8 ↔ UTF-16 の変換を自動的に処理します。 -
カスタム AssemblyScript クラスは、引数型または戻り値型としてはサポートされません。これらのランタイムクラス ID はコンパイルごとに安定しないためです (AssemblyScript#2982 を参照) 。
モジュール要件:
標準の入出力文字列処理ではこれらが必要となるため、モジュールは __new、__pin、__unpin がエクスポートされるよう、AssemblyScript のマネージドランタイムでコンパイルする必要があります。推奨される呼び出しは次のとおりです。
AssemblyScript は、ランタイムトラップ (メモリ不足、境界チェックなど) に対応するために env.abort もインポートします。ClickHouse はこのインポートを自動的に提供します。abort がトリガーされると、実行中のクエリは、デコード済みの AssemblyScript メッセージとソース位置を含む WASM_ERROR 例外で失敗します。
例:
asc でコンパイルし、生成された .wasm を system.webassembly_modules に読み込んだ後、UDFs を次のように宣言します。
Rust で UDF を開発する際の注意
Rust プログラム向けに、ClickHouse 用の WebAssembly UDF の開発を容易にするヘルパークレート clickhouse-wasm-udf を提供しています。このクレートはメモリ管理用の関数を提供しているため、clickhouse_create_buffer と clickhouse_destroy_buffer 関数を自前で実装する必要はなく、依存関係としてこのクレートを追加するだけで済みます。また、通常の Rust 関数を要求される ABI 形式にラップするためのマクロ #[clickhouse_wasm_udf] も用意されています。
このクレートを用いることで、次のように UDF を記述できます。
これらのマクロは、バッファ構造体を受け取りおよび返すラッパー関数を生成し、serde を用いてシリアライズ/デシリアライズ処理を自動的に行います。
モジュールで利用可能なホスト API
次のホスト関数をモジュールからインポートして使用できます。
clickhouse_server_version() -> i64— ClickHouse サーバーのバージョンを整数として返します (例: v25.11.1.1 の場合は 25011001) 。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 ランタイムトラップが発生すると、デコードされたメッセージとソース位置を含むWASM_ERROR例外によって UDF は終了します。env.abortをインポートしないモジュールには影響しません。
設定
クエリレベルの以下の設定値により、WebAssembly UDF の実行を制御します:
-
webassembly_udf_max_fuel— WebAssembly UDF インスタンス 1 回の実行ごとの fuel 上限。各 WebAssembly 命令は一定量の fuel を消費します。この値はランタイムに渡される前に 1024 倍されるため、webassembly_udf_max_fuel = 1はおよそ 1024 fuel 単位に相当します。有限の上限を設けないには 0 に設定します。適用されるのは、関数ごとの設定webassembly_udf_enable_fuelが true の関数のみで、これが既定値です。 -
webassembly_udf_max_memory— WebAssembly UDF インスタンス 1 つあたりのメモリ上限 (バイト単位) 。 -
webassembly_udf_max_input_block_size— 単一ブロックで WebAssembly UDF に渡される最大行数。すべての行を一度に処理するには 0 に設定します。 -
webassembly_udf_max_instances— 1 つの関数につき並列実行できる WebAssembly UDF インスタンスの最大数。
使用例: