找回密码
 注册
关于网站域名变更的通知
查看: 381|回复: 1
打印 上一主题 下一主题

Atomic write, 揭开你的面纱

[复制链接]
  • TA的每日心情
    开心
    2019-11-20 15:00
  • 签到天数: 2 天

    [LV.1]初来乍到

    跳转到指定楼层
    1#
    发表于 2019-4-3 08:00 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式

    EDA365欢迎您登录!

    您需要 登录 才可以下载或查看,没有帐号?注册

    x
    Atomic write, 揭开你的面纱
    . m9 f, k) j8 _* X& q3 i" ~
    最早听到Atomic write这个功能是从Fusion-io的DFS文件系统对于数据库的性能优化上。从字面意义上来说,它的意思是IO写入操作相对于其他操作来说,是一个单步的动作。实际上,对于一个IO操作来说,在SSD内部的实现是一个复杂的过程,需要牵涉到很多步骤,因此通常的IO操作并不是一个单步的动作。为了支持原子写,SSD需要通过一个特殊的设计,保证从用户的角度来看,好像这个动作是单步的。3 X1 M$ m# h" e  [! L, {" P7 {
    为了说明这个问题,先看一个多线程程序设计中常见的多线程同步问题。在这个例子中,设置了32个线程对同一个变量进行设置。3 _* E1 Y: Y9 a0 N* W
    3 T) c( N6 u3 t7 g9 Z
    unsigned int bitmap = 0;( }0 {3 [5 H0 K6 Y* V4 o) s# F

    # ~! t7 u2 Y3 {void *setbit_thread(void *opt)
    : I. [% K" M. C5 P- L4 I{8 H# d8 A) J4 A2 W& j
        bitmap |= 1UL << pos;
    : C5 F) e$ M- K    return NULL;  {  j3 l- F# O+ ^& x% x
    }+ v% n# Z$ K! g- o' S; [* F' r

    # k& z# i$ J$ I# G( F6 K4 n& u) lint main(int argc, void **argv)
    " t9 L% E! L! W0 B{
    ! ~! S* i  j1 ^5 o1 x5 u# J    pthread_t thread[32];
    / w5 {& H! S; ?2 \    for (int i = 0; i != 32; i++)
    5 x  C* k" p- o, [0 j2 @    {
    ) D2 L# y( R, ?& L' z        pthread_create(&thread, NULL, &setbit_thread, (void *)i);
    $ c! X+ h- j; F& l- _5 m- m    }7 H* G. M. a. o0 {/ v! g. R
        for (int i = 0; i != 32; i++)
    " R4 G) U7 T" x    {- }/ ^8 Y+ g! Z% [( p" S  f8 G) n
            void *ret;  y, @8 H, h' a9 T4 j
            pthread_join(&thread, &ret);
    ; w; C0 P, \0 k' [! W0 W: l3 H    }) W) G0 ?' l- x( {& j
        printf("bitmap = %08x\n", bitmap);
    ; S0 W( T+ D4 h& \+ ?6 D7 F8 G8 |    return 0;2 C9 J; v3 k& l* r9 o! w9 \0 H7 ^
    }
    # m: d7 I. c# D/ P6 t: ]" ~关键的一步是bitmap |= 1UL << pos。这一步在CPU实际运行时是分成了几步来做。因为多线程同时操作这个变量,并且该操作不是原子的,所以最后的运行结果并不一定是FFFFFFFF。为了解决这个问题,可以引用GCC __sync_fetch_and_or来实现对于bitmap的原子操作。该操作在不同的体系结构下(例如x86,ARM)的实现方式不相同,但是GCC屏蔽了底层实现的细节,通过这样的内建函数来完成支持对内存原子操作的语义。4 D* X7 P8 u, i3 ?! u" n
    再回过头来看原子写。由于设备中实际操作原子写IO并不是真正原子的,所以原子写并不是在所有的意义上都是保持原子含义的。那么到底原子写是针对哪些操作而言保持了原子性?& x( x6 u6 R& Q" Q6 e7 c
    在NVMe1.0标准中,详细规定了控制器操作原子性的精确含义。在NVMe 1.2标准中,又补充增加了对于Namespace名字空间的原子写操作。此外,在NVMe组织内部,对于IO操作和其他操作的原子性的精确定义也正在逐步落地,期望会在未来的NVMe 1.2后续版本中体现出来。& g5 [; Y5 u. J& X7 P0 M) G
    通俗的说,如果设备支持原子写能力,那么一次写入的IO相对于其他的IO(不管是其他的写请求还是读请求)都是原子的,不允许读出一半新写入的数据,另外一半是旧的数据。当然,为了简化设备端支持原子写的实现难度,在协议中约定,只有IO请求满足一定条件时才是原子的。NVMe中,对于控制器的原子操作能力定义包括了3个方面的内容:
    # p4 n& C- \9 y+ j2 z! ?. d1、Atomic Write Unit Normal (AWUN)
    # I6 T# Y, ~! q2 |; i2、Atomic Write Unit Power Fail (AWUPF)7 R* {6 S8 K3 @( F6 n
    3、Atomic Compare and Write Unit (ACWU)* G. F# q, O1 ]0 a- z& y
    AWUN是通常意义的原子写中IO的最大允许粒度,如果IO请求长度小于等于该值所对应的长度,那么这个IO请求相对于其他同样满足该约束的请求是原子的。在NVMe上保存的数据和IO读请求读到的数据必须是齐整的。
    ! t; _# D+ o% `$ `( G8 M在协议中剧了这样一个例子,两个写请求同时发给设备(同时的意思并不是说这两个请求在填入NVMe发送队列时是同时的。对每个命令来说,从写入到NVMe发送队列到从NVMe接收队列收到指令完成这段时间称为该指令的生命周期,那么同时指的是两个指令的生命周期有重叠)。请求A写入的范围是0~3,B的范围是1~4。那么下面的表中列出了最终结果中允许出现和不允许出现的状态。
    5 M3 y& U* ~8 z: G3 x* O
    $ H6 o4 _. v" s( z3 d$ K- @采用了NVMe协议作为接口协议的SSD,为了提高性能,在内部收到一个大的IO请求后,通常是切分成若干个小的请求,分别发送到不同的Flash通道中。这样在异常断电的情况下,就有可能部分数据写入成功,部分数据写入失败。这种现象类似于RAID5中的write hole,可能会造成上层应用的数据不一致。因此有必要在硬件上也支持对这样写入请求的完整交易。AWUPF指定了对于一个IO写请求来说,如果IO请求长度小于等于该值所对应的长度,那么出现写入未完成时即掉电的情况的话,下次开机后该写入请求或者全部成功生效,或者全部没有生效,避免出现部分新数据部分旧数据的情况。' y# e# V# I. M. j. u& l
    至于ACWU,则是表明在执行Compare And Write指令的时候,原子性的粒度,此处从略。$ J# h3 c  x' z4 x$ ^: Q$ m( _( a# ]$ c
    在NVMe 1.0中,原子写的功能定义较早,还没有较多考虑实现上的困难。同时并不是所有的IO请求都要求是原子请求。因此,在NVMe 1.2中,对这一部分设计进行了优化,增加了基于Namespace的原子写能力。该能力的主要变化是:# t( F7 A8 n  Q
    1、强调了每个Namespace的原子单位是不同的。增加了每个Namespace各自的原子性描述。其中NAWUN,NAWUPF,NACWU分别对应于AWUN,AWUPF和ACWU。这是因为在设备端实现中,支持原子写通常需要更大的付出,因此如果一个Namespace并不需要很强的原子写能力,则可以进行优化。此外,对于一个NVMe系统来说,底层可能包含若干NVMe设备,不同的设备所支持的原子能力不一定相同。
    $ z  p1 o2 n5 V( s1 {" U- ]5 w2、增加了基于Namespace的原子性边界描述:NABSN,NABO,NABSPF。在特定的设备端实现中,除了需要满足IO请求大小不能太大外,对于IO的起始位置还有更多的要求。例如,一般的企业级SSD都是按照4kB Block Size来进行逻辑地址的映射的。如果设备收到的IO请求跨越4kB的地址边界的话,需要将旧的数据先从SSD中读出来,拼接上新写入的数据再写回去。如果设备声明任意小于等于4kB的IO请求都是原子的话,那么对于从512B地址开始,长度为4kB的写请求也要求满足原子性。这显然比只支持4kB对齐的原子写要复杂得多。定义的NABSN,NABO,NABSPF进一步对IO请求支持原子性的请求边界、偏移量和断电的边界分别作出了详细规定,简化了设备支持所需要的代价。
    $ O! y0 _0 n& C: G3 r* k" n% p( v6 G那么,是否NVMe对于原子写的能力规定已经完善了呢?从协议来看,下面的一些内容仍然是讨论的热点:. G# p7 c% ^9 C( i. r
    1、NVMe规定的原子写只针对一个IO,对应的地址范围是连续的。但是,从实际需要来看,仍然有一些需求,需要针对一段不连续的地址范围实现原子写的能力。[6]提出了对于不连续的IO需要保证原子写的要求,目前仍在讨论中。1 M0 N6 M* {' q
    2、目前对于原子性和其他非IO请求的原子性约束方面上没有明确的规定。+ d0 V5 o  T: h. T0 N2 t
    3、NVMe的原子写能力是定义在控制器级别或者是Namespace级别。在一些SSD实现算法中,原子写比普通的非原子写代价大一些。作为一个优化,是否可以支持每一个IO自己表明是否支持原子写,以减小原子写的代价。- M5 R) q" Q$ G8 z# ^% _1 D- q/ s
    4 o  L( o) s# _


    1 O5 W2 R+ u  E* e1 f' a0 M原子写保障了或者所有的数据块全都写到设备,或者没有任何数据写入到设备。在很多应用特别是事务性交易中,需要保障在硬盘上的数据是完整的。无疑,原子写对这一类应用来说带来的好处是巨大的。由于不同的SSD提供的原子写能力不同,一些软件提供了灵活配置的能力,根据设备所提供的能力进行定制化。当然,如果配置不当,那么将会导致能力不能充分发挥并因其性能下降,甚至会导致数据一致性错误。
    ( k1 `; b) q/ F2 C$ T* O接着来说说存储层的原子性,这涉及到了文件系统,块设备层和硬盘本身。在众多的文件系统中,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原子写优化作一简单介绍。& R# u6 I4 M+ w4 H( z
    数据库特别是关系型数据库,需要保证数据交易的完整性和内在关系的自洽性,一大问题是如何保证写入到硬盘数据的自洽性。MySQL设计采用的ACID模型在架构上重点考虑了几个重要原则,用来保证在极端条件下的数据可靠性和性能:
    " g  I  [9 }; x/ zA:原子性,指的是在提交和回滚等动作的原子性和不被破坏。
    : h) r7 \: C' G8 ]' tC:一致性,指的是在极端情况下对于数据的保护,保障数据一致性。) k: y/ d9 j; v6 {1 B
    I:隔离性,指的是在InnoDB交易中各个交易的隔离,保障相互间不干扰。
    + r8 {8 U& w% H' SD:持久性,指的是数据库可以根据需要和硬件的能力来灵活配置。
    - ?6 m1 y7 v" q# ]2 K8 I! c" Q1 X8 g) S为了满足设计上的ACID准则,MySQL需要利用到软件和硬件上的一些特性。其中有一个Double Write Buffer的技术,是为了解决存储层对于IO写入不满足原子性要求而额外附加的设计。
    - p$ B. z# o0 I7 E在MySQL中,在硬盘和内存中的数据交换是以页为单位的。每一页保存了有关一个行或几个行的全部信息(如果数据库中的一行太大无法完全放入到一个页,那么会采用链接表等数据结构)并且通过CRC保障数据完整性。页大小通常是16kB并且由innodb_page_size配置常量来控制。在MySQL中,无论是前台的动作,还是后台的动作,都是基于页来对硬盘进行操作的。为了保证数据的一致性,MySQL要求数据是原子写入到数据文件中的。但是实际的存储层往往不支持。如果在写入一个页的时候发生了断电,那么这个页上一部分数据是属于旧的记录,另一部分属于新的记录,那么系统就不能进行再次上电后的恢复了。为了解决这个问题,引入了Double Write Buffer,数据首先写入到这部分硬盘空间,待数据完整写入后,再更新数据文件。如果这个过程中发生了意外,在恢复的时候,首先检查Double Write Buffer,如果它的内容完整,那么直接采用它来重写数据文件,否则则可以确定数据文件本身记录是完整的,这样就利用Redo Log来重新更新数据文件并舍弃Double Write Buffer中的不完整数据。通过这样双重更新确保了数据安全和关系一致,同时造成了写入数据量增加。
    1 M! A3 c  [' k. O+ i: t% u那么,为什么不能仅依赖于Redo Log来恢复数据呢?这是因为为了减小存储空间,Redo Log仅保存了当前发生变化的记录的信息增量,并没有保存一个页完整的信息。而在发生存储层异常的时候,并不能认为仅仅影响到了当前的变化增量,很可能全部的页的存储内容都会发生变化。
    1 K" G8 V# G* Y/ F: x, b3 ] + K0 X, b! |3 v  @2 |5 q
    在传统的机械式硬盘中, 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导致的写入数据不完整问题。
    6 Z, y2 a' D* m. |9 N$ E9 c& A5 x下面的代码是MySQL 5.7中对于相关部分的实现。一旦关闭了原子写,那么对于每一次操作来说,都会调用fil_io操作写入一个页,如果存储层能够保证数据安全的话,那么整个页的数据一致性就可以保证。' ~, Y4 F% T2 E$ v6 x

    # Y7 V9 w7 \( L/ i) E2 a总结起来,使用设备提供的原子写能力来优化Linux下MySQL数据库的性能,需要考虑下面几点:
    2 E; \9 x+ ?% @1 w+ H0 t: s1、        MySQL数据库通过设置环境变量innodb_doublewrite=0关闭Double Write Buffer,并设置innodb_flush_method=O_DIRECT从而关闭文件系统的Cache。/ S  r& K2 o9 d/ Q/ @$ p  s3 U
    2、        选择支持原子写的SSD,并保证设备提供的原子写能力不低于MySQL的页大小,该参数通常为16kB。启用该设备提供的原子写能力。7 V( @" F! u+ N5 n5 h/ c( `
    3、        在一些老式的内核中,需要确保Block Layer对应的调度器选项为noop。在一些较新的内核中,该参数对于NVMe SSD自动设置为noop。
    5 W- O" v  t5 e! z9 P1 t" z除了MySQL之外,MySQL的近亲MariaDB[11]也可以通过此类优化获得性能的提升。此外,PostgreSQL等数据库可通过关闭full_page_writes开关辅助存储层提供的能力来提升数据库的性能。3 Q) X8 j2 t# E* S/ c

    该用户从未签到

    2#
    发表于 2019-4-3 15:03 | 只看该作者
    发帖是心得 回帖是美德
    您需要登录后才可以回帖 登录 | 注册

    本版积分规则

    关闭

    推荐内容上一条 /1 下一条

    EDA365公众号

    关于我们|手机版|EDA365电子论坛网 ( 粤ICP备18020198号-1 )

    GMT+8, 2025-7-31 03:57 , Processed in 0.109375 second(s), 27 queries , Gzip On.

    深圳市墨知创新科技有限公司

    地址:深圳市南山区科技生态园2栋A座805 电话:19926409050

    快速回复 返回顶部 返回列表