2 个版本
0.1.3 | 2021 年 3 月 3 日 |
---|---|
0.1.2 | 2021 年 3 月 1 日 |
0.1.1 |
|
0.1.0 |
|
#427 在 并发 中
125KB
1.5K SLoC
lfchring-rs
本 crate 实现了一个并发、无锁的 一致哈希环 数据结构。
特性
本节记录了数据结构实现的特性。
有关支持的 crate features
的信息可以在本页面的文档中找到。
虚拟节点
该 crate 包括对每个环节点配置一定数量 虚拟节点 的支持(即每个节点可以在环上映射和放置多次,且这些次数的数量是可配置的),以提高负载均衡场景中的效率。
每个环节点的虚拟节点数量只能在数据结构创建时配置。
复制
此外,该 crate 包括对跟踪密钥复制的支持。这意味着,如果提供了调用者选择的复制因子,则在一致哈希环数据结构中查找密钥会报告所有分配了当前密钥副本的不同的环节点。请注意,一致哈希中的连续虚拟节点可能属于同一个不同的环节点;此 crate 确保将密钥分配给与(可配置的)复制因子相等的 不同 环节点数量(只要它们可用),必要时通过跳过一些虚拟节点来实现。
复制因子只能在数据结构创建时配置。
原始目的
本 crate 的主要目的是在多线程环境中正确工作,即多个线程可以访问一致哈希环数据结构并对其进行操作的情况下。更具体地说,它主要是为了使用情况而设计和实现的,其中大量读取线程可能需要访问它,而写入操作较少(如果您不确定“读取”和“写入”操作可能指什么,请不要担心,请继续阅读)。尽管如此,该数据结构也可以在单线程环境中同样良好地使用。
环形数据结构通过导出的 [HashRing<N, H>
] 类型表示,该类型在 Node
和 Hasher
上进行泛型。虚拟节点通过导出的 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::new
或 HashRing::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::iter
和Iter
)。 - 写入操作被认为是对环形本身进行有效更改的操作。常见示例是
HashRing::insert
和HashRing::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。
这种方法的替代方案是使用 Mutex
或 RwLock
,但该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