2

2022-17: 新轮子 hdfs-sys & hdrs

 2 years ago
source link: https://xuanwo.io/reports/2022-17/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

2022-17: 新轮子 hdfs-sys & hdrs

随着 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 这两个的库~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK