简介
目前,ClickHouse DBMS 代码库由多种编程语言组成,但 DBMS 本身的主要语言是 C++。在这方面,集成其他编译语言库的可能性相当有限。Rust 就是这样一种语言。它的性能与 C 和 C++ 相似。Rust 丰富的类型系统和所有权模型保证了内存安全和线程安全。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 输入 char 指针转换为 Rust 字节数组类型几乎不花费时间,因为指针到字符串的转换旨在实现零成本,而转换为字节数组在 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 的抱怨 - 当未安装 Rust 时,项目像往常一样构建,而当安装 Rust 时,构建只是可以工作。
我们选择了一个小型库进行概念验证。如果实验进展顺利,我们可以将使用范围扩展到更多库。同时,我们没有很高的期望 - 如果没有足够的热情,我们可以简单地放弃它。
我对 Denis Bolonin 所做的工作印象深刻,这使得这一切成为可能!