本文概述 HotCloud ’18 中的一篇讲云原生文件系统的论文,来回顾下存储领域大佬 Arpaci-Dusseau 在 6 年前对云原生文件系统的想法。论文链接 hotcloud18-paper-arpaci-dusseau.pdf (usenix.org) 文章的贡献主要有两点: 提出一些云原生文件系统所应该遵循的设计原则; 提出一种云原生文件系统 CNFS 的大概设计。 云原生文件系统的设计原则 作者把设计的原则分为存储和计算两个层次,但总体来讲都聚焦于成本和性能的权衡。这种权衡是云环境相对传统环境更容易做到的,也是云原生文件系统设计的核心。 存储原则 可靠性已经通过更底层的云存储服务得到保障。云存储比如对象存储 S3 或者块存储 EBS,已经提供了多副本等可靠性功能,因此云原生文件系统可以把这部分功能卸载到更底层的云存储。 云存储空间便宜且可以无限扩展。云原生文件系统的设计应该尽量利用这些便宜的存储空间对数据建立索引,用空间换时间。 云存储与本地存储有很大差异:带宽通常较高;延迟根据服务分级不同差异明显;更快分级的云存储访问成本反而更低。因此云原生文件系统应该按冷热层次化地放置数据,来兼顾成本和高性能。 CPU 原则 云上 A 个 CPU 计算 B 秒和 B 个 CPU 计算 A 秒(A * B = B * A)的成本相同。因此,云原生文件系统的计算任务应该尽量并行起来,这样可以在尽可能短的时间内完成任务。 类似于云存储的空间,云上的 CPU 数量也很容易扩展。云原生文件系统应该按需地使用 CPU,但也需要注意根据负载的变化对之前扩容的 CPU 数进行缩容,来控制成本。 由于云存储可以在计算节点间共享,可以适当的将与文件系统相关后台任务从计算节点分离出来,让计算节点的 CPU 资源更多地用于计算任务。 云服务中 […]

MinIO 是一个用 Golang 编写的对象存储 server 开源实现。s3fs 是一个基于 FUSE 框架的以 s3 存储服务作为后端并导出 POSIX 文件系统挂载的开源实现。 本文首先基于 MinIO 搭建一个简单的对象存储服务。然后进一步借助 s3fs 将这个对象存储服务挂载为一个文件系统目录。 搭建 MinIO 对象存储 安装 Minio go install github.com/minio/minio@latest 启动 mkdir minio_dir minio server minio_dir CLI 操作 curl https://dl.min.io/client/mc/release/linux-amd64/mc \ –create-dirs \ -o $HOME/minio-binaries/mc chmod +x $HOME/minio-binaries/mc export PATH=$PATH:$HOME/minio-binaries/ # 设置 alias,之后就可以将 myminio 作为第一级目录操作了 mc alias set myminio […]

Persistent Memory (pmem, PM) 是很特殊的设备,它既是内存接口,又有非易失的存储特性。因此:作为内存,它即面临内存所特有的cache coherence等问题;作为存储,它也面临外存系统所特有的崩溃一致性、持久化等问题。 因此,构建一个基于PM的存储系统也所要考虑的问题是比较多的。近年来,学术界大量文章都在关注PM存储系统或者PM编程模型,比如PM感知文件系统、PM KV存储、PM事务框架等。 本文分为持久化、原子性和崩溃一致性三小节,讨论了PM系统编程的一些必须注意的点。文章整体基于我之前所写调研综述的相关部分,并进行了精简和修改。本文参考了很多文章并加入了自己的理解,若有问题,恳请指正。 1. 持久化 (Durability) 然而,对PM的store操作后并不能保证数据肯定存储到PM上,这是因为CPU和PM之间还有几层cache,要保证持久化,我们需要用flush / fence相关的指令。 当使用PM时,CPU对内存的store操作可能会暂存在硬件管理的cache或者write buffer中,若用户不强制刷cacheline到内存,无法保证store操作的数据何时写到内存中。在原来的情况下,内存为掉电易失的DRAM,所以刷或者不刷cacheline只可能牵扯到系统的性能,而不会影响系统的正确性;但是在使用PM时,由于持久化的存储是我们的目标之一,我们就要额外注重数据持久化的时机,以进行更精确的控制。 在使用块设备时,类似的问题同样存在。写操作一般会被缓存到系统的page cache中,因此需要用户调用fsync等函数来进行磁盘同步,让数据真正持久化地存储到磁盘上。解决PM cache数据写回的问题和块设备同步磁盘问题的思路是类似的,只不过方法不同。如下图,在x86平台中,当数据离开CPU cache进入PM或者进入电容保护的持久区(虚线框),便意味着数据已经被持久化,因此只要使用将数据刷出cache并写回PM的指令,就可以保证相应数据持久化存储到PM了。 由于一般在使用PM时,是通过内存映射的方式进行的,所以使用操作系统实现的msync函数是可行的(msync和fsync具有相同的语义)。除此之外,用户也可以直接调用x86平台的cache刷新指令进行数据同步。 指令 说明 CLFLUSH 几乎所有CPU都支持,但没有任何并发性,串行地执行 CLFLUSHOPT + SFENCE 比CLFLUSH新,但是不是串行执行的,因此需要SFENCE CLWB + SFENCE 相对CLFLUSHOPT,可以在刷入PM后仍然让cache保持有效,局部性较好的数据使用此命令可以提升性能 NT stores + SFENCE 即non-temporal store,直接跳过cache的store命令,因此不需要刷新cache WBINVD 只能在内核模式用,将所有CPU的cache 刷入PM,一般不用,太影响性能 上表列出了其他的cache刷新指令,它们的行为各有不同,需要依据场景进行选择。除表中最后一项外,其他指令都是可以在用户空间直接使用的。在用户空间调用cacheline刷新指令的好处是不用切换到内核态,且用户能更清楚地知道哪块数据需要马上写回PM,所以用户的控制更精细,刷新指令的性能也要好于msync。但是,一些PM感知文件系统也需要msync的控制权,因为数据刷入PM若需要造成PM感知文件系统的metadata改变,那么用户空间使用cacheline刷新指令将导致PM感知文件系统metadata的不一致。所以用户程序应该仅在确保文件系统安全的情况下才使用cacheline刷新指令。 2. 原子性 (Atomic Updating) 这里的原子性指的是原子更新粒度(原子操作)或并发时的原子可见性(隔离性),而非ACID事务的原子性。虽然ACID事务的原子性也是需要借助原子操作实现,这里的原子性更类似与ACID中的I(isolation, 隔离性)。 PM是内存接口,其原子性操作和内存类似,因此其访问原子性也和内存一样,在无锁保护的情况下, x86 平台上支持8、16 或 64 […]

本文主要探索以mmap接口访问文件时,文件自身大小、mmap映射范围和我们所能访问区间之间的关系。主要通过几个小的实验程序来说明。 本文假定读者了解mmap可以作为文件访问的接口,若没有用过可以在Linux中直接man mmap看相关说明,或者去网上搜索其他资料。简单来说,文件的某一段经过mmap系统调用映射后会返回一个地址,这样我们可以像操纵内存一样操纵磁盘上的数据,因此”open +mmap+memcpy+msync “这套文件操作可以在很多的时候代替”open+read/write+fsync“这套文件操作。 但是相比write进行追加写的操作,被mmap映射的地址是无法做到改变被映射文件大小的,那么我们如果想改变文件大小怎么办?如果我们写的地址大于实际文件大小会出现什么情况?如果我们写的地址大于所映射的地址范围会有什么情况? 通过两组简单的测试,我们可以探究这个问题: 测试1:”文件范围内, mmap范围外” 会产生SIGSEGV段错误 测试1是简单的情况,我们mmap映射的范围小于文件的实际大小,那么当我们访问在文件范围内但不是映射区范围内的地址时,会产生”segmentation fault”(SIGSEGV)错误!这很好理解,因为我们访问了非法的内存地址。 如下图,具体的,我们创建一个1 MB的文件,然后将其前512 KB用mmap映射,然后尝试访问文件第800 KB,第800 KB虽然在文件的范围内,但是不在映射范围内。结果是产生segmentation fault (SIGSEGV) 段错误。 +——————+——————+ file_testmap: | mmapped | not mmapped | +——————+——————+ 0 (KB) 512 ^ 1024 | we try to access here –+ (it will cause seg. fault) 程序1: #define _GNU_SOURCE #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> […]

一个文件的长度和它实际所占用的磁盘空间很可能是不同的,这主要涉及到稀疏文件(sparse file)和文件打洞(hole punching)的概念。这两个特性需要操作系统和文件系统的支持,目前Linux的ext4、XFS等文件系统都支持这两个特性。 稀疏文件 (Sparse File) 了解系数文件最直观的例子是,创建一个文件,然后用lseek定位到较大的偏移量,在这个偏移量实际写一些内容,这时实际占用的磁盘空间很小,但文件的长度却比较大。比如: #include <fcntl.h> #include <assert.h> int main() { // 打开两个文件file_normal和file_sparse int fd = open(“file_normal”, O_RDWR|O_CREAT, 0755); int fd_sparse = open(“file_sparse”, O_RDWR|O_CREAT, 0755); assert(fd != -1); // 一个从0写入3个字节,一个从1000偏移写入3个字节 lseek(fd, 0, SEEK_SET); lseek(fd_sparse, 100000, SEEK_SET); write(fd, “ABCDEFG”, 3); write(fd_sparse, “ABCDEFG”, 3); close(fd); close(fd_sparse); return 0; } ls的-s选项可以在第一列打印出文件所占的磁盘空间: zjc@~$ ./sparse_file zjc@~$ ls […]

Consistency这个词在计算机各领域用的很多,比如分布式系统、体系结构和存储系统等等。本文只探讨存储系统crash consistency。Crash Consistency问题在存储系统中都会存在(数据库、文件系统、dedup系统 …),即系统遭遇断电、崩溃等情况时,相关联的数据没有全部持久化可能导致的不一致。本文以文件系统为例进行说明,所有内容基于自己对相关资料的理解,如有错误,恳请指正! 崩溃为什么会导致不一致 下表整理自我的OSTEP笔记[1],我们假设了一个有data和inode、bitmap两种metadata的简单文件系统,下表给出了一些可能导致不一致的情况,其中N表示断电时没有写完,F表示断电时已经完成: 多种metadata之间的一致性通常最麻烦:如上表的inode和bitmap,他们之间存在相同冗余信息(bitmap可以从inode推导出,但是这个推导是要遍历所有inode的,bitmap的作用就是用冗余的信息换取性能),并且由于并非在一个磁盘块,无法原子地同时更新,所以如果掉电时只有两者之一成功更新了,那么它们之间相同的信息便存在了不一致。metadata和data之间也存在不一致的情况:如上表中,若两种metadata都更新好了,但是data写到一半掉电了,那么下次开机后根据metadata读data时就会读到坏的数据,因此可以称为不一致。 追究其根本原因,是存储系统中底层硬件的一次磁盘I/O(512字节),无法保证上层的一次请求的相关联的所有data和metadata的原子写入;反过来想,如果上层的每次请求中data和metadata都连在一起且小于512字节,那么就不用额外的一致性机制保证Crash Consistency。 保证一致性的方法–以WAL为例 由于硬件或者底层的原子写单元和上层存储系统一次请求所涉及的更改不匹配,所以我们只能在上层存储系统中用额外的手段保证crash consistency,常用的方法有: WAL(Write ahead logging, 也叫logging或journaling), CoW(Copy-on-Write, 也叫shadow paging), log-structuring, ordered write, soft updates等等。本文只简单举例说明一下WAL这种最常见的方法如何保证crash consistency: 比如,WAL为了保证bitmap和inode等不同种metadata之间一致性,在更改metadata时,一定要先将这些metadata写入到磁盘上的log区域,然后再对目标位置的metadata进行更改,这样,如果系统在写log时掉电了,原始的metadata没有影响,如果在写原位置metadata时掉电了,又可以在开机时从log进行重做(所以文件系统中的WAL类似于DBMS中的redo log)。 不同人对一致性有不同的认识 对于一个对任何事要求都很低的人来说,也许只有文件系统由于crash而被破坏了、不能再正常使用了才是不一致;他可能认为仅仅metadata和data的不一致可能并不算不一致,因为文件系统还会正常工作,只是被FS服务的用户或应用得到了错误的数据,谁叫他把电线拔了呢?。因此对一致性的定义、对一致性强弱的要求也是因人而异,因系统设计目标而异的。 比如,ext4是一种基于WAL的文件系统,具体提供了3种logging模式:journal, ordered(default), writeback。这三种方法对一致性的强度依次减弱,可以帮助我们理解为什么不同人、不同场景需要不同强度的一致性:journal是把所有data、metadata先进行logging[2];ordered是用ordered write的方法保证data和metadata的一致性,用logging保证不同类别metadata之间的一致性,ordered write指但是先写data完成,再写metadata的顺序,data因此也不用进行logging;writeback则不管data和metadata的先后顺序,data也不写log,可能刚刚提到的要求很低的人和对性能要求更高的人才会用这个参数吧。 摘自kernel文档[2] data=journal All data are committed into the journal prior to being written into the main file system. Enabling this mode […]

“写放大”(Write Amplification)在存储系统中是很常见的。但是,即使都是在存储系统中,“写放大”也有很多种,各种的写放大原理并不是很一样。下边根据自己的理解,进行了下总结,如有问题,恳请指正。 1. 读写单元较大导致的写放大 在文件系统中,读写单元固定,比如都是4K,这样,如果write函数写的数据小于4K,则要先把整块读入,再修改,再把新的4K整体写入(O_DIRECT情况除外)。这个过程可以称为 RMW (Read-Modify-Write),这就是File System的写放大问题。[1][2][5] (注意:Read-Modify-Write被更广泛地用在原子指令[3]和RAID[4]中。) 再如,在DBMS等应用层存储系统中,同样存在自己管理的读写单元,如MySQL的默认读写单元称为页,默认是16KB,所以一次读写只能以页的单位进行,这时,小于页的数据读写同样会带来整页的读写,进而造成了“写放大”,道理和文件系统是一样的。 2. RAID中的Read-Modify-Write造成的写放大 如前段所述,RAID中更新一个块,需要额外读原始块、校验块,额外写校验块,所以多了两个读,一个写,也称为Read-Modify-Write[4]。 这是由于校验块必须更新,且根据异或运算的可逆性,新校验块=新数据块^旧校验块^旧数据块。 3. SSD中闪存特性造成的写放大 在SSD中,一个block可以分为多个page,在读的时候,可以以page为单位,但是写的时候,只能以block为单位。因此写的单元比较大。在上层(比如文件系统)读写单元相同的情况下,同样是读写1个page的大小,读的话直接读就行,写的话却需要先把与要写page同一个block的数据全复制一遍,加上修改的page后,再一起写入block。写入的数据量远比实际的大,这就是SSD的写放大问题。 4. 存储系统一致性机制造成的同步写放大 在存储系统的很多层次中,都有保证系统crash consistency(一致性)的设计。因此,不管是应用层的存储系统(如DBMS、KV-store)、虚拟化层中的镜像管理、系统层的文件系统,甚至是硬件层的SSD FTL[7],都要通过强制同步各种元数据的写入顺序,或者利用redo log的思想,用journaling、log-structured或copy-on-write等策略保证元数据写入目的位置生效前先完整地生成日志,来保证系统崩溃或断电时,元数据之间是一致。但是,如果多层存储系统重叠,由于一致性机制导致同步次数增加就会层层放大。 比如,运行在x86虚拟机中的levelDB,其一次更新操作就会(1)最终导致levelDB写log文件和写数据两次同步写,这两次写就又会(2)导致2次的Guest文件系统log写和2次Guest文件系统数据写,一共4次同步写,这4次写又会导致(3)虚拟化镜像管理层的4 x N次写(N取决于镜像为保证元数据crash consistency的同步次数,若是qcow2格式,N可能有5次之多[6]),最后导致(4)Host文件系统的4 x N x 2 = 8 x N次同步写。当然这是一种比较极端的情况,但实际应用中也应该存在。 5. 基于LSM树的KV系统的Merge操作造成的写放大 levelDB等KV存储广泛采用了LSM树等结构进行存储组织,其特点就是靠上的level的数据会最终被merge sort到下层,由于多数level在磁盘文件中,这也就导致了同一KV数据的总写放大,放大的倍数就是大约是level的数目。和前边4中写放大不同的是,这种写放大并非写操作时马上就会发生写放大,而是写操作发生时会潜在的导致“未来会发生”写放大,所以这种写放大只会导致整体写代价提升,不会影响实时的延迟性能,只可能会影响磁盘带宽或者在SSD做存储设备时影响闪存耐久。FAST 16上有篇论文也专门分析了这种写放大。[8] [1] Why buffered writes are sometimes stalled, http://yoshinorimatsunobu.blogspot.com/2014/03/why-buffered-writes-are-sometimes.html [2] Block size and read-modify-write, https://www.spinics.net/lists/linux-xfs/msg14456.html [3] […]

“新司机”虽然上路了,但并不知道走了多少弯路,因为还知道有没有走上正路。但上路也快一年了,当然有一些体会,就算是错的,也该想过些什么。如果我什么体会都不写出来,那么这些想法总会在我的脑子中绕啊绕啊的。所以我希望写些出来,也许这样就可以不用刻意提醒自己不要忘记它们,也好专心”开车”。而且我想,车技总是不断积累联系和回看总结的过程,也许我将来看现在自己的体会,不是嘲笑自己的车技太差就是羡慕自己的没有迷路运气。当然还有一种情况,就是我能因这些感想能将其他”新司机”带上(歪)路,我也是很欣慰的。。。 这系列文章,不去反省自己的缺陷,不去叙述自己的经历,只写下当前所总结的体会。本篇博客,我将写一下对存储栈的理解。后边,我还可能从存储组织(文件系统/存储结构)、存储缓存和虚拟化存储等几方面写下体会。 1. 层次化封装 就像OSI网络参考模型一样,存储也是有层次的,如果在网络中我们将这些层次称为网络协议栈(TCP/IP协议栈),那么在存储中我们经常用存储栈(storage stack)或I/O栈(I/O stack)与之对应。 顾名思义,栈这一种直上直下一层叠一层的结构。在网络的层次中,数据链路层关心的是MAC地址,网络层关心的是IP地址,栈中的所有层次分工协作,大家像流水线一样将数据层层传递如下图。 在存储层次中,也有类似的分工。比如以Linux存储栈为例,文件系统主要关心的是文件的组织结构,向上将接口暴露给用户;而文件系统下一层的块存储设备层则关心实际数据的去向,主要和存储设备打交道。下图是一个单机上比较复杂的情况,以一个运行在KVM虚拟机中的MySQL为例。当然如果考虑远程网络分布式存储,应该更复杂。 2. 为什么层次是必要的 除了网络和存储,其实编程语言中封装的这种概念,在我看来就类似这种层次化的形式。比如在很多编程语言中常用的SHA256、MD5等安全Hash算法或者链表、各种常用数据结构的库等。这节的问题就以编程中的封装和层次开发为例进行说明。 对编程语言的库来说,问题在于,既然哈希算法的计算方法都是开源的,数据结构都是固定的形式,为什么不自己写呢?首先想到的可能是自己写费时费力,其次一个被很多人使用的库一般都是由各路大神优化而来,应该比自己实现的效果要好,但最重要的,库是可以复用的。 比如那么当我们在这些其他人实现的库的基础上构建自己的应用的时候,就是在库的封装之上进行的开发,这就形成了库–>我的应用的这样的层次结构。进一步说,如果我们是在其他人的库的基础上开发了更高层的库,比如我在其他人写的二叉树库的基础上开发了用于排序的库,那么我们的封装层次就又涨了一层:二叉树库–>排序库–>其他人的应用。再展开说,我们甚至可以冲破一种编程语言的界限,表示出更完整的封装层次:数字逻辑–>指令集–>汇编语言–>系统调用–>编程语言标准库–>二叉树库–>排序库–>其他应用。 如果只是从左到右或者从上到下画出这种层次图,我们还是无法理解层次的必要性,但是如果我们想横向扩充,这种层次就是很必要的。极端的情况:如果我们所有的层次都重叠在一起,那么也许我想开发一个gui小应用,就要从汇编开始,一直沿着操作系统改到库和应用逻辑,简直不现实,这也是软件工程中耦合和内聚的概念。换句话说,不论我们是靠近硬件还是靠近应用的程序员,我们都可以从自己出发,只要向上层和下层提供不变的接口,那么不论我们在自己的范围怎么折腾,都不会破坏其他层次的工作。 回到存储,我们只有将管理文件的file system和下层的设备驱动通过块设备层和虚拟文件系统(VFS)层解耦,才能在单独编写多中设备驱动不用再去修改文件系统代码,也才能在添加新的文件系统功能的同时不用担心对老的设备是否可以工作。 3. 为什么层次可能影响性能 还是以编程语言为例,很多都认可C语言相对其他语言是高效的,但学汇编的人可能觉得汇编才是最高效的,这都没有错,汇编比C高效,但并不属于高级程序语言,C比Python高效,但是指针这种东西没有谁可以一时半会搞定。实际上,汇编–>C–>Python就可以认为是这样封装起来的。某种程度上来说,我们开发C语言应用可以看成是和开发Python是在一个层次上的。。。对于存储也是一样的,层次总是一层一层网上堆的。堆到越上层,越具有其简单易用的特性,而这种层次化页必然会带来性能上的降低。 现在在我看来,存储层次升高带来的性能损失的根本原因是因为在层次间信息的不对称的同时还有些层次要越层”搞事情”。具体来说,层次之间的沟通通过接口,由于层次间较松的耦合,某一层中无法获知其他层中的具体细节。比如,应用程序可能要告诉文件系统具体要读的文件名,但并不知道文件具体存在什么地址;文件系统可能告诉块设备驱动它具体要读内容的地址,但是块设备层就不知道这内容属于什么文件了。 但是,存储的管理不光“你告诉我你要什么数据,我把数据传给你”那么简单:首先,由于数据的重要性,各个层次都要用自己的机制保证数据的完整性和一致性;其次,也要利用数据的局部性,用dram作为cache来弥合内存和二级存储设备间的性能差异。那么大家都要保证数据可靠,大家也都在加把劲降低IO读写延迟,真的是齐心协力力量大吗? 当然大家都想整个系统变得更好,但是既然层级之间信息不对称,一定会因为缺少合作,而导致自己所做的工作不如预期,甚至起到副作用。我们是需要加强各层间的耦合交互?还是提升各层的“智能”让其揣测其它层的想法?还是应该聘请一个“存储栈独裁程序”来统一发出号令?这块儿学问就大了,还是新司机的我理解并不够深入,车技提升后还要展开多写写。。。 4. 在适当的层次做适当的事 Python认为程序员要指针没啥用,于是没有设计出指针的概念。而可能在一些C语言程序员眼中,Python因此就是一种有缺陷的语言。但是我们可能忽略了一点,就是没有谁禁止过Python程序员学习C语言,也没有人禁止过C程序员学Python。对于一个项目来说,没有最好的语言,只有最合适的。 对于存储层次同样如此,比如同样是数据加密,我们可以在应用程序中加密数据,可以在文件系统中加密数据,可以在数据网络传输之前进行,极端的情况,甚至也可以在硬盘硬件中进行加密。对于cache,我们可以在应用层开辟内存作为cache、或者利用文件系统缓存,或者磁盘的硬件缓存等。选择很多,不同的功能,适合的位置也是不同的,同样和上节一样,这块儿学问就大了,还是新司机的我理解并不够深入,车技提升后还要展开多写写。。。