Skip to content

【Zig 日报】项目分享:Zeno--用 Zig 编写的高性能嵌入式键值存储引擎。 #316

@jiacai2050

Description

@jiacai2050

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 采用分片优先架构,旨在保持并发环境下热路径的可预测性:

  1. 键空间分区:键空间被划分为 256 个独立的分片(通过 Key 的哈希路由),每个分片拥有自己的 ART 索引、锁、顺序计数器和内存 Arena。
  2. 分片局部操作:点操作在路由后是分片局部的。get 通过顺序锁实现无锁化——同一分片上的并发读取者无需相互序列化。put 获取分片排他锁;覆盖现有 Key 会跳过顺序锁计数器以实现最小延迟。
  3. 读取一致性:通过可见性栅栏和 ReadView 协调,因此扫描和视图内读取可以观察到稳定状态,而其他分片的写入仍可继续。
  4. 持久化机制:由 WAL + 快照处理。WAL 记录实时变更用于崩溃恢复,而快照提供更快的重启和定期状态压缩。

这种设计提供了极强的单 Key 延迟表现、良好的多核扩展性,并允许在吞吐量和持久化策略 (fsync_mode) 之间进行显式权衡。

⚖️ 许可证

基于 MIT 许可证分发。

加入我们

Zig 中文社区是一个开放的组织,我们致力于推广 Zig 在中文群体中的使用,有多种方式可以参与进来:

  1. 供稿,分享自己使用 Zig 的心得
  2. 改进 ZigCC 组织下的开源项目
  3. 加入微信群Telegram 群组

Metadata

Metadata

Assignees

No one assigned

    Labels

    日报daily report

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions