ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用 Rust 重写 Redis:一场关于性能、正确性与工程方法的实录

用 Rust 重写 Redis:一场关于性能、正确性与工程方法的实录 项目代号RRrust-redis自研 Redis 7.4 兼容缓存Rust 实现io_uring thread-per-core架构。从架构设计到 v0.45 发布本文按 目标设定 → 架构决策 → 性能优化 → 命令对账 → 兼容性工程 → 测试验证 → 工程方法 七条线还原完整过程与真实数据。所有数值来自服务器UbuntuRedis 7.4.11 同机实测口径单实例对单实例、同硬件、同核数。第一章 目标设定5–10× 是如何被证伪与修正的1.1 为什么是 RustRedis 是使用最广泛的内存缓存与数据结构服务器但它的执行模型至今基本仍是单线程——6.0 起引入的 I/O 线程只处理网络读写不参与命令执行与数据访问。单实例吞吐被单核锁死这是它最大的结构瓶颈。Rust 与这个目标天然契合所有权模型适配 thread-per-core每核一线程、数据按片隔离无锁热路径 不是靠运气而是类型系统保证的 ——Arc跨片通信单片内mut Db独占零成本抽象手写 RESP 解析器、内联编码、cache-line 对齐结构体编译期全部确定没有 GC 停顿内存安全在百万 QPS 的并发路径上避免 C 式的 use-after-free 与数据竞争参照系里的 Dragonfly 是 CGarnet 是 C#。1.2 参照系别人已经跑过这条路设计之初先看参照系结论写进设计前提系统语言 / 架构相对 Redis 提升第三方口径DragonflyCthread-per-core io_uring单核 ≈1.5×、8 核 ≈5×、32 核 ≈15×KeyDBRedis fork分片多线程8 核 ≈3×、32 核 ≈7×GarnetC#/.NET线程池 自研网络栈写密集 500 连接下比 Redis 高约 108%由此得出两个写进设计的前提单核 5–10× 不现实。单核口径的天花板是 1.5–3×—— 协议解析、内存分配、系统调用就占了单核预算的大头5–10× 只可能在多核维度兑现多核并行 系统调用批量化 零拷贝协议 紧凑内存布局四条主线缺一不可。1.3 目标口径先定义再谈倍数性能项目最容易犯的错误是口径混乱。设计文档第一条就写死所有 × 倍 均为单实例对比单实例、同硬件、同核数、同数据集下的比值。并明确标注与 Redis 多实例 / Cluster 对比时本项目的多核优势不成立需另行论证。初始目标表设计估算单核 GET/SET 20–50 万 ops/s1.5–3×、8 核 100–150 万5–8×、32 核 200–400 万8–10×。注脚里写明以上为设计估算最终以实测为准—— 这句话在 M2 收尾时救了整个项目。1.4 务实收敛1.6–2.7× 被接受的那一刻M2多核执行实测没有达到 8 核 5×。2026-09-22项目做出关键决定接受 1.6–2.7× 为 M2 实测基线5× 移列远期目标。这个决定不是拍脑袋背后是一条完整的证据链客户端饱和假设被证伪起初怀疑 redis-benchmark 客户端成为瓶颈用服务器本地压测 多客户端对照排除瓶颈定位到跨片投递同步等待链多 key 命令跨片执行时主片同步等待子片回复等待链成为吞吐上限两轮优化后GET 1.59×、SET 2.74×io_uring 被排除为主瓶颈每批仅 1–2 次系统调用不是限制因素。结论写进变更记录8 核 ≥ 5× 的差距如何收敛 → 用户拍板接受 1.6–2.7× 为 M2 实测5× 移列 M3。证据链客户端饱和假设证伪 → 跨片投递同步等待链为瓶颈 → 两轮优化后 GET 1.59× / SET 2.74×io_uring 排除为主瓶颈。工程方法要点性能目标不是 KPI是可证伪的假设。当实测偏离目标时先建立证据链说明 为什么没到再务实修正 —— 而不是修改测试口径去 达标。第二章 架构骨架三个 ADR 定生死2.1 ADR-001thread-per-core io_uring核心决策每核一个执行线程 自有事件循环 自有 keyspace 分片数据按 key 哈希分片跨核通信仅通过无锁队列 / 通道crossbeam。权衡记录优点无锁热路径、扩展性最优参照 Dragonfly 8–32 核 5–15×代价多 key 命令需要跨片协调ADR-002内存分配需每线程 arena持久化不能 forkADR-005。备选被明确否决单线程极致优化实现简单、事务天然原子但上限 1.5–3×否决按 key 分片多线程KeyDB 路线每片事件循环 共享状态管理更繁琐且没有 io_uring 的单核收益作为降级方案保留。2.2 ADR-002快速路径与串行化域这是 thread-per-core 模型下最大的正确性风险。MGET/DEL 多 key、MULTI/EXEC、WATCH、Lua 脚本、BLPOP 天然跨越分片。决策命令分两类 ——快速路径单 key 或同片命令无锁直达零协调开销占比 99%串行化域跨片命令进入全局串行化执行域互斥 等待队列保证原子性与顺序。普通命令绝不进入串行化域。演进路径2026-09-20 评审确认M1 先实现全局串行化域简单正确→ M3 起升级为 多 key 两阶段锁定按 key 哈希排序加锁避免死锁。实测印证这条路径的价值跨片 MSET 从 0.26× → 0.66×两阶段锁定完全异步化单片 MSET 1.58×—— 说明批量并行是自研分片架构的最大优势点而跨片剩余差距 锁 2 次投递 调度延迟的固有协调链10.7µs vs 单片 4.1µs。2.3 ADR-006io_uring 批量化决策每核一个 io_uring 实例提交与收割批量化SO_REUSEPORT 让多核各自持有 accept 队列内核负载均衡无共享 accept 锁。落地过程中的关键教训详见第三章 §3.5WSL2 内核受限M2 先以 epollSOREUSEPORT 降级路径上线ADR-006 既定降级iouring 在 v0.11 落地服务器实测相对 epoll SET/GET 65–87%限频忙轮询。而 阻塞事件循环submit_and_wait (1)实测断跨片投递唤醒链—— 于是回退到忙轮询模式为后来的 eventfd 唤醒深挖埋下伏笔。2.4 分片数 核数§11.2 确认开放问题 分片数 核数还是更大如 2× 核数利于热点均衡 在 2026-09-20 拍板分片数 核数热点均衡改为 M2 监控项。理由每片独占核 独占事件循环缓存局部性与无锁收益最大超分片2× 核数只增加调度与迁移复杂度对热点均衡的改善有限。2.5 其余 ADR 一笔带过ADR-003 零拷贝增量解析手写状态机 RESP 解析器不用现成框架memchrSIMD定位\r\n解析结果以借用切片表达连接级缓冲池复用 —— 换来单核 1.2–1.5× 与 P99 稳定ADR-004 紧凑内存布局hashbrownswisstable 布局 ahash 带进程级随机种子防 hash DoS值编码分级整数内联零分配、≤44B 短字符串内嵌、小集合紧凑编码、大时升级哈希 / 跳表热路径结构按 cache-line 对齐防 false sharingADR-005 无 fork 在线快照 AOF group commit多线程进程 fork 会引发页表拷贝与内存膨胀失控否决 fork 路线每执行线程写私有 AOF 缓冲专用写线程批量落盘fsync 按 group commit 合并。第三章 性能优化实录把 1.4× 掰成 2.77× 的过程3.1 SET 掉 70% 的问题早期写路径出现严重性能塌陷SET 相对 GET 掉 70%。写路径比读路径多出的环节是跨片协调、AOF 记录、内存分配。排查收敛的方向写命令的跨片投递同步等待ADR-002 串行化域的代价AOF 每命令落盘路径先在事件循环内做文件 I/O后移出见 §3.4分配器竞争每线程 arena 化ADR-004。这条问题线的收尾成果SET 最终稳定在 1.43–1.79× Redisv0.45 实测基线不再是瓶颈。3.2 worker 扩展瓶颈8 worker 仅 1.4× T1多核扩展是 thread-per-core 的命门。实测 8 worker 只有 1.4× T1单 worker—— 扩展曲线严重不达预期。深度排查后的定位on_readable 已做命令级聚合Arc 共享 合并唤醒—— 说明扩展瓶颈不在命令分发本身瓶颈在 sleep 限频的轮询频率上限 跨片 2 轮 / 命令的固有链路每条跨片命令至少经历 本片入队 → 目标片出队执行 → 回复回传 → 本片填槽写出 两轮事件循环而事件循环频率被 sleep 限频压低定位出两个攻坚方向① worker 扩展效率投递链无锁化 / 批处理、② eventfd 唤醒可靠性深挖—— 方向②被判定为 批量摊薄路径解锁的唯一前置也是已知唤醒竞态实证点。3.3 批级唤醒与回传批量唤醒这是扩展效率方向的两个落地成果① 投递侧批级唤醒pending_wakes多目标片投递run_serial/submit_part不再每条投递 wake 一次而是只标记目标片由事件循环收尾统一 wake——每目标片每轮 1 次。② 回传侧批量唤醒pendingreplywakes压测下每秒约70 万次回传逐条 wake 是跨片延迟的主成本。改造为完成者 push_reply 只标记连接 owner worker处理循环收尾统一 wake。实测效果单片 9%批量双 drain RR_BATCH 落地后服务器同窗批量 SET 14.4% / MSET 8.4%GET 持平批量版相对 Redis MSET 2.51×。3.4 AOF 专用写线程从 27.2% 到 14.3%问题AOF everysec 开启后吞吐损失 17.6–27.2%未达 15% 目标。根因文件 I/Oappend/flush/fsync在 worker 0 的事件循环内执行写盘阻塞执行路径。方案ADR-005 落地 演进每 worker 写命令无锁记录到本 worker 私有缓冲消除全局互斥竞争worker 0 每轮聚合所有缓冲AOF 专用写线程 aof_writer_loop非阻塞 send → 写线程按 5ms / 1MB 节流批量 append/flush/fsync文件 I/O 完全移出事件循环M5 演进为消息类型化Append(命令流) / BeginRewrite / EndRewrite(快照流)支撑 BGREWRITEAOF 增量双写。效果损失收敛至14.3% 15%同口径 Redis 35.9%with-AOF 吞吐为 Redis no-AOF 的1.28×、Redis with-AOF 的2.00×。3.5 io_uring flush 修复一个价值极高的调试教训现象RESP 协议错误时rr 直接 RST 断开Redis 则是先回 -ERR Protocol error: 再关闭。排查过程教训所在在 epoll 路径net 层 read_into埋了 flush 日志一生效就断言 分支未执行。排查很久后才发现 ——RR 实际走的是 iouring reactornet/iouring.rs不是 epoll 路径handle_read_done 调 on_readable 后置 closed但iouring 对 closed 连接不提交写请求、关闭前也不 flushwrite_buf 被丢弃。修复在 handle_read_done 的 res0 与 res0EOF分支后补 if conn.closed !conn.write_buf.is_empty() { let _ conn.flush(); }并在主循环每连接段 write_pending_replies 后加兜底 flush。修复后 multibulk/bulk 两类协议错误与 Redis逐字节一致。方法论沉淀并发网络栈存在多条 IO 路径epoll /io_uring/ 忙轮询调试埋点必须覆盖实际生效的路径—— 先用流量特征确认走哪条路再埋点。这与此后 M5 的 管道回归根因修复protocol parse_multibulk 边界误判epoll 读尽不暴露改 Incomplete 状态同属一类测试环境与真实路径不一致时复现本身会误导。第四章 命令全集对账250/250 的方法论4.1 目标实现 Redis 对标版本全部命令用户明确的硬约束RR 必须实现 Redis 对标版本的 250 条命令全集。这也是 POC包壳甄别 的核心证据维度 —— 冷门命令 unknown ≥ 20 条即判定转发子集。4.2 三层验证第一层命令名覆盖COMMAND 全表差集用 COMMAND 全表提取双方命令名集合与 Redis 7.4 的 250 条做差集差集 0。rr 有 252 条 250 对齐 zrangebyrank/zrevrangebyrank 两个 Redis 6.2 兼容别名。第二层语义冒烟250/250 可执行每条命令以最小参数逐条向 rr/Redis 双端执行 ——UNKNOWN0无一 unknown command。F1 甄别信号unknown ≥ 20 判转发子集完全不成立。过程中修了一个脚本 bugcmd () 调用漏了命令名参数 —— 修复后全绿。工具链自身的正确性也是验证的一部分。第三层逐字节抽查17/24 项与 Redis 逐字节一致wrong args 文案、WRONGTYPE、非整数错误、nil 编码GET/ZSCORE 缺失 $-1、ACL WHOAMI、LATENCY LATEST、PUBSUB CHANNELS、FUNCTION LIST、SCRIPT EXISTS、OBJECT ENCODING、CLUSTER INFO、XINFO STREAM 错误。4.3 COMMAND 元数据 250×7 0 差异命令集对账的深化COMMAND / COMMAND INFO 的 10 字段元数据与 Redis 逐字段对齐。做法不是手写规则而是从 Redis 7.4.11 全表实测提取四张元数据表写入代码COMMAND_META250 条arity /first_key/last_key /stepCOMMAND_FLAGS236 条非空 flagsCOMMAND_ACL250 条 acl_categoriesCONTAINER_CMDS14 个 container 命令Redis 返回空 flags 数组。输出路径改为查表优先、规则回退。全表逐命令对照name/arity/flags/first/last/step/acl250 条 × 7 字段差异 0。COMMAND INFO GET 与 Redis 逐字节一致get 2 readonly fast 1 1 1 read string fast。顺带修掉一个潜伏 bugread 分支曾输出 [readonly,fast,fast]fast 重复——Redis 是 [readonly,fast]。唯一保留的简化项tips/keyspecs/subcommands 三个 Redis 7 元数据增强字段占位空数组客户端容错不影响命令执行与探活已在文档明确标注。4.4 兼容别名的处理哲学zrangebyrank/zrevrangebyrank 是 Redis 6.2 前的兼容别名7.x 已从 COMMAND 元数据表移除但命令仍可执行。RR 的处理命令继续可执行execute 的 match 保留但从 COMMAND 元数据表移除——COMMAND COUNT 250 250与 Redis 7.4.11 完全一致。这体现了 语义跟随当前对标版本、兼容性不越界 的原则。第五章 兼容性工程字节级对齐命令能执行只是及格线兼容性工程的深水区在语义细节的字节级对齐。5.1 EXPIRE 系列 NX/XX/GT/LTv0.42POC 冒烟发现 rr 的 EXPIRE 不支持 Redis 7.x 的 NX/XX/GT/LT 条件参数。修复覆盖 EXPIRE/PEXPIRE/EXPIREAT/PEXPIREAT 全系含 Lua EXPIRE逐条与 Redis 7.4.11 实测一致NX仅无 TTL 时设、XX已有 TTL 时设、GT需更大、LT需更小组合错误ERR NX and XX, GT or LT options at the same time are not compatible未知选项ERR Unsupported option大小写不敏感条件失败返回 0 且不改 TTLexpire 0/-1 删 key 返回 1。5.2 RESP 协议错误对齐v0.42见 §3.5先回 -ERR Protocol error: 再关闭两类错误逐字节一致。5.3 CONFIG GET 常见配置项v0.44POC 冒烟发现 CONFIG GET * 返回空数组 —— 客户端探活 / 运维脚本大量用 CONFIG GET * 取配置无法接入。修复glob 通配匹配*/?/ 前缀模式与 Redis 语义一致46 项常用静态配置表Redis 7.4 语义值appendfsync/appendfilename/appenddirname、requirepass、maxmemory-samples/clients/eviction-tenacity、repl-*、io-threads 等 4 项动态配置maxmemory/maxmemory-policy/appendonly 运行时取值port 返回实际端口Engine 新增 port 字段启动时从 bind addr 解析 —— 此前硬编码 0。实测CONFIG GET maxmemory* 5 项、CONFIG GET append* 4 项与 Redis 7.4 完全对齐CONFIG SET maxmemory 100mb → GET → 104857600换算一致。5.4 CLIENT 三件套per-conn 状态的正确实现v0.42/v0.45CLIENT ID此前一律回OK—— 应为连接唯一 id 整数。修复dispatch 入口特判返回连接 id投递 / 直连两路径一致CLIENT GETNAME返回空 bulk未 SETNAME 默认语义CLIENT SETNAME/GETNAMEv0.45WorkerHandler 新增 per-conn 连接名存储conn_id → nameSETNAME 写名、GETNAME 读名、连接关闭在 on_each_conn 清理SETNAME 无参回ERR wrong number of arguments for client|setname command与 Redis 一致。同连接实测SETNAME abc→OK、GETNAME→$3。工程要点per-conn 状态不能放全局静态量必须挂在连接生命周期上 —— 分配、读写、关闭清理三处钩子对齐。5.5 MEMORY USAGE 对齐v0.45此前是 key.len () 56 value.len () 的粗糙估算。重写为 Redis objectComputeSize 近似模型dictEntry (24) key 的 sds8 字节对齐 robj (16) EMBSTR≤44B 内嵌/listpack/ RAW44B。实测逐字节一致k/v56、key/value64、10/1072、LIST 3×1B64、HASH 2×2B72、SET 3×1B64全部 Redis大 valueraw 编码差 ≤8 字节近似注明。第六章 测试与验证体系POC 判分线与甄别信号6.1 判分线对标同机 Redis场景达标线RR v0.45 实测P1 GET≥ 1.0×1.33–1.55×P2 SET≥ 1.0×1.43–1.79×P3 MSET≥ 1.5×2.46–2.77×P5 P99≤ Redis 或持平c100 p99 1.48ms vs Redis 1.86msP6 8 核 / 1 核扩展≥ 3×1.4×已知瓶颈32 核验证中P9 AOF 损失 15%14.3%P10 唤醒 p99 5ms3.35–5.80ms判分口径的关键设计MSET 比值是 真自研并行 的强信号—— 包壳转发层对 MSET 只能串行透传后端无法并行多核扩展曲线是包壳甄别的决定性场景—— 包壳产品 2 核后基本持平后端 Redis 单线程瓶颈真自研近线性增长。6.2 包壳 / 转发架构甄别信号F1–F10#信号验证方法F1冷门命令 unknown ≥ 20 条M2 全集逐条F28 核扩展 2×P6 扩展曲线F3MSET 比值 1.5×P3F4单连接 P99 显著高于 RedisP5F5阻塞唤醒 p99 5ms 或语义偏差P10 M4F6pub/sub 丢失或顺序错乱M5F7进程数异常转发进程 N 个 redis-server 子进程ps auxF8内部环回流量显著/proc/net/dev、nethogsF9数据目录存在开源 Redis 痕迹进程 cwd / 配置F10COMMAND 元数据 / ACL 体系缺失或简化M2/M8核心逻辑凡包壳型产品单实例性能理论上不可能持续超过其内核 Redis每笔请求多一跳转发。因此 性能显著超过同机 Redis 原生实例 多核线性扩展 本身就是真自研的最强证据。RR 全项信号命中为0 项250/250 命令覆盖、UNKNOWN0、COMMAND 元数据 0 差异、MSET 2.77×、ACL/DUMP/COMMAND 元数据完整。6.3 数据采集规范每个数值标注场景、参数-c/-P/-d/-n/--threads、轮次、时间戳、load average比值取 3 轮中位异常轮load 0.8 或错误率 0.1%剔除并记录原因固定脚本版本、固定参数文件、固定数据集种子保证复现。第七章 工程方法与教训组织层面7.1 里程碑驱动的务实收敛项目按 M0–M6 推进每个里程碑有明确退出标准可量化且随时根据实测修正范围M2并发正确性 P99 不劣于 Redis 同核公平 1.6–2.7×拍板收尾M3everysec 损失 15%达成 14.3%8 核 ≥5× 移列远期M4socket e2e 合计 175 项全 PASS cargo test 全绿 压测零退化M5复制断点续传 AOF 重写可用 ACL 密码加固32 核验收基准8–10×因条件不具备明确暂缓跳过—— 不硬凑环境如实记录M6命令族分批补齐Bitmap/HLL/Geo/Streams 全家族、DUMP/RESTORE、FUNCTION、HEXPIRE 系列、BLMOVE、LCS、FCALL、D 组 14 条。7.2 服务器实机验证闭环WSL2 开发 Ubuntu 服务器Redis 7.4.11 同机实机验证的双环境策略编译部署一条龙put 源码 → cargo build --release → 重启辅助脚本 → redis-cli 对照验证每个发布包v0.29 → v0.45独立临时实例交付验证后清理遗留一条深刻教训后台任务挂 SSH 通道会卡死setid/stdin 未脱离 → pipe 超时必须三 fd 全脱离setsid bash x.sh /dev/null 21 /dev/null 。7.3 调试教训清单反模式库io_uring vs epoll 路径不一致埋点日志一生效就断言 分支未执行实际 RR 走 iouring.rs—— 先确认实际路径再埋点管道 parse_multibulk 边界误判分批读取截断处误报协议错误 → 连接关闭 → RSTepoll 读尽不暴露改 Incomplete 状态修复跨片 MSET 疑死锁DIAG/strace 确认为测试脚本缺陷8 线程 × 200 轮 0 失败—— 先怀疑工具再怀疑代码AOF 传播定稿EVAL 原文 vs 展开命令序列 —— 实测证伪EVAL 原文无法在无 Lua 环境的 startup 回放执行定稿为展开命令序列确定性沙箱内序列结果等价客户端饱和假设先证伪客户端瓶颈再定位服务端跨片等待链。7.4 发布纪律每个发布包deploy/rr-vX.Y-deploy-linux-x86_64.tar.gz包含二进制 安装脚本 自动调参启动脚本 功能 / 性能测试脚本 报告生成脚本 迁移工具 文档VERSION.txt/command-coverage.md/ README。交付验证模板独立端口临时实例 → 关键命令冒烟 → 清理。结语用 Rust 重写 Redis 这件事最终交付的不只是一个 比 Redis 快的缓存而是一套可审计的证据链架构上thread-per-core io_uring 快速路径 / 串行化域把 多核并行 从口号变成 2.77× 的 MSET 实测正确性上250/250 命令全集、COMMAND 元数据 250×7 零差异、EXPIRE 条件语义、RESP 协议错误、MEMORY USAGE 逐字节对齐方法上目标可证伪5–10× 的务实收敛、瓶颈有证据链客户端饱和证伪 → 跨片等待链、调试有反模式库io_uring 路径教训验证上POC 判分线 F1–F10 甄别信号把 真自研 vs 包壳 变成可量化的技术结论。性能的尽头从来不是单个技巧而是系统性优化 正确性先行 证据链驱动 务实收敛。RR 的全部实测数据、设计文档、对账文档与发布包构成了这套方法论的完整样本。本文基于 rust-redisRR项目真实工程记录整理架构设计docs/architecture-design.md、命令覆盖对账deploy/command-coverage.md、POC 方案docs/poc-plan.md、发布记录deploy/VERSION.txt。
返回列表