当前位置: 首页 > news >正文

DeepSeek总结的将 Rust Delta Kernel 集成到 ClickHouse

来源:https://clickhouse.com/blog/integrating-rust-delta-kernel

将 Rust Delta Kernel 集成到 ClickHouse

作者:Melvyn Peignon, Kseniia Sumarokova, Raúl Marín
日期:2026年5月22日
阅读时间:24分钟

除非你过去几年一直呆在没有互联网的洞穴里,否则你可能听说过像 Delta Lake 和 Iceberg 这样的开放表格式。

目标很简单:定义一个数据格式,只要遵循协议,任何查询引擎都可以读写。随着时间的推移,这些格式已超越了简单的互操作性,引入了更丰富的表语义,例如事务支持、模式演化和直接在底层数据之上的版本化数据管理。

最近,我们宣布 ClickHouse 已准备好作为数据湖,支持这些表格式作为查询引擎。作为这一过程的一部分,我们一直在更深入地研究这些格式,并且像许多其他公司一样,遇到了一些相同的挑战。

采用这些表格式并非易事。支持它们需要跟上复杂且不断发展的协议,无论是通过外部库还是自定义实现。每个查询引擎仍然负责维护自己的集成,这通常会导致功能支持碎片化和维护开销增加。

在这篇文章中,我们探讨了如何通过将 Rust Delta Kernel 集成到 ClickHouse 来解决这个问题。这在表格式和 ClickHouse 查询引擎之间提供了一个经过维护且一致的接口,使我们能够公开更多功能,同时显著降低集成复杂性。

什么是 ClickHouse?

ClickHouse 是一个高性能、面向列的 SQL 数据库管理系统,用于在线分析处理。OLAP 指的是针对海量数据集进行复杂计算(例如聚合、字符串处理、算术)的 SQL 查询,在毫秒内处理数十亿和万亿行。

关于 ClickHouse 的介绍,包括它存在的原因以及如何实现其性能,我们推荐阅读《入门指南》。要深入了解其速度背后的架构决策,请参阅“ClickHouse 为何快速”系列。

本文的其余部分将重点讨论 ClickHouse 与 Delta Lake 的集成,探讨我们如何与 Delta Kernel 集成。

使用 Delta Lake

使 ClickHouse 准备好作为数据湖的关键目标之一是让用户能够直接从像 Delta Lake 这样的开放表格式查询数据。我们最初的方法是通过直接处理 Delta 协议来实现原生支持。虽然这给了我们完全的控制权,但也暴露了格式的复杂性以及跟进不断发展的规范的持续成本。

SELECTcityHash64(URL),count()AScntFROMdeltaLake('https://datasets-documentation.s3.amazonaws.com/lake_formats/delta_lake/')GROUPBYcityHash64(URL)ORDERBYcntDESCLIMIT5

随着支持范围的扩大,很明显,在保持集成可维护的同时实现完整的功能覆盖将变得越来越困难。这促使我们采用 Delta Kernel,使我们能够将协议处理工作外包,并专注于 ClickHouse 最擅长的领域:高性能查询执行。

介绍 Delta Kernel

Delta Lake Kernel 抽象了底层格式的大部分复杂性,在查询引擎和协议之间提供了一个清晰的边界。它处理 Delta 文件的处理,并向引擎公开定义良好的接口,允许 ClickHouse 操作“黑盒”对象,而无需管理底层机制。

与我们最初 ClickHouse 直接实现协议的方法相比,这降低了实现复杂性和维护开销,同时更容易跟上新功能的步伐。

Delta Kernel 提供了一系列保证,使其成为支持 Delta Lake 的有吸引力的基础,而无需引擎本身实现协议。特别是,它负责:

  • 解析存储为 JSON 的事务日志
  • 读取和解释 Delta 元数据
  • 解析快照并确定正确的数据文件集
  • 基于元数据应用数据跳过

通过集中这些逻辑,Kernel 消除了每个查询引擎独立实现和维护对不断发展的协议的支持的需要。

同时,像 ClickHouse 这样的查询引擎仍然需要保留对性能关键组件的控制,特别是文件读取。大量的工程工作投入到优化 Parquet 读取器中,这些优化对于整体查询性能至关重要。

Delta Kernel 在设计时就考虑到了这种平衡。

通过其引擎 API,它允许引擎插入自己优化的实现,用于文件访问和数据读取等组件,提供关于数据文件的元数据,包括统计信息和删除向量,以允许 ClickHouse 进行高效的下游过滤和处理。它还通知查询引擎需要应用于磁盘数据以使其与表的逻辑格式匹配的任何转换,以及用于与快照交互的更高级别接口。

Delta Kernel 增加了什么

除了抽象 Delta 协议的复杂性之外,Delta Kernel 还解锁了许多我们自己实现和维护起来更具挑战性的功能。我们无需为每个功能独立构建和发展支持,而是继承了一个一致且定义良好的实现,使我们能够直接在 ClickHouse 中公开这些功能。

在实践中,这使我们能够比使用原生实现可行的情况下更快地提供更广泛的功能集。

  • 写入。Delta Kernel 在事务级别提供了对管理 Delta 表写入的全面支持,包括处理事务日志和确保一致性。Delta Lake 为这些操作保证了 ACID 语义,防止部分或冲突更新,并支持安全的并发访问模式,这些模式从头开始正确实现很困难。然而,底层的 Parquet 数据文件由 ClickHouse 写入,而 Delta Kernel 仅负责协调和记录相关的事务元数据。

  • 模式演化。Delta 表可以随时间演化,而无需完全重写。可以以受控方式添加或修改列,所有更改都记录在事务日志中。这允许 ClickHouse 与结构随时间变化的数据集交互,而不会破坏查询或管道。为实现这一点,Delta Kernel 公开了反映表如何呈现给用户的逻辑模式,以及底层 Parquet 文件使用的物理模式。对于每个数据文件,它还提供模式转换元数据,使 ClickHouse 能够协调文件级模式与当前表定义之间的差异。这确保了即使模式随时间演化,数据也能一致且透明地读取。

  • 时间旅行。对 Delta 表的每次更改都会进行版本控制,允许查询数据的历史快照。这实现了可重复性、审计和调试工作流,因为用户可以查询数据集的历史版本,而无需维护单独的副本。Delta Kernel 支持灵活的、版本化的访问模式,允许用户在特定快照处读取表。它还通过更改数据馈送提供行级更改可见性,将插入、更新和删除作为事件流公开。这使得构建增量管道、审计修改或同步下游系统变得简单。在 ClickHouse 25.12 中,我们通过deltaLake表函数公开了 Delta Lake 的 CDF。这允许用户使用delta_lake_snapshot_start_versiondelta_lake_snapshot_end_version设置查询表版本之间的行级更改。

    SELECT*FROMdeltaLake('s3://path/to/table')SETTINGS delta_lake_snapshot_start_version=5,delta_lake_snapshot_end_version=10;

    结果包括元数据列,如_change_type_commit_version_commit_timestamp,允许用户推理数据如何随时间演化,并更直接地遍历表历史。

  • 分区修剪。Delta 元数据包括分区信息,允许引擎在查询执行期间跳过整个数据子集。通过 Kernel 公开这一点,ClickHouse 可以避免扫描不相关的文件并减少 I/O,从而提高查询性能。

  • 基于统计信息的修剪。除了分区,Delta 还跟踪文件级统计信息,如最小值和最大值。这些可用于数据跳过,允许查询忽略不能满足给定谓词的文件。这是大数据集的关键优化,显著减少了需要读取的数据量。

总而言之,这些功能代表了一个庞大的功能领域。原生实现它们不仅需要大量的工程努力,还需要持续的维护以跟上不断发展的 Delta 协议。通过采用 Delta Kernel,我们能够专注于查询执行和性能,同时依赖共享实现来保证协议的正确性和功能的完整性。

在 ClickHouse 中使用 Delta Kernel - 将 Rust 库添加到 C++ 数据库

要了解这种集成如何融入 ClickHouse,首先了解其构建系统的设计很重要。

ClickHouse 构建系统

ClickHouse 有许多设计选择,起初可能感觉有些特殊,但一旦理解,往往会证明其强大。一个很好的例子是其 SQL 扩展,例如表达式别名。ClickHouse 允许在同一子查询内定义和重用别名,甚至可以从列声明的不同部分引用,而不仅仅是最终的投影。虽然这最初看起来可能令人惊讶,但它实现了更灵活、更简洁的查询模式。

构建系统也不例外。像 ClickHouse 的其他部分一样,它有自己的特点,起初可能令人惊讶,但这是深思熟虑的设计决策的结果。要理解我们集成delta-kernel-rs时遇到的挑战,首先了解这些特性以及它们如何塑造代码集成的方式很重要。

首先,最终的二进制文件没有外部依赖,除了libc库,至少目前是这样。默认的 Linux 构建仅依赖于相对较旧的 glibc 版本,x86_64 上为 2.4(近 20 年),ARM 上为 2.18(约 11 年)。它不依赖libstdc++或任何其他外部库。这确保了二进制文件在不同环境下一致运行,无论系统上安装了什么都具有相同的行为。

第二个特点直接源于此。由于无法进行动态加载,所有依赖都必须构建到二进制文件中。我们通过将它们作为contrib/下的子模块存放在仓库中来实现这一点。这极大地简化了开发,消除了对系统库版本的担忧,并确保所有必需的代码始终可用。这种方法在 Go 和 Rust 等较新的生态系统中越来越普遍。话虽如此,它确实带来了权衡,特别是从打包的角度来看,它可能导致二进制文件更大,并引起对依赖项新鲜度的担忧。

这些约束自然导致了下一个设计选择。外部依赖项使用与核心代码库相同的编译器选项和标志构建,仅在需要时有极少的例外。我们还为每个依赖项维护我们自己简化的 CMake 配置,根据我们的需求进行定制。

以与内部代码相同的标准对待外部代码很快证明了其价值。通过在所有依赖项上运行我们的 sanitizer 和模糊测试器,我们经常在集成过程的早期发现问题。修复这些问题不仅改进了 ClickHouse 本身,也回馈了更广泛的开源生态系统。

ClickHouse 中的 Rust

Rust 被引入 ClickHouse 是为了向数据库引擎添加次要功能,例如新函数。最初的演示引入了BLAKE3crate 来提供blake3函数,使用一些手写的 C 包装器和 Corrosion 来处理 cmake 目标。

虽然这个最初的库后来被移除了(被 LLVM 的 C++ 实现取代),但其他库被添加并且仍然存在:

  • prql支持 PRQL 方言
  • skim支持客户端中的模糊搜索
  • chcache:一个实验性工具,旨在替代 ClickHouse 开发中的sccache
  • chdig- 严格来说不是一个库,而是一个很酷的终端用户界面,用于像top一样调试 ClickHouse

但是,在发展我们的 Rust 集成,或者仅仅是开发其他功能时,我们注意到初始实现的一些缺点,我们必须解决这些缺点。

首先,支持 sanitizer。ClickHouse 中的所有代码都使用 sanitizer 构建和测试。ASAN、MSAN、TSAN 和 UBSAN 构建针对每次提交和 PR 运行,因此 Rust 代码需要达到相同的标准。在解决了一些初期问题后,我们选择直接在 Rust 中启用 sanitizer,而不是依赖外部工具。然而,Rust 的 sanitizer 支持当前依赖于不稳定的编译器特性,这些特性仅在 nightly 工具链上可用。因此,为 Rust 采用 sanitizer 意味着我们必须使用 nightly Rust 版本来进行涉及 Rust 代码的所有构建。

我们遇到的第二个问题是当 Cargo 或 GitHub 尝试获取 crate 时出现的间歇性网络故障,这与我们在构建过程中避免依赖外部服务的更广泛政策相冲突。

在我们的规模下,这变得不仅仅是一个小麻烦。我们每天运行数千次构建,因此即使是很低的故障率也会转化为稳定的中断流,需要进行调查和修复。这促使我们将依赖项获取视为基础设施问题,而不是瞬态边缘情况。

为了解决这个问题,我们使用cargo-local-registry将所有 crate 及其依赖项作为一个子模块进行 vendored,并配合自定义的config.toml设置和标志来:

  • 禁用依赖项的在线获取
  • 改用 vendored 的源代码

这种方法的一个后果是某些依赖项被绑定到特定的rustc版本,特别是对于 sanitizer 构建。因此,我们被迫使用与 vendored 依赖项集匹配的编译器版本。

作为 vendoring 依赖项工作的一部分,我们引入了一个包含所有 crate 的工作区。这使我们能够统一依赖项解析,同时简化配置和构建管理。

您可以在另一篇博客文章中找到我们处理 Rust 大约一年的时间的总结。

集成 Delta Lake

尽管我们已经多次迭代和改进 Rust 集成,但以前涵盖所有用例的简化方法并不能直接用于集成delta-kernel-rs

构建 crate

为了集成delta-kernel-rs,我们不得不放弃以前的方法,将其构建为一个独立的包,或者更准确地说,作为它自己的工作区。

以前,当我们只有几个小的、简单的 crate 时,我们通过将它们的源代码复制到构建目录并从那里编译来处理构建。这有两个目的。首先,它避免了 Cargo 缓存冲突。在我们之前的方法中,我们将 crate 源代码复制到共享构建目录中,以注入自定义的 Cargo 配置。这导致多个 crate 和工作区重用相同的目标目录,导致 Cargo 错误地重用或使工件无效,并触发不必要的重建。其次,它提供了一种直接的方法,通过为每个构建生成自定义的Cargo.toml来注入不同的构建配置。

这种方法不适用于像delta-kernel-rs这样的项目。我们关心的 crate(ffi及其依赖项)依赖于基于路径的指向父目录的链接,并引用同一仓库中的其他 crate。与我们以前自包含且不引用其他 crate 的 crate 不同,ffi具有与仓库布局紧密耦合的内部关系。复制源会破坏这些假设,并要求对项目进行深度重组。

取而代之的是,我们现在在原地构建项目,保留其原始的工作区结构。我们不复制源或修改上游文件,而是为每个构建配置生成一个单一的Cargo.toml,并通过 Cargo 的--config标志传递它。这允许我们在保持源代码树完整的同时控制构建设置。

然而,这一变化重新引入了当多个工作区构建到同一目标目录时出现的缓存冲突的原始问题。我们没有在 Cargo 级别解决这个问题,而是直接在 Corrosion 中处理它。在实践中,这意味着 Corrosion 现在隔离每个工作区或配置的构建工件,防止跨工作区缓存干扰并消除不必要的重建。

OpenSSL 依赖

在其依赖关系图的深处,delta-kernel-rs引入了opensslcrate,而后者又链接到系统安装的 OpenSSL 动态库。这与我们避免系统依赖的要求冲突。

注意:虽然理论上可以通过切换到rustls来避免 OpenSSL,但在实践中,这对我们不起作用。使用带有rustlsreqwest会引入构建失败,因为aws-lc-sys链接到系统库,而使用native-tls的尝试也证明不成功。

经过几次迭代,我们不再试图仅通过 Cargo 标志来控制这一点,而是通过 Corrosion 正确配置它。这使我们能够链接到我们自己静态构建的 OpenSSL 库,并确保 CMake 正确解析依赖项链。虽然这种设置在大多数情况下工作可靠,但它需要 Cargo 配置和 CMake 集成之间的仔细协调。

损坏的交叉编译

在使 crate 构建并集成之后,我们发现交叉编译失败了。根本原因相对简单:Cargo 试图构建引用目标环境中不可用的系统库的动态库。

解决方法是将构建限制为仅staticlib,而不是使用默认的 crate 类型。这避免了链接到系统提供的动态库,并确保了交叉编译场景中的兼容性。

在此过程中,我们发现了一个 Cargo 错误(现已修复),即在命令行上多次指定--crate-type=staticlib会导致增量构建中断。

由于缺少 sanitizer 符号导致的随机 CI 失败

这是我们遇到的另一个问题,与 crate 本身无关,并且发生在代码已经集成并在 CI 中运行之后。在一些构建更改之后,我们开始看到关于缺少 ASAN 符号的莫名其妙的链接器错误。

这是意料之外的,因为这些构建未启用 sanitizer。内部对象没有明显的原因依赖 ASAN。经过几轮尝试和错误后,我们将问题追溯到sccache。然而,在本地重现问题的尝试未成功,因为构建标志的更改导致不同的缓存键。

sccache从 0.7.7 升级到 0.10.0 最初似乎解决了这个问题(以及几个补丁 [1][2]),尽管根本原因仍不清楚。几周后,问题再次出现,此后我们禁用了sccache。目前,我们没有使用任何包装器来缓存 Rust 构建。

当前状态

一方面,事情正在顺利进行,这很好。请记住,至少对我个人而言,我在调试和设置 Rust 构建上花费的精力是阅读 Rust 代码的 20 到 50 倍,更不用说编写 Rust 代码了,所以最重要的部分已经就位。

另一方面,有几件事无法正常工作。

  • 该 crate 的内存 sanitizer 构建被禁用。链接使用 MSAN 构建的ring时存在问题。解决这个问题需要理解嵌套依赖项的嵌套依赖项是如何构建的,我宁愿专注于移除它,而不是正确构建它。
  • 通常,在构建 Rust crate 时,我们不尊重 ClickHouse 使用的所有 CMake 选项。因此,Cargo 可能会向库中引入我们希望避免的指令集。例如,我们使用NO_ARMV81_OR_HIGHER来禁用较新的 ARM 指令并支持较旧的标准。问题是ring假设 Neon SIMD 指令始终可用。

如果我们能够向delta-lake-ffi传递我们自己的 S3 客户端,并将请求委托给外部,那么所有这三个问题都将得到解决。ClickHouse 已经有一个复杂的网络栈,具有重试、日志记录、事件跟踪和监控功能,因此在理想设置中,我们将能够完全选择退出 Rust 中的网络访问。这将消除依赖项和相关联的构建复杂性。

在实践中,当跨多个有主见的构建系统工作时,发现这类问题是一种常态,并且通常需要深入研究它们的内部。更广泛地说,我们发现 Rust 组合依赖项的方法比 C++ 引入了更多的复杂性,使得集成和对构建的控制变得更具挑战性。

回馈生态系统

在 ClickHouse 部署中常见的规模下运行,会暴露出在较小或要求较低的环境中很少出现的边缘情况和性能瓶颈。针对真实客户工作负载的测试揭示了 Rust kernel 中的几个限制,需要针对性的修复。我们没有维护长期存在的分支或内部补丁,而是优先考虑在可能的情况下将这些改进贡献给上游,确保它们不仅使 ClickHouse 受益,也使更广泛的生态系统受益。

值得注意的贡献包括:

  • 改进的日志记录灵活性用于调试性能问题。在具有大量元数据文件的环境中,我们观察到源自 Delta kernel 内部的查询性能缓慢,由于静态日志初始化,对执行的可见性有限。我们贡献了增强功能,允许在运行时动态配置日志记录,从而在生产系统中实现更有效的故障排除和操作内省。
  • 异步元数据处理以提高可扩展性。我们识别了同步处理元数据文件的瓶颈,特别是在具有高元数据基数的表中。为了解决这个问题,我们引入了更改以支持异步处理,通过修改 FFI 接口以传递句柄而不是引用。这允许元数据操作(包括对象存储读取)在单线程回调之外并行化,从而显著提高了大规模元数据文件的读取性能。

展望未来

虽然我们对 Rust kernel 及其与 ClickHouse 集成的当前状态非常满意,但在大规模操作和构建生产级工作流时,仍然存在许多差距。

最直接的限制之一是无法通过 Rust kernel 创建空的 Delta Lake 表。今天,ClickHouse 可以附加到现有的 Delta 表,推断其模式,并通过 ClickHouse 表使其可查询,例如:

CREATETABLEhits_deltaENGINE=DeltaLake('https://datasets-documentation.s3.amazonaws.com/lake_formats/delta_lake/');SELECTcityHash64(URL),count()AScntFROMhits_deltaGROUPBYcityHash64(URL)ORDERBYcntDESCLIMIT5;

然而,它无法初始化一个新表并在对象存储上物化相应的 Delta 日志和元数据。解决这个问题将显著提高可用性,允许用户直接从 ClickHouse 定义表,并使它们从一开始就由 Delta Lake 支持。这是我们打算贡献的一个领域,以实现更完整、更双向的集成。

前面描述的 Delta Kernel 中的更改数据馈送支持提供了表版本之间的行级更改。展望未来,这将为在 ClickPipes 中基于 Delta Lake 数据构建面向 CDC 的工作流提供基础。

在此基础之上,有一条清晰的路径可以改进可用性和内省。将表历史、提交时间线和版本元数据作为原生系统表在 ClickHouse 中公开,将使理解数据演变、调试管道和自信地操作增量工作负载变得更加容易。

结论

将 Rust Delta Kernel 集成到 ClickHouse 代表了我们在处理开放表格式方面的转变,从定制实现转向共享的、定义良好的抽象。这使我们能够加速功能开发,减少维护开销,并专注于最重要的事情:在大规模下提供高性能分析。同时,处理真实世界的工作负载强化了这样的观点:这些集成的强大程度取决于其周围的生态系统。通过将改进贡献给上游并继续缩小剩余的差距,我们希望塑造一个更强大、更可互操作的数据湖生态系统。随着这项工作的进展,我们期望 ClickHouse 和 Delta Lake 的结合将为批处理和实时分析工作负载提供一个日益无缝的基础。

http://www.cnnetsun.cn/news/2547040.html

相关文章:

  • 小微团队如何利用Taotoken管理多个项目的AI成本
  • AI Agent Harness Engineering 模型压缩技术:让智能体在资源受限设备上高效运行
  • 在Ubuntu 22.04上从零部署nnUNet_v2:一个医学影像研究生的踩坑与填坑实录
  • 5分钟拯救你的B站收藏:m4s缓存视频无损转换实战
  • 为什么92.7%的企业漏检DeepSeek生成的隐性偏见内容?3类高危prompt绕过案例首次公开
  • 告警风暴压垮值班工程师?DeepSeek 6.3+告警收敛策略全拆解,含Prometheus+Alertmanager联调秘钥
  • 【面试必备】Java面向对象三分钟速通:封装、继承、多态,这一篇就够了
  • 交叉拟合与Neyman正交性:驯服机器学习因果推断中的偏差
  • 老Mac焕新秘籍:3个步骤让你的旧设备运行最新macOS系统
  • 如何永久保存你的微信聊天记忆?WeChatMsg完整解决方案揭秘
  • 2026告别水印烦恼!免费图片去水印保姆级教程,从微信小程序到手机App一看就会
  • 人机协作新范式:盘点2026年当红之选的的AI论文写作软件
  • 设计工作文档版本迭代管理程序,规整多版文件,避免办公文件混乱重复存储。
  • 编写职场人情往来收支平衡管理程序,统计礼尚往来,合理规划职场社交成本。
  • FPGA加速SVM量子态判别:5.74纳秒低延迟与8位量化硬件实现
  • 【数据分析】基于matlab智慧城市温度与湿度分析系统【含Matlab源码 15555期】
  • 长期使用 Taotoken Token Plan 套餐的成本控制效果观察
  • Label Studio:一站式数据标注与AI模型训练完整指南
  • Nodejs后端服务集成Taotoken多模型API的实践路径
  • PICO Unity APK闪退的五大根因与工程化排查指南
  • 灾变瞬间生成人员分布图,为抢险决策提供可靠依据 ——视频孪生智能态势研判矿山抢险决策技术方案
  • 2026最权威AI论文写作工具榜单:这些被高校和导师悄悄推荐的软件你还没用?
  • 具身智能场景优先级矩阵
  • 【MySQL全面教学】MySQL多表查询与JOIN Day6(2026年)
  • 【企业级落地】使用 Midscene.js 自动化生成并导出带截图的详尽测试/运行报告
  • PotPlayer字幕翻译插件:5步实现免费自动化双语字幕体验
  • 3分钟永久激活IDM:开源脚本让下载加速无限制
  • 独立开发者如何利用 Token Plan 套餐应对项目周期性的用量高峰
  • Mermaid在线编辑器:如何用5分钟创建专业级技术图表
  • Zotero重复条目合并终极方案:3分钟彻底清理文献库的完整指南