《机器学习平台统一化分布式存储Ceph的进阶优化》一文提及,网易云音乐常将 CephFS 用于AI训练的共享存储,通过 Kubernetes 调用创建 PV/PVC 对接 CephFS 存储。目前使用的 Ceph 版本是 Luminous。
为满足业务方防误删的诉求,网易数帆存储团队为 CephFS 开发了一个类似回收站的防误删兜底功能。
假定有一个深度为2的目录
➜ ~ tree dir
dir
├── dir1
│ ├── file1
│ └── file2
└── dir2
├── file3
└── file4
2 directories, 4 files
如果执行 rm -rf dir 来删除整个文件夹,实际的调用会是
unlinkat(5, "file2", 0) = 0
unlinkat(5, "file1", 0) = 0
unlinkat(4, "dir1", AT_REMOVEDIR) = 0
unlinkat(5, "file4", 0) = 0
unlinkat(5, "file3", 0) = 0
unlinkat(4, "dir2", AT_REMOVEDIR) = 0
unlinkat(AT_FDCWD, "dir", AT_REMOVEDIR) = 0
可以看到也就是一棵目录树每一枝从叶子节点开始向上逐级删除,直到目录树的根。
客户端会发送这些请求到 CephFS 后端的 MDS,MDS 则处理这一些请求。
不像一些本地文件系统,只需要释放元数据就可以继续在原位置继续覆盖写入新数据。CephFS 作为一个分布式存储系统,需要将数据和元数据都删除,才能保证不浪费空间。然而当有大量删除的时候,数据的删除会是一个非常耗时的操作,所以实际上 Ceph 实现了延迟删除,先删除元数据,数据并没有被马上删除,而是被移动到了一个名叫 stray 的特殊目录,后续再后台一点点删除。
这样就完成了一个完整的删除流程。
在这里,我们有一个出发点,希望改动对用户尽量是无感的,不需要用户去修改自己的业务使用姿势的。
在对整个流程有一个基本概念后,就可以考虑尝试一些方案了。
第一个方案的想法是对客户端进行改造。
将客户端收到的 unlink 系统调用,给转化成为 rename 请求,转移到一个我们指定的目录。
这里可能会有2个问题:
一个是 CephFS 支持 ceph-fuse 和 kernel 两种客户端,好巧不巧的是两者我们都有在使用,同时维护这两套恐怕是一个不太小的工作。
另一个是 Kubernetes 在使用 CephFS 的时候,往往是会去 auth 里创建一个对应的 user,并且限制它能够访问的目录。如果是做 PV/PVC 内部级别的回收站可能会比较容易,但是当完全删除一个 PV/PVC 时想要给误删兜底可能会比较麻烦。
第二个方案的想法是改造原有的 stray 目录。
某种意义上,stray 就是相当于一个残疾的回收站,可以尝试进行改造。
但是问题也非常显而易见,原有的流程中,删除先完成了元数据的更新,然后stray只是为了辅助后续数据异步删除的存在。
如果试图把 stray 改成正经的回收站,需要把元数据保留以便后续的恢复,并且把 stray 目录暴露出去。stray 目录外的文件进 stray 的过程变成了一个元数据修改的过程(原流程是删除),然后再删除 stray 内的文件就变成了需要删除元数据和数据的过程,仍然需要一个类似原流程中 stray 的异步删除数据的机制来辅助。
如果要改,代码侵入太大,也没什么优势,方案不具备很好的可行性。
第三个方案的想法是改造 MDS 端请求的接收。这个也是最后选择的方案。
客户端发送的仍然是 unlink 和 rmdir 请求,然后 MDS 收到请求后,将其转成特殊的 rename 请求进行处理,其实想法和方案一类似,只是在 MDS 进行操作。
通过在 MDS 处理来规避方案1可能会遇到的问题,建立起一个 CephFS 全局的回收站。流程与改动相比于方案2会更简洁和独立。
不同于自上向下的删除,删除一个节点,直接移动目录树的一枝即可;整个删除的过程是自下而上的,这其实增加了很多困难。
首先是删除的时候我们需要做什么,如果想要删除的时候就重现原有的目录树结构,必然需要引入额外的类似 mkdir 的操作,而且数量似乎不太可控,目录层级深得话,一个 op 变成了 N 个 op,从语义上也变得很奇怪,代码实现上也不容易。
命名规则上inode+文件名是可以唯一标识一个删除的inode的。
这里我们选择了文件平铺在回收站里的方案,没有直接去重现原有的目录树结构。文件名采取的是父inode+本身inode+文件名的规则。
1个unlink/rmdir op 只对应1个 rename 的 op,更容易实现和理解
命名加了父inode,也就是保留了层级信息,后续还是可以恢复成一棵树的
无论是直接在删除的时候构建树还是删除后再去恢复树,本质上所需要的 rename(mv)操作都是差不多的。直接删除的时候构建树的策略不太能够以原有的文件名来进行保存,会存在很多重复名称的创建和删除,应该会考虑 inode+文件名的方式保存。那么恢复的时候就需要 rename 到原有的名称,然后再移动整个树枝,整个流程甚至还多了一堆删除时的 mkdir 等其他操作。后恢复树的策略则是多了读取父子关系并计算生成树的耗时。考虑到这只是一个低概率的防误删兜底方案,我们选择了后者。
多 MDS 场景下,会出现多个目录层级分属于不同的 mds auth,会导致一些问题,后续会提到。
实际的结果类似
➜ ~ tree dir
dir
├── file1
└── file2
0 directories, 2 files
变为
# ls -l .trash0
total 1
-rw-r--r-- 1 root root 0 Oct 25 11:37 1099511650201-1099511650202-file1
-rw-r--r-- 1 root root 0 Oct 25 11:37 1099511650201-1099511650203-file2
drwxr-xr-x 2 root root 0 Oct 25 11:37 1-1099511650201-dir
线上启用了多 MDS,测试的时候也碰到了一些问题,进行了一些方案的调整。
我们做的事情是将 unlink/rmdir 转成 rename。比方说删除 A,实际就是 mv A .trash。
其中,CephFS 中, rename 请求是由 dest 的 auth mds 处理的,也就是 .trash 的 auth mds,这对 unlink 转 rename 来说是没有问题的。
但是对于 rmdir 转 rename 来说,rmdir 本身需要检查目录是否为空,这个是由 A 的 auth mds 来进行的,和 rename 的 auth 是不一致的。
流程就变成了:
client 发送 rmdir 至 mds.0 处理
mds.0 完成是否为空检查,试图去执行 rename,发现不是回收站的 auth mds,提示 client 重发请求到 mds.1
client 重发 rmdir 至 mds.1 处理
mds.1 发现不是需要删除目录的 auth mds,无法进行是否为空判断,提示 client 重发请求到 mds.0
陷入死循环
于是,我们就将一个回收站进行了拆分,每个 rank 的 MDS 拥有一个自己的回收站,保证 unlink/rmdir 和回收站的 auth mds 是一致的。
回收站的清理与恢复都是比较直观的。
因为回收站可以当作一个普通的正常目录来对待,清理可以按照自己的需求,根据时间、容量情况来制定规则。当回收站有大量文件的时候,浏览起来可能不是一个快的事情。
回收站的恢复其实就是将回收站里的文件 mv 回我们想要恢复的地方。这里主要是有2个关心的点:
在恢复目录树的时候,如果有同名文件文件夹的重复创建和删除、或者是有一堆临时文件(比如说 vim 产生的 swap)时,会面临选择问题,我们需要挑选想要的内容进行恢复。如果原有的目录层级比较深,这样的情况会经常发生。
如果有大量的文件需要恢复,会有大量的 mv,恢复时间会非常久。这是自下而上删除所带来的问题,在上面 layout 里也有说明。
本文介绍网易数帆通过 unlink 转 rename 的方式实现了一个 CephFS 的回收站,但 CephFS 的回收站更多的是作为一种兜底方案,而不应作为一个被过分依赖的东西。自下而上的删除,在耗时上有天然的劣势。优化性能的方向一个是优化 CephFS MDS 本身元数据的处理性能,一个是提供额外的删除接口来引导业务方适配完成整个树枝的直接删除。
最后再打一点点广告:
网易数帆存储团队目前正在开发基于 Curve 的开源分布式文件存储系统 CurveFS,CurveFS 的研发目标是替换 CephFS(重点聚焦在性能、易用性、云原生等方面),支持将数据存储在 S3 对象存储和 Curve 集群本身,并支持在二者之间互相迁移,以在性能和成本之间进行平衡。官网:http://www.opencurve.io/,GitHub:https://github.com/opencurve/curve(文件系统相关代码在fs分支),用户群:添加微信号 opencurve 为好友,注明加 Curve 用户群。