第一部分:复制链路


1.1 链路类型

链路,来根线连接起来,不就可以了么?没问题,裸光纤连起来,这就是链路,如果是一座楼内,甚至一个园区内,管井你可以随便用,铺设一根光缆,没多少钱。问题是本地和远端相隔太远,出了园区,你就不能随便在两个楼之间飞一根线了,必须租用电信部门提供的各种线路了,当然,点对点无线传输也是个可以考虑的路子,如果两楼之间相隔不太远且视觉直达,到可以考虑这种方式。电信部门有各种专用链路或者共享链路提供出租,最原始的一种就是裸光纤,两地之间直接通过电信部门部署好的光缆,其中分出两根纤芯给你,当然,电信部门会有中继站,负责对光信号的路径交叉及信号增强中继。因为不可能任意两点间都恰好有光纤直连,必须通过中继和交叉。有了裸光纤,你两端跑什么信号什么协议什么速率就随你了,只要光模块的波长功率合适,两端就可以连通。裸光纤不能走太远距离,一个是价格太贵另一个是电信部门也不可能租给你用,因为距离太远的时候,光缆资源越来越稀缺,不可能让你独占一根,要知道,电信部门使用DWDM设备是可以在一路光纤上目前复用高达80路光波的。实际上近距离传输也很少有人用裸光纤了,尤其是大城市,因为资源太稀缺,除非互联网这种体量的用户,其他基本都是租用与别人贡献的某种虚拟链路。比如使用ADSLEPON/GPONE1等最后一公里接入方式,上到电信部门的IP网,或者直接上到SDH同步环网,不同的方式和速率,价格也不同。

存储设备一般都支持iSCSI协议复制,那么此时可以接入使用以太网作为最终连接方式的比如ADSLGPON等,如果仅支持FC协议复制,要么增加一个FCIP路由器再接入IP网,要么使用局端提供的专用FC协议转换设备直接上SDH同步网。


1.2 长肥管道效应

链路的时延除了与距离有关之外,还与链路上的各个局端中继和转换设备数量有关。光在光纤中传播靠的是全反射,等效速度为每秒20万千米,而更多时延则是由信号转换和中继设备引入的,电信运营商的网络可分为接入网和骨干网,本地的信号比如以太网或者FC,先被封装成接入设备所允许的信号,比如GPON等,再视专线类型,上传到以太网交换机、路由器或者直接到局端骨干网入口设备比如OTN。在这种高时延链路之下,每发送一个数据包,要等待较长时间才能得到ack,此时如果源端使用同步复制,性能将非常差,链路带宽根本无法利用起来,太高时延的链路必须使用异步复制。应用端同步IO模式+同步复制,这种场景是吞吐量最差的场景,异步IO+同步复制,效果其实尚可,最好的还是异步复制(不管同步还是异步IO)。

不管是异步复制还是同步复制,如果使用了FCP或者TCP这种有滑动窗口的传输协议,那么难免会遇到传输卡壳,TCP有个最大可容忍未ACKbuffer量,传输的数据达到这个buffer,就必须停止发送,而必须等待ack返回,只要一卡壳,链路上这段时间内就不会有数据传送,严重降低了传输带宽,为此,专业一点的存储设备都要支持多链接复制,向对端发起多个tcp链接,在一条链接卡壳的时候,另一条正在传输数据,这个与数字通信领域常用的多VC/队列/缓冲道理是一样的,一个VC由于某种策略导致卡壳的时候,其他VC流量一样会利用起底层链路的带宽。


第二部分:双活数据中心


2.1 双活并发访问的底层机制

从存储系统的视角来看双活数据中心,怎么个双活法?应用首先得双活,应用不双活,就无所谓双活数据中心。也就是多个实例可以共同处理同一份数据,而不是各处理各的(互备,或者说非对称双活)谁坏了对方接管,支持多活的应用典型比如Oracle RAC、各类集群文件系统等,这些应用的每个实例之间是可以相互沟通的,相互传递各种锁信息及元数据信息,从而实现多活。一般来讲,这类多活应用,其多个实例一定要看到同一份数据,如果这同一份数据有多个副本,那么一定要保证着多个副本之间是时刻一样的(有些互联网应用除外,不要求实时一致性),这与多核心多路CPUCache Coherency思想是一样的(具体可以看本公众号最早的一篇文章聊聊CAPI“,所以冬瓜哥一直认为底层通了,一通百通)。所以,要么让这多个应用实例所在的主机通过网络的方式共享访问同一个数据卷,比如使用SAN(多活数据库比如Oracle RAC,或者共享式集群文件系统,这两类多活应用需要访问块设备)或者NAS(有些非线编集群应用可以使用NAS目录),用这种方式,数据卷或者目录只有唯一的一份而且天生支持多主机同时访问。在这个基础上,如果将这份数据卷镜像一份放到远端数据中心的话,而且保持源卷和镜像卷时刻完全一致(同步复制),那么多活应用就可以跨数据中心部署了,多活应用看到的还是同一份数据,只不过本地实例看到本地的数据卷,远端实例看到远端的数据卷,数据卷在底层用同步复制实现完全一致,这与在本地多个应用实例看到唯一一份数据卷副本的效果是一模一样的,这也就做到了多活,能够在整个数据中心全部当掉时,短时间内几乎无缝业务接管。


但是,实现这种双活,也就是让存储系统在底层实现源卷和镜像卷时刻一致,并不是那么简单的事情。首先,传统容灾数据复制技术里,业务是冷启动(灾备端应用主机不开机)或者暖启动(灾备端应用主机开机但是应用实例不启动,比如Windows下对应的服务设置为手动启动),这个极大节省了开发难度,灾备端存储系统所掌管的镜像卷,不需要挂起给上层应用提供数据IO,而只需要接收源卷复制过来的数据即可。而多活场景下,应用实例在灾备端也是启动而且有业务IO的,那就意味着,源卷和镜像卷都要支持同时被写入,而且每一笔写入都要同步到对端之后才能ACK,这种方式称之为双写,以及双向同步,只有这样,才能做到两边的实例看到的底层数据卷是一模一样的,而不是其中某个实例看到的是历史状态,而其他实例看到了最新状态,后者是绝对不能发生的,否则应用轻则数据不一致,重则直接崩溃。这种双写双向同步,看似简单,同步不就行了么?其实,不了解底层的人可能也就到这一步了,然后就去出去装逼去了,殊不知,很多坑你都没有填,说不定哪天就把自己给装死了。


存储端实现双写双向同步的第一个难点在于,如何保障数据的时序一致性。与单副本本地多活应用系统相比,双副本多活,也就是双活数据中心,两边的应用实例各自往各自的副本写入数据,如果A实例像A卷某目标地址写入了数据,那么当这份更新数据还没来得及同步到B卷之前,B实例如果发起针对同一个目标地址的读操作,B卷不能响应该IO,因为B卷该目标地址的数据是旧数据。B卷如何知道A实例已经在A卷写入了数据呢?这就需要复杂的加锁机制来解决,A端的存储系统收到A实例写入A卷的IO之后,不能够ACKA卷,它需要先向B端的存储系统发起一个针对该目标地址的Exclusive排他锁,让B端存储系统知道有人要在A端写数据了,然后才能向A卷中写入数据,如果B端的B卷针对此目标地址正在执行写入操作(由B实例发起,但是通常不会出现这种情况,应用实例之间不会出现两个以上实例同时写入某目标地址的操作,因为多活应用实例之间自身也会相互加锁,但是存储系统依然要考虑这种情况的发生),则此次加锁不成功,A端存储系统会挂住A实例的写操作,直到能够加锁成功为止,这里可以定期探寻也可以spin lock,不过在这么远距离上去spin lock恐怕性能会很差,所以不会使用spin lock的模式。加锁之后,B端如果收到B实例针对该目标地址的读或者写IO,都不能够响应,而是要挂住,此时B端存储系统要等待A端存储系统将刚才那笔针对A卷的写IO发送过来之后,才能够返回给B实例,这样,存储系统任何时刻都能保证对A实例和B实例展现同样的数据副本。同理,B卷被B实例写入时,也需要执行相同的过程,对A存储的A卷对应地址加锁,然后后台异步的将数据同步到A端。


第二个难点,就是如何克服高时延链路导致的IO性能降低。通过上面的描述,大家可以看到上文中有个坑没被填,也就是A存储在何时给A实例发送写成功ACK?是在向B端加锁成功后,还是在A实例写入的数据被完全同步到的B端之后?如果是后者,那就是传统的同步复制技术了,把这块数据同步到B端,是需要一定时间的,如果使用的是TCPIP方式传输,根据上文中的分析,还会出现卡壳,等待传输层ACK等,时延大增,性能当然差。但是如果A端在向B端加锁成功后立即给A发送写ACK,那么时延就可以降低,此时虽然数据还没有同步到B,但是B端已经获知A端有了最新数据这件事情,如果B实例要访问B端的这份数据,B存储会挂住这个IO,一直等到A将数据复制过来之后才会返回给B实例,所以不会导致数据一致性问题。也就是说,这种方式,拥有异步IO的性能效果,以及同步IO的数据一致性效果,两者兼得。而代价,则是丢失数据的风险,一旦数据还没有同步到B端之前,链路或者整个A端故障,那么B端的数据就是不完整且不一致的,要性能就注定要牺牲一致性和RPO


第三个难点:解决死锁问题。如果某个应用不按照规律来,该应用的两个实例在两边分别同时发出针对同一个目标地址的写IO操作给两边的存储控制器,两边会同时向对方发起加锁请求,这个过程中由于链路时延总是存在的,锁ack总会延时收到,导致两边同时对该地址加了锁,结果谁都无法写入,这便是死锁。解决这个问题就得找一个单一集中地点来管理锁请求,也就是让其中一个存储控制器来管理全部锁请求,那么无疑该存储控制器一定会比对端更快的抢到锁,不过这也不是什么大问题,本地访问永远先于对端访问,这无可厚非。


综上所述,双活或者多活数据中心方案里,存储系统的实现难点在于锁机制。与多CPU体系(也是一种多活体系结构)相比,多CPU在针对某个进程写入数据到Cache时,是不向其它节点加锁的,而只是广播,如果多个进程同一时刻并发写入同一个目标地址,那么就发送多个广播,谁先后到,覆盖先到的,此时CPU不保证数据一致性,数据或被撕裂或被循环覆盖,这个场景需要程序遵守规则,写前必须抢到锁,而这个锁就是放在某个目标地址的一个集中式的锁,程序写前使用举例来讲spin lock来不断测试这把锁是否有人抢到,没抢到就写个1到这个地址,而spin lock本身也必须是原子操作,spin lock对应的底层机器码中其实包含一条lock指令,也就是让cpu会在内部的Ring以及QPI总线上广播一个锁信号,锁住所有针对这个地址的访问,以协助该进程抢到锁,保证抢锁期间不会有其他访问乱入。存储系统其实也可以不加锁,如果上层应用真的两边并发同时访问同一个地址,证明这个应用实现的有问题,但是存储系统为了保证数据的一致性,不得不底层加锁,因为谁知道哪个应用靠不靠谱,多线程应用很多,程序员们已经驾轻就熟,但是多实例应用,非常少,出问题的几率也是存在的。


冬瓜哥很少提及具体产品,因为冬瓜哥认为底层通一通百通,不需要了解具体产品实现,所有实现都逃不出那套框架。但是为了迎合大家口味,冬瓜哥还是提一提产品比较好。可以肯定的是,HDS的双活,是确保将数据完全同步到对端之后(同时向对端加锁),才返回本端应用写ACK信号;而EMC Vplex所谓的基于目录的缓存一致性,则是使用了加锁完便ACK方式,这也就是其号称“5ms时延做双活”的底层机制。至于其他家的双活,逃不出这两种模式,爱是谁是谁,爱抄谁抄谁,冬瓜哥就不操心了。不过,基于目录的缓存一致性其实是出自CPU体系结构里的学术名词,EMC很善于包装各种市场和技术概念,连学术名词都不放过。不过,双活的这种一致性机制,与多核或者多CPU实现机制的确是同样思想,在MESI协议中,某个节点更新了数据,会转为M态(Modify),并向其他所有节点发起Probe操作作废掉其他节点中对应的cache line,这一点和上述思想是一致的,只不过M态的数据一般不用写回到主存,而是呆在原地,其他节点入有访问,则Mcacheowner节点返回最新数据,而不是将数据同步到所有其他节点上(早期某些基于共享总线的CPU体系结构的确是这样做的)。


2.2 双活与Server-SAN

能从双活扯到ServerSAN?是的,ServerSAN本身也是多活的,其实现机制类似于传统存储系统的双活方案,只不过其有分布式的成分夹杂在里面。目前主流ServerSAN实现方式是将一个块设备切片比如切成几个MB大小的块,然后用Hash算法来均衡放置到所有节点中,每个切片在其他节点保存一到两份镜像,主副本和镜像副本保持完全同步,不复制完不发送ack。由于时刻同步,所以ServerSAN可以承载多活应用,每个节点上都可以跑一个应用实例,所有实例看到时刻相同的存储空间,可以并发写入多个镜像副本,为了防止某些不守规矩的应用,多节点间也要实现分布式锁,也要解决死锁,所以一般使用集中式锁管理,比如zookeeper之类。如果把ServerSAN多个节点拆开放到多个数据中心,这就是多活数据中心了。所以你能看到,ServerSAN本身与双活数据中心都是同样的思想。