本篇文章的作者为龙姐姐说的都队的李晨光,他们团队在本次 Hackathon 较量中构建了一个基于 TiKV 的分布式 POSIX 文件系统 TiFS,继承了 TiKV 弱小的分区容错和严格一致性个性,为 TiKV 生态开拓了一个新的畛域。
起源
一次给敌人安利 TiDB 时,得悉他只有一台机器,那跑 TiDB 集群有什么劣势呢?我通知他能够每块盘跑一个 TiKV 实例,这样实现了多磁盘容灾,就不须要组 RAID 了。
当然最初一句只是玩笑话,毕竟 TiDB 是个数据库,只能做到数据容灾。但转念一想,如果把文件系统的数据也存进 TiKV,不就能做到文件系统容灾了吗? 于是咱们花了几天写出了 TiFS 的雏形 —— 一个充斥 bug 常常死锁的 POSIX 文件系统,但令人兴奋的是这个想法的确可行。
雏形进去后,咱们须要思考更多的问题。比方文件系统基于 TiKV 的劣势是什么?又比方 CAP 应该如何取舍?相比于常见的分布式文件系统存储后端,我认为 TiKV 最大的劣势是人造反对分布式事务,基于此咱们能够保障文件系统的 严格一致性 。如果咱们要保障严格一致性,即咱们要构建一个 CP 零碎,那实用场景该当是通用 POSIX 文件系统,齐全笼罩本地文件系统的需要,另外还能实现跨机器的文件合作或满足其它分布式应用的文件存储需要。
更酷的是,反对多实例合作的单机利用运行在 TiFS 上就可能变成分布式应用,比方 SQLite on TiFS 就是另一个 分布式关系型数据库!
设计细节
值
TiFS 一共须要在 TiKV 中存储系统元数据(Meta)、文件元数据(Inode)、文件块(Block)、文件句柄(FileHandler)、符号链接(SymbolicLink)、目录(Directory)和文件索引(FileIndex)共七种值。其中文件块是用户写入的通明数据,符号链接只存储指标门路,而另外五种都是序列化的构造数据。
零碎元数据
零碎元数据仅有一个用来生成文件序列号(inode number)的整数,其构造如下:
struct Meta {inode_next: u64,}
整个文件系统只有一份零碎元数据,且仅在 mknod
和 mkdir
的过程中被更新。
文件元数据
每个文件都有一份对应的文件元数据,其构造如下:
struct Inode {
file_attr: FileAttr,
lock_state: LockState,
inline_data: Option<Vec<u8>>,
next_fh: u64,
opened_fh: u64,
}
其中 file_attr
字段中存储了 POSIX 文件系统所必要的元数据,比方文件序列号、文件大小、块数量等,具体构造可参考文档;lock_state
字段存储了以后的上锁状态和持锁人,用于实现 flock;inline_data
字段可能会存储大量文件内容,用于晋升小文件的读写性能;next_fn
字段是一个自增整数,用于生成文件句柄;opened_fn
字段用于记录关上状态的文件句柄数量。
文件句柄
文件系统对用户的每次 open 调用生成一个文件句柄,仅用于存储句柄的读写限度,其构造如下:
struct FileHandler {flags: i32,}
目录
每个目录都须要存储一份子文件列表以实现 readdir
,列表中的每一项都存储了一个子文件的文件序列号、文件名和文件类型,其构造如下:
type Directory = Vec<DirItem>;
struct DirItem {
ino: u64,
name: String,
typ: FileType,
}
文件索引
咱们能够间接遍历目录来实现文件查找,但为每个文件链接创立索引显然是更高效的解决方案。每个文件索引仅含有指标文件的序列号,其构造如下:
struct Index {ino: u64,}
键
TiKV 自身只提供简略的键值对存储,键和值都是不定长的字节数组,所以设计零碎之前须要给键分逻辑区域。TiFS 一共有零碎元数据、文件元数据、文件块、文件句柄和文件索引五种键,其中文件块类的键能够用来存储文件块数据、符号链接和目录,另外四种键都只用于存储前文提到的同名值。
咱们依据第一个字节辨别不同类的键,这个字节可称为键的域(scope)。键的字节数组通用布局如下:
零碎元数据
零碎元数据域只有惟一键值对,其键的字节数组布局如下:
文件元数据
文件元数据域的键仅含有大端序编码的文件序列号,这样所有的文件元数据都程序地存储在 TiKV 上,能够在 statfs
操作时间接用 TiKV 的 scan
接口扫描出所有文件的元数据。
文件元数据键的字节数组布局如下:
文件块
文件块域的键由文件序列号和块序列号的大端序编码形成,这样同一文件的所有的文件块都程序地存储在 TiKV 上,能够在读取大段数据时间接应用 TiKV 的 scan
接口一次扫描出所需的文件块。
文件块键的字节数组布局如下:
文件句柄
文件句柄域的键由文件序列号和句柄号的大端序编码形成,其字节数组布局如下:
文件索引
文件索引的键由大端序编码的目录文件序列号和 utf-8 编码的文件名形成,其字节数组布局如下:
一致性
TiKV 同时反对乐观事务和乐观事务,但因为 Rust 版客户端只提供了实验性的乐观事务反对,以及无抵触状况下乐观事务性能较差,目前 TiFS 只应用了 乐观事务。
利用场景
TiFS 能够用于大文件存储,但它相比于现有的大文件存储计划没有特地的性能或存储效率上的劣势,它的次要应用场景是 小文件读写和简单的文件系统操作。git 近程仓库能够间接应用 TiFS 存储我的项目并运行 git 工作,比方 rebase 或 cherry-pick,而无需先转存到本地文件系统;多节点利用读写同一文件时能够间接应用 flock 来解决抵触。它的空间治理无需简单的 SDK 或 API 接入,只须要调用简略的文件系统 API 或 shell 脚本。
另外,像文章结尾所说的,反对多实例合作的单机利用运行在 TiFS 上就可能变成分布式应用,如运行在 TiFS 上的 SQLite 就成了分布式关系型数据库。当然这种场景对单机利用自身的要求比拟严苛,首先利用自身得反对单机多实例,另外尽量不依赖 page cache 或其它缓存以防止写入不可见。
测试与性能
目前咱们还没有给 TiFS 写测试,开发过程中咱们始终以 pjdfstest 为正确性基准并最终通过了它。但 pjdfstest 并不能笼罩读写正确性和并发下正确性,前面须要再跟进其它的测试。
从实践上来说 TiFS 的读写性能的影响因素次要有三个:文件系统块大小、网络带宽提早和负载块大小。上面咱们给出了一份 benchmark 的后果,并针对读写 $IOPS$ 和读写速度作出了四张图表。
咱们首先来探讨 $IOPS$ 的变化规律,如下两张图别离是程序写 $IOPS$ 和程序读 $IOPS$ 随负载块大小的变动,四条折线代表不同的文件系统块大小和数据正本数。
因为程序读写的 IO 操作是线性的,每次 IO 操作都是一次 TiKV 事务。如果咱们疏忽每次 IO 操作之间的轻微区别,那一次 IO 操作的耗时 $T$ 就是 $IOPS$ 的倒数,且 $T$ 由 FUSE 的 IO 耗时 $T_f$,TiFS 自身的逻辑耗时 $T_c$、网络传输耗时 $T_n$ 和 TiKV 的逻辑耗时 $T_s$ 叠加而成。如果不思考流式解决,耗时就是线性叠加,即 $T=T_f+T_c+T_n+T_s$, $IOPS=1/T_f+T_c+T_n+T_s$。
只思考读的状况,$T_f$ 与负载块大小正相干;$T_n$ 和 $T_s$ 跟负载块和文件系统块中的较大者正相干(因为 TiFS 每次 IO 操作必须读写文件块整数倍的数据),而更大的流量可能会造成更多的网络和磁盘 IO 耗时;$T_c$ 的实践变化规律未知。
TiKV 是个非常复杂的零碎,其外部也可分为逻辑耗时、磁盘 IO 耗时和网络耗时。本文在性能剖析时暂且将 TiKV 简化,只思考单正本的状况。
图读 $IOPS$ 随负载大小变动 中,文件块和负载块均为 $4K$ 时,随着负载的增大 $T_f$, $T_n$ 和 $T_s$ 都在减少,故 $IOPS$ 减小。文件块 $64K$ 和 $1M$ 的状况下,当负载块小于文件块,$T_n$ 和 $T_s$ 简直不变,$T_f$ 增大,$IOPS$ 减小;当负载块大于文件块,$T_f$, $T_n$ 和 $T_s$ 都在减少,故 $IOPS$ 继续缩小。变动折线大抵合乎预期。
图中横坐标为对数间隔,且取样点过少,斜率仅供参考。
程序写数据时,如果负载块小于文件块,则 TiFS 须要先读一个脏文件块,会造成额定的 $T_c$ 和 $T_n$。这一点在文件块比拟大时比拟显著。如图写 $IOPS$ 随负载大小变动 中,文件块 $1M$ 时 $IOPS$ 的极大值显著处于文件块与负载块相等时。
另外咱们能够发现,负载块和文件块为 $4K$ 或 $64K$ 时的 $IOPS$ 的简直相等的。此时每秒最小流量 $4K \times 110=440K$,每秒最大流量 $64K \times 100=6.25M$,对网络或者磁盘的压力很小。在流量足够小的状况下能够认为 $IOPS$ 到达到了下限,此时 $T_n$ 的次要影响因素变成了网络提早(本机测试能够认为是 $0ms$)。特地在写 $IOPS$ 随负载大小变动 图中,文件块和负载块在 $4K$ 和 $64K$ 之间变动对 $IOPS$ 简直无影响。咱们称此时的 $T$ 为 TiFS 在以后网络环境下的 固有操作提早,它次要由 $T_c$ 和 $T_s$ 决定。TiFS 和 TiKV 的逻辑耗时造成了固有提早,过高的固有提早会造成小文件读写体验很蹩脚,但具体的优化思路还须要进一步的 perf。
读写速度等于 $IOPS$ 与负载块的乘积,而 $IOPS$ 在负载块 $4K$ 到 $1M$ 之间没有激烈变动,咱们很容易就能看出负载块 $1M$ 时读写速度达到最大值。上面两张是负载块 $1M$ 时不同集群配置下的读写速度比照图。
文件块 $64K$ 下未开启 Titan 的比照图已有单正本数据,三正本数据仅供参考,未进行反复比照。
咱们能够看到写速度受 文件块大小和 Titan 的影响比拟大,读速度则简直不受影响。文件块越小,TiKV 须要写入的键值对越多,导致了额定耗时;但文件块过大会导致 RocksDB 写入性能不佳,开启 Titan 能够缩小不必要的值拷贝,显著晋升性能。
将来
TiFS 在架构上存在的一个问题是文件块存储老本比拟高。TiKV 采纳多正本冗余,空间冗余率(理论占用空间 / 写入数据量)个别 3 起步;而像 HDFS 或 CephFS,JuiceFS 等反对 EC 冗余模式的分布式文件系统冗余率可降到 1.2 到 1.5 之间。EC 冗余在写入和重建数据时须要编解码,均须要额定的计算资源。其在写入时能够升高网络开销和存储老本,但重建时一次须要读取多个数据块,有额定的网络开销,是一种就义局部读性能以升高写入时网络开销及存储老本的冗余策略。
目前 TiKV 要反对 EC 冗余还比拟艰难,前面 TiFS 会尝试反对 EC 冗余的对象存储来存文件块以升高存储老本,但近期的工作还是集中在 正确性验证和性能调优。正确性验证局部包含找其它的开源文件系统测试和自建测试。性能调优局部包含 TiFS 的自身的调优工作和 TiKV 的高性能应用,以升高固有提早。如果你对这个我的项目感兴趣,欢送来试用或探讨。