/ 中存储网

【冬瓜哥手绘】从多控缓存管理到集群锁

2015-07-23 10:59:02 来源:中存储网

IT领域有个名言警句: “凡是说不清楚的,证明你压根就没懂!凡是怕说出来让被人把逼格抢走了的,证明你在吃老本,很快也会玩完,不如趁早转行重获新生“。 ——佚名。

本文分享缓存镜像、控制器间链路RDMA、缓存一致性、集群锁等知识大串接。是一篇超级大餐。吃顿垃圾快餐,抽盒烟,刷个微信甘愿被优衣库之流毫无逼格的网络炒作浪费你的时间,甘愿被娱乐至死,不如来学习知识吧,学习趁早,忽悠凭老,年轻时候不学习被或者主动浪费时间,到老了只能哭。

人们越来越浮躁,越来越关注市场、变化、八卦,这让一些大忽悠在这年头非常吃得开。冬瓜哥走的是另外一条路线,以不变应万变,十年磨一剑,认江湖风雨,认嬉笑怒骂,十年后,二十年后,江湖还是那片江湖,只不过物是人非,而只有亘古不变的经典,才能永流传,任它沧海或桑田。有些看似花哨的东西,昙花一现,皆为云烟。

【主线1】从双控的缓存镜像说开去

传统存储系统为了保持冗余,弄了两台服务器来处理IO。只要是两点或者多点协作,就是集群了,既然是存储集群,那就一定逃不出冬瓜哥上一篇文章(《集群FS架构终极深度梳理图解》)所给出的大框架架构。这两台服务器之间的协作关系有多重方式,但是不管哪种方式,都需要缓存镜像。

1. AP模式。在AP模式下,每个Lun都具有ownership,针对某个Lun的IO不能被下发给非该Lun的owner的控制器,实际上,多路径软件从非该Lun owner的控制器根本扫不到该Lun。仅当控制器发生故障或者手动切换owner的时候,多路经软件通过超时或者私有协议获取等渠道来判断控制器的状态然后跟随控制器的切换而切换访问路径。此时,某个控制器收到写IO数据之后,需要通过控制器间的链路将数据写入对端控制器的缓存地址空间里暂存,然后向主机应答写完成。

2. 非对称双活。分为厂商私有的非标准模式和标准的ALUA模式。Lun从每个控制器都可以扫描到而且也可以发IO,但是IO会被转发到Owner控制器执行。同理,每个控制器收到写IO之后也需要向对方推送一份。

有Lun ownership的上述两种架构之下,本地的读写缓存,与对端镜像过来的缓存,是两个独立管理的部分,因为各自管各自的Lun。

【支线】“缓存分区”的谬误

缓存分区的概念是最早大型机上的,HDS首先将其应用在了存储系统里的,其背景是将缓存按照多种Page Size分为多个管理区域。目前不管是主机还是存储,其OS常用的缓存管理分页大小为4KB,这就产生一个问题,如果应用程序下发的IO基本都是8KB对齐的,那么分页为4KB就显得很没必要了,虽然2个页面拼起来一样可以当8KB用,但是系统依然要为每个页记录元数据,这些元数据也是耗费RAM空间的,如果页面变为8KB,那么就能降低一半的元数据量,节约下来的内存可用于其他作用比如读写缓存。

后来,有人声称它家的产品也支持缓存分区,我还被吓了一跳,总之就是大家有的它家都有,大家没有的它家也有的策略。但是仔细一看,它家的缓存分区却是完全另一回事了,其本质上就是限定读缓存、写缓存所占空间的比例。有很多人问过这个问题:“xx产品是否可以调整写缓存的比例”?这问题看似顺理成章,按理说像样的产品都应该支持,但是多数产品却不是以“写缓存比例”来命名这个功能的,而是以另一种抽象名词——HWM和LWM,高水位线、低水位线。这词看上去逼格够高,但是曲高和寡。所谓高水位线,就是当脏数据达到这个比例的时候,开始刷盘,刷盘到脏数据比例降低到LWM的时候,停止刷盘。这个HWM就是写缓存所占空间比例。如今可好,这种在10年前的低端阵列上都有的功能,被包装为“缓存分区”出来忽悠了,逼格着实不高,当然,它逼格比你高的话,那么你就注定被他给忽悠的一愣一愣的而且还一个劲膜拜。冬瓜哥看着大片逼格不到位的兄弟们被忽悠,心里着急啊!

所谓“读缓存”和“写缓存”,其在物理上并不是分开的,甚至逻辑上也并不分开,整个RAM空间不管对于读还是写,都是统一的一个大空间,读入的page,当被更改之后,就变成了dirty page,系统会为dirty page动态维护一个链表,刷盘的时候按照链表按图索骥将脏页写盘之后,这些页就不再脏,从写缓存变成了读缓存。可以看到,所谓“读缓存”“写缓存”,其逻辑上根本就不存在,存在的只有干净页和脏页或者空页。所以,这种所谓“缓存分区”的概念,不攻自破。其实,不同pagesize的缓存空间,物理上也没有分开,逻辑上也是通过链表来将不同page size的页面串成逻辑上的几块缓存空间,所有page物理上都是凌乱分布在物理RAM里的。

【支线】缓存镜像的底层实现——以太网、IB、DMA、RDMA

多数只关心到缓存能够镜像就截止了,然而,要想达到更高的逼格,就必须深入到底层,至高无上的逼格则是深入到电路层面,最后理论物理,最后数学、哲学,最后神学,最后变为神。我们在神的面前,全部都是浮于表面的芸芸众生。

缓存镜像在底层的实现,有两种模式,一种是走协议栈传递消息,另一种是DMA/RDMA,其实这两种模式没有本质不同,都是将一串数据(缓存块+描述)传递到对方的内存并通知对方处理。为了将一串数据传递到远方,这其实是网络领域研究的课题,只不过属于”局部网络“,冬瓜哥在08年出版的《大话存储》第一版一书中,开场就明确指出:计算机体系内部就是一个大网络,网套着网,网中有网,交换,路由,该有全有。那天遇到一个冬瓜哥粉丝,说他对书中的这句话佩服的五体投地,说他经历了这么多年的工作和学习,回头想想这句话,有种返璞归真的感觉。

那么,将数据传递到远方,一定需要物理层链路层以及上层协议栈等各层来协同分步处理,是的,有些双控之间利用万兆以太网来传递缓存块,而传统以太网不保证以太网帧按序无误不丢失的发送到对方,所以需要TCP协议来保障传输,而TCP还得依赖IP,所以,双控或者多控之间可以直接用TCPIP来发送数据串,开发简单,但是性能、时延不行。新型以太网加入传输保障方面的增强,所以,开发者可以抛弃TCPIP这种抵消协议栈,开发自己的轻量协议栈,也可以做到带传输保障的数据收发。

然而,传统数据收发的协议栈,即便是轻量级协议栈,时延也还是太高,因为一个控制器接收到写IO之后只有在成功将脏页推送给对端节点镜像之后才能返回给主机写应答,所以,缓存镜像过程的时延非常重要,越低越好。传统的网络数据收发协议栈时延高的一个很大原因在于其需要至少一次内存拷贝(用户态程序buffer到内核协议栈顶层buffer),实际上根据情况不同可能需要多次拷贝。重量级协议栈比如TCPIP的传输保障状态机是在主机OS内核执行,这个又得增加处理时间。

与此相比,还有另一种时延更加低的传输数据的方式,即零拷贝方式,这也是目前Linux下IO栈的实现方式。所谓零拷贝,就是说底层设备驱动会直接将用户态buffer中数据的基地址的物理地址通知给DMA引擎从而让外设直接从该地址将数据读入设备后发送到外部存储网络中。而上述零拷贝DMA操作,是发生在PCIE控制器和MC内存控制器之间的,那么,如果两台机器使用以太网或者IB网对连起来,是否可以让一台机器也直接将对方机器中的应用buffer里的数据DMA到自己应用buffer里呢?这就是RDMA,在此冬瓜哥不想再展开了,因为本文篇幅已经严重超长,冬瓜哥后续将写一篇详细介绍RDMA体系结构的文章,由于冬瓜哥太忙,近期恐怕没有时间,这次只能点到为止了。但是冬瓜哥写东西基本是一蹴而就,要写就一下子的事,文如泉涌,后会有期。

为何网络栈不能避免零拷贝?因为网络栈需要处理各种丢包重传、乱序重排等一系列事务,这些处理比较复杂,所以不直接在用户空间折腾,而是拷贝到内核空间来折腾。

IB时延低么?是低,但那是相对于以太网+TCPIP而言。IB在PCIE面前只能俯首称臣,不可否认RDMA逼格高,但是在DMA Over PCIE面前,依然被一砖撂倒。IB卡依然要经过PCIE才能进入系统IO总线,如果抛弃IB直接使用PCIE来传数据,那当然是更加快捷了。所以,不少系统直接采用PCIE直连或者PCIE Switch来进行DMA操作,每个节点通过NTB机制各自映射其他节点的内存到本地地址空间,节点将要向其他节点发送的数据写入对方在本地地址空间内的映射窗口,从而被硬件路由到对方节点内存的对应地址中,之后本地通过Doorbell机制通知对方完成事件。普通CPU中的PCIE控制器体系中并不提供NTB,而仅在特定平台比如Intel Jasper Forest中提供NTB以及DMA Engine用以实现地址翻译和数据移动,对于使用普通平台CPU的产品,就必须增加PCIe Switch来实现多机之间的DMA。关于PCIE switch体系结构冬瓜哥如果后续有时间的话可以继续向大家分享。

然而,不是所有传统存储平台都能消受PCIE/Switch方式的DMA,因为PCIEswitch芯片目前仅有Avago和PMC两家提供,Avago的PCIE Switch用起来有诸多问题,而品质相对更好的PMC的PCIE Switch产品还没有全面量产。所以,目前使用成熟的RDMA over Converged Ethernet或者over IB的使用更多,硬件方面比较成熟,软件方面也有现成的RDMA库可供调用。

【主线2】多点同时故障

上文介绍了数据在多个控制期间传递的几种方式。在一个系统内,缓存有两份副本的话,就可以冗余一份副本的丢失,而如果有三份副本的话,就可以允许2份副本同时丢失。近期某厂商在其某多控存储产品里实现了允许2控制器同时失效的技术。其本质就是在控制器间做了3个缓存镜像副本。几乎是同一个周,另一家厂商同时宣布了一款同档次产品,其可允许三个节点同时故障,也就意味着其同一个Dirty状态的缓存page会在4个控制器内有4分副本,从接收数据IO的节点,同时向其他三个节点镜像3份副本,就可以允许3个节点同时故障了。这两家看上去是要死磕了,冬瓜哥分分钟就可以做个可容许7控同时损坏的方案出来。

【支线】师出同门

不得不说的是,这两家产品都与一个日系厂商的高端存储存在某种渊源。前者是一开始O之,拿回来拆解研究,几年后,弄出自己的类似产品,其很多理念上参考了这家日系但又有所改进。具体冬瓜哥就不扩展讲述了,待后续单独写一篇高端存储架构方面的文章。国内目前看来,有三家厂商有自己的所谓高端存储,其中两家是自研,另一家看上去像O的,但人家说不是,又不出来给大家布道。

国内的纯自研存储厂商宏杉科技也有自己的高端存储产品,16控全对称架构,16控制器共享后端所有磁盘,而并非低逼格的分布式架构可比,是目前国内唯一采用全对称共享架构的高端存储系统,在冬瓜哥的上一篇文章中,冬瓜哥曾经说过,对称+共享的集群架构属于”高雅“派,而ServerSAN分布式架构,则属于市井派。

【主线3】对称式多活架构下的缓存镜像有什么特殊之处

对称多活架构下没有Lun Ownership,是全对称双活,任何节点收到数据在镜像到对端的同时,可以自行处理,包括算xor,make dirty,flush等一系列动作。对称式多活架构下,虽然也可以做成像非对称架构那样双方各自保有对端的Dirty页,同时自己单独处理自己的Dirty页的形式,但是对称双活是需要两边对称处理同一份数据的,所以多数实现都是直接把双方的缓存实时相互镜像,数据部分两边通过同步复制+锁来保持时刻一致。

同步加锁的最方便的方式就是针对每个数据块设置一把锁而且只能存在唯一的一把,放在唯一的位置。比如针对某段缓存,可以设置成以0KB~64KB为一个单元,任何节点想要对其进行写入或者任何更改操作,则必须先抢到针对这64KB块的锁。

然而,加锁是否有必要?比如,如果有两个目标地址相同的写IO同时各自到达不同的控制器,则此时任何一个控制器可以使用PCIE write withlock这个PCIE事务来确保将这整个块完整的且不被其他IO乱入的写入到对端的缓存内,在这期间,这个块是不能被其它控制器写入的。写入结束之后,其他IO才可以继续写入该块。利用这种PCIE事务,再加上同步缓存镜像,可以在保证数据块不被撕裂交织写入的前提下,实现天然的保序及一致性。比如A控先接收到针对某地址的写IO块,但是由于种种原因,尚未来得及将其同步到B控缓存相应页面之前,B控又接收到了针对同一个地址或者地址段有重叠的写IO块,而B控反应速度很快,立即将这个IO块利用PCIE带锁的写事务将数据同步到了A控缓存相同偏移量处,也就是覆盖了上一个A控收到的写IO,A控之前尚未完成缓存镜像,所以A控随即再将这块数据同步到B控同样偏移量处,这一步对数据一致性没有影响,无非是多复制了一次数据,这里可以做一些优化,比如通过一个集中的状态位来避免多余的拷贝过程。

如果按照上述理论,双控实现分布式锁机制,A控存储和管理奇数块的锁,B控偶数块,那么如果A或者B控突然挂掉怎么办?数据是有镜像,可以保证不丢失,但是锁的状态呢?不过还好,剩余的控制器发现与他配合分摊锁管理的节点当掉之后,会主动把所有的锁拿到本地管理,也就是奇数锁节点宕机,偶数锁节点动态生成一份奇数锁的副本。如果是多控系统,则系统动态选举另一个节点来分摊这些奇数块的锁管理。锁和缓存数据块,是可以完全分离的,拥有某个数据块脏页,不意味着这个块的锁必须在这个节点管理。

【主线4】加锁及原子操作

然而,上述方案并不可行,PCIE每次读写事务最大数据量为4KB,但是实际实现多数采用了256或者512Byte,所以,带锁的PCIE读写操作,并不能防止4KB以及更大数据块的整体一致性,如果不加锁,这个块或许会被撕裂,导致不一致。所以,还是要集中加锁。如上文所述,每个块的锁只能有一把,但是系统内有多个控制器,每个都有自己的RAM,那么锁放到哪里呢?可以集中在一个节点存放,也可以按照某种规则分开存放,比如奇数编号的块的锁放在A控,偶数的则放在B控,每个人都到对应的锁所在位置抢锁,抢锁过程中会用到Test and Set或者又称为Compare and Set操作,锁的本质就是一个bit,为0表示没有人要操作这个块,为1则表示有人正在操作这个块,所以,先把锁读出来判断如果为0,则表示无人操作,则立即写一个1进去占有这把锁,如果读出来发现是1,则原地等待一段时间或者不断读出来判断(相当于不断向蹲坑的人吆喝“完事没我憋不住了”)。问题是当某个人读出来发现是0,还未将1写入之前,另一个人也读出来发现是0,然后两个人分别写了1进去,此时就是脑裂,两个人都认为其占有了锁,最后导致数据不一致。所以,“将锁读出来”这个动作,本身也要对这把锁先进行加锁操作,而这就是个死循环,对此,硬件提供了对应的指令,比如CAS指令,某个人只要读取了这把锁,在写回1之前,硬件保障不能有任何其他人也读入这把锁,底层硬件就是将系统访存总线锁定,对于非共享总线的的CPU体系,就得在内部器件中维护一张锁表来加锁。这些在执行期间底层不允许被他人乱入指令,被称为原子操作。

然而,正如冬瓜哥在本公众号(大话存储)另外一篇文章《集群FS架构深度解析》中所说一样,并不是每个产品最终都实现为上述的那种粒度极为细致的完全对称的协作架构的,都是有所取舍,下面我们就来看一下现实中的产品取舍之后的样子。

【主线5】业界的对称多活产品对锁的实现粒度

HDS AMS——读写缓存全镜像+真对称

HDSAMS存储系统是业界率先支持对称双活的中端存储系统,其采用了缓存同步复制,不管是从磁盘读出的数据,还是前端主机写入的数据,都同步镜像到对方控制器缓存中,相当于双控缓存中的数据部分完全一致,包括相对地址偏移量也都是一致的。理论上讲,读缓存不需要镜像,可以两边各读各的,但是这样做会增加复杂性,会导致同一个磁盘块可能处于两个独立缓存的不同位置,需要两边交换各自的映射表,也就是先得把两边的映射表相互给给它镜像了才可以,而且在写缓存镜像的时候会多一轮查表过程。如果想避免查表,则可以使用hash等算法进行多路组关联方式进行磁盘到缓存的映射,但是又会增加冲突,导致频繁换页,影响性能。所以,要么选择占内存耗费CPU资源,要么选择换页冲突,都不太合适。所以,两边保持所有数据时刻一致也是一种折中做法,一般来讲,一个控制器读入的数据,另一个控制器也拥有一份,一定程度上也可以接受,因为IO的访问多数时候都有局部性,在一个控制器命中的页,下一个时刻很有可能会在另一个控制器也命中。但是这种方式的确是浪费了缓存空间,不管有多少个控制器,缓存等效可用空间只有单个控制器的容量。

VNX——写镜像+假对称

EMC在其DMX高端存储中采用了读写全镜像的方式,当时还被HDS的售前攻击,倒头来HDS的AMS反倒自己把读写全镜像了。再回来说VNX。VNX的对称式双活,其实是个假的。业界对对称式双活的定义是:多个控制器可以同时处理同一个Lun的IO。但是,这个定义却让VNX给钻了空子。如果把一个Lun切分成多个切片,比如2GB,而每个2GB切片倒头来还是有Owner,也就是所有针对某个2GB切片的IO必须转发给Owner节点处理,两个控制器分别均摊其中一半数量的切片Owner,那就不会存在锁的问题,大大简化了开发,还成功忽悠了市场。这招够绝的吧。

宏杉——写镜像+细粒度真对称

国内有家存储厂商名曰宏杉科技,由H3C存储原班人马组建,是国内第一个推出Raid2.0产品的厂商,以至于后续另外某厂不得不弄出个Raid2.0+,其实至今冬瓜哥也不知道这个加号是什么意思,谁知道可以告诉瓜哥一声,要干货。

宏杉科技所推出的高端存储,最大支持16控,其采用的是对称式多活+共享后端存储方式的”高雅“架构,在这个浮躁的年代,大家都去玩市井的Server-SAN了,能保持高雅架构的人不多了。底层按照Cell(其实就是分块)作为管理单位,Cell没有Owner,任何控制器都可以直接处理任何Cell,无需转发IO,采用分布式锁设计,块粒度为64KB。难能可贵的是,宏杉存储对读不镜像,每个节点上预读入缓存的数据可能都不相同,充分利用了缓存空间。然而,宏杉并没有透露其如何实现全局缓存管理,多个节点各管各的读写缓存,是个很复杂的事情,因为要实现缓存一致性,没有点深厚功底和研发实力,这块是没人敢碰的。

【主线6】多控间的全局缓存管理

正如《三体》中所描述的场景一样,两个点耦合之后的状态是确定的,而三个点对称耦合在一起,其状态成了不确定态。如果多个控制器实现读写全部镜像,那不会有问题,比如一个8控系统,任何一个控制器要写入某个块,加锁之后,向所有控制器相同偏移量处写入对应的块,数据冗余7份,浪费太大。一般是两两循环镜像,比如在8控内实现两两镜像,例如控1镜控2互相镜像,控3控4相互镜像,而如果控1接收到针对某数据块的写IO操作,目标数据块在控3上被读缓存了,那么控3的这块缓存就要被清掉,因为已经不能用了。做到这件事,很复杂,首先,所有控制器必须知道所有控制器缓存目前都缓存了哪些数据块,其次任何一笔更新操作都要同步广播给所有节点,实现cache coherency,这套机制异常复杂,这也是多核心多CPU之间的机制,甚至为了过滤不必要的流量,还需要考虑将节点分成多个组,每个组之前放一个过滤器,这就更复杂了。

所以,多控间想要实现真正均匀对称的全局缓存,而且保证性能和一致性,工程上几乎不可能,除非不计成本。现实中,都是做了妥协的结果,有人保持点高雅,有人则彻底简单粗暴,但是所有产品几乎都对外展示出一副很高雅的模样。本文较长,冬瓜哥在此就不再展开了,可以关注冬瓜哥后续的讲高端存储架构方面的文章。

【主线7】常用的集群锁方式

综上所述,集群是个如此复杂的系统,尤其是对称式协作集群。下面是对各种集群锁管理方式的一个总结,由于冬瓜哥水平有限,错误在所难免,也希望各位指正。

[集中式锁]:找一台或者几台节点单独管理所有的锁,所有节点都到此加锁。典型实现就是基于Paxos算法的比如Chubby、Zookeeper。

[分布式锁]:集群中的所有节点都兼职承担锁管理节点,按照某种规则,比如hash、奇偶数等静态或者动态算法,每个节点只承担部分数据块/对象的锁管理任务,静态分担算法实现简单,并且方便故障恢复。

动态算法比较复杂,比如,某个节点接收到某个数据块的写IO足够多次,则该数据块的锁就被迁移到该节点来管理。这种情况下,每个节点必须知道某个数据块到底该去哪里申请加锁,而且节点故障之后,其他节点必须有渠道来获知这个节点之前管理的是那些数据块的锁,其机制较为复杂。有两种方式可以实现,第一种,每次锁位置的变化向所有人同步,所有人维护一张映射表;第二种:加锁时,把锁请求向所有人逐一发送,相当于敲开一个门就问一句“你这有没有管理xx数据块的锁?”,如果没有,就继续敲下一个的门。这种方式的典型做法就是Token Ring,任何一个节点想要更改某个数据块之前,先给所有人都申请一下“我要加锁这个块有人不同意么?”,这个请求会按照一个环的顺序游历所有节点,每个节点都会把自己的意见写入这个请求,同意(二进制1)或者不同意(二进制0),最终该节点收到了自己发出的这个请求,通过检查所有节点的同意或者不同意的bitmap,对其做与操作,如果结果为0,证明其他节点有人正在占有该锁。这种方式属于现用现锁的模式,所有人都不维护任何锁映射表,所有节点只知道自己目前拥有哪些块的锁,而不知道别人的。谁要加锁,谁就发一个token请求出去先看看有没有人占用。请求在Token Ring中只能朝着一个方向发送,否则会产生死锁或者脑裂。

[TDM锁]

加锁过程是个原子操作,原子操作的本质是一串连续的操作不能够被打断,被其他人乱入。还有一种理论上的方式,利用时分复用技术,也可以保证原子性。将时间切成多个时隙,每个节点占用一个时隙,在这个时隙中,只能该节点发出锁请求,其他人只能响应,而不能发出请求,这样就避免了多个人在一个共享的通道上同时发出锁请求导致的冲突或者死锁。不过,芯片内部这种方式实现起来比较简单,多个节点之间,保证时钟的同步是个难事,不过也不是不可能的,需要很复杂的技术,比如GSM无线网里就是使用GPS和修正来同步时钟。

冬瓜哥在努力保持着高逼格和原创,从冬瓜哥这拿走的东西,终生受用,不管你到哪个公司,忽悠谁的产品,或者被谁忽悠,你最终会发现,冬瓜哥用心弄出来的东西,才是货真价实、中立、有一说一的纯干货。

如果哪天冬瓜哥堕落到贴个厂商ppt或者ctrl+v个白皮书,每页下面写几行字评论的地步的话,请大家把冬瓜哥骂醒,在这个浮躁的环境下,瓜哥希望把真正有用的、不忽悠的,干货,分享给大家。有垃圾,就得有精品,有快餐,就得有正餐,有浮躁,就得有坚持。总要有人去做那些难以做到的事情,否则我们的社会会瞬间崩溃掉。

本文写作过程中获得了以下业界公司及专家的指点,在此深表谢意,排名不分先后:宏杉科技、Javen Wu、杨勇、雷迎春、刘爱贵。

本文转载须全文转载,包括二维码和所有图片文字,并注明出自“大话存储”公众号。长按识别二维码关注“大话存储”获取业界最高逼格的存储知识。看了好的请点赞/转发/红包,平时群里发红包装逼,不如把红包猛烈的砸向冬瓜哥吧!冬瓜哥后续会有更多高逼格的东西出炉。大话存储,只出精品。

长按图片发红包:

长按扫码关注“大话存储”