zookeeper的基本概念
zookeeper是一个开源的分布式协调服务,其设计目标是将那些复杂的且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一些简单的接口提供给用户使用。zookeeper是一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现诸如数据订阅/发布、负载均衡、命名服务、集群管理、分布式锁和分布式队列等功能。
zookeeper的应用场景
数据发布/订阅
- 数据发布/订阅系统,即所谓的配置中心,也就是发布者将数据发布到zookeeper的一个或者一系列节点上,供订阅者进行数据订阅,进而达到动态获取数据的目的,实现配置信息的集中式管理和数据的动态更新。
- 发布/订阅系统⼀般有两种设计模式,分别是推(Push)模式和拉(Pull)模式。在推模式中,服务端主动将数据更新发送给所有订阅的客户端,而拉模式则是由客户端主动发起请求来获取最新数据,通常客户端都采用定时进行轮询拉取的方式。
- ZooKeeper 采用的是推拉相结合的方式:客户端向服务端注册自己需要关注的节点,一旦该节点的数据发生变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知之后,需要主动到服务端获取最新的数据。
- 如果将配置信息存放到ZooKeeper上进行集中管理,那么通常情况下,应用在启动的时候都会主动到ZooKeeper服务端上进行一次配置信息的获取,同时,在指定节点上注册⼀个Watcher监听,这样一来,但凡配置信息发生变更,服务端都会实时通知到所有订阅的客户端,从而达到实时获取最新配置信息的目的。
命名服务
实现全局唯一id分配,在zookeeper中,每一个数据节点都能够维护一份子节点的顺序顺列,当客户端对其创建一个顺序子节点的时候 zookeeper 会自动以后缀的形式在其子节点上添加一个序号。
集群管理
集群管理包括集群监控与集群控制两大块,前者侧重对集群运行时状态的收集,后者则是对集群进行操作与控制。
- 集群监控:
-
- 如何快速统计当前生产环境下一共有多少台机器
-
- 如何快速获取机器的上下线情况
-
- 如何实时的监控集群中每台主机的运行时状态
zookeeper的两大特性
- 客户端如果对zookeeper的数据节点注册watcher监听,那么该数据节点的内容或者是其子节点列表发生变更时,zookeeper服务器就会向订阅者发送变更通知。
- 对于在zookeeper上创建的临时节点,一旦客户端与服务端之间的会话失效,那么临时节点也会被删除。 利用这两个特性,便可以实现集群机器存活监控系统。
分布式日志收集系统的实现
在一个典型的日志系统的架构设计中,整个日志系统会把所有需要收集的日志源机器分为多个组别,每个组别对应一个收集器,这个收集器起始就是我们的一个后台机器,专门用于收集日志。
对于大规模的分布式日志收集系统场景,通常要解决以下两个问题。
- 变化的日志源机器 在生产环境中,伴随着机器的变动,每个应用的机器几乎每天都是在变化的(机器硬件问题、扩容、机房迁移或是网络问题等都会导致一个应用的机器变化),也就是说每个组别中的日志源机器通常是在不断变化的。
- 变化的收集器机器 日志收集系统自身也会有机器的变更或扩容,于是会出现新的收集器加入或是老的收集器退出的情况。
实现步骤
-
注册收集器机器: 使⽤zooKeeper来进行日志系统收集器的注册,典型做法是在zooKeeper上创建⼀个节点作为收集器的根节点,例如/logs/collector,每个收集器机器在启动的时候,都会在收集器节点下创建自己的节点,例如/logs/collector/[Hostname]
-
任务分发: 待所有收集器机器都创建好自己对应的节点后,系统根据收集器节点下子节点的个数,将所有日志源机 器分成对应的若干组,然后将分组后的机器列表分别写到这些收集器机器创建的子节点(例如/logs/collector/host1)上去。这样⼀来,每个收集器机器都能够从自己对应的收集器节点上获取日志源机器列表,进而开始进⾏日志收集⼯作。
-
状态汇报: 完成收集器机器的注册以及任务分发后,我们还要考虑到这些机器随时都有挂掉的可能。因此,针对这个问题,我们需要有⼀个收集器的状态汇报机制: 每个收集器机器在创建完自己的专属节点后,还需要在对应的子节点上创建⼀个状态⼦节点,例如/logs/collector/host1/status,每个收集器机器都需要定期向该节点写⼊自己的状态信息。我们可以把这种策略看作是⼀种⼼跳检测机制,通常收集器机器都会在这个节点中写⼊日志收集进度信息。日志系统根据该状态⼦节点的最后更新时间来判断对应的收集器机器是否存活。
-
动态分配: 如果收集器机器挂掉或是扩容了,就需要动态地进⾏收集任务的分配。在运⾏过程中,日志系统始终关注着/logs/collector这个节点下所有⼦节点的变更,⼀旦检测到有收集器机器停⽌汇报或是有新的收集器机器加⼊,就要开始进⾏任务的重新分配。⽆论是针对收集器机器停⽌汇报还是新机器加⼊的情况,日志系统都需要将之前分配给该收集器的所有任务进⾏转移。为了解决这个问题,通常有两种做法:
- 全局动态分配: 这是⼀种简单粗暴的做法,在出现收集器机器挂掉或是新机器加⼊的时候,日志系统需要根据新的收集器机器列表,立即对所有的日志源机器重新进行⼀次分组,然后将其分配给剩下的收集器机器。
- 局部动态分配: 全局动态分配方式虽然策略简单,但是存在一个问题:一个或部分收集器机器的变更,就会导致全局动态任务的分配,影响面比较大,因此风险也就比较大。局部动态分配就是在小范围内进行任务的分配,如果⼀个收集器机器挂了,那么日志系统就会把之前分配给这个机器的任务重新分配到那些负载较低的机器上去。同样,如果有新的收集器机器加⼊,会从那些负载高的机器上转移部分任务给这个新加⼊的机器。
zookeeper原理
zookeeper是如何实现分布式数据一致性的?
zookeeper主要是依靠ZAB协议来实现分布式数据一致性的,基于此协议,zookeeper实现了一种主备模式的系统架构来保持集群中各副本数据的一致性,其表现形式就是使用一个单一的主进程来接收并处理客户端的所有事务请求,并采用ZAB的原子广播协议,将服务器数据的状态变更以Proposal的形式广播到所有的副本进程中,ZAB协议的主备模型架构保证了同一时刻集群中只能够有一个主进程来广播服务器的状态变更,因此能够很好地处理客户端大量的并发请求。但是,也要考虑到主进程在任何时候都有可能出现崩溃退出或者重启的情况,因此ZAB协议还需要做到当前主进程出现异常情况时,依旧能够正常的工作。
分布式一致性协议有哪些,各有什么特点?
最常见的2PC与3PC协议
- 2PC属于两阶段提交协议,也是解决分布式事务问题最常用的解决方案。它的主要流程主要是分为两个步骤,第一阶段由协调者向所有的参与者发送事务内容,并等待参与者响应是否可以提交的回应,如果出现等待超时或者有的参与者回应了失败,那么协调者将会对所有的参与者发送回滚请求,参与者根据undo日志将数据回滚到执行前的状态,回滚后向协调者发送ACK信息,在协调者收到了所有参与者返回的ACK信息后,将中断当前事务。
- 3PC属于三阶段提交协议,相比于2PC实现成本更高,难度也更大,它的实现原理是在2PC的基础之上增加了一个cancommit阶段,这个阶段主要是协调者向所有的参与者询问是否可以正常执行。
- 存在的问题?
- 同步阻塞问题: 2PC协议在二阶段提交时,如果参与者占用公共资源时,其它参与者都会处于阻塞状态,因为参与者没有超时释放资源机制,在3PC协议协调者与参与者都引入了超时机制便解决了占用公共资源超时问题。
- 单点故障问题: 协调者如果出现问题宕机的话,那么阶段二将无法继续执行,其它参与者将会一直处于锁定事务资源的状态中直到协调者重启后恢复。
- 数据不一致性问题: 协调者发送commit请求时宕机,导致部分参与者收到了commit请求。这个时候需要重启后不断重试提交,直到提交成功为止。也就是能够保证数据最终一致性,不保证实时一致性。
Paxos协议
- Paxos协议也称做Paxos算法,是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一。
- Paxos的版本主要有三个 Basic Paxos, Multi Paxos, Fast-Paxos, 具体实现只有基于Multi Paxos实现的ZAB协议与Raft协议。
Basic Paxos
角色介绍
- Client: 客户端,向分布式系统发起请求,并等待响应。
- Proposer: 提案的发起者,试图说服Acceptor对此达成一致,并在发生冲突时充当协调者以推动协议向前发展
- Acceptor: 决策者,可以批准提案,并进行投票,投票结果是否通过以多数派为准。
- Learner: 最终决策的学习者,学习者充当该协议的复制因素(不参与投票)
流程介绍,basic paxos流程一共分为4个步骤
- Prepare: Proposer提出一个提案,编号为N,此N大于这个Proposer之前提出的所有编号,请求Acceptor接受这个提案
- Promise: 如果编号N大于此前Acceptor之前接收的任何提案编号则接受,否则拒绝
- Accept: 如果达到多数派,Proposer会发出accept请求,此请求包含提案编号和对应的内容
- Accepted: 如果此Accpetor在此期间没有接受到任何大于N的提案,则接收此提案内容,否则忽略
如果多数派中的一个Acceptor发生故障,因此多数派大小变为2。在这种情况下,Basic Paxos协议仍然成功。
Proposer在提出提案之后但在达成协议之前失败。具体来说,传递到Acceptor的时候失败了,这个 时候需要选出新的Proposer(提案人),那么 Basic Paxos协议仍然成功
当多个提议者发生冲突时的basic Paxos 最复杂的情况是多个Proposer都进行提案,导致Paxos的活锁问题。
针对活锁问题解决起来非常简单: 只需要在每个Proposer再去提案的时候随机加上一个等待时间即可。
Multi Paxos
针对basic Paxos是存在一定得问题,首先就是流程复杂,实现及其困难,其次效率低(达成一致性需要2轮RPC调用),针对basic Paxos流程进行拆分为选举和复制的过程。
- 第一次流程,确定Leader
- 第二次流程,直接由Leader确认
Multi-Paxos在实施的时候会将Proposer,Acceptor和Learner的角色合并统称为"服务器"。因此, 最后只有"客户端"和"服务器"。
Raft协议
Raft协议相对Paxos协议来讲理解起来会简单很多,它对服务器的状态划分为三种,分别是
- Leader: 接受客户端写请求,写入本地后再同步到其它的副本当中。
- Fllower: 从Leader中接收更新请求,然后写入本地日志文件,对客户端提供读请求。
- Candidate: 如果Follower在一段时间之内没有收到Leader发送过来的心跳,就判定Leader可能发生了故障,发起选主提议,节点状态会从Fllower变为Candidate状态,直到选主结束。
ZAB协议
ZAB协议的核心是定义了对于那些会改变zookeeper服务器数据状态的事务请求的处理方式,即所有请求必须由一个全局唯一的服务器来协调处理,这种服务器被称为Leader服务器,余下的服务器则称为Fllower服务器,Leader服务器负责将一个客户端事务请求转化成一个事务Proposal(提议),并将该Proposal分发给集群中所有的Fllower服务器,之后Leader服务器需要等待所有Fllower服务器的反馈,一旦超过半数的Fllower服务器进行了正确的反馈后,那么Leader就会再次向所有的Fllower服务器分发Commit消息,要求将前一个Proposal进行提交。
ZAB协议的两种基本模式
ZAB协议包含两种基本的模式,分别是崩溃恢复与消息广播模式
消息广播模式
ZAB协议的消息广播过程使用原子广播协议,类似于一个二阶段提交过程,针对于客户端发起的请求,Leader服务器会将请求转化为一个事务Proposal(提议),并将该Proposal分发给集群中的所有Fllower服务器,等待超过一般的Follower服务器进行了正确的反馈之后,就可以开始通知所有Fllower服务器进行事务提交的信息了,但是,在这种简化的二阶段提交模型下,无法处理因Leader服务器崩溃退出而带来的数据不一致性问题,因此才有了崩溃恢复模式来解决这个问题,另外,整个消息广播协议是基于具有FIFO特性的TCP协议来进行网络通信的,因此能够很容易保证消息广播过程中消息接受与发送的顺序性。
在整个消息广播过程中,Leader服务器会为每个事务请求生成对应的Proposal来进行广播,并且在广播事务Proposal之前,Leader服务器首先会为每个Proposal分配一个全局唯一单调递增的id,简称ZXID,由于ZAB协议需要保证每个消息严格的因果关系,因此必须将每个事务Proposal按照ZXID的先后顺序来进行排序和处理。
消息广播具体过程:
- Leader服务器会每一个Follower服务器都各自分配一个单独的队列,然后将需要广播的事务Proposal依次放入这些队列当中去,并且根据FIFO策略进行消息发送。
- Follower服务器在接收到Leader服务器发送过来的Proposal后,都会首先将其以事务日志的形式写入到本地磁盘中,再写入成功后向Leader服务器发送一个ACK响应,这个阶段类似于mysql中innodb引擎中的Redo日志是一样的,2PC协议的第一阶段也是这么做的。
- Leader服务器收到超过一半Follower服务器的ACK响应之后,就会广播一个Commit消息给所有的Follower服务器以通知其进行事务提交,同时Leader自身也会完成对事务的提交,而每一个Follower服务器在接收到Commit信息后,也会完成对事务的提交。
崩溃恢复模式
ZAB协议的消息广播过程中一旦Leader服务器出现崩溃,或者由于网络原因导致Leader服务器失去了与过半Follower的联系,那么就会进入崩溃恢复模式。在ZAB协议中,为了保证程序的正确运行,整个恢复过程结束后需要选举出一个新的Leader服务器,因此,ZAB协议需要一个高效可靠的Leader选举算法,从而能够保证快速地选举出新的Leader,同时Leader选举算法不仅仅需要让Leader自身知道已经被选举为Leader,同时还需要让集群中的所有其他机器也能够快速地感知到选举产生出来的新Leader服务器。
ZAB协议规定了如果一个事务Proposal在一台机器上被处理成功,那么应该在所有的机器上都被处理成功,哪怕机器出现故障崩溃。在崩溃恢复过程中,可能会出现的两个数据不一致性的隐患以及ZAB协议针对这种情况是如何处理的?
- ZAB需要确保那些已经在Leader服务器上提交的事务最终被所有服务器提交
- ZAB协议需要确保丢弃那些只在Leader服务器上被提出的事务
-
- 怎么理解这句话呢,大概意思就是客户端发起的请求在Leader服务器上转成事务Proposal之后,还没有向Follower服务器进行广播就挂了,那么这个事务Proposal是需要被丢弃的,如果知道Leader选举的同学应该很快就能理解,每个Proposal都对应一个ZXID,在主服务器崩溃之后选新的Leader那么就是根据ZXID来判断,ZXID最大的服务器当选Leader服务器,因为ZXID最大证明它的数据是最全的,如果ZXID相同那就比较服务器本身配置的myid。解释到这里,如果只有Leader服务器有的Paoposal,Fllower服务器没有的,那么Leader崩溃之后新主不可能有这个Proposal,所以在原Leader服务器恢复之后需要把这个Proposal丢弃,这样才能保证新的Leader服务器拥有集群中所有机器最大ZXID的Proposal,即新Leader一定具有所有已经提交提案。
数据同步过程
崩溃恢复之后选出了新的Leader,在正式开始工作之前,Leader服务器会首先确认事务日志中的所有Proposal是否都已经被集群中过半机器提交了,即是否完成数据同步。所有正常运行的服务器,要么成为Leader,要么成为Follower与Leader保持同步。Leader服务器需要确保所有的Follower服务器能够接收到每一条事务Proposal,并且能够正确地将所有已经提交的事务Proposal应用到内存数据库中,具体的,Leader服务器会为每个Follower服务器都准备一个队列,并将那些没有被各Follower服务器同步的事务以Proposal消息的形式逐个发送给Follower服务器,并在每⼀个Proposal消息后⾯紧接着再发送⼀个Commit消息,以表示该事务已经被提交。等到Follower服务器将所有其尚未同步的事务 Proposal 都从 Leader 服务器上同步过来并成功应用到本地数据库中后,Leader服务器就会将该Follower服务器加入到真正的可用Follower列表中,并开始之后的其他流程。
zookeeper中服务器角色划分
Leader服务器
Leader服务器是zookeeper集群工作的核心,其主要工作有以下两个:
- 事务请求的唯一调度和处理者,保证集群事务处理的顺序性
- 集群内部各服务的调度者 Leader服务器是使用责任链模式来处理每个客户端请求的,Leader服务器的请求处理链如下:
- PrepRequestProcessor。请求预处理器,也是leader服务器中的第一个请求处理器。在Zookeeper中,那些会改变服务器状态的请求称为事务请求(创建节点、更新数据、删除节点、创建会话等),PrepRequestProcessor能够识别出当前客户端请求是否是事务请求。对于事务请求,PrepRequestProcessor处理器会对其进⾏⼀系列预处理,如创建请求事务头、事务体、会话检查、ACL检查和版本检查等。
- ProposalRequestProcessor。事务投票处理器。也是Leader服务器事务处理流程的发起者,对于非事务性请求,ProposalRequestProcessor会直接将请求转发到CommitProcessor处理器,不再做任何处理,而对于事务性请求,处理将请求转发到CommitProcessor外,还会根据请求类型创建对应的Proposal提议,并发送给所有的Follower服务器来发起⼀次集群内的事务投票。同时,ProposalRequestProcessor还会将事务请求交付给SyncRequestProcessor进行事务日志的记录。
- SyncRequestProcessor。事务日志记录处理器。⽤来将事务请求记录到事务日志⽂件中,同时会触发Zookeeper进行数据快照。
- AckRequestProcessor。负责在SyncRequestProcessor完成事务日志记录后,向Proposal的投票收集器发送ACK反馈,以通知投票收集器当前服务器已经完成了对该Proposal的事务日志记录。
- CommitProcessor。事务提交处理器。对于非事务请求,该处理器会直接将其交付给下一级处理器处理;对于事务请求,其会等待集群内针对Proposal的投票直到该Proposal可被提交,利用CommitProcessor,每个服务器都可以很好地控制对事务请求的顺序处理。
- ToBeAppliedRequestProcessor。该处理器有一个toBeApplied队列,用来存储那些已经被CommitProcessor处理过的可被提交的Proposal。其会将这些请求交付给FinalRequestProcessor处理器处理,待其处理完后,再将其从toBeApplied队列中移除。
- FinalRequestProcessor。用来进行客户端请求返回之前的操作,包括创建客户端请求的响应,针对事务请求,该处理器还会负责将事务应用到内存数据库中。
Follower服务器
Follower服务器是zookeeper集群状态下的跟随者,其主要工作有以下三个:
- 处理客户端非事务性请求(主要是读取数据),转发事务请求给Leader服务器
- 参与事务请求Proposal的投票
- 参与Leader选举的投票 与Leader服务器处理请求一样,Follower服务器也是采用责任链模式组装的请求处理链来处理每一个客户端请求,由于不需要对事务请求的投票处理,因此Follower的请求处理链会相对简单
与Leader服务器的请求处理链最大的不同点在于Follower服务器的第⼀个处理器换成了FollowerRequestProcessor处理器,同时由于不需要处理事务请求的投票,因此也没有了ProposalRequestProcessor处理器。
- FollowerRequestProcessor。其用作识别当前请求是否是事务请求,若是,那么Follower就会将该请求转发给Leader服务器,Leader服务器在接收到这个事务请求后,就会将其提交到请求处理链,按照正常事务请求进行处理。
- SendAckRequestProcessor。其承担了事务日志记录反馈的角色,在完成事务日志记录后,会向Leader服务器发送ACK消息以表明自身完成了事务日志的记录工作。
Observer服务器
Observer是ZooKeeper⾃3.3.0版本开始引⼊的一个全新的服务器角色。从字面意思看,该服务器充当了一个观察者的角色——其观察ZooKeeper集群的最新状态变化并将这些状态变更同步过来。
Observer服务器在工作原理上和Follower基本是一致的,对于非事务请求,都可以进⾏独立的处理,⽽
对于事务请求,则会转发给Leader服务器进行处理。和Follower唯一的区别在于,Observer不参与任
何形式的投票,包括事务请求Proposal的投票和Leader选举投票。简单地讲,Observer服务器只提供
非事务服务处理,通常用于在不影响集群事务处理能力的前提下提升集群的非事务处理能力。
另外,Observer的请求处理链路和Follower服务器也⾮常相近,其处理链如下:
另外需要注意的一点是,虽然在图中可以看到,Observer 服务器在初始化阶段会将SyncRequestProcessor处理器也组装上去,但是在实际运行过程中,Leader服务器不会将事务请求的投票发送给Observer服务器。