#consistent-hashing #ring #data-structures #lock-free

lfchring

支持虚拟节点和复制跟踪的并发、无锁一致哈希环数据结构实现

2 个版本

0.1.3 2021 年 3 月 3 日
0.1.2 2021 年 3 月 1 日
0.1.1 2021 年 3 月 1 日
0.1.0 2021 年 3 月 1 日

#427并发

Apache-2.0

125KB
1.5K SLoC

lfchring-rs

Crates.io docs.rs GitHub License deps.rs Build Status GitHub Workflow Status

本 crate 实现了一个并发、无锁的 一致哈希环 数据结构。

特性

本节记录了数据结构实现的特性。

有关支持的 crate features 的信息可以在本页面的文档中找到。

虚拟节点

该 crate 包括对每个环节点配置一定数量 虚拟节点 的支持(即每个节点可以在环上映射和放置多次,且这些次数的数量是可配置的),以提高负载均衡场景中的效率。

每个环节点的虚拟节点数量只能在数据结构创建时配置。

复制

此外,该 crate 包括对跟踪密钥复制的支持。这意味着,如果提供了调用者选择的复制因子,则在一致哈希环数据结构中查找密钥会报告所有分配了当前密钥副本的不同的环节点。请注意,一致哈希中的连续虚拟节点可能属于同一个不同的环节点;此 crate 确保将密钥分配给与(可配置的)复制因子相等的 不同 环节点数量(只要它们可用),必要时通过跳过一些虚拟节点来实现。

复制因子只能在数据结构创建时配置。

原始目的

本 crate 的主要目的是在多线程环境中正确工作,即多个线程可以访问一致哈希环数据结构并对其进行操作的情况下。更具体地说,它主要是为了使用情况而设计和实现的,其中大量读取线程可能需要访问它,而写入操作较少(如果您不确定“读取”和“写入”操作可能指什么,请不要担心,请继续阅读)。尽管如此,该数据结构也可以在单线程环境中同样良好地使用。


环形数据结构通过导出的 [HashRing<N, H>] 类型表示,该类型在 NodeHasher 上进行泛型。虚拟节点通过导出的 VirtualNode<N> 类型表示。

节点

任何类型都可以用作一致性哈希环的节点,只要它实现了 Node 特性。实现此特性需要一个方法 (Node::hashring_node_id),该方法允许将类型唯一地表示为一个字节数组。实现 Node 特性可以非常简单,如下所示

struct ExampleNode {
    various: u64,
    fields: Vec<i32>,
    // . . .
    unique_name: String,
}

impl Node for ExampleNode {
    fn hashring_node_id(&self) -> Cow<'_, [u8]> {
        Cow::Borrowed(&self.unique_name.as_bytes())
    }
}

struct StrNode(str);

impl Node for StrNode {
    fn hashring_node_id(&self) -> Cow<'_, [u8]> {
        Cow::Borrowed(self.0.as_bytes())
    }
}

请注意,Node 可以是无大小的,并且该包已经为以下类型提供了实现

  • 字符串
  • str
  • Vec<u8>
  • &[u8]
  • [u8]

Hasher

要将 Node 插入(或从)一致性哈希环中,它需要被哈希一次或多次(取决于每个 Node 配置的虚拟节点数)。然后根据它们的哈希摘要将产生的虚拟节点适当地“放置”在环上。

可以使用多种哈希算法。该包包括一个实现 [HashRing<N, H>] 的实现,该实现使用标准库的 DefaultHasher(通过 HashRing::newHashRing::with_nodes)构造)。然而,您也可以通过实现 Hasher 特性来提供自己的哈希算法实现。该特性定义了 Hasher::digest 方法,该方法提供一个输入字节数组,产生一个作为所有者 Vec<u8> 的哈希摘要,用于在一致性哈希环上以正确顺序放置 Node

不依赖于标准库的哈希机制是一种设计选择,允许使用 [HashRing<N, H>] 与产生比 u64 更长摘要的哈希函数。

特性 blake3-hash

通过启用blake3-hash存储库功能,可以提供使用BLAKE3加密哈希函数作为实现的Hasher,该函数在blake3存储库中实现,并可通过BLAKE3访问。

有关更多信息,请参阅Blake3Hasher类型的文档。

功能blake2b-hash

通过启用blake2b-hash存储库功能,可以获得基于BLAKE2b加密哈希函数和blake2b_simd存储库Hasher实现。

有关更多信息,请参阅Blake2bHasher类型的文档。

两种主要操作类型

环形上支持的所有操作可以分为两大类:“读取”和“写入”。

  • 读取操作需要访问存储在环形中的信息,但不会对环形本身进行更改。此类操作的最常见示例是根据一致性哈希算法查找键应分配到的位置(例如,参见HashRing::nodes_for_key)。另一个示例是通过迭代器遍历一致性哈希环形中包含的所有虚拟节点(例如,参见HashRing::iterIter)。
  • 写入操作被认为是对环形本身进行有效更改的操作。常见示例是HashRing::insertHashRing::remove方法,它们分别向一致性哈希环形插入新节点或删除现有节点。

示例

以下是一个基本的单线程示例,展示了其用法。

use std::sync::Arc;
use hex_literal::hex;
use lfchring::{HashRing, Node, VirtualNode, Vnid};

const VIRTUAL_NODES_PER_NODE: Vnid = 2;
const REPLICATION_FACTOR: u8 = 3;

// Create an empty ring (see the docs for further options on constructing a ring).
let ring = HashRing::new(VIRTUAL_NODES_PER_NODE, REPLICATION_FACTOR).unwrap();

// Insert three new Nodes.
let nodes: Vec<Arc<str>> = vec![Arc::from("Node1"), Arc::from("Node2"), Arc::from("Node3")];
ring.insert(&nodes).expect("hash collision when inserting 3 new Nodes");

assert_eq!(ring.len_nodes(), 3);
assert_eq!(ring.len_virtual_nodes(), 3 * VIRTUAL_NODES_PER_NODE as usize);

// Look up a key in the ring.
let key = hex!("232a8a941ee901c1");
let virtual_node_clone = ring.virtual_node_for_key(&key).unwrap();

// The Nodes that should own a replica for the particular key can also be found via:
let replica_owning_nodes = ring.nodes_for_key(&key).unwrap();
assert_eq!(replica_owning_nodes, virtual_node_clone.replica_owners());

// In this example, since the ring is populated with 3 Nodes and the replication factor is also
// equal to 3, all Nodes should be assigned with a replica of the key.
// However, the following assertion would fail, because of the order in which the replica
// owning Nodes are reported: they are reported according to the order in which their virtual
// nodes have been placed on the ring, rather than the order of the Nodes were inserted.
assert_ne!(nodes, replica_owning_nodes); // `ne` due to the order in which Nodes are reported!

实现说明

Arc

请注意,在多线程上下文中,使用HashRing<N, H>的用户应显式将其包装在Arc中,以便在线程之间共享。单线程上下文可以选择不这样做,以避免原子引用计数的开销;然而,整个存储库内部都在使用Arc

RCU-like同步

该crate的实现健壮性基于一种类似于RCU(读取-复制-更新)的技术,用于通过多个线程同步对一致哈希环数据结构的并发访问。这意味着该数据结构的实现不依赖于锁,而是通过原子地读取 [HashRing<N, H>] 内部的指针,复制数据,更新本地副本,然后将新地址原子地存储到指针中。实现所基于的原始操作属于 crossbeam_epoch crate。

这种方法的替代方案是使用 MutexRwLock,但该crate并没有采用这种方式。

垃圾回收

使用类似于 RCU 的技术对 [HashRing<N, H>] 进行并发读写操作会引发一个主要问题:当发生突变时,由 [HashRing<N, H>] 内部指向的底层数据结构不再需要,因此会被丢弃并立即释放。然而,可能仍有多个并发读取线程需要访问它。像Java和Go这样的垃圾回收语言通过依赖于其运行时的垃圾回收器来仅在没有任何线程可以访问它时清理它,从而简单地解决这个问题。Rust的轻量级运行时不包括垃圾回收器;它而是依赖于其独特的RAII(资源获取即初始化)系统。

为了解决这个问题,该crate基于 crossbeam_epoch crate,该crate实现了基于时代的内存回收。这也是 Guard 类型以及 [pin] 函数重新导出的地方,以便于使用。强烈建议任何考虑使用此crate的人阅读 crossbeam_epoch crate 的文档。

性能

该crate尚未进行适当的基准测试和评估。

非正式地,它看起来和感觉都很快,尤其是在多个并发读取线程和罕见的写入操作的情况下。

运行测试

$ RUST_LOG=debug cargo t --all-features -- --nocapture

生成文档

$ cargo doc --all-features --no-deps

许可证

根据Apache许可证第2版分发。

有关详细信息,请参阅所附的 LICENSE文件https://apache.ac.cn/licenses/LICENSE-2.0

依赖关系

~0.5–1.5MB
~34K SLoC