简介
目前,ClickHouse DBMS 代码库包含几种编程语言,但 DBMS 本身的主要语言是 C++。因此,集成其他编译语言库的可能性非常有限。Rust 就是其中一种语言。它的性能与 C 和 C++ 相似。Rust 丰富的类型系统和所有权模型保证了内存安全和线程安全。它有相当多的实用库,因此我们考虑在 ClickHouse 中使用它们。
将 Rust 集成到 ClickHouse 中
构建 Rust
Rust 有自己的包管理器,称为 Cargo。它用于下载依赖项、Rust 编译和包分发。另一方面,ClickHouse 使用 CMake 和 Ninja 构建系统。这两种项目构建方法默认情况下不兼容,因此我们需要为 Rust 构建系统实现某种集成。我们的第一个方法是实现我们自己的 CMake 函数,该函数将启动 Cargo 并检测其输出。这种方法取得了成功,它使我们能够使用 Rust 库作为依赖项构建 ClickHouse,但它也有一个很大的缺点:我们需要为每个新 Rust 库更改和重新实现我们函数的部分内容。经过一番搜索,我们发现了一个专为将 Rust 库集成到 CMake 项目而设计的实用程序——Corrosion-rs。它使我们能够更轻松地集成 Rust 项目——只需使用 3 行 CMake 文件。
BLAKE3
我们决定将 BLAKE3 作为示例 Rust 库进行集成。BLAKE3 是一种高性能安全加密哈希函数。
它被添加为一个子模块并连接到 ClickHouse 构建系统,但我们仍然无法在 C++ 代码中使用它的方法。为此,我们需要修复 Rust 数据类型和 C++ 数据类型之间的兼容性问题。解决方案很简单:我们在 Rust 中编写了一个 shim 函数。它以 C 数据类型变量作为输入,在将数据转换为相应的 Rust 类型后调用 BLAKE3 哈希,然后以 C 格式返回生成的哈希值。我们使用 C 来保证兼容性,因为 Rust 外部函数接口只提供与 C 兼容的类型。
由于它的输入和返回类型等于 C 类型,我们创建了一个包含该函数声明的 .h 头文件,它使我们最终能够在 ClickHouse 代码中使用它。
性能如何?
我们在三种不同的输入上测量了 BLAKE3 的性能。以下是将 BLAKE3 与 ClickHouse 中类似哈希函数进行比较的图表
如您所见,BLAKE3 的性能比 SHA224 或 SHA256 快 2 倍以上,比 MD5 稍快。此外,BLAKE3 是安全的,不像 MD5 和 SHA-1,并且不受长度扩展的攻击,不像 SHA-2。
这些结果符合我们的预期,并与 BLAKE3 作者针对 1KB 输入长度的测试结果一致。
但是 shim 方法怎么样?我们使用它们在调用 BLAKE3 之前和之后转换 Rust 和 C 数据类型,因此它们需要一些时间来完成所有转换。为了测量由于转换而产生的开销,我们使用了 perf top 实用程序,并将它的数据作为火焰图展示
在这里我们可以看到,shim 函数中最耗时的部分是将 Rust 哈希数据转换为 C 格式,它大约占整个查询时间的 1.15%。此外,在开始时将 C 输入字符指针转换为 Rust 字节数组类型几乎不需要时间,因为将指针转换为字符串旨在为 0 成本,将字符串转换为字节数组在 Rust 中具有恒定成本。
问题
在 BLAKE3 集成过程中,我们遇到了一些问题。第一个问题与 C++ 内存消毒器有关,它无法理解对 Rust 中内存的操作,并将它们标记为误报。它通过对输入数据使用 _msanunpoison 内存消毒器函数并添加一个具有更明确字节数组转换的 shim 方法版本来解决。经过所有修复后,误报消失了,内存消毒器正常工作。
另一个问题与多平台构建和链接有关。虽然 ClickHouse 支持的大多数平台都可以使用 Cargo 轻松配置,但有些平台需要通过 CMake 或特定包/框架进行额外配置。目前,只有一个平台 (aarch64-darwin) 仍然不受支持,因为在构建过程中出现了一些链接问题。
结论
Rust 语言库集成功能已实现,BLAKE3 哈希函数已作为示例库添加。这使我们能够
- 将来在 ClickHouse 中添加和使用其他 Rust 库。
- 将来在 ClickHouse 中与其他哈希函数一起使用 BLAKE3,利用其速度和安全特性。
来自 Alexey Milovidov 的评论
我们希望将对 Rust 的支持作为一项实验添加到构建中。Rust 有一个充满活力的社区,并且有许多优秀的、高质量的库,我们可以在 ClickHouse 中使用它们。同时,我们希望使集成尽可能不引人注目。我们使用“你不用不付钱”的原则:Rust 集成不应妨碍您,也不应夸耀自己有多好。
对集成有以下要求
- 它应该是可选的——代码不应要求 Rust 进行构建:如果未安装 cargo,它应该在没有 Rust 库的情况下简单地构建;
- 静态链接(没有新的动态库);它不应引入对 glibc 中的新符号版本 的依赖;二进制文件应该可以在旧的 Linux 系统上运行;二进制文件应该是整体的;
- 支持与 C++ 代码进行交叉编译,因为我们始终使用交叉编译来构建我们的代码(即使目标平台与主机平台相同,也使用带有自定义 sysroot 的封闭式构建);
- 支持使用消毒器和模糊测试:它们主要与 C++ 代码相关,但二进制文件应与 Rust 代码链接,并且在使用消毒器时也能正常工作。
满足这些要求比我们想象的要难得多,但结果比我预期的要好。没有人抱怨 Rust——当它没有安装时,项目照常构建,而当它安装时,构建正常工作。
我们选择了一个小型库来进行概念验证。如果实验顺利,我们可以将使用范围扩展到更多库。同时,我们没有太高的期望——如果热情不足,我们可以简单地放弃它。
我对 Denis Bolonin 的工作印象深刻,正是他的工作使这一切成为可能!