存储迁移:zfs send recv 还是 rsync?
坑边闲话:存储系统随时面临扩容的考验。在不重建 RAID 的情况下,使用 ZFS 自带的扩容方案也是可行的。然而 RAID-Z 磁盘组变多之后,占用量不均衡的现象很令人烦恼。此时购买一个全新的阵列,将原有阵列的数据挪过去就变得可行。
1. ZFS 添加阵列的弊端·
在传统认知里,RAID 的重建是很重要的操作,比如我们可以往 RAID-5 里添加硬盘,此时会产生一次重建:RAID 卡会按照一定的原则,将原阵列中的一部分数据和校验值分摊到新的硬盘上,使得新阵列的所有硬盘具有相同的容量占用。在机械硬盘时代,这个过程耗时非常久。尽管 SSD 可以让重建工作变得更简单,但这依旧是个麻烦且会带来服务降级的操作,搞不好还会造成服务质量的严重下降,甚至造成服务的短暂中断。
作为对比,ZFS 支持实时扩容。ZFS 的操作很简单,当目前的存储池占用达到 80% 左右的时候,系统提示告警。当然,80% 只是一个常见值,用户可以自行设定阀值。在收到告警之后,用户需要添加一个一模一样的 RAID-Z 到现有的阵列组里,新老 RAID-Z 以 stripe 组的形式提供单一存储。然而这样的问题很明显:新旧阵列的数据占比不均衡,即老 RAID-Z 已经接近满占用,但是新 RAID-Z 几乎是零占用。此时往存储池里写数据,在极端情况下只有新 RAID-Z 在接收,老 RAID-Z 一点写入量都没有。
可以简单计算一下系统的文件占用:
- 第一次扩容
- 扩容后的占用布局:(
0.8
,0
) - 扩容后的占用量为 $\frac{0.8+0}{2}=0.4$
- 可写入总计 0.8 个阵列可用容量。
- 在此达到全池占用 80% 后,占用布局为 (
1.0
,0.6
),写入到 0.4 个阵列可用容量时出现降速。
- 扩容后的占用布局:(
- 第二次扩容
- 扩容后的占用布局:(
1.0
,0.6
,0
) - 扩容后的占用量为 $\frac{1.0+0.6+0}{3} = 0.53$
- 可写入总计 0.8 个阵列可用容量。
- 再次达到全池占用 80% 后,占用布局为 (
1.0
,1.0
,0.4
),到达 80% 占用前,全程只有两个阵列的写入速度,且不降速。
- 扩容后的占用布局:(
- 第三次扩容
- 扩容后的占用布局:(
1.0
,1.0
,0.4
,0
) - 扩容后的占用量为 $\frac{1.0+1.0+0.4+0}{4}=0.6$
- 可写入总计 0.8 个阵列可用容量。
- 再次达到全池占用 80% 后,占用布局为 (
1.0
,1.0
,0.8
,0.4
),到达全池 80% 占用前,全程只有两个阵列的写入速度,且不降速。
- 扩容后的占用布局:(
- 第四次扩容
- 扩容后的占用布局:(
1.0
,1.0
,0.8
,0.4
,0
) - 扩容后的占用量为 $\frac{1.0+1.0+0.8+0.4+0}{5}=0.64$
- 可写入总计 0.8 个阵列可用容量。
- 再次达到全池占用 80% 后,占用布局为 (
1.0
,1.0
,1.0
,0.7
,0.3
),到达全池 80% 占用前,全程先有三个阵列的写入速度,写入 0.6 个阵列可用容量后,降速为双阵列写速。
- 扩容后的占用布局:(
由此可见,每次扩容后的新可用空间永远不变,但是 pool 的占用比一直在增高,只是后续随着总空间的容量越来越大,分母越来越大,写入的占用比例增速不再明显。至于写入速度则比较难以计算,但总体也是二到三个阵列 stripe 的速率。
下面简单展示每次扩容一个阵列达到阀值时的阵列占用率。
1 | #1 ==> 0.8 |
为了进一步澄清这个事实,笔者提供了一段 Python 代码,可以计算出扩容后的布局:
1 | # 单阵列的最高占用比例 |
将结果稍微可视化处理,可以得到下图:
根据以上计算,我们得出两个重要结论:
- 通过逐个添加同规模同容量的阵列到存储池以实现扩容,可以保证每次扩容后新的可用容量为添加的新阵列的可用容量。
- 通过上述扩容方式,文件在不重写的情况下,将不会均匀分布在所有磁盘中。这带来的好处是损毁某些阵列,不会造成全部数据的丢失,但弊端是读取性能比较一般。因此 ZFS 建议单阵列的读取性能应该设置为服务的极限读取性能,如果单阵列的性能无法达到服务的要求,则应该在创建阵列的时候考虑增大阵列的可用磁盘数量,即增加条带宽度。
2. 如何解决 ZFS 扩容带来的问题?·
一般有几种常见策略可以解决上述问题:
- 阵列的手动平衡。通过检测文件的时间戳,判定出哪些文件在哪个阵列里。随后逐个重新写入,即可完成平衡。要做到精准的话,需要每次添加阵列之前都做一次文件分布记录,比如使用
tree
命令将目录导出。- 缺点:繁琐无比,而且需要耗费大量的精力。
- 优点:可以保证平衡占用。
- 寻找一个空余的大空间,将所有数据挪过去,再挪回来。
- 缺点:成本高,一般人很难找到一个合适的大空间。几百 TiB 的数据就需要几百 TiB 的临时空间,这几乎不可能做到。此外,还有两次完整传输带来的时间成本。
- 优点:可以做到严格的再平衡。
今天我们的主要思路是方法 2,即重新写入。为了实现重新写入,一般有几个常用的方案:
- 使用
rsync
命令按文件进行搬迁。 - 使用
zfs send
和zfs recv
管道传输整个文件系统。
从性能上看,方案 2 更加合适,它可以实现 full stream,最大化地使用到磁盘的极限性能。rysnc
的速率也不慢,但是很难摸清楚它的块读取策略。
2.1 最大化 rsync
的性能·
rsync
用来做文件迁移是非常好用的。
2.2 理解 zfs send
/ zfs recv
的问题·
我们设想有如下存储场景:
- 单个 RAID-Z 的条带宽度为 4,即 4 盘 RAID-Z;
- RAID-Z 磁盘组的数量为 3,即该存储池经历过两次扩容。
- 现在每个磁盘的容量是 18TB,需要将该存储池的数据迁移到一个所有盘均为 22TB 的新存储池中。
假如 zfs send
的时候,先从一号 RAID-Z 里读取三个块,然后平均写入到新存储池的三个 RAID-Z 中,则效果非常理想,因为老 RAID-Z 里逻辑上相邻的的块来自于老的文件,写入时将这些块平均分散到所有的 RAID-Z 里,无疑提升了新存储池的并行度,此后读取这个文件时,将不再是只从一个 RAID-Z 里读取,而是从三个 RAID-Z 里同时读取。如此完成数据迁移,将带来平均的占用以及更高的单文件读取性能。
然而,如果 zfs send
并行地从老存储池的三个 RAID-Z 里读取块,然后分别写入到三个新的 RAID-Z 里,则到了后期,只剩老 RAID-Z(原先占用量最高的组)没有搬迁完毕。此时剩余部分将会平均分配到三个新 RAID-Z 里。这样的结果依旧不够明确,因为我们不知道搬迁的前期,新旧 RAID-Z 的读取和写入是否有一一对应的关系,如果答案是肯定的,那么将保持原有的布局,不会提升性能;反之,ZFS 可能会采取一种调度策略,尽量保证从一个老 RAID-Z 里读出的块序列,可以随机分配到新的三个 RAID-Z 里。那么答案是什么呢?需要做实验才清楚。
总结·
这篇博客讨论了创建 RAID-Z 时需要注意的问题,同时对比了迁移存储的一些方法。