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

Atomic write, 揭开你的面纱

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

    [LV.1]初来乍到

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

    EDA365欢迎您登录!

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

    x
    Atomic write, 揭开你的面纱
    ( f0 r  e+ t. S4 a" l$ }8 h+ [
    最早听到Atomic write这个功能是从Fusion-io的DFS文件系统对于数据库的性能优化上。从字面意义上来说,它的意思是IO写入操作相对于其他操作来说,是一个单步的动作。实际上,对于一个IO操作来说,在SSD内部的实现是一个复杂的过程,需要牵涉到很多步骤,因此通常的IO操作并不是一个单步的动作。为了支持原子写,SSD需要通过一个特殊的设计,保证从用户的角度来看,好像这个动作是单步的。
    # s3 q9 L* ]" O为了说明这个问题,先看一个多线程程序设计中常见的多线程同步问题。在这个例子中,设置了32个线程对同一个变量进行设置。4 i5 V5 O, w) ]2 S7 `% T* F
    * K# z( }: A0 K  N% D2 M" B
    unsigned int bitmap = 0;& I/ D8 w) @! c; x9 Z9 ]) ~

    " k; Y& V) X# Y2 k! W7 k+ Yvoid *setbit_thread(void *opt)
    0 `9 A6 C& \# ~: j" A* K- ]{
    * k3 ^) J1 W: Y3 G+ S- i$ V# ^    bitmap |= 1UL << pos;
    " C8 j5 L5 S# L) Z! I- u    return NULL;
    / ?6 L! ?# @' q0 ^+ y}
    , z9 I9 R" I( U1 O* x. n6 w# k, F) c
    int main(int argc, void **argv); o. d! q9 _8 K# r3 O2 }$ S
    {
    . ]1 U6 O5 l8 j* ~; d    pthread_t thread[32];, N* n* _" |' K% I. \% d* h
        for (int i = 0; i != 32; i++)+ a6 m( U' R) a' r
        {6 M  Q& d3 b* x! ?' F
            pthread_create(&thread, NULL, &setbit_thread, (void *)i);
    : Y+ n0 G, Q; c* n    }
    , v6 h8 e0 }6 y& g  _+ U7 H    for (int i = 0; i != 32; i++)" Z0 _; K) v3 K! @
        {
    % B) ]4 w: \* k- D: R* s# X        void *ret;
    . d, I; P0 N# e, J7 {        pthread_join(&thread, &ret);
    ; v' }' e+ \! d& i4 ^    }
    ! u- l, X" {0 i' C. J    printf("bitmap = %08x\n", bitmap);/ z6 V! ]- t7 H
        return 0;  `: h! r3 @& O" F
    }& k* w: c  F0 G
    关键的一步是bitmap |= 1UL << pos。这一步在CPU实际运行时是分成了几步来做。因为多线程同时操作这个变量,并且该操作不是原子的,所以最后的运行结果并不一定是FFFFFFFF。为了解决这个问题,可以引用GCC __sync_fetch_and_or来实现对于bitmap的原子操作。该操作在不同的体系结构下(例如x86,ARM)的实现方式不相同,但是GCC屏蔽了底层实现的细节,通过这样的内建函数来完成支持对内存原子操作的语义。
    * ^7 T$ U  F/ Q2 f  D' a1 Q; ]% X; K再回过头来看原子写。由于设备中实际操作原子写IO并不是真正原子的,所以原子写并不是在所有的意义上都是保持原子含义的。那么到底原子写是针对哪些操作而言保持了原子性?, U! |5 }& d3 N) }
    在NVMe1.0标准中,详细规定了控制器操作原子性的精确含义。在NVMe 1.2标准中,又补充增加了对于Namespace名字空间的原子写操作。此外,在NVMe组织内部,对于IO操作和其他操作的原子性的精确定义也正在逐步落地,期望会在未来的NVMe 1.2后续版本中体现出来。
    ' H2 Q8 T" `' W0 h6 U$ U通俗的说,如果设备支持原子写能力,那么一次写入的IO相对于其他的IO(不管是其他的写请求还是读请求)都是原子的,不允许读出一半新写入的数据,另外一半是旧的数据。当然,为了简化设备端支持原子写的实现难度,在协议中约定,只有IO请求满足一定条件时才是原子的。NVMe中,对于控制器的原子操作能力定义包括了3个方面的内容:
    & n2 Q/ ]: X+ I1、Atomic Write Unit Normal (AWUN)' h& h2 z* T3 v) u* {
    2、Atomic Write Unit Power Fail (AWUPF)9 Y! N5 d3 a3 P
    3、Atomic Compare and Write Unit (ACWU)
    5 n- I5 ~0 E' c7 u5 `5 f" DAWUN是通常意义的原子写中IO的最大允许粒度,如果IO请求长度小于等于该值所对应的长度,那么这个IO请求相对于其他同样满足该约束的请求是原子的。在NVMe上保存的数据和IO读请求读到的数据必须是齐整的。
    , |) E! m; K  X2 T/ _在协议中剧了这样一个例子,两个写请求同时发给设备(同时的意思并不是说这两个请求在填入NVMe发送队列时是同时的。对每个命令来说,从写入到NVMe发送队列到从NVMe接收队列收到指令完成这段时间称为该指令的生命周期,那么同时指的是两个指令的生命周期有重叠)。请求A写入的范围是0~3,B的范围是1~4。那么下面的表中列出了最终结果中允许出现和不允许出现的状态。
    - L8 D1 D( b, }% Q3 n, M 9 ]. Y; \/ U  q1 B2 S0 Q5 Z2 Q; t' g
    采用了NVMe协议作为接口协议的SSD,为了提高性能,在内部收到一个大的IO请求后,通常是切分成若干个小的请求,分别发送到不同的Flash通道中。这样在异常断电的情况下,就有可能部分数据写入成功,部分数据写入失败。这种现象类似于RAID5中的write hole,可能会造成上层应用的数据不一致。因此有必要在硬件上也支持对这样写入请求的完整交易。AWUPF指定了对于一个IO写请求来说,如果IO请求长度小于等于该值所对应的长度,那么出现写入未完成时即掉电的情况的话,下次开机后该写入请求或者全部成功生效,或者全部没有生效,避免出现部分新数据部分旧数据的情况。5 G+ h/ o* }, Z' U& p" h4 o
    至于ACWU,则是表明在执行Compare And Write指令的时候,原子性的粒度,此处从略。0 i1 W9 F9 r5 j. ^; ~
    在NVMe 1.0中,原子写的功能定义较早,还没有较多考虑实现上的困难。同时并不是所有的IO请求都要求是原子请求。因此,在NVMe 1.2中,对这一部分设计进行了优化,增加了基于Namespace的原子写能力。该能力的主要变化是:6 d( E, G% `0 @& a. i
    1、强调了每个Namespace的原子单位是不同的。增加了每个Namespace各自的原子性描述。其中NAWUN,NAWUPF,NACWU分别对应于AWUN,AWUPF和ACWU。这是因为在设备端实现中,支持原子写通常需要更大的付出,因此如果一个Namespace并不需要很强的原子写能力,则可以进行优化。此外,对于一个NVMe系统来说,底层可能包含若干NVMe设备,不同的设备所支持的原子能力不一定相同。
    , r4 _  n; C. I- ]) L  W6 c- w, N! l2、增加了基于Namespace的原子性边界描述:NABSN,NABO,NABSPF。在特定的设备端实现中,除了需要满足IO请求大小不能太大外,对于IO的起始位置还有更多的要求。例如,一般的企业级SSD都是按照4kB Block Size来进行逻辑地址的映射的。如果设备收到的IO请求跨越4kB的地址边界的话,需要将旧的数据先从SSD中读出来,拼接上新写入的数据再写回去。如果设备声明任意小于等于4kB的IO请求都是原子的话,那么对于从512B地址开始,长度为4kB的写请求也要求满足原子性。这显然比只支持4kB对齐的原子写要复杂得多。定义的NABSN,NABO,NABSPF进一步对IO请求支持原子性的请求边界、偏移量和断电的边界分别作出了详细规定,简化了设备支持所需要的代价。2 K/ Z; a6 `5 E# b; v" V3 k
    那么,是否NVMe对于原子写的能力规定已经完善了呢?从协议来看,下面的一些内容仍然是讨论的热点:- R5 a" ~% `" A3 [
    1、NVMe规定的原子写只针对一个IO,对应的地址范围是连续的。但是,从实际需要来看,仍然有一些需求,需要针对一段不连续的地址范围实现原子写的能力。[6]提出了对于不连续的IO需要保证原子写的要求,目前仍在讨论中。
    % Y3 b- N9 g& c# A: B. ]' [2、目前对于原子性和其他非IO请求的原子性约束方面上没有明确的规定。
    & A2 r) I) r2 G+ w* m2 ]3、NVMe的原子写能力是定义在控制器级别或者是Namespace级别。在一些SSD实现算法中,原子写比普通的非原子写代价大一些。作为一个优化,是否可以支持每一个IO自己表明是否支持原子写,以减小原子写的代价。+ i! Q- S. E# U, J& O
    ( ?% S9 _) d5 @0 j1 B

    0 H3 N( k7 g2 l8 O/ @
    原子写保障了或者所有的数据块全都写到设备,或者没有任何数据写入到设备。在很多应用特别是事务性交易中,需要保障在硬盘上的数据是完整的。无疑,原子写对这一类应用来说带来的好处是巨大的。由于不同的SSD提供的原子写能力不同,一些软件提供了灵活配置的能力,根据设备所提供的能力进行定制化。当然,如果配置不当,那么将会导致能力不能充分发挥并因其性能下降,甚至会导致数据一致性错误。% b- G6 ?& Y8 Q! ^. ?
    接着来说说存储层的原子性,这涉及到了文件系统,块设备层和硬盘本身。在众多的文件系统中,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原子写优化作一简单介绍。. L% S0 Q! W# o' V7 K
    数据库特别是关系型数据库,需要保证数据交易的完整性和内在关系的自洽性,一大问题是如何保证写入到硬盘数据的自洽性。MySQL设计采用的ACID模型在架构上重点考虑了几个重要原则,用来保证在极端条件下的数据可靠性和性能:
    " b' I! Z3 ?5 ^3 A  X- OA:原子性,指的是在提交和回滚等动作的原子性和不被破坏。0 P  Y! _9 N7 f* H5 o0 f
    C:一致性,指的是在极端情况下对于数据的保护,保障数据一致性。. G" R1 p! z  f# W0 I, T& ]
    I:隔离性,指的是在InnoDB交易中各个交易的隔离,保障相互间不干扰。/ S7 o0 {0 H/ f4 ]. e
    D:持久性,指的是数据库可以根据需要和硬件的能力来灵活配置。
    . N" V  Z0 H9 A为了满足设计上的ACID准则,MySQL需要利用到软件和硬件上的一些特性。其中有一个Double Write Buffer的技术,是为了解决存储层对于IO写入不满足原子性要求而额外附加的设计。8 m1 O, U' e" G# {' D9 x
    在MySQL中,在硬盘和内存中的数据交换是以页为单位的。每一页保存了有关一个行或几个行的全部信息(如果数据库中的一行太大无法完全放入到一个页,那么会采用链接表等数据结构)并且通过CRC保障数据完整性。页大小通常是16kB并且由innodb_page_size配置常量来控制。在MySQL中,无论是前台的动作,还是后台的动作,都是基于页来对硬盘进行操作的。为了保证数据的一致性,MySQL要求数据是原子写入到数据文件中的。但是实际的存储层往往不支持。如果在写入一个页的时候发生了断电,那么这个页上一部分数据是属于旧的记录,另一部分属于新的记录,那么系统就不能进行再次上电后的恢复了。为了解决这个问题,引入了Double Write Buffer,数据首先写入到这部分硬盘空间,待数据完整写入后,再更新数据文件。如果这个过程中发生了意外,在恢复的时候,首先检查Double Write Buffer,如果它的内容完整,那么直接采用它来重写数据文件,否则则可以确定数据文件本身记录是完整的,这样就利用Redo Log来重新更新数据文件并舍弃Double Write Buffer中的不完整数据。通过这样双重更新确保了数据安全和关系一致,同时造成了写入数据量增加。5 d2 _- U; r4 P0 ^0 @
    那么,为什么不能仅依赖于Redo Log来恢复数据呢?这是因为为了减小存储空间,Redo Log仅保存了当前发生变化的记录的信息增量,并没有保存一个页完整的信息。而在发生存储层异常的时候,并不能认为仅仅影响到了当前的变化增量,很可能全部的页的存储内容都会发生变化。
    ' M# }+ \! T  I$ C, }! [+ ? + ^: C* P0 T* f3 I& ]9 Y, a; x
    在传统的机械式硬盘中, 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导致的写入数据不完整问题。
    - m( D7 l) m2 e; e# @# g6 Q9 v) T5 n下面的代码是MySQL 5.7中对于相关部分的实现。一旦关闭了原子写,那么对于每一次操作来说,都会调用fil_io操作写入一个页,如果存储层能够保证数据安全的话,那么整个页的数据一致性就可以保证。! L2 j& D* v, ^9 B

    2 T4 p" Y  N6 ^( N1 d0 \$ Q* ?! M& }4 G总结起来,使用设备提供的原子写能力来优化Linux下MySQL数据库的性能,需要考虑下面几点:
    , \! P% _, j9 ^  y" k' Q1、        MySQL数据库通过设置环境变量innodb_doublewrite=0关闭Double Write Buffer,并设置innodb_flush_method=O_DIRECT从而关闭文件系统的Cache。
    ' F6 e- u; _3 n0 G4 I; a9 D: G2、        选择支持原子写的SSD,并保证设备提供的原子写能力不低于MySQL的页大小,该参数通常为16kB。启用该设备提供的原子写能力。7 U, ^1 V0 N1 O6 }5 O+ g/ E' D+ h
    3、        在一些老式的内核中,需要确保Block Layer对应的调度器选项为noop。在一些较新的内核中,该参数对于NVMe SSD自动设置为noop。; Y3 c: e' E$ f/ e3 H
    除了MySQL之外,MySQL的近亲MariaDB[11]也可以通过此类优化获得性能的提升。此外,PostgreSQL等数据库可通过关闭full_page_writes开关辅助存储层提供的能力来提升数据库的性能。4 r2 D. J7 ]  @  E

    该用户从未签到

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

    本版积分规则

    关闭

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

    EDA365公众号

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

    GMT+8, 2025-10-8 18:28 , Processed in 0.140625 second(s), 26 queries , Gzip On.

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

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

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