CubeFS 大视野|万亿级元数据路由设计与优化

文摘   2024-10-21 09:24   中国香港  

01

前言


文件系统就是管理文件的组织及访问的系统。


文件系统一直是发展统一的。从出生至今,无论单机文件系统还是分布式文件系统,都使用两种最基础的概念 inode 和 dentry(可参阅前序文章,CubeFS 大视野 | 分布式文件系统的历史演进)。


首先应意识到各现实的文件系统没有高级、低级之分,只有实际场景的功能选择;比如早期万行代码的 ext2 文件系统依然在使用,如要支持大文件、校验、容灾、快照等功能就需要选择 xfs、Btrfs、ZFS 等等。


本文主要探讨 CubeFS 服务于多租户、多名字空间、海量文件时,元数据的高效路由方案(可参阅前序文章,CubeFS存储技术揭秘-元数据管理


02

现状


目前 CubeFS 是基于 Inode Range 路由方案。每个 Volume 由 Master 分配多个 MP,Volume 之间的 MP 相互独立,每个 MP 管理该 Volume 下的固定一段 inode,关键在于每个 MP 还需对应一个 Raft Group。


图 1,CubeFS inode 分区


图 2,CubeFS dentry 分区


可优点化


  • 在单集群大规模 Volume 场景下,集群中会存在大量的 MP 以及其 Raft Group。

  • 目前大目录限制了 2000W 的 dentry,因为单分区下子目录的 dentry 树无法分裂,虽然该文件数量满足大多数场景,但如果在对象存储这类扁平化平铺元数据场景下,不能满足超大规模目录项,从而引发场景使用限制及性能问题。

  • 对 S3 场景的元数据路径查找,需要逐层查找,效率较低。

  • 数据聚合簇极小。每个 MP 只能聚合自身管理的 inode 下相应 Volume 的小 IO。


CubeFS 的 Volume 可以理解为下文的 Space 概念;MP 对应于下文的 Shard。


03

方案一:Key Range 分区


采用 Key Range 分区路由,同一集群多个 Space 可共享 Shard 分区,Space 与分区互相独立,利用分层解除了两者之间的绑定关系。


图 3,Key Range 分区方案

分布实例


假设集群中存在如下 3 个 Shard,Range = Space + InodeID:


Shard ID

Range

1

[/space1/1, /space1/3)

2

[/space1/3, /space2/2)

3

[/space2/2, max)

表 1,Key Range Shard 分布


元数据路由分布如下:


用户索引 Key 分为 inode 和 dentry:        inode  Key = Space + InodeID        dentry Key = Space + 父目录 InodeID + dentry 名分区索引 Shard Key = Key


表 2,Key Range 元数据路由分布


  • Key Range 分区模式下,写入的 Key 和 Shard Key 相同,即通过范围索引。

  • 按照 Space + Inode 拼接目录项名作为 Shard Key;相邻的文件就会落于同一个 Shard 上,以此实现目录 lookup/readdir 等操作的亲和性,同时尽可能避免同一目录下的跨分区的事务操作。

  • 父目录的元数据和其下 dentry 项存放在同一 Shard 上,能保持 readdir 等操作的局部性。

  • 不同 Space 有不同的 Shard Key 前缀,因此可以实现不同 Space Shard 的独立性。


优缺点


优点


  1. Key Range 分区方案是分层管理模式。底层的 Shard 扩容与上层 Space 没有关联,即是说 Raft Group 数量只跟集群数据规模有关,和上层租户数量无关。

  2. 支持目录亲和性放置策略,单目录下的文件可以组织到一个或多个相邻 Shard 分区上,提升 lookup/readdir 性能。

  3. 多 Space 数据可共享同一个 Shard,提升跨 Space 级的小 IO 聚合簇能力。

  4. 支持元数据扁平化组织,即 1 次查找即可获取相应元数据;也可扩展为上述的层级命名空间结构用于支持文件系统目录树语义。

  5. 保持二级索引全局有序,可实现类似:where a >= 10、where b like "123*" 等局部搜索语义,无需遍历所有分区数据。


注:业界有较多的分布式 KV、NewSQL 产品均采用 Range 分区方案,如 CockRoachDB、TiDB、AWS S3 等。


缺点


采用 Key Range 分区在集群初始化时默认只有 1 个 Shard 分区,需要通过不断分裂来进行扩展,冷启动时可能会成为瓶颈。


预分区


自然的 Range 分区虽具有数据局部放置性特点,但无法兼顾数据负载的均衡性。即集群初始化后还需要依赖数据写入来进行分裂、扩容,在冷启动、数据迁移场景会引入读写瓶颈。


引入分区预分裂方案即预创建分区;为 Space 创建特定的 Range 前缀来实现不同 Space 数据的预分配和打散。


注:Range 预分区只能解决不同 Space 数据的打散读写;对于同一个 Space 的数据,还是会写入到固定分区上。


比如集群初始化时预创建以下分区:


Shard ID

Range

1

[a, e) 

2

[e, t)

3

[t, max)

表 3,预分区 Shard 分布


Range Prefix 为固定前缀,Space 的分布如下:


Space

SpaceID

Space Shard Key Prefix

Shard

Space1

1

a1

1

Space2

2

f2

2

Space3

3

z3

3

space4

4

a4

1

...


_


_


_

表 4,预分区的 Space 分布


文件系统关键流程


文件查找


比如查找 表 2 中已有文件 space1/b/f2。


  • 先查找根目录 space1/1,在 shard 1 中获取 (space1/1, b) 对应的元数据,得到 space1/b 的 InodeID 为 3。

  • 接下来查找目录 space1/b 下 (space1/3, f2) 元数据,得到 space1/b/f2 InodeID 5。

  • 最后在 shard 2 中查找 space1/5 的元数据,即目标文件对应的 Inode。


文件层级查找方式与现有 inode range 设计类似。


文件创建


创建操作,主要涉及两个元数据变更:


  • 为创建新的文件 inode 元数据

  • 更新其父目录的元数据(dentry,links,mtime 等)。


由以上路由规则,子文件/目录的 dentry 项和其父目录元数据基本保持在同一个分区中;因此可使用分区插入更新原语,类似:


// 创建文件时更新父目录insert {new_file_dentry} with update set {mtime=now}where shard_key = "space1/3"// 创建目录时更新父目录insert {new_dir_dentry} with update set {links++, mtime=now}where shard_key = "space1/3"


对于超大规模目录,该目录的 dentry 项可能会跨多个相邻分区;更新操作如需跨分区,则通过分布式事务保证原子性和强一致性。


文件删除


删除和创建流程一致,均可通过单分区上的更新原语。


// 删除文件时更新父目录delete {file_dentry} with update set {mtime=now}where shard_key = "space1/3"// 删除目录时更新父目录delete {dir_dentry} with update set {links--, mtime=now}where shard_key = "space1/3"


目录列举


单目录列举只需在一个分区内即可获取到所有子文件/子目录的 dentry 元数据;可通过递归列举子目录。若还需文件的 inode 元数据,则可向各文件所在的分区发起读取请求。


重命名


rename 操作分为同目录、跨目录:


  • 同目录 rename 操作,由于 inodeID 保持不变,只需单分片更新原语即可。

  • 跨目录 rename 操作,由分布式事务保障强一致性。


硬链接


海量数据场景文件 inode 和 dentry 大概率不在同分区,则需要通过分布式事务保证原子性和一致性。


  • 操作文件 inode 分区,将文件元数据的 link++

  • 在父目录分区中新增 dentry 项


第二个阶段可用分区更新原语:


insert {new_file_dentry} with update set {mtime=now}where shard_key = "/space1/4"


软链接


  • 创建新的文件的 inode,内容指向原文件

  • 在父目录分区中新增 dentry 项,可用分区更新原语


分区分裂与迁移


分裂


  • 随着同一分区的数据不断写入,当分区数据达到一定阈值(默认 1G)或触发服务器资源告警时会进行分裂。

  • 分裂时要进行数据分布和热点探识,决策出分裂点。比如 [a, z) 分区中数据分布中位线在 d 附近,则将分裂为 [a, d) 和 [d, z) 两个分区。

  • 分裂策略上设定优先级,优先 Space 之间进行分裂,其次再在 Space 内部进行分裂。

  • Range 段内分裂决策可以通过两种方法计算:

    通过迭代器进行整体数据统计,遍历出数据中位点,决策出 SplitKey 的位置(参考 CockroachDB 和 TiKV Size Split 做法)。

    另一种则是近似计算,依赖底层 KV 引擎的实现。比如采用 RocksDB 时,定义相应的 table property collector 在每个 SST File 创建时收集其分布和大小,可近似统计出相近范围内的数据,用于 SplitKey 的判断和决策。

  • 多分区数据存储在同一个 Rocksdb 实例上,分裂时按照数据量和热点进行分割;申请新的分区并修改相应范围即可完成分裂操作,无需迁移数据。


迁移


当服务节点上分区数量较多、负载较高时会触发迁移操作,数据迁移以分区为粒度。


  • 在分区 Raft Group 添加新目标节点

  • 以快照方式传输全量数据,并应用 WAL 同步增量数据

  • 触发 Raft 切主,移除旧节点并完成数据清理


合并


对于数据量下降的分区,可通过定期巡检触发分区合并。


  • 合并前选择相邻的 Range 分区进行,将多个相邻的 Range 迁移到同一组节点上

  • 合并时只需修改分区 Range 范围合并为一个即可

  • 合并后再删除旧 Range 及其下的数据


04

方案二:单一 Hash 分区


采用单一 Hash 分区 Key 路由方式,同集群下 Space 间共享 Shard 分区。允许指定一种 Hash key 来支持局部性放置策略,支持父目录元数据和其子文件/目录放置在同一分区。


分布实例


假设集群中存在如下 3 个 Shard,Range = hash(Space + InodeID):


Shard ID

Range

1

[0, 1000)

2

[1000, 5000)

3

[5000, max)

表 5,Hash Range Shard 分布


元数据路由分布如下:


用户索引 Key 同【方案一】分区索引 Shard Key = hash(Space + InodeID)


表 6,Hash Range 元数据路由分布


  • Hash 分区模式,同目录下的元数据和 dentry 项均在同一个分区,实现数据的局部放置。

  • Hash 分区模式支持预分区,有更好的数据均衡性。


优缺点


优点:


  1. 大部分优点与优化【方案一】相似,但实现和管理更加简单。

  2. 同目录的元数据都在同一分区。

  3. 数据分区分裂更加简单;Range 由 Hash 组成,分裂时按照中位数一分为二即可。


缺点:


  1. 单一 Hash 分区导致大目录 dentry 项只会存在于一个分区上,无法进行分裂。

  2. 海量 Space 场景下,单个分区会承担集群种几乎所有 Space 的元数据,导致每个分区都有海量 Space 数据上报问题。


定制 Hash 分区


针对【缺点・ 2】,可为某类 Space 指定 Hash 分区,将该 Space 打散到固定一些分区上,进而优化分区数据上报过多的问题。


Space

Hash Key

Shard

space_test*

1

[1, 2) 对应

space_default*

hash(space_default*/*) % 999 + 1

[2, 1000)

表 7,定制 Hash Range 元数据路由分布


05

方案三:组合 Hash 分区


组合 Hash 分区 Key 路由方式,同集群下 Space 间同样共享 Shard 分区。引入的二级 Hash 使超大目录数据可分裂成多个分区。


分布实例


假设集群中存在如下 3 个 Shard,Range = {hash(space + InodeID), hash(name)}:


Shard ID

Range

1

{[0, 1000), [0, 1000)} 

2

{[0, 1000), [1000, max)} 

3

{[1000, max), [0, max)} 

表 8,组合 Hash Range Shard 分布


元数据路由分布如下:

用户索引 Key 同【方案一】分区索引 Shard Key1 = hash(Space + InodeID) 同【方案二】        Shard Key2 = hash(dentry's name)


表 9,组合 Hash Range 元数据路由分布


优缺点


优点:


  1. 较之【方案二】,对超大目录可分裂为多个相邻的分区,解除大目录存储限制。


缺点:


  1. 当触发分区分裂时需要判断分裂点,即按照 Key1 还是 Key2 分裂。

  2. 二级索引不支持全局有序,某些条件查询需要查询二级索引所在的所有节点分区。


06

总结


工程化的文件系统依然有不断迭代优化空间,本文针对 CubeFS 现状,在普通业务、大规模 Space 场景下对元数据路由的探讨。【方案三】组合 Hash 分区,通过预分区和定制 Hash Key 方式提供数据均衡与局部性放置;可分裂的二级索引保证超大目录的平铺使用场景。在某些特殊使用场景该方案依然不够完善,欢迎阅者分享并一起探讨。


参考文献


1、https://en.wikipedia.org/wiki/File_system

2、CubeFS 大视野 | 分布式文件系统的历史演进

3、CubeFS存储技术揭秘-元数据管理


作者介绍


Jie Shen,CubeFS Maintainer,当前在 CubeFS 项目参与 Blobstore 的设计与研发。




CubeFS 简介




CubeFS 于2019年开源并在 SIGMOD 发表工业界论文,目前是云原生计算基金会 (CNCF) 托管的孵化阶段开源项目。作为新一代云原生分布式存储平台,兼容 S3、POSIX、HDFS 等协议,支持多副本和纠删码引擎,提供多租户,多 AZ 部署、跨区域复制等特性;适用于大数据、AI、容器平台、数据库及中间件存算分离,数据共享、数据保护等广泛场景。


►►►

往期推荐



文章转载自CubeFS点击这里阅读原文了解更多


CNCF概况(幻灯片)

扫描二维码联系我们!




CNCF (Cloud Native Computing Foundation)成立于2015年12月,隶属于Linux  Foundation,是非营利性组织。 

CNCF云原生计算基金会)致力于培育和维护一个厂商中立的开源生态系统,来推广云原生技术。我们通过将最前沿的模式民主化,让这些创新为大众所用。请关注CNCF微信公众号。

CNCF
云原生计算基金会(CNCF)致力于培育和维护一个厂商中立的开源生态系统,来推广云原生技术。我们通过将最前沿的模式民主化,让这些创新为大众所用。
 最新文章