冬瓜哥手绘从多控缓存管理到集群锁
Posted 大话存储
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了冬瓜哥手绘从多控缓存管理到集群锁相关的知识,希望对你有一定的参考价值。
IT领域有个名言警句: “凡是说不清楚的,证明你压根就没懂!凡是怕说出来让被人把逼格抢走了的,证明你在吃老本,很快也会玩完,不如趁早转行重获新生“。 ——佚名。
本文分享缓存镜像、控制器间链路RDMA、缓存一致性、集群锁等知识大串接。是一篇超级大餐。吃顿垃圾快餐,抽盒烟,刷个微信甘愿被优衣库之流毫无逼格的网络炒作浪费你的时间,甘愿被娱乐至死,不如来学习知识吧,学习趁早,忽悠凭老,年轻时候不学习被或者主动浪费时间,到老了只能哭。
人们越来越浮躁,越来越关注市场、变化、八卦,这让一些大忽悠在这年头非常吃得开。冬瓜哥走的是另外一条路线,以不变应万变,十年磨一剑,认江湖风雨,认嬉笑怒骂,十年后,二十年后,江湖还是那片江湖,只不过物是人非,而只有亘古不变的经典,才能永流传,任它沧海或桑田。有些看似花哨的东西,昙花一现,皆为云烟。
【主线1】从双控的缓存镜像说开去
传统存储系统为了保持冗余,弄了两台服务器来处理IO。只要是两点或者多点协作,就是集群了,既然是存储集群,那就一定逃不出冬瓜哥上一篇文章(《集群FS架构终极深度梳理图解》)所给出的大框架架构。这两台服务器之间的协作关系有多重方式,但是不管哪种方式,都需要缓存镜像。
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内核执行,这个又得增加处理时间。
为何网络栈不能避免零拷贝?因为网络栈需要处理各种丢包重传、乱序重排等一系列事务,这些处理比较复杂,所以不直接在用户空间折腾,而是拷贝到内核空间来折腾。
然而,不是所有传统存储平台都能消受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块的锁。
如果按照上述理论,双控实现分布式锁机制,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体系,就得在内部器件中维护一张锁表来加锁。这些在执行期间底层不允许被他人乱入指令,被称为原子操作。
【主线5】业界的对称多活产品对锁的实现粒度
HDS AMS——读写缓存全镜像+真对称
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、杨勇、雷迎春、刘爱贵。
长按图片发红包:
长按扫码关注“大话存储”
强赠冬瓜哥真容:
(请注意,冬瓜哥不是西瓜哥,这是两个人,很多人给混淆了,冬瓜哥很早就叫冬瓜哥了,另外一个则代表某司而且是“参考”鄙人昵称以混淆视听搅混水的,所以冬瓜哥原本想低调行事,目前不得不高调出来以正视听了)
以上是关于冬瓜哥手绘从多控缓存管理到集群锁的主要内容,如果未能解决你的问题,请参考以下文章