架构

dozycat 如何在 JuiceFS 之上构建:host 侧数据面、HA 元数据引擎、对象存储 blob 后端与本地磁盘缓存,以及 dozycat 仅做的四件事。

设计原则:不重造文件系统

dozycat 的核心策略是不从零造一个分布式文件系统。多挂载一致性、原子 rename、跨挂载锁、对象存储分片与 compaction 这些东西,JuiceFS 已经打磨了多年。dozycat 以 JuiceFS 为底座,只在它上面补四块针对 microVM 舰队的能力。

这条路径的前提是工作负载画像清晰:dozycat 面向 git-on-disk + grep,文件多在 1KB–1MB、元数据密集、读多写少、强调一致性。结论是可用性由元数据延迟决定,不是带宽——这直接定了调优方向(低延迟元数据 + 客户端元数据缓存 + 挂载预热),吞吐不是瓶颈。

为什么是 JuiceFS(而非 gcsfuse / 自研)

方案结论原因
gcsfuse否决一文件一 object、弱 POSIX、rename 非原子、无真正多写一致性 —— 跑 git 容易坏库。
自研(blob + tree + WAL + CAS)否决等于重造 JuiceFS 已做五年的东西;多挂载一致性、原子 rename、锁、compaction 全要自己保证,风险与工期不可接受。
JuiceFS选定POSIX 完备、对象存储原生、磁盘缓存、原生多客户端挂载 + 元数据引擎事务一致性、跨挂载 flock/POSIX 锁。剩下缺的恰好是我们能轻量补的几块。

数据面:host 侧 JuiceFS + virtio-fs(形态 B)

dozycat 的关键架构选择是把数据面放在 host 侧,而不是塞进每个 guest。host 运行 JuiceFS 客户端与 virtiofsd,guest(一个 Cloud Hypervisor microVM,我们叫它 cat)通过 virtio-fs 把它挂进来:

Host
├── JuiceFS client(host 进程,持对象存储/元数据凭证,cache 在本地 NVMe)
│     └── mount → /srv/jfs/<tenant>
├── virtiofsd  --shared-dir=/srv/jfs/<tenant>/worktrees/<vmid>  --socket=/run/viofs/<vmid>.sock
└── Cloud Hypervisor  --fs tag=repo,socket=/run/viofs/<vmid>.sock  --memory shared=on
      └── guest(cat):  mount -t virtiofs repo /mnt/repo   (无网络、无凭证)

这样做有三个直接收益:

  • cache 在 host 跨同宿主多 cat 共享去重;同租户同 repo 收益尤其大。
  • 凭证只留在 host。guest 只有一个本地 vhost-user socket,拿不到任何对象存储或元数据引擎凭证。
  • 元数据连接数 = host 数,而非 cat 数 —— 在万级轮换下这是生死线。
同步不停。 virtiofsd 和 JuiceFS 客户端都是 host 进程,不会被 guest 的 pause/snapshot 冻结。把一个 cat 暂停、快照或迁移时,host 侧仍在继续把 staging 上传到对象存储。数据面与 VM 冻结解耦,是选 Cloud Hypervisor(原生 virtio-fs)最大的红利。

当环境只有 in-guest FUSE、没有 virtio-fs 时,可回退到形态 A(每个 guest 内跑 JuiceFS 客户端)+ host 侧 dozyfs-syncd + 交接租约。代价是 cache 不跨 VM 共享、guest 需持短期凭证、元数据连接数随 VM 线性增长。

元数据引擎:HA、低延迟、不丢写锚点

元数据引擎同时是多挂载的线性化点元数据持久性的关键依赖。本场景元数据密集 → 低延迟重要;不丢写 → 必须 HA + 持久化。

引擎延迟持久 / HA判断
Redis亚毫秒(最低)需 AOF appendfsync always + Sentinel/Cluster 才安全;默认会丢要极致元数据延迟且肯做 AOF+HA
PostgreSQL(Cloud SQL HA)毫秒强(同步复制 + PITR)默认推荐:持久 + 运维省心;git 元数据热点由客户端缓存吸收
TiKV强(Raft)超大规模 / 超高元数据 OPS 才需要
SQLite最低单机、无网络共享多挂载直接否决

默认选 PostgreSQL(Cloud SQL,开 HA + 自动备份/PITR):满足“不丢写”对元数据持久性的硬要求,运维负担最低。真机验证(杀主库切换)已证同步复制下已提交元数据零丢失。规模爆炸时再按实测 OPS 升 Redis-Cluster(分片)或 TiKV。

blob 后端:对象存储里的 chunk / slice / block

权威持久数据全部落在对象存储(S3/GCS)。JuiceFS 把文件切成三层:

  • chunk(≤64MB)→ slice(一次连续写)→ block(≤4MB,存对象存储)。
  • 本场景文件多 ≤1MB < 单 block(4MB),所以多数文件就是单个 block,读写都是一次对象操作,简单高效。
关键事实。 对象存储上的 object 按 inode/chunk 数字命名,不含路径。也就是说“哪个目录热”从对象存储侧根本看不出来 —— 必须在文件系统/元数据层做 inode→path 映射。这正是 dozycat 热目录识别要二开的根因。

本地磁盘缓存:读缓存 + writeback

对象存储是地板,本地盘是有界、可丢的热缓存。两层正交:

介质容量持久性
本地盘 cache(--cache-dir/--cache-sizehot_gb,默认 ≤1GB可丢(仅缓存副本)
对象存储 S3/GCSquota_gb:1GB → 100GB+,上不封顶权威持久
  • 读缓存:block 落本地盘,命中即一次本地读;超出按 LRU 淘汰回对象存储(数据不丢,只是变冷)。
  • writeback 写缓冲--writeback):写先落本地持久暂存再异步上行 → fsync 快,但有丢写风险,必须配 drain(见下)。暂存盘要用持久 virtio-block,崩溃重启可续传。
  • 元数据客户端缓存:调大 --attr-cache/--entry-cache/负缓存,吃掉 git 的海量 stat 与对不存在路径的查询。

dozycat 在 JuiceFS 之上只做这四件事

  1. 热目录识别:消费 JuiceFS 访问日志(/mnt/repo/.accesslog),按目录聚合 EWMA 频率,用元数据引擎把热 inode 翻译成路径,产出“哪个目录热”榜单(对象存储侧看不出,必须在这里做)。
  2. VM 生命周期 drain(不丢写):在 cat 关机/冻结之前,停收新写 → 等 writeback 暂存全部上行 + 元数据提交确认 → 才允许销毁。已 fsync 的写一个不丢。
  3. 挂载预热编排(热度驱动):冷启/唤醒后缓存为空,首访全 miss。激活前按热度榜 juicefs warmup <hot-paths> 预取并 pin,工作负载跑起来时缓存已暖。
  4. 元数据引擎选型/运维 + 1000 节点控制面:引擎部署/HA/备份,以及由 dozycat(API/UI/CLI 同源)驱动的 node / cat / volume / route registry 和 cat-agent 派发。

JuiceFS vs dozycat 责任分界

能力JuiceFS 现成dozycat 二开
FUSE / POSIX 语义、原子 rename、flock/POSIX 锁
对象存储后端(chunk/slice/block)
本地磁盘 cache(读缓存 + writeback)调参 / 容量策略
多客户端挂载 + close-to-open 一致性使用约定(每 VM 一 worktree)
缓存预热 juicefs warmup是(手动)自动编排 + 热度驱动
热目录识别(哪个目录热)
VM 生命周期 drain(不丢写)部分
VM 冷/热 × 启/停 + 数据同步不停
凭证隔离 + 1000 节点控制面/网络
每 VM 独立 volume × 10w 轮换(serverless)
元数据引擎 HA/持久化(不丢写锚点)选项选型 + 运维

更多概念(dozycat / node / cat-agent / cat / volume / route)见 概念;稳定路由 xxxxx.dozyfs 与冷热轮换的生命周期细节见相应页面。源码与设计文档在 github.com/dozycat/dozyfs