导读
这是一个技术创新的故事。
在事实业务的压力和技术现实的感召下,带着含糊的地图,百度桑田·存储 CFS 和 TafDB 两个技术团队启程进入无人区,寻找解开「千亿文件的状况下,文件存储系统仍然放弃高性能」难题的钥匙。
新架构小试牛刀后带来的惊喜还未继续多久,便被横贯在背后的平地给阻挡,退回到终点还是持续向前行……
如本文作者所言,对论文背地的故事进行讲述,是为了可能帮忙读者更好地了解这个翻新后果自身,亦能为正在处于翻新过程的读者提供参考,愿大家早日找到那把钥匙。
全文 18851 字,预计浏览工夫 22 分钟。
一、引言
本文的次要目标是解读百度桑田·存储团队发表于 EuroSys 2023 的论文《CFS: Scaling Metadata Service for Distributed File System via Pruned Scope of Critical Sections》,论文全文能够在 https://dl.acm.org/doi/10.1145/3552326.3587443 下载。
论文披露了百度智能云文件存储 CFS 的元数据系统的外围设计,对⻓期困扰文件系统元数据畛域的 POSIX 兼容性和高扩展性(特地是写扩展性)难以兼顾的问题进行了解答。这是一个大规模分布式文件系统是否扩大到百亿甚至千亿级别文件数,同时放弃高性能稳定性的一个关键问题。
作为一种高度凝练的体裁,论文除了展现必要的剖析过程,并不会告知读者这些翻新是如何被想到的。 咱们认为讲清楚这次翻新的整个过程,既有助于了解论文自身,也是对论文内容的重要补充 。基于这个思考,咱们将整篇文章依照以下构造组织内容:
- 首先,咱们概括介绍了文件系统元数据问题产生的背景和业界对该问题的摸索,并特地对本文着重解决的写扩展性问题进行了定量分析;
- 而后,咱们通过介绍 CFS 元数据架构的演进历史,向读者展现了咱们摸索的过程以及对元数据问题实质的思考,最终引出论文里介绍的零碎架构;
- 最初,咱们具体介绍了论文架构的整体设计和其中要害的细节。
二、背景
2.1 文件系统的概念
文件系统的定义是一种采纳树形构造存储和组织计算机数据的办法,这个树形构造通常被称为层级命名空间(Hierarchical Namespace)。如下图所示,这种命名空间的特点是整个构造像一棵倒挂的树,非叶子结点只能是目录。如果不思考软链接追随(follow symlinks)、硬链接(hardlink)的状况,从根节点(/)登程,每一个目录项(目录、文件、软链接等非法类型)都能够由一条惟一的门路达到。
文件系统能够分为元数据(Metadata)和数据(Data)两个局部。数据局部指的是一个文件具体存储了哪些内容,元数据局部是指层级命名空间树形构造自身。例如,如果咱们要读取 /a/b/file 这个文件的数据,元数据局部负责找到 /a/b/file 这个文件存储在哪儿,数据局部则负责把文件的内容读出来。
文件系统次要有两种 POSIX 和 HDFS 两种实现⻛格:
- POSIX:全称 Portable Operating System Interface,是 IEEE 制订的 UNIX 可移植操作系统兼容规范。该规范定义了一个文件系统相干的接口子集,是文件系统畛域最根底和权威的规范。POSIX 兼容文件系统就是指兼容该规范的文件系统;
- HDFS:源自 Hadoop 大数据生态,对 POSIX 规范做了一些比拟实用的简化和批改,次要是放弃了对 hardlink、随机写的反对,并减少了一些实用的递归操作。通常也将其归类为 POSIX-like 文件系统,意思是和 POSIX 类似。
POSIX 和 HDFS ⻛格的文件系统不存在显著的分界线,在实现技术上是互通的,理论应用时通过简略的接口转换也能够互现替换,当然这种转换会以就义肯定的兼容性为前提。论文形容的零碎是 POSIX ⻛格的,但研究成果同样实用于 HDFS ⻛格的零碎,甚至文章里用于比照的 HopsFS 和 InifiniFS 均是 HDFS ⻛格的零碎。
2.2 文件系统元数据问题的形象
在摸索元数据问题的过程中,咱们逐渐建设了文件系统元数据问题的形象,在此处先进行介绍,以不便开展后续的内容。 一个文件系统的元数据系统只有正确实现了这个形象,就解决了 POSIX 兼容性问题的外围局部。
这个形象是说,一个规范的 POSIX 文件系统层级命名空间要求实现以下语义:
- 写操作的要求
- 关联变更:所有操作均须要实现关联变更(编号 1),即增删目录下的子项的同时更新父目录必要的属性信息。关联变更须要原子实现,即 all-or-nothing,要么全副都发生变化,要么全副都不发生变化;
- rename: rename 的作用是对目录项进行重命名,这是整个文件系统里最简单的操作,除了满足关联变更的要求外,还有很多其它简单的要求,请自行参阅 POSIX 规范的定义。
- 读操作的要求
- 点读:包含门路查找(lookup,编号 2)和 inode 获取属性(getattr,编号 3)两种:
- 门路查找:用于一级一级找到对应目录项,单个 lookup 门路查找操作的输出是父目录 inode + 子项的名称,返回值是目录项的 inode。inode 是一个文件系统中惟一标记一个目录项的编号,通常是一个 64 位整数;
- 获取属性:获取指定 inode 的属性信息,属性信息包含 size、nlinks、ctime、mtime 等。
- 范畴读:目录遍历(readdir,编号 4),获取一个目录下所有子项的列表,通过一系列的 readdir 调用,每次返回一部分子项,直到获取到齐全部子项。
对于上述的形象,咱们进一步做如下补充阐明:
- 关联变更的实质是精确反映子项列表的变动
- 一方面,关系到元数据的一致性。例如,删除目录的时候须要判断目录是否非空,这在很多零碎里会保护一个字段来记录,如果这个字段的信息滞后了,就有误判的可能性。当一个非空目录被误判为空目录,目录自身会被删除,目录下的子项残留了下来。这些残留的子项是所谓的孤儿节点,无奈通过门路查找拜访到;
- 另外一方面,关系到缓存的正确性和效率。为了优化文件系统元数据的性能,客户端通常会缓存 lookup 和 readdir 操作的后果,而后再次拜访的时候能够通过获取父目录的属性信息,疾速判断父目录是否被批改,从而确认缓存是否无效。如果没有这个机制,操作要么无条件从新执行保证数据正确,要么容忍可能存在的信息滞后。这个缓存机制对大目录的 readdir 成果尤其重要,能够节俭反复执行大目录的 readdir 带来的大量 I/O 开销。
- 读操作隐含的两种索引需要
- < 父目录 inode, 子项名称 > 索引:这个索引须要反对点读和范畴读,别离对应 lookup 和 readdir。须要特地阐明的是,POSIX 对 readdir 并没有规定须要依照字母序返回后果,只须要实现不反复、不脱漏返回即可,这种状况下范畴读中的“子项名称”参数能够是某种零碎外部的游标(marker);
- <inode> 索引:这个索引只须要反对点读,对应 getattr 申请获取属性信息。
2.3 分布式文件系统的元数据服务
二十多年前,随着 GFS、HDFS 等分布式文件系统的衰亡,元数据和数据拆散的架构逐步成为分布式文件系统畛域的支流共识。 这种架构将整个零碎分成负责元数据的元数据服务(Metadata Service)和负责数据的数据服务(Data Service)两个局部。
对于数据服务,因为不同文件的数据互相独立,同一文件的不同局部也很容易条带化和分块解决,人造具备并行处理的条件,因而较容易扩大到很大的规模。但对于元数据服务,层级命名空间的目录构造带来了很强的父子依赖关系,并行处理并不是件容易的事件。
咱们通常通过以下指标来综合掂量一个元数据服务实现的优劣 :
- 扩展性:掂量一个实现能够扩大到多大的规模,实际上能够细分为两个指标:
- 规模扩展性:指的是零碎能够存储多少目录项,数亿、百亿,还是千亿。因为文件占比是最高的,通常也将这个指标称为零碎反对的文件数;
- 性能扩展性:通过减少节点,零碎的元数据读写 OPS 可能别离扩大到什么量级,以及读写 OPS 和节点规模是否出现线性关系。
- 延时:掂量单个申请须要破费多⻓工夫能力实现解决;
- 均衡性:掂量零碎是否有能力疏散热点,让不同节点的解决压力大抵平衡。
针对扩展性和均衡性,须要特地指出的是,只管很多实现认为本人具备十分好的扩展性,然而依据木桶原理,扩展性的短板是由体现最差的那个节点决定的。因而,如果一个零碎的均衡性没做好,扩展性的理论体现不会特地好。
元数据服务的架构倒退在业界经验了三个阶段:
阶段一:单点元数据架构
晚期分布式文件系统存储的单个文件都比拟大,文件数量不会超过数亿,单点的元数据服务齐全能够满足需要。GFS、HDFS 等零碎是这个阶段的代表。
这个单点只是逻辑上的,物理上元数据服务依然会有多个备节点,在故障时候主动切换以保障服务连续性。这个阶段的零碎很显著没有扩展性和均衡性可言,然而延时性能是比拟好的。
阶段二:(耦合式)分布式元数据架构
随着 AI 等新型负载的风行,古代文件系统里存储的文件越来越小,文件数量数却越来越多。单个文件系统的文件大小可能只有数十 KB,文件数却高达数十亿。单点架构的极限是存储和解决数亿文件,曾经不能适应时代的需要,因而,可扩大的多节点分布式元数据服务成为必选项。HDFS Federation、Lustre DNE1/DNE2、CephFS、BeeGFS 等零碎都是这一阶段倒退起来的。
这个阶段的分布式元数据服务是在单点架构上的横向扩大,每个节点同时负责数据存储和文件系统语义的解决。为了和起初的分离式架构做辨别,本文将这种架构称为耦合式架构。
耦合式架构零碎广泛采纳子树分片或 Hash 分片的形式来将整个层级命名空间散布到不同的节点上,可能大抵平均地在数据量上打散元数据,然而元数据在创立时就确定了其物理地位,不能动静负载平衡,无奈很好的解决数据热点问题。
只管有很多钻研提出了各种负载动静平衡的计划,某些开源零碎代码上也反对该能力,但并没有理论落地生产的案例。 归根到底,是因为元数据架构同时耦合了数据存储和解决逻辑,动静迁徙的过程既要实现数据的搬迁,又要保障处理过程不中断或者中断工夫极短(数秒钟),工程实现的难度极大。
当一个操作须要多个节点参加时,如何保障元数据的一致性是一个比拟难的问题,实现上要么须要引入简单容易出错的多节点参加的分布式锁机制(Lustre DNE2、BeeGFS、CephFS),要么放松对 POSIX 或 HDFS 语义的残缺反对(HDFS Federation)。前者在抵触时开销较大使得零碎的写扩展性存在瓶颈(后文会有定量分析),后者须要业务革新以感知底层的数据分布。
引入分布式锁的目标是为了正确实现关联变更。关联变更里最外围的一个要求就是原子性。当须要变更的数据扩散在多个节点时,分布式锁能够保障变更同时失效或者同时不失效。 当然,实现一个能正确容忍各种异样的分布式锁机制也是一个有肯定难度的技术活儿,因为和本文关系不大不再开展。
综合来说,这个阶段的零碎肯定水平上解决了规模扩展性的问题,可能存储几十亿文件,延时指标在申请不跨节点的时候是比拟好的,然而写扩展性和均衡性体现不是很好。
阶段三: 分离式元数据架构
计算机领域有一条经典的方法论:
Any problem in computer science can be solved by another layer of indirection.
计算机科学畛域的任何问题都能够通过减少一个间接的中间层来解决。
这个方法论具体的做法是将简单零碎进行分层,每一档次专一于解决一个畛域的问题做到极致,最初叠加不同档次实现残缺的性能。这在计算机领域是屡试不爽的套路。经典的 OSI 7 层网络模型,近年来在大数据和数据库畛域风行的存储计算拆散架构,都是最好的佐证。
2017 年,HopsFS、ADLS 两个工作将相似的想法引入到分布式文件系统元数据畛域,在这之后呈现的重要零碎和钻研工作简直都是基于此思路的。该思路将元数据服务从架构上分成两层:
- 数据库层:这一层负责数据存储,通常采纳 NewSQL 或分布式 KV 零碎(以下统称 Table 零碎),实现数据的长久化的同时提供分布式事务能力;
- 元数据代理层:这一层对外提供 POSIX 或 HDFS 接口,对内将层级命名空间的数据转换成 Table 零碎中的记录,解决时利用事务保障操作的正确性。
分层思路让整个零碎的设计变得十分简洁。原来耦合式架构里同一个节点既负责数据存储还负责文件语义,两者揉杂在一起剪一直理还乱。分层之后,数据存储的局部首先被从元数据节点里剥离了进来,元数据节点只剩下解决逻辑,变得更专一。
Table 零碎提供的 SQL 或者 KV 接口易用性和通用性十分好,和存储交互的逻辑表白起来也很容易。最重要的是,Table 零碎提供了事务。事务是计算机领域最弱小和最有用的能力之一,ACID 配合适合的隔离级别,(简直)所有的简单的一致性问题都能够基于这个能力来简洁地解决,文件系统语义要求的一致性当然也不例外。
除了架构变得更简洁外,分离式架构借助 Table 零碎诸多成熟的能力,还解决了之前架构难以解决的规模扩展性和均衡性的问题:
- 规模扩展性:Table 零碎具备十分好的规模扩展性,能够存储海量的数据,例如,在对象存储服务上,百度智能云的 TafDB 曾经存储了万亿条数据,这个规模还在一直增⻓。通过将文件系统元数据以适合的格局编码存储到这类零碎中,能够满足元数据服务对百亿、千亿文件的存储需要;
- 均衡性:文件系统的元数据编码到 Table 零碎之后,保护层级关系变成了多条记录同时变更时的原子性和一致性问题,这是事务能够保障的。剥离了文件系统语义之后,数据自身并没有非凡之处,Table 零碎能够应用数据分片决裂、合并、平衡机制进行热点的疏散,这些机制十分平庸和成熟,产生的服务中断工夫能够管制在秒级。
分离式架构没有在更早的工夫点呈现是有其历史起因的。
事务⻓期只能在单机上良好运作,直到 2012 年 Spanner 横空出世,才让人们明确了怎么去组合利用 Paxos/Raft 这类分布式共识算法、Percolator 事务机制来构建一个弱小的分布式事务零碎。这之后从 Spanner 衍生的各种开源或闭源的 Table 零碎就雨后春笋般凋敝了起来,肯定规模的技术公司都有能力研发这类零碎。 分离式元数据架构呈现的工夫点,刚好吻合这类零碎初步成熟并在各类场景开始落地的工夫,简直是瓜熟蒂落。
遗憾的是,分离式架构同样没有解决写扩展性的问题,写延时的体现甚至比耦合式架构更差:
- 事务在架构里的本质作用其实也是一种分布式锁,并没有解决其它分布式锁机制的缺点,当一个写操作须要多节点参加时,无论是吞吐还是延时的体现都会比拟差;
- 分离式架构解决申请时,须要先通过元数据代理层,再到数据库层,比耦合式架构的解决门路要更⻓,因而人造在性能上,特地是延时指标上更具劣势。读申请能够通过客户端间接读数据库层的办法来进行优化,但写申请就没有方法这么解决了。
2.4 为什么元数据服务须要分布式锁
前文咱们始终在强调通过分布式锁来保障一致性,这个一致性就是指关联变更的正确性。 在这一章节,为了让大家更好地了解这个问题,咱们再补充阐明一下分布式锁是怎么实现这个保障的。以分离式架构创立文件 create file “/A/f2” 为例,须要通过以下步骤:
- 创立操作发送到元数据代理;
- 读取 A 目录的属性并对 A 加锁,在这一阶段实际上还会去查看目录是否存在、用户是否有权限创立等;
- 插入 f2 文件的记录;
- 更新 A 目录的属性,次要是 mtime/ctime/links/size 等属性。这个阶段变更还没有提交,客户端不可⻅;
- 解锁并提交这次变更,这次提交如果波及到多个元数据分片,须要采纳 2PC 提交(two phase-commit)来保障变更同时失效;
操作 2 – 5 须要在锁的爱护下进行,如果没有锁的爱护,会很容易呈现并发问题。如下图所示,零碎应用 children 字段示意该目录下有多少个无效的子目录或文件,这个字段能够在删除目录的时候疾速判断目录是否为空。当没有锁爱护的时候,并发的操作 create file “/A/f2” 和 create file “/A/f3” 别离读到 A.children=0,各自 +1 并更新,最初失去的 children 字段数据是谬误的 1。
这个谬误会导致后续删除目录操作误判目录为空,使一些文件成为孤儿节点 。例如,在这个例子完结之后,用户先执行 delete file “/A/f2″,再执行 delete directory “/A”,这时看到 children=0 认为目录是空的,就能够把 A 目录删除掉了。此时,f3 文件还存在,但却再也无奈通过 /A/f3 门路拜访到了!
理论零碎的并发状况远比上述的例子要简单得多,但从上述的例子,咱们能够简略理解到为什么分布式文件系统的元数据操作须要分布式锁来爱护。
2.5 对分布式锁性能影响的定量分析
在答复了为什么元数据服务须要分布式锁之后,咱们在本章节进一步展现分布式锁对写性能的影响。这里间接援用论文里对 HopsFS 创立文件操作做的定量分析,剖析后果同样实用于其它零碎。
在 Peak throughput 这个图里,咱们结构了锁抵触比例从 0% 到 100% 的负载,察看零碎的 OPS 和客户端数量的关系。在没有抵触的状况下(抵触比例 0%),OPS 简直随着客户端数量线性增⻓,然而当抵触比例越来越高时,OPS 有显著的降落,当抵触比例达到 100% 时,整个曲线变得很平,阐明零碎的性能曾经齐全丢失了扩展性。
在 Latency breakdown 这张图里,咱们进一步合成了 HopsFS 的执行工夫,能够发现,当抵触比例为 50% 和 100% 的时候,锁抵触在整个操作中的占比高达 83.18% 和 93.86%。
上述的试验表明,锁抵触是影响零碎扩展性和操作延时的要害。
另外,咱们能够看到一个有意思的后果,那就是即便在无抵触的状况下,锁在整个操作里的耗时占比也达到了 52.9%。这是因为零碎并不知道何时会发生冲突,因而须要始终以最坏的状况来预防。如果能升高甚至打消这部分延时,就能够极大的升高零碎的延时。 事实上,无论是耦合式架构,还是分离式架构,很多零碎都会通过一些数据的搁置策略,让一个子树或者一个目录的数据尽可能散布到一个分片上,以不便做锁抵触的优化。
三、CFS 元数据架构的演进历史
论文披露的 CFS 元数据架构在外部的代号是 Namespace 2.0,即第二代架构。第二代架构对第一代架构存在显著的继承关系,正是因为咱们剖析分明了第一代架构的局限性,才使得第二代架构的设计成为可能。
在正式介绍 Namespace 2.0 的架构之前,让咱们先花一些工夫讲一讲 CFS 元数据服务是怎么一步步演进到明天这个架构的,这个对了解整篇论文是有帮忙的。
3.1 Namespace1.0:分离式架构上的微翻新
当 2017 年咱们开始设计 CFS 时,最重要的一个设计指标就是零碎应该具备撑持海量文件的能力,这个海量的量级要远超传统零碎的量级。云上的文件系统是多租户的,租户数量叠加单个租户的规模,就会导致整个集群的文件数量爆炸式增⻓。例如,一个用户 10 亿文件,100 个租户就是 1000 亿文件。即便一开始的数据规模没有那么大,但作为面向未来三五年甚至更久的设计,必须具备肯定的前瞻性,为将来架构上的扩大留足余地。
咱们一开始就将单点架构排除在外。面对大量的租户,单点架构会导致须要十分多的小集群,带来微小的运维复杂度。另外,随着单个文件系统规模的扩充,单机的能力下限、热点数据这些问题迟早会遇到。这些因素会导致单点架构无奈走很远,那就不如在一开始就朝着真正的分布式的方向致力。
在调研的时候,咱们留神到 HopsFS、ADLS 这两篇论文的工作,通过研判之后认为这两个工作代表的分离式元数据架构是将来整个畛域的发展趋势。 过后咱们曾经看到这个架构在写延时和写扩展性方面的劣势,但咱们认为这不过是一朵乌云,汽⻋刚创造进去的时候还跑不过⻢⻋呢,驱散这朵乌云不过是工夫问题。
在百度智能云外部,起初以“桑田”品牌命名的新一代存储体系过后正在建设中。这套体系以 brpc + braft 为基石,通过组件化的形式实现各类易运维、高性能、超大规模的分布式存储服务。 这个体系的元数据底座 TafDB 是一个类 Spanner 的零碎,简直和 CFS 同时启动研发。
综合这些因素,由 TafDB 提供海量数据存储能力和分布式事务,CFS 本人实现文件语义层这条技术路线在原理上确定了下来。 在 TafDB 就绪之前,CFS 基于 MySQL 开始后期的研发工作。
确定技术路线之后,咱们针对 CFS 的指标业务场景,做了一些设计上的调整:
1. 文件属性拆散
这个调整是说将文件属性(file attributes)从元数据服务中剥离进去,和文件数据(file data)一起放到数据服务(Data Service)中进行解决。次要的思考和性能有关系:
- 读性能的思考:在 2.2 章节咱们指出,属性局部只须要满足 inode 索引的点读即可,TafDB 作为一种全局有序的存储系统,点读的解决性能要比全局无序的数据服务要低;
- 写性能的思考:在批改文件数据的时候,POSIX 要求批改 ctime、mtime 等属性,追加写还随同着文件大小(file size)的更新,这会在数据写数据门路上引入频繁的元数据更新操作。类 Spanner 零碎的写 OPS 下限大概为百万级别,不可能都耗费在文件数据批改操作上,因而,整个零碎的写性能将比这个数字更低,不能满足业务的需要。
这个调整让整个零碎的文件属性操作的扩大能力变得十分好 ,起初 Namespace 2.0 沿用了该设计。读者能够在论文试验局部能够找到对该设计收益的定量分析。
2. 读写拆散
另外一个调整是将元数据读写门路做了离开解决。对于读申请,咱们绕过了元数据代理层间接拜访 TafDB,这样能够缩短读延时,同时少了元数据代理层的转发开销之后 OPS 也有肯定晋升。 对于写申请,则将每个文件系统(CFS 反对多租户,一套零碎中存在很多文件系统实例)的写申请收敛到一个单点进行解决,这么做的起因详述如下。
TafDB 提供的隔离级别是快照隔离级别(Snapshot Isolation),但文件系统场景理论须要串行化快照隔离级别(Serializable Snapshot Isolation)来防止孤儿节点问题。在下图的例子中,rmdir “/a” 和 create “/a/b” 操作并发了,在操作开始后,他们读到的是事务开始前的快照数据,同时看到 /a 目录存在,而后都操作胜利了,这就导致 b 成为一个不可达的孤儿节点。
通过在父目录的记录上制作写抵触能够躲避该问题,代价是让事务的抵触变得更频繁。咱们前文对锁的代价进行过定量分析,这在 TafDB 这种采纳乐观锁模型的零碎中代价更为昂扬,因为这类零碎判断事务抵触是在提交前的最初一刻,在这之前因抵触回退的事务曾经实现了绝大部分的操作,包含多轮 RPC 和落盘开销。
这些代价对于写性能的影响极大,容易导致大量不可控的⻓尾甚至整个零碎雪崩。这个问题在短期内没有特地好的解决思路。同时,依据公司内的教训,单点架构的写性能如果优化好能够达到 5 万 OPS 左右,咱们认为这个性能短期内是够用的。因而,咱们决定将每个文件系统的写申请膨胀到一个单点进行解决,写扩展性和写延时的问题留到当前再去解决。
通过下面的调整之后,Namespace 1.0 顺利的实现了研发并落地,大略的架构如下图所示。在这个架构里,所有文件相干的操作均由分布式的 FileStore 负责,其它的元数据读操作间接由客户端 ClientLib 发给 TafDB 解决,写操作通过 Namespace 进行解决,Namespace 是一个 Multi-Raft 的实现,每个 Raft 复制组负责一个文件系统。
CFS 元数据架构 Namespace 1.0
零碎的数据结构如下图所示。其中 TafDB 中的数据存储在一张大表中,以 <parent\_inode, name> 为 primary key,负责实现 lookup 和 readdir,以 <inode> 为 secondary key,负责实现目录的 getattr。
3.2. Namespace 1.X: 1.0 根底上不太胜利的摸索
1.0 上线之后,CFS 顺利承接了一些元数据读性能要求较高的业务。以一个外部业务为例,迁徙到 CFS 之前采纳了某开源解决方案,高峰期数十万 getattr OPS 导致元数据节点 CPU 打满,申请大量⻓尾甚至失败,零碎⻛雨飘摇,彷徨在解体的边缘。CFS 很轻松地接下了该业务,整个过程波澜不惊。 起初咱们剖析这个案例,认为次要的收益来自文件属性的拆散,这个优化使得文件的 getattr 间接由 FileStore 解决,将 getattr 打散得十分充沛。
然而写的性能始终是一个痛点,咱们基于 1.0 的架构做了大量的试验和剖析,尝试晋升单机的性能。在这个过程中比拟胜利的优化包含:
- 解决流程精简:咱们剖析了每一类写申请的全流程,从内核到 TafDB,将一些反复的条件查看进行了精简,将能够合并的操作进行了合并;
- 引入缓存:因为每个文件系统的元数据写都是单点,Namespace 模块任何时刻看到的数据都是最新的,具备引入缓存的条件,缓存命中状况下事务中的大部分读申请都能够被优化掉;
- 事务抵触预处理:在 TafDB 中呈现事务抵触的代价比拟大,咱们将事务抵触的解决上提到 CFS 这一层,Namespace 模块在执行事务前剖析其中存在的冲突点,依照每个冲突点进行排队,从而让下发到 TafDB 的申请无事务抵触。
通过这些优化,咱们让写延时和 OPS 别离晋升了一倍多。 但和单点架构、耦合式架构相比,这些优化不能对消解决门路变⻓带来的开销,写性能指标上依然存在不小的差距。 另外,如果咱们想要将解决能力扩大到多个节点,原来所做的缓存、事务抵触预处理机制在肯定水平上会生效,解决延时和单机 OPS 将再度好转。
到 2019 年年中的时候,事件变得有点儿让人失望,咱们悲观的发现,在简直穷举了所有可能和不可能的计划和思路后,摆在咱们背后的仿佛只有一条路线可走,即回到耦合式架构,基于 TafDB 做二次开发,在尽可能保留 TafDB 的长处的前提下实现文件系统语义。
咱们已经笃信分离式架构是将来倒退的大趋势,只管存在一些显而易⻅的问题,但汽⻋刚进去的时候也跑不过⻢⻋,解决这些问题不过是迟早的事件。然而,至多在 POSIX 畛域,看起来这辆新的汽⻋究竟不是汽⻋,不过是更好一点儿的自行⻋。
3.3. Namespace 2.0: 柳暗花明又一村
不论咱们怎么看好分离式架构,事实的问题终归还是要解决的。CFS 和 TafDB 两个团队决定最初坐下来一起再致力一把,要是不行就真搭伙儿了。
在数学和物理史上,很多新的实践都是从拆除旧实践的基石开始的,典型的例子有非欧几何、相对论。当然,和巨匠们相比,咱们的工作微不足道,这里只是借用比喻一下。
咱们决定放下所有的先验常识和成⻅,去寻找整个文件系统元数据大厦最底层的砖头,从那些砖头开始探讨。
很快,咱们发现现有的简直所有工作,都是从如何满足 POSIX 规范的那一堆接口开始的,但这些接口自身也是有内部结构和逻辑的,还不是最底层的砖头。
通过提炼 POSIX 接口背地的实质要求,咱们建设了 2.2 章节形容的形象模型,并将每种操作用极其简略的伪代码写了进去。
从这个形象模型登程,咱们推导出一组极其简略的论断:
- 影响元数据服务扩展性的本源是更新父目录属性时产生的抵触,这是关联变更的一部分;
- 这些变更实质上只是一些数学运算,分为两种。一种是加减运算,针对 links、children、size 等属性(如前文,children 不是规范属性,是为了便于保护目录下的子项数量),每次创立删除子项都须要精确更新。另外一种赋值操作,针对 ctime、atime、mtime 等属性,每次创立删除子项的时候用新值笼罩掉旧值;
- 以上两条对所有批改操作均成立,rename 的要求会更简单的额定要求。
有肯定编程教训的同学应该能够发现第二条的形象要求能够用原子变量操作来表白! 传统的实现为了这几个原子变量操作的正确性,将抵触范畴至多扩充到整个父目录记录,这是很不精密的做法,相似于给整个操作上了 mutex 锁来爱护临界区(critical section)。
在编程上优化这类的问题的路线,就是尝试放大临界区范畴,采纳开销更小的保护方式来代替 mutex,如果能优化到仅仅依赖原子变量操作就能保障正确性就最好了。这实际上就是咱们接下来采纳的优化思路,论文题目对此有比拟精准的概括。
计算机领域常常会看到一些常识或教训逐步变成大家的常识,但过后这些常识或教训产生的过程却是通过一些挫折的。
咱们总共花了一个季度来探讨和设计 Namespace 2.0,下面这几个关键点当初看来曾经成为咱们的常识,但在过后消耗了咱们一个多月的工夫来推导。方案设计实现之后,接下来的工作异样顺利。在已有零碎的根底上,咱们又花了一个季度做了 demo 版本进行计划验证和性能评估。demo 版本其实和起初的正式版本差异很小,这使得正式开发和测试的周期比拟短,到 2020 年 Q1 末的时候 Namespace 2.0 就开始在线上小流量了。尔后陆陆续续上线了一些优化,但整体架构直到现在都没有大的变动。
四、实现思路
Namepace 2.0 的领导思路是一直放大写操作的临界区范畴并最终实现无锁化。 上图给了一个简略的示意图,在 Namespace 1.0 中通过锁爱护的关联变更,在 Namespace 2.0 仅通过原子操作就满足,并发的操作不再须要串行执行。
为了实现这一点,咱们采纳了一系列技术的组合:
第一步,通过适合的数据布局,将抵触范畴放大到单个分片
零碎的首要指标是将 TafDB 的元数据申请的执行范畴从跨分片升高到单个分片,这样才可能让去除分布式锁成为可能,否则会有一致性问题,这个在背景局部咱们曾经论证过。这个指标督促咱们从新思考数据布局。
首先,Namespace 1.0 将文件属性由文件存储独自负责的做法,曾经被验证可能提供更好的文件属性操作性能,Namespace 2.0 持续沿用此设计。
其次,咱们察看到,任何一个目录项的元数据须要同时满足两类索引的需要,人造就是两个局部,一部分用于 getattr,保留的是属性信息,另外一部分用于 lookup 和 readdir,保留的是和父目录无关的索引信息。后者是关联变更的一部分,关联变更的残余局部和父目录属性有关系。 通过调整数据布局,将整个关联变更波及的数据耦合到一个分片上,能够起到让事务抵触聚焦到单个分片的成果。这个调整其实意味着“属性离开存储”这一规定扩大到了包含文件在内的所有类型的目录项。
最初,咱们将原来事务爱护的整个操作,拆解成两个 TafDB 单分片操作的组合(目录的状况),或一个 FileStore 操作 + 一个 TafDB 单分片操作的组合(文件的状况),配合通过精心排列的程序,使得这两个操作只须要满足别离满足原子性即可保障执行成果,不再须要一把大锁来爱护整个范畴。
第二步,将单分片操作的抵触范畴放大到字段级别,并实现无锁化
在抵触放大到单分片之后,如果不通过任何优化,单个目录下的操作依然须要串行执行。TafDB 通过对存储引擎进行扩大,引入单分片原语(single-shard atomic primitive)的技术,将原来的行抵触缩小成具体字段的原子操作,并能对并发的操作主动合并。
第三步,精简元数据代理层,进一步缩短执行门路
在上述优化后,除了非常复杂的 rename 操作,元数据代理层(Namespace 1.0 的 Namespace 模块)的作用仅剩实现 POSIX 接口并转发申请,咱们将这一层的能力间接整合到客户端中,只保留简单 rename 的解决,重新命名为 Renamer。
五、整体架构
CFS 元数据架构 Namespace 2.0
根据上述的实现思路,整个零碎架构上分成四个局部:
- Namespace 存储层(TafDB):TafDB 负责存储层级命名空间里除文件属性外的其它局部;
- 文件存储层(FileStore):这一层是一个以平坦形式组织的块存储层,块是文件数据的根本单位。文件属性和文件数据一起存储在该零碎里。对于文件属性而言,这是一个不反对范畴读、只反对点读的 KV 零碎;
- Rename 服务(Renamer):Multi-Raft 架构,每个文件系统由一个 Raft 复制组提供对简单 rename,即所谓 Normal Path rename 的反对;
- 客户端库(ClientLib):客户端负责接管具体的申请,拆解成上述模块的外部申请。ClientLib 提供必要的接口,以不便接入 Linux VFS 层,目前反对 FUSE、Samba、NFS-Ganesha。
从下面的架构图能够看出,CFS 中不再存在传统分布式文件系统的 MDS 或 NameNode 角色,所有的性能全副打散到其它模块。
这个设计在维持分离式架构长处的同时,解决了其存在的全副毛病,在扩展性、延时、均衡性等方面都达到了比拟好的状态。
咱们认为,这个新的思路给分离式架构这辆汽⻋换上了新的引擎,让这辆汽⻋在各方面的体现全面超过了⻢⻋。
六、实现细节
6.1 元数据组织和分片
和 InfiniFS 相似,CFS 将每一个目录项的记录拆解成两局部,别离是 inode id record 和 attributes record。这个合成是进一步缩减抵触域的根底。
TafDB 应用一张 inode\_table 表来存储所有的数据,表的主键为 <kID, kStr>。这张表里实际上存储了混合存储的两类数据,包含所有的 inode id record 和目录的 attributes record。FileStore 负责存储文件的 attributes record。整体如下图一样组织:
对于 inode id record,<kID, kStr> 中的 kID 局部代表父目录的 inode,kStr 代表子项的名字。这种记录是为了满足 lookup 和 readdir 的需要,除了 inode 和 type,其它字段都是有效的。
对于目录的 attributes record,kID 就是目录本人的 inode,kStr 是保留字符串 /\_ATTR。其它字段寄存的是各个属性字段,inode 字段在这里留空没有理论作用。
inode\_table 整体上按 <kID, kStr> 有序存储所有数据,分片规定是依照 kID 来的。TafDB 做了一个非凡的保障,即分片无论如何决裂和合并,同一个 kID 的数据始终存储在同一个分片上。 这意味着同一个目录的关联变更波及到的数据都在一个分片进行解决,CFS 实现了目录级别的 range partition。所有分片的决裂和合并操作都是 TafDB 主动进行的,不须要 CFS 关注和人工干预。
须要指出的是,咱们认为目前的设计没有必要持续摸索单目录多分片,以往的耦合式零碎里存在此类设计的根本原因是因为一个节点上的热点无奈准确疏散,热点会影响同节点上的其它目录的操作,多个热点间也会相互影响,只能通过扩散目录压力的形式来缓解问题。但咱们的架构不存在此问题,理由如下:
- TafDB 极其状况下能够决裂到单个目录独占整个分片,单个分片独占整个机器,单目录的解决能力和单点架构相当。这个粒度足够小,解决能力足够强;
- 文件属性拆散到 FileStore 之后,单分片不须要解决占比最高的文件属性操作,压力远小于传统的设计。
6.2 缩减分布式锁开销
6.2.1. 优化跨组件的协同开销
将元数据扩散到 TafDB 和 FileStore 两个组件存储之后,首先要保障的是两个零碎的对外一致性。咱们通过精心排列的执行程序来解决这个问题:
- 对于所有的创立操作,先创立 FileStore file attribuets record,后创立 TafDB inode id record;
- 对于所有的删除操作,先删除 TafDB inode id record,再删除 FileStore file attribuets record;
以 create、unlink、rename 为例,具体的执行过程如下:
文件系统在对文件操作之前都会经验从特定目录开始 lookup 的过程,只有 inode id record 存在对应的文件才会被看到,因而,只须要保障 file attributes record 比 inode id record 更早产生更晚沦亡,就能够保障有效数据不会被用户看到,从内部视角察看,一致性没有被突破。图示给出的操作程序能够达到这个成果,惟一的副作用是操作失败可能残留垃圾,这能够通过垃圾回收来解决。
这个办法能够推广到 TafDB 外部的状况,对于 inode id record 和 attribuets record 均存储在 TafDB 的目录也同样实用。当然,具体的操作执行程序须要思考 POSIX 的兼容性要求,可能和文件不完全一致,在此不做具体开展。
6.2.2. 优化 TafDB 外部的跨分片开销
在 TafDB 这样的零碎中,如果一个事务波及到多个分片,必然须要 2PC 事务提交机制来保障 ACID。咱们采纳的数据布局,让目录的 attribuets record 和其子项的 inode id record 都在一个分片内,使得这些必要的批改操作能够从传统的跨分片事务简化到单分片事务。在接下来的章节咱们将介绍这个简化技术。通过这一次裁剪之后,CFS 中除了 Normal Path rename,所有的 TafDB 操作均是高度优化的单分片事务。
6.3 缩减单分片锁
6.3.1. 单分片原语
关联变更波及到多条记录,在执行过程中还存在 read-and-modify 模式,特地的,对父目录属性的批改就须要先读出旧值再更新。 一个奢侈的实现形式会蕴含多轮条件查看、读和写操作,效率必定是不高的。为了解决这个问题,咱们提出了单分片原语(single-shard atomic primitive)的概念。
每一种原语实现了一个定制的单分片事务,这个事务原子地实现所须要的所有读、写和条件查看,保障执行过程是 all-or-nothing 的,只有全副条件查看为真,才会执行胜利,否则不会对分片数据做任何批改。原语作为一种非凡的单分片事务,高度优化后的执行代价和写一条 WAL 日志相当。
咱们演绎了所有的 POSIX 操作,最初总结出三种原语,如下表所示,表中同时标注了每种原语的应用范畴、类 SQL 模式的接口形容和简要的执行过程:
论文里给了 create、unlink、同目录文件 rename 伪代码作为示例解释了原语是如何工作的。在这里咱们能够看一下其中 create 的例子。
create 的第一个步骤是在 FileStore 中创立文件,第二个步骤就是 instert\_with\_update 原语,次要实现了以下工作:
- WHERE 做了两个条件查看,kID=@parent\_id, kStr=”/\_ATTR”, type=dir 这几个条件联结查看 kID、kStr、type 是否合乎指定的值。当查看通过时意味着 inode 为 @parent\_id 的 attribuets record 在 TafDB 中存在,且类型为目录;
- SET 对合乎 WHERE 条件的记录更新属性,具体的就是更新 children、size、mtime 等字段;
- INSERT 插入 inode id record 记录,主键是 kID=@parent\_id, kStr=@name,次要的字段是 inode;
在文章里原语是以类 SQL 的模式表白的,这只是为了表白的不便,理论 TafDB 提供的是 KV 层接口。相比于规范的接口,原语强化了条件查看的能力,同时在语句执行出错时可能返回更精密的出错条件,CFS 负责将这些条件转译成 POSIX 错误码。 依据咱们的教训,即便应用的 Table 零碎自身不反对这样的扩大,进行二次开发所需的工作量也很小,和耦合式元数据服务相比,工作量和实现难度更是要低好几个数量级。
最初,咱们总结一下原语的劣势:
- 极大地升高了和 TafDB 的通信开销,交互轮次减到一次;
- 将很多的关联操作压缩到一次解决,升高了整体的解决损耗,并存在进一步优化的空间;
- 简化了元数据服务的设计,不同的 POSIX 操作能够被 3 种原语概括,传统的实现则须要实现所有的 POSIX 接口。
6.3.2. 抵触合并
如果沿用规范的实现,上述的原语依然在父目录属性更新的时候导致排队。前文咱们剖析过,这里抵触的实质其实是对属性的更新操作,实质上是一些原子变量操作。依据这一点,咱们进一步实现了两个加强机制用于弱化及合并处理事务抵触。
第一个机制是 delta apply,针对 links、children、size 这些数字属性。它们在变更的时候会加减增量,和原子变量加减一样,程序并不重要,只有保障最终的叠加后果是对的。delta apply 就实现了这种合并加减成果。
第二个机制是 last-writer-win,针对权限、mtime、ctime 这些单纯的笼罩操作,咱们简略的保留最初一个赋值操作的后果。
原语通过查看 SET 语句波及的是加减运算(如 children+=1)还是赋值运算(如 mtime=@now)就能够主动决定使用 delta apply 还是 last-writer-win。这个检测齐全不须要感知文件系统的语义,具备通用性,实际上能够推广到任何只是局部字段原子变量操作的场景,实现比写写抵触更小范畴的抵触。
6.4 移除元数据代理层
将 Normal Path rename 之外的所有操作均拆解成了原语的组合之后,这些操作之间只有落到单个分片上时才可能会产生抵触,分离式架构里的元数据代理层存在的惟一价值变成了接口翻译,串联起各个环节来实现 POSIX 接口,但这一点齐全能够由客户端来承当,因而咱们做了一个大胆的决定,将这一层性能齐全集成到客户端来实现。
6.5 强统一 Rename
rename 是一个文件系统里最简单的操作,最坏的状况下,波及到 6 个元数据对象,包含 4 个 attributes record,2 个 inode id records。依据咱们的线上统计,99% 的 rename 都产生在同一个目录内文件间,这种 case 波及到的变更都在一个 TafDB 分片内,能够采纳前文提及的办法优化。因而,咱们将 rename 分为 Fast Path 和 Normal Path 两种。
Fast Path 的 rename 基于 insert\_and\_delete\_with\_update 原语实现,只解决同一个目录内的文件 rename,剩下的其它类型均为 Normal Path。Normal Path 由 Renamer 进行解决,Renamer 是每个文件系统一个 Raft 复制组,负责对 Normal Path rename 操作进行冲突检测。只有通过冲突检测的申请会发往 TafDB 持续解决应用 2PC 事务进行解决,这个检测能够保障收回去的申请不会导致孤儿或环。
Normal Path rename 和其它操作并发的正确性基于两个方面保障:
- 其它操作都不会扭转子项的归属,顶多导致看到的信息不是最新的,但不会对成环或孤儿的判断产生理论影响。在解决 Normal Path rename 的 2PC 事务中,咱们会对可能误判的状况进行进一步的查看,如果呈现变动则进行回退重试,整个处理过程和乐观锁机制相似;
- 1PC 事务只是对一般 2PC 事务的高度优化,ACID 和事务隔离级别的保障没有被毁坏,因而并发的 1PC、2PC 事务进一步由 TafDB 进行抵触解决,保障执行的后果合乎隔离性和正确性承诺。
整个机制的正确性能够通过 TLA+ 之类的形式化验证伎俩证实。
6.6 垃圾回收
当 ClientLib 呈现网络分区或者过程解体的时候,没有做完的操作会导致 TafDB 或 FileStore 中残留垃圾。零碎通过两种机制进行解决这些垃圾:
- 周期对账:周期性地在 TafDB 和 FileStore 之间进行对账,回收垃圾数据;
- 按需解决:当 getattr、readdir 在执行的时候发现某些 attributes record 无奈找到时能够⻢上发动一次垃圾回收,回收对应的 inode id record。
七、试验
论文里将 CFS 和 HopsFS、InfiniFS 进行了具体的比照。测试结果显示,在 50 节点规模的测试中,与 HopsFS 和 InfiniFS 相比,CFS 各操作的吞吐量进步至 1.76 – 75.82 倍和 1.22 – 4.10 倍,并将它们的均匀提早别离最高升高了 91.71% 和 54.54%。在竞争较高和目录较大的状况下,CFS 的吞吐量劣势则会进一步扩充一个数量级。
想理解残缺测试后果的读者能够浏览论文原文的试验局部,在这里就不再赘述。
八、总结
本文介绍了百度智能云文件存储 CFS 的元数据系统的外围设计,对⻓期困扰文件系统元数据畛域的 POSIX 兼容性和高扩展性(特地是写扩展性)难以兼顾的问题,进行了解答。这是一个大规模分布式文件系统是否扩大到千亿级别文件数,同时放弃高性能稳定性的一个关键问题。
分离式元数据架构是近年来文件系统元数据畛域的发展趋势,业界有后劲存储千亿文件的零碎均是基于这种架构实现的。这类架构采纳相似“存算拆散”的思维,将元数据服务分为两层,别离是负责存储数据的数据库层,和偏计算逻辑、负责实现文件系统语义的元数据代理层。但这种架构并没有解决写扩展性较差、写延时偏高的问题。
文件存储 CFS 进一步倒退了分离式元数据架构,通过精心设计数据布局,让元数据的不同局部以更迷信的形式在零碎里打散和聚合。打散是为了进步数据处理的并行度,聚合则让相互依赖的数据防止多轮交互可能被一次就解决完。在这个数据布局的根底上,CFS 一直修剪元数据写操作临界区的范畴,最终实现了无锁化,解决了分离式元数据架构写扩展性较差、写延时偏高的问题。
CFS 的这套设计曾经在生产环境中稳固运行了超过 3 年工夫,为云上蓬勃发展的的大数据、AI、容器、生命科学等场景的业务提供了无力撑持。
须要指出的是,CFS 的翻新和整个百度桑田存储技术体系的大力支持是分不开的。合抱之木,生于毫末;九层之台,起于累土。正是因为前人和其它团队做了很多扎实的工作,咱们才能够对一些不成熟、非常规的想法疾速地进行验证和试错,并在验证齐全之后迅速落地生产环境。在百度外部,这样的例子还有很多。
在研发 CFS 元数据系统的过程中,咱们失去的另外一个播种是狐疑精力,技术上没有什么权威是不可挑战的,多问一句“从来如此便对吗”没什么害处。
最初,再次感激 CFS 论文的合作方中国科学技术大学先进数据系统实验室(Advanced Data Systems Laboratory, ADSL)的老师和同学们,这篇论文的面世离不开你们的共同努力。
—— END——
举荐浏览:
基于 openfaas 托管脚本的实际
百度工程师挪动开发避坑指南——Swift 语言篇
百度工程师挪动开发避坑指南——内存透露篇
增强型语言模型——走向通用智能的路线?
基于公共信箱的全量音讯实现
百度 APP iOS 端包体积 50M 优化实际 (二) 图片优化