带着问题学习分布式系统之中心化复制集
假若我说有三个节点(计算机)要维护同一分数据,如果你对分布式系统并不了解,那么你可能会有什么问题呢,我想可能有两个最基本的问题:
- 为什么同一份数据要保存多分?
- 这些节点数据要一致吧,否则同时从多个节点读的时候数据不一样?
第一个问题,为什么要同一分数据要保存多分,是因为分布式系统中的节点都有一定的概率发生故障,虽然单个节点的故障概率比较小,但当系统规模不断上升,故障的概率就变大了许多。节点的故障会对系统的可用性、可靠性产生影响。当数据在系统中只有一份存储时,如果发生断电、主机crash、网络故障,那么导致的是数据的暂时不可用;如果发生磁盘损坏,则会导致数据丢失,也就是系统不可靠。因此数据冗余(复制集、副本集),即同一分数据在系统中不同节点保存多分,可以有效提高系统的可用性和数据的可靠性。当然,数据冗余也有一些缺点,比如占用额外的带宽和存储资源。
第二个问题一下就命中了复制集的要害,也是分布式系统中对复杂的问题之一: 副本的一致性问题,即从系统外部读取系统内部各个副本间的数据在一定的约束条件下是一致的。在这篇文章中介绍过CAP理论,即对于分布式数据存储,最多只能同时满足一致性(C,Consistency)、可用性(A, Availability)、分区容错性(P,Partition Tolerance)中的两者。而分布式系统中网络分割一定是存在的,所以CAP -- “it is really just A vs C!"
同时必须注意的是,A和C都是一个度的问题,而不是0和1两个极端,因此可以在满足一定的可用性同时保证一定的一致性。不同的需求决定了到底是一致性重要还是可用性更重要,比如在购物网站中,可用性非常重要,分分钟的不可访问都会带来巨大损失;而在《带着问题学习分布式系统之数据分片》中提到的元数据管理,一致性就非常重要,不然就乱套了。
为了解决第二个问题,人们提出了很多不同的副本控制协议,即在分布式系统中,按照特定的流程规范来读写副本数据,使得副本满足一定的可用性和一致性的要求。
副本控制协议有很多,也有不同的分类标准,比如:同步与异步、强一致性与弱一致性、中心化与去中心化。本文主要介绍中心化副本控制协议,在讲解不同的系统实现时,也分提到在同步与异步、强一致性与弱一致性方面的选择。
本文地址:http://www.cnblogs.com/xybaby/p/7153755.html
中心化副本控制协议
所谓的中心化,就是对于副本集的更新操作有一个中心节点来协调管理,将分布式的并发操作转化为单点的并发操作,从而保证副本集内各节点的一致性。其优点在于逻辑简单,将复杂的问题(分布式并发)转换成一个有成熟解决方案的问题(单点并发)。但缺点在于,副本集的可用性依赖于中心节点,如果中心节点故障,即使有中心节点自动切换机制,也会出现数10秒的不可用。
大多数的分布式存储都会采用中心化副本控制协议,比如GFS,TFS,MongoDB
而去中心化则是说副本集中没有中心节点,所有节点的地位是平等的,大家都可以接受更新请求,相互通过协商达成数据的一致。去中心化副本控制协议的最大好处在于可用性比较强,只要有大多数节点存活就能提供服务。但缺点时协议流程复杂,尤其是需要强一致性保证的时候。
在业界中,Dynamo,cassandra就是基于去中心化协议,虽然 Dynamo 尝试通过引入 Quorum 机制和 vector clock 机制解决读取数据的一致性问题,但其一致性模型依旧是一个较大的问题。
本文主要对中心化副本控制协议进行详细介绍。
中心化副本控制协议在分布式存储系统中使用非常规范,但各家实现又有不同。这里主要集合分布式文件系统与分布式数据库来做对比分析
回顾《带着问题学习分布式系统》一文,对于中心化副本控制协议,提出了以下疑问,本章节试图给出解答。
(3.1)写节点怎么将变更的数据同步到其他节点,同步还是异步;
(3.2)非写节点能否提供读数据,如果能够允许,会不会读取到过时的数据。(3.3)主节点是怎么产生的,当主节点宕机的时候,怎么选择出新的主节点。是有统一的复制集管理中心(记录谁主谁次,各自的状态),还是复制集自己选举出一个主节点?
主从节点数据更新流程
第一个问题:复制集之间数据的同步是同步模式还是异步模式。
在中心化副本控制协议中,主节点(primary)提供写入操作,数据会同步到其他节点。注意,上面语句中第一个同步是指复制集中节点间数据趋于一致的过程。
所谓同步(Synchronous replication),就是说对于客户端请求,系统阻塞到复制集中所有节点都更新完成,才能向客户端返回,即write all。而异步(Asynchronous replication)模式,只要一个或者部分节点更新则算写入操作成功,通常是write one。
上图(来源于Distributed systems for fun and profit,下同)即为同步模式,客户端的请求被发送到s1这个副本集,s1将请求转发给s2、s3,等s2、s3都操作完成之后再向客户端返回结果。
在同步模式下,系统的可靠性非常好,只要有一个节点正常,就能保证数据不丢失。但是系统的更新可用性非常差,只要有一个节点异常,就无法完成更新;而且,响应延迟比较大,取决于副本集中网络延时最大、处理速度最慢的节点。
上图则是异步模式,客户端的写请求只要在一个节点上完成就立即向客户端返回结果。在上图中,数据被写入到s1即认为写入成功,向客户端返回,系统在后台由s1向s2、s3节点同步数据。
异步模式下,系统的吞吐量会比较好,但是存在数据丢失的风险,比如上图中,如果在数据同步到s2 s3之前s1挂掉,那么刚才客户端的更新就丢失了,关键在于客户端认为已经写入成功了。另外,异步模式下,客户端在写入成功之后,立刻从系统读取数据,有可能读不到最新的数据,比如上图中,客户端写入s1之后立刻从s2 读取。
在数据同步的时候选择同步模式还是异步模式呢,这个取决于系统对一致性、可用性、响应延迟的要求。
比如在分布式文件系统GFS中,需要保证复制集内副本的强一致性,而单次读写的响应延迟并没有那么重要,因此选择了同步模式,即primary需要等到所有的secondary都写入成功才会向客户端返回。
而在分布式数据库MongoDB中,决定权交给了用户,用户可以决定使用同步模式还是异步模式。在《CAP理论与MongoDB一致性、可用性的一些思考》一文中详细介绍了writeconcern这个写入选项。如果w:1.那么只会写入到primary节点就立即返回,系统会在后台向secondary节点同步数据,即为异步模式。如果w:N(N为复制集节点数目),那么primary节点需要等到所有secondary都更新到最新的数据之后(或者等待超时)才向客户端返回,也就是同步模式。即使在去中心化副本控制协议,如cassandra,也提供给用户自行设定一致性等级。
前面已经提到了同步模式、异步模式各自的优劣,这里以MongoDB为例具体讨论,看看同步、异步模式对系统一致性、可用性的影响。
在异步模式(w: 1)下,系统的响应延迟很低,可用性非常好,但存在两个问题。第一:同一个客户端在得到成功写入的返回之后立即从secondary节点读取,有可能读不到最新的数据;第二:在主从切换的时候(后面会详细讲解这一过程),可能发生rollback,简单来说,数据只持久化到了primary,secondary节点还未更新到最新的数据,此时如果primary故障,系统会选举出新的primary,即使旧的primary恢复正常后以secondary身份重新加入复制集,新的primary不会承认其数据,这就导致了更新丢失的问题。
同步模式下(w:N或者w:Majority),需要等待所有节点都写入成功,响应延迟会比较高,在数据库应用中一般很难接受,之前基于《通过一步步创建sharded cluster来认识MongoDB》中的复制集(一个primary、一个secondary、一个arbiter)做过实验,如果将secondary shutdown(db.shutdownServer),然后用writeConcern: {w: "majority”, wtimeout: 10}写入数据,客户端会阻塞到超时,然后给出超时信息。不过,w:majority保证了写入的数据不会丢失,即不会出现rollback的问题。
第二个问题:数据的流向
即数据是如何从Primary节点到secondary节点的。
首先是比较有意思的GFS,GFS写入流程如下:
GFS的写入将控制流与数据流分开,客户端会把数据流链式推送到各个节点,推送的过程并不关心谁是primary、谁是secondary。需要注意的是,各节点(GFS中称之为chunkserver)只是缓存数据,等到Primary向secondary发送持久换指令(step5)的时候再回真正持久化写入。
更一般的,数据的流向有两种,链式与主从模式。
链式就是指从一个节点推送到最近的节点,比如GFS,“最近” 可以用IP地址或者节点间心跳TTL来衡量,如图所示(图片来源于清华阿里-大数据课程的PPT):
不难看出,写入过程中每个节点的带宽利用都比较均衡,可以充分利用网络资源,也不会有单点压力;但是需要经过多个节点,写入延迟会比较大。
而主从模式则是指数据同时从primary节点到secondary节点,如图所示(来源)
默认情况下MongoDB也是采用的链式模式,但是可以通过设置 settings.chainingAllowed = false 来采用主从模式。在主从模式下,Secondary会从Primary拉取OPLOG并应用到本地。显然,在这种模式下Primary节点的带宽压力比较大,但是写入延迟会小一些。
第三个问题:部分节点写入失败了怎么办
不管是同步模式还是异步模式,也不管是链式推送还是主从模式推送,复制集中数据的写入都是1PC(1 phase commit)。即数据的更新只有一个commit阶段,而没有prepare阶段,如果某些节点发生故障,那么提交在故障节点上会失败,甚至是提交了部分、不完整的数据。
复制集中多个节点的更新本质上来说应该是分布式事务问题,理论上应该保证原子行:要么都更新成功,要么都不更新,而不会出现部分节点更新成功的情况。但经典解决方案如两阶段提交代价太大,因此分布式存储中的复制集更新大多采用best effort 1pc,只不过不同的系统对更新失败的处理有所区别。
比如MongoDB,以第一个问题中提到的例子,writeconcern为w: majority,由于其中一个secondary挂掉,写入操作是不可能成功的。因此,在超时时间到达之后,会向客户端返回出错信息。但是在这个时候直接连接到rs的primary节点,数据是持久化到了primary节点,不会被回滚。
另外,对于分布式图片存储haystack,如果更新失败,会重试流程,直到成功或超时,重试的话所有节点都会重试。那么可能出现两个问题,某个节点上数据部分写入(写入部分数据就崩溃了);由于重试是对复制集中所有节点重试,因此某个节点上同一份数据可能写入了多份。对于第一个问题,由于有checksum,因此不怕部分写入失败;第二个问题,由于有offset和元数据,重复写入也不是问题。haystack(以及GFS)通过这种巧妙的方式解决了分布式更新问题。
主从节点数据读取
复制集中,不同的系统在数据读取方面有两个问题。第一:secondary节点是否提供读服务;第二,如果可以从Secondary读取,那么这个接口是否开放给用户
第一个问题,如果secondary节点提供数据读取服务,那么是否会读取到过期的数据(即不是最新成功写入的数据) ?比如在异步写入的时候,客户端得到成功写入的返回之后,立即去secondary上读取,那么有可能读到过时的数据,这对于强一致性的情况是不能允许的。我们知道,元数据的管理一般也是复制集,而元数据需要保证强一致性,因此,元数据的写入一般都是同步的。比如GFS中,master由一个active(也就是primary节点)、多个standby(也就是secondary节点)组成,在元数据写入到active的时候,要保证本地和远程机器都写入成功才能返回;而且只有active提供读取服务。
第二个问题,如果复制集中的节点都能提供读取服务,那么接口是否提供给最终用户呢?在haystack中,多个在不同机器上的物理卷组成一个逻辑卷,一个逻辑卷就是一个复制集。当读取请求到达的时候,是由haystack的元数据服务器(directory )根据负载均衡的原则选出提供服务的物理卷,即用户是不知道读取请求是落地到哪个物理节点的。而对于mongodb,用户可以在查询语句里面指定是从Primary读取,还是从Secondary读取,或者让系统来选择(Nearest)。
读取方式与用户角度的一致性非常相关,比如在MongoDB中,不同的readrefence导致一致性、可用性的差异,具体可见《CAP理论与MongoDB一致性、可用性的一些思考》
主节点选举
在中心化副本控制协议中,这个中心(primary)是怎么选出来的呢?是上级指定还是民主选举呢?
GFS系统中,Primary节点是由master(GFS中的元数据服务器)通过lease机制选择的,关于Lease机制的,可以参见《带着问题学习分布式系统之数据分片》一文中相关章节的介绍。简单说来,GFS给某个节点颁发Lease,该节点就成为了Primary节点,Primary节点也可以在过期之前重新申请Lease,而且Lease的颁发、申请信息都是在chunkserver与master的心跳中,因此也不会带来过多额外的开销。使用Lease机制能很好的避免在复制集中出现双主(同时有两个节点认为自己是Primary)现象。
而在Zookeeper、TFS、MongoDB中,都是通过去中心化的协议选举出Primary节点,选举出Primary节点之后,就变成了中心化的副本控制协议,当Primary出现故障之后,会重新选举过程。对于民主选举,两个因素非常重要:第一是强一致性,只能选举出一个Primary;第二个是可用性,选举过程要越快越好。
为了达到强一致性,需要使用分布式一致性协议,目前较为常见的协议有Paxos协议,该协议可以实现所有备份均可以提供对外服务,并且保证强一致性,通过理论和实践检验可以达到分布式的要求。Raft协议则是Paxos的一种特化,在这个协议的实现中,备份间需要区分主从角色,只有主节点可以提供对外服务,协议实现简单高效,能很容易的同各种分布式数据一致性同步场景相结合,是工程实现最好的选择。
在mongodb3.2中,Primary选举算法改成了raft-like算法,此举的目的也是为了缩短选举的时间,具体可见Replica Set Protocol Version。
在TFS中,meta server(元数据服务器)也是通过raft协议选举primary的,下面两个gif形象展示了primary初始选举以及当primary故障之后的重新选举(图片来源于清华阿里-大数据课程的PPT)。
上图展示了Primary选举过程
上图展示了在原来的Primary挂掉之后(也可能仅仅是失去响应),剩余的节点是如何选举出新的Primary。
为了防止出现双主的情况,在投票过程中至少要有超过半数的节点同意才能选出Primary,这也是为什么复制集中节点数目都是奇数个的原因。
总结
本文介绍了在分布式存储领域使用得比较广泛的中心化副本控制协议,通过不同的系统回答了在具体实现方便上的一些选择。不过,对于具体的系统以及相关的协议,并没有很深入介绍,感兴趣的读者可以查阅相关链接。
references
刘杰:分布式原理介绍
Distributed systems for fun and profit
经典论文翻译导读之《Finding a needle in Haystack: Facebook’s photo storage》
大数据平台核心技术(里面有TFS的介绍)