MENU

【代码札记】LTO磁带备份指北

December 6, 2023 • 瞎折腾

上文说了怎么用LTO磁带归档,还说了我要自己写一个归档管理软件。这个软件写的差不多了,还在测试。趁着这个机会说说怎么用LTO磁带备份电脑上的数据。

环境

关于备份,我看了好多备份软件。他们主要分为两种:一种是单机的,在电脑上安装一个程序,插上磁带机就可以使用;另一种则是CS结构,需要把磁带机插在一台服务器上,然后需要备份的客户端向服务器提交数据。从我的需求来看,我自然是不需要一台专用的备份服务器。但单机软件普遍卖的很贵,最便宜的也要一百多欧元。因为沾上了磁带,自然免不了“企业级”。只要沾上了这三个字,那溢价可是大了去了。

本着不买立省100%的原则,我将目光转向了Linux。因为在上一篇谈归档的时候,我就注意到网上很多人在狗叫什么“tar就够了哪用得着什么花里胡哨的归档软件”。对于归档来说,tar肯定是不够用,但是作为备份,也许tar是够用的。但是很明显,tar是一个Linux程序,而我使用的是Windows,脑瘫Razer在Bios里偷工减料,使得我必须关闭安全启动才能启动Linux,但我又不想牺牲安全性,可是磁带机使用的是雷电接口,本质上是个PCIe设备,没办法像USB那样直接透传给虚拟机。面对这样一种天不时地不利人不和的状况,我发现了Cygwin

这个东西我不是第一次用了。早在我不想让微软在我的硬盘上满地拉屎时,我就用这个组件来代替微软的编译器在Windows上编译代码。我在谷歌的海洋中先帆船后潜水时注意到,这个东西竟然能够支持磁带设备!它能够将NT内核的\\,\TAPE0映射到Linux上的/dev/st0/dev/nst0。那话不多说,就是它了。

要在Cygwin上使用磁带机,你可能需要额外安装一个mt包,用于管理磁带机的倒带。此外,使用mt命令让磁带机倒带则需要管理员权限。

一切皆文件

在最开始拿到磁带机的时候,我注意到磁带机是一个SCSI设备,本想着写点代码用Java调用,但却发现好像只有C的兼容性最好,因为SCSI相当于是直接和设备交流。IBM给了一个很长的SCSI编程手册,我想想看,还是不要折磨自己的好。

幸运的是,Linux的设计哲学就是“一切皆文件”。得益于驱动程序的帮助,磁带机在Linux下被抽象为一个字符设备文件。也就是说,我们可以通过类似Java中的输入输出流那样和磁带上的数据交互。如果跳出编程的思维,从使用的角度来看,就是说我们可以将其视作一个普通文件进行操作。

例如我们要将一段文字写到磁带上:

mt rewind
echo "Hello world!" > /dev/st0
cat /dev/st0

最后的cat命令将会打印出我们写到磁带上的Hello world。注意到这里我使用的是/dev/st0,其中st的含义是scsi tape,这个设备在每次操作完磁带后都会进行一个rewind操作,将磁带倒回最开始的地方。而/dev/nst0多了一个n,表示操作完成后不要回转磁带。

因为/dev/st0是一个字符设备文件,它不关心磁带的位置,如果你请求一个,他就开始读。所以在操作磁带前务必使用mt tellmt rewind确认磁带位置。为什么要特意强调这一点呢?我们做个append操作试试看:

mt rewind
echo "Appending string" >> /dev/st0
cat /dev/st0

这个时候cat打印出来的却只有我们的Appending string,前面的部分消失了。按照道理说,如果/dev/st0是一个普通文件的话,>>操作符应该会将后来的字符串续写到原有数据之后才对。但问题在于,/dev/st0并不是一个普通文件,因为读写时磁带都是从前往后走,所以自然而然也就没有续写一说。可是怎么才能续写呢?我们用不会回转的nst0试试看:

mt rewind
echo "Hello world!" > /dev/nst0
echo "Appending string" > /dev/nst0
mt rewind
cat /dev/st0

这个时候cat打印出来的却只有Hello world了,我们后加的数据反而不见了。如果这时候我们执行两次cat /dev/nst0,就会产生这样的效果:

$ cat /dev/nst0
Hello world!

$ cat /dev/nst0
Appending string

看起来我们追加的部分好像并不和第一个字符串在一起。为什么cat在读出Hello world之后就结束了呢?为什么第二个cat就只打印出后来追加的字符串呢?

一开始我也很疑惑这个问题,后来搜索未果,跑去问了ChatGPT。简单总结一下就是,磁带机对应的设备文件并不只是单纯的设备文件。我们使用重定向、dd、tar之类的命令读写它时,实际上对应的系统调用是read和write,当然,使用前后还要open和close。但这种设备文件和普通文件不同,设备文件的背后是驱动程序。驱动程序在访问过程中充当一个重要的翻译,他将你的读写调用翻译成磁带机能够理解的scsi命令,并将scsi命令的响应翻译成读写调用的结果。至于mt命令也通过这个设备文件管理倒带,这个是使用了ioctl这个特殊的调用。而作为设备文件读写时,驱动程序承担起了额外的作用,例如在写入数据前插入文件标记、在写入后插入EOF标记,这些标记对于应用程序和用户来说是透明的,但对于驱动程序和磁带机来说却是辅助其运行的重要部分。例如通过mt fsf命令就可以在不同的数据块之前切换:

mt rewind
mt fsf 1
cat /dev/nst0

通过mt fsf 1,我们从磁带开头向后跳跃一个文件,刚好来到我们后来插入的Appending String这里。果不其然,cat命令打印出来的也是我们后来追加的字符串。

可是问题来了,如果我确实要追加字符串,应该怎么办呢?我们看看mt命令的文档:

This manual page documents the tape control program mt. mt performs the given operation, which must be one of the tape operations listed below, on a tape drive. The commands can also be listed by running the program with the -h option. The version of mt is printed with the -v or --version option. The path of the tape device on which to operate can be given with the -f or -t option. If neither of those options is given, and the environment variable TAPE is set, it is used. Otherwise, a default device defined in the file /usr/include/sys/mtio.h is used.

Some operations optionally take an argument or repeat count, which can be given after the operation name and defaults to 1. The postfix k , M , or G can be used to give counts in units of 1024, 1024 1024, or 1024 1024 * 1024, respectively.

The available operations are listed below. Unique abbreviations are accepted. Not all operations are available on all systems, or work on all types of tape drives.

  • fsf

    Forward space count files. The tape is positioned on the first block of the next file.

  • ...
  • bsf

    Backward space count files. The tape is positioned on the last block of the previous file.

  • ...

使用bsf操作,刚好可以回到前一个文件的结尾:

mt rewind
mt fsf 1
mt bsf 1
echo "Now we're talking" > /dev/nst0

这时我们直接在上一段数据的结尾写入新的字符串。随后rewind之后cat:

$ cat /dev/st0
Hello world!
Now we're talking

这里可以看到,我们的数据成功地追加到了上一段数据的后面,即便我们用的是截断的>操作符。如果这个时候我们再mt fsf 1试试看呢?

$ mt fsf 1
mt: /dev/tape: Input/output error

由于磁带本身的性质,当我们在第一个数据块后面写入“Now we're talking”的时候,原本的Appending String就被覆盖掉了。没有了正确的文件标记,磁带机认为后面没有数据,于是当我们想往后找下一块数据的时候,驱动程序就告诉我们发生了IO错误。

当然了,除了文本数据,我们还可以使用其他数据:

$ mt rewind

$ dd if=【完整熟肉】舞台剧「孤独摇滚!」【8_20千秋楽夜公演】\[中字_汉化\].mp4 of=/dev/st0 bs=4M
记录了403+1 的读入
记录了403+1 的写出
1690491848字节(1.7 GB,1.6 GiB)已复制,31.8105 s,53.1 MB/s

$ dd if=/dev/st0 of=test.mp4 bs=4M
记录了403+1 的读入
记录了403+1 的写出
1690491848字节(1.7 GB,1.6 GiB)已复制,37.4315 s,45.2 MB/s

$ sha256sum *.mp4
ff461ca52619b076243e8bd5bb3da7d075d053e4687c12e85d51d871237b636a *【完整熟肉】舞台剧「孤独摇滚!」【8_20千秋楽夜公演】[ 中字_汉化].mp4
ff461ca52619b076243e8bd5bb3da7d075d053e4687c12e85d51d871237b636a *test.mp4

这里我用了一个MP4文件来做测试,和刚才的字符串类似,可以对/dev/nst0连续使用多个dd来写入不同的文件。不过这有一个问题:写入的只是数据,文件名却没有了。这对于备份来说可是不太好——数据都在,你看着猜吧。

tarball

好在我们不需要重复造轮子:tar就能完美解决这个问题。或者说,几乎任何一种压缩包都可以满足我们这个需求,只不过tar是最简单的一个。

在挑选压缩包格式的时候,第一个问题就是速度。磁带机写入时磁带是以某一个速度旋转的,如果写入的速度不够快,就会导致磁带机走走停停,甚至要倒回来一点再开始写。这种行为对于磁带机的磨损非常大,甚至还有一个专有名词,叫shoe-shining,翻译过来就是擦皮鞋。磁带会像那皮鞋那样反复转来转去,只因为设备提供数据的速度跟不上写入速度,磁带转过去了,可是待写入的数据还没产生。而数据来了,磁带已经转过去了,这个时候磁带机就得先刹车、再倒车,倒回去之后再开始转。如此反复,磁带机可就命不久矣了。虽然现代LTO磁带机能够根据数据流的速度动态调整磁带的转速,但不稳定的速度也会带来磨损。

为了缓解这个问题,首先要从数据源入手。这里的数据源是我们的tar命令,它将文件和对应的信息打包输出成为二进制流,我们可以让tar直接写到/dev/nst0上,也可以通过管道交给dd来写。无论如何,如果我们的tar命令太慢,那磁带机也就只能干等。怎么才能让tar变快呢?最简单的办法就是不用压缩,然后是加大blocking factor。

这里简单说说tar是怎么把文件打包的。Tar格式非常简单,基本上就是一个元数据块(作为头),跟着一个数据块。而Tar的基本单位是一个Record,默认情况下是10240字节一个Record,但为了提高效率,例如我要备份的文件都是几百个GB的大文件,那我们可以通过增大Record来减少整体Record的数量,进而减少冗余数据,同时还能提高tar的读写速度。我们可以通过-b 2048来将Record的大小调整为2048 * 512 = 1MB。但Record作为固定长度的基本单位,如果一个文件的长度小于Record的大小,那么类似于磁盘的簇,这个Record就只能存储这一个文件,由此就引发了空间浪费。

为了消除这种空间浪费,我们可以使用压缩。Tar本身支持各种压缩,但测试发现这些压缩算法普遍比较慢,唯一快的要算是zstd了。但比起软件压缩,我们还有更好的选择:LTO磁带机的硬件压缩。我的LTO 6磁带机自带硬件压缩,所以可以直接让tar输出原始流,然后磁带机去进行压缩。这样一来我们便不必担心CPU的压缩速度跟不上磁带机的消耗速度了。

如果最后还是慢的话,我们就要求助于mbuffer了。这个小工具能够在管道中充当一个蓄水池,我们可以让它在缓存写到一定程度之后再开始向磁带机发送数据:

tar -b 2048 -cvf - path | mbuffer -m 8G -P 95 | dd of=/dev/nst0 bs=4M

这个命令就利用mbuffer在管道中提供了一个8GB的缓存,来自tar的数据会先暂存在缓存中,等到数据量达到缓存容量的95%,才开始向dd命令发送数据。

硬件加密

我之前在归档时说过,如果30年后忘记了磁带的密码,那真是莫大的悲哀。但是备份和归档不一样,备份是一种比较近的数据。按照我的计划,每一个备份做多存续3个月,而且它们原封不动地复制了我电脑上的数据,万一失窃泄露的还是挺多的,所以加密是很有必要的。可是和压缩一样,软件加密是下下策,我们有磁带机的硬件加密。

但问题来了,压缩我们可以用mt compression命令来管理,可是加密好像就不行了。在Linux上有stenc,但cygwin上无法编译。好在有人将其移植到了Windows上,叫做LTOEnc,我们启动一个管理员权限的终端就可以运行它了。

至于密码,将密码存储在磁盘上是一种很不明智的选择,因此我选择在运行时生成。将文本密码过一遍sha256,用对应的32字节哈希值来充当磁带机的密码。

Bash脚本

为了便于操作,我将上述操作编写成了脚本,放在了GitHub上。同时我将管理加密的脚本和负责备份的脚本分开了。在备份之前,先使用apply-lto-key.sh设置好磁带机的加密,然后使用backup-c-and-c.sh备份。

备份的流程是利用tar通过管道输出到dd,因为数据量比较大,所以能看到一个进度条或者统计信息会比较安心。通过使用选项可以让dd命令打印出当前的进度,例如已写入的数据量、写入的速度等:

dd of=/dev/nst0 status=progress

写入完成后再运行clear-lto-key.sh清空磁带机的加密密钥。

之后如果要恢复数据的话,按照同样的流程先设置磁带机的密码,然后使用tar -b 2048 -tvf /dev/nst0即可。关于文件的顺序,可以参考前文。在磁带开头是C盘的备份,如果从磁带开头执行mt fsf 1就能找到D盘的备份。

Cygwin的背刺

本来事情到这里应该就已经结束了。我也觉得我这写了一天bash脚本,也该结束了。没想到,这个时候没有意外的话就该出现意外了。

我注意到我的脚本备份速度总是在60MB/s上下波动。我以为是C盘小文件太多,速度起不来。我为了验证这个想法,在D盘上跑了一下,发现即便是几十GB的文件也是这个速度,遂开始排查。我分别尝试了如下组合:

  • 使用dd命令,if从文件读,of直接写到磁带。能跑满160MB/s
  • 使用dd命令,if从文件都,输出到管道;使用dd命令,从管道读,写到磁带机。只能跑到60MB/s

这样一来就一目了然了:竟然是管道的效率太低。至于tar,在处理单个文件的时候问题不大,可是处理多个文件的时候也会在Cygwin上出现性能问题。

经过查证,我发现最根本的问题在于Windows没法实现Linux的fork调用。这个调用在Linux下是吧一个进程一分为二,一个进程可以继续执行,而另一个进程可以获知自己是分出来的进程,进而可以执行一些别的任务。但是在Windows中,进程就是进程,如果想要一分为二就只能再从头创建一个。这就导致Cygwin在模拟POSIX的fork调用时进行了许多麻烦的模拟,导致所有使用fork的调用都会被拖慢。

但是磁带机是SCSI设备,它不能像USB那样透传到虚拟机里。我的雷蛇笔记本在bios上偷工减料,我5年前的HP都能无痛安全启动openSUSE,到了雷蛇这里却只认微软的证书。用不了Linux,现在Windows上的兼容层性能又不行,那能怎么办呢?

Powershell,启动!

那当然是转会纯Windows解决方案啦!不过这个方案只能算是下下策吧。原本我是想玩一下Linux下的磁带的,但是鉴于目前形势所迫,纯Windows的解决方案由于没有tar、dd、mt、字符设备文件这些东西,直接往上写数据肯定是不可能了。那就只能换一种思路。

LTFS一直是个好东西,既然直接写数据不可能了,那我就用LTFS好了。但是LTFS有个问题:它只增不删。即便你删掉了上面的文件,实际的数据也还是留在磁带上的。当你继续写入文件时,并非从开头开始写,而是接着旧数据往后写。所以用不了几次,一盘LTFS磁带就写满了,即便逻辑上什么也没有。

不过办法还是有的。HPE的StoreOpen里面自带了一个mkltfs的工具,我可以使用命令行格式化一盘磁带。并且这个操作是幂等的,也就是说我可以用它格式化一盘普通磁带,也可以直接再次格式化LTFS磁带,从而起到删掉数据的作用。不过挂载就没有那么幸运了:纯命令行的挂载是做不到的,需要使用GUI挂载。不过这倒也问题不大,因为GUI允许你单独挂载一个磁带机。类似于没有光碟的光驱那样,你可以现挂载驱动器,再用命令行处理磁带。

至于加密,经过测试,我可以通过LTOEnc启用加密(mixed模式),然后再挂载LTFS驱动器,这样之后无论是格式化还是读写就全都是加密数据了。至于压缩文件的格式,我是用命令行版的7z,也就是7za,通过-ttar选项来创建tar归档。整体和Linux差不多,只是从EOF变成了更加复杂的LTFS。不过不一样的部分则是这次我们不会再遇到性能瓶颈了。

我把这部分脚本也放到了GitHub上,欢迎参考。

后记

这次是真的结束了。顺便说一个小插曲,最后在收尾的时候,我明明记得7za是有一个进度条的,怎么到了我的脚本里突然就没了?我把脚本里面的变量复制到控制台运行,有输出;进到脚本运行就没输出。百思不得其解。最后无意中发现,我用来计时的Measure-Command竟然默认会吞掉stdout输出,开发人员认为这会影响被计时命令的性能。彳亍口巴。

总之这次是真的好了,在尽最大努力不自己写软件的情况下。

-全文完-


知识共享许可协议
【代码札记】LTO磁带备份指北天空 Blond 采用 知识共享 署名 - 非商业性使用 - 相同方式共享 4.0 国际 许可协议进行许可。
本许可协议授权之外的使用权限可以从 https://skyblond.info/about.html 处获得。

Archives QR Code
QR Code for this page
Tipping QR Code
Leave a Comment

已有 1 条评论
  1. 哈哈,你们猜怎么着?我又换回cygwin辣!因为Windows上的7zip用7z格式打包时速度很快,还带有crc,但是因为最后要更新header,就导致ltfs要把整个文件重新复制一遍再做修改。而使用7z打包tar的时候,因为改变不了record大小,所以只能跑到20MB/s。最后我左思右想,Cygwin上使用管道传给dd主要是想看一个速度的统计数据,如果我直接让tar写到磁带机,是没有任何性能问题的(除了小文件)。但是作为牺牲,我只能给tar加上`-v`选项让它打印出正在处理的文件名。这对于大文件来说问题不大,可是在处理众多小文件时,控制台的打印速度会拖慢tar的处理速度,但就这也比Windows上的7z打包tar要快!

    垃圾Windows,呸!!!