本次冬瓜哥想为大家介绍一下所谓原子写,Atomic Write。该技术并不是用原子来写写画画(如配图所示那种),那是纳米物理科学家玩的,咱一般人玩不了这个:)。该文属于一篇逼格甚高的长篇说明文,冬瓜哥认为凭借该说明文,冬瓜哥终于可以拿着它让高中语文老师给打个60分及格作文了!(设计台词:哎呀,老师看不懂啊,老师只会看鸡汤文啊,这都些乱七八糟的?这样,给你负80分,看在你没功劳有苦劳的份上,加80分,最终得分,零分!)
1. 从文件系统删除文件说起
文件删除操作过程比较复杂,如果简化的来讲,可以分为两步:
1. 删除该文件在文件记录表中的条目
2. 将该文件之前所占据的空间对应的块在空间追踪bitmap中将对应的bit置0.
假设该文件的文件名非常短,尺寸也非常小,只有不到4KB,那么,上述这两个动作,就可以分别只对应一个4K的IO(如果文件系统格式化时选择4K的分块大小的话),第一个4K将更新后的记录表覆盖到硬盘对应的区域,第二个IO将更新后的bitmap的这4K部分覆盖下去。仅当这两个IO都结束时,该文件才会彻底被删除。
该是问“如果”和“为什么”的时候了。如果,文件系统将更新记录表这个IO发到了硬盘上并且成功写入,而更新bitmap的IO没有发出、或者发出了但是正在去往硬盘的路上的某处,此时系统突然断电,那会有什么结果?
放在早期的文件系统,再次重启系统之后,会进入FSCK(文件系统一致性检查及修复)阶段,也就是WinXP那个经典的蓝底黄滚动条界面。因为文件系统会维护一个dirty/clean位,在做任何变更操作之后,只要操作完成,该位就被置为clean,那么下次重启就不会进入FSCK过程,而我们上述的例子中,这两笔IO是一组不可分割的“事务(Transaction)”,一笔事务中的所有IO要么都被执行,要么干脆别被执行,结果就是这文件要么完全被删除,要么就不被删除还在那,大不了再删除一次。但是,如果一笔事务中的某个/些IO完成,另一些没完成,比如,记录表中已经看不到这个文件,但是空间占用追踪bitmap中却还记录着该文件之前被占用的空间的话,那么表象上就会看到这样的情况:双击我的电脑进去某个目录,看不到对应的文件,而右键点击硬盘属性,却发现该文件占用的空间并没有被清掉。这就产生了不一致。所以,FSCK此时需要介入,重新扫描全部的记录表,与bitmap中每个块占用与否重新匹配,最后便会将bitmap中应该被回收却没有来得及回收的bit重新回收回来。
所谓原子写,就是指一笔不可分隔开的事务中的所有写IO必须一起结束或者一起回退,就像原子作为化学变化中不可分割的最小单位一样。
2. 单笔写IO会不会被原子写?
上面的场景指出,一笔事务中的多笔IO可能不会被原子写,那么单笔IO总能被原子写了吧?很不幸,也无法被原子写。原因和场景有下面三个:,
2.1 上层一笔IO被分解成多笔IO
上层发出的一笔IO可能会被下层模块分解为多笔IO,这多笔IO执行之间如果断电,无法保证原子性。有多种情况可以导致一笔IO被分解,比如:
A. IO size大于底层设备或者IO通道控制器可接受的最大IO size时,此时会由Device Driver将IO分解之后再发送给Host Driver。
B. 做了Raid,条带深度小于该IO的size,那么raid层会将该IO分解成多个IO。
2.2 外部IO控制器不会主动原子写
那么,当一笔IO(分解之后的或者未分解的,无所谓)请求到了底层,由Host Driver发送给外部IO控制器硬件的时候,外部IO控制器总可以实现原子写了吧?IO控制器硬件总不可能只把这笔IO的一部分发给硬盘执行吧?很不幸,IO控制器的确就是这样做的。比如,假设某笔写IO为32KB大小,IO控制器并不是从主存将这32KB数据都取到控制器内缓冲区才开始向后端硬盘发起IO,而是根据后端SAS链路控制器前端的buffer空闲情况,来决定从Host主存DMA多少数据进去,数据一旦进入该buffer,那么后端SAS链路控制器就会将其封装为SAS帧写到后端硬盘上。这个buffer一般只有几KB大小。所以很有可能一笔主机端的32KB的IO,在断电之前,有部分已经写入硬盘了,而剩余的部分则未被写入。虽然主机端的协议栈、应用都没有收到这笔IO的完成应答,但是硬盘上的数据已经被撕裂了,一半是旧的,一半是新的。
(Adaptec的Raid控制器一般会将整个IO取回到板载DDR RAM,然后将对应的RAM pages设为dirty,然后返回给host写应答(向competition queue中入队一个io完成描述结构体)。也就是说,Adaptec的Raid卡是可以保证单IO原子写的,但有个前提是Cache未满,当Cache满或者某种原因被disable比如电容故障等的时候,就无法实现原子写了。至于其他的卡是否保证,冬瓜哥并不清楚。)
2.3 硬盘也不会主动原子写
硬盘本身并不会原子写。硬盘接收到的数据也是一份一份的,每个SAS帧是1KB的Payload,SAS HBA会分多次将一笔IO发送给硬盘。至于硬盘是否会将这笔IO的所有数据都接收到才往盘片上写入,冬瓜哥不是硬盘厂商的研发所以并不知晓,但是冬瓜哥知道的是,不管硬盘是攒足了再写还是收到一个分片就写,其内部的磁头控制电路前端一定也是有一定buffer的,该buffer被充满就写一次。不管怎么样,当磁头在盘片上划过将数据写入盘片期间,突然断电之后,盘片上的数据几乎一定是一部分新一部分旧的,不一致,甚至一个扇区内部都有可能被撕裂。纵使Host端的确会认为该IO未完成,但是木已成半舟。
Every Enterprise SCSI drive provides 64k powerfail write-atomicity. We depend upon it and can silently corrupt data without it.
对于PCIE接口的固态盘,情形也是一样的。SSD从主存DMA时一般每次DMA 512Byte,也就是PCIE Payload的普遍尺寸。当攒足了一个Page的数据时,SSD就开始写入Flash了,而并不是等整个IO数据全部DMA过来才写入Flash。但是仅当整个IO都写入完毕之后,才会向host端competition queue写入io完成描述结构。如果是打开了write back模式的写缓存,那么仅当整个io数据全部DMA到写缓存中之后才会返回io完成描述ack,但是掉电之后,不管是完整取回的还是部分取回的,未完成的io会不会由固态盘固件继续完成,就取决于固件的实现了。
3. IO未完成,再来一遍不就行了么?
有人说了,既然Host端知道某笔IO未完成,那么重启之后,对应的应用完全可以再重新发送这笔IO吧,重新把之前写了一部分的数据全部再写一遍不就行了么?这个问题很复杂,要分很多场景。
比如,Host未宕机,而是存储系统突然宕机,或者突然承载存储IO的网线断掉。此时应用程序会收到IO错误,取决于应用程序如何处理,结果可能不同。比如应用程序层可能会保存有缓冲,在这里实现原子写,比如应用可以在GUI弹出一个重试按钮,当外部IO系统恢复之后,用户点击重试之后,应用会将该原子Transaction涉及的所有IO再次重新执行一遍,此时便可以覆盖之前不一致的数据为一致的。而如果外部存储系统长时间不能恢复,而应用程序也被重启或者强行关闭的话,那么该Transaction未完成,而且在硬盘上留下不一致的数据。当应用再次启动的时候,取决于应用处理方式的不同,结果也不同。
比如应用完全依靠其操作员来决定该如何处理,比如如果是数据库录入,录入员上一笔录入失败,那么其势必再次录入,此时应用可以将录入员再次录入的数据覆盖之前不一致的数据。但是更多实际场景未必如此,比如,录入员可能并不是根本不管其要录入的记录之前是什么而直接录入新数据,而是必须参考之前的数据来决定新数据,而之前的数据已经不完整,或者录入员并不知道该数据是错误的,而在错误数据的基础上计算出了更加错误的新数据,从而将更加错误的数据更新到硬盘上,埋了一颗雷,这就是所谓数据的”连环污染“。
再比如数据库类的程序,其虽然记录了redo log用于追踪所有的变更操作,但是一旦某个数据块发生不一致,redo log是无能为力的。如下图所示的场景: