摘要:与众多业界领先的分布式文件系统架构相仿,3FS系统整体架构由四大核心组件构成:Cluster Manager(集群管理器)、Client(客户端)、Meta Service(元数据服务)以及Storage Service(存储服务)。这些组件均通过RDMA网络
与众多业界领先的分布式文件系统架构相仿,3FS系统整体架构由四大核心组件构成:Cluster Manager(集群管理器)、Client(客户端)、Meta Service(元数据服务)以及Storage Service(存储服务)。这些组件均通过RDMA网络实现高速互联,而在DeepSeek内部,实际采用的是InfiniBand技术。
Cluster Manager作为整个集群的中枢控制系统,肩负着节点管理的重任。为了确保自身的高可用性,Cluster Manager采用了多节点热备方案,并复用Meta Service所依赖的FoundationDB来实现选主机制。Meta Service与Storage Service的所有节点,均通过周期性的心跳机制来维持其在线状态。一旦这些节点的状态发生变化,Cluster Manager将负责在整个集群内进行通知。
Client则通过心跳机制向Cluster Manager汇报其在线状态。若Client失去联系,Cluster Manager将负责回收该Client上的文件写打开状态。Client提供了两种客户端接入方案:FUSE客户端hf3fs_fuse,以其易用性和对常见POSIX接口的支持,能够快速对接各类应用,尽管在性能方面并非最优;而原生客户端USRBIO则提供了SDK接入方式,虽然需要应用进行代码改造才能使用,但其性能相比FUSE客户端可提升3至5倍。
Meta Service专注于元数据服务,采用了存算分离的设计理念。元数据被持久化存储到FoundationDB中,而FoundationDB同时提供了事务机制,以支撑上层实现文件系统目录树的语义。Meta Service的节点本身是无状态的,且支持横向扩展。它们负责将POSIX定义的目录树操作翻译成FoundationDB的读写事务来执行。
Storage Service则专注于数据存储服务,采用了存算一体的设计。每个存储节点都管理着本地的SSD存储资源,并提供读写能力。为了确保数据的可靠性和可用性,每份数据都以3副本的形式进行存储,并采用了链式复制协议CRAQ(Chain Replication with Apportioned Queries),以提供write-all-read-any的语义,从而更加友好地支持读操作。系统还会对数据进行分块处理,并尽可能地将这些数据块打散到多个节点的SSD上,以实现数据和负载的均衡分布。
3FS 架构深度解析集群管理整体架构3FS集群能够部署单个或多个管理服务节点mgmtd。在这些mgmtd节点中,仅有一个作为主节点,负责响应所有的集群管理请求,其余均为备节点,仅提供查询主节点信息的响应。其他角色节点需定期向主mgmtd发送心跳以维持在线状态,从而提供服务。
每个节点启动后,需向主mgmtd上报必要信息以建立租约。mgmtd将收到的节点信息持久化存储到FoundationDB中,确保在主节点切换后信息不丢失。这些信息涵盖节点ID、主机名、服务地址、节点类别、状态、最后心跳时间戳、配置信息、标签和软件版本等。租约建立后,节点需周期性地向主mgmtd发送心跳以续租。租约的有效性根据以下规则判断:若节点超过T秒(可配置,默认为60秒)未上报心跳,主mgmtd将判定其租约失效;若节点与主mgmtd超过T/2秒未能续租,该节点将自动退出。对于元数据节点和客户端,有效的租约意味着服务可用。然而,对于存储服务节点,情况更为复杂。一个存储节点上可能包含多个CRAQ的Target,每个Target的服务状态可能不一致,因此节点的可服务状态并不能代表一个Target的可服务状态。Target的服务状态会进一步细分为多种类型。元数据和存储节点(包括其上的Target)的信息,以及下文将描述的CRAQ复制链表信息,共同构成了集群的路由信息(RoutingInfo)。主mgmtd负责将路由信息广播到所有节点,以便各节点在需要时通过它找到其他节点。
mgmtd的选主机制基于租约和FoundationDB读写事务实现。租约信息LeaseInfo存储在FoundationDB中,包括节点ID、租约失效时间和软件版本信息。有效的租约意味着记录的节点是当前主节点。每个mgmtd每10秒执行一次FoundationDB读写事务进行租约检查。这一机制通过以下几点确保了选主的正确性:LeaseInfo的读取和写入在同一个FoundationDB读写事务中完成,确保了并发租约检查的串行处理,避免了多个mgmtd同时认为自己成为主节点的情况。主节点切换后,新主节点会静默120秒才开始提供服务,这个时间远大于租约的有效时长60秒,确保了老主节点上的在飞任务有足够的时间处理完毕,避免了新、老主节点并发处理的情况。
3FS提供两种客户端形态:FUSE客户端hf3fs_fuse和原生客户端USRBIO。FUSE客户端适配门槛低,开箱即用。在FUSE客户端中,用户进程的每个请求都需经过内核VFS、FUSE转发给用户态FUSE Daemon处理,涉及4次“内核-用户态”上下文切换和数据1-2次拷贝。这些开销导致FUSE客户端性能存在瓶颈。而USRBIO是一套用户态、异步、零拷贝API,使用时需业务修改源代码适配,使用门槛较高。每个读写请求直接从用户进程发送给FUSE Daemon,消除了上下文切换和数据拷贝开销,实现了极致性能。
FUSE客户端基于libfuse lowlevel api实现,要求libfuse 3.16.1及以上版本。相较于业界其他实现,其最大特色在于使用了C++20协程。
基于共享内存RingBuffer的通信机制在高性能存储和网络领域得到广泛应用,如DPDK、io_uring等。这些实现通常采用无锁、零拷贝设计,相比其他通信机制性能显著提升。3FS借鉴这一思路实现了USRBIO,相较于原有的FUSE实现,具有以下特点:执行路径精简,完全在用户态实现,无需经过内核VFS、FUSE内核模块处理读写数据。与RDMA打通,整个处理过程无拷贝开销,仅加速最关键的读写操作,其他操作复用FUSE现有逻辑,在效率和兼容性间取得平衡。使用说明可参考3FS代码库USRBIO API Reference文档。
在实现上,USRBIO使用了多个共享内存文件:每个USRBIO实例使用一个Iov文件和一个Ior文件。Iov文件用作读写数据的buffer,用户需提前规划好所需总容量。文件创建后,FUSE Daemon将其注册为RDMA memory buffer,实现整个链路的零拷贝。Ior文件用于实现IoRing,用户需提前规划好并发度。整个文件上抽象出提交队列和完成队列。所有USRBIO实例的共享内存文件在挂载点3fs-virt/iovs/目录下均建有symlink,指向/dev/shm下的对应文件。Iov、Ior共享内存文件通过symlink注册给FUSE Daemon,这是3FS FUSE实现上的独特之处。
通常,文件系统实现非标能力时,集成ioctl接口是较为标准的方法。3FS除了使用此方法外,对于USRBIO、递归删除目录、禁用回收站的rename、修改conf等功能,采用了集成到symlink接口的非常规做法。这可能基于以下原因:ioctl需专用工具或编写代码使用,而symlink只要有挂载点即可直接使用;与其他接口相比,symlink使用频率较低,可传递参数更多。symlink的处理逻辑如下:当目标目录为挂载点3fs-virt下的rm-rf、iovs、set-conf目录时,执行特定操作。例如,rm-rf将link路径递归删除,请求发送给元数据服务处理;iovs根据target文件后缀判定是否创建Ior;set-conf将config设置为target文件中的配置。当link路径以mv:开头时,执行rename操作并禁用回收站。其他symlink请求由Meta Service处理。
FFRecord3FS未对小文件进行优化,直接存取大量小文件性能较差。为弥补这一短板,3FS设计了FFRecord(Fire Flyer Record)文件格式,以充分发挥系统的大IO读写能力。FFRecord文件格式具有以下特点:合并多个小文件,减少训练时打开大量小文件的开销;支持随机批量读取,提升读取速度;包含数据校验,确保数据读取完整可靠。FFRecord文件格式的存储布局中,每条样本数据序列化后按顺序写入,文件头部包含每条样本在文件中的偏移量和crc32校验和,便于随机读取和数据校验。
存储服务整体架构3FS面向高吞吐能力设计,系统吞吐能力随SSD和网络带宽线性扩展。即使个别SSD介质故障,也能提供高吞吐能力。3FS采用分摊查询的链式复制CRAQ保证数据可靠性,CRAQ的write-all-read-any特性对重读场景友好。每个数据节点通过Ext4或XFS文件系统管理其上多块NVME DISK,为内部模块提供标准POSIX文件接口。数据节点包含关键模块:Chunk Engine负责chunk分配管理;MetaStore负责记录分配管理信息,并持久化到RocksDB中;主IO handle提供正常读写操作。数据节点间组成不同链式复制组,节点间有复制链间写IO、数据恢复sync写IO。
CRAQ链式复制将多个数据节点组成一条链chain,写操作从链首开始传播至链尾。链尾写完后,逐级向前发送确认信息。标准CRAQ的读操作全部由链尾处理,因为尾部数据完全写完。多条链组成chain table,存放在元数据节点。Client和数据节点通过心跳从元数据节点获取chain table并缓存。集群可有多个chain table,用于隔离故障域和不同类型任务。3FS写操作采用全链路RDMA,链的后继节点通过单边RDMA从前序节点读取数据,性能更高。3FS读操作可向多个数据节点同时发送读请求,数据节点通过比较commit version和update version读取已提交数据。多节点读相比标准CRAQ的尾节点读,显著提高吞吐。数据打散方面,传统的链式复制以固定节点形成chain table。若某节点故障,只能由其他节点分担读压力。3FS采用分摊式打散方法,一个节点承担多个chain,多个chain的数据在集群内多个节点进行数据均摊。若某节点故障,更多节点分担读压力,避免节点读瓶颈。
步骤1:分配FoundationDB读写事务;步骤2:事务内写目标文件的dentry、inode;创建文件继承父目录layout,根据stripe size选取多条chain,并记录在inode中;写打开创建场景还会写入对应file session;步骤3:事务内将父目录inode、目标dentry加入读冲突列表,保证父目录未被删除及检查目标文件已存在场景;步骤4:提交读写事务。
读写流程写数据流程:
步骤1:Client获取数据的目标chain,并向chain首节点NodeA发送写请求;
步骤2:NodeA检查chain version并锁住chunk,保证对同一chunk的串行写,再用单边RDMA从client读取数据,写入本地chunk,记录updateVer;
步骤3:NodeA将写请求传播到NodeB和NodeC,NodeB和NodeC处理逻辑与NodeA相同;
步骤4:chain尾节点NodeC写完数据后,将回复传播到NodeB,NodeB更新commitVer为updateVer;
步骤5:NodeB将回复传播到NodeA,NodeA处理同NodeB;
步骤6:NodeA回复Client写完成。
读数据流程:
步骤1:Client获取数据所在的chain,并向chain某个节点NodeX发读请求;
步骤2:NodeX检查本地commitVer和updateVer是否相等;
步骤2.1:如果不等,说明有其他flying的写请求,通知Client重试;
步骤2.2:如果相等,则从本地chunk读取数据,并通过RDMA写给Client。
采用写时复制(COW)方式: 在数据修改时,Allocator 优先分配新的物理块。系统首先读取已存在的 Chunk Data 到内存中,然后填充更新数据。拼装完成后,数据将被写入新分配的物理块。
尾部追加写入: 对于尾部追加操作,数据直接写入已存在的 Block 中。系统会生成一份新的元数据,包含新写入的 Location 信息和已有的 Chunk Meta 信息,并以原子性方式写入到 LevelDB 或 RocksDB 中,避免覆盖写带来的写放大问题。
故障处理与恢复流程: 当存储服务因崩溃、重启或介质故障导致对应的存储 Target 无法参与数据写操作时,该 Target 会被移动到链的末尾。服务重启时,offline 节点上的数据为老数据,需要与正常节点的数据进行补齐以确保一致性。
恢复节点的心跳与链式恢复: Offline 节点周期性地从 Cluster Manager 拉取最新的 Chain Table 信息,直至所有存储 Target 在 Chain Table 中都被标记为 offline 后,才开始发送心跳。这确保了节点上的所有存储 Target 能各自独立进入恢复流程。恢复采用 full-chunk-replace 方式,支持边写边恢复。上游节点发现下游 offline 节点恢复后,会通过链式复制将写请求转发给下游,即使 Client 仅写入部分数据,也会复制完整的 chunk 以实现数据恢复。
恢复步骤与数据同步原则:
步骤 1: Local Node 向 Remote Node 发起元数据获取请求,Remote Node 读取本地元数据。步骤 2: Remote Node 返回元数据信息给 Local Node,Local Node 对比数据差异。步骤 3: 若存在差异,Local Node 读取本地 chunk 数据到内存。步骤 4: Remote Node 单边读取 Local Node 内存中的 chunk 数据。步骤 5: Remote Node 申请新 chunk 并写入数据。Sync Data 原则:
若 chunk 在 Local Node 存在而在 Remote Node 不存在,则需同步。若 chunk 在 Remote Node 存在而在 Local Node 不存在,则需删除。若 Local Node 的 Chain Version 大于 Remote Node,则需同步。若 Chain Version 相同但 Commit Version 不同,也需同步。其他情况(如数据完全相同或正在写入的请求数据)则无需同步。整体架构与选择: 业界普遍采用分布式高性能 KV 存储系统构建大规模文件系统元数据组件,如 Google Colossus 和 Microsoft ADLS。3FS 元数据服务遵循相同设计思路,底层基于支持事务的分布式 KV 存储系统,上层元数据代理负责提供 POSIX 语义接口。3FS 选择 FoundationDB 作为底层 KV 存储系统,其提供了 NoSQL 的高扩展性、高可用性和灵活性,同时保证了 serializable 的强 ACID 语义。
架构优势与简化设计: 该架构简化了元数据整体设计,将可靠性、扩展性等分布式系统通用能力下沉到分布式 KV 存储。Meta Service 节点仅作为文件存储元数据的 Proxy,负责语义解析。利用 FoundationDB 的 SSI 隔离级别事务能力,目录树操作串行化,冲突处理和一致性问题交由 FoundationDB 解决。Meta Service 只需在事务内实现元数据操作语义到 KV 操作的转换,降低了语义实现复杂度。
存算分离与无状态设计: 在存算分离架构下,各 MetaData Service 节点无状态,Client 请求可发送到任意节点。但内部通过 inode id hash 保证同目录下创建和同一文件更新等请求转发到固定元数据节点上攒 Batch,以减少事务冲突并提升吞吐。计算和存储具备独立 scale-out 能力。
数据模型与语义实现: Metadata Service 采用 inode 和 dentry 分离的设计思路,模拟出两张逻辑表。在定义好的 inode 和 entry 结构之上,通过 FoundationDB 的读写事务正确实现各类 POSIX 元数据操作。本文抽取了几种代表性操作进行说明,展示了如何在 FoundationDB 上实现 POSIX 元数据操作的复杂性和细节。
本文深入探讨了 3FS 系统的关键设计,展现了其深思熟虑的设计理念。不可否认,3FS 是一个设计精良的作品。然而,与文件存储领域的主流做法相比,其设计也存在差异。作为系列文章的上篇,下文将进一步对比 3FS 与业界知名文件系统,从整个文件存储领域的角度分析其优点和局限性,并总结我们从 3FS 中获得的启示以及我们的看法。
来源:华远系统