ZFS 复制(replication)

replication 是OpenZFS的数据管理功能,提供了一个确保硬件故障最小化丢失和宕机的机制。简单来说,能够通过跨磁盘或跨主机的快照复制,实现数据的冗余和容灾。

例如,你可以将一台主机的 /home 目录构建快照,然后通过 zfs send 将快照 serialized 数据流并通过 zfs receive 传输文件和目录到另外一个主机。接受快照就像处理一个动态文件系统,可以在另外一台主机上直接访问接收的快照。

在实践中,通过网络复制快照,不仅可以跨机房(物理容灾),而且可以设置定时任务完成。只要接收方存储容量足够,并且随着时间复制修改,并且网络也有能力处理数据传输。

环境要求

  • Replication 要求发送和接收方至少有一个OpenZFS pool:

    • 存储池可以是不同大小

    • 存储池可以是不同的RAIDZ级别

    • 存储池也可以使用不同的参数属性

  • 根据快照大小和网络传输速率,第一次 replication 可能需要非常长的时间,特别是复制整个存储池

  • 在完成了首次复制之后,后续的增量数据复制通常会很快

  • zfs send | zfs recv 是基于块级别的复制,并且内置了checksum,所以能够保障数据完整型

  • 建议在启动 Replication 之前先检查目标服务器是否有足够空间容纳发送方数据

数据复制

备份docs数据集
time=`date +%Y-%m-%d_%H:%M:%S`

zfs snapshot zdata/docs@$time

zfs send -v zdata/docs@$time | zfs receive zstore/docs

当使用了 -v 参数会看到同步数据的进度

备份docs数据集
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/jailszdata/vms

  • zdata/jailszdata/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 ,我用来备份到移动硬盘。该答案提供了一个简单脚本,很简单,但是值得参考

一个简单replication复制脚本可参考
#!/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
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

参考