架构
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,读写都是一次对象操作,简单高效。
本地磁盘缓存:读缓存 + writeback
对象存储是地板,本地盘是有界、可丢的热缓存。两层正交:
| 层 | 介质 | 容量 | 持久性 |
|---|---|---|---|
| 热 | 本地盘 cache(--cache-dir/--cache-size) | hot_gb,默认 ≤1GB | 可丢(仅缓存副本) |
| 冷 | 对象存储 S3/GCS | quota_gb:1GB → 100GB+,上不封顶 | 权威持久 |
- 读缓存:block 落本地盘,命中即一次本地读;超出按 LRU 淘汰回对象存储(数据不丢,只是变冷)。
- writeback 写缓冲(
--writeback):写先落本地持久暂存再异步上行 →fsync快,但有丢写风险,必须配 drain(见下)。暂存盘要用持久 virtio-block,崩溃重启可续传。 - 元数据客户端缓存:调大
--attr-cache/--entry-cache/负缓存,吃掉 git 的海量stat与对不存在路径的查询。
dozycat 在 JuiceFS 之上只做这四件事
- 热目录识别:消费 JuiceFS 访问日志(
/mnt/repo/.accesslog),按目录聚合 EWMA 频率,用元数据引擎把热 inode 翻译成路径,产出“哪个目录热”榜单(对象存储侧看不出,必须在这里做)。 - VM 生命周期 drain(不丢写):在 cat 关机/冻结之前,停收新写 → 等 writeback 暂存全部上行 + 元数据提交确认 → 才允许销毁。已
fsync的写一个不丢。 - 挂载预热编排(热度驱动):冷启/唤醒后缓存为空,首访全 miss。激活前按热度榜
juicefs warmup <hot-paths>预取并 pin,工作负载跑起来时缓存已暖。 - 元数据引擎选型/运维 + 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。
dozycat