#key-value-store #concurrency #embedded

更快-rs

微软研究院FASTER的Rust封装

19个不稳定版本

0.11.0 2020年9月30日
0.10.0 2019年11月3日
0.9.0 2019年9月18日
0.8.5 2019年7月13日

#422 in 编码

Download history 1/week @ 2024-03-09 86/week @ 2024-03-30 21/week @ 2024-04-06

每月52次下载

MIT 协议

1.5MB
32K SLoC

C# 27K SLoC // 0.2% comments C++ 4K SLoC // 0.1% comments Rust 590 SLoC // 0.0% comments Visual Studio Solution 277 SLoC Visual Studio Project 169 SLoC

License Cargo Build Status

实验性的FASTER Rust封装

[dependencies]
faster-rs = "0.11.0"

包括FASTER的实验性C接口。这是一个通用的FASTER实现,允许存储任意键值对。此封装只关注Linux支持。

安装依赖项(Ubuntu)

$ add-apt-repository -y ppa:ubuntu-toolchain-r/test
$ apt update
$ apt install -y g++-7 libaio-dev uuid-dev libtbb-dev

请确保也克隆子模块,这最好通过使用带有 git clone --recurse-submodules 的方式克隆来完成。

接口

此封装试图保持与原始FASTER设计一致,通过提供一个与原始C++版本类似的接口。用户可以定义自己的键值类型,并提供自定义的读写修改操作逻辑。

读取、更新和读写操作都需要一个单调的序列号来形成FASTER将持久化的操作序列。读取操作需要一个序列号,以便在CPR检查点边界之前,FASTER保证在该点之前的读取没有访问检查点之后的数据更新。如果持久化不重要,可以将序列号安全地设置为 1 以适用于所有操作(如上述示例中所示)。

以下示例中提供了有关检查点和恢复的更多信息。

基本示例

以下示例展示了创建FASTER键值存储以及对 u64 值的基本操作。

通过运行 cargo run --example basic 来尝试它。

extern crate faster_rs;

use faster_rs::{status, FasterKv};
use std::sync::mpsc::Receiver;

fn main() {
    // Create a Key-Value Store
    let store = FasterKv::default();
    let key0: u64 = 1;
    let value0: u64 = 1000;
    let modification: u64 = 5;

    // Upsert
    for i in 0..1000 {
        let upsert = store.upsert(&(key0 + i), &(value0 + i), i);
        assert!(upsert == status::OK || upsert == status::PENDING);
    }

    // Read-Modify-Write
    for i in 0..1000 {
        let rmw = store.rmw(&(key0 + i), &(5 as u64), i + 1000);
        assert!(rmw == status::OK || rmw == status::PENDING);
    }

    assert!(store.size() > 0);

    // Read
    for i in 0..1000 {
        // Note: need to provide type annotation for the Receiver
        let (read, recv): (u8, Receiver<u64>) = store.read(&(key0 + i), i);
        assert!(read == status::OK || read == status::PENDING);
        let val = recv.recv().unwrap();
        assert_eq!(val, value0 + i + modification);
        println!("Key: {}, Value: {}", key0 + i, val);
    }

    // Clear used storage
    match store.clean_storage() {
        Ok(()) => {}
        Err(_err) => panic!("Unable to clear FASTER directory"),
    }
}

使用自定义键

结构体 可以使用 serde 进行(反)序列化,并作为键支持。为了使用这样的 结构体,需要从 serde-derive 派生 SerializableDeserializable 的实现。所有实现这两个特质的类型将自动实现 FasterKey,从而可以用作键。

以下示例展示了如何将基本结构体用作键。可以通过运行以下命令进行测试:cargo run --example custom_keys

extern crate faster_rs;
extern crate serde_derive;

use faster_rs::{status, FasterKv};
use serde_derive::{Deserialize, Serialize};
use std::sync::mpsc::Receiver;

// Note: Debug annotation is just for printing later
#[derive(Serialize, Deserialize, Debug)]
struct MyKey {
    foo: String,
    bar: String,
}

fn main() {
    // Create a Key-Value Store
    let store = FasterKv::default();
    let key = MyKey {
        foo: String::from("Hello"),
        bar: String::from("World"),
    };
    let value: u64 = 1;

    // Upsert
    let upsert = store.upsert(&key, &value, 1);
    assert!(upsert == status::OK || upsert == status::PENDING);

    assert!(store.size() > 0);

    // Note: need to provide type annotation for the Receiver
    let (read, recv): (u8, Receiver<u64>) = store.read(&key, 1);
    assert!(read == status::OK || read == status::PENDING);
    let val = recv.recv().unwrap();
    println!("Key: {:?}, Value: {}", key, val);

    // Clear used storage
    match store.clean_storage() {
        Ok(()) => {}
        Err(_err) => panic!("Unable to clear FASTER directory"),
    }
}

使用自定义值

结构体 可以使用 serde 进行(反)序列化,并作为值支持。为了使用这样的 结构体,需要从 serde-derive 派生 SerializableDeserializable 的实现。

为了在自定义类型上使用读写操作,还需要实现 FasterRmw 特质,该特质公开了一个 rmw() 函数。此函数可用于实现读写操作的定制逻辑。

以下示例展示了如何将基本结构体用作值。可以通过运行以下命令进行测试:cargo run --example custom_values

extern crate faster_rs;
extern crate serde_derive;

use faster_rs::{status, FasterKv};
use serde_derive::{Deserialize, Serialize};
use std::sync::mpsc::Receiver;

// Note: Debug annotation is just for printing later
#[derive(Serialize, Deserialize, Debug)]
struct MyValue {
    foo: String,
    bar: String,
}

fn main() {
    // Create a Key-Value Store
    let store = FasterKv::default();
    let key: u64 = 1;
    let value = MyValue {
        foo: String::from("Hello"),
        bar: String::from("World"),
    };

    // Upsert
    let upsert = store.upsert(&key, &value, 1);
    assert!(upsert == status::OK || upsert == status::PENDING);

    assert!(store.size() > 0);

    // Note: need to provide type annotation for the Receiver
    let (read, recv): (u8, Receiver<MyValue>) = store.read(&key, 1);
    assert!(read == status::OK || read == status::PENDING);
    let val = recv.recv().unwrap();
    println!("Key: {}, Value: {:?}", key, val);

    // Clear used storage
    match store.clean_storage() {
        Ok(()) => {}
        Err(_err) => panic!("Unable to clear FASTER directory"),
    }
}

FasterRmw 的内置实现

几种类型已经实现了 FasterRmw 并提供了读写逻辑。这些实现可以在 src/impls.rs 中找到,但它们的 RMW 逻辑如下所述

  • 数值类型使用加法
  • 布尔型和字符型用新值替换旧值
  • 字符串和 Vec 添加修改
  • HashSet 执行并集操作

检查点和恢复

FASTER 的容错性由 并发前缀恢复(CPR)提供。这保证了操作的语义。

如果操作 X 被持久化,那么 X 之前的所有操作也将被持久化(并且 X 之后的操作不会被持久化)。

使用 checkpoint() 函数进行操作持久化。定期调用 refresh() 函数也很重要,因为它是线程向系统报告进度的方式。

各个会话(访问 FASTER 的线程)将持久化不同数量的操作。最近持久化的序列号可以通过 continue_session() 函数返回,并允许推理哪些操作被(未被)持久化。这也是线程在恢复后应继续提供操作的序列号。

examples/sum_store_single.rs 中可以找到一个很好的检查点/恢复的示例。自己试试吧!

$ cargo run --example sum_store_single -- populate
$ cargo run --example sum_store_single -- recover <checkpoint-token>

基准测试

可以对FASTER的C包装器和Rust包装器进行基准测试。要构建和运行C基准测试,请遵循微软的说明,链接在这里:这里,然后运行二进制文件 benchmark-c。它采用与原始基准测试相同的参数和输入格式。

运行Rust基准测试

基准测试以一个单独的crate形式编写在benchmark目录中。在目录内运行 cargo run --release -- help 来查看可用的选项。

基准测试包括两个子命令 cargo run --release -- [process-ycsb|run]

  • process-ycsb 将获取提供的YCSB文件的输出,并生成一个只包含8字节键的输出文件,格式符合Rust和C基准测试的预期。
  • run 实际上使用提供的加载和运行密钥执行基准测试。工作负载和线程数可以自定义。

基准测试与原始的C++实现非常相似,因此最好遵循他们的说明来设置YCSB。

依赖关系

~0.9–4MB
~78K SLoC