Jerry's Blog

Back

本文尝试系统拆解 Mega 项目中的 Scorpio 模块:从 FUSE 的基本原理,到 Overlay 文件系统的分层设计,看看如何用用户态文件系统实现一个按需加载的 monorepo 挂载方案。

一、背景:为什么需要 Scorpio?#

在大型 monorepo 场景下,开发者经常会遇到一个两难选择:

  • 完整克隆:仓库体积可能达到几十 GB,git clone 需要耗费大量时间和磁盘空间
  • 稀疏检出:配置复杂,对目录结构变化不够友好,难以动态扩展工作集

Scorpio 提供了第三条路:通过虚拟文件系统挂载远程仓库

它的目标形态是:

  • 仓库被”挂载”到本地某个目录
  • 初始只拉取目录树和必要元数据
  • 文件在第一次访问时再按需下载
  • 本地修改仍然可以正常版本控制和提交

粗略对比一下工作流:

传统方式:git clone (下载全部) → 本地操作 → git push
Scorpio: mount (下载目录树) → 按需加载 → 本地操作 → commit & push
text

要实现这种行为,关键是 FUSE(Filesystem in Userspace)


二、FUSE 原理简述#

2.1 什么是 FUSE?#

FUSE 是一个允许在用户态实现文件系统逻辑的框架。

  • 传统文件系统(ext4、NTFS 等)运行在内核空间,开发和调试门槛较高
  • FUSE 把「协议」留在内核里,把「具体读写逻辑」搬到了用户态进程中

这给了我们一个机会:用普通用户态进程的方式实现自定义文件系统,例如:

  • sshfs(远程目录挂载)
  • rclone(云存储挂载)
  • 以及本文讨论的 Scorpio

2.2 FUSE 架构#

┌─────────────────────────────────────────────────────────────────────────┐
│                           用户空间 (User Space)                        │
│                                                                         │
│   ┌─────────────┐         ┌──────────────────────────────────────┐     │
│   │ Application │         │     FUSE Daemon (用户态文件系统)      │     │
│   │  (ls, cat)  │         │                                      │     │
│   └──────┬──────┘         │  ┌──────────────────────────────┐   │     │
│          │                │  │  实现 read/write/lookup 等    │   │     │
│          │ open/read      │  │  例如: Scorpio, sshfs, rclone │   │     │
│          │                │  └──────────────────────────────┘   │     │
│          │                └──────────────────┬───────────────────┘     │
└──────────│───────────────────────────────────│─────────────────────────┘
           │                                   │
           │ syscall                           │ /dev/fuse
           ▼                                   ▼
┌──────────────────────────────────────────────────────────────────────────┐
│                           内核空间 (Kernel Space)                       │
│                                                                          │
│   ┌─────────────┐      ┌──────────────┐      ┌─────────────────┐        │
│   │     VFS     │ ───► │ FUSE Kernel  │ ───► │   /dev/fuse     │        │
│   │ (虚拟文件   │      │   Module     │      │ (字符设备)       │        │
│   │  系统层)    │      │              │      │                 │        │
│   └─────────────┘      └──────────────┘      └─────────────────┘        │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘
text

2.3 FUSE 请求处理流程#

cat /mnt/fuse/file.txt 为例:

1. 应用程序调用 open("/mnt/fuse/file.txt", O_RDONLY)


2. 系统调用进入内核 VFS 层


3. VFS 发现挂载点属于 FUSE,转发请求到 FUSE 内核模块


4. FUSE 内核模块将请求序列化,写入 /dev/fuse


5. 用户态 FUSE 守护进程从 /dev/fuse 读取请求


6. 守护进程处理请求(如从网络获取文件内容)


7. 守护进程将响应写回 /dev/fuse


8. FUSE 内核模块将响应返回给 VFS


9. 应用程序收到 open() 的返回值
text

2.4 FUSE 核心操作#

FUSE 要求实现一组基础文件系统操作,对应到常见系统调用:

操作描述对应系统调用
lookup查找目录项stat, access
getattr获取文件属性stat, fstat
readdir读取目录内容readdir, getdents
open打开文件open
read读取文件内容read, pread
write写入文件内容write, pwrite
create创建文件creat, open(O_CREAT)
mkdir创建目录mkdir
unlink删除文件unlink, remove
rename重命名rename

2.5 Rust 中的 FUSE 实现#

Scorpio 使用的是 rfuse3 库,提供了异步 FUSE 支持。实现上就是在一个 trait 里把这些操作补全:

use rfuse3::raw::{Filesystem, Request};

impl Filesystem for MyFS {
    async fn lookup(&self, req: Request, parent: u64, name: &OsStr) -> Result<ReplyEntry> {
        // 查找 parent 目录下名为 name 的文件
        // 返回文件的 inode 和属性
    }

    async fn read(&self, req: Request, ino: u64, offset: i64, size: u32) -> Result<ReplyData> {
        // 读取 inode 为 ino 的文件,从 offset 开始读取 size 字节
    }

    async fn write(&self, req: Request, ino: u64, offset: i64, data: &[u8]) -> Result<ReplyWrite> {
        // 将 data 写入到 inode 为 ino 的文件的 offset 位置
    }
}
rust

三、FUSE 进阶主题#

前面介绍了 FUSE 的基本工作原理,这一节我们深入探讨协议细节、性能局限以及不同实现库的差异。

3.1 FUSE 协议细节#

FUSE_INIT:握手与能力协商#

FUSE 会话从 FUSE_INIT 开始,内核与用户态守护进程通过这个消息协商协议版本和支持的功能:

┌─────────────────┐                    ┌─────────────────┐
│   FUSE Kernel   │                    │   FUSE Daemon   │
│     Module      │                    │   (Scorpio)     │
└────────┬────────┘                    └────────┬────────┘
         │                                      │
         │  FUSE_INIT request                   │
         │  ┌─────────────────────────────┐     │
         │  │ major: 7                    │     │
         │  │ minor: 38                   │     │
         │  │ max_readahead: 131072       │     │
         │  │ flags: ASYNC_READ |         │     │
         │  │        WRITEBACK_CACHE |    │     │
         │  │        PARALLEL_DIROPS | ...│     │
         │  └─────────────────────────────┘     │
         │ ─────────────────────────────────────►
         │                                      │
         │  FUSE_INIT reply                     │
         │  ┌─────────────────────────────┐     │
         │  │ major: 7                    │     │
         │  │ minor: 38                   │     │
         │  │ max_write: 1048576          │     │
         │  │ flags: ASYNC_READ |         │     │
         │  │        BIG_WRITES | ...     │     │
         │  └─────────────────────────────┘     │
         │ ◄─────────────────────────────────────
         │                                      │
text

关键协商参数:

参数说明
major/minor协议版本,双方取最小值
max_readahead内核预读窗口大小
max_write单次写请求最大字节数
flags能力标志位(见下表)

常用能力标志:

Flag作用Scorpio 使用
FUSE_ASYNC_READ允许并发读请求
FUSE_WRITEBACK_CACHE启用内核写回缓存
FUSE_PARALLEL_DIROPS允许并发目录操作
FUSE_BIG_WRITES支持大于 4KB 的写请求
FUSE_READDIRPLUSreaddir 同时返回属性
FUSE_PASSTHROUGH直接 I/O 穿透(Linux 6.9+)🔜

版本兼容#

FUSE 协议版本演进(节选):

版本引入特性
7.23FUSE_WRITEBACK_CACHE
7.28FUSE_PARALLEL_DIROPS
7.31FUSE_EXPLICIT_INVAL_DATA
7.38FUSE_PASSTHROUGH(实验性)

Scorpio 基于 rfuse3,目前支持到 7.31+,对低版本内核会自动降级禁用新特性。

3.2 FUSE 的性能局限#

尽管 FUSE 提供了极大的灵活性,但其架构决定了几个固有瓶颈:

用户态-内核态切换开销#

传统文件系统 (ext4):
  App → syscall → VFS → ext4 → 返回
        └─────────────────────────┘
              1 次上下文切换

FUSE 文件系统:
  App → syscall → VFS → FUSE kernel → /dev/fuse

        ┌─────────────────────────────────┘


  FUSE daemon (用户态处理) → /dev/fuse → FUSE kernel → 返回
        └─────────────────────────────────────────────────┘
                        4 次上下文切换
text

这意味着:

  • 每次文件操作至少 4 次用户/内核态切换
  • 数据需要在内核缓冲区和用户态之间 多次拷贝
  • 对于小文件密集型 I/O(如 npm install),开销显著

实测对比#

操作ext4FUSE (典型)开销倍数
顺序读 4KB3 μs15 μs~5x
随机读 4KB8 μs35 μs~4x
stat()0.5 μs5 μs~10x
readdir 100 项20 μs150 μs~7x

数据来源:基于 Linux 5.15 的典型 benchmark,实际结果因场景而异。

3.3 性能优化与 Bypass 方案#

为了解决 FUSE 的性能瓶颈,业界有几种探索方向:

3.3.1 FUSE Passthrough(Linux 6.9+)#

从 Linux 6.9 开始,FUSE 引入了 passthrough 模式:对于已知的本地文件,可以绕过用户态守护进程,直接由内核完成 I/O。

普通 FUSE 读取:
  read() → kernel → /dev/fuse → daemon → 读取后端 → 返回

Passthrough 模式:
  read() → kernel → 直接读取后端文件 → 返回
                    (绕过 daemon)
text

适用场景:

  • Overlay 的 upper/lower 层是本地文件
  • 只读文件的缓存命中后

Scorpio 的 libfuse-fs 正在评估 passthrough 支持,可显著降低已缓存文件的访问延迟。

3.3.2 io_uring 与 FUSE#

io_uring 提供了高性能的异步 I/O 接口,与 FUSE 结合可以:

  • 减少系统调用次数(批量提交)
  • 降低上下文切换开销
  • 支持零拷贝(特定场景)
// rfuse3 未来可能的 io_uring 集成方向
// (当前仍使用 epoll/kqueue)
let ring = IoUring::new(256)?;
ring.submitter().register_files(&[fuse_fd])?;
rust

目前 rfuse3 仍基于 tokio 的 epoll 模型,io_uring 支持在路线图中。

3.3.3 virtiofs:虚拟化场景的最优解#

对于虚拟机/容器场景,virtiofs 提供了比传统 FUSE 更好的性能:

┌─────────────────┐     ┌─────────────────┐
│   Guest VM      │     │      Host       │
│                 │     │                 │
│  ┌───────────┐  │     │  ┌───────────┐  │
│  │ virtiofs  │──┼─────┼─►│ virtiofsd │  │
│  │  (kernel) │  │     │  │           │  │
│  └───────────┘  │     │  └───────────┘  │
│                 │     │        │        │
│  共享内存直通   │     │        ▼        │
│  (DAX window)   │◄────┼──── Host FS     │
└─────────────────┘     └─────────────────┘
text

优势:

  • 使用 virtio 协议,减少虚拟化开销
  • 支持 DAX(直接访问)模式,零拷贝
  • Guest 内文件操作延迟接近 Host 原生
方案适用场景延迟吞吐
传统 FUSE通用
FUSE + Passthrough本地后端
virtiofsVM/容器很高
virtiofs + DAXVM极低极高

3.4 FUSE 库对比#

不同语言和场景有多种 FUSE 库可选:

语言异步特点生态
libfuseC官方参考实现,功能完整⭐⭐⭐⭐⭐
go-fuseGo✅ (goroutine)高级封装,易用⭐⭐⭐⭐
fuserRust同步 API,简单稳定⭐⭐⭐
rfuse3Rust✅ (tokio)异步,底层控制强⭐⭐⭐
polyfuseRust实验性,API 友好⭐⭐

为什么 Scorpio 选择 rfuse3?#

  1. 异步原生:Scorpio 大量网络 I/O,需要与 tokio 生态无缝集成
  2. 底层控制:可以精确控制 FUSE_INIT 协商的能力标志
  3. 性能导向:支持 READDIRPLUSPARALLEL_DIROPS 等优化特性
  4. Rust 安全性:内存安全,适合长期运行的守护进程
// rfuse3 的 MountOptions 配置示例
let mut options = MountOptions::default();
options
    .force_readdir_plus(true)      // 启用 READDIRPLUS
    .uid(getuid())
    .gid(getgid());
rust

libfuse vs rfuse3 API 对比#

// libfuse (C) - 同步回调
static int my_read(const char *path, char *buf, size_t size,
                   off_t offset, struct fuse_file_info *fi) {
    // 同步读取,阻塞当前线程
    return pread(fi->fh, buf, size, offset);
}
c
// rfuse3 (Rust) - 异步回调
async fn read(
    &self,
    _req: Request,
    inode: u64,
    fh: u64,
    offset: u64,
    size: u32,
) -> Result<ReplyData> {
    // 异步读取,不阻塞 executor
    let data = self.backend.read(inode, offset, size).await?;
    Ok(ReplyData { data })
}
rust

四、Scorpio 整体架构#

4.1 设计目标#

Scorpio 的整体设计可以概括为四个核心目标:

  1. 按需加载:访问某个文件时才从服务器拉取内容,节省带宽和本地磁盘
  2. 本地读写:在挂载目录下表现得像一个普通仓库,支持常规读写操作
  3. 版本控制:与 Git 流程集成,支持 commitpush 等操作
  4. 构建隔离:为 CI/CD 提供独立的构建工作空间

4.2 分层架构#

┌─────────────────────────────────────────────────────────────────────────┐
│                              应用层                                    │
│                                                                         │
│   开发者工具 (IDE, 编译器, 脚本)                                         │
│         │                                                               │
│         ▼                                                               │
│   /workspace/src/main.rs  (挂载点)                                      │
└───────────────────────────────────┬─────────────────────────────────────┘

┌───────────────────────────────────▼─────────────────────────────────────┐
│                            FUSE 接口层                                 │
│                                                                         │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │                         MegaFuse                               │   │
│   │                                                                │   │
│   │   职责:                                                        │   │
│   │   - 实现 FUSE Filesystem trait                                 │   │
│   │   - 管理多个 OverlayFs 实例                                     │   │
│   │   - Inode 分配和映射                                            │   │
│   │   - 将请求路由到对应的文件系统层                                 │   │
│   └─────────────────────────────────────────────────────────────────┘   │
└───────────────────────────────────┬─────────────────────────────────────┘

┌───────────────────────────────────▼─────────────────────────────────────┐
│                           联合文件系统层                                │
│                                                                         │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │                        OverlayFs                               │   │
│   │                                                                │   │
│   │   ┌───────────────────────────────────────────────────────┐     │   │
│   │   │  Upper Layer (读写层)                                  │     │   │
│   │   │  - Passthrough 到本地目录                              │     │   │
│   │   │  - 所有写操作都在这里                                   │     │   │
│   │   │  - Copy-on-Write 语义                                  │     │   │
│   │   ├───────────────────────────────────────────────────────┤     │   │
│   │   │  CL Layer (可选,变更列表层)                            │     │   │
│   │   │  - 用于 CI/CD 场景的增量变更                            │     │   │
│   │   ├───────────────────────────────────────────────────────┤     │   │
│   │   │  Lower Layer (只读层)                                  │     │   │
│   │   │  - Dicfuse 虚拟文件系统                                 │     │   │
│   │   │  - 从 Mega 服务器按需拉取                               │     │   │
│   │   └───────────────────────────────────────────────────────┘     │   │
│   └─────────────────────────────────────────────────────────────────┘   │
└───────────────────────────────────┬─────────────────────────────────────┘

┌───────────────────────────────────▼─────────────────────────────────────┐
│                            数据存储层                                  │
│                                                                         │
│   ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐     │
│   │   Tree Store     │  │  Content Store   │  │   Local Store    │     │
│   │                  │  │                  │  │                  │     │
│   │  目录树元数据     │  │  文件内容缓存    │  │  本地修改数据     │     │
│   │  (sled DB)       │  │  (内存+磁盘)     │  │  (passthrough)   │     │
│   └──────────────────┘  └──────────────────┘  └──────────────────┘     │
└───────────────────────────────────┬─────────────────────────────────────┘

┌───────────────────────────────────▼─────────────────────────────────────┐
│                            网络层                                      │
│                                                                         │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │                      Mega Server API                           │   │
│   │                                                                │   │
│   │  GET /api/v1/tree/{commit_id}     获取目录树                    │   │
│   │  GET /api/v1/blob/{blob_id}       获取文件内容                  │   │
│   │  POST /api/v1/commit              提交变更                      │   │
│   └─────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘
text

4.3 模块职责概览#

模块位置职责
MegaFusefuse/mod.rsFUSE 入口,管理多个 overlay 实例
Dicfusedicfuse/mod.rs只读虚拟层,提供目录树和按需加载
OverlayFslibfuse-fs联合文件系统,合并多层视图
Antaresantares/轻量级挂载,用于 CI/CD 隔离
Managermanager/Git 操作:commit, push, diff
Daemondaemon/HTTP API 服务

五、核心组件拆解#

5.1 Dicfuse:按需加载的只读层#

Dicfuse 是 Scorpio 里的「虚拟只读层」,实现了一个”字典式”文件系统:目录树结构和文件内容分开存储,内容按需加载。

pub struct Dicfuse {
    readable: bool,
    pub store: Arc<DictionaryStore>,  // 元数据存储
}

impl Dicfuse {
    /// 按需加载文件内容
    async fn load_one_file(&self, parent: u64, name: &OsStr) -> std::io::Result<()> {
        // 1. 查找父目录的 Tree 对象
        let parent_item = self.store.find_path(parent).await?;
        let tree = fetch_tree(&parent_item).await?;

        // 2. 在 Tree 中找到目标文件
        for item in tree.tree_items {
            if item.name == name && item.mode == Blob {
                // 3. 从服务器拉取 Blob 内容
                let url = format!("{}/{}", blob_endpoint, item.id);
                let content = client.get(url).send().await?.bytes().await?;

                // 4. 缓存到本地
                self.store.save_file(inode, content.to_vec());
            }
        }
        Ok(())
    }
}
rust

读取时的关键路径:

                     首次访问 /workspace/src/main.rs


              ┌─────────────────────────────────────────┐
              │  Dicfuse.lookup("src", "main.rs")       │
              │                                         │
              │  1. 检查本地缓存 → 未命中               │
              │  2. 查询 Tree Store 获取元数据          │
              │     → 找到: inode=42, size=1024        │
              │  3. 返回文件属性(不加载内容)          │
              └─────────────────────────────────────────┘


              ┌─────────────────────────────────────────┐
              │  Dicfuse.read(inode=42, offset=0)       │
              │                                         │
              │  1. 检查 Content Store → 未命中         │
              │  2. 从 Mega 服务器拉取 Blob             │
              │     GET /api/v1/blob/{sha1}            │
              │  3. 存入 Content Store                  │
              │  4. 返回文件内容                        │
              └─────────────────────────────────────────┘
text

目录结构可以提前知道,真正的内容只有在需要时才从服务器拉下来。

5.2 OverlayFs:联合文件系统#

OverlayFs 把多层文件系统组合成一个统一视图,是「只读远程层 + 本地读写层」这个设计的核心。

                    用户视角(合并视图)
                    /workspace/
                    ├── src/
                    │   ├── main.rs      (来自 Upper,已修改)
                    │   └── lib.rs       (来自 Lower,只读)
                    └── Cargo.toml       (来自 Lower,只读)

         ═══════════════════════════════════════════════════

            ┌─────────────────┼─────────────────┐
            ▼                 ▼                 ▼
     ┌─────────────┐  ┌─────────────┐  ┌─────────────┐
     │ Upper Layer │  │  CL Layer   │  │ Lower Layer │
     │  (读写)     │  │  (可选)     │  │  (只读)     │
     ├─────────────┤  ├─────────────┤  ├─────────────┤
     │ src/        │  │             │  │ src/        │
     │   main.rs ✏ │  │             │  │   main.rs   │
     │             │  │             │  │   lib.rs    │
     │             │  │             │  │ Cargo.toml  │
     └─────────────┘  └─────────────┘  └─────────────┘
text
  • 读操作优先级Upper > CL > Lower
  • 写操作:始终写入 Upper 层(Copy-on-Write)
impl MegaFuse {
    /// 挂载一个 overlay 文件系统
    pub async fn overlay_mount(
        &self,
        inode: u64,
        store_path: &Path,
        need_cl: bool,
        cl_link: Option<&str>,
    ) -> std::io::Result<()> {
        // 构建层结构
        let lower = store_path.join("lower");   // Dicfuse 映射
        let upper = store_path.join("upper");   // 本地写入层

        let mut lower_layers = vec![];

        // 可选的 CL 层
        if need_cl {
            let cl_path = store_path.join("cl").join(cl_link);
            lower_layers.push(new_passthroughfs_layer(cl_path));
        }

        // 添加只读下层
        lower_layers.push(new_passthroughfs_layer(lower));

        // 创建读写上层
        let upper_layer = new_passthroughfs_layer(upper);

        // 组装 OverlayFs
        let overlayfs = OverlayFs::new(
            Some(upper_layer),  // 读写层
            lower_layers,       // 只读层列表
            config,
            inode,
        )?;

        self.overlayfs.lock().await.insert(inode, Arc::new(overlayfs));
        Ok(())
    }
}
rust

5.3 Inode 管理#

FUSE 通过 inode 唯一标识文件。Scorpio 需要在多个 overlay 实例之间避免 inode 冲突,因此会做「按批分配」:

pub struct InodeAlloc {
    // 每个 overlay 分配一个 inode 区间
    // 避免不同 overlay 的 inode 冲突
    allocations: Mutex<HashMap<u64, InodeBatch>>,
}

struct InodeBatch {
    start: u64,
    end: u64,
    next: u64,
}

impl InodeAlloc {
    /// 为新的 overlay 分配 inode 批次
    pub async fn alloc_inode(&self, overlay_inode: u64) -> InodeBatch {
        let mut alloc = self.allocations.lock().await;

        // 计算新的区间
        let batch_size = 0x1000_0000;  // 每个 overlay 分配 256M 个 inode
        let start = overlay_inode * batch_size;
        let end = start + batch_size - 1;

        let batch = InodeBatch { start, end, next: start + 1 };
        alloc.insert(overlay_inode, batch);
        batch
    }
}
rust

5.4 Antares:CI/CD 构建隔离#

Antares 是基于 Scorpio 抽出来的一个「面向 CI/CD 的挂载层」,关注点是:

  • 为每个构建 Job 提供独立的 Upper 层
  • 共享只读的 Dicfuse 层,节省内存和网络
┌──────────────────────────────────────────────────────────────┐
│                       CI/CD Pipeline                        │
│                                                              │
│  Job 1 ─────► Antares Mount ─────► /mnt/job1/                │
│               (独立工作空间)        ├── src/ (Dicfuse)        │
│                                    └── build/ (Upper)        │
│                                                              │
│  Job 2 ─────► Antares Mount ─────► /mnt/job2/                │
│               (独立工作空间)        ├── src/ (Dicfuse)        │
│                                    └── build/ (Upper)        │
│                                                              │
│  共享只读层:Dicfuse(单例,节省内存)                        │
└──────────────────────────────────────────────────────────────┘
text

实现上只是对 overlay 的一个包装:

pub struct AntaresFuse {
    pub mountpoint: PathBuf,
    pub upper_dir: PathBuf,           // 独立的写入层
    pub dic: Arc<Dicfuse>,            // 共享的只读层
    pub cl_dir: Option<PathBuf>,      // 可选的 CL 层
    fuse_task: Option<JoinHandle<()>>,
}

impl AntaresFuse {
    /// 构建 overlay 并挂载
    pub async fn mount(&mut self) -> std::io::Result<()> {
        let overlay = self.build_overlay().await?;

        // 启动 FUSE 会话
        let handle = mount_filesystem(overlay, &self.mountpoint).await;

        self.fuse_task = Some(tokio::spawn(async move {
            let _ = handle.await;
        }));

        Ok(())
    }

    /// 卸载
    pub async fn unmount(&mut self) -> std::io::Result<()> {
        // 调用 fusermount -u 卸载
        Command::new("fusermount")
            .arg("-u")
            .arg(&self.mountpoint)
            .output()
            .await?;
        Ok(())
    }
}
rust

六、关键路径拆解#

6.1 启动流程#

#[tokio::main]
async fn main() {
    // 1. 加载配置
    config::init_config("scorpio.toml")?;

    // 2. 初始化 ScorpioManager,检查工作目录状态
    let mut manager = ScorpioManager::from_toml(config_file)?;
    manager.check().await;  // 同步目录树元数据

    // 3. 创建 MegaFuse,挂载工作目录
    let fuse = MegaFuse::new_from_manager(&manager).await;

    // 4. 启动 FUSE 会话
    let mount_handle = mount_filesystem(fuse, mountpoint).await;

    // 5. 启动 HTTP daemon
    tokio::spawn(daemon_main(Arc::new(fuse), manager));

    // 6. 等待退出信号
    tokio::select! {
        _ = mount_handle => {},
        _ = signal::ctrl_c() => {
            mount_handle.unmount().await?;
        }
    }
}
rust

6.2 文件读取流程#

用户执行: cat /workspace/src/main.rs


┌─────────────────────────────────────────────────────────┐
│  1. VFS → FUSE 内核模块 → /dev/fuse                     │
│     FUSE_LOOKUP: parent=1, name="src"                   │
└────────────────────────┬────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│  2. MegaFuse.lookup(parent=1, name="src")               │
│     → 查找 overlay 映射                                  │
│     → 委托给 OverlayFs.lookup()                          │
│     → 返回 inode=2, attr={dir, mode=0755}               │
└────────────────────────┬────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│  3. MegaFuse.lookup(parent=2, name="main.rs")           │
│     → OverlayFs 按优先级查找:                           │
│       a. Upper 层:不存在                                │
│       b. Lower 层 (Dicfuse):存在元数据                  │
│     → 返回 inode=42, attr={file, size=1024}             │
└────────────────────────┬────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│  4. MegaFuse.open(inode=42, flags=O_RDONLY)             │
│     → 返回 file handle                                   │
└────────────────────────┬────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│  5. MegaFuse.read(inode=42, offset=0, size=4096)        │
│     → OverlayFs.read() → Dicfuse.read()                 │
│     → 检查 Content Store:未命中                         │
│     → HTTP GET /api/v1/blob/{sha1}                      │
│     → 缓存内容到 Content Store                           │
│     → 返回文件数据                                       │
└─────────────────────────────────────────────────────────┘
text

6.3 文件写入(Copy-on-Write)#

用户执行: echo "new content" >> /workspace/src/main.rs


┌─────────────────────────────────────────────────────────┐
│  1. MegaFuse.open(inode=42, flags=O_WRONLY|O_APPEND)    │
│     → OverlayFs 检测到写操作                             │
│     → 触发 Copy-on-Write                                │
└────────────────────────┬────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│  2. Copy-on-Write 过程:                                │
│     a. 从 Lower 层读取原始内容                           │
│     b. 在 Upper 层创建同名文件                           │
│     c. 将原始内容复制到 Upper 层                         │
│     d. 标记 Upper 层文件为"覆盖"                         │
└────────────────────────┬────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│  3. MegaFuse.write(inode=42, data="new content\n")      │
│     → 写入 Upper 层文件                                  │
│     → Lower 层原始文件保持不变                           │
└─────────────────────────────────────────────────────────┘

写入后的层结构:
┌─────────────┐
│ Upper Layer │  src/main.rs  ← 包含新内容
├─────────────┤
│ Lower Layer │  src/main.rs  ← 原始内容(被遮盖)
└─────────────┘
text

6.4 提交流程#

用户执行: scorpio commit -m "update main.rs"


┌─────────────────────────────────────────────────────────┐
│  1. 扫描 Upper 层变更                                    │
│     → 遍历 upper/ 目录                                   │
│     → 收集所有修改/新增/删除的文件                        │
└────────────────────────┬────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│  2. 构建 Git 对象                                        │
│     a. 为每个修改的文件创建 Blob 对象                    │
│     b. 构建新的 Tree 对象(合并变更)                    │
│     c. 创建 Commit 对象                                  │
│        - parent: 上一个 commit                          │
│        - tree: 新的根 tree                              │
│        - message: "update main.rs"                      │
└────────────────────────┬────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│  3. 推送到服务器                                         │
│     POST /api/v1/commit                                 │
│     Body: { objects: [...], commit: {...} }             │
└────────────────────────┬────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│  4. 更新本地状态                                         │
│     → 清空 Upper 层                                      │
│     → 更新 Tree Store 到新 commit                        │
└─────────────────────────────────────────────────────────┘
text

七、性能优化策略#

7.1 元数据预取#

目录树可以提前拉取,降低首次访问延迟:

impl Dicfuse {
    /// 批量预取目录内容
    pub async fn prefetch_directory(&self, inode: u64) {
        let tree = fetch_tree(inode).await;

        // 将所有子项元数据存入 Tree Store
        for item in tree.items {
            self.store.insert_metadata(item);
        }
    }
}
rust

7.2 内容缓存策略#

┌─────────────────────────────────────────────────────────┐
│                    缓存层次                              │
│                                                         │
│  L1: 内存缓存 (LRU,限制大小)                           │
│       ↓ miss                                            │
│  L2: 本地磁盘缓存 (Content Store)                       │
│       ↓ miss                                            │
│  L3: Mega 服务器                                        │
└─────────────────────────────────────────────────────────┘
text

7.3 并发控制#

对读多写少的场景,用 RwLock 做简单的读写分离:

pub struct DictionaryStore {
    tree_cache: RwLock<HashMap<u64, TreeNode>>,
    content_cache: RwLock<HashMap<u64, Vec<u8>>>,
}

// 读操作使用读锁,允许并发
async fn get_metadata(&self, inode: u64) -> Option<TreeNode> {
    self.tree_cache.read().await.get(&inode).cloned()
}

// 写操作使用写锁
async fn insert_metadata(&self, inode: u64, node: TreeNode) {
    self.tree_cache.write().await.insert(inode, node);
}
rust

八、与其他方案对比#

特性ScorpioGit Sparse CheckoutVFS for GitGitFS
按需加载❌(需预定义)
本地修改
版本控制✅(内置)✅(Git)✅(Git)
构建隔离✅(Antares)
实现方式FUSEGit 原生FUSEFUSE
平台支持Linux/macOS全平台WindowsLinux

九、使用示例#

9.1 基本使用#

# 启动 Scorpio
./scorpio -c scorpio.toml

# 查看挂载的工作空间
ls /workspace
# src/  Cargo.toml  README.md

# 像普通仓库一样使用
cd /workspace
cargo build
vim src/main.rs

# 提交变更
curl -X POST http://localhost:8000/api/commit \
  -H "Content-Type: application/json" \
  -d '{"message": "fix bug"}'
bash

9.2 Antares 在 CI/CD 中的使用#

# 启动 Antares daemon
./antares serve --bind 0.0.0.0:2726

# 创建构建环境
curl -X POST http://localhost:2726/mounts \
  -H "Content-Type: application/json" \
  -d '{
    "mountpoint": "/mnt/job1",
    "upper_dir": "/var/antares/upper/job1",
    "labels": ["ci", "build"],
    "readonly": false
  }'

# 在隔离环境中构建
cd /mnt/job1
./build.sh

# 清理
curl -X DELETE http://localhost:2726/mounts/{mount_id}
bash

十、总结#

从整体上看,Scorpio 用 FUSE 搭了一套适合大体量 monorepo 的访问方案:

  1. FUSE 提供基础设施:把文件系统逻辑放在用户态,避免内核开发成本
  2. Dicfuse 负责按需加载:目录树和文件内容解耦,内容按访问懒加载
  3. OverlayFs 提供本地读写语义:Copy-on-Write 保护远程只读层,同时允许本地修改
  4. Antares 面向 CI/CD 做隔离:为流水线构建提供轻量、可回收的工作空间

这种组合在以下场景特别有价值:

  • 超大体量 monorepo(数十 GB 甚至更大)
  • 频繁切换分支 / 项目子目录
  • CI/CD 构建隔离和缓存复用
  • 带宽或磁盘资源受限的环境

参考资料#

Scorpio:基于 FUSE 的 Monorepo 虚拟文件系统
https://jerry609.github.io/blog/scorpio-fuse-explained
Author Jerry
Published at December 11, 2025
Comment seems to stuck. Try to refresh?✨