在 2023年 9月1日 UTC时间的 10:01 分,Aiursoft 发生了一次重大事故。

所有服务全部中断,并且损失了一些数据。当天可用性严重降级。

我们花费了大约4个小时,尝试理清这一切发生了什么。下面是详细过程。

背景

Aiursoft 的基础设施运行在一个Vsphere 数据中心中,使用了存储计算分离的设计,由多台 ESXI 进行计算,由一台 NFS 服务器进行存储。

这台 NFS 服务器具有 12 块东芝的 2T 低价机械硬盘,有两块 6TB 戴尔企业级 NVME 固态硬盘。

之前,这个 NFS 服务器的配置方法是:将12块 2T 机械组成 ZFS Pool,并使用那两块企业级固态作为 ZFS 的缓存,从而同时享受机械的容量和固态的性能。

file

但是,很快我们发现,这样搭配的结果,性能比纯固态还是要差一些。

变更

于是,我们考虑到 ZFS 机械硬盘+固态缓存的性能会形成瓶颈😐,因此就决定放弃使用机械硬盘来提供业务,转而将机械硬盘用于纯备份,而将全部数据放在NVME固态中。

经过测试,上述改造可以有效提高所有业务的读写速度,尤其是随机读写速度。

因此在 2023 年 8 月 30 日,我们放弃了使用固态硬盘作为缓存的思路,而是将两块 NVME 固态组合成了一个 RAID 0 的ZFS Pool,并创建了dataset:NVME,挂载到了 /mnt/nvme ,以提供 NFS 存储服务。

很快我们就开始享受纯NVME的性能增益了。确实纯 NVME 固态对性能提升非常明显。尤其是开机、更新、安装系统的速度都显著增益。

file

当然,此时此刻我们还没有配好备份。因此这是非常危险的一个阶段:全部用户数据都被放在了 RAID 0 中!一旦任意一块硬盘损坏,都会损失所有数据,而且无法恢复。

危机

当然我们理解这个阶段的危险性。因此,立刻对 NVME 固态的数据设计备份是一个非常要紧的工作。

我们最开始尝试了 rsync 备份,但是考虑到要备份的文件都在被高速读写,使用 rsync 的效率很低,甚至很难完整复制完一个正在被读写的文件。

sudo rsync -avh --inplace --partial --delete --progress /mnt/nvme/ /mnt/pool/backup/

既然使用了ZFS,我们决定使用 ZFS 的快照功能来备份。ZFS的快照功能非常强大,可以在不停止服务的情况下,对文件系统进行快照,然后将快照发送到其他地方。

因此,我们开发了这样一段备份脚本:

#sudo rsync -avh --inplace --partial --delete --progress /mnt/nvme/ /mnt/pool/backup/
echo "Installing prerequisites..."
sudo apt install -y pv

echo "Deleting existing backups..."
sudo zfs destroy pool/backup@backup
sudo zfs destroy nvme/data@backup

echo "Creating snapshot..."
sudo zfs snapshot -r nvme/data@backup

echo "Sending snapshot from nvme/data to pool/backup..."
sudo zfs send -vR nvme/data@backup | pv | sudo zfs receive -vF pool/backup

echo "Deleting data backup"
sudo zfs destroy nvme/data@backup

上述脚本的作用是对 /mnt/nvme/data 进行快照,然后将快照发送到 /mnt/pool/backup 中。

理论上它非常安全,因为它不会修改生产环境的数据,并且会在发送完毕后删除在NVME上的快照以释放空间。发送快照的目标是一个独立的ZFS Pool,因此即使发送失败,也不会影响生产环境。

考虑到它的安全性,这段脚本在开发完成后,我们没有在任何试验环境中测试,而是直接在生产环境中运行。当然,实践证明这个决定是非常错误的。

事故

在 2023 年 9 月 1 日,我们在 10:01 UTC 时间收到了一条警报,说业务已经停止。

我此时此刻正在 NFS 上检查性能信息,因此我正好开始了调查。

出于对 Vsphere 数据中心的调查流程,我立刻检查了 Vsphere 的状态,此时发现所有 VM 已经全部停止,并且都发生了错误,处于 Orphan 状态。

我试图通过 VCenter 重启所有 VM,但是发现所有 VM 都无法启动,因为它们的磁盘都无法找到。

我试图在 ESXI 上访问 NFS,发现 NFS 无法访问。此时可以判断:NFS 服务器已经停止。

于是我立刻登录 NFS,并且在 NFS 上运行了下列命令:

(这部分是在事故发生后真实运行的源命令归档)

anduin@NFS:~$ zpool list
NAME   SIZE  ALLOC   FREE  CKPOINT  EXPANDSZ   FRAG    CAP  DEDUP    HEALTH  ALTROOT
nvme  11.6T  4.28T  7.35T        -         -     3%    36%  1.00x    ONLINE  -
pool  21.8T  5.02T  16.8T        -         -     2%    23%  1.00x    ONLINE  -

anduin@NFS:~$ zpool status
  pool: nvme
 state: ONLINE
  scan: scrub repaired 0B in 00:22:16 with 0 errors on Fri Sep  1 08:55:55 2023
config:

	NAME                                                        STATE     READ WRITE CKSUM
	nvme                                                        ONLINE       0     0     0
	  nvme-Dell_Express_Flash_PM1725b_6.4TB_AIC_S5C112  ONLINE       0     0     0
	  nvme-Dell_Express_Flash_PM1725b_6.4TB_AIC_S5C111  ONLINE       0     0     0

errors: No known data errors

  pool: pool
 state: ONLINE
  scan: scrub in progress since Fri Sep  1 08:35:37 2023
	3.41T scanned at 1.22G/s, 110G issued at 39.4M/s, 4.05T total
	0B repaired, 2.66% done, 1 days 05:09:52 to go
config:

	NAME                               STATE     READ WRITE CKSUM
	pool                               ONLINE       0     0     0
	  raidz2-0                         ONLINE       0     0     0
	    ata-TOSHIBA_HDWD120_33GU8WUAS  ONLINE       0     0     0
	    ata-TOSHIBA_HDWD120_33GV7R9AS  ONLINE       0     0     0
	    ata-TOSHIBA_HDWD120_33GVJ01GS  ONLINE       0     0     0
	    ata-TOSHIBA_HDWD120_33UW1T0AS  ONLINE       0     0     0
	    ata-TOSHIBA_HDWD120_52QPEN1AS  ONLINE       0     0     0
	    ata-TOSHIBA_HDWD120_52VPEEUAS  ONLINE       0     0     0
	    ata-TOSHIBA_HDWD120_62BPNH6AS  ONLINE       0     0     0
	    ata-TOSHIBA_HDWD120_62DPLXKAS  ONLINE       0     0     0
	    ata-TOSHIBA_HDWD120_62DPLY6AS  ONLINE       0     0     0
	    ata-TOSHIBA_HDWD120_62DPS2ZAS  ONLINE       0     0     0
	    ata-TOSHIBA_HDWD120_821NVTHAS  ONLINE       0     0     0
	    ata-TOSHIBA_HDWD120_821P90RAS  ONLINE       0     0     0

errors: No known data errors

anduin@NFS:~$ zfs list -t snapshot
NAME               USED  AVAIL     REFER  MOUNTPOINT
nvme/data@backup  11.0G      -     4.27T  -

anduin@NFS:~$ tmux ls
no server running on /tmp/tmux-1000/default

anduin@NFS:~$ tmux a
no sessions

anduin@NFS:~$ zfs list -t snapshot
NAME                 USED  AVAIL     REFER  MOUNTPOINT
pool/backup@backup     0B      -     4.32T  -

上面的信息显示,NFS 服务器的两块 NVME 固态都在线,机械硬盘也在线。我的备份脚本是使用 tmux 来运行的,因此我尝试使用 tmux ls 来查看 tmux 的状态,但是发现 tmux 服务已经停止。

但是诡异的事情是:两次我运行 zfs list -t snapshot 的结果竟然是不一样的!第一次运行时,显示了一个名为 nvme/data@backup 的快照,但是第二次运行时,快照就消失了!并且出现了:pool/backup@backup快照。

这个时候我认为我应该立刻去检查数据还在不在。因为现在是特殊情况:我的数据只有一份,并且使用了 RAID 0,一旦发生数据丢失后果将是灾难性的。

于是我立刻尝试访问 NFS 上的数据:

anduin@NFS:~$ cd /mnt/backup/
anduin@NFS:/mnt/backup$ ls
anduin@NFS:/mnt/backup$ cd /mnt/nvme/
anduin@NFS:/mnt/nvme$ cd /mnt/nvme/
anduin@NFS:/mnt/nvme$ ls
 Aiursoft     Books      Dvorak     Gbiner   GPT        Jimmoen   Live        Monitoring   NugetNinja   Rdf        Runner1      Shubuzuo

幸运的是:数据还在。

此时我也注意:虽然 /mnt/nvme 也就是生产的 NFS 路径有数据,但是 /mnt/backup 是没有数据的。不过并不奇怪这件事,因为备份脚本的作用只是发送一个快照,而不是将数据复制到另一个地方。

既然数据还在,为什么 NFS 服务器会停止呢?

于是我试着探究更多线索:

anduin@NFS:~$ zfs list
NAME          USED  AVAIL     REFER  MOUNTPOINT
nvme         4.27T  6.99T       96K  none
nvme/data    4.27T  6.99T     4.27T  /mnt/nvme
pool         4.36T  11.7T      219K  none
pool/backup  4.32T  11.7T     4.32T  /mnt/nvme
pool/data    48.1G  11.7T     48.1G  /mnt/pool

在上面的输出中,我惊讶的发现,有两个 zfs dataset 的 MOUNTPOINT 竟然是同一个目录!这是不可能的,因为 ZFS 不允许两个 dataset 挂载到同一个目录。

这种情况已经远远超过了我的理解范围,因此我尝试将两个 dataset 的 MOUNTPOINT 改为不同的目录:

anduin@NFS:/mnt/nvme$ sudo zfs set mountpoint=/mnt/backup pool/backup
cannot unmount '/mnt/nvme': unmount failed

anduin@NFS:/mnt/nvme$ sudo zfs set mountpoint=/mnt/backup pool/backup
cannot unmount '/mnt/nvme': unmount failed

在这里我浪费了非常长的时间,尝试将两个 dataset 的 MOUNTPOINT 改为不同的目录,但是都失败了。它的错误信息是:无法卸载 /mnt/nvme,因为它是一个忙碌的目录。我认为这个错误是合理的,因为 /mnt/nvme 是 NFS 的挂载点,NFS 是不会自动卸载的。

于是我尝试了重启 NFS 服务:

anduin@NFS:/mnt/nvme$ sudo systemctl stop nfs-kernel-server

然后尝试了再次修改 MOUNTPOINT:

anduin@NFS:/mnt$ sudo zfs set mountpoint=/mnt/backup pool/backup
anduin@NFS:/mnt$ zfs list
NAME          USED  AVAIL     REFER  MOUNTPOINT
nvme         4.27T  6.99T       96K  none
nvme/data    4.27T  6.99T     4.27T  /mnt/nvme
pool         4.36T  11.7T      219K  none
pool/backup  4.32T  11.7T     4.32T  /mnt/backup
pool/data    48.1G  11.7T     48.1G  /mnt/pool

于是我重启了 NFS 服务:

anduin@NFS:/mnt$ sudo systemctl start nfs-kernel-server

然后我尝试访问 NFS 服务器时,问题已经得到了解决。

实际我恢复 Vsphere 数据中心花了非常久,这是由于 Orphan 状态的 VM 无法通过 VCenter 进行管理,因此我需要手动在 ESXI 上进行管理。而且由于 NFS 服务器的数据已经损坏,因此我需要手动删除所有 VM ,然后重新挂载。

另外,由于数据中心开启了 DPM 和 DRS,以及开启了使用 NFS 的 HA,在事故发生时,HA 系统已经认为系统已经损坏,因此不停尝试将 VM 迁移,而 DPM 甚至为了省电对 ESXI 进行了关机。这些都导致了我在恢复数据中心时遇到了非常多的问题。

后面的修复工作大约花费了 3 个小时,最终在 13:00 UTC 时间,我们的数据中心恢复了正常。

原因

虽然我们的数据中心已经恢复了正常,但是仍然需要弄清楚这一切发生了什么。

尤其是最诡异的现象,为什么两次列举快照出现了不同的结果?为什么两个 dataset 的 MOUNTPOINT 竟然是同一个目录?

file

第一个问题,在仔细阅读备份脚本后有了解答:

备份脚本会在 nvme 上创建一个快照,随后将快照复制到另一个pool,然后删除快照。此时 tmux 显示备份已经结束,因此很可能在第一次运行 zfs list -t snapshot 时,脚本的运行还没结束,而是在 sudo zfs send -vR nvme/data@backup | pv | sudo zfs receive -vF pool/backup

#sudo rsync -avh --inplace --partial --delete --progress /mnt/nvme/ /mnt/pool/backup/
echo "Installing prerequisites..."
sudo apt install -y pv

echo "Deleting existing backups..."
sudo zfs destroy pool/backup@backup
sudo zfs destroy nvme/data@backup

echo "Creating snapshot..."
sudo zfs snapshot -r nvme/data@backup

echo "Sending snapshot from nvme/data to pool/backup..."
sudo zfs send -vR nvme/data@backup | pv | sudo zfs receive -vF pool/backup

echo "Deleting data backup"
sudo zfs destroy nvme/data@backup

几乎同时,脚本在问号处运行结束了,因此第二次运行 zfs list -t snapshot 时,nvme 的快照已经被删除了,而 pool 的快照已经被创建了。

而第二个问题,我们很快在试验室里运行上述脚本就得到了完全相同的结论:

file

只要运行上述脚本,一定会导致两个 dataset 的 MOUNTPOINT 是同一个目录。而在正常操作下,ZFS 是不允许两个 dataset 挂载到同一个目录的。

这可能是我们没有正确理解 zfs send 和 receive 的用途。在我们的理解中,zfs send 和 receive 是用来将一个 snapshot 复制到另一个 dataset 的。但是实际上,它很可能会保持 MOUNTPOINT 的一致性,因此,zfs send 和 receive 就会将 MOUNTPOINT 也复制过去,然后导致两个 dataset 的数据重叠。

另外,我们也重新理解了 zfs list:它展示的 MOUNTPOINT 只是一个dataset的metadata,和一个dataset是否被mount无关。因此,判断一个目录是否是ZFS Dataset不应该基于 zfs list,而是应该基于 df -Th。正是因为这个关键失误导致了在调查时出现了障碍。

教训

这次事故给我们带来了非常大的教训。首先,我们应该在试验环境中测试脚本,而不是直接在生产环境中运行。只有在对操作非常有把握,运行的脚本已经广泛测试之后才应当在生产环境执行。

其次,我们应该对脚本的运行结果进行更多的检查和监控。我们将考虑针对crontab、tmux建设更多的基于Prometheus的监控和告警系统。

当然,我们也决定对老的备份脚本进行改造,并且新的脚本在试验室中被反复验证,再在生产服务器上逐行执行。

改进

新的脚本我们重新探讨了备份的思路:

我们备份的目标其实并吧是将快照发送过去,而是我们需要文件在磁盘损坏后仍然可以访问。因此我们决定组合使用snapshot send和rollback。

在rollback后,我们直接mount zfs的dataset即可看到发过去的snapshot中的文件。而此时,snapshot已经不再重要,这里可以直接删除snapshot。(这里不会影响安全性,因为发过去的文件是归档文件,它不会被修改,也不会和snapshot有区别)

当然,为了避免出现上述问题,我们干脆在备份期间将整个dataset unmount掉。而一个目录是否被mount,这并不是ZFS的问题,而是Linux本身的设计哲学:设备会被mount变成目录,已经导致了判断一个目录是否属于一个设备就比Windows困难。在Windows上,盘坏、盘掉都会直接粗暴的以D盘找不到的错误直接展示,而在Linux的世界,这里这需要万分小心。当然,unmount掉然后发送快照自然可以规避这些问题。

最终的备份脚本是:

#sudo rsync -avh --exclude "*.vswp" --inplace --partial --delete --progress /mnt/nvme/ /mnt/pool/backup/

echo "Deleting exising backups..."
sudo zfs destroy pool/backup@backup
sudo zfs destroy nvme/data@backup

echo "Creating snapshot..."
sudo touch /mnt/nvme/$(date +"%Y-%m-%d-%H-%M-%S")
sudo zfs snapshot nvme/data@backup

echo "Unmounting backup..."
sudo zfs unmount pool/backup

echo "Sending snapshot from nvme/data to pool/backup..."
sudo bash -c 'zfs send -v nvme/data@backup | zfs receive -vF pool/backup'

echo "Reseting workspace to snapshot..."
sudo zfs rollback pool/backup@backup
sudo zfs mount pool/backup

echo "Deleting snapshots..."
sudo zfs destroy nvme/data@backup
sudo zfs destroy pool/backup@backup

上述脚本会删除所有快照(这是安全的,因为备份好的文件就在 /mnt/backup 下),然后对数据打一个标签,随后快照,再将整个快照发送到备份pool,随后立刻rollback,再mount,即可让快照里的文件呈现为目录,随后删除快照(这也是安全的,因为备份好的文件也已经在 /mnt/backup 下了)。

实际我们在实验室里测试多次,并且在生产环境备份数次,其工作良好,任何情况下数据至少都有两份:

file

如图所示,绿线是生产的pool,黄线是备份的pool。