Storforge
原理预计 45 分钟

深挖 Weka:这次带着实现者的眼睛

重读 Weka 架构白皮书 —— 学完前三个阶段,你会看到完全不同的东西:每个设计决策背后是哪个你踩过的坑。

学完这节你能做到

  • 解释 Weka 前端/计算/驱动分层与 CPU 核绑定的原因
  • 说清 bucket 虚拟元数据服务器的扩展与迁移机制
  • 列出 forgefs 要仿制的四个机制和放弃的三个

重读白皮书:这次你带着伤疤

L0 的第一课你解剖过 Weka 的三层架构 —— 那时它对你是「宣传材料」。 现在不一样了:你写过会被 kill -9 的 WAL(L1),用 io_uring 打满过一块 NVMe(L2), 在三节点集群上处理过成员抖动、EC 重建和 Raft 选主(L3)。 带着这些伤疤重读 Weka 白皮书,你会看到完全不同的东西:每一个设计决策, 都对应一个你已经亲手踩过、或者运维时半夜处理过的坑

这节课把 Weka 的四个核心机制拆到「可以照着实现」的深度,最后圈定 forgefs 的仿制范围 —— L4 的五节课就按这张图施工。

进程模型:独占 CPU 核和网卡队列的账

L0 讲过 Weka 把机器切成 Frontend/Compute/Drive 三类进程,每个独占一个 CPU 核。 当时一笔带过的问题现在要算清:独占到什么程度,为什么值得

Weka 的每个核心进程是一个单线程事件循环,busy polling,永不 sleep:

  • CPU 核:用 cgroup/isolcpus 从内核调度器手里整个拿走。没有上下文切换, 没有别的进程来污染 L1/L2 cache。你在 L2 测过:一次上下文切换约 13 微秒, 而 NVMe 一次读只要 80100 微秒 —— 调度抖动足以让 P99 翻倍。
  • 网卡队列:每个进程绑一个独立的网卡硬件队列(SR-IOV 虚拟功能或 DPDK 独占), 收发包不过内核协议栈,也不和别的进程抢中断。你运维时调过 irqbalance 和 RSS, Weka 的答案是:根本不让内核碰这些包。
  • 单线程无锁:既然一个核只跑一个逻辑单元,核内就不需要任何锁。 跨核通信走消息传递(无锁队列),不共享可变状态。

对照 forge:forge-node 跑在 tokio 上,work stealing 意味着任务会在核之间迁移, 锁和原子操作到处都是。这一档差距我们认了,但要能说清代价: tokio 方案的延迟抖动主要来自调度和锁竞争,大约让 P99 比中位数高 510 倍; Weka 的模型能把这个比值压到 23 倍。L5 调优时你会亲手量这笔账。

i单线程无锁不是复古,是前沿

Redis、Seastar/ScyllaDB、DPDK 应用全走这条路:thread-per-core + 消息传递。 逻辑越是元数据这种「小对象、高频次、强顺序」的负载,单线程模型赢得越多 —— 锁的开销和缓存行弹跳在这种负载下是主要成本。记住这个结论,下一段马上用到。

bucket:元数据切 64k 份的深意

L0 讲过 bucket 是「虚拟元数据服务器」,现在从实现者角度追三个问题。

第一问:为什么是切碎,而不是多活? CephFS 的多活 MDS 你运维过:按目录子树动态分区, 听着美好,实际上热点目录一出现就得靠 balancer 迁子树,迁移本身又造成抖动 —— 你见过 MDS 在 rank 之间来回倒腾一个热目录的惨状。Weka 反着来:不做动态智能, 做静态切碎。整个元数据空间按哈希切成 64k 个 bucket,一个热目录的文件天然散在 大量 bucket 里,热点被哈希拆掉了,根本轮不到 balancer 出场。

第二问:为什么是 64k 这个量级? bucket 数远大于核数(64k 对几百个 Compute 核), 好处有三:负载均衡的粒度够细(每核摊几百个 bucket,单个 bucket 忙不影响大局); 故障接管的单位够小(一个节点挂了,它的 bucket 分给所有幸存节点,而不是砸给某一个 —— 和你熟悉的 Ceph PG 对 OSD 的关系同构);扩容时只搬 bucket 归属关系,不用重切数据。

第三问:bucket 内部怎么并发? 不并发。一个 bucket 同一时刻只被一个 Compute 进程的单线程持有,bucket 内的操作天然串行 —— 于是 inode 更新不需要锁, 目录项插入不需要锁,一致性由「单写者」保证。这就是上一段单线程模型的兑现: 把并发问题转化成放置问题,用切碎 + 单写者取代锁。GPFS 的 token manager 为每个 inode 的锁令牌全局协调,你见过它在几亿文件的目录树下被打爆; Weka 直接取消了这个协调者。

bucket 的持久化走 Weka 自己的 EC 条带(元数据也是数据),接管时新持有者从共享的 盘上状态重放日志即可 —— 元数据服务是无状态可搬运的,状态永远在盘上。

Checkpoint单选

CephFS 多活 MDS 和 Weka bucket 都能横向扩展元数据,本质差异在哪?

数据通路:条带、EC、聚合写

数据面你在 L3 已经实现了一半:reed-solomon 4+2、条带布局、故障域约束。 Weka 在这条通路上多做的三件事,是 L4 striping 课的施工图:

  1. 文件到条带的地址翻译。对象存储里一个 key 对应一个 blob;文件系统里 一个文件是可以在任意偏移读写的字节流。Weka 把文件切成固定大小的 chunk, 若干 chunk 凑一个条带做 EC。你要实现「文件偏移 → 条带号 → 节点集合」的翻译层。
  2. 满条带写与小写的分流。整条带的写直接 EC 落盘,零惩罚;不满一条带的小写 如果原地更新,就要读旧数据 + 旧校验块再算 —— 这是你运维 Ceph EC 池时 小 IO 慢 10 倍的根源(读改写惩罚)。
  3. 日志式小写聚合。Weka 的答案:小写先以日志形式追加落盘(持久化即返回), 后台再把日志里的数据聚合成满条带写下去,回收日志空间。眼熟吗? 这就是你 L1 写的 Bitcask 模型 —— 所有写都是追加,后台 GC —— 只是这次追加的目的地是分布式条带,不是本地文件。

rebuild:为什么节点越多重建越快

你运维 RAID 的年代,一块 20T 盘重建要两天,期间第二块盘再挂就全军覆没。 根源是重建带宽收敛在单点:热备盘的写带宽就是天花板。

Weka 的重建是「众包」的,你在 L3 已经见过雏形,这里把 scaling 逻辑说透:

  • 每块盘上的条带 chunk,其兄弟 chunk(同条带的其余 5 个)均匀散布在全集群所有盘
  • 一块盘挂掉,它上面每个 chunk 的重建 = 读 4 个兄弟 + 算 RS + 写 1 个新 chunk, 而这些读写均匀摊给所有幸存盘
  • 100 块盘的集群,每块盘只承担约 1% 的重建流量;200 块盘就是 0.5% —— 重建时间随集群规模近似线性下降,和 RAID 恰好相反
  • 优先级上,Weka 先重建「已经掉了 2 个分片」的条带(再掉一个就丢数据), 后重建只掉 1 个的 —— 风险排序,不是顺序扫描

L4 的 rebuild 课会把这套逻辑实现成声明式循环:目标状态减现状等于任务队列, 像 K8s controller 一样对账 —— 你运维 K8s 时天天受益的模式,这次自己写一个。

forgefs 范围圈定

对照实现者视角的 Weka,forgefs 明确仿制四个机制:

机制Weka 原版forgefs 等价物在哪节课
虚拟元数据服务器64k bucket,单写者inode 哈希分片到 forge-node,分片内串行fs-metadata
条带化数据通路chunk → 条带 → EC复用 L3 的 4+2,新增地址翻译层striping
日志式小写聚合日志追加 + 后台聚合同思路,粒度更粗striping
众包式重建全集群分摊 + 风险优先级声明式重建循环 + 令牌桶限速rebuild

明确放弃三个:内核客户端(用 FUSE,慢 5~10 倍,但两周能跑起来)、 DPDK/SPDK 用户态 IO 栈(继续用 tonic + io_uring)、快照与分层(纯工程量,教学价值低)。 放弃不等于装看不见 —— milestone 课要写一份诚实的差距清单,每一项标注差多少、为什么。

新增两个 crate:forge-fs(文件系统逻辑:inode、目录、分片、条带地址翻译,纯库, 不碰网络)与 forge-fuse(FUSE 客户端进程,把 forge-fs 接到内核)。 forge-fs 不依赖 fuser —— 文件系统逻辑必须能脱离挂载点做单元测试,这是接口边界的老规矩。

AI 结对:给 forgefs 写设计提案pair with ai

写代码之前先让 AI 出设计提案,你来当评审 —— 这是 L4 起必须养成的习惯: 系统越大,返工成本越高,评审文档比评审代码便宜十倍。review 要点: 它有没有把 forge-fs 和 forge-fuse 的依赖方向说反(应该是 forge-fuse 依赖 forge-fs); 非目标清单是不是真的敢砍(AI 倾向什么都想要);最后那两个「最可能返工的决定」 是否和你的判断一致 —— 不一致的地方,恰好是你该深挖的。

我在 forge 项目(Rust 分布式存储,已有 forge-util/forge-store/forge-bench/forge-proto/forge-node/forge-cli 六个 crate,forge-node 已实现 gossip 成员管理、一致性哈希放置、reed-solomon 4+2 EC、openraft 元数据)基础上,要新增文件系统层 forgefs。请写一份 1~2 页的设计提案 docs/design/forgefs.md,包含:

1. 新增 crate 划分:forge-fs(文件系统逻辑,纯库)与 forge-fuse(FUSE 客户端,用 fuser crate),说明为什么文件系统逻辑不能和 FUSE 绑在一起;
2. 元数据方案:inode 按哈希分片到各 forge-node,对照 Weka bucket 说明相同点和粒度差异;
3. 数据方案:文件切 chunk 凑条带走已有的 EC 4+2 通路,小写走日志追加 + 后台聚合;
4. 明确的非目标清单:内核客户端、DPDK/SPDK、快照、分层、配额;
5. 每个设计点标注风险和验收方式(最终验收是 pjdfstest 子集 + 挂载后编译真实项目 + 节点故障注入)。

只写提案,不写代码。写完后列出你认为提案里最可能返工的两个决定,说明理由。

小结

  • Weka 的进程模型 = thread-per-core + 独占网卡队列 + 核内无锁;forge 用 tokio 认下这档差距,但要能量化代价
  • bucket 的三层深意:哈希静态切碎拆热点、64k 粒度摊薄接管、单写者串行取代锁 —— 把并发问题转化成放置问题
  • 数据通路三件套:地址翻译、满条带直写、小写日志聚合 —— 第三件就是分布式版的 Bitcask
  • 众包重建的 scaling:兄弟 chunk 全集群散布,重建时间随规模线性下降,风险优先级排序
  • forgefs 仿四弃三:仿 bucket/条带/聚合/重建,弃内核客户端/DPDK/快照;新增 forge-fs 与 forge-fuse 两个 crate

延伸资料