TA的每日心情 | 开心 2019-11-20 15:00 |
---|
签到天数: 2 天 [LV.1]初来乍到
|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
Atomic write, 揭开你的面纱
, f$ F, B& R: b$ R最早听到Atomic write这个功能是从Fusion-io的DFS文件系统对于数据库的性能优化上。从字面意义上来说,它的意思是IO写入操作相对于其他操作来说,是一个单步的动作。实际上,对于一个IO操作来说,在SSD内部的实现是一个复杂的过程,需要牵涉到很多步骤,因此通常的IO操作并不是一个单步的动作。为了支持原子写,SSD需要通过一个特殊的设计,保证从用户的角度来看,好像这个动作是单步的。
! l0 A4 g0 t- p$ v为了说明这个问题,先看一个多线程程序设计中常见的多线程同步问题。在这个例子中,设置了32个线程对同一个变量进行设置。
. { O8 H" S6 R
5 R1 t$ O0 t) Iunsigned int bitmap = 0;
6 n$ Y; {, o4 y( _3 m- F# Q9 W2 ?. j8 @; E, f6 Q: J
void *setbit_thread(void *opt)- f& }9 ~# B3 B& N# t' J. z
{
8 R4 s: |& {; K+ V) I" [2 \ bitmap |= 1UL << pos;4 M" c" u' Y8 V/ n: y5 q( N' W; A
return NULL;! {( h: |: u, a- S% B5 f6 {
}
5 j4 {- k$ C) C7 b1 D- R/ t
' |8 t& k' g/ Y' O* |$ |int main(int argc, void **argv)
6 o" e ?/ @7 s5 l{6 n+ g1 ~9 v1 X- g0 s
pthread_t thread[32];
# o7 c/ a' n x& P for (int i = 0; i != 32; i++)
* Q- t! M( O! P$ X {3 A! f1 O$ T- Z P+ a/ M0 e6 A
pthread_create(&thread, NULL, &setbit_thread, (void *)i);
/ k9 G7 M6 v/ p K, K" o }% ~8 x# m8 D0 r. n. X# d
for (int i = 0; i != 32; i++)
" _/ K7 s! M0 B$ b, s9 d# Q9 y {
1 K! z0 W+ N" p5 J8 c void *ret;' _0 \) @4 P" R0 j8 o
pthread_join(&thread, &ret);
) d$ t! |) R0 M8 o6 }$ f, v) X }% l3 z1 g% j/ c; W$ l' p: f) J
printf("bitmap = %08x\n", bitmap);
" U4 n, w0 \/ d8 N/ j: K7 p3 k+ ^/ ?" n return 0;
2 k2 v+ F' i, a' Q; W% ]}
R& C6 s' m2 Z0 u7 K$ V5 ]& X关键的一步是bitmap |= 1UL << pos。这一步在CPU实际运行时是分成了几步来做。因为多线程同时操作这个变量,并且该操作不是原子的,所以最后的运行结果并不一定是FFFFFFFF。为了解决这个问题,可以引用GCC __sync_fetch_and_or来实现对于bitmap的原子操作。该操作在不同的体系结构下(例如x86,ARM)的实现方式不相同,但是GCC屏蔽了底层实现的细节,通过这样的内建函数来完成支持对内存原子操作的语义。" d3 k x6 ]5 j
再回过头来看原子写。由于设备中实际操作原子写IO并不是真正原子的,所以原子写并不是在所有的意义上都是保持原子含义的。那么到底原子写是针对哪些操作而言保持了原子性?, d- N( f% o' h1 R. j
在NVMe1.0标准中,详细规定了控制器操作原子性的精确含义。在NVMe 1.2标准中,又补充增加了对于Namespace名字空间的原子写操作。此外,在NVMe组织内部,对于IO操作和其他操作的原子性的精确定义也正在逐步落地,期望会在未来的NVMe 1.2后续版本中体现出来。
3 N) W# K; p) G/ \3 M% p* r3 X5 \通俗的说,如果设备支持原子写能力,那么一次写入的IO相对于其他的IO(不管是其他的写请求还是读请求)都是原子的,不允许读出一半新写入的数据,另外一半是旧的数据。当然,为了简化设备端支持原子写的实现难度,在协议中约定,只有IO请求满足一定条件时才是原子的。NVMe中,对于控制器的原子操作能力定义包括了3个方面的内容:
" q4 A6 |5 ^; |1、Atomic Write Unit Normal (AWUN)% w( E/ M, }* K1 M3 i
2、Atomic Write Unit Power Fail (AWUPF)# ]) E9 |: C; E( B7 g
3、Atomic Compare and Write Unit (ACWU)3 I1 A! p" K+ @) _( z
AWUN是通常意义的原子写中IO的最大允许粒度,如果IO请求长度小于等于该值所对应的长度,那么这个IO请求相对于其他同样满足该约束的请求是原子的。在NVMe上保存的数据和IO读请求读到的数据必须是齐整的。
$ J5 Z( I% c# a3 |! Z1 }在协议中剧了这样一个例子,两个写请求同时发给设备(同时的意思并不是说这两个请求在填入NVMe发送队列时是同时的。对每个命令来说,从写入到NVMe发送队列到从NVMe接收队列收到指令完成这段时间称为该指令的生命周期,那么同时指的是两个指令的生命周期有重叠)。请求A写入的范围是0~3,B的范围是1~4。那么下面的表中列出了最终结果中允许出现和不允许出现的状态。3 P5 t2 [8 x; K0 p& y) S
( f4 l0 Y1 q0 b: G1 o; B' }采用了NVMe协议作为接口协议的SSD,为了提高性能,在内部收到一个大的IO请求后,通常是切分成若干个小的请求,分别发送到不同的Flash通道中。这样在异常断电的情况下,就有可能部分数据写入成功,部分数据写入失败。这种现象类似于RAID5中的write hole,可能会造成上层应用的数据不一致。因此有必要在硬件上也支持对这样写入请求的完整交易。AWUPF指定了对于一个IO写请求来说,如果IO请求长度小于等于该值所对应的长度,那么出现写入未完成时即掉电的情况的话,下次开机后该写入请求或者全部成功生效,或者全部没有生效,避免出现部分新数据部分旧数据的情况。
" I( P6 c2 X* Q0 `5 v8 t E至于ACWU,则是表明在执行Compare And Write指令的时候,原子性的粒度,此处从略。
4 P7 A3 d$ j0 j. j在NVMe 1.0中,原子写的功能定义较早,还没有较多考虑实现上的困难。同时并不是所有的IO请求都要求是原子请求。因此,在NVMe 1.2中,对这一部分设计进行了优化,增加了基于Namespace的原子写能力。该能力的主要变化是:
% x3 }! k- f8 L. r1 Q1、强调了每个Namespace的原子单位是不同的。增加了每个Namespace各自的原子性描述。其中NAWUN,NAWUPF,NACWU分别对应于AWUN,AWUPF和ACWU。这是因为在设备端实现中,支持原子写通常需要更大的付出,因此如果一个Namespace并不需要很强的原子写能力,则可以进行优化。此外,对于一个NVMe系统来说,底层可能包含若干NVMe设备,不同的设备所支持的原子能力不一定相同。
/ o3 Q: ~! E& G$ ~/ J; @" Q7 j9 v2、增加了基于Namespace的原子性边界描述:NABSN,NABO,NABSPF。在特定的设备端实现中,除了需要满足IO请求大小不能太大外,对于IO的起始位置还有更多的要求。例如,一般的企业级SSD都是按照4kB Block Size来进行逻辑地址的映射的。如果设备收到的IO请求跨越4kB的地址边界的话,需要将旧的数据先从SSD中读出来,拼接上新写入的数据再写回去。如果设备声明任意小于等于4kB的IO请求都是原子的话,那么对于从512B地址开始,长度为4kB的写请求也要求满足原子性。这显然比只支持4kB对齐的原子写要复杂得多。定义的NABSN,NABO,NABSPF进一步对IO请求支持原子性的请求边界、偏移量和断电的边界分别作出了详细规定,简化了设备支持所需要的代价。
! o/ {0 r6 R) \8 M$ Z0 J那么,是否NVMe对于原子写的能力规定已经完善了呢?从协议来看,下面的一些内容仍然是讨论的热点:$ W& N' |3 Y. g! a& m' [' ]/ w- f
1、NVMe规定的原子写只针对一个IO,对应的地址范围是连续的。但是,从实际需要来看,仍然有一些需求,需要针对一段不连续的地址范围实现原子写的能力。[6]提出了对于不连续的IO需要保证原子写的要求,目前仍在讨论中。- \; {% J3 D8 |, _. b# i
2、目前对于原子性和其他非IO请求的原子性约束方面上没有明确的规定。
& i7 Y7 f5 N! ^' G* P8 R: r3、NVMe的原子写能力是定义在控制器级别或者是Namespace级别。在一些SSD实现算法中,原子写比普通的非原子写代价大一些。作为一个优化,是否可以支持每一个IO自己表明是否支持原子写,以减小原子写的代价。/ l* \* Q2 }& V m+ e
- `; L/ S5 G3 ^( i* M4 D
+ {! h/ w% ^( ]4 {- C0 i1 p( r原子写保障了或者所有的数据块全都写到设备,或者没有任何数据写入到设备。在很多应用特别是事务性交易中,需要保障在硬盘上的数据是完整的。无疑,原子写对这一类应用来说带来的好处是巨大的。由于不同的SSD提供的原子写能力不同,一些软件提供了灵活配置的能力,根据设备所提供的能力进行定制化。当然,如果配置不当,那么将会导致能力不能充分发挥并因其性能下降,甚至会导致数据一致性错误。# o2 U# o; s8 a/ T
接着来说说存储层的原子性,这涉及到了文件系统,块设备层和硬盘本身。在众多的文件系统中,Sun的ZFS在不依赖于硬件特性的条件下天然支持原子写;FusionIO的DFS依赖于其PCIe SSD提供的原子命令硬件支持,从而支持文件系统的原子写。不过在Linux社区里,现阶段Ext3,Ext4,XFS等文件系统以及它们所依赖的Linux IO Stack对于原子写的支持并不友好。BtRFs作为下一代的文件系统,在将来可能会增加对于原子写的支持。Btrfs的主要贡献者之一Chris Mason提议在Block IO层增加对于原子写的支持,并在设备驱动中使用blk_queue_set_atomic_write来向Block IO层注册对于原子写的支持能力。目前对于这一部分的支持仍然在讨论中,尚未提交到Linux mainstream。此外,Windows的NTFS并不支持原子写。在硬件方面,部分SSD厂商已经开始支持NVMe设备硬件层面的原子写能力,但是还缺乏上层系统层面的支持。当然,很多应用已经迫不及待需要尝鲜体验原子写带来的好处了,下面就数据库MySQL原子写优化作一简单介绍。' S4 U- [/ G8 p
数据库特别是关系型数据库,需要保证数据交易的完整性和内在关系的自洽性,一大问题是如何保证写入到硬盘数据的自洽性。MySQL设计采用的ACID模型在架构上重点考虑了几个重要原则,用来保证在极端条件下的数据可靠性和性能:
& H6 S7 L# T, E |( H9 E3 v |A:原子性,指的是在提交和回滚等动作的原子性和不被破坏。- S" y9 R: k8 W; U* j. m
C:一致性,指的是在极端情况下对于数据的保护,保障数据一致性。, Q+ \0 _4 \8 S
I:隔离性,指的是在InnoDB交易中各个交易的隔离,保障相互间不干扰。) t% _: M/ w( a& C1 K4 c
D:持久性,指的是数据库可以根据需要和硬件的能力来灵活配置。7 o' f9 S7 h8 b5 G
为了满足设计上的ACID准则,MySQL需要利用到软件和硬件上的一些特性。其中有一个Double Write Buffer的技术,是为了解决存储层对于IO写入不满足原子性要求而额外附加的设计。
5 Q" h1 w2 I' U) W1 e! c/ I: S在MySQL中,在硬盘和内存中的数据交换是以页为单位的。每一页保存了有关一个行或几个行的全部信息(如果数据库中的一行太大无法完全放入到一个页,那么会采用链接表等数据结构)并且通过CRC保障数据完整性。页大小通常是16kB并且由innodb_page_size配置常量来控制。在MySQL中,无论是前台的动作,还是后台的动作,都是基于页来对硬盘进行操作的。为了保证数据的一致性,MySQL要求数据是原子写入到数据文件中的。但是实际的存储层往往不支持。如果在写入一个页的时候发生了断电,那么这个页上一部分数据是属于旧的记录,另一部分属于新的记录,那么系统就不能进行再次上电后的恢复了。为了解决这个问题,引入了Double Write Buffer,数据首先写入到这部分硬盘空间,待数据完整写入后,再更新数据文件。如果这个过程中发生了意外,在恢复的时候,首先检查Double Write Buffer,如果它的内容完整,那么直接采用它来重写数据文件,否则则可以确定数据文件本身记录是完整的,这样就利用Redo Log来重新更新数据文件并舍弃Double Write Buffer中的不完整数据。通过这样双重更新确保了数据安全和关系一致,同时造成了写入数据量增加。8 G3 s l& L+ R5 M2 x' k
那么,为什么不能仅依赖于Redo Log来恢复数据呢?这是因为为了减小存储空间,Redo Log仅保存了当前发生变化的记录的信息增量,并没有保存一个页完整的信息。而在发生存储层异常的时候,并不能认为仅仅影响到了当前的变化增量,很可能全部的页的存储内容都会发生变化。 O0 |) O0 d. e- w& W* C0 c2 r0 ~
) g& C) R# @5 [ R
在传统的机械式硬盘中, Double Write Buffer写入是顺序操作,相对于数据文件写入这样的随机写操作来说,顺序写入的代价非常小。同时还通过尽量将写操作聚合进行来降低IO压力。实际系统中Double Write Buffer带来的性能损失大约只有5~10%。在新型的SSD存储中,因为顺序写和随机写性能相近,因此两次写入带来的带宽压力不能忽略。同时SSD通常的寿命是有限的,Double Write Buffer导致数据重复写入对于SSD寿命有较大影响。在MySQL中可以通过设置innodb_doublewrite=0关掉Double Write Buffer功能,但是这样一旦发生异常调电或内核崩溃,可能造成数据不一致的问题。如果SSD设备支持原子写,那么MySQL可以借助这个能力来避免因为关掉Double write buffer导致的写入数据不完整问题。/ s1 B( [9 V. v. M6 k
下面的代码是MySQL 5.7中对于相关部分的实现。一旦关闭了原子写,那么对于每一次操作来说,都会调用fil_io操作写入一个页,如果存储层能够保证数据安全的话,那么整个页的数据一致性就可以保证。) i1 O a8 c2 q+ T7 i4 u
+ O! Y# _5 ^' ~9 P H/ l总结起来,使用设备提供的原子写能力来优化Linux下MySQL数据库的性能,需要考虑下面几点:# w" R; {6 F8 I( E0 ^' w
1、 MySQL数据库通过设置环境变量innodb_doublewrite=0关闭Double Write Buffer,并设置innodb_flush_method=O_DIRECT从而关闭文件系统的Cache。: f# N) Y" V/ V" _1 `. H n4 m
2、 选择支持原子写的SSD,并保证设备提供的原子写能力不低于MySQL的页大小,该参数通常为16kB。启用该设备提供的原子写能力。
% j6 \* V) J0 T; t! U- i# N# n3、 在一些老式的内核中,需要确保Block Layer对应的调度器选项为noop。在一些较新的内核中,该参数对于NVMe SSD自动设置为noop。! Z& @: K) R: c7 l2 a) v
除了MySQL之外,MySQL的近亲MariaDB[11]也可以通过此类优化获得性能的提升。此外,PostgreSQL等数据库可通过关闭full_page_writes开关辅助存储层提供的能力来提升数据库的性能。) D& ~. G0 r) ]+ Q4 a; ?- L# T
|
|