개요
WebAssembly 모듈은 ClickHouse에서 호출할 수 있는 하나 이상의 함수를 포함하는 컴파일된 바이너리 파일입니다. 모듈은 한 번 로드해 여러 번 재사용하는 라이브러리나 공유 객체로 생각하면 됩니다. UDF를 포함하는 WebAssembly 모듈은 Rust, C, C++처럼 WebAssembly로 컴파일할 수 있는 모든 언어로 작성할 수 있습니다. WebAssembly로 컴파일된 코드(“guest” 코드)와 ClickHouse에서 실행되는 코드(“host”)는 전용 메모리 공간에만 접근할 수 있는 샌드박스 환경에서 실행됩니다. 게스트 코드는 ClickHouse가 호출할 수 있는 함수를 export합니다. 여기에는 사용자 지정 로직을 구현하는 함수(UDF 정의에 사용됨)뿐 아니라 메모리 관리와 ClickHouse와 WebAssembly 코드 간 데이터 교환에 필요한 지원 함수도 포함됩니다. 코드는 운영 체제나 표준 라이브러리에 의존하지 않는 “freestanding” WebAssembly(즉,wasm32-unknown-unknown)로 컴파일해야 합니다. 또한 기본 32비트 WebAssembly 대상만 지원됩니다(wasm64 확장은 지원되지 않음).
모듈은 ClickHouse와 상호작용하기 위해 지원되는 통신 프로토콜(ABI) 중 하나를 따라야 합니다.
컴파일이 완료되면 모듈의 바이너리 코드는 system.webassembly_modules 테이블에 삽입되어 ClickHouse에 로드됩니다.
그 후 CREATE FUNCTION ... LANGUAGE WASM 구문을 사용해 모듈이 내보낸 함수를 참조하는 UDF를 생성할 수 있습니다.
사전 요구 사항
ClickHouse 구성에서 WebAssembly 지원을 사용 설정하세요:wasmtime(기본값이며 현재 유일한 구현) — Wasmtime을 사용합니다
빠른 시작
이 예시는 콜라츠 추측 계산기를 구현하여 WebAssembly UDF를 만드는 전체 워크플로를 보여줍니다. 이 단계에서는 프로그래밍 언어가 필요하지 않으므로, WebAssembly를 사람이 읽을 수 있는 형태로 표현한 WebAssembly Text 포맷(WAT)으로 코드를 작성합니다. ClickHouse는 모듈이 바이너리 형식이어야 하므로, 트랜스파일러를 사용해 WAT를 WASM으로 변환합니다. 이 변환을 수행하려면 WebAssembly Binary Toolkit (WABT)의wat2wasm 또는 wasm-tools의 parse 명령을 사용할 수 있습니다.
FORMAT RawBlob을 사용해 ClickHouse client로 직접 파이프하여 system.webassembly_modules 테이블에 삽입합니다.
그런 다음 모듈이 내보내는 steps 함수를 참조하는 UDF를 정의합니다:
:: 뒤에는 모듈의 함수 이름을 지정한다는 점에 유의하십시오.
이제 쿼리에서 collatz_steps 함수를 사용할 수 있습니다:
number 컬럼은 UInt32로 명시적으로 CAST됩니다. 이는 WebAssembly 함수가 CREATE FUNCTION 문에 지정된 시그니처와 정확히 일치하는 타입을 요구하기 때문입니다.
그 결과, 1부터 100까지의 수에 대한 Collatz 단계 수열을 얻을 수 있으며, 이는 OEIS의 A006577 수열에 해당합니다.
시스템 테이블을 통해 WASM 모듈 관리
WebAssembly 모듈은 다음 구조를 가진system.webassembly_modules 테이블에 저장됩니다:
- 컬럼
nameString — 모듈 이름입니다. 비어 있을 수 없으며, 영문자, 숫자, 밑줄 문자만 사용할 수 있습니다.codeString — 원시 바이너리 WASM 코드입니다. 쓰기 전용이며, 읽기 시 빈 문자열이 반환됩니다.hashUInt256 — 모듈 바이너리의 SHA256입니다(디스크에는 존재하지만 아직 로드되지 않은 경우 0).
모듈 삽입
클러스터 전체에 모듈 배포
system.webassembly_modules는 인스턴스별 table이므로 INSERT는 connection을 처리하는 레플리카에만 적용됩니다. INSERT 문에는 ON CLUSTER 형식이 없으므로, 이어서 CREATE FUNCTION ... ON CLUSTER를 실행하면 모듈이 없는 레플리카에서는 실패합니다:
system.webassembly_modules 테이블 대신 cluster 테이블 함수에 쓰세요:
이 패턴은 기본 분산 쓰기 경로가 각 세그먼트 내의 모든 레플리카를 거치는 방식에 의존하며, 이는 클러스터가
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 Interface 버전입니다.ROW_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 — 함수에 대한 제한된 연료 예산을 활성화합니다. 기본값:true.false이면 이 함수에서는 쿼리 수준 설정webassembly_udf_max_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 값 2개로 구성됩니다. 버퍼는 데이터 자체가 아니라 이 구조체를 가리키는 포인터인 handle로 전달됩니다. 게스트 코드는 이러한 버퍼를 생성하고 제거하기 위한 2개의 함수를 내보내야 합니다. 크기가 0이 아닌 경우 데이터 포인터도 0이 아니어야 합니다. 양수 크기를 선언하면서 선형 메모리 오프셋 0을 가리키는 버퍼는 유효한 버퍼가 아니며, 게스트 코드는 이러한 버퍼를 생성해서는 안 됩니다. ClickHouse는 이러한 버퍼를 거부하고 WASM_ERROR와 함께 쿼리를 실패시킵니다.
각 입력 블록에 대해 ClickHouse는 다음을 수행합니다.
- 함수의
serialization_format(기본값은MsgPack)을 사용하여 인수 컬럼을 직렬화합니다. 행 기반 포맷은ARGUMENTS에 선언된 순서대로 인수 값을 행별로 작성합니다. 이름이 지정된 필드가 있는 포맷은 인수 이름을 사용하므로, 이러한 포맷을 사용할 때는ARGUMENTS에 인수 이름을 선언하십시오. - 모듈에서 내보낸
clickhouse_create_buffer를 호출하고, 반환된 버퍼가 가리키는 메모리에 직렬화된 데이터를 복사합니다. - 입력 버퍼 handle(함수에 인수가 없으면
0)과 행 수라는 2개의i32인수로 사용자 정의 함수를 호출합니다. 함수는 게스트 코드가 직접 할당한 결과 버퍼의 handle인 단일i32를 반환합니다.0을 반환하면 쿼리가 오류와 함께 실패합니다. - 결과 버퍼를 읽습니다. 이 버퍼에는 동일한 포맷으로 직렬화된, 정확히 같은 수의 행을 포함하는 컬럼이 정확히 하나 있어야 합니다.
JSONEachRow처럼 이름이 지정된 필드가 있는 포맷에서는 결과 컬럼의 이름이result여야 합니다. - 입력 버퍼(있는 경우)와 결과 버퍼 handle 모두에 대해
clickhouse_destroy_buffer를 호출합니다. 게스트 코드는 결과 버퍼를 직접 해제해서는 안 되며, 입력 handle을 결과로 반환해서도 안 됩니다. 그렇게 하면 버퍼가 두 번 제거됩니다.
webassembly_udf_input_split_memory_ratio가 이를 동적으로 분할합니다. ClickHouse는 직렬화된 행을 측정하여 입력이 게스트 선형 메모리의 해당 비율을 넘어서기 전에 새 호출을 시작하므로, 행 수는 데이터에 따라 달라집니다. webassembly_udf_max_input_block_size가 0이 아니면 이를 대체하여 호출당 지정된 행 수의 고정 상한이 적용되며, 비율이 0이면 전체 블록을 한 번의 호출로 전달합니다. 호출은 결코 1개 행 미만으로 줄어들지 않으므로, 단일 행 자체가 모듈 메모리에 비해 너무 큰 경우에는 여전히 게스트 내부에서 실패합니다.
버퍼 내부의 정확한 바이트는 선택한 포맷에 따라 결정되며, 게스트 코드는 이를 직접 디코딩하고 인코딩해야 합니다. 특히
String 값의 경우:MsgPack: ClickHouse는String값을str계열이 아닌bin타입 계열(0xc4/0xc5/0xc6)로 작성합니다. 결과를 파싱할 때는 두 계열 모두 허용됩니다.RowBinary: 각String앞에는 고정 폭 정수가 아닌 부호 없는 varint(LEB128) 형식의 바이트 길이가 접두사로 추가됩니다.JSONEachRow: 각 결과 행은 예를 들어{"result":"value"}와 같이result키를 가진 객체여야 합니다.
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에 매핑됩니다(WASM 메모리에서는 UTF-16). ClickHouse가 UTF-8 ↔ UTF-16 변환을 자동으로 처리합니다. - 사용자 정의 AssemblyScript 클래스는 인수 또는 반환 타입으로 지원되지 않습니다. 런타임 클래스 id가 컴파일할 때마다 안정적이지 않기 때문입니다(AssemblyScript#2982 참조).
__new, __pin, __unpin이 내보내지도록 AssemblyScript 관리 런타임으로 컴파일되어야 합니다. 표준 문자열 입출력 처리는 이를 전제로 합니다. 권장 호출 방식은 다음과 같습니다.
env.abort도 가져옵니다. ClickHouse는 이 import를 자동으로 제공합니다. abort가 트리거되면 현재 실행 중인 쿼리가 실패하며, 디코딩된 AssemblyScript 메시지와 소스 위치가 포함된 WASM_ERROR 예외가 반환됩니다.
예시:
asc로 컴파일하고 생성된 .wasm을 system.webassembly_modules에 로드한 후, UDF를 다음과 같이 선언합니다:
Rust로 UDF를 개발할 때 참고할 사항
Rust 프로그램용으로는 ClickHouse의 WebAssembly UDF 개발을 간소화할 수 있도록 헬퍼 크레이트 clickhouse-wasm-udf를 제공합니다. 이 크레이트는 메모리 관리 함수를 제공하므로clickhouse_create_buffer와 clickhouse_destroy_buffer 함수를 직접 구현할 필요 없이 크레이트를 의존성으로 추가하면 됩니다. 또한 일반적인 Rust 함수를 필요한 ABI 형식으로 감싸는 매크로 #[clickhouse_wasm_udf]도 제공합니다.
이 크레이트를 사용하면 다음과 같이 UDF를 작성할 수 있습니다:
serde를 사용해 직렬화/역직렬화를 자동으로 처리합니다.
모듈에서 사용할 수 있는 호스트 API
다음 호스트 함수는 import하여 모듈에서 사용할 수 있습니다: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를 import하지 않는 모듈은 영향을 받지 않습니다.
설정
다음 쿼리 수준 설정은 WebAssembly UDF 실행을 제어합니다:-
webassembly_udf_max_fuel— WebAssembly UDF 인스턴스 실행당 연료 한도입니다. 각 WebAssembly 명령어는 일정량의 연료를 소비합니다. 이 값은 런타임에 전달되기 전에 1024배로 스케일되므로,webassembly_udf_max_fuel = 1은 약 1024 연료 단위에 해당합니다. 유한한 제한을 두지 않으려면 0으로 설정합니다. 이는 함수별 설정인webassembly_udf_enable_fuel이 true인 함수에만 적용되며, 기본값은 true입니다. -
webassembly_udf_max_memory— WebAssembly UDF 인스턴스당 바이트 단위의 메모리 한도입니다. -
webassembly_udf_max_input_block_size— 모든 ABI에 대해, 단일 블록에서 WebAssembly UDF에 전달되는 최대 행 수입니다. 0으로 설정하면ABI BUFFERED_V1함수의 호출 크기를webassembly_udf_input_split_memory_ratio가 결정하며, 다른 ABI는 파이프라인 블록 전체를 받습니다. -
webassembly_udf_input_split_memory_ratio— 모듈이 시작 시점에 보유한다고 선언한 선형 메모리(페이지 없이 시작하는 모듈의 경우 메모리 상한) 중 한 번의 호출 입력이 차지할 수 있는 비율입니다.webassembly_udf_max_input_block_size가 0일 때ABI BUFFERED_V1함수의 블록을 분할하는 데 사용되며, 다른 ABI는 직렬화된 입력 블록을 전달하지 않으므로 이 설정을 무시합니다. 기본값:0.5로, 나머지 메모리는 게스트 자체의 작업 집합에 남겨둡니다 — 비율이 1에 가까우면 게스트의 할당자 내부에서 여전히 실패할 수 있습니다. 파이프라인 블록 전체를 한 번의 호출로 전달하려면 0으로 설정하거나compatibility를 26.9 이전 버전으로 설정합니다. 값은 0 이상 1 이하여야 합니다. -
webassembly_udf_max_instances— 함수별로 병렬 실행할 수 있는 WebAssembly UDF 인스턴스의 최대 개수입니다.