一、3FS & Smallpond 概述
Fire-Flyer File System(3FS) 是一个高性能分布式文件系统,旨在解决人工智能训练和推理工作负载的挑战。它利用现代固态硬盘(SSD)和远程直接内存访问(RDMA)网络,提供一个共享存储层,从而简化分布式应用程序的开发。
Smallpond 基于 3FS 和 DuckDB 构建,专注于 PB 级数据的快速处理。Smallpond 定位为 AI 数据的辅助工具,能够无缝集成到 3FS 的存储生态中,支持数据清洗、转换和分析等任务。
官方开源代码链接:
- https://github.com/deepseek-ai/3FS
- https://github.com/deepseek-ai/smallpond
1. 3FS 核心亮点
- 高性能解耦架构:通过解耦架构,结合数千个 SSD 的吞吐量和数百个存储节点的网络带宽,实现高吞吐量和低延迟。
- 数据强一致性:采用 Chain Replication with Apportioned Queries (CRAQ) 技术,确保分布式环境中的数据强一致性,适合多节点并发读写。
- 多样化工作负载支持:支持数据准备、数据加载器、检查点和 KVCache 等多样化工作负载,提升 AI 应用效率。
2. Smallpond 核心亮点
- 高性能查询:基于 DuckDB 的列式存储和向量化执行引擎,查询速度远超传统工具。
- 低开销:无需分布式集群支持,单机即可处理大规模数据,部署简单。
- 与 3FS 协同:直接利用 3FS 的存储能力,避免数据迁移成本,实现高效数据流水线。
3. 性能表现
3.1 3FS 峰值吞吐量
在180节点集群中实现 6.6TiB/s 读取吞吐量
上图展示了在大型 3FS 集群上进行读取压力测试的吞吐量。该集群由 180 个存储节点组成,每个存储节点配备 2×200Gbps 的 InfiniBand NICs 和 16 个 14TiB 的 NVMe SSD。大约有 500 多个客户端节点用于读取压力测试,每个客户端节点配置了 1×200Gbps 的 InfiniBand 网卡。最终的总读取吞吐量达到了约 6.6TiB/s,其中包含来自训练作业的后台流量。
3.2 GraySort 基准测试下的 smallpond
GraySort 是一种用于评估大规模数据排序性能的基准测试,目标是评估系统在处理大规模数据集(至少 100TB)时的排序性能。
DeepSeek 团队使用 GraySort 基准测试评估 smallpond 的性能,在实现中采用了两阶段方法,这两个阶段都从 3FS 读取和写入数据:
(1) 使用键的前缀位进行洗牌, 对数据进行分区;
(2) 在分区内的排序。
测试集群包括 25 个存储节点(每个节点 2 个 NUMA 域,每个 NUMA 1 个存储服务,每个节点 2×400Gbps 网卡)和 50 个计算节点(每个节点 2 个 NUMA 域,192 个物理核心,2.2TiB 内存,每个节点 1×200Gbps 网卡)。在 8,192 个分区中对 110.5TiB 的数据进行排序,耗时 30 分钟 14 秒,平均吞吐量达到 3.66TiB/min。
3.3 KVCache 客户端读取吞吐量
KVCache 是一种用于优化大型语言模型(LLM)推理过程的技术。它通过在解码层中缓存前一个标记的键和值向量,避免了重复计算。(KVCache 具体原理在我的FlashMLA学习笔记里有详细讲解)
单节点 KVCache 查找峰值速率超 40 GiB/s
上图展示了所有 KVCache 客户端(每个节点 1×400Gbps 网卡)的读取吞吐量,突出了峰值和平均值,峰值吞吐量可达 40GiB/s。下图展示了在同一时间段内,垃圾回收(GC)期间移除操作的 IOPS。
二、3FS 解耦架构详解
1. 解耦架构
解耦架构 是一种将系统中的不同功能模块分离的设计思想,使得每个模块可以独立开发、测试和维护,同时通过高效的通信机制协同工作。
通俗来讲,就像是把一个复杂的系统拆成几个独立的小模块,每个模块只负责做一件事,但它们之间可以通过一种高效的方式协同工作。好比一个工厂,把生产流程分成多个独立的车间,每个车间只负责一个环节,但通过传送带把它们连接起来,让整个生产过程更加高效。
解耦架构的目标主要有两点:
- 位置无关性:应用程序可以以一种“位置无关”的方式访问存储资源,即不需要关心数据存储的具体位置。这种设计简化了分布式应用程序的开发,使得应用程序可以更灵活地部署和扩展。就像在超市买东西,你只需要告诉收银员你想要什么,而不用管商品放在哪个货架上。这样用户使用起来更方便,系统也更灵活。
- 高可扩展性:每个模块都可以独立扩展。比如,如果数据量增加了,可以单独增加存储服务的节点;如果需要处理更多的用户请求,可以增加客户端的数量。就像工厂可以根据需求增加更多的车间或工人。
在 3FS 中,解耦架构的核心目标是通过分离存储资源(SSD)和网络带宽(存储节点),实现高性能、高可扩展性和高可用性的分布式文件系统。
2. 3FS 系统布局
3FS系统由四大核心组件构成:集群管理器(Cluster Manager)、元数据服务(Metadata Service)、存储服务(Storage Service)和客户端(Client)。这些组件通过高速的RDMA(Remote Direct Memory Access)网络连接在一起,RDMA网络可以是InfiniBand或RoCE(RDMA over Converged Ethernet),它们提供了低延迟和高吞吐量的通信能力。
- 集群管理器:它是3FS的“大脑”,负责监控整个集群的状态,包括各个节点的健康状况和配置信息。集群管理器通过接收来自其他组件的心跳信号来检测故障,并在必要时进行故障切换。例如,如果某个元数据服务节点失败,集群管理器可以迅速将请求重定向到其他可用节点,确保系统的高可用性。
- 元数据服务:这个组件负责管理文件系统的元数据,例如文件和目录的属性、权限、位置等信息。元数据服务是无状态的,这意味着它不存储任何本地状态信息,所有的元数据都存储在一个可靠的分布式键值存储系统中,如FoundationDB。这种设计使得元数据服务可以轻松地扩展和升级,而不会影响系统的整体运行。
- 存储服务:存储服务负责实际存储文件的数据块。每个存储服务管理一组本地SSD(固态硬盘),并提供高性能的块存储接口。存储服务通过链式复制(Chain Replication)和分配查询(Apportioned Queries,CRAQ)协议来确保数据的强一致性和高可用性。这种设计不仅提高了数据的可靠性,还充分利用了SSD和RDMA网络的高性能特性。
- 客户端:客户端是用户与3FS交互的接口。3FS提供了两种类型的客户端:FUSE客户端和本地客户端。FUSE客户端允许用户像使用本地文件系统一样访问3FS,而本地客户端则提供了更高效的I/O操作接口,适用于对性能要求较高的应用。
通过将这四大组件解耦,3FS实现了高度的灵活性和可扩展性。每个组件都可以独立优化和扩展,而不会影响其他组件的运行。例如,如果需要增加存储容量,只需添加更多的存储服务节点即可;如果需要提高元数据处理能力,可以增加更多的元数据服务节点。这种模块化的设计使得3FS能够轻松适应不同规模和性能需求的场景。
3. 文件系统多接口支持
在现代计算环境中,不同的应用程序对文件系统的接口有不同的需求。有些应用可能需要高性能的I/O操作,而另一些应用则更关注易用性和兼容性。为了满足这些多样化的需求,3FS提供了两种类型的客户端接口:FUSE客户端和本地客户端。
3.1 FUSE 客户端
FUSE(Filesystem in Userspace)是一种允许用户在用户空间中实现文件系统的技术。3FS的FUSE客户端使得用户可以像使用本地文件系统一样访问3FS。
FUSE客户端的优点:
- 易于使用和部署,不需要对应用程序进行任何修改。用户可以使用标准的文件系统API(如
open
、read
、write
等)来操作3FS中的文件和目录。 - 具有较低的采用门槛,大多数现有的应用程序都可以无缝地与它集成。
局限性:在处理高并发I/O操作时可能会遇到性能瓶颈,原因在于:
- 内存复制开销:用户空间文件系统守护进程无法访问应用程序内存。内核与用户空间之间的数据传输消耗内存带宽并增加端到端延迟。
- 原始多线程支持:当应用程序发起
I/O
请求时,FUSE
将这些请求放入一个由自旋锁保护的多线程共享队列中。用户空间文件系统守护进程随后从该队列中检索并处理请求。由于锁争用,FUSE
的I/O
处理能力无法随着线程数量的增加而扩展。
3.2 本地客户端
为了满足对性能要求较高的应用,3FS还提供了一个本地客户端。本地客户端通过直接与存储服务和元数据服务通信,绕过了FUSE的某些限制,从而实现了更高的性能。本地客户端提供了更高效的I/O操作接口,例如异步零拷贝I/O操作,这使得它能够充分利用SSD和RDMA网络的高性能特性。虽然本地客户端的使用需要对应用程序进行一定的修改,但它为性能敏感型应用提供了显著的性能优势。
3.3 文件接口灵活性与兼容性
3FS的多接口支持不仅提供了选择的灵活性,还确保了与现有应用程序的兼容性。体现在以下几个方面:
- 原子目录操作:3FS支持原子性的目录操作,例如原子移动文件/目录和递归删除整个目录。这种操作在对象存储中是难以实现的,但在文件系统中却非常重要。例如,一个常见的应用场景是创建一个临时目录,将文件写入其中,然后将整个目录移动到最终位置。如果没有原子移动操作,应用程序需要逐个文件地移动,这不仅效率低下,还可能导致数据不一致。
- 符号链接和硬链接:3FS支持符号链接和硬链接,这使得用户可以创建轻量级的文件和目录引用。符号链接和硬链接在数据备份、快照和动态更新数据集等场景中非常有用。例如,用户可以通过符号链接将一个文件或目录链接到多个位置,而无需复制实际的数据,从而节省存储空间。
- 熟悉的接口:3FS的文件系统接口是基于POSIX标准的,这意味着用户无需学习新的API。大多数现有的应用程序都是基于POSIX文件系统接口开发的,因此它们可以很容易地迁移到3FS。
4. I/O操作与元数据操作分离——异步零拷贝API
4.1 Linux的io_uring 介绍
io_uring
是 Linux 内核 5.1 版本引入的一种高性能异步 I/O 框架,由资深内核开发者 Jens Axboe 开发。它旨在提供一种更高效、更灵活的 I/O 模型,以满足现代应用程序对高吞吐量和低延迟 I/O 操作的需求。
核心原理
io_uring
通过两个核心队列实现高效的异步 I/O 操作:
- 提交队列(Submission Queue, SQ):用户态应用程序将 I/O 请求提交到该队列中,内核从该队列中读取请求并处理。
- 完成队列(Completion Queue, CQ):内核在完成 I/O 请求后,将结果写入该队列,用户态应用程序从该队列中读取完成事件。
这两个队列通过共享内存的方式在用户态和内核态之间通信,减少了数据拷贝和上下文切换的开销。
操作步骤
- 第一步:应用程序通过向 io_uring 的 提交队列 提交 I/O 操作。
- 第二步:SQ内核线程从 提交队列 中读取 I/O 操作。
- 第三步:SQ内核线程发起 I/O 请求。
- 第四步:I/O 请求完成后,SQ内核线程会将 I/O 请求的结果写入到 io_uring 的 完成队列 中。
- 第五步:应用程序可以通过从 完成队列 中读取到 I/O 操作的结果。
4.2 3FS中的异步零拷贝API
异步零拷贝 API
的灵感来自 Linux 的 io_uring
。以下是 API 中的关键数据结构:
- Iov:一个用于零拷贝读/写操作的大内存区域,在用户进程和本地客户端之间共享。
InfiniBand
内存注册由客户端管理。在本地API
中,所有读取的数据将被读入Iov
,所有写入的数据应在调用API
之前写入Iov
。 - Ior:一个用于用户进程与本地客户端之间通信的小型共享环形缓冲区。
Ior
的使用类似于 Linux 的io_uring
,用户进程将读/写请求入队,而本地客户端则将这些请求出队以完成。请求以批处理的方式执行,其大小由io_depth
参数控制。无论来自不同的环还是同一个环,多个批次都是并行处理的。然而,对于多线程应用程序,仍然建议使用多个环,因为共享一个环需要同步,这可能会影响性能。
在本地客户端中,多个线程被创建以从 Iors
中获取 I/O
请求。这些请求被批处理并分发到存储服务,从而减少因小型读请求而产生的 RPC
开销。
5. 数据存储与元数据存储的分离(一)—— 文件元数据存储
5.1 文件块的位置
- 3FS 将文件数据划分为相等大小的 块,并将这些块 条带化 到多个 复制链 中。用户可以为每个目录指定 链表、块大小 和 条带大小。
- 每个块独立存储在多个存储服务上,其块 ID 通过连接文件的 inode id 和块索引生成。
- 在创建新文件时,元数据服务根据 条带大小采用轮询策略 从指定的链表中选择连续的复制链,并生成一个随机种子以打乱所选链。这种分配策略确保了数据在链和 SSD 之间的均衡分布。
- 当应用程序打开文件时,客户端联系元服务以获取文件的数据布局信息,然后独立计算块 ID 和数据操作的链,最小化元服务在关键路径中的参与。(将部分决策(计算)的权力下放给客户端)
5.2 事务性键值存储上的文件元数据
FoundationDB 是一个开源的、分布式的、事务性的键值存储数据库,最初由 FoundationDB 公司开发,后被苹果公司收购并重新开源。它设计用于处理大规模的结构化数据,能够在多个普通服务器集群上运行,并且支持 ACID 事务,确保数据的一致性和可靠性。
3FS 选择使用 FoundationDB 作为底层的 KV 存储系统。该架构简化了元数据整体设计,将可靠性、扩展性等分布式系统通用能力下沉到分布式 KV 存储,元服务节点只是充当文件存储元数据的代理,负责语义解析。
元服务遵循无状态架构,增强了可维护性,支持无缝升级或重启服务。
文件系统元数据主要由 inode 和 目录条目 组成:
- inode 存储文件、目录和符号链接的属性信息,每个 inode 由一个全球唯一的 64位标识符 标识。
- inode 键通过将 “INOD” 前缀与 inode id 连接构成,值根据其类型而异:
- 基本属性:所有权、权限、访问/修改/变更时间。
- 文件 inode 的 附加属性:文件长度、块大小、链表中选择的范围、shuffle 种子。
- 目录 inode 的 附加属性:父目录的 inode ID、子目录/文件的默认布局配置。
- 符号链接 inode 的 附加属性:目标路径字符串。
- 目录条目 键由 “DENT” 前缀、父 inode ID 和 条目名称 组成,值存储目标 inode ID 和 inode 类型。
元操作利用 FoundationDB 的事务:
- 只读事务:用于元数据查询(如
fstat
、lookup
、listdir
)。 - 读写事务:用于元数据更新(如创建、链接、取消链接、重命名)。
对于写事务,FoundationDB 跟踪读/写键集以形成冲突检测集,当检测到并发事务冲突时,元服务会自动重试该事务。
5.3 动态文件属性
在大多数本地文件系统中,删除已打开的文件会延迟,直到所有相关的文件描述符关闭。因此,有必要跟踪该文件的所有文件描述符。训练作业在启动期间会打开大量文件。存储所有文件描述符会对元服务和 FoundationDB
施加很大负担。由于训练作业不依赖于此功能,3FS
不跟踪以只读模式打开的文件描述符。
3FS
为每个以 写模式 打开的文件描述符(fd
)维护一个文件会话,因为删除以写模式打开的文件可能导致来自并发写入的不可回收垃圾块。当删除具有活动写会话的文件时,元服务会延迟删除,直到所有其文件描述符关闭。为了防止离线客户端的会话持续存在,3FS
元服务定期检查客户端的存活状态,并清理离线客户端的会话。
文件长度存储在 inode
中。客户端定期(默认 每5秒)向元服务报告以写模式打开的每个文件的最大写入位置。由于多个客户端可能进行并发写入,上述方法仅确保文件长度的最终一致性。在处理 关闭/fsync 操作时,元服务通过查询存储服务中最后一个块的 ID
和长度来获取精确的文件长度。为避免多个元服务对同一文件长度的并发更新导致事务冲突,元服务使用 inode ID 和汇合哈希算法将文件长度更新任务分配到多个元服务。
对于小文件,3FS 使用潜在使用的链的数量(初始值为 16,每次向更多链写入额外的文件块时翻倍)作为提示,以避免在更新小文件长度时查询所有 200 条链。
6. 数据存储与元数据存储的分离(二)—— 块存储系统
块存储系统是3FS的核心组件之一,它负责实际存储文件的数据块。其设计目标是即使在存储介质发生故障时也能实现尽可能高的带宽。3FS
的读/写吞吐量应与 SSD
的数量和客户端与存储服务之间的双向网络带宽线性扩展。应用程序以无关位置的方式访问存储服务。
块存储系统是 3FS 实现数据强一致性的关键所在,将在下一小节详细介绍。
三、数据强一致性详解
每个存储服务管理几个本地 SSD
,并提供一个块存储接口。存储服务实现了 带有分配查询(Chain Replication with Apportioned Queries, CRAQ)的链式复制,以确保强一致性。CRAQ
的 全写-任读(write-all-read-any) 方法有助于释放 SSD
和 RDMA
网络的吞吐量。一个 3FS
文件被分割成大小相等的块,这些块在多个 SSD
上进行复制。
1. CRAQ 介绍
为了实现高性能和高可用性,3FS的块存储系统采用了链式复制(Chain Replication)和分配查询(Apportioned Queries)CRAQ 协议。
CRAQ是一种针对读取密集型工作负载优化的数据复制协议。它的目标是确保数据在多个副本之间保持一致,同时最大化读取性能。
1.1 链式复制(Chain Replication, CR)
链式复制(Chain Replication, CR)是一种用于分布式系统的数据复制方法,能够提供强一致性的存储接口。其核心思想是将多个节点按照顺序组织成一条链,写操作从链头开始,逐步传播至链尾,而读操作则全部由链尾处理,以确保数据的一致性。
在写操作中,客户端的写请求首先被链头节点接收,然后写数据依次向链中的下一个节点传播。当写操作到达链尾并被应用到所有副本后,该写操作才被视为提交完成。此时,链尾会向链头发送确认消息,链头再将确认结果返回给客户端。通过这种方式,CR 确保了所有写操作按顺序传播并最终一致。如下图,是一个拥有四个节点的链:
在读操作中,所有客户端的读请求都由链尾节点处理。由于写操作只有在到达链尾并提交后才算完成,因此链尾节点返回的数据始终是最新的、已提交的值。
1.2 CRAQ 优化读吞吐量
上述设计保证了强一致性,但也造成了一定的局限性:由于所有读请求都集中在链尾节点,其读吞吐量受限于单一节点的处理能力,无法通过增加节点数来扩展读性能。此外,随着链长度增加,写操作需要经过更多节点传播到尾部,这可能导致写延迟上升。
CRAQ(Chain Replication with Apportioned Queries)针对以读为主的工作负载场景,通过允许链中任意节点处理读请求来提高读取吞吐量,同时仍然保证强一致性。为了保证这一点,CRAQ 规定:
- 多版本存储与状态标记:每个节点可以存储对象的多个版本,每个版本包含一个单调递增的版本号,并标记为“干净(clean)”或“脏(dirty)”状态。新版本初始标记为clean,只有当写操作在链尾提交后,版本才会被标记为clean。
- 写操作传播流程:
- 当节点接收到写操作时,会将该对象的新版本添加到本地存储中。
- 如果该节点不是链尾,则将新版本标记为脏,并将写操作传递给下一个节点。
- 如果该节点是链尾,则将新版本标记为干净,并反向通知其他节点数据已经提交。
- 当节点收到链尾的提交确认消息时,会将对应版本标记为干净,并删除所有旧版本。
- 读操作传播流程:
-
如果节点存储的最新版本是干净的,则直接返回该值。如下图所示:
-
如果最新版本是脏的,节点会向链尾发送“版本查询请求”,获取链尾已提交的最新版本号,并返回对应的对象值。如下图所示:
-
CRAQ 相较于 CR 的性能提升体现表现在:
- 以读为主的工作负载:大部分读请求由非链尾节点(C-1 个)处理,读吞吐量随链长度线性扩展。
- 以写为主的工作负载:大部分读请求需通过非链尾节点查询版本,但这些查询比完整读取轻量,因此链尾能以更高速率处理查询,从而提高整体吞吐量。
深入阅读:CRAQ: 分布式对象存储系统
2. CRAQ 下的数据放置
每个 文件块 使用分配查询(CRAQ
)的链式复制在一系列存储目标上进行复制。在 CRAQ
中,写请求被发送到头目标并沿着链传播。读取请求可以发送到任何存储目标。通常,读取流量在链中的所有目标之间均匀分配,以实现更好的负载平衡。在每个 SSD
上创建多个存储目标,这些目标加入不同的链。
假设有 6 个节点:A
、B
、C
、D
、E
、F
。每个节点有 1 个 SSD
。在每个 SSD
上创建 5 个存储目标:1
、2
、… 5
。那么总共有30个目标:A1
、A2
、A3
、…、F5
。如果每个块有 3 个副本,则构建链表如下。
链 | 版本 | 目标1(头) | 目标2 | 目标3(尾) |
---|---|---|---|---|
1 | 1 | A1 | B1 | C1 |
2 | 1 | D1 | E1 | F1 |
3 | 1 | A2 | B2 | C2 |
4 | 1 | D2 | E2 | F2 |
5 | 1 | A3 | B3 | C3 |
6 | 1 | D3 | E3 | F3 |
7 | 1 | A4 | B4 | C4 |
8 | 1 | D4 | E4 | F4 |
9 | 1 | A5 | B5 | C5 |
10 | 1 | D5 | E5 | F5 |
每个链都有一个版本号。如果链发生变化(例如,存储目标离线),则版本号会递增。只有主集群管理器才能对链表进行更改。
可以构建几个链表以支持不同的数据放置需求。例如,可以创建两个链表,一个用于批处理/离线作业,另一个用于在线服务。这两个表由位于互斥节点和 SSD
上的存储目标组成。
从逻辑上讲,每个链的状态独立变化。每个链可以包含在多个链表中。链表的概念是为了让元数据服务为每个文件选择一个表,并在表中的链上划分文件块。
3. 恢复期间的流量平衡
假设读取流量在上述链表中的所有存储目标之间均匀分布。当 A
失败时,其读取请求将被重定向到 B
和 C
。在重负载下,B
和 C
的读取带宽会立即饱和,B
和 C
成为整个系统的瓶颈。更换故障的 SSD
并将数据同步到新的 SSD
可能需要几个小时。在此期间,读取吞吐量受到影响。
为了减少性能影响,我们可以让更多的 SSD
共享重定向的流量。例如,在以下链表中,A
与其他每个 SSD
配对。当 A
失败时,其他每个 SSD
接收 A
的读取流量的 1/5。这种负载平衡的最优解是通过使用整数规划求解器获得的。
链 | 版本 | 目标1(头) | 目标2 | 目标3(尾) |
---|---|---|---|---|
1 | 1 | B1 | E1 | F1 |
2 | 1 | A1 | B2 | D1 |
3 | 1 | A2 | D2 | F2 |
4 | 1 | C1 | D3 | E2 |
5 | 1 | A3 | C2 | F3 |
6 | 1 | A4 | B3 | E3 |
7 | 1 | B4 | C3 | F4 |
8 | 1 | B5 | C4 | E4 |
9 | 1 | A5 | C5 | D4 |
10 | 1 | D5 | E5 | F5 |
4. CRAQ 下的数据复制
CRAQ
是一种针对读取密集型工作负载优化的写全读任意复制协议。利用所有副本的读取带宽对于在全闪存存储系统中实现最高读取吞吐量至关重要。
4.1 写操作的处理
当存储服务接收到写请求时,它会经过以下步骤:
- 版本检查: 服务检查写请求中的链版本是否与最新已知版本匹配;如果不匹配,则拒绝请求。写请求可以由客户端或链中的前驱发送。
- 数据提取: 服务发出
RDMA
读取操作以提取写入数据。如果客户端/前驱失败,RDMA
读取操作可能会超时,写入将被中止。 - 获取锁: 一旦写入数据被提取到本地内存缓冲区,将从锁管理器获取要更新的块的锁。对同一块的并发写入被阻塞。所有写入在头目标处被序列化。
- 更新数据: 服务将块的已提交版本读取到内存中,应用更新,并将更新后的块存储为待处理版本。存储目标可以存储块的两个版本:已提交版本和待处理版本。每个版本都有一个单调递增的版本号。
- 版本替换: 如果服务是尾部,则已提交版本被原子地替换为待处理版本,并向前驱发送确认消息。否则,写请求将被转发到后继者。当已提交版本被更新时,当前链版本作为一个字段存储在块元数据中。
- 消息传播: 当确认消息到达存储服务时,该服务用待处理版本替换已提交版本,并继续将消息传播给其前驱。然后释放本地块锁。
故障处理示例:
假设链中有 3 个目标:A, B, C
。写请求刚刚在 A
的第 5 步进入。A
将请求转发给后继者 B
。然后 B
瞬间失败,转发的写请求丢失。当集群管理器检测到 B
的失败时,它将 B
标记为离线,并将其移动到链的末尾,并广播更新后的链表。一旦 A
接收到最新的链表,它将写请求转发给新的后继者 C
。C
可能尚未接收到最新的链表,并拒绝该请求。但是 A
可以继续将请求转发给 C
。最终,C
获取最新的链表并接受请求。
4.2 读请求的处理
当读取请求到达存储服务时:
- 返回已提交版本: 当服务仅拥有已提交版本的块时,该版本将返回给客户端。
- 处理待处理版本: 与 CRAQ 不同,我们的实现不会向尾部目标发出版本查询。当同时存在 已提交版本 和 待处理版本 时,服务会回复一个特殊状态码以通知客户端。客户端可以等待短暂的时间并重试。或者客户端可以发出一个放宽的读取请求以获取待处理版本。
5. 故障检测
5.1 心跳机制
集群管理器依赖心跳来检测故障停止故障。如果集群管理器在可配置的时间间隔内(例如 T
秒)未收到来自服务的心跳,则声明该服务失败。如果服务无法与集群管理器通信超过 T/2
秒,则停止处理请求并退出。心跳可以视为向管理器 续租 的请求。
5.2 状态管理
集群管理器在存储服务的成员变更中扮演着更为关键的角色。它维护着链表和存储目标状态的全局视图。每个存储目标都有一个公共状态和一个本地状态。
公共状态指示它是否准备好处理读取请求,以及写请求是否会被传播到它。公共状态与链表一起存储,并分发给服务和客户端。
公共状态 | 读取 | 写入 | 备注 |
---|---|---|---|
服务中 | Y | Y | 服务正常并处理客户端请求 |
同步中 | N | Y | 服务正常,数据恢复正在进行中 |
等待中 | N | N | 服务正常,数据恢复尚未开始 |
lastsrv | N | N | 服务宕机,并且这是最后一个服务目标 |
离线 | N | N | 服务宕机或存储介质故障 |
本地状态仅由存储服务和集群管理器知道,并存储在集群管理器的内存中。如果存储目标发生介质故障,相关服务将在心跳中将目标的本地状态设置为离线。如果存储服务宕机,由该服务管理的存储目标将被标记为离线。
本地状态 | 备注 |
---|---|
最新 | 服务正常并处理客户端请求 |
在线 | 服务正常,目标处于同步或等待状态 |
离线 | 服务宕机或存储介质故障 |
5.3 状态转换
存储目标可以根据最新的本地状态在不同的公共状态之间变化。本地状态充当触发事件的角色。集群管理器定期扫描每个链,并根据状态转换表更新链上目标的公共状态。
本地状态 | 当前公共状态 | 上一个公共状态 | 下一个公共状态 |
---|---|---|---|
最新 | 服务中 | (任何) | 服务中 |
同步中 | (任何) | 服务中 | |
等待中 | (任何) | 等待中 | |
lastsrv | (任何) | 服务中 | |
离线 | (任何) | 等待中 | |
在线 | 服务中 | (任何) | 服务中 |
同步中 | 服务中 | 同步中 | |
未提供服务 | 等待中 | ||
等待中 | 服务中 | 同步中 | |
未提供服务 | 等待中 | ||
lastsrv | (任何) | 服务中 | |
离线 | (任何) | 等待中 | |
离线 | 服务中 | 没有前驱 | lastsrv |
有前驱 | 离线 | ||
同步中 | (任何) | 离线 | |
等待中 | (任何) | 离线 | |
lastsrv | (任何) | lastsrv | |
离线 | (任何) | 离线 |
6. 数据恢复
当存储服务退出(例如,进程崩溃或在升级期间重启)或发生存储介质故障时,所有相关的存储目标将被标记为离线,并由集群管理器移动到链的末尾。一旦服务重启,服务上的每个目标将独立进入恢复过程。整个恢复过程与正常活动重叠,并最小化任何中断。
6.1 离线的存储服务启动
- 离线服务启动:服务定期从集群管理器拉取最新的链表。但在所有存储目标在最新链表中被标记为离线之前,它不会发送心跳。这确保了所有目标都会经过数据恢复过程。
- 写请求处理:当写请求在恢复期间到达时,请求始终是全块替换写入。本地已提交版本被更新,任何现有的待处理版本被放弃。由于当前服务是尾部,确认消息被发送给前驱。前驱的完整状态通过连续的全块替换写入流复制到返回服务。
- 元数据同步:在存储目标的数据恢复开始之前,前驱向返回服务发送一个转储块元数据请求。然后服务迭代本地块元数据存储,以收集目标上所有块的
ID
、链版本和已提交/待处理版本号,并将收集到的元数据回复给前驱。 - 同步完成:当同步完成消息到达时,服务知道存储目标是最新的。它在发送给集群管理器的心跳消息中将目标的本地状态设置为最新。
6.2 存储服务发现之前离线的后继者上线
- 写请求转发:服务开始将正常的写请求转发给后继者。客户端可能只更新块的一部分,但转发的写请求应包含整个块,即全块替换写入。
- 元数据比较:服务向后继者发送一个转储块元数据请求。一旦接收到后继目标上所有块的元数据,它会在本地目标上收集块元数据。然后,它比较两份块元数据,以决定哪些块应该被转移。
- 块转移:选定的块通过发出全块替换写请求转移到后继者。
- 首先为每个块获取块锁。
- 链版本、已提交版本号和块内容被读取并通过发送全块替换请求转移到后继者。
- 块锁被释放。
- 同步完成:当所有所需的块都已转移后,向后继者发送同步完成消息。
块转移规则:
- 如果一个块仅存在于本地目标上,则应该被转移。
- 如果一个块仅存在于远程目标上,则应将其删除。
- 如果本地块副本的链版本大于远程块副本的链版本,则应进行传输。
- 如果本地/远程块副本的链版本相同,但本地已提交版本号不等于远程待处理版本号,则应进行传输。
- 否则,两个块副本要么是相同的,要么正在被进行中的写请求更新。
四、Smallpond 介绍
1. Smallpond 是什么?
Smallpond 是由 DeepSeek 推出的一个轻量级数据处理框架,专为高性能和大规模数据处理设计。它基于 DuckDB 和 3FS 构建,能够高效处理 PB 级数据。
DuckDB 是一个开源的 OLAP(在线分析处理)数据库,专为数据分析而设计。它类似于 SQLite,是一个可以嵌入到应用程序中的进程内数据库。这种设计使得 DuckDB 能够在同一内存地址空间内传输数据,从而消除了通过套接字复制大量数据的需要,大大提高了性能。
Smallpond 的核心目标是将 DuckDB 的高性能 SQL 分析能力扩展到分布式环境中,同时结合 3FS 的分布式存储优势,提供一个简单易用且高效的解决方案。
2. 核心功能
- 高效数据处理:Smallpond 借助 DuckDB 的高性能分析能力,支持高效的数据加载、查询和转换。
- 支持多种数据格式:Smallpond 支持多种数据格式,包括 Parquet、CSV、JSON 等。
- 并行处理:Smallpond 支持数据分区和并行处理,能够充分利用集群资源,提高处理效率。
- 灵活的 API:Smallpond 提供了高层和低层两种 API。高层 API 以
DataFrame
为核心,使用上类似 Pandas 和 PySpark;低层 API 提供更灵活的配置选项。 - 惰性执行:Smallpond 使用惰性求值机制,避免不必要的计算,并优化工作流程。
3. 应用场景
- 大规模数据预处理:Smallpond 能高效处理和转换大规模数据集,支持数据清洗、格式转换和特征提取。
- 数据分析与实时查询:Smallpond 能快速执行复杂的数据分析和实时查询任务,适用于数据仪表盘和实时监控系统。
- 分布式机器学习训练:Smallpond 为分布式机器学习训练任务提供强大的数据支持,提升训练效率。
- 嵌入式数据分析应用:Smallpond 轻量级的设计使其能够轻松嵌入到各种应用中,为嵌入式设备或资源受限的环境提供高效的数据分析能力。
4. 使用示例
import smallpond
# 初始化会话
sp = smallpond.init()
# 加载数据
df = sp.read_parquet("prices.parquet")
# 处理数据
df = df.repartition(3, hash_by="ticker")
df = sp.partial_sql("SELECT ticker, min(price), max(price) FROM {0} GROUP BY ticker", df)
# 保存结果
df.write_parquet("output/")
# 显示结果
print(df.to_pandas())
五、小结
总的来说,3FS 是一个高性能分布式文件系统,通过块存储、元数据管理和动态文件属性优化,实现了高效的数据存储与管理。它利用现代 SSD 和 RDMA 网络,支持高吞吐量和低延迟的数据访问,特别适合 AI 训练和推理工作负载。Smallpond 则是基于 3FS 和 DuckDB 构建的轻量级数据处理框架,专为大规模数据处理设计。它具备高效性、易用性和无需运维的特点,支持多种数据格式和并行处理,能够显著提升数据处理效率。两者结合,为 AI 数据处理提供了强大的支持。
写在最后:DeepSeek 开源周 回顾总结
在 2025 年 2 月 24 日至 28 日的 DeepSeek 开源周期间,DeepSeek 开源了五个核心项目,这些项目涵盖了 AI 开发的计算优化、通信效率和存储加速等关键领域,共同构成了一个面向大规模 AI 的高性能基础设施。
- FlashMLA:针对 NVIDIA Hopper GPU 优化的多头线性注意力解码内核,显著提升了推理效率,支持实时翻译和长文本处理。
- DeepEP:专为混合专家模型(MoE)设计的通信库,通过优化节点间数据分发与合并,大幅提升了训练速度。
- DeepGEMM:基于 FP8 的高效矩阵乘法库,专为 MoE 模型优化,显著提升了大规模矩阵运算效率。
- DualPipe & EPLB:创新的双向流水线并行算法和动态负载均衡工具,优化了资源利用率,提升了并行训练效率。
- 3FS & Smallpond:高性能分布式文件系统,支持 RDMA 网络和 SSD 存储,解决了海量数据的高速存取与管理问题。
这些项目相辅相成,从提升 GPU 显存效率、打通分布式通信瓶颈,到优化核心算子性能,再到提高并行训练效率和夯实数据底座,共同构筑了一个面向大规模 AI 的高性能基石。通过开源,DeepSeek 不仅展示了其在 AI 基础设施领域的创新能力,还推动了技术的普及和行业标准的制定。
通过 DeepSeek 开源周系列的学习,让我深刻体会到技术的普惠价值。开源不仅是一种技术分享,更是一种推动行业发展的力量。通过开源,DeepSeek 为开发者提供了强大的工具和资源,降低了 AI 开发的门槛,使得更多人能够参与到这一领域的创新中来。这种开放精神不仅促进了技术的传播,也为整个行业带来了新的活力和可能性。未来,我相信开源将继续成为推动技术进步的重要力量,而 DeepSeek 的这些项目也将成为 AI 领域的重要基石,为更多开发者和研究者提供支持和启发。
同时,欢迎大家阅读我整理的五篇学习/解读笔记,如有错漏,欢迎批评指正!