图片

本文字数:5714;估计阅读时间:15 分钟

作者:Alexey Milovidov

本文在公众号【ClickHouseInc】首发

图片

我最近在 P99 会议上做了一个分享(https://www.p99conf.io/session/clickhouses-c-rust-journey/),介绍了我们在以 C++ 为主的 ClickHouse 项目中使用 Rust 的一些实践经验。

下面是演讲的视频。

https://www.youtube.com/watch?v=Q61QnV5r1fA

我也希望能以带注释的形式分享这次演讲内容,因此我在文中附上了演示幻灯片和加了注释的演讲文字稿。

介绍

图片

大家好,我是 ClickHouse 的开发者 Alexey,今天我想讲讲我们团队从 C++ 向 Rust 迁移的故事。

什么是 ClickHouse?

图片

那么,ClickHouse 到底是什么?它是一个开源的分析型数据库管理系统,拥有大量贡献者,并在 GitHub 上获得了非常高的人气。实际上,它是目前最受欢迎的开源分析型数据库,主要使用 C++ 编写。整个项目大约有 150 万行代码,相较于 MySQL 或 Postgres 这样的数据库系统来说,这个规模并不算大,可以说是处于一个正常水平。ClickHouse 已有十多年的发展历史,全球有许多开发者通过网络参与进来,持续贡献和修改它的源代码。目前有成百上千家公司——甚至可能更多——在生产环境中使用它。

从招聘角度来说,具备 C++ 技能的开发者并不难找。很多高校至今仍教授 C++,其中一些教学质量还很不错。而且 C++ 在数据库系统中应用非常广泛。MySQL 是用 C++ 写的,Postgres 用的是 C,MongoDB 是 C++,Redis 用 C,ClickHouse 用 C++,DuckDB 也是 C++。当然,并非所有数据库系统都基于 C 或 C++,但这类语言依旧很常见。像图形软件、音频处理、音乐、三维建模、CAD、操作系统——这些大多都是用 C++ 编写的;视频游戏领域,C++ 仍是主流;科学计算分析等场景也不例外。总之,C++ 无处不在。

但问题在于,如果我们现在重新开始开发 ClickHouse,还会选择 C++ 吗?或许我们应该直接用 Rust?把我们辛苦积累下来的数据库代码从 C++ 全部重写为 Rust,是否值得?

C++ 是一种痛苦的语言吗?

图片

C++ 到底有什么问题?它真的那么难用吗?可以说,是的。因为 C++ 是一种不安全的编程语言,不具备内存安全机制。段错误、内存损坏、竞态条件这些问题随处可见。初学者开始写 C++ 时,几乎必然会遇到段错误、竞态问题以及内存问题。这些问题几乎不可避免。虽然说可以尽力去避免,但这背后的代价是巨大的。

以 ClickHouse 为例,我们构建了一个庞大的持续集成系统,每天运行数千万次测试。其实是成千上万个测试用例,在各种配置组合下运行,配合所有可能的运行时检查器(sanitizer):包括地址检查器、线程检查器、内存检查器以及未定义行为检查器。我们对主分支上每一次提交的每个 Pull Request 都会执行完整的功能测试、压力测试、模糊测试(包括覆盖率引导的模糊测试)。这些手段确实能起到很大作用,显著降低了 bug 数量。但再完善也无法达到 100%,总会有少数问题漏网之鱼。

所以,也许现在是考虑使用 Rust 重写的时候了。

选择 Rust 的理由 vs 反对迁移的声音

图片

Rust 的优点显而易见:内存安全、线程安全、现代化的生态系统。有些优秀的库只在 Rust 中提供,C++ 并没有。同时,Rust 也很“火”,很多新一代工程师都喜欢用它。有时候我甚至觉得,我们还要花钱请人写 C++,而有人愿意免费帮我们写 Rust,甚至愿意倒贴钱,只为能用上自己喜欢的语言。如果这门语言有如此吸引力,那我们完全可以把它当作一种优势来利用。

当然,也有很多反对声音。最主要的问题是:代码重写一向是高风险、耗时长的工作,可能需要数年才能完成。如果我们整个团队都投入到重写中,那我们不仅会失去大量时间,错失很多进展机会,还会丢掉我们在 C++ 中积累下来的大量知识。说实话,我个人也不是很喜欢围绕 Rust 那种话题热度和社区戏剧性。几乎每周,社交平台都会有一些帖子,有人离开社区,有人感到失望,有人爆发争执。而这些在 C++ 世界里是不会发生的。C++ 世界虽然有些沉闷,但稳定。每一位老一辈程序员都清楚这门语言不够理想,但它就是存在,我们只能继续使用。

我们的方法:逐步迭代开发

图片

这张幻灯片展示了 ClickHouse 如何以迭代方式引入 Rust:首先在 2022 年通过 corrosion 项目将 Rust 集成进 CMake 构建系统,接着引入了 BLAKE3 哈希库作为首个非核心组件的试点,并展示了性能对比图表。

我们并没有选择推倒重写,而是采用了渐进式的开发策略。我们希望先体验一下 Rust 的生态和语言特性,看看是否值得深入。第一步,就是让 Rust 能顺利参与到我们的构建流程中。

ClickHouse 使用 CMake 作为构建系统,而 corrosion 项目正是专门为 C++ 中构建和链接 Rust 库而设计的 CMake 扩展工具。你只需为 Rust 库的接口写一个简洁的封装,其余的事情 corrosion 都能帮你搞定。于是我们选择在一个“无关紧要”的场景中先试试水——我们设定的任务是:为 SQL 添加一个新函数。ClickHouse 的 SQL 支持已有约 2000 个函数,我们决定选一个新函数,而所需的 BLAKE3 哈希库正好是 Rust 独有的。

将 Rust 代码集成进 ClickHouse

图片

我们通过 GitHub 上的 Pull Request #33435 实现了这次集成:引入了 BLAKE3 哈希库,最终于 2022 年 10 月 3 日合并,共提交了 134 次、涉及 25 个文件。这次尝试非常成功,Rust 的集成运行得很好。

用 Rust 改进 clickhouse-client

图片

接下来,我们尝试做些更实用的改进。幸运的是,我们收到了一位外部开发者的贡献。他不是我们的员工——当然,现在他已经加入我们团队了,虽然主要工作还是 C++ 开发。但他提交的这项功能非常实用:改进了命令行客户端 clickhouse-client 的历史记录浏览体验。

这其实挺合情合理的,因为 Rust 在处理命令行工具方面非常强大。有时候我甚至觉得,现在大家用 Rust 做的最多的事情就是重写各种 CLI 工具(笑)。不过,Rust 的用途远不止于此。

这项功能非常实用,是一位社区成员贡献的成果。这让我们看到了 Rust 在实际功能开发中的潜力。

社区贡献与持续改进

图片

从 2022 年 9 月到 12 月,我们陆续收到了多个 Rust 相关的 PR,这些改动涵盖了模糊搜索、构建修复、交互式历史记录等功能。

当然,这个过程并不总是一帆风顺。你从这些 PR 就能看出,最开始是引入新库,接着要修构建,然后因为某些问题又回退,再修复,再回退,再次修复。事情变得有些混乱,具体细节我稍后会详细介绍。

PRQL - 管道式关系查询语言

图片

PRQL(Pipeline Relational Query Language)是一种现代化的数据查询语言,用管道式结构替代了传统 SQL 的写法。幻灯片展示了一个 track_plays 查询在 ClickHouse SQL 与 PRQL 中的对照示例。

虽然我个人更喜欢 SQL,但也不是所有人都这么想。我们尝试使用 PRQL,作为一次规模较大的库集成实验。其实这只是一个学生的课程项目。项目完成后,学生也顺利拿到了优异成绩。虽然这个库存在一些明显的问题,但它的实验意义是明确的。

delta-kernel-rs:用于 Delta Lake 的 Rust 库

图片

我们继续推进更实质的集成项目,这一次不再只是为了测试,而是真正出于功能需求:为 Delta Lake 格式提供支持。

Delta Lake 是数据湖中常用的数据格式之一。所谓数据湖,指的是数据的存储格式可以与查询引擎解耦。你可以将数据集中存储,然后使用任意查询引擎来访问它,比如 ClickHouse,或 DataFusion。这种架构现在越来越流行,能提升数据可访问性,但其生态系统仍处于早期阶段——分散、脆弱。

在引入 Delta Lake 和 Iceberg 支持时,我们调研了可用的库。最初相关库只存在于 Java 世界,直到几年后,才逐步出现了 Rust 实现。而 C++ 下仍无相关实现。我们面临两个选择:要么自己用 C++ 写一套库,但这几乎是浪费时间——不过是解析一些 JSON 文件、转发 HTTP 请求;要么复用 Rust 社区已有的成熟库。

现在,我们有了这样的选择。Databricks 提供的官方库 delta-kernel-rs 正是其中之一。我们采用了它,确实成功运行了——当然,也遇到了一些问题。

将 Rust 与 C++ 结合的挑战

图片

Rust 是一门非常优秀的编程语言,设计时吸取了大量经验教训,专为避免 C++ 中常见的问题而生。但当我们尝试将 Rust 与 C++ 混合使用时,还是会遇到不少挑战。

首要挑战就是确保构建过程具有可复现性。所有依赖项都需要固定版本,并从源码构建,构建流程必须隔离网络,具备封闭性。在 C++ 代码库中我们已经实现了这些目标,虽然过程并不轻松。而在引入 Rust 后,这些工作需要重新做一遍。理论上只需对依赖进行 vendoring 就足够了,但实际操作中仍有不少繁琐步骤。

Rust 与 C++ 的接口封装

图片

另一个常见问题是封装层本身容易出错。这些封装代码中可能会引入 bug,甚至导致段错误和崩溃。好在我们有一个强大的持续集成系统,这是我们多年维护 C++ 项目中积累下来的成果。这个系统能快速捕捉到封装层的崩溃问题,而且这些错误通常都很简单,不会像内存损坏那样让你调试数周。

Rust 的 panic 问题

图片

Rust 另一个特有问题是 panic。简单来说,panic 是程序的强制终止机制,类似断言失败或 std::terminate。虽然它不会造成内存安全问题,但依旧会导致程序崩溃。

实际上,这并不是 Rust 语言本身的问题,而是很多库使用 panic 的方式不够妥当。本该优雅处理的错误,比如解析失败,反而直接触发 panic。我认为,这往往是因为 Rust 没有内建异常机制。虽然可以实现栈展开,但尝试模仿 C++ 的异常机制会非常麻烦,最终导致很多库过度依赖 panic 来报告错误。为了避免服务端程序莫名其妙被终止,我们必须主动发现并修复这类问题。

Rust 中的 panic 实例

图片

举个例子。我们刚引入 PRQL 时,模糊测试工具很快就发现了一个问题:当用户输入 x -> y 这样的查询时,服务器会立刻崩溃。虽然修复起来不难,我们通知作者之后很快就解决了,但这种潜在问题还是必须重视。

通过模糊测试(fuzzing)发现缺陷

图片

Rust/C++ 混合代码中的内存检查

图片

关于 sanitizer 工具:如果你只用 Rust,可能确实不太需要。但一旦和 C++ 混合使用,就必须启用包括 memory sanitizer 在内的各种检查工具。Rust 中生成的数据也需要插桩处理,这样才能被 C++ 的内存检查器识别为“已初始化”。三年前这确实是个问题,但到了 2025 年,我们已经拥有了完整支持 memory sanitizer 的 Rust 工具链,只需加一个编译选项就能解决。

交叉编译难点

图片

交叉编译并不是什么新鲜事。C++ 社区早在二三十年前就已支持这一能力。但由于许多构建系统默认不考虑交叉编译的需求,导致这项能力并未成为“标准配置”。我们团队从一开始就强制使用交叉编译,哪怕目标平台和主机平台相同,也坚持这么做以保证构建可复现。Rust 本身的交叉编译体验确实更好,但如果你把 C++ 和 Rust 混合使用,就必须为两个系统分别维护交叉编译逻辑,这确实有些繁琐。

依赖库冲突问题

图片

还有一种常见情况是:你在 C++ 里使用了专门定制的 OpenSSL 版本以满足合规要求,而 Rust 库却通过间接依赖引入了系统默认的 OpenSSL。这种情况虽然可以通过替换为 RustTLS 来规避,但如果你想让它改用你自己的 OpenSSL,也不是做不到,只是会引入不少配置复杂度。

代码组合性的挑战

图片

一个系统中有很多行为约定,比如内存分配、线程创建、连接池管理、HTTP 请求的处理方式等。当你引入一个外部 Rust 库时,如果它内部行为与这些约定不一致,就可能带来系统性问题。这不是 Rust 的错,而是多语言混合开发时容易出现的通病。

小插曲:PRQL 中的符号体积问题

图片

还有一些令人意想不到的小问题。例如,在链接 PRQL 库之后,我们发现最终生成的二进制体积显著增长。

查看符号表后我们发现,某个解析器组合库生成的符号名称极其冗长,单个符号就占用了数百 KB 空间。这种情况在 Rust 编译器输出中并不常见,我们只得手动修复。

依赖数量激增

图片

再谈谈依赖数量。当我们还只有 C++ 时,整个项目依赖约 156 个库,已经不少了。但引入 Rust 后,Rust 的依赖数量接近 700。某种程度上,这也体现了 Rust 的优势:模块化强、组合性好,代码风格统一,远胜于 C++ 各写各的风格。当然,它还不至于像 npm 那样失控,但对比之前少量依赖的安心状态,确实需要一点心理适应。

最终感想

图片

归根结底,Rust 确实也有它的问题。但这很正常,任何新技术都不可能十全十美。Rust 是一门优秀的语言,我们并没有仓促地把 ClickHouse 重写成 Rust,而是通过渐进方式逐步引入。现在,如果你喜欢 Rust,愿意用它开发 ClickHouse,我们非常欢迎。如果你能用 Rust 为我们的代码库贡献力量,我们将感激不尽。我们不是那种顽固守旧的 C++ 工程师,非常欢迎你加入 ClickHouse 的社区开发者行列。谢谢!

征稿启示

面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:Tracy.Wang@clickhouse.com

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐