DeepSeek发布开源第五弹!AI 时代的极速存储引擎【3FS】

server/2025/3/5 10:23:58/

3FS(Fire-Flyer File System)是 DeepSeek 开源的一款高性能分布式文件系统,专为 AI 训练、大规模数据处理和推理优化设计。它采用 链式复制 (Chain Replication) + CRAQ 技术,实现了强一致性和高吞吐的分布式存储架构,同时具备计算存储分离和RDMA 零拷贝传输,极大优化了数据访问效率。

功能解析

定位与设计目标:3FS 是 DeepSeek 开源推出的一款高性能分布式文件系统,专为 AI 训练、深度学习和大数据场景下的大规模数据访问而设计。它的目标是在海量数据处理时提供极高的吞吐量和低延迟,消除传统存储在这些场景下的瓶颈。同时,3FS 希望提供易用的文件接口,让开发者无需学习新的存储API,就能像使用本地文件系统一样使用分布式存储。这意味着应用可以直接用 POSIX 文件语义(如文件读写、目录操作等)在 3FS 上运行,从而降低上手难度

核心特性

  • 并行分布式架构:3FS 将数据分布到多个存储节点上,并行提供服务。多个客户端或进程可以同时访问不同节点上的数据,避免集中访问单点瓶颈。这种架构类似于为数据管道安装“涡轮增压器”,在 AI 模型训练、大规模数据预处理等需要高并发读写的场景下,提供远超单机文件系统的性能。官方测试中,180 个节点集群的总读取吞吐量达到 6.6 TiB/s,展示了这种并行架构的威力。

  • 存储计算分离:采用解耦的存储架构,即计算节点与存储节点分离。存储层整合了上千块 SSD 的总带宽,以及数百台存储服务器的网络带宽,整体性能线性扩展。应用不需要关心数据的物理位置,可以不考虑本地性地访问数据。即使计算任务与数据存储不在同一台机器,通过高速网络(如 RDMA)仍能高速访问。这种“计算-存储分离”设计确保在大规模集群中,加入更多存储节点可以线性提升容量和吞吐,而不会因数据本地性问题限制计算调度。

  • 强一致性保证:3FS 非常注重数据一致性,它实现了链式复制 (Chain Replication) 并结合分配查询 (CRAQ) 技术。在写入数据时,采用“写全链,读任意”的策略保证所有副本更新,然后允许从任意副本读取最新数据。这样,应用层可以简单地认为对文件的操作是线性一致的,不必处理复杂的最终一致性问题。强一致性的好处是数据可靠、语义清晰:一次写入成功后,后续所有读取都会看到这个写入的结果,简化了分布式应用的逻辑。

  • 熟悉的文件接口:3FS 提供标准的文件系统接口(POSIX风格)。例如支持分层目录、文件的创建删除、文件移动重命名、符号链接和硬链接等传统操作。开发者可以直接使用常用的文件 API,无需学习新的对象存储接口或定制的SDK。这使得迁移现有应用变得简单——许多使用文件的代码(如加载CSV/Parquet数据集的代码)几乎无需修改就能接入3FS。这种设计也支持一些文件系统特有的便利功能,例如原子性的目录操作(一次操作移动或删除整个目录树)、以及使用硬链接/符号链接来快速生成数据集快照等,方便管理不断增长的训练数据。

  • 专为AI工作负载优化:3FS 在设计时就考虑了典型的 AI 和数据处理工作负载需求:

    • 数据准备:支持将数据清洗或转换后的输出组织为目录树结构,高效管理分析管道产生的大量中间文件。
    • 分布式数据加载:训练时,3FS 允许跨计算节点随机读取训练样本。由于读性能高且支持并发随机IO,训练框架不必再预先将数据集复制到每个节点或反复改写顺序,可以直接从全局文件系统中按需读取样本,省去了复杂的预取和数据shuffle
    • 检查点保存:大型模型训练常需要并行保存模型检查点,3FS 提供对并发写入和超大文件的高吞吐支持,确保在保存上百GB甚至TB级别模型时不会成为瓶颈,训练作业可以更频繁地保存状态而不拖慢进度。
    • 推理 KV 缓存:针对大语言模型(LLM)推理,3FS 引入KVCache机制,将推理过程中生成的大量键-值向量缓存存储在SSD上。相比传统做法把这些缓存放在内存中,使用3FS存储可以大幅提升缓存容量(因为SSD总容量远大于内存),而且通过高并发IO仍能提供每节点40+ GiB/s的读取带宽。这种设计为部署超大推理模型提供了一种高性价比的方案,避免因为内存不足而频繁重新计算历史token的隐含状态。
  • 性能与易用性的平衡:为了让不同类型的应用都能充分利用3FS,官方提供了两种客户端:一是FUSE客户端,二是原生高性能客户端。FUSE 客户端通过标准的 FUSE(Filesystem in Userspace)机制,将3FS挂载为本地文件系统,使绝大部分应用无需修改就能使用3FS,这是最低的使用门槛。而对性能要求极高的应用,3FS 提供了原生客户端库,直接集成到应用中,绕过FUSE带来的开销,从而发挥网络和存储的最大性能潜力。接下来会详细介绍其架构如何同时支持这两种客户端模式。

架构分析

整体架构概览:3FS 由四大组件构成,分别是集群管理器(Cluster Manager)元数据服务(Metadata Service)存储服务(Storage Service)客户端(Client)。它们通过高速网络(RDMA,比如 InfiniBand 或 RoCE)互联,协同实现一个完整的分布式文件系统。下面逐一介绍各组件及其协作关系:

  • 集群管理器:负责整个集群的节点管理和配置分发。元数据服务和存储服务都会定期向集群管理器发送心跳,以报告自己的在线状态和健康情况。集群管理器监控这些心跳,从而在有节点加入、退出或故障时更新集群成员列表,并将最新的集群配置分发给其它服务和客户端。为了防止单点故障,集群管理器通常以多实例部署,通过选举保持一个主(Primary)节点。当主管理节点故障时,会自动切换(选举)另一个管理器为新的主节点,保证管理功能不中断。集群的配置数据(例如当前有哪些存储节点、链式复制的拓扑等)需要持久化保存以备恢复。3FS 可以使用像 ZooKeeper、etcd 这类可靠的分布式协调服务来存储配置;在 DeepSeek 内部环境中,他们巧妙地复用了文件元数据所用的同一套键值存储来保存这些配置,从而减少额外的外部依赖。

  • 元数据服务:元数据服务类似于文件系统的“目录管理者”。所有关于文件和目录的名称、层次结构和属性信息都由它来处理。举例来说,当客户端执行创建文件、打开文件、列出目录、修改权限等操作时,这些请求会发送到某个元数据服务。3FS 的元数据服务是无状态(stateless)的,因为文件系统的元数据实际保存在一个分布式事务键值数据库中(当前实现使用 FoundationDB)。每个元数据请求,元数据服务都通过事务读写这个后端数据库来查询或更新文件信息。由于采用了事务型KV存储,多个元数据服务器可以并行处理不同请求,保证了一致性:如果两个客户端几乎同时创建同名文件,只有一个会成功,另一个事务将检测到冲突并重试/失败,最终避免重名。元数据服务无状态的好处在于易扩展和维护:可以部署多台元数据服务器分担请求负载,而且升级或重启某个元数据服务不会影响整体功能,因为元数据都在后端数据库,客户端遇到超时还可以自动切换到其他元数据服务器节点。

  • 存储服务:存储服务运行在各个存储节点上,每个实例管理该节点上的若干本地 SSD。3FS 将文件的数据内容切分成固定大小的数据块(chunk)进行存储,每个 chunk 会复制到多个存储节点上以提高可靠性和读性能。具体来说,存储服务实现了链式复制(Chain Replication)协议:对于每个数据块的一组副本,多个存储节点组成一个链(链的长度等于复制因子,比如三副本就形成长度为3的链)。链上第一个节点称为链头(head),最后一个节点称为链尾(tail)。写入请求总是由链头处理,然后沿着链逐点传递,最终链尾确认写入完成;读取请求则可以发送给链上任意一个节点处理。由于采用 CRAQ(Chain Replication with Apportioned Queries)策略,在完成一次写入后,链上所有副本都已有最新数据,因此后续读请求无论命中哪个副本都能读取到最新内容。这实现了前述的“强一致性,写全读任意”。链式复制充分利用了RDMA 高速网络和 SSD 并发读能力:写入时顺序传递保证一致性,而读取时多个副本分摊读流量,实现几乎线性增长的读带宽。每个存储节点为了加入多个不同的链,通常会将每块物理 SSD 虚拟划分成多个存储目标实例(例如把一块盘当作5个逻辑目标),这样系统在构建复制链时,可以让同一台机器的不同目标加入不同的链,从而均衡负载。

  • 客户端:客户端是跑在使用3FS的应用所在节点上的软件组件,负责将应用的文件IO请求转发到上述服务并获取数据。3FS 提供两种客户端实现

    1. FUSE 客户端:这是一个通过 FUSE 内核模块实现的用户态文件系统进程。它允许用户把3FS挂载到本地路径,然后应用对这个挂载点内文件的操作都会由内核转交给 FUSE 客户端处理。FUSE 客户端会将文件操作请求发送给3FS的元数据服务或存储服务,再将结果返回给应用进程。使用FUSE的优点是透明、兼容性高,无需修改应用程序。但传统FUSE有性能损耗(后面详述)。
    2. 原生客户端库:这是直接集成在应用中的一套3FS访问库。应用可以直接调用这个库的API进行文件读写,实现零拷贝的高速IO。原生客户端通过 RDMA 网络直接与存储服务通信,省去了内核和FUSE层的额外开销。对于追求极致性能的场景(如分布式训练中每秒百万级的小批量样本读取),可以采用原生客户端方式。

数据流说明:结合以上组件,简要说明几种常见操作的流程:

  • 打开文件/获取元数据:当应用执行open()或查询文件信息时,客户端会与某个元数据服务通信。元数据服务从后端 FoundationDB 数据库中读取该文件的元数据(如文件大小、权限、所在目录、以及数据块布局等),然后返回给客户端。由于元数据服务是无状态的,客户端可以连接到任意一台元数据服务器处理请求。打开文件时,元数据服务除了基础信息外,还会将文件的数据布局提供给客户端,包括文件被切分的 chunk 大小,以及这些 chunk 分布在哪些存储节点链上。客户端缓存这些布局信息后,在后续实际读写数据时就无需再经过元数据服务,直接与存储服务交互。这种设计减少了数据面上的元数据查询开销,使元数据服务不成为高频IO时的瓶颈。

  • 读取文件数据:应用对已打开文件执行读操作时(例如read()),客户端根据之前获得的文件布局,计算出读请求对应的 chunk 编号,然后直接通过 RDMA 网络向对应 chunk 的某个存储服务节点发出读请求。由于每个 chunk 有多副本,客户端通常可以选择其中任意一副本的节点来读数据。为了负载均衡,3FS 通常会让客户端均匀地轮流从不同副本读取,这样多个副本同时为一个文件服务读请求,提高并发吞吐。存储服务收到读请求后,从本地SSD上读取相应数据块,通过RDMA直接传回给客户端。因为使用了RDMA的远程直接内存访问,数据可以从存储节点的内存直达客户端应用的内存区域,中间拷贝次数降到最低。总的来说,读路径为:应用 -> 客户端 -> 存储服务(直接取SSD数据)-> 返回应用,元数据服务在读阶段不参与,确保了读扩展性。

  • 写入文件数据:当应用写文件时,流程类似但稍复杂一些。客户端根据写入偏移算出目标 chunk,并将写请求发送给该 chunk 所属链的链头存储节点。链头存储服务先将数据写入自己的SSD(或缓存),然后通过RDMA将数据转发给链中的下一个节点;第二个节点收到后也写本地存储,再传给第三个,以此类推直到链尾节点。链尾写入完成后会发送确认,沿链返回给链头,最终客户端收到写成功的应答。整个过程中,写操作只有在所有副本都持久化后才确认成功,这保证了强一致性(任何节点发生故障,数据至少已经在其他副本上落盘)。但因为是流水线传输,链上节点可以并行处理不同偏移的写请求,且利用RDMA传输开销极低,使得整体写入吞吐仍然很高。写入后的数据可立即被新的读请求从任一副本读取,不会出现读到过期数据的情况。

  • 元数据变更:像创建文件、删除文件、重命名目录等属于元数据更新操作,这些操作由元数据服务通过事务在 FoundationDB 中执行。比如删除目录时,元数据服务会启动一个事务,将该目录下所有文件和子目录的对应元数据键删除;又如重命名文件,实际上需要在数据库中“删除旧目录项、新增新目录项、更新目标父目录ID”等多个步骤,但借助事务可以一次性完成。这些元数据更新只有事务全部成功提交后才对外生效,期间如果别的操作冲突(例如两个客户端同时试图创建同名文件),FoundationDB 会检测到冲突并使其中一个事务失败回滚,由元数据服务重试或返回错误。这种机制保证了元数据操作的原子性一致性,就像在本地文件系统上一样可靠。

  • 故障处理:如果某个存储节点突然失效,它管理的那些数据块副本暂时不可用。此时链式复制的设计可以派上用场——对于失效节点所在的链,3FS 会由集群管理器协调更新链表(把出故障的目标移除链或用其它节点替换),并增加链的版本号。客户端获取到新的链配置后,之后对涉及该链的数据访问会路由到更新后的节点。与此同时,系统会在后台将缺失的数据块副本重新复制到新的节点,恢复原有的冗余水平。这保证了即使个别存储设备故障,数据仍有其他副本可用且一致性不受影响。元数据服务和集群管理器的冗余架构也确保了管理平面的健壮:主管理器故障时迅速切换,元数据服务无状态可横向扩展,不会因为单点失败导致整个文件系统瘫痪。

源码解析

下面我们从源码实现角度,选取3FS中的关键机制进行剖析,解释其实现方式和运作原理。

1. 元数据管理的实现

3FS 将文件系统的元数据存储委托给了 FoundationDB 这样成熟的分布式事务型键值存储,这使得元数据相关的源码逻辑更多是在组织键值调用事务API方面。总体上,3FS 将文件系统的目录结构和属性映射为两个核心KV集合:inode 表目录项表

  • Inode 表:在文件系统中,每个文件或目录都有一个唯一的 inode 编号。3FS 使用一个全局递增的64位ID作为 inode,并在 FoundationDB 中以键值对存储每个 inode 的属性信息。实现中,为了避免所有inode落在一个键空间影响分布,3FS 构造键时在前缀加入如 "INOD" 和 inode ID(小端序编码)组合,这样不同 inode 键会均匀分布到数据库的不同节点。每个 inode 键对应的值包含该对象的属性结构体,不同类型的inode(文件/目录/符号链接)存储的字段略有区别。例如:

    • 文件inode:包含基本属性(拥有者、权限、时间戳等)以及文件特有的长度块大小数据布局信息等。布局信息中记录了该文件的数据块大小(chunk size),以及分配使用了哪个链表范围条带大小(解释:3FS预先生成了很多复制链组合,文件会选取其中一部分用于存储自己的块;如果文件很大,可以跨多个链条带存储,提高并行度)。
    • 目录inode:除基本属性外,会存储父目录的inode号以及该目录默认的布局策略(例如该目录下新文件默认使用哪个链表、默认chunk大小和stripe条带等)。父inode的记录主要用于在执行移动目录时检查避免形成环(环路检查需要沿着父链一直查到根,看目标是不是源的子树)。
    • 符号链接inode:会在属性中存储符号链接指向的目标路径字符串。
  • 目录项表:目录内容以键值形式存储,每一个目录中的文件或子目录对应一个目录项记录。实现里,键设计为 "DENT" 前缀 + 父目录inode + 文件名。这样的组合使得同一目录下的所有项的键都有相同前缀开头,从而在FoundationDB中形成一个连续的键区间。通过对这个前缀做范围查询,就可以高效地列出目录内容。目录项键的值部分通常只需要保存目标对象的 inode 号和类型(文件/目录等),因为其他信息可以通过查对应 inode 键获取。

基于上述键值设计,元数据操作在源码中大多表现为对FoundationDB事务的一系列读写操作

  • 文件创建 (create): 元数据服务会发起一个事务,在 FoundationDB 中分配新的 inode ID,然后写入一条 inode 记录(带初始属性)和对应父目录下的一条目录项记录。代码实现中,会先读取父目录inode确认目录存在且有权限,然后通过 FoundationDB 的原子增值获取下一个可用inode ID,接着并行准备好两条 set 操作(inode键=值,目录项键=值)。最后提交事务。如果提交失败且错误表示冲突(比如目录下同名文件已被别的事务创建),则重试或返回已存在错误。

  • 文件打开/查找 (lookup/open): 这通常是只读事务。根据路径逐级查找目录项键。在代码中,会按路径片段循环查询 FoundationDB:从根inode开始,查询 "DENT + 根inode + 第一级名称" 找到第一级inode,再用它查下一级,如此迭代。如果某级查询结果为空,则路径不存在返回错误。如果最终定位到目标inode,则再取出 inode 键对应的值,组装成文件元数据结果返回给客户端。在open文件时,还可能将文件的布局相关信息解析出来一并返回,供客户端后续使用。

  • 重命名 (rename): 这是一个典型的需要跨目录的原子更新操作。源码中会开启事务,同时处理多个键:删除源目录下旧的目录项键、新增目标目录下的新目录项键、更新目标条目的 inode 如果需要(比如覆盖文件情况)以及更新被移动目录的父inode字段(如果是目录在不同父下移动)。所有相关键值操作打包在一个事务提交,以保证要么全部成功要么全部不生效。FoundationDB 会自动检测如果有其他事务同时修改相关目录,会导致冲突使提交失败,从而保证并发重命名的正确性。

  • 删除 (unlink/rmdir): 分为删除文件和删除目录两种。删除文件在事务中删除该文件对应的目录项键和 inode 键即可(前提是没有打开的写会话,后文讨论)。删除目录需要先确保目录为空,然后删除目录inode和目录项。为了提升效率,3FS 对递归删除整个目录树提供了支持:在安全确认需要删除一个大目录树时,元数据服务可以直接遍历该目录的所有子项并一次性删除相关的成百上千条键,这比起应用逐个遍历删除要高效得多,特别适合训练产生海量小文件后需要一键清理的场景。

在实现这些元数据操作时,3FS 利用了 FoundationDB 的事务冲突检测机制来保证一致性,同时通过自动重试隐藏了并发冲突带来的复杂性。也就是说,在源码里元数据服务会捕获 FoundationDB 提交事务时返回的错误码,如果表示读写集冲突,就简单地重新执行事务逻辑。由于大多数元数据操作耗时很短(几次KV读写),偶尔重试一次对整体性能影响不大,但却换来了一个无锁却可靠并行的元数据处理架构:多台元数据服务器可以同时处理不同请求,只有真正访问相同目录或文件的冲突操作才会串行化。这在高并发元数据操作(例如多个进程同时在不同目录创建文件)的情况下,可以充分利用多核和多服务器并行能力。

另一个源码实现细节是文件句柄会话管理:POSIX 语义中,如果一个文件已被打开,删除操作通常并不会立即物理删除文件内容,而是等最后一个文件描述符关闭后再真正删除。3FS 在这方面做了一定取舍优化。在代码里,引入了“文件会话”(file session)概念,当文件以写入模式打开时,元数据服务会记录一个写会话;如果有人试图删除该文件,元数据服务检查到还有活跃的写会话,就不会立即删除inode和目录项,而是将删除操作挂起。只有当所有写会话都结束(例如对应客户端关闭了文件或失去心跳被认定掉线),才真正执行删除。这避免了并发写时删除导致数据块成为孤儿无法收回的问题。不过对于只读模式打开的文件,3FS 出于性能考虑不跟踪会话(训练场景通常不依赖删除延后语义)。也就是说,如果某文件仅以读方式打开着,另一方删除了它,3FS 会立即在元数据上移除该文件,但并不会主动回收其数据块,使得已经打开的读句柄仍然可以正常读完内容(相当于延迟回收机制)。这一实现权衡了严格语义和性能负载:不跟踪只读打开可以显著减轻元数据服务和数据库的开销,因为训练作业常常同时打开成千上万个文件进行读取,如果都记录会话将造成巨大开销。

2. 存储层链式复制与数据路径实现

存储层的核心是链式复制 (Chain Replication) 协议的实现,它直接关系到数据的可靠存储和高效读取。链式复制在代码层面涉及数据分布、故障恢复和读写流程等部分,我们分别说明:

  • 链表与数据分布:3FS 在部署时,会生成一个或多个**“链表 (chain table)”配置,用来决定数据块如何在存储节点间复制。链表可以看作一个二维表,每一行代表一条复制链,包括多个存储目标实例的组合。例如,在一个简单场景里,有6台存储节点(A, B, C, D, E, F),每台节点有1块SSD。如果我们定义每块SSD上跑5个目标实例(A1…A5等),并设置复制因子为3(每个chunk三副本),那么可以构造出许多条长度为3的链,例如:链1用 {A1, B1, C1},链2用 {D1, E1, F1},链3用 {A2, B2, C2},链4用 {D2, E2, F2},…如此排列,确保每条链的3个实例都在不同节点上,而且链数很多。链表版本也被跟踪:如果某条链因为故障更改了成员,则其版本号加1。平时只有主集群管理器可以修改链表,这样保证全局一致。3FS 利用链表来进行数据布局:每个新建文件会选取链表中的一段连续条目作为自己的数据块存储方案,并可能使用一个随机种子来“洗牌”选择起点,从而不同文件的数据分布在链表上是均匀且错开的。如果文件特别大超出了选定链表段的容量,还可以配置条带(stripe)跨多个链段存储,类似RAID0将文件拆分成并行的大块。链表的存在,让3FS的数据分布有规律可循**:客户端拿到文件布局信息(链表段起点和条带大小等)后,不需要从元数据服务查询每个块该去哪里,而是自己计算第n个chunk应该对应链表上的哪一条链,从而知道具体哪些节点。这样客户端就能自主定位所有块,元数据服务从文件打开后就不再介入数据路径,系统扩展性更好。

  • 写入链式复制流程:在源码中,每个存储服务进程会管理多个链实例的角色(有的作为链头,有的是中间节点或链尾)。当链头节点收到客户端发送来的写请求时,代码首先在本地将这段数据写入暂存(通常直接写入SSD的指定块区域或缓存)。然后,链头通过RDMA将该写请求转发给链中的下一个节点(中间节点)。中间节点的代码逻辑与链头类似:一方面将数据写到自己的存储介质,另一方面如果有后续节点则继续转发。这个过程实现上通常是异步流水线的:链头在等待自己磁盘写入的同时就开始给下家发送数据,以减少延迟。最后链尾节点完成写入后,不再有下家,于是它会给上游返回一个写成功的确认消息。确认会沿链路反方向传播回链头,链头收到链尾确认后,再回复客户端完成。只有当链尾确认后,客户端才认为写入成功。这确保了此时所有副本都持久化完成,满足强一致性要求。由于RDMA传输耗时极低且CPU不参与数据拷贝,整个链路上的多段IO加起来延迟仍然很小。在正常运行时,链式复制可以近似看做各节点并行工作的流水线:当一个写请求在链上传输时,链头已经可以开始处理下一个写请求。因此整个链路的吞吐接近单节点写入能力乘以节点数,远高于传统主备复制串行确认的吞吐。

  • 读取负载均衡与并发:读取流程中,客户端可以选择链上的任意副本发请求。3FS 的策略倾向于将读请求平均分布到链内不同节点:例如对于一系列顺序块的读取,客户端可能轮询链的三个节点分别取不同块,从而每台存储节点各自承担1/3的读任务。这种简单但有效的负载均衡在源码中通过随机或Round-Robin选取副本实现。如果某个节点繁忙或延迟较高,客户端也可以切换到链上另一副本重试读请求。因为所有副本数据是一致的(除了正在写入未完成时,暂不可读最新数据的情况,见下),这样的读冗余机制显著提高了集群整体的读扩展能力和可靠性。对于小IO的高QPS场景,存储服务还支持将多个小读请求打包批量处理,以减少频繁RPC调用带来的额外开销。这些细节都体现了3FS为追求高读性能做出的努力。

  • 一致性保障机制:在链式复制中,需要处理写入进行时的读请求一致性问题。举例来说,当链头收到新数据还未完全传播到链尾时,有其他客户端发起了对同一数据块的读请求。3FS 使用 CRAQ(带分配查询的链复制)协议来保障一致性,其原理是在数据版本管理副本状态上下功夫:当一个数据块正在更新时,链上节点会标记新数据为“暂未提交”状态,只有当链尾确认后,才将此版本标记为提交可读。在代码实现上,每个数据块副本可能维护一个版本号或状态标志。读请求到达副本时,如果该副本有更新尚未提交,且自己不是链尾,它可以选择等待链尾确认或者将读请求转发到链尾处理。通常,为简化实现,3FS 可以采用保守策略:在写事务进行时暂时避免提供旧版本数据。因为写入延迟很短,绝大多数情况下不会碰到读正好卡在写中的情况。一旦写提交,所有副本都有新数据,则读请求自然都能读到更新后的内容。借助这种机制,3FS 实现了强一致的读写:要么读到之前提交的旧数据,要么等到新数据提交后再读,绝不会读到部分更新的中间状态数据。

  • 故障恢复:当某个存储节点失效,链复制协议需要将该节点从链中剔除并保证数据仍有足够副本。集群管理器在检测到存储服务失联后,会根据预先设计的策略更新链表:涉及该故障节点的链要么缩短为链长-1(若暂时降低冗余),要么用集群中空闲的备用目标顶替并增加链版本。新的链信息广播给客户端和相关服务后,对于新的写入,会遵循新的链进行;对于已有的数据块,系统会异步触发重复制,从仍然可用的副本复制数据到新替换的节点上,恢复链的完整长度。源码中,这可能通过后台数据修复任务实现:扫描受影响的链和chunk,逐个从健康副本拷贝数据块给新加入的副本。因为3FS chunk尺寸一般较大且使用高速网络,这种修复可以相对快速完成。期间客户端的读请求由于读任意副本策略,也可以继续从存活的副本获取数据,只是副本数减少可能稍影响并发性能。总体而言,存储层代码通过版本化的链配置后台重平衡,实现了自动的故障容忍和恢复,保证数据安全性的同时尽可能减少性能下降时间。

3. 客户端 I/O 与 FUSE 性能优化实现

3FS 的客户端设计是很有特色和创新性的。它同时兼顾了便利性(FUSE挂载)和高性能(原生零拷贝IO)这两个看似矛盾的方面,从源码角度可以看看它是如何做到的。

FUSE 的问题:首先了解传统 FUSE 的性能瓶颈。FUSE让用户空间进程实现文件系统逻辑,但内核与用户空间通信需要拷贝数据和状态,多线程处理也有锁开销。具体来说,每当应用发起一次文件读写,内核会将请求排队,唤醒FUSE守护进程处理;FUSE进程读出请求,执行相应操作(比如通过网络从3FS存储服务获取数据),然后将结果写回内核;内核再复制数据给应用。这个过程中数据至少经过两次拷贝(内核->FUSE用户态->内核->应用),耗费CPU和内存带宽。此外,Linux FUSE 实现对于并发请求使用一个全局队列加自旋锁保护,多线程FUSE守护同时读取这个队列时会产生锁竞争,在高并发IO下难以线性扩展。实际测试表明,传统FUSE每秒处理小IO(比如4KB读)到一定数量后(几十万次)就达到瓶颈,更多线程并不能提高速率,严重限制了存储系统的性能发挥。

集成原生客户端:针对上述瓶颈,3FS 在实现上采用了一个巧妙的方案:在 FUSE 守护进程内部嵌入原生客户端逻辑。也就是说,3FS 并没有让用户在FUSE和原生两者二选一,而是将二者结合。具体方式是:让FUSE只处理元数据等轻量操作,而将真正的数据读写I/O通过原生方式执行。实现时,当应用通过FUSE挂载点open()打开文件时,FUSE守护进程处理open请求,调用元数据服务获取文件信息以及数据布局,然后返回给应用一个文件描述符。但在内部,3FS 识别出这个文件支持原生IO,于是在FUSE进程中为该文件创建一个原生I/O会话,并将其与这个文件描述符关联。随后,当应用对这个文件调用read()write()时,FUSE内核模块依然会将请求送到守护进程,但守护进程这边检测到该文件有原生通道,于是不再按传统方式逐个read处理,而是使用原生客户端的方法直接执行高速I/O。

零拷贝异步IO:3FS 原生客户端的API受到 Linux 内核新式IO接口 io_uring 的启发。io_uring通过内核与用户空间共享环形队列来提交和完成IO,极大降低了系统调用和拷贝开销。3FS 在用户态实现了类似机制:应用进程与FUSE守护进程之间通过共享内存环形缓冲区(I/O Ring,源码中简称 Ior)交换IO请求和完成通知。具体而言,应用在用户态通过3FS提供的库将读写请求放入这个环(包括请求哪段文件的哪块数据、目标内存缓冲区等信息),然后立即返回继续做其他事情;FUSE守护进程内的原生客户端线程会不断轮询这个环获取新请求,将请求整理批量后通过RDMA发送给对应存储节点,并在数据返回后将结果状态写入环供应用检查。整个过程应用线程几乎不被阻塞,可以异步地提交大量I/O,而且数据通过RDMA直接从存储节点送入应用提供的缓冲区(这些缓冲区提前向RDMA注册过,以确保直接访问),没有多余拷贝。这样就绕开了FUSE原本内核<->用户复制数据的缺陷,实现了真正的零拷贝传输。多个I/O请求还能批量发送,减少每次RPC开销。

多线程与批处理:在实现上,为充分利用多核,原生客户端在守护进程中启动了多线程来处理I/O环中的请求。这些线程各自获取一定数量请求后,会合并小请求(例如连续的块或同一目标节点的多个读)以减少网络交互次数。合并后的请求通过RDMA并行发往不同存储节点。这种批处理和多线程策略,在源码中通过参数(如io_depth)控制批大小,并确保不会因为单队列锁而阻塞所有线程——实际上多个I/O环可以分配给应用的不同线程,各守护线程各取各的,以此消除了FUSE原有的单队列瓶颈。换句话说,3FS 重写了 FUSE I/O 通道部分,使其更加并行友好和低开销。

元数据操作处理:需要注意,文件的打开、关闭、stat等元数据相关操作依然是由FUSE守护进程串行处理的,因为它们涉及与内核交互和调用元数据服务。这些操作频率相对数据读写低很多,一般不会成为瓶颈。而采用上述方案后,大量高频的读写I/O走原生通路,不再受FUSE框架限制,这就达到了兼顾语义一致和性能最大化的效果。对于应用而言,它只管用常规文件接口打开文件并发起读写,但是在背后3FS已经帮它用优化的方式完成了数据传输。这种设计在源码中相当复杂,但思想非常超前,确保了兼容性(POSIX语义一致,由FUSE元数据层保证)和性能(数据面用高速通道)同时满足。

实现示例:比如某应用调用read(fd, buf, length)试图读取文件,3FS库会让应用线程将“读fd某偏移length数据,目标放buf”这个请求写入共享环缓冲。守护进程线程拿到后,查到fd对应的文件layout,计算涉及的chunk和所在存储节点列表,然后通过RDMA从那些节点并行取回数据直接写入应用提供的buf地址。完成后,在环上标记该请求完成并写入实际读取的字节数等。应用线程稍后检测到请求完成,就可以继续处理数据了。这整个过程中,读的字节并没有经过内核空间多次搬运,而是“一跳”从远端SSD到应用缓冲。如果是并发多个请求,多个线程和多个RDMA连接会齐头并进,大幅提高吞吐和降低CPU占用。这正是3FS代码在客户端层面实现的亮点之一。

总结来说,3FS 源码通过嵌入式原生客户端、共享内存环和RDMA零拷贝,把 FUSE 的易用性和定制客户端的性能结合起来。在实现细节上,它参考Linux io_uring设计用户态I/O队列,充分利用RDMA特性,达到了在用户态实现内核级高速IO的效果。这在其他分布式文件系统中是少见的创新做法。

创新点分析

相较于传统的分布式存储方案(如 HDFS、Ceph、Lustre 等),3FS 在多方面引入了新的思路和改进:

  • 强一致性的链式复制:传统的大规模存储系统常在一致性和性能之间权衡,例如 Hadoop HDFS 采用主从复制但不支持同时多点写入,很多对象存储则采取最终一致性模型。3FS 引入的 CRAQ 链式复制协议让所有副本在写入后同步,从而支持线性一致读写。特别地,“写全部,读任意”的策略让读请求可以分散到不同副本承担,在提高性能的同时依然保证读取到最新数据。这兼顾了一致性和读扩展性,是对经典链式复制算法的巧妙运用。相比之下,一些传统系统要么读只能从主节点获取最新数据(形成热点),要么读副本可能拿到过期数据(需要应用容错),3FS 的方案对开发者来说更加透明简单。

  • 无状态元数据服务 + 事务性KV存储:很多分布式文件系统的元数据管理都相当复杂,例如 HDFS 使用单一NameNode管理元数据,需要特殊的Journal保证一致,CephFS 则有专门的MDS集群处理命名空间。一旦元数据服务器需要维护大量状态和锁,扩展性和故障切换都会变得困难。3FS 的创新在于把元数据存储层交给 FoundationDB,利用其分布式事务特性,实现了多个元数据服务器并行处理的架构。元数据服务器自身不持久保存数据,只做事务逻辑,这意味着可以弹性扩展或替换,也降低了开发难度。通过 SSI 级别的事务隔离,3FS 实现了跨节点的原子文件操作(如目录移动),这一点在传统方案中往往很难保证(有些文件系统干脆不支持跨目录的原子操作或只能 eventual 方式)。将工业级事务KV数据库融入文件系统架构,是一个相对新颖的尝试,带来了一致性强、扩展性好的元数据处理能力。

  • 计算存储分离与 RDMA 优化:在数据中心环境下,“计算-存储分离”并不新鲜,但3FS将其发挥到极致,加上了RDMA直连的加持。许多老一代文件系统虽然也能部署在独立存储服务器上,但通常使用TCP/IP通信,延迟和CPU开销较高,数据局部性还是很重要。而3FS生于高速网络时代,彻底假设网络不是瓶颈,从架构和代码上都针对 RDMA 调优。例如数据面大量使用零拷贝RDMA读写,减少内核参与,以及依靠高速网络实现数据全局共享、随机读取(而不是要求把计算调度到数据所在节点)。这种设计对比如 Hadoop 的“将计算移到数据所在处”形成鲜明对比。3FS 的理念是数据可以放在集群任意位置,通过足够的网络带宽和协议优化,远程访问也几乎像本地一样快。这种完全的 locality-oblivious 访问模式让资源调度更加灵活,计算节点不需绑定存储,从而更适合云原生或大规模集群环境。

  • 融合FUSE与原生客户端的设计:在客户端方面,3FS 的实现可谓独树一帜。传统上,要获得高性能,分布式文件系统通常提供专用客户端库内核驱动(如 Ceph 提供librados、Lustre有内核模块),但这需要应用专门适配。而为了通用性,又提供FUSE接口但性能损失明显。3FS 的创新点在于同时提供一个挂载式接口,但在内部利用用户态异步IO实现接近库直连的性能。相当于开发者既可以像使用NFS那样挂载3FS,又能在大流量I/O时享受接近底层协议的效率。特别是3FS借鉴io_uring思想,用共享内存环传递请求,极大减少了上下文切换和拷贝。这种“在FUSE中实现零拷贝IO”的做法业内少见,解决了长久以来 FUSE 性能受限的问题。可以说,3FS 找到了兼顾易用性和性能的平衡点,这是非常有价值的创新。

  • 针对AI场景的特别优化:3FS 出现的背景是深度学习和大数据,这也使其在一些特定场景做了创新性的优化:

    • 通过大条带并行多链分布,让单个大文件可以跨许多存储节点并发读写,这类似于传统并行文件系统的striping但更灵活,充分利用SSD阵列的集合带宽,适配训练时读取大型数据集或写出巨型模型文件。
    • 引入KVCache理念,将推理时的历史keys/values缓存存放在集中式高速SSD存储上,而不是依赖昂贵的多GPU大内存。这种思路提供了一种全新的扩展模型部署方案,属于针对AI推理优化的创新点,在通用文件系统中是没有的。
    • 符号链接/硬链接快照:3FS 支持利用文件系统本身的链接机制,快速对数据集进行snapshot或版本管理。例如训练过程中持续追加新数据文件,可以用硬链接为某次实验生成一致视图,而不必复制大量数据。这对于不断变化的数据集管理非常便利,是文件系统相对于对象存储的一大优势,3FS 强调并支持了这些能力。
  • 模块化与易维护:3FS 将功能拆分为集群管理、元数据、存储服务等模块,各自职责清晰,并通过心跳和配置管理松耦合协作。这种架构类似微服务,使得每个模块可以独立演进或扩展。特别是无状态的元数据服务可以任意增减实例,存储服务也可以通过更新链表调整数据分布,整体动态性强。相比一些老系统(比如HDFS NameNode难以横向扩展,Ceph Monitor/OSD角色耦合度高),3FS 更加灵活。在源代码层面,也借鉴了很多成熟组件:如 FoundationDB 提供事务库,FUSE内核模块提供基础接口,RDMA使用业界标准库等,避免了重复造轮子。这种善用现有技术并融合的方式,使得3FS开发效率和质量都有保障,也体现了一定的创新理念——不盲目从零开始造分布式存储,而是巧妙组装优化已有优秀组件,形成新的解决方案。

综上,3FS 的创新点体现在技术架构和应用场景两个层面:它不仅在架构机制上融合了先进理念(强一致+高性能并存,软硬件解耦,用户态高效IO),还紧扣AI实战需求做了针对性优化。这些让3FS 有别于以往的大数据存储方案,成为一款在当下AI时代极具亮点的分布式文件系统。

优化与改进建议

虽然3FS已经展示出卓越的性能和优秀的设计,在实际应用和未来发展中,仍有一些方面可以考虑进一步优化,以提升其性能、扩展性和易用性:

  1. 元数据层性能进一步提升:当前3FS依赖FoundationDB保证元数据的一致和事务性,这带来了强一致性但也可能在超大规模时形成性能瓶颈。一个改进方向是在元数据服务上增加缓存层。例如对于经常访问的目录信息,可以在元数据服务进程内做短暂缓存,减少每次都查询FoundationDB的开销。同时,可以考虑元数据分区策略,比如按目录拆分元数据到不同的FoundationDB集群或Key空间,从而进一步并行扩展元数据操作吞吐。此外,FoundationDB本身有每秒事务数限制,3FS可以研究引入批量元数据操作(如批量创建小文件)接口,减少事务提交频率。在保证一致性的前提下,尽量降低底层事务存储成为瓶颈的风险。

  2. 小文件读写优化:AI训练中经常会遇到大量小文件(比如成千上万个小图片或文本片段),尽管3FS在并发和元数据方面做了改进,小文件I/O仍然可能低效。潜在的优化包括:在存储服务侧实现小文件合并存储,即将多个小文件内容打包进单个大chunk,这样读取时可以顺序读取并在客户端缓存拆分,避免频繁处理过多小块。在元数据上,也可以考虑针对超大目录(含上百万文件)提供特殊处理,如划分子目录或分片,以避免单一目录下目录项过于集中导致FoundationDB范围查询变慢。通过这些手段,3FS 可以进一步缓解小文件场景下的性能问题。

  3. 进一步减低客户端开销:3FS 已经通过用户态原生IO显著降低了FUSE带来的延迟,但仍然存在系统调用和上下文切换方面的开销(例如应用需要通过共享内存轮询结果,FUSE守护线程调度等)。未来优化可以考虑完全用户态直连的客户端库模式:即对于特定高性能应用,允许直接链接一个客户端库,与元数据服务和存储服务通信,而不经过FUSE进程。这相当于提供两种模式:挂载模式和直连库模式。目前3FS其实已有原生库嵌在FUSE中,如果能提炼出独立库,让应用直接使用(需应用做一些修改),就可以避免FUSE进程存在的少量开销,进一步提升极端情况下的IO性能。与此同时,开发一个内核态客户端驱动也可以作为长期方向,这样在需要时可绕过FUSE框架直接在内核中执行网络IO,达到跟本地文件系统几乎一致的性能。不过内核开发复杂度高,而且缺乏FUSE那样的通用性,需要权衡投入产出。

  4. 弹性扩容与数据重平衡:当前3FS通过链表机制定义了数据在节点间的分布,这种静态链表在集群规模固定时非常高效。然而在实际部署中,如果需要在线扩容(增加新的存储节点、SSD),如何将新资源纳入并平衡已有数据就成为一个挑战。建议3FS未来实现自动数据重平衡功能:例如当有新节点加入时,动态生成新的链表或扩展链表长度,然后后台将部分数据块迁移到新节点,以充分利用新增容量和带宽。目前架构下可以由集群管理器触发这样的迁移任务。类似地,对于存储节点长期负载不均的情况,也可引入数据移动机制,调整某些文件的链分配(通过更新文件inode的链段范围,并搬迁相应chunk)。这些改进可以让3FS 在弹性伸缩场景下保持性能稳定,避免某些节点过热或容量耗尽。同时,需要确保迁移过程对前台业务影响最小,可以采用分批渐进迁移和优先空闲时间段操作。

  5. 磁盘及网络层面的改进:3FS 已经充分利用NVMe SSD和RDMA网络,但仍有一些底层优化空间。例如对于SSD设备,可以考虑IO调度优化和QOS:当并发请求非常多时,对同一SSD的访问可能产生抖动,未来可以在存储服务中实现多队列调度策略或者用IO_uring直接提交磁盘IO,进一步降低kernel block层的开销。对于网络层,如果在纯RDMA环境外运行(比如只在TCP/IP环境),可以探讨结合使用如DPDK等用户态网络框架来提升效率。虽然这偏底层,但可以让3FS在不同网络条件下都有较佳表现。此外,引入压缩或批量传输也可以在一定程度上提高广域网或低速网络下的效率——例如可选对传输的数据块压缩,以换CPU降带宽。

  6. 数据冗余和存储效率:3FS 目前采用多副本确保数据可靠性,这虽然性能很好但存储开销较高。在一些对存储成本敏感的场景,可以考虑引入纠删码(Erasure Coding)作为冷数据的备份方式。比如对于很久不访问的文件,可以后台将其副本由3份转为纠删码片段,散布到更多节点,这样既保留容错又降低空间占用。当数据再次活跃时再还原为热复制。实现纠删码需要更复杂的数据路径和恢复逻辑,但在大规模存储中是节省成本的有效方案,Ceph等系统已经证明可行。如果3FS 能结合复制和纠删码两种机制,根据数据温度动态切换,将使其在性能与存储成本间获得更好的平衡。

  7. 监控和运维工具:作为新兴的分布式存储,3FS 在易用性上可以进一步完善。例如提供更丰富的监控指标(通过Prometheus等)和可视化界面,实时展示集群的性能、热点文件、节点健康状态等信息。特别是在AI训练场景下,用户可能希望跟踪每秒读取样本数、延迟分布等,以便优化作业。3FS 可以内置这些统计或提供接口让外部工具采集。另外,调优指导和自动诊断工具也很重要:在性能不达标时,能够分析是网络瓶颈、磁盘瓶颈还是元数据冲突过多,并给出相应调优建议(比如增加元数据服务器数或优化客户端并发参数等)。这些运维层面的改进不会直接提升系统性能上限,但会极大提升用户对系统的掌控度,确保3FS 发挥出应有的性能并及时发现问题。

总结来看,3FS 已经在架构上奠定了高性能、可扩展的基础。以上建议涵盖了从元数据、数据到运维的各层面优化方向:既包括提高当前设计在极端情况的效率,也包括拓展新功能来适应更广泛的需求。随着社区使用和反馈的增多,这些改进将帮助3FS 进一步成熟,成为在AI和大数据时代可靠且强大的分布式文件系统。


http://www.ppmy.cn/server/172575.html

相关文章

复试准备日常

实验室面前投了 aiot 这周四 532图像处理实验室&#xff08;我的项目大多也是图像处理的&#xff09;&#xff08;预计下周末&#xff09;提前到3.4号下午6点 我不在第一批里面 软专不知道要几个 研二好像要去绍兴&#xff1f;&#xff1f;&#xff1f; 感知计算面试预计明后天…

栈与队列_逆波兰表达式求值

栈与队列_逆波兰表达式求值 一、leetcode-150二、题解1.引库2.代码 一、leetcode-150 逆波兰表达式求值 逆波兰式&#xff08;Reverse Polish Notation&#xff0c;RPN&#xff0c;或逆波兰记法&#xff09;&#xff0c;也叫后缀表达式&#xff08;将运算符写在操作数之后&am…

在virtualbox安装ubuntu并配置ssh连接

virtualbox ubuntu ssh 下载virtualbox 下载镜像&#xff1a;https://releases.ubuntu.com/24.04/ubuntu-24.04.2-live-server-amd64.iso 新建安装虚拟机 端口转发 ssh配置 连接配置 # cat .ssh/config …

netframework 读取appsettings.json

AppSettingsHelper&#xff1a; using Newtonsoft.Json.Linq; using System; using System.IO;public class AppSettingsHelper {private static JObject _appSettings;static AppSettingsHelper(){try{// 获取 appsettings.json 文件的路径var filePath Path.Combine(AppDom…

CentOS7 安装Redis 6.2.6 详细教程

本文主要介绍CentOS7系统下安装Redis6.2.6的详细教程。 1.安装依赖 redis是基于C语言开发&#xff0c;因此想要在服务器上运行redis需要验证是否安装了gcc&#xff0c;没有安装gcc则需先安装 查看是否安装gcc gcc -v如果没有安装gcc&#xff0c;则通过如下命令安装 yum in…

STM32MP1xx的启动流程

https://wiki.st.com/stm32mpu/wiki/Boot_chain_overview 根据提供的知识库内容&#xff0c;以下是STM32 MPU启动链的详细解析&#xff1a; 1. 通用启动流程 STM32 MPU启动分为多阶段&#xff0c;逐步初始化外设和内存&#xff0c;并建立信任链&#xff1a; 1.1 ROM代码&…

mapbox进阶,使用点类型geojson加载symbol符号图层,用于标注带图标的注记,且文字居中在图标内,图标大小自适应文字

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;mapbox 从入门到精通 文章目录 一、&#x1f340;前言1.1 ☘️mapboxgl.Map 地图对象…

C语言_数据结构总结1:静态分配方式的顺序表

纯C语言代码&#xff0c;不涉及C 1. 初始化 #define MaxSize 50 typedef int ElemType; typedef struct SQList { ElemType data[MaxSize]; //定义一个数组存放顺序表元素 int length; //顺序表当前的长度&#xff08;元素个数&am…