经过大约一个多月的测试,我觉得这个软件应该已经成为所谓的“finished software”了。所以今天把这个软件的详细细节写成文章。
前情回顾
之所以编写这个软件,是因为之前提到有使用磁带归档的需求。我要用磁带存数据,我又不想买硬盘,我还想确保我的数据在未来的一段时间里保证可用,同时我还不想额外花钱,那怎么办呢?只能自己写软件了。
归档方面,我决定使用现成的LTFS,这个格式本身就是开放的,不会将用户绑定到某一个软件上,所以只要有对应的LTFS实现,这一盘磁带可以不挑磁带机、不挑系统地被读取。当然,除了软件,在归档方面还有许多守则和规则需要遵守,比如说定时把磁带拿出来验证,确保你的设备能跟上时代,之类的。
基于这些需求,我编写了一个基于命令行的Kotlin程序:OfflineArchiveManagement
这个软件基于LTFS设计,首先需要把磁带挂载成LTFS系统,然后程序以读取普通文件的方式读取数据并计算哈希,然后将结果保存到数据库中。之后验证的时候可以对于磁带上数据的哈希和数据库中的记录,如果一致则万事大吉。如果不一致则可以通过搜索相同的哈希和文件大小,来寻找其他磁带上的备份。
命令行设计
既然是命令行程序,那我们不妨就从命令行交互入手。按照我的设计,这个软件主要有5个功能,每个功能都是一个独立的命令:
media
:媒体相关的操作,例如遍历磁带上的数据file
:文件相关的操作,例如寻找一个文件status
:系统相关的操作,例如打印出目前系统的状态export
:导出,将数据库中的数据按照Json格式导出到文件中import
:导入,将Json数据从文件导入到数据库中
关于导入导出就不多说了,他们就是单纯的读取和写入数据库表中的数据,没啥技术含量。至于数据库,出于「没有必要勿增实体」的原则,我选用SQLite数据库,默认情况下会将文件存放在用户的根目录下。
数据库中的表只有两种:媒体(t_media
)和媒体上的文件(t_file_on_media
)。
媒体表,显而易见,它存储我们用于保存数据的媒体/媒介,比如磁带。每一个媒体有一个唯一的ID,对应磁带的标签,如果没有标签的话自己编一个写上也行,无外乎是为了在一群一模一样的磁带中好找罢了。此外出于前瞻性考虑,还额外设计了两个信息字段:媒体类型(media_type
)和代(generation
)。比如日后我需要从LTO6升级到LTO7,但同时拥有这两代的磁带,那我便可以通过generation
字段来区分他们。对于软件来说终归都要挂载成LTFS,但对于用户(我)来说,区分一下代数就能简化找磁带的心智负担。当然,这个软件也并非只能使用在LTFS上,普通的硬盘也可以使用,这也就是为什么我要设计媒体类型这个字段。最后,为了便于甄别哪些磁带需要拿出来验证,我设计了一个last_seen
字段用来保存上一次操作这个媒体的时间戳。
文件表则相对复杂,但也比较直观。对于我们,我们使用文件系统上的相对路径来索引。例如磁带挂载的盘符是E盘,那文件E:\something\test.txt
的路径就是something/test.txt
,这里我们使用unix风格来记录。每一个文件使用媒体ID和这个相对路径来索引,这二者构成一个联合主键。其次是两个信息字段:文件大小和SHA3-256。最后是这个文件最后一次被访问的时间戳。
关于哈希,直接写死采用SHA3-256并不是一种好的设计模式,但我考虑到如果为了灵活而引入类似Multihash之类的东西,那必然要在软件中做足兼容:遇到MD5怎么办,遇到SHA1怎么办?诸如此类。虽然从实际来说,我只会使用SHA3-256,并且在这个哈希函数变得不安全之后才会遇到问题,但在编程来说,一个数据库字段可能存储了SHA3-256,也可能存储了来自未来的数据。要成为Finished Software,那自然应当设计全面、考虑妥当。所以为了不给自己找麻烦,我决定在代码里写死,毕竟SHA2-256(也就是常说的SHA-256)是2001年发布的,到现在不也还是很安全的。SHA3系列是2015年发布的,按照现在的预期来看,除非有什么爆炸性的技术突破,否则SHA3高低能用到2038年。倘若之后SHA3变得不在安全了,那就交给未来的我去烦恼吧——如果那时候我已经不再使用这个软件了,那未来的我也不必烦恼了!
关于哈希的安全性,我认为主要取决于使用场景。假如你要对一段消息进行签名,那需要首先获得这段消息的“摘要”,这个就是由哈希函数来生成的。通过摘要来认证一段数据,这就要求哈希函数不能被人为操控,否则给定哈希可以任意捏造原文,那这种认证也就用处不大了。这就是为什么MD5被认为不安全,因为人们已经能够做到给定一个文件和它的MD5,在保证MD5相同的情况下按照人的意愿篡改文件而保持MD5不变。
但是对于不考虑蓄意破坏的情况下,在好多设计中MD5仍然被用于检查数据的完整性和一致性,例如大名鼎鼎的S3协议。在S3协议中,安全传输由TLS(HTTPS)保证,而MD5只是确保客户端和服务端就传输的数据达成一致,确保服务端不会在客户端不知情的情况下将错误的数据存储到服务器上。
至于是否要考虑蓄意破坏的情况,这就看你的产品设计了。我这个软件是给自己存数据的,所以设计上就随性一些:找个最好的,或者找个最快的,这些都无可厚非。倘若你的软件要处理成千上万条敏感或机密的数据,那你最好多花些时间想想各种方案的利弊。
说了这么多,接下来我们主要看看前三个比较有用的命令:
media
这部分的调用方式是oam media <media_id> operation
,可以看出,对于任何一种操作,媒体ID都是必要的。具体的操作有5种:
walk
:遍历给定的路径purge
:清空数据库中有关这个媒体的文件记录,但不删除媒体本身list
:列出数据库中记录的这个媒体上存储的文件记录remove
:删掉这个媒体的记录,同时删除数据库中记录的相关文件信息info
:管理这个媒体的信息字段
我们先从最简单的说:info
,它实际上就是把媒体的信息打印出来:媒体的ID、类型、代数和最后一次见到的时间。如果要设置这些信息,就使用info set <name> <value>
,对于字段的name无外乎两种:type
和gen
,value嘛,就是你要设置的值。
随后是remove和purge,这两个都是删除,purge会删掉数据库中存储在这个媒体上的所有文件记录,但保留这个媒体,相当于在数据库中把这个媒体给清空了;而remove则一并会将这个媒体也删掉。
后面的list也比较简单,类似ls
,当你想看看这个媒体上都存了点啥,可是又不想找出磁带挂载LTFS,这个是一个很方便的功能。虽然LTFS工具本身也有记录索引的功能,但是这个list命令会一并打印出文件的哈希、文件大小和最后一次见到的时间。
下面主要说说walk,这个命令涵盖了添加文件到数据库、验证等功能。它的使用方法如下 oam media walk [<options>] [<path>]...
,这里的path是LTFS实际挂载的盘符,通常来说一盘磁带只对应一个盘符。但为了方便使用,比如从硬盘拷贝数据进去,如何快速确定拷贝进去的数据是正确的呢?可以先使用这个软件将硬盘上的路径walk一遍,这样数据库中就会有这些文件的数据(将媒体id设置成临时的local
),但考虑到我的硬盘都比较小,一盘磁带能存储2TB多的数据,我需要分两三块硬盘来存,所以就设计了多个path,这样可以直接一条命令处理所有文件了。之后数据写入磁带,再通过软件内提供的对比分析功能就能得知文件有没有正确拷贝了。
关于walk时的具体操作,使用option来控制。主要的option有三个,也比较好记:auv
,一听就是老北京。这里的-a
就是append,它会忽略已经有的文件,只将没见过的文件计算哈希并存入数据库,这个在处理新磁带或追加新文件之后比较有用。然后是-v
,将会忽略不认识的文件,只对照数据库计算已知的文件并对比哈希,如果哈希不一致会有提示。最后是-u
,虽然我是将LTFS当成只读来用,但不可避免地有时候会需要修改一些文件,修改之后就得同步更新数据库里的哈希,所以这个选项会计算所有已知文件的哈希,并且将结果存入数据库,通常情况下用不到。比较方便的一点是,这三个选项可以混搭,比如你在数据准备阶段,可以使用-au
来添加新文件,同时更新已有文件的哈希;在处理磁带时也可以使用-av
来做验证的时候,一并添加新的文件。
磁带在读写时具有线性的特质,也就是说从头到尾最好一次就把所有要做的事情都做了,要不然新增文件过一遍磁带,验证文件就要把磁带倒回去,然后再过一遍,这样相当于损耗了两次磁带的寿命。我这个软件在编写的时候就考虑到了这个特性,因此会使用LTFS提供的ltfsattr
工具读取LTFS分区上每个文件的起始块号,保证所有文件操作都是符合磁带的操作性质的。
这样,使用media命令就可以管理媒体上的文件记录了。但要想有用,还得看看file命令。
file
File命令,也显而易见,它是处理和文件记录有关的操作。具体有两种操作:
find
:根据关键词,查找文件名check
:根据哈希和大小对比分析相同文件,检查文件的副本数
考虑到高级查询的设计比较麻烦,所以这里只实现了单个关键词不限大小写的模糊查询,类似于sql里面的%keyword%
这种。如果真的要做复杂的查询,那数据库文件的位置也告诉你了,下载个DBeaver自己写SQL查呗。查询命令会给出符合条件的文件名、所在媒体ID以及大小和哈希,后两个有助于用户判断他们是否是同一文件。
check命令也使用了类似的原理,它将哈希和大小都相同的记录判断成是同一文件,然后找出这个文件出现在哪些媒体上。如果没有任何option的话,他会打印出所有文件的副本数和对应的媒体ID。为了方便使用,我们可以使用-n 3
这个选项让它只打印出副本数小于3个的文件记录。同时也可以用多个-m id
选项来限定这个命令只搜索哪些媒体。比如我们在复制数据时,可以只使用-m local -m type_id
来搜索本地硬盘上的数据和目标磁带上的数据,而不考虑已经存在系统中的数据。
现在我们可以搜索磁带中的文件了,那磁带呢?
status
status命令用于打印整个系统的摘要,比如说系统里有多少条文件记录,有多少个不重复的文件。如果用户乐意的话,通过-l
开关可以让命令打印出系统中所有已知的媒体。不光如此,还可以通过-o 3
选项,来打印出所有超过3个月没见过的媒体(按每月30天计算)。这样有助于用户决定要把哪些磁带拿出来读一遍。
使用场景
上面说完了命令行设计,下面说一说我的使用场景。我的使用场景比较简单,无外乎是新增数据、检查旧数据,和查找我要的数据在哪盘磁带上。
不过这里要先介绍一下有关磁带的寿命问题。与硬盘不同,磁带涉及到更多的物理操作,比如装载和卸载,这个简单的操作需要磁带机的马达和磁带中的导轮发生物理交互,因此磁带机和磁带都有关于装载和卸载次数的规定。比如昆腾的磁带机装载次数是10万次,而昆腾的LTO6磁带则是2万次装载。这个数字像是保质期,你超过了也不一定坏,只是保证你没有超过这个数字时一定可以正常使用。目前我还没有超过这个数字,但这确实提醒我们,磁带并不像硬盘和U盘那样,用的时候就插上,不用的时候就拔下来——这样会显著增加磁带的损耗。
此外,在读写时磁带需要物理地经过磁头,这样对于磁带的物理介质本身也是一种磨损(当然,磁带随软,但对磁头来说也会有磨损),关于磁带的寿命,网上众说纷纭,有的说磁带可以经过200次完全读写,有的说只有100次,也有的说可以读写1万次。反正不管怎么说,读写肯定是会给磁带带来损耗的,而硬盘对于读操作来说几乎没有什么特别大的损耗。这一点也是我们值得注意的。所以接下来的使用场景我也会对应地说一说我的使用习惯对应产生的损耗。
写入新磁带
我的策略是使用三盘磁带存储同一份数据,一盘是二手的HP磁带,一盘是全新的HP磁带,还有一盘是全新的IBM磁带。通过分散厂家和批次来降低磁带损坏的可能性,同时使用一盘二手磁带来降低整体的成本。
写入磁带之前,我会事先在硬盘上准备好数据。我不使用压缩,因此一盘磁带的容量大约在2TB多,我倾向于留出大约100到200GB的空白空间,所以一次写入的数据大约就在2TB上下浮动。对于小文件,比如Minecraft文件夹,我会将它打包成tar,并且在打包之前,使用teracopy之类的工具生成一份哈希清单。使用tar的原因在于一个文件的损坏不会传染到其他文件上。像是7z这种压缩,它是按块来的,一个块损坏可能会带着好多文件一起损坏。使用tar时,即便有文件损坏,只要我解压出来,按照哈希清单计算一下文件的哈希,我就知道哪些文件损坏,哪些没坏了。对于损坏的文件,可以去其他两盘磁带上找。如果同时都坏了,那就没办法了。
准备好数据之后,我会使用如下命令对本地文件先做一次记录:
./oam media local remove
./oam media local walk -a D:\... E:\... F:\...
这里先删掉local这个媒体,然后将分散在D、E和F盘上的数据加入到数据库。
写入磁带的过程我是用TeraCopy,其实其他的软件也可以,反正比Windows Explorer靠谱就行。如果实在没得选,Windows Explorer,或者ltfs自带的ltfscopy也行。写入完成后使用如下命令验证:
./oam media <media_id> walk -a G:\
假设LTFS分区挂载在G盘,这里就会把G盘下的所有文件都计算一遍哈希,然后存到数据库中。目前LTO-6磁带完全读写一次差不多要5个小时。所以一盘磁带基本上一天就过去了。
拷贝完成之后使用如下命令检查:
./oam file check -m local -m <media_id> -n 2
正常情况下应该没有任何东西被打印,因为这些数据在local上有一份,在磁带上有一份。如果不放心,可以用-n 3
来打印出文件检查一下。
当全部拷贝完三盘磁带后,使用类似的命令做最后一次检查:
./oam file check -m local -m id1 -m id2 -m id3 -n 3
如果无事发生,那么就可以删掉local了。
在这个过程中,我们需要格式化一次LTFS,写一次,读一次。所以一套操作下来可能会损耗不到3个读写次数。
验证数据
虽然卖磁带的说LTO磁带可以存储30年,但这并不是一动不动的30年。出于安全考虑,期间应该定期把磁带拿出来读一边,验证上面的内容。并且随着时代更新设备。要不然30年后人家都LTO-18了,你这LTO-6没有机器读的出来,那不就悲催了。
但是就像我先前说的,磁带机和磁带都是有寿命的,每一次使用都是一次磨损。所以我们需要在验证数据和延长寿命之间寻求一种平衡。我目前的策略是每月读取一盘磁带,这样一来每组数据我都可以确保在一个月内至少有一盘数据是完整的,对于每盘磁带来说差不多是每三个月读取一次。假设一个月有4周,那就是有8天周末,假设每天过一盘磁带,那最多我可以有8组磁带作为归档,可以存储16TB的数据,远超过我目前拥有的数据量。
至于需要处理更多数据时怎么办?到时候再说呗。无外乎两种解决方法:再买个磁带机,一次处理两盘磁带;或者多请假,让自己有更多时间来处理他们。如果比较佛系,也可以就固定那么多时间,但是不按照组来了,就按照最后见到的日期轮,这样平均一组数据的确认间隔就会大于一个月,降低了安全性,但也可以接受。
可以使用如下命令列出媒体:
./oam status -l -o 3
这会列出近三个月没操作过的媒体,顺序按照最后所见的时间升序排列,也就是最先出现的已经是最久远的一个。一般来说只要照着前几个结果选就可以了。
验证数据的过程比较简单:
./oam media <media_id> walk -v G:\
验证一次就是完全读取一次,假设一盘磁带每3个月读一次,那假设磁带寿命是100次完全读写,抛去3次写入数据,为读取数据留出10次的寿命,那还剩下87次,这种做法可以持续不到22年(21.75年)。
不过从现实角度来看,再过几年LTO-7就要出了,如果我不跟着升级LTO-7的话,只怕是6代的设备就会越来越难找,所以21年的操作空间还是很充裕的。
如果验证的时候发现有数据坏了,那么可以看看这盘磁带的寿命,如果磁带寿命不容乐观(比如那盘二手磁带),那就要考虑换新了。这时候就要找出其他的磁带,把数据复制出来,在硬盘上验证一次,然后写入新磁带。
查询并取出文件
当我需要查询文件的时候,可以直接使用find命令:
> ./oam file find lupin
AA0001:[Kamigami] Lupin III the castle of cagliostro [BD x264 1080p FLAC Sub(Chs,Cht,Jap)].mkv (10180722386)
last seen: Fri Dec 01 11:54:29 CST 2023
sha3-256: 65B101A284BAF455A438BFBDED4D010EA022192FFDF75C1F0DC3FCCF6F8AE490
KJ1913:[Kamigami] Lupin III the castle of cagliostro [BD x264 1080p FLAC Sub(Chs,Cht,Jap)].mkv (10180722386)
last seen: Thu Jan 04 20:01:56 CST 2024
sha3-256: 65B101A284BAF455A438BFBDED4D010EA022192FFDF75C1F0DC3FCCF6F8AE490
AA0002:[Kamigami] Lupin III the castle of cagliostro [BD x264 1080p FLAC Sub(Chs,Cht,Jap)].mkv (10180722386)
last seen: Sat Dec 02 15:30:16 CST 2023
sha3-256: 65B101A284BAF455A438BFBDED4D010EA022192FFDF75C1F0DC3FCCF6F8AE490
>
这里我搜索的关键词是lupin
,它就展示出了宫崎骏的鲁邦三世的电影。这时我可以选一盘磁带把数据读出来。也可以等下次验证的时候一并取出。
至于更复杂的查询,比如要同时满足多个关键词,这个可以配合findstr
或者grep
来实现。
后记
相比动辄大几千美元的买断制甚至是订阅制的磁带归档软件,我自己写的这个软件界面不漂亮,功能也不多,但对我来说足够使用,而且也具有足够的前瞻性。从“开源节流”和“自己动手丰衣足食”的角度来说,我觉得还是很成功的。开头也说了,这个软件算得上是Finished Software了,之后也不会有太大的功能性更新,可能最多就是日常修修bug,跟着Java的版本提升一下之类的。但考虑到JVM的向下兼容,我认为即便不提供新的编译产物,现有的Release也能够坚持一阵。毕竟现在Java 1.5编译出来的东西还能在Java 17上跑。
人生苦短,我建议所有人都用Java。
-全文完-
【代码札记】LTO磁带归档管理 由 天空 Blond 采用 知识共享 署名 - 非商业性使用 - 相同方式共享 4.0 国际 许可协议进行许可。
本许可协议授权之外的使用权限可以从 https://skyblond.info/about.html 处获得。
checksum可以用xattr的方式直接写在LTFS索引里
如果磁带里数据出问题,会直接读不出来,报medium error