【冬瓜哥论文】原子写,什么鬼?!

 

为什么要原子写?为什么很多场景没有原子写?不原子写的后果以及应对方式?哪些产品支持原子写?...

本次冬瓜哥想为大家介绍一下所谓原子写,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是无能为力的。如下图所示的场景:



可以看到,1时刻内存中的该数据块,其CRC与数据是匹配的,而掉电后硬盘上的状态,CRC与整个数据块是不匹配的。数据库之类对数据一致性要求非常高的程序都会对每个数据块做校验以防止数据位由于各种原因发生bit跃变。但是对于上图最右边的情况,数据库程序是无法判断该块到底是发生了bit翻转,还是由于底层没有原子写而导致的CRC不匹配,校验错误,所以会认为该块是一个坏块。当然,本例中,我们预先知道该坏块其实是由于原子写失败而导致的,但是程序并不知道。其实,此时用redo log强行把4321再覆盖到第一行上,就可以恢复数据,但是数据库并不敢去这么做,已经说了,数据库并不知道该块是不是由于比如第二行或者第三行里某些数据位发生反转而导致的CRC校验错误,所以不能直接把4321再写一遍到第一行上就认为该块被恢复了,为了验证该坏块是否是由于4321未被原子写入所导致,数据库可以先读入该块到内存,然后根据redo log把第一行改为4321,然后再算一遍CRC如果与坏块中的CRC一致了,证明该坏块的确是由于4321被撕裂而导致CRC不一致,此时数据库可以把CRC更正过来然后恢复该块。但是,如果是下面这种场景,数据库就无能为力了:



该例中,第一行被完整更新,但是CRC未被完整更新,导致撕裂。数据库发现redo log中的4321已经被完整更新到了数据块上,但是CRC依然错误,那么此时数据库无法判断到底是因为数据块中其他的数据位发生了翻转出错,还是因为CRC未被原子写,此时数据库无计可施,只能报告坏块。

4.    业界为了避免数据不一致而做的妥协——两次/三次提交

4.1 文件系统日志

如果将要写入硬盘数据文件中的数据/元数据先不往原始文件中写,而是写到硬盘中的一个单独的文件中,这个文件被称为journal/日志(也有人叫log),FS收到下游协议栈返回成功信号之后才向应用返回写入成功信号。(这里又牵扯到如果设备写成功了,而FS在向app返回成功信号时断电,那么app认为没成功,而底层其实已经成功了,此时就需要靠app来决定下一步动作,比如重新再来一遍,或者后续发现其实已经成功了)。这个过程中,原始数据文件是没被写入的,依然保持上一个一致的状态。日志中的数据/元数据在某些时候会写入到硬盘上的原始数据/元数据文件中,比如每隔几秒钟会触发一次针对原始数据/元数据文件的写入,这个过程叫做checkpoint或者commit,如果这期间发生掉电宕机,重启之后FS可以分析日志,将日志中没有commit完成的操作再commit一遍到数据文件中。

有人会有疑问:如果FS在写日志的时候,发生了宕机掉电导致的数据块撕裂怎么办?那么发生撕裂的这笔IO就处于未完成状态,此时该IO的数据会被完全丢弃,以原始数据文件中对应的数据为准,也就是应用再发起针对该IO目标地址的读操作时,FS会从原始数据文件中读出内容,此时读出的便是上一次的一致状态的数据,此时应用可以基于这个数据继续工作,比如选择重新录入新数据。这就是两次提交的好处,第一次先提交到日志文件,一旦IO被撕裂,那么原始数据文件中的数据依然是完好的。那么,如果数据被commit到原始数据文件的过程中,如果一旦发生数据块撕裂,怎么办?这个可以参考上述那两个图中所示的场景,FS只要重放(replay、redo)日志中未完成的操作,重新覆盖一边对应的数据块即可,由于多数FS并不对数据块做校验,所以不会出现上述那个问题。

4.2 MySQL Double Write Buffer

为了解决带校验的数据块撕裂导致的坏块误判问题,MySQL采用了三次提交的方式。第一次先将IO写操作数据提交到redo log日志中(如果IO尚未写入log或者写了一部分尚未commit之前宕机,那么重启之后根据这个断点undo回上一个commit点时的数据),第二次将本应commit到原始数据文件中的数据再写入到硬盘上一个单独的文件中,叫做Double Write Buffer,当commit到DWB的期间一旦发生掉电宕机,那么DWB里的数据就是不一致的,那么重启之后,数据库可以利用redo log+原始数据文件(一定是一致的),来重放/redo,从而将系统恢复到最近的时刻,重放期间依然是先从log写入DWB,再从DWB写入到数据文件(因为如果绕过DWB,直接从redo重放到原始数据文件的话,一旦该过程再宕机,原始数据文件就可能不一致,最后的希望也就没了),如果重启之后又宕机了,就再来一遍,循环。数据文件在被提交到DWB之后,就相当于有了一份备份,数据库再从这个备份中将数据导入到原始数据文件,如果导入过程中出现宕机,没关系,重启后只需要再从DWB的断点重新再次覆盖一下原始文件即可。

ext4文件系统的日志方式中有一个是data=jurnal,其底层就是先将数据和元数据更新都写到日志中,然后再提交到原始数据文件中,这种机制相当于MySQL的DWB了。

4.3 某传统厂商文件系统的做法

该文件系统也采用日志技术,但是为了保证速度,采用带电池保护的RAM来承载日志。在向数据文件commit数据的时候,也采用类似MySQL DWB类似思想,但是形式却不同。其每次都会将更新的数据写入到硬盘上空闲的空间上,并且同时更改映射表指针的指向。每隔10秒钟,或者系统内其他一些功能所触发,该文件系统对数据文件批量提交更新的数据,只不过,该过程并不是把数据拷贝到原始数据文件覆盖,而是把元数据提交,由于元数据的更改每次也都是写入空闲空间,所以元数据的提交无非就是最终将根指针的指向做一次跳转而已。一旦在这个过程中任何一处发生宕机,那么重启之后其可以利用日志重放之前的变更。由于其对每个数据块也做了checksum校验,所以其也会存在块撕裂导致的坏块问题。与DWB机制一样,其每次将数据写入空闲空间,上一次commit之后的数据文件并没有被更改,所以一旦遇到坏块,那么其可以利用日志+上一次的原始数据文件来进行重放,如果在这个过程中又出现问题,或者各种bug或者未知原因导致的重放失败,那么至少上一次成功commit之后的数据是可用的,其可以回滚到上一个状态,虽然有丢数据,但是至少可以保证数据一致。

据不可靠消息,Oracle并没有像MySQL一样对数据块采用三次提交的办法,而是数据直接由内存持续的写入硬盘中的数据文件,此时,存在一定几率由底层无法原子写而导致的块撕裂而无法使用redo log恢复,从而出现各种级别的错误,严重者甚至整个库无法被拉起来。
5.    如何在外部存储设备上实现原子写
如果能够在外部设备中保证IO的原子写,那么诸如MySQL的DWB就可以不要了,会节省开销提升IO性能。如果在硬盘中可以实现多IO为一组的原子写,那么存储系统控制器里为了保证一致性而做的复杂机制就可以被简化。单IO原子写的前提是,操作系统内核模块比如块层里的LVM、软Raid等,不能把单笔写IO分割成多笔,如果在这里分割了,外部设备的单IO原子写就失去了意义。然而,根本就无法保证软raid和LVM不分割,这完全取决于条带或块大小以及应用下发的IO大小。另外,Device Driver会向系统上报一个对应设备所能支持的最大IO size,如果应用的IO size大于这个size,Device Driver也会将其分割,所以,应用层必须预先得到这些参数,然后加以配合来实现单IO原子写。

5.1 SAS/SATA硬盘实现单IO原子写

HBA场景:HBA处不适合负责原子写,因为HBA控制器内部要尽量简单和高效。那么当HBA将数据源源不断的用SAS或者SATA链路一帧一帧的传送给硬盘时,硬盘仅当将一笔IO的所有数据都接收到其内部缓冲之后,才发起写盘操作。而仅仅如此的话也并不能保证原子写,硬盘必须在内部采取日志方式,将该IO先写入硬盘上保留的日志区,日志成功写入后,再向HBA控制器发送cmd complement帧,同时才能开始向数据区写入数据,同时HBA再向host端的competition queue入队io完成描述。一旦上述过程宕机,硬盘重启后可以用日志redo。如果IO之前尚未完整的写入日志,则硬盘实际数据区的内容也依然完好,硬盘只需要从日志中删除该笔IO的不完整数据即可,就当该IO没发生过。这就是实现原子写的代价,一定是降低了性能。有一个要注意的地方是,采用了日志方式之后,要保证IO的时序一致性,比如有一笔IO已经被成功commit到日志,那么后续如果收到针对有重叠的目标地址的读IO,硬盘要返回已经提交的数据而不是从盘片上读出数据返回,这会增加计算量。

Raid卡场景: Raid卡是个带内虚拟化设备,适合实现原子写,由于其具有天然的优势——有超级电容的保护。Raid卡只需要保证将一笔IO数据完全DMA到其内部的缓冲器中,并将对应的缓冲器page置为dirty,之后,便可以向host端完成队列入队该io已完成的描述了。这便实现了原子写。

5.2 PCIE接口的固态盘实现单IO原子写

如果打开了wb模式的写缓存,固态盘必须将一笔写IO的全部数据都收到自己的缓冲内部之后,然后即可向host端完成队列入队io完成应答。如果没有打开写缓存,那么仅当该笔IO全部内容都被commit到flash之后,固态盘固件才会向主机应答。一旦上述过程中发生宕机,那么固态盘此时有两种方式来保证IO的原子性。

方式1:redo模式。当打开wb模式缓存时,该IO完整数据如果已经进入了缓存并且应答,那么固态盘要保证该IO在掉电之后,依靠电容将其数据写入flash,也就是掉电后必须redo。如果没有电容或者电容容量太低,那么就得将该IO先写入flash的日志区之后,再向主机应答,重启后redo日志。

方式2:undo模式。如果不打开写缓存,固态盘控制器从host端主存源源不断的DMA数据到内部的容量非常有限的缓冲区,只要缓冲满一个页面,控制器就开始向后端flash写入。那么,根据上文所述,会产生不一致。但是,此时可以利用SSD内部机制的一个天然优势,那就是,Redirect on Write机制,提到这个机制,大家可以想到上文中介绍的那个每次都重定向写的文件系统的机制,也是RoW。上一次的旧数据并不被原地覆盖,所以可以完好的保留。所以,SSD可以不等整个IO的数据都到缓冲就可以先让数据写到Flash,写成功之后,再更新地址映射表,正如那个特殊文件系统的做法一样,一旦数据写入过程中发生宕机,那么下次重启,由于地址映射表尚未更新,所以该目标地址自然就会被指向之前的旧数据,达到了“要么全写入要么不写入”的原子写效果。这种方式可以认为是出错即undo的方式,与数据库里的undo机制不同,由于SSD内部天然的RoW,不需要像数据库一样采用CoW方式将旧数据拷贝出来形成回滚段。

5.3 实现多IO为一组的原子写

要实现多个IO要么一起全写入要么一起都不写入的效果,就必须增加对应的软件描述接口,以便让上游程序告诉下游部件“哪几个IO需要实现原子性”,必须这样,别无他法。

方式1:带外方式。比如增加一种通知机制,单独描述给下游部件,比如“事务开始,后续发送的32个IO为一组原子IO。带外方式的缺点在于,IO必须连续,期间不能被乱入其他非该事务的IO,而优点在于能够节省开销;

方式2:带内方式。带内方式则是将该信息直接嵌入到每个IO上,比如第一个IO中嵌入“事务开始,事务ID=1,共32个IO,此为第一个”,期间可以被乱入其他非事务性IO或者其他事务ID的IO。该事务的最后一个IO(第32个)中会携带“事务结束,ID=1,共32个IO,此为第32个”的信息。带内方式优点在于灵活,可以乱入其他事务或者非事务的IO,以供底层更充分的重排优化,缺点在于开销大,每笔IO都需要携带对应信息。

由于多IO原子写无法透明实现,需要修改应用、内核以及外部硬件固件,生态关系协调困难,认知度低,场景少,基本上可以靠数据库自身的日志机制解决,所以目前实际产品中只有极少数采用私有访问协议的产品实现了,而实现方式是上面的哪一种冬瓜哥也不清楚。

然而,理想虽好,由于实现方式复杂,所以一般产品都选择不支持原子写。即便支持,也基本上是上述的出错即undo的方式,这样能降低实现复杂度,同时不增加IO的时延,因为IO流程与非原子写状态下是一样的,采用wormhle数据传输方式,只是写成功后才更新映射表,写不成功没关系,回滚到旧数据块上。而缓存模式+redo模式下,会增加IO时延,因为必须等IO数据全部DMA到缓存中之后才对主机端应答,相当于stor-forwarding数据传输方式。
6.    宝存Direct-IO系列PCIE闪存卡/盘支持单IO原子写
目前,宝存科技的固态存储产品共有两个产品系列:采用私有访问协议的Direct-IO系列,包括标准PCIE闪存卡及U.2 PCIE接口闪存盘;采用标准访问协议的Hyper-IO系列,包括企业级SATA SSD及将在今年Q3发布的U.2 PCIE接口NVMe SSD。

其中,Direct-IO PCIe闪存卡系列拥有最高的性能,采用Host Based FTL,私有指令协议,有标准PCIe接口闪存卡形态和U.2(SFF8639)两种形态,最大容量6.4TB。“Direct”指的就是跳过第三方控制器如嵌入式CPU(采用Host Based FTL)以及采用更加精简高效的私有指令以获得更低的时延性能。该产品系列定位在对性能要求极高的互联网后端系统以及传统行业中诸如快速大数据分析等场景。



也就是在该系列产品上,支持小于32KB 的单笔IO的原子写操作。具体,使用如下命令开启原子写:

shannon-detach /dev/scta
shannon-format -a 1 /dev/scta
shannon-attach /dev/scta
注意,以上命令会擦除整个盘。由于宝存DIRECT-IO卡是会自动识别冷热数据,并存储在不同的位置,在原子写模式下,为了保证一个IO的原子性,关闭了冷热数据识别,所以卡上存储的数据格式发生了变化,重新格式化。所以,要打开原子写的话,部署之初就得开启。

另外,如果想要保证端到端的IO原子性,IO必须满足下列条件才可以实现端到端的原子写:

        1. 在盘上的地址4K对齐,长度为4K的整数倍,且Size ≤ 32KB。主要原因是为了与Flash的Page边界对齐,以便充分降低复杂度保证性能。

        2. 必须为DIRECT-IO写。因为如果用了Buffer-IO,内核可能将一笔IO分割为多笔4K,导致底层无法原子写。

        3. 如果用文件系统,只能用XFS或者是开启了bigalloc的ext4。原因是,文件系统分配空间时如果按照4K的粒度分配,可能会导致应用发出的一笔IO所包含的数据被存储在多个位置,也会影响原子性。XFS和开启了bigalloc的 ext4可以按照更大的粒度(比如64KB)分配空间。

如果使能了原子写,则宝存Direct-IO系列SSD内部针对符合上述第1项要求的IO采取WB缓存+redo的模式。保证了任何已经应答给主机的IO一定已经完全被读入了内部缓冲区,并且在收到掉电信号之后在服务器电源的残余电量的支撑保证该IO写入Flash。
7.    其他应该掌握的关键信息
NVMe支持乱序执行乱序完成

与NCQ SATA / TCQ SCSI协议类似,NVMe协议支持乱序执行和乱序完成。

地址重叠的指令必须原子化执行

NVMe SSD可以批量从host端单个send queue中取回多笔io描述结构,以及同时从host端的多个send queue中取回多个io描述结构,内部并发执行。但是对于地址有重叠的io,比如同时拿到一笔针对某个块的写io,和读io,那么就会产生相关性。

针对同一个或者重叠的目标地址范围的IO,主机端程序不应该在上一笔写IO未应答之前再发起一笔读或者写IO,这应该算是host端程序的bug。但是从“道义”上讲,SSD依然要接受这种场景,并且保证IO的原子性,不能撕裂,但是可以不保证IO的顺序。比如以下发送了两笔写,SSD有可能把后来的写先写下去,而先来的写后写下去,这个行为host端程序需要知悉和接受。但是SSD不可以把先来的IO的部分数据写入该块,后来IO的部分数据写入该块,也就是撕裂了该块,这样是不行的,必须确保重叠目标地址区域的原子性。

NVMe SSD内部可能存在多个线程来并行处理所有IO,由于可以乱序执行乱序完成,上述的针对地址重叠IO的原子性执行,就需要在多个线程之间同步,一个有效的办法是维护一个Range Lock,执行任何IO时,先到lock中将该目标地址段加锁,以防止后续IO的乱入,该IO执行完后,解锁。

但是如果IO Size过大,实现原子性的代价就会增加,因为会对内部buffer的占用比例增加以及锁定的粒度增加,一般外部设备都会有个最大所支持的原子性IO块大小以保证性能不降低太多。NVMe规范定义了一个设备端的属性,AWUN(详见下文),设备端利用这个字段向host端驱动通告其可以保证多大的数据块不被撕裂,超过这个尺寸便不保证了。该属性不仅是NVMe设备具有,其他协议的存储设备也都有这种属性。

AWUN

Atomic Write Unit Normal。我们上文只介绍了掉电情况下的原子写保障,其实正常非掉电情况下,针对数据块的读写也会有原子性要求。比如线程A向某个数据块中写入数据,同时另一个线程B从该数据块中读出数据。假设数据块为8K大小。假设这两笔IO被SSD批量取到,开始执行。由于执行和完成都是可以乱序的,那么就可能存在下列场景: 外部设备已经将线程A准备好的针对该数据块的0~4K数据写入介质,此时,线程B的IO乱入了,SSD固件执行了线程B的IO请求,读出的则是0~4K区段由线程A刚刚写入的新数据,以及4~8K区段的之前的旧数据,该8K的块被撕裂。想不被撕裂,那么就需要固件做特殊处理,向Range Lock加锁该8K,不管是线程A还是B的读还是写IO先被执行,都加锁,后被执行的读到的一定是先被执行的写IO写下去的数据。也可以做一些优化,当数据还在buffer中的时候,如果有人要读,就从buffer中读而不是Flash读,这就像CPU机器指令执行流水线的前递操作一样,数据直接从CPU流水线后部的寄存器拷贝给下一条指令的前部的寄存器。

请注意一点,虽然8K的块被撕裂,但是站在其中这两个4K块的角度上看的话,每个4K的块却都并没有撕裂。所以,作为设备来讲,它并不知道到底什么IO粒度应该保证不撕裂。其实做到极致灵活的话应该是这样:SSD固件扫描所有的已进入的IO Size,取最大Size,保证当前已进入的IO在这个粒度上不被撕裂。但是如上文所说,IO Size如果太大,则锁定范围太大,阻碍其他IO的执行。所以,设备根据自身实现和设计情况,会给出一个最大可保证的正常不掉电情况下支持的原子IO Size,这就是AWUN了。

故事:某位固态存储系统架构师在与冬瓜哥的一次闲聊中谈到了一个现象:RAW(Read after Write)场景下,也就是发出一笔写IO,然后短时间内立即发送针对同一个目标地址的读IO的话,不少产品的IO时延有很高的几率会达到毫秒级别。冬瓜哥突然想到了,是不是就是因为SSD内部要实现原子写,视不同的AWU,AWU越大,是不是增加的时延也越大?这一点有待考证。

加锁的方式无法保证掉电情况下的原子性。掉电时最大多少Size的数据块会被保证原子性,也有个最大单位,这就是AWUPF。

AWUPF

Atomic Write Unit Power Fail,掉电时设备可支持的最大原子写粒度。该参数最小为1个LBA也就是512Byte,0.5K,最大则是“任意粒度”。设备厂商可以根据自身情况声明任意粒度。

NVMe fused commands

NVMe协议里规范了一种操作叫做fused command,其支持两个连续的command的原子性。host端程序在cmd中标记这两个cmd为fused类型,并指出谁是第一个谁是第二个,入队同一个queue,且要求这两笔IO操作的目标LBA地址段必须是相同的。如果第一个cmd执行出错,那么ssd要自动抛弃第二个cmd;如果第二个cmd执行出错,那么第一个cmd的执行结果如何处理,冬瓜哥不是很清楚,协议中并没有明确描述,如果冬瓜哥来制定协议,一定是把好手,因为会对任何情况的背景、实现、原因描述的事无巨细。要知道一份协议,不同的人看会有不同的理解,描述清楚是很重要的,会减少很多潜在的兼容性问题。

fused command多数情况下用于仲裁加锁,比如读出某个数据块,判断其值是否为“已被锁”,如果不是,将其改为“已被锁”,然后写入,这两个命令可以组成一组fused command,这样固态盘收到之后,便可以原子的执行,之间不会乱入其他IO,也就可以完成多host利用该数据块作为锁的仲裁机制了。在多控全固态闪存中会有很大用处。
【致谢】
1.     该文中关于数据库redo log方面的底层原理得到了路向峰、DBA Minor、DBA Lunar的指点,表示感谢!

2.     该文中的其他一些信息比如主流SSD是否支持原子写,fused cmd,写后读性能问题等信息,是从张泰乐、吴忠杰等处获取的,表示感谢!
【声明】
冬瓜哥并不是数据库方面专家,对应的知识其实也是去找专业DBA取经之后加上自理理解而总结出来的,可能是不准确或者完全错误的,在此也希望广大DBA们对坏块、redo log等底层机制做出更深层次的分析。
【尾部有八卦一则,请往下拖】 
推荐阅读:

【冬瓜哥画PPT】最完整的存储系统接口/协议/连接方式总结

【冬瓜哥画PPT】惊了!原来高端存储架构是这样演进的!

固态盘到底该怎么做Raid

【冬瓜哥画PPT】浅谈闪存控制器架构

【冬瓜哥论文】浅析固态介质在存储系统中的应用方式

你绝对想不到的两种高逼格存储器

【冬瓜哥手绘】大话众核心处理器体系结构

关于SSD元数据及掉电保护的误解

关于闪存FTL的Host Base和Device Based的误解

【八卦一则】
大概8年前,冬瓜哥在一个叫做itpub的论坛(据说那里是数据库大牛集散地)发了个贴,就是要讨论一下这个问题,也就是比如突然断电、突然拔网线之后,数据一致性的问题,这其实是个非常复杂非常高深的问题。结果呢,一帮ID们在根本没有经过大脑思考的前提下,把冬瓜哥淹死在唾沫中,冬瓜哥至今再也没去过那个论坛。现在冬瓜哥知道了,即便是今天,这个问题依然是个很复杂的问题,大部分人其实是说不清楚的,包括冬瓜哥自己在这里也未必讲的清楚。冬瓜哥要向那时候的itpub论坛上某些渣dba致以深切的问候!如果你就是当年那些跪舔数据库厂商,不求甚解不具备深入分析问题的能力徘徊于表面而且自我感觉良好牛逼哄哄不可一世上来就喷的那些人中的一员的话,下面这个表情,冬瓜哥送给你!


    关注 大话存储


微信扫一扫关注公众号

0 个评论

要回复文章请先登录注册