使用 Zig 构建 Redis 克隆 — 第4部分

发布: (2025年12月23日 GMT+8 09:01)
6 min read
原文: Dev.to

Source: Dev.to

旅程:从网页开发到系统编程

我在构建 Zedis(一个兼容 Redis 的内存数据库)的过程中学到了很多,我鼓励你也尝试一下。如果你和我一样来自网页开发,构建数据库可能会让人感到望而生畏——但它极具回报,你会发现许多之前从未想象过的计算基础。

基准概览

我使用 50 个并发客户端对 Zedis 进行了 1 000 000 次请求的基准测试。下面的 flame graph 立即揭示了问题:

函数执行时间百分比
Parse.parse75 %
Client.executeCommand13 %
Command.deinit8 %

对于 Redis 克隆来说,解析应该很快,命令执行才应当占据主要时间。

深入探讨 perf

火焰图显示了 时间 花在哪里,而不是 原因。为了获取更多细节,我使用了 Linux 的 perf 工具:

perf stat -e cache-references,cache-misses,\
L1-dcache-loads,L1-dcache-load-misses,\
dTLB-loads,dTLB-load-misses \
./zedis benchmark --clients 50 --requests 1000000

输出

Performance counter stats for ‘zedis’:
   20,718,078,128 cache-references
    1,162,705,808 cache-misses # 5.61% of all cache refs
   81,268,003,911 L1-dcache-loads
    8,589,113,560 L1-dcache-load-misses # 10.57% of all L1-dcache accesses
      520,613,776 dTLB-loads
       78,977,433 dTLB-load-misses # 15.17% of all dTLB cache accesses
     22.936441891 seconds time elapsed
     15.761103000 seconds user
     64.451493000 seconds sys

该程序在用户态花费 15.76 秒,在系统态花费 64.45 秒,总计 22.93 秒 的实际运行时间——这表明它是 I/O 受限的,更多时间在等待网络操作,而不是在执行有用的计算。

解析瓶颈

在从 Zig 0.14 迁移到 0.15 的过程中,reader API 发生了变化。由于不熟悉新的接口,我默认使用了 readSliceShort,它 一次读取一个字节

var b_buf: [1]u8 = undefined;
const bytes_read = try reader.readSliceShort(&b_buf);

这对性能是灾难性的:每读取一个字节都会产生一次系统调用和函数开销。

Redis 协议示例消息

*3\r\n
$3\r\nSET\r\n
$4\r\nkey1\r\n
$5\r\nvalue\r\n
  • *3 – 接下来有三个批量字符串
  • $3 – 一个 3 字节的字符串(SET
  • $4 – 一个 4 字节的字符串(key1
  • $5 – 一个 5 字节的字符串(value

缓冲读取 – 解决方案

与其逐字节读取,不如一次性分配缓冲区,让读取器使用它:

var reader_buffer: [1024 * 8]u8 = undefined;
var sr = self.connection.stream.reader(&reader_buffer);
const reader = sr.interface();

const line_with_crlf = reader.takeDelimiterInclusive('\n') catch |err| {
    if (err == error.ReadFailed) return error.EndOfStream;
    return err;
};

现在解析器一次处理 大块数据,而不是成千上万次微小读取,这正是高性能网络服务器应当采用的方式。

仍有进一步优化的空间(例如直接从缓冲区解析而不逐行处理),但当前的做法保持了代码的可读性。

内存分配开销

最初我对所有分配都使用 std.heap.GeneralPurposeAllocator。默认情况下它会启用许多安全检查、堆栈跟踪和账务功能,这会导致 显著的开销。火焰图显示大量时间花在分配器内部的互斥锁上。

切换到更精简的分配器后,大部分问题得到了解决:

// Example: using the page allocator
const allocator = std.heap.page_allocator;

在多核场景下,std.heap.smp_allocator 也是一个可选方案。

经验教训

  • 系统编程需要好奇心,了解库和运行时的内部实现。
  • 阅读源代码(Zig、Redis、TigerBeetle、PostgreSQL)是非常宝贵的。
  • 性能分析工具perf、火焰图)帮助你定位真正的瓶颈。
  • 缓冲 I/O轻量级分配器 对于高吞吐量服务器至关重要。

这是一项艰苦的工作,但回报巨大。

基准测试结果

使用的命令行工具:

./redis-benchmark -t get,set -n 1000000 -c 50

SET

请求数时间 (秒)并行客户端负载吞吐量 (请求/秒)平均延迟 (毫秒)
1 000 0004.25503 bytes235,294.120.115
1 000 0004.60503 bytes217,344.060.121

GET

请求数时间 (秒)并行客户端负载吞吐量 (请求/秒)平均延迟 (毫秒)
1 000 0004.29503 bytes233,045.920.113
1 000 0004.53503 bytes220,799.300.119

延迟汇总 (毫秒)(针对首次 SET/GET 运行)

SET:
  avg 0.115  min 0.056  p50 0.119  p95 0.127  p99 0.143  max 2.223

GET:
  avg 0.113  min 0.048  p50 0.119  p95 0.127  p99 0.135  max 0.487

Zedis 当前在两种操作上 比 Redis 慢 5–8 %。虽然它尚未在原始吞吐量上超越 Redis,但在最优化的内存存储之一仅相差个位数百分比,这对一个学习项目来说已经是相当不错的成就。

结束语

我对 Zedis 当前的性能相当满意。这可能是该项目的暂时告一段落——至少现在是这样。请关注后续更新,我将继续学习和探索系统编程!

# Systems Programming and Database Internals

If you’re a web developer curious about systems programming, I can’t recommend this enough. Start small, make mistakes (you will—I made plenty!), profile them, fix them, and watch your understanding deepen. You don’t need to be a C wizard or have a CS degree—just curiosity and persistence.

Thanks for reading! Subscribe to follow my journey—I’m learning systems programming and sharing everything I discover along the way.

**Zedis Source Code**

---

**F2023 #25 – Potpourri:** Redis, CockroachDB, Snowflake, MangoDB, TabDB
Back to Blog

相关文章

阅读更多 »