引言
3FS 推出之后,围绕着其源码解读和设计相关的文章陆续推出。本文主要结合代码和 design_notes 对 Storage 部分进行分析探讨。
架构和整体位置
画板
上图中 StorageService 即为本次分析的重点。ChunkStorage 本身提供了三个基础的功能如下:
-
-
2. 基于 RDMA 的通信链路,数据面 I/O 处理。
-
3. 提供支撑链式复制(Chained-Replication)在容错、数据一致性方面方面的支持。
其中前两条更偏向单机引擎探讨,第三条侧重 StorageService 对于分布式层面的支持。本文主要从前两个部分做一些分析,分布式部分涉及到容灾以及和 Client/Meta 的联动处理,不在本次讨论范围。
空间纳管
一句话总结
: 负责空间池化,以参数可调的方式实现
利用率和性能
的可调性。
池化概念
chunk 是 3FS 中的重要概念,是文件逻辑 LBA 到池化存储 mapping 的一个"连接"。一个上层文件的LBA会映射成一组 chunks,按 inode_
{seq} 单调递增。3FS 做的存储池化就是把底层的存储空间打平来承载上层的产生的 chunks。client 按固定大小进行切分 chunk(大小做到可调), 则 StorageService 的工作是尽可能减少空间碎片的目标下提升性能,做好 trade-off。
空间管理
如下图所示一个物理磁盘和逻辑 StorageTarget 是 1 对多的关系,每个 Target 有一个专属的 ChunkEngine 具体负责 Target 内的空间和 I/O 管理。假设把 disk 以及其关联所有 Target 路径关联作为一个抽象的 DiskUnit 来看待,则整体 high-level 关系如下图:
下钻到 ChunkEngine, 主体上包含处理 chunk 空间相关的 Allocator 模块,以及数据面接口。这里只关注空间管理相关的 Allocator 部分逻辑。
Allocator 和两部分数据打交道,用户 Data 和 元数据 Meta Data。数据 Data 保存在文件系统中。元数据保存在 RocksDB中。
空间分配
下图是 ChunkEngine 中 分配 chunk 的整体流程:
-
• Allocator 由 11 个实例组成,负责从最小的 64KB 到最大的 64MB 的 chunk 分配
-
• 每个 Allocator 会分配 256 个 File,用作真正的数据进行存储,在逻辑上每个 File 按照 Group 组织,一个 Group 包含 256 Chunk(使用 bitmap 索引)
-
• 分配 Group 的顺序如上图所示,可以保证文件的大小均衡。整体上看,Engine 通过调用 Allocator::allocate 请求分配一个新存储块。Allocator 会检查现有的存储块是否可用,如果没有会通过 ChunkAllocator 和 GroupAllocator 来分配新的块和Group。在 ChunkAllocator 不能满足需求时,例如没有 active_group,全部都是 full_group 时,会请求 GroupAllocator 分配一个新的 Group。
-
-
1. 根据调用者提供的 size,选择相应的 allocator。
-
2. 按照内存状态挑选
最不空闲
的已分配的 group(如果没有,则分配 group),从 group 中挑选 chunk 进行分配。
-
3. 在 Engine 中更新 chunk_id 到 chunk 的映射,并且持久化映射关系和 allocator 的分配信息。
理解
-
1. 上述 a, b 步骤主要是让 Group 的内部碎片收敛进而提升利用率,a 类似操作系统中 slab 的概念;b 采用贪心的方式去尽可能填满 group 内的洞。
-
1. 这种方式非常依赖上层 chunk 切片的固化 pattern,只有相互配合才能 match;一种更灵活的方式是 allocator 按需分配,即收到 chunk 分配请求首次按需分配,灵活但可能存在一些毛刺。
-
2. 在删除或者反复擦写不多的场景下,一般都按照上图的 group 分配走向顺序向后;否则存在空洞的 group 比较分散的时候,按上述策略分配顺序会比较散,从写入的 locality 角度来说不太友好,不过 NVME SSD 的情况下,这个性能问题应该不会太突出。
-
2. c 是保证上层业务索引(meta service 中索引存储)在底层 chunk 发生位置变更下而不用变化的前提,因为 chunk_id -> chunk location 的映射是
底层自治
的(存储在本地 rocksdb 中),而上层只按照 chunk id 寻址。
-
3. 自治性: 每个 Target 都是一个资源自闭包的抽象,拥有自身独立的 MetaDB (rocksdb实例)。
引用计数
当块分配成功,存储块被读写的时候,Engine 会通过 Allocator::reference 引用这个存储块。ChunkAllocator 会增加这个块的 Position reference count。
当存储块不再需要的时候, Engine 通过 Allocator::dereference 释放存储块。ChunkAllocator 会减少这个块的 Position reference count。当 reference count 为 0 的时候释放。
当一个 Group 为空时,GroupAllocator 会回收这个组。
碎片整理
-
• 当发现申请了过多 chunk 但是已经废弃的情况(可能是删除或者是上层文件层的覆盖),则会针对相应的一些 group 进行碎片整理,整理之前 group 需要进入到 Frozen 态。
-
• 找到使用但是空闲程度较高的 group(如上图中的group),将其使用到的 chunk move 到另外的 group,并将整理好的 group 从 Frozen 放到 Active 态以供重复。上图中的搬运(Move)本质上是针对文件删除或者随机覆盖写之后所产生的空洞进行回收,让碎片率高的 group 通过整理之后变得连续,但是搬运会产生背景流量,在 Allocator 的空间不能做弹性伸缩的情况下,这样做的意义还不是特别明白。需要说明的是碎片整理是周期性的后台任务,因此在存在较多删除的情况下后台整理的流量应该是需要被考虑 (调整碎片 ratio)。此外考虑到数据搬运的过程中,作为 src 的 Group0 可能还存在读请求,因此基于引用计数的方式是一种常规的做法,即确保在一个 group 被整理完成重新变成 active。在被重复利用之前,其中每个 chunk pos 的 ref count 都为 0。分配逻辑中 ref/unref 的粒度是 group 中的具体一个 chunk pos[group_id, index], 以上图为例,左侧 move 的 src pos 对应 [group_0, 3]。
空间回收上, chunk 对应的 pos 被 unref 之后开始,如果 refcount 降为 0 会进行 deallocate 空间回收,3FS 因为使用了文件系统,在空间回收上可以做一些简化处理,当 group 空间全部回收后,采用的是对 group 进行整体回收空间(punch hole)。