-
Notifications
You must be signed in to change notification settings - Fork 1
【Zig 日报】项目分享:Zeno--用 Zig 编写的高性能嵌入式键值存储引擎。 #316
Description
Zeno 是一款使用纯 Zig 语言编写的高性能、嵌入式键值存储引擎。它专为现代工作负载设计,优先考虑可预测的低延迟、零隐式内存分配和高效的分片并发。其名称(Node/节点)反映了支撑每项操作的核心索引和存储节点,请勿将其与 Node.js 混淆。
Zeno 最初是作为一个研究数据库存储内部原理和自适应基数树(ART)的学习实验。实验结果和性能表现非常出色,因此它演变成了一个独立的引擎。
🚀 核心特性
- 自适应基数树 (ART) 索引:O(k) 查找时间,通过 SIMD 优化的节点转换(Node4 到 Node256)。
- 分片并发:256 分片架构,通过顺序锁(seqlock)+ 标记指针(tagged-pointer)ART 实现无锁 GET。并发读取者互不阻塞;写入者按分片序列化。
- 零隐式分配:遵循严格的 Zig 实践,每个需要分配内存的函数都接受显式的
Allocator。 - 持久化存储:
- WAL (预写日志):批量异步持久化模式,实现高吞吐量写入。
- 快照:高效的流式快照备份与恢复。
- 结构化数值:通过
union(enum)支持复杂数值类型,确保严格的运行时类型安全。
📊 性能基准测试
Zeno 为速度而生。以下数据来自目前在 Ubuntu 24.04.4、AMD Ryzen 7 5700X、32GB DDR4 @ 3200MHz 环境下运行的基准测试套件。
测试方法:稳态测试使用 1,000 个轮转 Key,2,000 次预热迭代,100,000 次测量迭代。延迟列显示 p50/p99 分位数。扩展性测试每个配置运行 1,000,000 次操作。
点操作吞吐量
| 操作类型 | 吞吐量 | p50 | p99 |
|---|---|---|---|
| DB PUT (覆盖写, 稳态) | 14.75M ops/sec | 70 ns | 90 ns |
| DB GET (稳态) | 10.71M ops/sec | 90 ns | 110 ns |
| DB GET (稳态, TTL 混合) | 17.47M ops/sec | 50 ns | 100 ns |
| DB PUT Group16 (稳态) | 1.18M items/sec | 12.98 µs | 19.83 µs |
| ART Lookup (索引查找) | 20.98M ops/sec | 50 ns | 60 ns |
| ART Insert (索引插入) | 30.27M ops/sec | 30 ns | 40 ns |
| WAL Append (异步) | 0.66M ops/sec | 1.38 µs | 3.71 µs |
分片扩展性
GET 操作通过顺序锁实现无锁化——同一分片上的多个读取者并行进行,无需序列化。PUT 操作通过分片互斥锁序列化写入者;修改 ART 结构的插入操作会额外受顺序锁计数器的保护。
GET —— 无竞争(每个线程位于不同分片):
| 线程数 | 1 | 2 | 4 | 8 | 16 |
|---|---|---|---|---|---|
| 吞吐量 | 35.58M | 67.24M | 119.28M | 169.73M | 203.11M ops/sec |
| 扩展倍率 | 1.00x | 1.89x | 3.35x | 4.77x | 5.71x |
GET —— 热点(所有线程访问同一个 Key):
| 线程数 | 1 | 2 | 4 | 8 | 16 |
|---|---|---|---|---|---|
| 吞吐量 | 34.05M | 65.10M | 121.84M | 226.88M | 281.13M ops/sec |
| 扩展倍率 | 1.00x | 1.91x | 3.58x | 6.66x | 8.26x |
注:GET 热点表现出超线性扩展,因为多个读取者可以同时遍历相同的缓存 ART 路径而无竞争。
GET —— 均匀分布 10k Keys(真实工作负载):
| 线程数 | 1 | 2 | 4 | 8 | 16 |
|---|---|---|---|---|---|
| 吞吐量 | 10.83M | 18.99M | 34.10M | 56.15M | 89.70M ops/sec |
| 扩展倍率 | 1.00x | 1.75x | 3.15x | 5.19x | 8.29x |
PUT —— 无竞争(每个线程位于不同分片):
| 线程数 | 1 | 2 | 4 | 8 | 16 |
|---|---|---|---|---|---|
| 吞吐量 | 41.76M | 68.33M | 122.99M | 248.82M | 203.71M ops/sec |
| 扩展倍率 | 1.00x | 1.64x | 2.95x | 5.96x | 4.88x |
PUT —— 均匀分布 10k Keys(真实工作负载):
| 线程数 | 1 | 2 | 4 | 8 | 16 |
|---|---|---|---|---|---|
| 吞吐量 | 12.02M | 16.04M | 27.25M | 43.20M | 55.18M ops/sec |
| 扩展倍率 | 1.00x | 1.33x | 2.27x | 3.59x | 4.59x |
高频覆盖写校准
对于频繁覆盖大数值(字符串、数组)的工作负载,Zeno 会累积保留 Arena 字节,直到调用 compact_shard。下表显示了压缩频率、p99 延迟和保留内存之间的权衡(Payload=1KB, Keys=64, Ops=50k):
| compact_every_N | p50 | p99 | max | 最终保留内存 | 总耗时 |
|---|---|---|---|---|---|
| 1000 | 110 ns | 6.14 µs | 138.54 µs | 0 B | 106.27 ms |
| 5000 | 100 ns | 4.38 µs | 175.05 µs | 0 B | 48.33 ms |
| 10000 | 100 ns | 3.76 µs | 63.37 µs | 0 B | 39.42 ms |
| off (关闭) | 90 ns | 3.69 µs | 123.54 µs | 48.83 MB | 25.10 ms |
- 当需要限制保留字节且能接受中等维护开销时,请使用 5000。
- 仅在追求极致吞吐量且可以接受高内存占用时,请使用 off。
要在您的机器上复现这些数据:
zig build bench -Doptimize=ReleaseFast
🛠 使用方法
在您的 build.zig.zon 中添加 zeno:
.{
.name = "my-project",
.version = "0.1.0",
.dependencies = .{
.zeno = .{
.url = "https://github.com/zeno-core/zeno/archive/refs/heads/main.tar.gz",
},
},
}然后,在您的 build.zig 中:
const zeno = b.dependency("zeno", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("zeno", zeno.module("zeno"));快速示例
const std = @import("std");
const zeno = @import("zeno");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// 内存模式引擎(不持久化)
const db = try zeno.public.create(allocator);
defer db.close() catch {};
// 写入一个值
const value = zeno.types.Value{ .string = "Alice" };
try db.put("user:1", &value);
// 读取值,调用者拥有返回值的内存所有权
if (try db.get(allocator, "user:1")) |val| {
defer val.deinit(allocator);
std.debug.print("Found: {s}\n", .{val.string});
}
// 删除
_ = try db.delete("user:1");
}使用 WAL 和快照恢复的持久化引擎:
const db = try zeno.public.open(allocator, .{
.wal_path = "./data.wal",
.snapshot_path = "./data.snapshot",
.fsync_mode = .batched_async,
});
defer db.close() catch {};🏗 架构
Zeno 采用分片优先架构,旨在保持并发环境下热路径的可预测性:
- 键空间分区:键空间被划分为 256 个独立的分片(通过 Key 的哈希路由),每个分片拥有自己的 ART 索引、锁、顺序计数器和内存 Arena。
- 分片局部操作:点操作在路由后是分片局部的。
get通过顺序锁实现无锁化——同一分片上的并发读取者无需相互序列化。put获取分片排他锁;覆盖现有 Key 会跳过顺序锁计数器以实现最小延迟。 - 读取一致性:通过可见性栅栏和
ReadView协调,因此扫描和视图内读取可以观察到稳定状态,而其他分片的写入仍可继续。 - 持久化机制:由 WAL + 快照处理。WAL 记录实时变更用于崩溃恢复,而快照提供更快的重启和定期状态压缩。
这种设计提供了极强的单 Key 延迟表现、良好的多核扩展性,并允许在吞吐量和持久化策略 (fsync_mode) 之间进行显式权衡。
⚖️ 许可证
基于 MIT 许可证分发。
加入我们
Zig 中文社区是一个开放的组织,我们致力于推广 Zig 在中文群体中的使用,有多种方式可以参与进来:
- 供稿,分享自己使用 Zig 的心得
- 改进 ZigCC 组织下的开源项目
- 加入微信群、Telegram 群组