高可用是构建分布式系统的基石。一方面,出于成本考虑, 分布式系统往往采取比较廉价的硬件,其可靠性相对于小型机、专有硬件有很大的不足, 而分布式系统的规模一般比较大,假如硬件的可靠性只有三个9(99.9%), 一个1000台机器规模的集群每天将面临1台机器宕机的风险,在如此大规模的情况下,存储介质,比如硬盘可能会随时都有损坏,结点之间的网络可能随时都会有抖动,机房可能局部或整体断电,地区或数据中心可能会出现不可用,如果不在设计时考虑这些问题,当这些情况出现的时候,系统将很快处于不可用的状态;另一方面,分布式系统的程序在设计与实现上也更为复杂,软件上既要容错硬件的不可靠,同时软件自身也有可能有问题, 在对外部环境容错的同时需要考虑对软件BUG的容错。
OceanBase在设计之初就充分考虑了高可用问题,确保OceanBase有能力在这些异常出现后,仍然能最大可能的提供服务。
冗余是实现高可用的通用方法,为防止数据丢失,将数据冗余多份;为防止系统中某些节点故障,对某些功能节点进行冗余。 冗余带来的好处是可以抵御某些节点的损失,而带来的坏处则主要是成本、性能和一致性问题。
成本主要包括额外的存储、网络、机柜等硬件成本,这是构建高可用分布式系统的必不可少的开销,其总体成本较专有硬件仍然要低,因为专有硬件实际上也需要在内部对某些模块进行冗余以获取高可用性。 性能和一致性问题则是高可用分布式系统必须要处理问题,这两个问题直接影响整个系统正确性、延时与吞吐量。
在传统myql或oracle中,我们往往通过添加备机来实现高可用,且为了实现高性能和高可用,一般会使用“最大可用”模式:
主机尽力将数据同步到备机,而不管是否同步成功,当主机挂掉以后,将备机升级成主机以继续提供服务,这就意味着如果主机在宕机前有数据没有同步到备机,要么通过某种特殊的手段将数据从宕掉的主机同步到备机,要么就接受暂时的数据不一致而继续提供服务,在这种情况下,如果出现主机永久损坏,则会造成数据丢失:
为了解决这个问题,可以使用最大保护模式(早期的MySQL版本不支持),即主机将日志同步到备机成功后再应答客户,这样会引入另外一个问题,即备机故障或网络抖动会导致失败,影响可用性; 小微引入了共享存储,将数据库redo log放在共享存储上,主机宕机以后,备机需要确保主机所有的数据都已经同步才能对外提供服务:
这样在主机宕机时,备机作一些额外的检查后升级为主机继续提供服务,这样可以确保数据一致性,但引入了共享存储。传统的主备模式还有另外一个问题是主备之间无法通过自身决出主备,需要人工切换或者使用一个第三方组件:
但是又引入了HA模块的稳定性问题,如果HA模块和主机的网络不通, HA将不能识别主机是活着还是网络有问题,此时HA如果进行切换而主机还活着则会造成后果很严重的双主问题。
故障可以分为单机故障(磁盘、内存等硬件介质损坏或单台软件Crash都属于单机故障),机架/机房故障(比如整个机架或机房的电源断电)以及地区/数据中心(比如地区地震造成该区网络隔离)故障,一般来说,故障单位越小,出现频率越高,而除非自然灾害,一个地区出现故障的概率极小,故障单位越小,实现高可用的难度和成本越低,故障单位越大,由于引入环境、距离等因素,实现高可用的难度和成本会呈指数倍增长。比如为了预防单机故障,只需要在本机房预备冗余节点,故障时通过某些技术方案,可以做到实时切换;而为了预防数据中心故障,则需要在另外一个地区预备冗余的数据中心,在故障时由于通信距离等原因,基本上无法做到无损切换。
OceanBase虽然在设计之初就考虑到了硬件和软件的不可靠,但OceanBase的高可用并非一蹴而就,在实现过程中,为了快速实现或者绕过某个暂时无法攻克的技术难点,会进行综合权衡,只实现一些出现概率较高的基本高可用策略,而通过人肉或其它手段来保证在某些很难出现的灾难出现后可以尽快恢复。然后通过逐步迭代,逐渐完善,将高可用的范围提高,比如OceanBase最初的时候只考虑单机故障的高可用,到目前为止已经可以实现同城IDC之间的容灾。
分布式系统为了设计与实现的简单,往往会在系统中设置一个全局单点,来负责相对比较轻量的管理任务, OceanBase中的rootserver就是这样一个角色;它主要负责集群中其它角色的管理并作为用户的入口,通常其压力不高且无本地数据需要存储,所需信息都可以通过其它角色的汇报来重建。
而作为一个分布式数据库,OceanBase面临着两个很难解决的问题:数据一致性与分布式事务,在早期的OceanBase版本中,采取的策略是暂时绕过这两个问题,等技术积累到一定程度后再回过头来解决,所以在OceanBase中另外增加了一个单写入节点,这个节点的压力很高,数据无法通过其它节点来恢复,我们需要保证这些单节点的高可用。另外一个是保存基线数据结点的高可用,这些结点被设计成可以弹性伸缩,本身具备高可用属性,但仍然需要考虑磁盘故障以及数据副本之间的一致性。 我们会在下面的章节分别描述对这两类节点的高可用策略。
在早期OceanBase的版本中,主要依靠主备来为单点提供高可用, 使用两台机器,其中的一台角色为主的机器对外提供服务,另外一台机器做为热备, 当主机挂掉后,备机切换为主,继续提供服务。
如前所述,这种“最大可用”模式的主备机制主要有两个问题:第一个问题在于这两台机器无法通过自身来决出主备,必须要依赖于一个第三方组件,早期我们使用了HA(linux-ha.org) 来做为仲裁组件,HA使用VIP机制,两台机器共享VIP, 同一时刻VIP只会加载在其中的一台机器, VIP会提供给外部应用程序作为OceanBase集群的入口地址,即VIP加载在哪一台机器上,该机就会作为主对外提供服务,程序可以通过不断检测VIP是否存在来判断本机是否为主机,当HA通过我们提供的检测程序检测到主机故障后,就会将VIP切换到备机, 此时外部请求就会路由到原来的备机,原来的备机检测到VIP“飘”到本机后,会将自己的角色置为主:
使用HA主要有几个问题:
另外一个问题在于这种机制无法保证数据不丢失, 某些极端情况下需要停机恢复,如果有机器永久损失,则可能会造成数据的丢失,在某些业务上可能无法接受。
而Updateserver是OceanBase中至关重要的节点,其数据丢失直接影响用户,也不能通过其它类型节点来重建,所以Updateserver最早抛弃HA模式,而改为通过Rootserver来选主:
Updateserver每次写入都会生成一条日志,每条日志有一个惟一且单调递增的日志号, 各Updateserver向Rootserver汇报自己的日志号, Rootserver选取日志号最大的Updateserver为主并发放租约,备Updateserver同时需要向主Updateserver注册,由主Updateserver发放租约。Updateserver使用一主多备的形式, 每次写入必须要写入多数派个节点成功才能应答客户,写入请求首先发送到主Updateserver,主Updateserver生成日志并同步到备机,收到多数派个成功回应以后应答客户。如果收不到足够多的回应,则不会应答客户端,该条写入可能生效,也可能不生效。
由于要求写入多数派个节点才算成功,所以主备间的网络延迟不能太高,目前OceanBase要求updateserver主备分布在同城的不同IDC, 如果采取一主两备的模式, 最大可以容忍一个同城IDC故障。
当某一台机器同步日志失败时,主机会将其剔除在恢复之前不再向其同步日志, 这对网络要求很高,如果网络连续出现抖动,则会造成可用性问题。在最新版本OceanBase, 将同步日志的方式也改为Paxos方式,一条日志只需要写到多数派个结点上成功便为成功,不需要各台备机顺序回应,进一步容忍短暂的网络问题。
虽然Updateserver去掉了对HA的依赖,但Rootserver仍然需要HA来选主,由于HA无法部署在两个IDC,所以我们对IDC之间的容灾使用的策略是在另外一个IDC部署一个备集群,在主集群出现故障时,通过人肉的方式将备集群切换为主来提供服务。
基于这个原因,OceanBase 在0.5里彻底取消了基于HA的主备机制,而是通过使用类似paxos的算法来进行选举:
让程序自身投票来决出主备, 当一台机器得到多数派的认可,它即可以成为主,这样系统能容忍一定数量节点的不可用,比如,如果是2台,则不能容忍有机器宕机,3台则可以容忍一台机器宕机, 3台机器可以部署在不同的机房以容忍机房故障。
Updateserver仍然通过Rootserver来选主,但这样也存在一个问题,当Updateserver和Rootserver同时故障的时候,Updateserver必须要等Rootserver恢复完成后才能恢复,增加了故障恢复的时间 。在后续的OceanBase版本中,将去除Updateserver这个单写入节点,并将其选主的权力下放到自身,摆脱由Rootserver选主的局面。届时Rootserver的工作会更为简单,单点不会成为OceanBase的瓶颈。
Rootserver/Updateserver是通过冗余节点来进行容灾,备节点一般不提供服务或只提供有限的服务,基线数据则是通过冗余数据来实现高可用与扩展服务能力。通过冗余3~6份数据来提供更多的服务能力。冗余的数据不能存储在相同的机器上,以避免机器宕机后损失可用性。同时在可能的情况下,数据需要分布在不同的机架上,以抵御整机架断电或断网,OceanBase在早期的实现中,为了简化实现与对机器分布的要求,未考虑数据分布在不同的机柜,曾出现过整机架断网而造成服务不可用。
基线数据的副本数决定了一个集群同时有多少台机器可以宕机,如果使用三副本,则同时可以有两台机器宕机,每个基线数据结点都和Rootserver保持心跳,当该结点宕机以后,rootserver会检测到并根据目前系统中所拥有的副本数量启动复制,为了避免因网络抖动所带来的不必要的副本复制,我们设定在安全的情况下(比如剩余副本数大于1) 可以容忍副本丢失一段时间(比如8小时),当副本丢失超出该时长后才启动复制。
副本数的选择和集群中机器的数量、单机数据量以及数据恢复速度相关,一般情况下会选择至少3个副本,因为2副本情况下,如果出现一个副本丢失,集群需要立即启动复制,而此时集群可能正处于请求高峰期。阳老师有一个关于副本数选择的计算方法:
“假设总共有N个节点计算机,它们的平均无故障时间都是M,云计算系统对一个数据副本丢失并进行复制的时间为T,则一台计算机在T时间内出故障的概率是T/M,不出故障的概率是(1-T/M):
N台机器在该时间内都不出故障的概率是(1-T/M)的N次方;
N台机器在该时间内恰好有1台出故障的概率是:(1-T/M)的(N-1)次方T/MN;
N台机器在该时间内恰好有2台出故障的概率是:
(1-T/M)的(N-2)次方T/MT/MN(N-1)/(2*1)
因此,N台机器在该时间段内至少有两台机器故障的概率是:
P2(N, M, T)=1-都不出故障的概率-恰好1台出故障的概率
因此,N台机器在该时间段内至少有两台机器故障的概率是:
P3(N, M, T)=1-都不出故障的概率-恰好1台出故障的概率--恰好2台出故障的概率
因此假如N=1000,M=50,000小时,T=600秒,则
P2 (N=10台,M=50,000小时,T=600秒) = 5.0*10的-10次方;
P2 (N=1000台,M=50,000小时,T=600秒) = 6.1*10的-9次方;
P2 (N=5000台,M=50,000小时,T=600秒) = 1.4*10的-4次方;
P3 (N=10台,M=50,000小时,T=600秒) = 4.5*10的-15次方;
P3 (N=1000台,M=50,000小时,T=600秒) = 6.2*10的-9次方;
P3 (N=5000台,M=50,000小时,T=600秒) = 7.6*10的-7次方;
可以看出,当机器数量达到5000台时,至少3台机器出故障的概率低于百万分之一,而至少两台机器出故障的概率高于万分之一。因此采用3个数据副本时数据有比较高的可靠性。”
并计算出一个表格,假设故障时其它服务器以25MB/s的速度恢复丢失数据:
MTBF为机器的平均无故障时间,从表中可以看出三副本同时丧失的概率是极低的。
基线数据的另一个较为普遍的异常为磁盘损坏, OceanBase存储基线数据的角色为ChunkServer, ChunkServer并未使用RAID方式来使用磁盘,一块磁盘损坏就意味着永久损失该盘上的所有数据,需要Rootserver从另外的机器上进行复制。 当ChunkServer检测到该磁盘出错(读取或写入失败)时,会主动将该盘剔除并上报, 让Rootserver启动复制程序补足副本。
基线数据需要和增量数据合并而产生新的基线数据,这个过程是由每台ChunkServer各自完成自己所负责的数据分区,即相同数据的几个副本会各自己完成这个合并过程,为了保证各台ChunkServer合并出来的新基线数据是一致的,每台Chunkserver在汇报副本信息的时候需要同时汇报校验值,以检查副本是否一致,如果出现不一致的情况,则是软件有bug或数据有问题,ChunkServer保留两个版本,解决问题后回退到上一个版本进行重新合并。这种问题出现一般都是软件Bug, 根据之前的经验,出现这种情况的时候,用户有可能已经读到不正确的数据,而造成这种问题的主要原因是Updateserver节点的数据不一致,我们通过改造Updateserver的日志同步方式(由主备改为一主多备且要求多数派成功)和加强校验后,这个问题已经得到杜绝。
到目前为止,OceanBase可以部署在同城的不同IDC并容忍少数个IDC故障。OceanBase一般会在同城选择三个不同的IDC进行部署。 目前还无法做到跨数据中心的容灾。 主要原因是由于通信距离的增加,异地数据中心之间的网络延时较高,无法做到同步复制数据,而通过异步的形式进行复制则无法做到无损容灾,我们目前通过一些手段,比如实时拷贝数据库的redo log到异地数据中心,可以做到最坏情况只丢失几秒的数据。
相较于传统的主备模式,OceanBase可以容忍少数派的节点损坏不中断服务且不丢失数据,为很多需要高可用且不能丢数据的业务提供了共享存储以外的解决方案。
原文链接
本文为云栖社区原创内容,未经允许不得转载。