本文基于CFS论文和github上源代码的理解,不当之处,还请指正交流。

ChubaoFS(CFS)是京东开发的分布式文件系统和对象存储系统。其主要声称是云原生的分布式文件系统,主要用于k8s容器环境。

CFS是一个分布式的文件系统,支持多元数据服务器,支持posix接口(目前一些接口不支持),支持数据的append写和overwrite写模式,支持大文件和小文件的高性能读写。

由于研发的初衷主要用于docker容器环境,其主要需求如下:1)容器的支持化存储, 当容器销毁数据也可以存在。2)不同的容器可以并发访问同一个文件 3)存储资源不同的服务和应用程序共享。也就是需要一个可持久化,高并发访问,可共享访问的存储系统。

对于容器环境而言,最好的持久化存储系统当然是分布式文件系统。当前开源的分布式文件系统确实没有一个好的可用的存储系统。CephFS由于多元数据服务器的不稳定,生产环境用的不多。GlusterFS自身的数据一致性的问题,并且不适合大量小文件的的存储。MooseFs和HDFS都是基于GFS实现的分布式文件系统,其元数据无法扩展,对小文件的直接支持也不是很好,并且overwrite支持有数据不一致的风险问题。SeaweedFS对小文件的支持比较好,但是不支持posix接口,对大文件的支持也不好。

CFS分析了现有的开源的分布式文件系统的缺点和优点,设计和开发了自己的特点分布式文件系统。其主要的特色如下:

通用的高性能存储引擎

即可支持大文件,也可以较好的支持小的文件存储。使用了本地文件系统的punch hole接口完成小文件的删除后的空间回收操作。(类似haystack中,用xfs的punch hole 操作代替compact操作过程)。

不同场景的复制协议

对应append操作和overwrite操作两种场景 ,使用了两种复制协议:append操作使用master-slave复制协议,overwrite操作使用了raft复制协议。

CFS的架构概览

基本的概念

Meta partition和 Data Partition:都对应磁盘的数据块。Meta partition 用于存储一部分元数据(包括inode和dentry),Data Partition 保存文件对应的数据块。

Volume: 类似于一个独立的文件系统的实例。一个Volume里包含若干个meta partition和data partition。这些meta partition和 data partition都只属于该volume。

252b8b5382acfee98c29691f01d058af.png

如图所示,volume的相关信息包括:容量,Meta partition的数量,Data partition的数量,以及相关的副本数等其他信息。

CFS 的三个组件

Resource Manager

Resource manager负责volume卷的管理(创建,删除),meta partition和data partition的管理(创建和删除),node的管理(添加和删除)。跟踪每个节点的磁盘和内存利用率,并检查meta和data node的存活状态。

Resource Manager也是一个集群,通过raft协议实现数据强一致性,底层用rocksdb持久化保存配置等相关信息。

Metadata SubSystem

负责文件系统的元数据的管理,主要是inode和dentry的管理。Meta Node也是集群,元数据保存在meta partition中,在meta partition层级构成一个raft group完成数据的强一致性。

Data Server

数据服务器,负责数据的读写。目前一个节点对应一个cfs-server服务进程。是否可以该成和ceph的osd类似,一个服务进程对应一个磁盘,而不是一个节点。当单个服务进程crash后,不影响其他磁盘继续服务。

Client 客户端

文件系统的客户端,目前实现了用户态的fuse接口。

元数据服务MetaNode

元数据存储的基本单位是 meta partition,一个partition的内部通过2个Btree分别保存 inode tree和 dentry tree。Inode tree是用ino作为key,dentry是用parent ino和 name作为key。

514811bf53807158c2bf1914e3aa113d.png

在内存中inode和dentry分别保存在2棵B tree上。其定期snapshot持久化到磁盘上对应一个partition。在Btree里,Dentry的存储的key是(parentIno, name), inode的存储的key值是ino

对于inode,其根据ino在meta partition中分片,对于dentry其保存在父目录的inode所在的分片。也就是inode和所有子dentry项保存在同一个分片上,对于大型目录,是否可以承受?

假设1个目录有1千万个文件,dentry项的结构如下:如果一个name的平均长度是20个字节,8+20+8+4=40个字节,占用大约400M的内存,占用空间并不大。

type Dentry struct {
   ParentId uint64 // FileID value of the parent inode.
   Name     string // Name of the current dentry.
   Inode    uint64 // FileID value of the current inode.
   Type     uint32
}

元数据的复制也是通过multi-raft协议来完成复制的。通过定期的snapshot持久化inode和dentry到磁盘上。通过snapshot和journal的组合来恢复元数据。

create / mkdir

cfs创建文件分2个步骤:1)创建对应的inode 2)添加dentry项到父目录中。这没有太大的问题。2步操作在raft里都是原子的操作。如果第2步操作失败了,会产生孤儿的inode (orphan inode),不影响文件系统系统正常的操作,对客户无感知。该orphan inode 会占用内存和磁盘空间而已,后期通过fsck工具可以检查后清除。

这里可以优化的点是:如果dentry项和inode在相同的meta partition,是否可以一次打包发给该meta partition一次完成,而不用发送两次请求。在同一个raft group里可以完成inode和dentry的原子修改。

Unlink 和 rmdir

删除操作unlink和 rmdir操作正好相反,第一步先在父目录中删除dentry项,第二步删除inode,如果第二步出错,也同样的产生孤儿inode,和创建过程处理类似。

Hard link

对于hard link操作。Hand link 分2个步骤操作:1)执行inode的nlink++操作 2)执行添加dentry项的操作。如果第二步出错,第一步inode的nlink值已经增加,导致该inode最终也为孤儿inode。也需要依赖后续的fsck工具检查删除。

Rename

对于rename操作分4个步骤:1)执行inode的 nlink++操作 2)添加dest的dentry项 3)删除src的dentry项 4)执行inode的nlink--操作。rename分4个步骤,如果第二步出错,就导致和link操作失败同样的效果。第三步出错,导致的结果是:建立了一个dest的hard link文件。用户的角度看就是新的文件名修改成功,但是旧的文件名没有删除,并且旧的文件名和新的文件名都指向相同的inode。这时候,如果用旧的文件名创建一个文件就会出错。

对于rename的处理会产生问题,对应用系统影响比较大:首先rename操作使用还是比较频繁的;其次rename操作会导致创建失败这样严重的问题。

主要的难点在于 src的dentry项和dest的dentry项可能在不同meta partition中,导致要处理跨两个raft group的原子操作。rename还导致dentry项和inode分布在不同的meta partition中。是否可以建立一个单独的服务 rename server,对应所有的rename操作,客户端都先把请求发送给 rename server,rename server有基于master-slave的日志系统。rename server 先把所有相关的操作写入日志持久化,然后由rename server再向 src dentry 和 dest dentry 发送相关修改请求。本质还是基于日志完跨raft group的原子操作。

数据服务DataNode

每个DataNode 里包含一些 data partition。一个data partition包括partition的元数据和Extent Store存储。

b817284842f0493214ee3cc4a332cc25.png

如图所示,有6个partition,在partition6目录下有以ExtentID命名的extent文件。

Extent是一个大小默认最大为128M大小的数据块。每个data partition里包含许多的Extent数据块,对应本地文件系统xfs上的一个文件。

Extent有两种类型:normal extent和tiny extent两种类型。Normal extent针对大文件分配,一个normal extent只分配给一个文件,而一个tiny extent 被多个小文件共享,保存多个小文件的数据。

在文件的inode里,保存了该文件的layout信息。如下图所示:在Inode结构体的Extents中,ExtentsTree是一个B+树。

91a3d755f0729c44e05b3a83e08f6e04.png

B+树中内容 如下图所示:其保存的信息:文件起始FileOffset到Size的数据保存在PartionId对应的partion上,具体在ExtentId对应的Exent的ExentOffset开始的位置。

651adb6a876cd99405cba19f34376466.png

可以看到,CFS的inode里,不但保存了常规的文件属性等信息,还保存了文件的数据分布的信息(layout info),一个文件的数据是在所有该volume的data partition上的所有extent中全局分布。

CFS的数据的写操作分2种情况:overwrite和 new write(包括append write和 write hole,可以概括为所有的新写操作)。

所有文件的new write操作使用的是master-slave复制协议:客户端把操作请求直接发送给master data partition节点,master data partition节点并行发送给slave节点完成写操作。写操作返回给客户端后,客户端负责向meta node更新该文件的inode中的layout信息,至此,写操作才算成功完成。

New write的写操作在master和slave上数据只写一次,直接写入extent文件中,不需要日志操作,因为所有新写都是append操作追加到Extent尾部。如果全局断电,Leader可以和follower根据extent的size最大者为基准来修复来完成数据的一致性。如果inode的layout信息没有及时更新到meta node上,可能会有数据丢失,如果用户想要确保数据不丢,就需要调用fsync操作确保元数据刷新到meta node上,但不会导致数据的不一致性。

对于overwrite操作,通过raft协议来实现复制。Raft协议是基于日志的复制协议,所有的操作都先顺序写入日志,后续apply到raft的状态机,也就是具体的文件数据中。

如何判断是new write操作还是over write操作呢?代码实现比较简单,就是检查该段数据在inode 的layout中有没有对应的ExtentKey记录。

ba7e68f61247dbda6680adac9c1611da.png

对于overwrite,如果大块覆盖写,由于raft的日志,就会导致写带宽减半这样的沉重代价。

其实对应大块的overwrite操作,不一定要执行in-place的写模式。可以write new place,然后更新ExtentKey记录。其类似于new write操作。后续在后台删除旧的数据就可以了。

另外,CFS所有的元数据都保证在内存中,定期做snapshot持久化到磁盘上。Inode里保存了layout信息的方法,明显增加了inode的size的大小,增加了缓存到内存中的空间开销。这一点和设计有点不符:如果想要把元数据都缓存在内存中,应该让元数据的size尽量小。

不过在大多数场景也可以接受:对于小文件,layout信息比较少;对于大文件,即使inode的layout的信息可能比较多,但是总体的文件数量一般不多。

数据的删除

删除一个文件,客户端发请求给Meta Node,Meta Node检查该inode,并标记为删除状态。会产生后台任务,meta node 会给 data node 发送删除请求,datanode会删除相关的Extent信息。

对于normal extent,直接删除文件即可。对应tiny extent数据直接调用本地文件系统的punch hole操作删除空间。这是CFS的利用本地文件系统的punch hole机制实现的一个小技巧。

Data Recovery

当数据恢复时,针对2种不同复制协议,实现不同的恢复策略。对于append write,需要扫描所有的extent来完成修复。

目前默认的一个partition的大小是120G,每个Extent默认最大128M,也就差不多1000个extent。

如果整个集群1PB的数据,1PB/128M =8338608 大约8百万个Extent都要扫描。这个工作量有点大。如果Extent的大小设置为1G, 1PB/1G=1M, 至少也需要1百万个Extent需要扫描。

假设1个节点是4T的数据盘,每个节点12盘位的,一个节点承载48T的数据。总共需要1PB/48T*3副本=64个节点。每个节点需要扫描1百万/64=16384, 也就是每个节点需要扫描16384个extent的文件的元数据。这个数量级似乎还可以接受。

对于overwrite写操作,由raft协议完成修复。这里就不多介绍了。

Resource Manager

CFS采用了基于利用率的分布策略。

基于利用率的分布策略放置文件元数据和数据是Master最主要的特征,该分布策略能够更高效的利用集群资源。数据分片和元数据分片的分布策略工作流程:1. 创建卷的时候,master根据剩余磁盘/内存空间加权计算,选择权重最高的数据节点创建数据/元数据分片,写文件时,客户端随机选择数据分片和元数据分片。2. 如果卷的大部分数据分片是只读,只有很少的数据分片是可读写的时候,master会自动创建新的数据分片来分散写请求。

基于利用率的分布策略能带来两点额外的好处:1. 当新节点加入时,不需要重新做数据均衡,避免了因为数据迁移带来的开销。2. 因为使用统一的分布策略, 显著降低了产生热点数据的可能性。

这种做法确实比较简单。但是系统负载压力大或者空间不足时,添加新的节点后,大量的写请求(元数据创建请求,数据的写请求)会压到新的节点上去,导致写请求负载不均不能发挥集群整体的性能。旧的节点依然承担旧数据的读取压力。

如果一个存储系统,写请求少,而读取请求比较多时,这种设计也说的过去。新增节点只承担少量的写请求,大量的读请求还是旧的节点承载。

如果实现数据迁移,在短期内新节点会有迁移数据的压力,这可以通过选择集群迁移的时间:比如压力较小进行而缓解。迁移完成后,整个集群负载相对均衡,能发挥整个集群的性能优势。

应用场景和案例

目前ChubaoFS主要用于一下领域:

  • 机器学习
  • ElasticSearch
  • Nginx日志:在docker 环境下,所有的docker的日志目录映射在CFS上。
  • Spark大数据
  • Mysql数据库备份

可以看到CFS目前的应用场景还是大数据的场景。主要是大文件的顺序读写场景。

结语

CFS实现了支持posix语义的分布式的文件系统,其主要优点如下:

  • 分布式的元数据服务:相对于HDFS,MooseFS在大数据方面的优势。
  • 支持overwrite的强一致性的数据复制。

相比较于HDFS,数据只支持append操作,CFS支持overwrite操作。相比较于MooseFS和GlusterFS,BeeGFS实现了强一致性的操作,并且在全局断电重启后,可以实现数据的一致性。

可以说:CFS修复了目前开源的分布式文件系统的2个缺陷:分布式元数据和数据的强一致性的支持。

目前CFS主要的缺陷:

  • Posix支持不全,rename还有问题,常用的文件锁需要支持。
  • 目前CFS的元数据都缓存在内存中,而CFS的inode由于包含了大量layout信息。元数据比较大,会占大量的内存。这也可以通过增加元数据服务器的内存和节点数量来缓解。
  • 没有数据迁移。新加节点会产生负责均衡。这对于大多数文件系统写少读多的情况也没太大问题。
  • 稳定性:jd已经在内部有许多生成环境的案例,应该问题不大。
  • 性能问题:论文中性能和cephfs比可能会好一些。但cephfs本身性能就不好。这还需要后续的持续优化。

目前CFS还是比较适合大文件的顺序读写场景,但是可以支持少量overwrite操作的场景。在小文件的处理上,数据方面虽然把小文件都写入相同的tiny extent共享,但元数据的处理没有太大优化。

转载自知乎:https://zhuanlan.zhihu.com/p/140279730

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐