ZFS作为一个划时代的文件系统,被移植到了多个OS平台上,它具有的特性主要如下:
1. 接近于无限的存储空间 ZFS针对若干磁盘组成的集合抽象出了zpool的概念,而文件系统则是活动的,每个文件系统实例可以从zpool中分配空间,zpool的大小可以动态变化,在空间不足的时候可以动态添加磁盘,在空间充足的时候也可以通过移动数据的办法抽调出磁盘。ZFS的一些理论极限如下:对于任意ZFS文件系统实例,支持248个快照,支持248个文件,单个文件系统实例最大16EB,单个文件最大16EB,单个zpool最大128ZB,每个zpool最多支持264个设备等等。
2.COW事务模型 COW是Copy On Write的缩写,是指在对数据进行修改时并不是直接原地修改,而是拷贝一份新的与修改的内容合并。使用COW的一个例子就是Unix的fork函数实现,通常fork的过程中子进程需要拷贝父进程的页表等虚拟内存信息,而万一子进程对拷贝的页表修改较少这样的拷贝工作就会带来额外的负担,因此fork的时候子进程不拷贝父进程的页表等信息,而是与其父进程共享,在修改的时候拷贝出一份新的进行修改,在此处COW的主要功能是减少额外的copy带来的负担。对于文件系统而言,COW是实现快照等功能的基础,通过保留每个快照的时刻的数据而重定向新修改的数据,可以保证快照时刻的完整数据。对于通常的文件系统,例如ext3/4,xfs等,写入是modify in place的,这样的好处是可以保证在顺序写入后修改时磁盘上的数据的连续性,而劣势在于修改的时候面对宕机断电等意外情况,必须保证任意时刻的数据是合法的,因此ext3/4和xfs均引入了日志机制(ext3使用到了JBD),在写入数据的时候记入额外的信息来保障数据可以恢复,随之带来的则是一次数据的写入可能会引发若干次的日志写入操作,而日志的存储区通常与数据存储区是分开的,这就带来了磁盘额外的seek操作,影响性能。ZFS和Btrfs等文件系统采取了另外一种处理办法,即对所有的数据修改均采用COW的办法,这样可以保证磁盘上的数据时刻均是完整的,不必记录额外的日志,这里有一个关于各种使用COW文件系统的性能对比,采用COW的劣势在于处理随机写后顺序读取的情况,对于modify in place的文件系统,数据在磁盘上仍是连续存储的,而ZFS则由于COW机制,随机更新的数据已经散落在磁盘上的各处,后续的顺序读取则不必要的引入了多余的seek操作。
事务模型(Transactional Model)是从关系型数据库中借鉴而来的概念,对文件系统而言指的是保证一组操作的原子性,即要么成功,要么失败,不存在中间状态,并且各个transaction之间是隔离的,互不影响。ext3/4,xfs的日志实现和ZFS/BTRFS的实现均采用了事务的模型,关于ZFS的实现将在后文中说明,此处需要注意的是,即便使用了事务模型,也不一定能保证用户数据逻辑上的完整性,因为只有用户接触到的最上层应用才能确保数据逻辑上的完成性。对于数据库而言是用户接触的最上层应用,而文件系统通常是作为其他应用软件存储数据的仓库,事务模型只能保证文件系统在异常情况下其内部逻辑的正确性,而用户数据逻辑上的正确性仍需要自己保证。
3.端到端的数据安全性 ZFS的主要设计者之一Jeff Bonwick写过一篇博文《ZFS End-to-End Data Integrity》,概要性的说明了ZFS如何保证其数据的安全性。关于“端到端的数据安全性”笔者曾与一位网友进行过讨论,在这里而言这个概念是指从ZFS接收到数据开始到调用内核中各个不同硬件设备注册的驱动写入数据之间的过程,对于之前或者之后的过程,由于软件分层次的限制,ZFS无法得知上层应用传输的数据的正确性和硬件设备驱动、firmware没有bug,故无法保证数据的安全性。
ZFS的数据安全性主要通过RAID,checksum校验,多副本(ditto blocks)数据冗余的方法保证,分别介绍如下:
1>RAID:ZFS的卷管理器功能实现了RAID 0/1/10,RAID Z(RAID 5)/Z2(RAID 6)/Z3多个级别的RAID功能 ,其中RAID1/10,RAID Z/Z2/Z3有对数据的保护功能,RAID 1/10比较常见,RAIDZ/Z2/Z3则是ZFS引入的raid级别,RAIDZ相当于RAID 5实现,但没有RAID 5存在的write hole ,RAID Z类似与RAID 5,支持一块盘损坏情况下数据的恢复,RAID Z2存储了两套校验码,允许两块盘损坏情况下数据的恢复,同理,RAID Z3支持三块盘损坏情况下数据的恢复。
2>checksum校验: 通常的ext3/4,xfs默认数据写入磁盘之后,读出的数据和写入的数据是相同的,而在大规模的集群中,silent corruption是极其常见的情况,而此类文件系统无法判断出此类错误,当用户发现数据损坏时为时已晚。为了解决这个问题,ZFS为所有写入磁盘的数据都生成了checksum,checksum算法基于Fletcher或者SHA256,每块数据生成的校验值存储不是存在数据块内部(这是一个先有鸡还是先有蛋的问题),而是在指向该块数据的指针中,接下来ZFS也对此指针生成checksum,记录在指向此指针的指针中,依次类推,ZFS的checksum向上递归至文件系统的根节点,根节点也生成了checksum并存储了多份,形成了一颗hash tree。在读取数据的过程中,zfs会对读取到的数据重新生成校验值,和存储在父节点中的校验值进行对比,万一相同,则证明数据正确,否则出现了silent corruption的情况,此时万一使用了RAID或者是数据存储了多份,ZFS会使用别的正确的数据修复错误的这个版本,万一没有RAID或者所有版本均存在错误,ZFS会报告错误。
3>多副本数据冗余 用户可以设置ZFS中每份数据存储的份数,最多为3份,而ZFS默认会为文件系统的元数据在zpool的多块磁盘上存储多份,避免这些关键数据的损坏带来的更为严重的后果。
4.online fsck 常见的各种Unix文件系统都提供了fsck工具,用以检测文件系统内部可能存在的数据错误情况,而这种操作需要文件系统处于offline的状态,而zfs的scrub(擦洗)功能允许文件系统在online的情况下进行fsck工作。并且通常的fsck工具只检查元数据而非数据,而ZFS的scrub功能检查包括数据、元数据在内的所有数据。ZFS Best Practices Guide建议对于使用企业级硬盘的服务器,每月进行一次scrub操作,而对于桌面级磁盘,建议每周执行一次scrub操作。
5. 高效的快照功能 由于ZFS采用的COW机制,每次的写入或者是更新均不会影响到磁盘上已有的数据,也就是说,万一必要的话,ZFS可以记录下每次写入/更新的操作内容,这是实现快照功能的基础,实际的快照功能实现只是在其中选择一个从ZFS文件系统角度而言完整的事务作为快照点。笔者曾经使用过linux下的LVM卷管理器,LVM卷管理器支持快照功能,在同样的硬件环境和用例情况下对二者的快照功能进行对比测试,lvm打开快照功能之后,数据更新写入性能下降到原来的1/6,对比zfs创建快照之后的更新写性能,下降约15%,发现有少量读取io,相比lvm中kernel的kcopyd线程有大量的读取io,由于ZFS延迟合并了写入,并且判断出是对于旧有整个block的更新,所以就不需要读取旧有的数据,更新旧有元数据就可以了,但是由于LVM工作在IO栈的device mapper层,也没有对fs加hook区分,无法从细粒度层面上对block写入进行感知和优化,这里有对于lvm使用中需要注意问题的详尽描述。lvm的snapshot功能直到最近才加入了对于递归快照的支持,而对于ZFS来说,实现递归快照并不会带来更多的复杂度。
基于高效的快照实现,ZFS实现了快照数据的发送(send)和接收(receive)功能,通过此功能,ZFS可以高效的实现离线异地数据同步的功能,得益于COW机制,ZFS可以快速的计算出来自于同一个ZFS文件系统实例的两个快照之间修改的数据,dump出文件发送到远端进行合并,而不必使用rsync来计算所有文件的数据块的校验值进而同步数据的办法。
6. 自适应字节序功能 从5中的描述也可以看出,ZFS的数据可以在不同的平台(主要是由CPU的大小端决定)导入和导出。
7.高效的缓存管理功能 对于文件系统而言,除非是sync写入的或者OS内存紧张的状态(在linux下由dirty_ratio参数控制),否则数据是首先写入文件系统缓存,之后由文件系统或者操作系统的刷新线程(对于linux 2.6.32以上内核为backing device的flush线程,低于此版本为pdflush线程)异步写入的,由此可见缓存的实现是文件系统写性能的决定性因素之一。ZFS作为一个革新的文件系统,实现了自己的一些缓存替换算法,但为了保持与原有OS组件的兼容性,而又不损失性能,ZFS实现了自己的page cache功能,其他文件系统,例如UFS则继续使用旧版本的page cache,ZFS采用了Adaptive Replacement Cache缓存替换算法,取得了较好的性能。ARC相当于CPU的L1 Cache,使用内存实现,因此存储的数据有限,ZFS类似CPU的L2 Cache,包括两个部分:L2ARC和ZIL(ZFS Intent Log).L2ARC是读缓存,存储在L2ARC的数据是可以随时丢弃掉,从磁盘获取到的干净数据,而ZIL中存储的则是缓存的脏数据,需要写入到磁盘,通常ZIL中缓存的都是需要同步写入的数据,ZIL将需要同步写入的数据缓存起来,并在合适的时机按照一个事务组批量异步写入。为了保证数据的安全性,写入ZIL的数据都会记录日志,此日志只有在恢复的过程中才会读取,使用ZIL可以显著提高NFS和数据库应用的性能。类似与CPU的Cache分层中容量递增,读写速度递减的规则,ZFS通常使用SSD作为L2ARC和ZIL。
8.可变的block size 由于早期的磁盘物理参数限制,在各类*nix系统中,磁盘sector的大小通常定义为512字节,文件系统分配空间的最小block size也定义为了512字节。随着Advanced Format的出现,磁盘的sector可以做到4KB甚至更大,更大的sector在存储大文件时带来更好的性能,而在许多*nix系统中,这个常量是hard coded的,修改起来并不容易,ZFS支持从512字节到128KB大小的block size,针对各种不同的应用环境调优选择,以达到更高的性能。
9.数据去重功能 数据去重功能是现在各种中高端存储设备的必备功能之一,甚至有不少备份软件也内置了数据去重的功能,通过使用SHA256等特征较好的hash算法计算数据的指纹,在写入的过程中比对指纹,相同的数据只存储一份进而提高了存储利用率。ZFS的数据去重功能相当消耗内存,因此通常使用7中描述的L2ARC来存储数据指纹。笔者曾从事了两年多的数据消冗去重文件系统的研发和设计工作,从个人的角度来看ZFS的数据去重功能实现还有很多提高的余地,这点将在后文代码分析的过程中详细说明。
10.其他的功能特点 ZFS支持对数据的压缩,当前支持LZJB和ZIP两种压缩算法。支持deadline模式下的IO优先级调度,这点对于一些延时敏感的应用来说相当重要。支持读取时的数据预读功能,支持高效的IO请求排序和汇聚,支持按照用户和文件系统实例的磁盘限额功能等,最新版本的ZFS支持了透明的数据压缩功能,并且使用了非对称的加密算法提高安全级别。