ZFS 复制(replication)
replication
是OpenZFS的数据管理功能,提供了一个确保硬件故障最小化丢失和宕机的机制。简单来说,能够通过跨磁盘或跨主机的快照复制,实现数据的冗余和容灾。
例如,你可以将一台主机的 /home
目录构建快照,然后通过 zfs send
将快照 serialized
数据流并通过 zfs receive
传输文件和目录到另外一个主机。接受快照就像处理一个动态文件系统,可以在另外一台主机上直接访问接收的快照。
在实践中,通过网络复制快照,不仅可以跨机房(物理容灾),而且可以设置定时任务完成。只要接收方存储容量足够,并且随着时间复制修改,并且网络也有能力处理数据传输。
环境要求
Replication
要求发送和接收方至少有一个OpenZFS pool:存储池可以是不同大小
存储池可以是不同的RAIDZ级别
存储池也可以使用不同的参数属性
根据快照大小和网络传输速率,第一次
replication
可能需要非常长的时间,特别是复制整个存储池在完成了首次复制之后,后续的增量数据复制通常会很快
zfs send | zfs recv
是基于块级别的复制,并且内置了checksum,所以能够保障数据完整型建议在启动
Replication
之前先检查目标服务器是否有足够空间容纳发送方数据
数据复制
time=`date +%Y-%m-%d_%H:%M:%S`
zfs snapshot zdata/docs@$time
zfs send -v zdata/docs@$time | zfs receive zstore/docs
当使用了 -v
参数会看到同步数据的进度
root@xcloud:~ # zfs send -v zdata/docs@$time | zfs receive zstore/docs
full send of zdata/docs@2025-08-05_22:48.09 estimated size is 1.77T
total estimated size is 1.77T
TIME SENT SNAPSHOT zdata/docs@2025-08-05_22:48.09
22:50:45 314M zdata/docs@2025-08-05_22:48.09
22:50:46 1.88G zdata/docs@2025-08-05_22:48.09
22:50:47 3.51G zdata/docs@2025-08-05_22:48.09
22:50:48 3.69G zdata/docs@2025-08-05_22:48.09
22:50:59 3.84G zdata/docs@2025-08-05_22:48.09
...
备注
实际上,通过 snapshot
发送( send
),然后在目的端接收( receive
):
目的端会创建相同的 snapshot 名字 ,(似乎是)然后再
clone
出来同名的dataset
,所以你会在目的存储zstore
看到docs@2025-08-05_22:48:09
快照(但是实际使用空间几乎是0):
zstore/docs
的快照(注意,这是接收目的端)zfs list -t snapshot zstore/docs
输出显示:
zstore/docs
的快照(目的端,快照使用空间几乎为0?)NAME USED AVAIL REFER MOUNTPOINT
zstore/docs@2025-08-05_22:48.09 85.2M - 1.73T -
检查对应的数据集(可以看到用了 1.7T):
zstore/docs
的数据集(注意,这是接收目的端)zfs list -t filesystem zstore/docs
输出显示数据集才真正使用了 1.7T
zstore/docs
的数据集(注意,这是接收目的端)NAME USED AVAIL REFER MOUNTPOINT
zstore/docs 1.73T 9.38G 1.73T /zstore/docs
递归数据复制
上述数据复制是指定了 zdata
pool 中的 docs
数据集,那么对于具有很多个数据集的存储池该如何复制呢?需要一个个指定数据集么?
zfs提供了一个 -r
参数表示 recursive
(递归),可以包含所有的子数据集。注意,这个 -r
参数不仅可以用于 list
也可以用于复制。
首先检查
zdata
所有(-r
)的filesyatem
类型(-t
)的数据集
zdata
存储池zfs list -r -t filesystem zdata
输出显示有如下这么多卷集:
zdata
存储池,可以看到有很多卷集(dataset)NAME USED AVAIL REFER MOUNTPOINT
zdata 1.75T 4.37T 104K /zdata
zdata/docs 1.73T 4.37T 1.73T /zdata/docs
zdata/jails 3.86G 4.37T 140K /zdata/jails
zdata/jails/containers 2.37G 4.37T 96K /zdata/jails/containers
zdata/jails/containers/dev 2.37G 4.37T 2.38G /zdata/jails/containers/dev
zdata/jails/containers/pg 588K 4.37T 4.68M /zdata/jails/containers/pg
zdata/jails/containers/store-1 604K 4.37T 4.69M /zdata/jails/containers/store-1
zdata/jails/media 1.03G 4.37T 1.03G /zdata/jails/media
zdata/jails/templates 462M 4.37T 112K /zdata/jails/templates
zdata/jails/templates/14.3-RELEASE-base 457M 4.37T 457M /zdata/jails/templates/14.3-RELEASE-base
zdata/jails/templates/14.3-RELEASE-skeleton 4.59M 4.37T 4.39M /zdata/jails/templates/14.3-RELEASE-skeleton
zdata/movices 120K 4.37T 120K /zdata/movices
zdata/softwares 96K 4.37T 96K /zdata/softwares
zdata/vms 13.6G 4.37T 112K /zdata/vms
zdata/vms/.config 104K 4.37T 104K /zdata/vms/.config
zdata/vms/.img 96K 4.37T 96K /zdata/vms/.img
zdata/vms/.iso 5.62G 4.37T 5.62G /zdata/vms/.iso
zdata/vms/.templates 196K 4.37T 196K /zdata/vms/.templates
zdata/vms/idev 3.10G 4.37T 120K /zdata/vms/idev
zdata/vms/xdev 4.93G 4.37T 124K /zdata/vms/xdev
备注
其中的 zdata/docs
我已经做过复制,所以不需要再做
主要是复制 zdata/jails
和 zdata/vms
为
zdata/jails
和zdata/vms
卷集做递归
快照(-R
参数表示递归send
):
time=`date +%Y-%m-%d_%H:%M:%S`
zfs snapshot -r zdata/jails@$time
zfs snapshot -r zdata/vms@$time
zfs send -v -R zdata/jails@$time | zfs receive zstore/jails
zfs send -v -R zdata/vms@$time | zfs receive zstore/vms
注意,这里有一些有用的参数需要关注:
发送端
zfs send
参数:-R
表示发送指定存储池(pool)或数据集(dataset)的整个递归集合。并且接收时,所有已删除的源快照都会在目标端删除-I
包括最后一个复制快照和当前复制快照之间的所有中间快照(仅在增量发送时需要)
接收端
zfs recv
参数:-F
扩展目标池,包括删除源上已删除的现有数据集-d
丢弃源池的名称并将其替换为目标池名称(其余文件系统路径将被保留,并且如果需要还会创建)(这个没有明白,待实践)-u
目标端不要挂载文件系统(很有用的参数,如果没有这个参数,则目标端会自动挂载,挺清晰,但是对于备份数据可能不需要自动挂载)
上述方法参考 how to one-way mirror an entire zfs pool to another zfs pool ,我用来备份到移动硬盘。该答案提供了一个简单脚本,很简单,但是值得参考
#!/bin/sh
# Setup/variables:
# Each snapshot name must be unique, timestamp is a good choice.
# You can also use Solaris date, but I don't know the correct syntax.
snapshot_string=DO_NOT_DELETE_remote_replication_
timestamp=$(/usr/gnu/bin/date '+%Y%m%d%H%M%S')
source_pool=tank
destination_pool=tank
new_snap="$source_pool"@"$snapshot_string""$timestamp"
destination_host=remotehostname
# Initial send:
# Create first recursive snapshot of the whole pool.
zfs snapshot -r "$new_snap"
# Initial replication via SSH.
zfs send -R "$new_snap" | ssh "$destination_host" zfs recv -Fdu "$destination_pool"
# Incremental sends:
# Get old snapshot name.
old_snap=$(zfs list -H -o name -t snapshot -r "$source_pool" | grep "$source_pool"@"$snapshot_string" | tail --lines=1)
# Create new recursive snapshot of the whole pool.
zfs snapshot -r "$new_snap"
# Incremental replication via SSH.
zfs send -R -I "$old_snap" "$new_snap" | ssh "$destination_host" zfs recv -Fdu "$destination_pool"
# Delete older snaps on the local source (grep -v inverts the selection)
delete_from=$(zfs list -H -o name -t snapshot -r "$source_pool" | grep "$snapshot_string" | grep -v "$timestamp")
for snap in $delete_from; do
zfs destroy "$snap"
done
根据 Oracle Solaris ZFS Administration Guide > Sending and Receiving ZFS Data 说明,当使用 -i
参数时候,可以发送两个快照之间的增量部分,不过要求已经完成过上一次快照复制(即已经存在 newtank/dana
):
zfs send -i tank/dana@snap1 tank/dana@snap2 | ssh host2 zfs recv newtank/dana
其他复制案例参考
fastest way to copy zfs volume from server to another 备份虚拟机的案例:
# 在不停止vm情况下发起第一次虚拟机快照
zfs snapshot rpool/vm-X-disk-Y@initial_snapshot
# 第一次初始化复制虚拟机:
# 发送方使用 -p 参数表示在数据流中包含dataset的属性(在使用 -R 递归参数时,这个 -p 参数是隐含默认的)
# 接收方使用 -F 参数表示在执行接收操作之前,强制将文件系统回滚到最新快照。结合发送端的 -R -I 命令,会销毁发送端不存在的快照和文件系统
zfs send -Rpv rpool/vm-X-disk-Y@initial_snapshot | ssh -o BatchMode=yes root@new_server_ip zfs recv -Fv rpool/vm-X-disk-Y
# 在完成第一次初始化vm复制之后,停止虚拟机,然后创建增量快照 @final_snapshot
vm-bhyve stop vm-X
# 这样由于增量快照极小,可以降低虚拟机停机时间
zfs snapshot rpool/vm-X-disk-Y@final_snapshot
# 后续增量虚拟机快照传输,注意 zfs send 增加了 -I 参数
zfs send -Rpv -I rpool/vm-X-disk-Y@initial_snapshot rpool/vm-X-disk-Y@final_snapshot | ssh -o BatchMode=yes root@new_server_ip zfs recv -Fv rpool/vm-X-disk-Y
# 最后就可以在备份服务器上启动复制好的虚拟机
上述复制采用了最小化停机操作(即初次复制虚拟机时不停虚拟机,初次复制完成后,停虚拟机,再增量快照并传输增量部分)
如果对虚拟机停机时间没有要求,那么可以简化为一次性停机复制:
vm-bhyve stop vm-X
zfs snapshot rpool/vm-X-disk-Y@now
zfs send -Rpv rpool/vm-X-disk-Y@now | ssh -o BatchMode=yes root@$IP zfs recv -Fv rpool/vm-X-disk-Y