Apache OpenDAL 在最近的版本中增加了 Adapter 的概念,将 OpenDAL 支持的服务范围从传统的文件系统和对象存储扩展到了诸如 RedisMoka 等 Key-Value 存储服务上。今天这篇周报就介绍一下我们为什么要增加这个抽象,它解决了哪些问题,以及社区现在的进展。

背景

OpenDAL 旨在自由,无痛,高效的访问数据,其中自由意味着 OpenDAL 可以以相同的方式访问不同的存储服务。为了实现这一点,OpenDAL 实现了不少服务的支持,比如 azblob,fs,ftp,gcp,hdfs,ipfs,s3 等等。这些服务虽然接口和实现各有各的差异,但是他们共性是都有着类 POSIX 文件系统的抽象,OpenDAL 不需要额外的抽象层就能访问其中的数据。换言之,OpenDAL 对存储底层的操作对用户来说是透明的,离开 OpenDAL 用户也可以正常访问这些数据。基于这样的设计, OpenDAL 没有考虑过支持 Key-Value 服务:因为我们无法从 Key-Value 服务中读取到任何有意义的数据,其存储的数据结构高度依赖于应用的具体实现。

但是在后续社区的多次讨论中,我们逐渐发现在 Key-Value 服务的基础上定义一个自己的存储结构,并用来实现 OpenDAL 的接口同样是有价值的。Databend 提出了这样的需求:他希望 OpenDAL 能够实现一层缓存,可以将数据存储在不同的缓存服务上,比如内存,本地文件系统和 Redis 上,他们能够设置一定的淘汰策略,自动过期。换言之,Databend 希望 OpenDAL 能够支持易失性数据的存储,他们会被用作缓存和临时的数据存储,用户不会通过其他方式(比如 awscli)来访问它。在这种场景下, Key-Value 服务是一个黑箱,用户只通过 OpenDAL 来访问。既然不需要暴露给外部访问,OpenDAL 完全可以自行决定在 Key-Value 服务上的存储结构。

设计

过去 OpenDAL 的实现路径是非常简单的:

impl Accessor for s3::Backend { .. }

每个服务提供一个 Backend 结构体并实现 Accessor 接口即可。

但是 Key-Value 服务的接口都是非常相似的,为了避免重复实现相似逻辑,OpenDAL 引入了 Adapter:

impl<S> Accessor for kv::Backend<S> where S: kv::Adapter { .. }

impl kv::Adapter for redis::Adapter { .. }

OpenDAL 为所有的 kv::Backend<S: kv::Adapter> 实现了 Accessor,每个具体的 Key-Value 服务只需要实现 kv::Adapter 即可:

#[async_trait]
pub trait Adapter: Send + Sync + Debug + Clone + 'static {
    /// Return the medata of this key value accessor.
    fn metadata(&self) -> Metadata;

    /// Fetch the next id.
    ///
    /// - Returning id should never be zero.
    /// - Returning id should never be reused.
    async fn next_id(&self) -> Result<u64>;

    /// Get a key from service.
    ///
    /// - return `Ok(None)` if this key is not exist.
    async fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>>;

    /// Set a key into service.
    async fn set(&self, key: &[u8], value: &[u8]) -> Result<()>;

    /// Scan a range of keys.
    ///
    /// If `scan` is not supported, we will disable the block split
    /// logic. Only one block will be store for one file.
    async fn scan(&self, prefix: &[u8]) -> Result<KeyStreamer> {
        let _ = prefix;

        Err(io::Error::new(
            io::ErrorKind::Unsupported,
            anyhow::anyhow!("scan operation is not supported"),
        ))
    }

    /// Delete a key from service.
    ///
    /// - return `Ok(())` even if this key is not exist.
    async fn delete(&self, key: &[u8]) -> Result<()>;
}

这里的 getsetdelete 都是绝大多数 Key-Value 服务支持的接口,此外 kv::Adapter 还会根据是否支持 scan 来决定内部的具体实现。具体的存储格式目前还没有完全稳定下来,这里就不展开介绍了,感兴趣的同学可以直接查看代码~

进展

kv::Adapter 的基础上,OpenDAL 实现了以下 Key-Value 服务的支持:

  • memory: 基于 BtreeMap 实现的 Service
  • moka: 基于高性能缓存库 moka 实现的 Service
  • redis: 基于 redis 实现的 Service

接下来 Databend 社区会尝试使用这些 Service 作为缓存层来加速热点查询,并根据生产中的反馈来改进 OpenDAL 的实现~

社区还计划增加以下 Key-Value 服务的支持:

欢迎感兴趣的同学尝试~