随着 S3 等服务的广泛流行,HDFS 已经不再时髦。但是仍然有很多用户在使用 HDFS 作为存储底座,因此 DatabendOpenDAL 提出了支持 HDFS 的需求OpenDAL 旨在成为链接所有存储服务的 Open Data Access Layer,HDFS 显然是需要支持的服务之一,(而且 Databend 是 OpenDAL 最大的用户)

实现方案

HDFS 是用 Java 开发的服务,目前社区提供了如下对接方案:

  • Java API: 引入 hdfs 发行版中提供的 Jar 包即可,这也是绝大多数 HDFS 用户使用的方式
  • C API libhdfs: HDFS 基于 JNI 提供了 C binding,用户链接 libhdfs.so 即可反向调用 Java 函数
    • hdfs-rs: 最近更新时间 2015 年,没有集成测试
    • fs-hdfs:fork 自 hdfs-rs,没有集成测试,只支持 hadoop 2.7.3
    • rust-hdfs:最近更新时间 2020 年,没有集成测试
  • WebHDFS REST API: HDFS 基于 HTTP 封装一套 API 接口,用户只需要 HTTP Client 即可访问
  • libhdfs3Pivotal 基于 HDFS RPC 接口开发的 C/CPP Client,后来贡献给了 Clickhouse 社区。

直接对接 Java API 需要使用 JNI,Rust 社区的选择包括 jni-rsj4rs。在尝试一个下午之后发现基于 JNI 对接相比于使用 libhdfs 没有什么明显的好处,反而带来了不少维护上的麻烦(有不少 conversion 都要自己处理,还要自行解决启动 JVM 之类的问题),所以放弃了这个方案。

WebHDFS 的问题在于要求 HDFS 集群启用 dfs.webhdfs.enabled 配置,而且通过 HTTP 传输大于 10MB 的文件时存在着一些已知的性能问题:WebHDFS vs Native Performance

libhdfs3 被 ClickHouse 与 datafusion 采用,是目前应用较为广泛的方式,但是这个库要求使用 RPC 来访问 HDFS,引入了一套额外的依赖,更严重的是它会破坏一些用户的预期:因为有些用户业务使用 hdfs 的方式都是提供一个自己的 Jar 包,在里面做了一些封装和逻辑供外部的客户端加载,显式地调用 RPC 接口可能会让他们的一些内部逻辑失效。

因此 OpenDAL 计划采用原生的 C API libhdfs 来对接 HDFS,但是社区提供的选择我都不太满意,我想要:

  • 完整的 API 支持:兼容所有 HDFS API 版本,使得 OpenDAL 对接 HDFS 时不需要操心兼容性问题
  • 良好的抽象分层:将 C bindings 与 Rust API 剥离开,当 Rust API 无法满足需求时用户能够自行封装而不需要重新造轮子
  • 完善的集成测试:C bindings 需要与各个主流 HDFS 版本进行链接并测试 API,而 Rust API 更需要完整的集成测试确保 API 正常且不出现内存泄露
  • 易用的接口:屏蔽 HDFS 的内部细节,对外暴露更符合 Rust 习惯的 Fileio::Read 等接口,降低用户学习成本

既然社区没有满足我需求的实现,那就自己来造一个吧~

设计

C bindings 与 Rust API 分开是 Rust 中比较通用的做法,比如:

因此我计划拆分出 hdfs-syshdrs 两个部分,其中:

  • hdfs-sys 负责暴露 HDFS C API 的接口并链接 libhdfs
  • hdrs 则负责在 hdfs-sys unsafe API 基础上封装更符合 Rust 风格的 safe API

hdfs-sys 实现

最简单的方案是使用 bindgen,只需要有 hdfs.h 就能自动生成出所有的 Rust 接口。但是 HDFS 是一个开发跨度长达十数年的服务,部署什么版本的用户都有可能,我们在实现 hdfs-sys 的时候需要将 API 的兼容性考虑在内,因此我模仿 clang-sys 实现了一套 API 的兼容逻辑。

HDFS 的 C API 都是向前兼容的,不会移除或者修改函数,每次新增函数时都会更新 minor 版本号。基于这一核心原则,我从 hadoop 代码库中 checkout 出来从 hadoop 2.2 至 hadoop 3.3 这 13 个版本的 hdfs.h 头文件,以 hadoop 2.2 作为基准,逐个版本的对比差异,并为每个版本赋予一个 feature flag:

#[cfg(feature = "hdfs_2_2")]
mod hdfs_2_2;
#[cfg(feature = "hdfs_2_2")]
pub use hdfs_2_2::*;
#[cfg(feature = "hdfs_2_3")]
mod hdfs_2_3;
#[cfg(feature = "hdfs_2_3")]
pub use hdfs_2_3::*;
#[cfg(feature = "hdfs_2_4")]
mod hdfs_2_4;
#[cfg(feature = "hdfs_2_4")]
pub use hdfs_2_4::*;

换言之,当用户启用 hdfs_2_2 时,他就可以使用 2.2 版本暴露的 API,当用户启用 hdfs_2_3 时,他就可以使用 2.2 和 2.3 版本所有的 API。测试集的覆盖也使用了一样的原则:

#[test]
#[cfg(feature = "hdfs_2_3")]
fn test_hdfs_abi_2_3() {
    test_hdfs_abi_2_2();

    let _ = hadoopRzOptionsAlloc;
    let _ = hadoopRzOptionsSetSkipChecksum;
    let _ = hadoopRzOptionsSetByteBufferPool;
    let _ = hadoopRzOptionsFree;
    let _ = hadoopReadZero;
    let _ = hadoopRzBufferLength;
    let _ = hadoopRzBufferGet;
    let _ = hadoopRzBufferFree;
}

考虑到实际情况,hdfs-sys 并没有去支持所有的 hadoop 版本:

  • hadoop 1 实际采用量很低且不再维护新的 patch 版本,所以不予支持
  • hadoop 2.2 是 hadoop 2 首个 stable release,因此 2.1 (只有 beta) 和 2.0(只有 alpha)都不予支持
  • hadoop 2.2 理论上能够兼容现行的所有 hadoop 环境,因此将其作为默认的 feature

有了 hdfs-sys 之后,我们就可以放心使用 API 而不需要担心版本的问题了~

hdrs 实现

就像 hdfs-sys 说的那样:Work with these bindings directly is boring and error proven。直接使用 hdfs-sys 意味着大量的 unsafe 代码,需要跟 raw pointer 打交道,我们需要一层更加高级的封装,比如:

pub fn stat(&self, path: &str) -> io::Result<Metadata> {
    let hfi = unsafe {
        let p = CString::new(path)?;
        hdfsGetPathInfo(self.fs, p.as_ptr())
    };

    if hfi.is_null() {
        return Err(io::Error::last_os_error());
    }

    // Safety: hfi must be valid
    let fi = unsafe { Metadata::from(*hfi) };

    // Make sure hfi has been freed.
    unsafe { hdfsFreeFileInfo(hfi, 1) };

    Ok(fi)
}

这样用户可以避免:

  • 处理 StringCString 的转换
  • 跟 unsafe 打交道
  • 处理 raw pointer
  • 处理动态分配结构体的手动 free
  • 从 errno 到 io::Error 的转换

hdrs 为绝大多数常用的接口进行了此类的封装并暴露了与 std::fs 类似的接口。

除此以外,hdrs 还通过 Github Action 进行了与 hadoop 2.10.1,3.2.3,3.3.2 等版本的集成测试,保证 hdrs 的核心 API 在真实的用户环境中也能正常工作。

总结

回顾一下之前聊到的需求:

  • 完整的 API 支持
  • 良好的抽象分层
  • 完善的集成测试
  • 易用的接口

hdfs-sys 支持了从 hadoop 2.2 开始到最新版本所有的 API 接口,满足了完整的 API 支持的需求。而 hdfs-sys 与 hdrs 的抽象分层使得用户可以根据自己的需求选择依赖 hdfs-sys 或者 hdrs。同时 hdfs-sys 和 hdrs 都通过 Github Actions 进行了集成测试,确保核心的逻辑工作正常以及不会出现意外的 break。hdrs 在 hdfs-sys 的基础上封装了类似 std::fs 的接口,尽可能降低用户的学习成本。

接下来 OpenDAL 将会基于 hdrs 展开 HDFS 的支持工作,并通过真实的需求来进一步完善 hdfs-sys 与 hdrs 这两个的库~

参考资料