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 群组
Zeno 是一款使用纯 Zig 语言编写的高性能、嵌入式键值存储引擎。它专为现代工作负载设计,优先考虑可预测的低延迟、零隐式内存分配和高效的分片并发。其名称(Node/节点)反映了支撑每项操作的核心索引和存储节点,请勿将其与 Node.js 混淆。
Zeno 最初是作为一个研究数据库存储内部原理和自适应基数树(ART)的学习实验。实验结果和性能表现非常出色,因此它演变成了一个独立的引擎。
🚀 核心特性
Allocator。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 次操作。
点操作吞吐量
分片扩展性
GET 操作通过顺序锁实现无锁化——同一分片上的多个读取者并行进行,无需序列化。PUT 操作通过分片互斥锁序列化写入者;修改 ART 结构的插入操作会额外受顺序锁计数器的保护。
GET —— 无竞争(每个线程位于不同分片):
GET —— 热点(所有线程访问同一个 Key):
注:GET 热点表现出超线性扩展,因为多个读取者可以同时遍历相同的缓存 ART 路径而无竞争。
GET —— 均匀分布 10k Keys(真实工作负载):
PUT —— 无竞争(每个线程位于不同分片):
PUT —— 均匀分布 10k Keys(真实工作负载):
高频覆盖写校准
对于频繁覆盖大数值(字符串、数组)的工作负载,Zeno 会累积保留 Arena 字节,直到调用
compact_shard。下表显示了压缩频率、p99 延迟和保留内存之间的权衡(Payload=1KB, Keys=64, Ops=50k):要在您的机器上复现这些数据:
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中:快速示例
使用 WAL 和快照恢复的持久化引擎:
🏗 架构
Zeno 采用分片优先架构,旨在保持并发环境下热路径的可预测性:
get通过顺序锁实现无锁化——同一分片上的并发读取者无需相互序列化。put获取分片排他锁;覆盖现有 Key 会跳过顺序锁计数器以实现最小延迟。ReadView协调,因此扫描和视图内读取可以观察到稳定状态,而其他分片的写入仍可继续。这种设计提供了极强的单 Key 延迟表现、良好的多核扩展性,并允许在吞吐量和持久化策略 (
fsync_mode) 之间进行显式权衡。⚖️ 许可证
基于 MIT 许可证分发。
加入我们
Zig 中文社区是一个开放的组织,我们致力于推广 Zig 在中文群体中的使用,有多种方式可以参与进来: