这期周报分享一个 Apache OpenDAL 的低垂果实:通过去除额外的复制开销将 OpenDAL KV 性能提升 1000%!
背景
OpenDAL 在很久之前增加了 Kv Adapter:通过将 Key-Value 存储后端的常用 GET/SET 操作抽象出来以大幅度简化对接一个 kv service 的成本。维护者需要简单的实现 GET/SET/DELETE 等函数就能够对接形如 Redis/HashMap 这样的存储后端:
#[async_trait]
pub trait Adapter: Send + Sync + Debug + Unpin + 'static {
async fn get(&self, path: &str) -> Result<Option<Vec<u8>>>;
async fn set(&self, path: &str, value: &[u8]) -> Result<()>;
async fn delete(&self, path: &str) -> Result<()>;
}
但是对于纯内存的数据结构来说,Kv Adapter 并不是零开销的:
- 写入的时候需要对数据进行额外的多次复制
- 读取时需要将所有的数据都复制出来
不仅如此,现在的 Kv Adapter 只能存储字节流,无法存储额外的 metadata,使得其使用场景受到了不应该的限制。
改进
Issue kv: Use Bytes as value to avoid copy between read/write 提出可以在 map 中存储 Bytes
而不是 Vec<u8>
来避免额外的复制,而 Alternative implementation for memory backend 则更进一步的指出我们可以在 map 中存储一个自定义的数据结构,里面可以附加上的 metadata。
结合以上两个思路,我提出了新的 Typed Kv Adapter:
#[derive(Debug, Clone)]
pub struct Value {
pub metadata: Metadata,
pub value: Bytes,
}
#[async_trait]
pub trait Adapter: Send + Sync + Debug + Unpin + 'static {
async fn get(&self, path: &str) -> Result<Option<Value>>;
async fn set(&self, path: &str, value: Value) -> Result<()>;
async fn delete(&self, path: &str) -> Result<()>;
}
在数据结构中引入自行设计的 Value
结构体,存储支持零开销复制的 Bytes
而不是 Vec<u8>
,使得我们的 Adapter 不需要额外的复制就能够读写数据。
迁移 moka
将一个 service 从 kv::Adapter
迁移至 typed_kv::Adapter
非常简单,我们只需要将其定义声明修改即可:
- inner: SegmentedCache<String, Vec<u8>>,
+ inner: SegmentedCache<String, typed_kv::Value>,
然后修改对应 Adapter 的接口,比如:
- async fn get(&self, path: &str) -> Result<Option<Vec<u8>>>
+ async fn get(&self, path: &str) -> Result<Option<typed_kv::Value>>
效果
在演示 PR 中,我将 moka 从 kv::Adapter
迁移到了 typed_kv::Adapter
。在所有的基准测试中,读写性能都有了显著提高。其中,最低的 read 提升了 10%
,最高的提升达到了 1519.8%
;write 部分受益于零开销实现的 Bytes
类型,在 service_moka_write_once/16.0 MiB
上取得了 166323%
的性能提升。
下一步计划
接下来,OpenDAL 将把所有的内存后端迁移到 typed_kv::Adapter
,并增加更多的 kv 后端支持。然后可以进行一次横向的 bytes 读写性能比较,看看在 OpenDAL 的标准使用场景下,看看哪个内存后端性能更强~