为了回答:
请描述查找 /mnt/icfs/dir01/file.txt
的过程。
从零开发分布式文件系统(四):一道经典面试题,深度对比 CephFS 与 3FS 的元数据架构优劣 写8千字解释
为啥 DeepSeek-3FS元数据无状态,CephFS 的 元数据 要搞得这么复杂? 3千字解释
还是解释不清楚,
森林, 踪迹, 阳光, 森林路径, 小路, 树木, 早晨
一、一个问题,2个图
面试官:看你项目经历有实现过文件系统,请描述查找 /mnt/icfs/dir01/file.txt
的过程。
小王:
感谢您的提问。 在描述具体过程之前, 我想使用一次反问权利来澄清一下背景信息,因为这会影响查找的细节。
请问, 这个文件系统的集群规模多大?存储了多少文件?是1个、1亿还是10亿个文件?
因为: 不同的产品有不同愿景, 不同愿景会有不同架构 不同不同架构会不同使用场景 面对同一个问题就会不同处理方式别想太多--只管去面
下面是是回答依据
unsetunset小王回答:GFS查找过程:unsetunset
假设你要读取 GFS 上的文件 /data/weblog.txt
,起始位置是第 130 MB 处,流程如下:
- 第一步(偏移 → chunk 索引):
客户端拿文件路径
/data/weblog.txt
和偏移量 130 MB,计算该偏移属于哪个 chunk。
每个 chunk 是 64 MB,所以:- chunk 0:0 – 64 MB
- chunk 1:64 – 128 MB
- chunk 2:128 – 192 MB
所以 130 MB 落在 chunk 索引 = 2(第 3 个 chunk,从 0 开始计数)。
- 第二步(向 Master 查询元数据):
客户端向 Master 发送请求:“文件
/data/weblog.txt
的 chunk 索引 2 的 chunk handle 是什么?副本在哪里? Master 在内存中查命名空间 + 文件 → chunk 映射 + 副本列表,回应说:- chunk handle =
xyz123
- 副本在 ChunkServer A、ChunkServer C、ChunkServer F 上
- 第三步(客户端选择副本 + 读取数据):
客户端选一个最优副本(例如 A)然后发请求:
“给我 chunk handle = xyz123,从这个 chunk 内部偏移 2 MB 开始的数据。” (因为 130 MB − 128 MB = 2 MB)
然后 ChunkServer A 读取对应数据并返回给客户端
大街, 树木, 小路, 阳光, 此刻, 林地, 森林, 踪迹, 森林路径
unsetunset小王回答: Ceph 文件查找过程unsetunset
- 起点:连接根目录
客户端首先连接到一个已知的 MDS。
- 该 MDS 可能就是根目录
/
的权威 MDS,也可能不是。 - 根目录的权威 MDS 通常是预先确定的 MDS0。
- 逐级遍历路径
沿路径 /mnt/icfs/dir01/file.txt
逐级查找:
- 接收客户端请求的 接收 MDS 检查第一级目录
mnt
的权威 MDS。 - 如果
mnt
的权威 MDS 是自己,则直接查询本地元数据缓存。 - 如果不是,则将请求 转发 给正确的权威 MDS(例如 MDS-2)。
- MDS-2 解析其下目录
icfs
的目录项,并判断其权威 MDS。 - 逐级重复该过程,直到找到最终文件
file.txt
的 inode。
返回结果
- 找到 inode 后,信息沿原路返回客户端。
- 客户端获得 inode 后,直接与 OSD(对象存储守护进程)交互,完成文件数据的读写。
unsetunset3FS 文件访问流程unsetunset
1. 路径解析
- 客户端根据文件路径(如
/mnt/icfs/dir01/file.txt
)向 元数据服务(MDS) 请求文件信息。 - MDS 解析路径,找到对应的 inode。
2. inode → chunk 列表
- inode 中记录了文件的逻辑数据块(chunk)列表。
- 每个 chunk 对应一个 链(Chain),用于存储多个副本。
3. chunk → 链/链表
- 每个 chunk 会被放到一个 链链表(Chain List) 上,链上包含多个存储目标(Storage Target)。
- 链表可区分不同的业务场景,如在线服务或离线批处理。
4. 客户端访问链节点
- 写操作:数据写入链头节点 → 沿链传播 → 链尾确认完成。
- 读操作:客户端从链上任意节点读取数据 → 返回数据给客户端。
二、GFS
GFS
数据存储服务chunkserver
- 每个 chunk 都会存上整整三份副本(replica)。其中一份是主数据(primary),两份是副数据(secondary)
- GFS存储建立普通文件系统之上EXT4满足大文件存储,内部文件没有indoe概念只有全局编号
- GFS不满足全部文件语义操作
元数据服务
- 客户端会发出两部分信息,一个是文件名,另一个则是要读取哪一段数据,也就是读取文件的 offset 及 length 当做key ,value 数据存储位置
- 类似块存储概念
unsetunset1.1 输入 文件路径+偏移量 返回什么?unsetunset
假设与前置设定
- 假设 chunk 大小 = 64 MB(在 GFS 中常用值)
- 假设文件路径
/data/logs/app.log
,该文件在 Master 的元数据中对应若干 chunk - 假设这个文件在内部被分割成多个 chunk,chunk index 从 0 开始编号
- 假设该文件最少有 3 个 chunk:chunk 0, chunk 1, chunk 2
- 假设当前客户端请求的偏移量(byte offset)是 100 MB 处(即偏移量 = 100 * 2^20 字节 ≈ 104857600 字节)
- 假设副本数 = 3(GFS 默认一般是 3 份副本)
- 假设 Master 内存中有如下映射(示意):
- “CSx” 表示某个 chunkserver 节点(例如机器标识符)
输入
- 文件路径:
/data/logs/app.log
- 偏移量:100 MB (即字节偏移量大致 be 100 × 2^20 = 104,857,600 bytes)
计算 chunk index
- 客户端/GFS 客户端库会先把偏移量映射到文件的哪一个 chunk。
- chunk 0:范围第 0 ~ 63,999,999 字节
- chunk 1:范围第 64,000,000 ~ 127,999,999 字节
- chunk 2:范围第 128,000,000 ~ 191,999,999 字节
- 100 MB 落在 chunk 1 的区间(因为 64 MB ≤ 100 MB < 128 MB)
→ 所以 chunk index = 1
客户端因此知道要访问这个文件的第 1 块。
向 Master 请求元数据
客户端向 Master 发送请求,请求内容可能包含:
- 文件路径
/data/logs/app.log
- chunk index = 1
Master 接收到这个请求后:
- 在命名空间结构中确认
/data/logs/app.log
存在 - 在内部的文件→chunk 映射表里查这条映射:对于该文件,第 1 块对应 chunk handle = H1
- 查副本位置信息:该 chunk 的副本存在哪些 chunkservers?查询得出 CS2、CS4、CS5
- Master 将这条响应返回给客户端
输出(Master 返回给客户端)
Master 的返回结果会包含:
- chunk handle = H1
- 副本列表 = [ CS2, CS4, CS5 ]
客户端选取副本 / 发起读取请求
客户端从副本列表 [ CS2, CS4, CS5 ] 中选择一个(例如离自己最近、网络延迟低、负载低的 CS4):
- 客户端向 CS4 发送读取请求,内容包括:
- chunk handle = H1
- 偏移量在该 chunk 内部的偏
unsetunset1.2 GFS 没有文件inode每个如何表示tree结构unsetunset
GFS的元数据与“树”结构
虽然GFS没有inode,但它同样需要管理文件和目录,形成树状结构。
- 元数据的三大支柱:Master节点主要管理三类元信息:
- 命名空间(Namespace):这就是GFS的“目录树”,它是一个逻辑结构,记录了所有的文件和目录路径,例如 /data/weblog.txt。
- 文件到块的映射:记录每个文件具体由哪些数据块(Chunk)组成。Master会为每个Chunk分配一个全局唯一的64位标识符,称为 Chunk Handle。
- 块的位置信息:记录每个Chunk副本存储在哪些ChunkServer上。值得注意的是,这部分信息Master并不持久化存储,而是通过ChunkServer定期上报的心跳信息来动态获取和更新。
unsetunset1.3 GFS使用场景unsetunset
| | |
---|
| | |
| | |
| | |
| | |
| | |
追加写 (record append),16 客户端 | | |
| | |
- GFS论文是2003年的数据:
- 当时典型机器是 1 GHz CPU、几十 MB/s 磁盘带宽。
- 网络一般是 100 Mbps 或 1 Gbps,不是现代高速 100G。
- 表格里的吞吐量(10 MB/s 单客户端读)在当时已经算不错了。
- 所以绝对值与现代硬件带宽不可比,不是设计瓶颈,而是时代限制
GFS 的愿景
- 用廉价的商用服务器组成集群,处理 PB 级别 / 大规模数据。
- 重点是 高吞吐量 和 _容错性_。例如,顺序读写要很快,节点故障要能自动恢复,副本机制容忍硬件故障。
GFS 的适用场景
- 大数据分析、日志存储、搜索索引构建。这类场景写入很多、追加写入常见、顺序读取多。
- 批量处理任务比交互式任务多。例如 MapReduce 作业、数据聚合、批日志处理等。
GFS 的局限性 /取舍
- 随机读写性能较差:对小文件频繁读取、修改会导致效率低,因为每次访问都要请求 Master 获取元数据,且 Chunk 大小较大,浪费 I/O。
- 小文件开销高:很多元数据(命名空间、文件→Chunk 映射、权限等)都要由 Master 管理。小文件多会占大量元数据,Master 内存压力、管理复杂性都增大。
- POSIX 语义支持不完全:GFS 不严格支持所有 POSIX 特性(例如文件锁定、原子 rename、读写一致性等方面有放宽)
- Master 的单点:Master 节点集中管理元数据,是潜在瓶颈 /单点故障(尽管设计中有日志 /重启 /副本机制,但原始 GFS 中 Master 是关键且风险点) [
二、Ceph
unsetunset2.1 先看一组数据unsetunset
节点数量与性能
延迟与吞吐量
- Ceph:愿景是 统一存储系统(块、文件、对象),
- 强调 可扩展、高可用、分布式一致性。 这里并没有性能
- 去中心化元数据管理(CRUSH 算法定位对象),MDS 只负责目录树。
MDS节点个数超过128个,单节点性能急速下降,性能延迟增加
- 小集群(1-8 节点)
- MDS 吞吐非常高,尤其是
openssh+include
(>4000 ops/s),openshared
和 openssh+lib
也在 2000-3000 ops/s。 - 单节点 MDS 可承载大量操作,因为并发冲突和分布式协调开销很小。
- 中等规模(16-64 节点)
- 吞吐开始下降,每个 MDS 处理的 ops/s 逐渐降低。
- 说明随着 MDS 集群增大,跨节点协调、锁争用、元数据分布开销增加。
- 大集群(64-128 节点)
openssh+include
保持最高吞吐makefiles
最低(<1000 ops/s),说明创建小文件操作对 MDS 压力大。- 每个 MDS 吞吐继续下降到 ~500-2500 ops/s 不等。
- 大规模集群下,负载分散,单 MDS 吞吐下降,但整体集群总吞吐可能仍高。
- 不同操作差异明显:
1️⃣ 愿景
- 大规模、可扩展、高可靠统一存储(对象、块、文件)。
- 去中心化,自动平衡和故障恢复。
2️⃣ 架构
- RADOS:核心对象存储,CRUSH 算法决定数据分布。
- OSD:存储数据和副本。
- MON:集群状态监控。
- MDS:CephFS 的元数据管理。
- RBD / CephFS:块存储 / 文件系统接口。
3️⃣ 使用场景
- 云平台 VM 虚拟磁盘
- 大数据 / AI 数据存储
- 企业文件共享 / NAS
- 长期备份与归档
4️⃣ 技术选择亮点
- 存储方式:对象为主,块和文件为接口
- 可靠性:副本或纠删码
- 性能优化:BlueStore + 客户端直连 OSD
- 元数据管理:RocksDB + MDS 单线程
unsetunset2.2 元数据节点单线,无锁冲突,为什么性能还是这么慢unsetunset
问:ceph元数据单节点性能为么这么慢?不是因为单线慢了,更维护一致性
回答:ceph 设计目标可扩展,非低延迟,
底层数据对象存储(块存储),通过crush算法完全去中心化,
然后增加文件系统这个功能后,底层数据对象存储根本不负责解决这个事情,
不提供kv查询,
元数据节点承担 维护目录树,一致性责任
例如节点重启时候, 为了减少对存储池查询, 通过不同元数据节点协商 动态加载整个目录树, 分布式锁 缓存一致性
mds状态
为什么慢
- 文件元数据比 KV 重:路径解析、权限校验、目录更新、link/count、目录遍历等多步逻辑,远比简单的 get/set 多 I/O 与 CPU 步骤。
- 元数据写入要多次同步:修改通常要写 RocksDB/WAL、同步到 BlueStore/RADOS,产生写放大和 fsync 延迟。
- 后端延迟:RocksDB compaction、磁盘(尤其是非 NVMe)延迟、网络往返都会拉长一次元数据操作的时延。
- 大量小文件/随机操作:每个操作都得走 MDS,无法批处理,容易把 MDS 压垮。
- 热点不均衡:少数热目录会集中在同一个 MDS 上,造成单点饱和。
- 客户端缓存/lease 失效:缓存未命中或需要回退/验证,会增加额外交互。
- 跨 MDS 协调成本:当目录被拆分到多个 MDS 时,跨子树操作需要分布式协调/锁,短期内会变慢。
- 后台任务影响:compaction、rebalancing、GC、快照合并等会占用 I/O/CPU,抬高延迟。
2.3 架构原因
三. 3FS(Fire-Flyer File System)
unsetunset3.1 测试数据unsetunset
| |
---|
| 6.6 TiB/s(在 180 节点、500+ 客户端的读压测中) |
| |
| 3.66 TiB/min(排序 110.5 TiB 数据用时 30 分 14 秒) |
| |
| 在 AWS 上用 SoftRCoE + 大规格实例做 FIO,可把实例带宽压满,验证在无 IB 的云环境也能获得高吞吐 |
| 指出“性能高度依赖硬件/网络/测试方法”,建议做现实性核查(瓶颈归因) |
unsetunset3.2 关键点unsetunset
unsetunset🔹 技术愿景unsetunset
- 面向 AI 训练与推理、高性能计算(HPC)场景。
- 提供 高吞吐、低延迟、可扩展 的分布式文件系统。
- 支持海量小文件与大文件,兼顾强一致性与文件系统语义。
unsetunset🏗 架构设计unsetunset
核心组件(全部通过 RDMA 互连):
- Cluster Manager(集群管理器)
- 管理集群成员、配置分发、主备切换。
- 配置存储在可靠 KV 存储(如 FoundationDB/ZooKeeper/etcd)。
- Metadata Service(元数据服务)
- 处理文件系统操作:open、create、rename 等。
- 无状态,元数据存储在事务性 KV 存储(FoundationDB)。
- Storage Service(存储服务)
- 管理本地 SSD,提供 chunk 存储。
- 使用 Chain Replication with Apportioned Queries (CRAQ) 实现强一致性。
- 文件拆分成 chunk,跨多 SSD 链式复制。
- Client(客户端)
- FUSE 客户端:易用,兼容多数应用。
- Native 客户端:支持异步零拷贝 I/O,提高性能。
- 客户端可直接计算 chunk ID 与链表,减少元数据依赖。
unsetunset⚙️ 技术选型unsetunset
- 网络:RDMA(InfiniBand / RoCE)低延迟高带宽。
- 存储:本地 SSD,链式复制,支持高并发访问。
- 元数据管理:FoundationDB,事务性 KV 存储,Serializable Snapshot Isolation。
- 文件系统接口:
- 支持原子目录操作、符号/硬链接。
- 保持 POSIX 文件接口,便于现有应用迁移。
- 优化:
- 异步零拷贝 API(类似 io_uring)。
- Chunk 物理存储分配池 + Copy-on-write 元数据更新。
- 大文件 stripe,按目录可配置链表、chunk/stripe 尺寸。
- 恢复过程最小化对正常 I/O 的干扰。
- FUSE 对小随机读性能有限,native 客户端解决。
unsetunset数据放置与复制unsetunset
- Chunk 存储
- 文件分块,跨多个链复制。
- 写入:head → tail;读取:任意 replica。
- CRAQ 协议优化读密集型 workload。
链式复制 (Chain Replication) 与 CRAQ
这是一种强一致性的复制协议。
链式复制(Chain Replication)
写入只能将请求发送至head节点,写入流程:
- 写入路径(head → tail): 所有写入请求都必须发送到链的头部(Head),然后由头部依次同步到链中的下一个副本,直到传递到尾部(Tail)。尾部确认后,才代表整个写入成功。这确保了所有副本都以相同的顺序接收数据,保证了强一致性。
- 读取路径(任意副本): 读取请求可以发送到链上的任何一个副本。这极大地扩展了读取吞吐量,因为读流量可以被均匀分摊到所有副本上,而不用像主从复制那样只从主副本读取。
- CRAQ优化: 这是对基础链式复制的改进。它允许链上所有副本都处理读请求,但副本之间通过版本号等机制来协调,确保读取到的总是已提交的最新数据,从而在保持强一致性的同时获得了极高的读性能。