[关闭]
@Catyee 2021-08-16T09:47:09.000000Z 字数 17745 阅读 386

Zookeeper

分布式理论


一、什么是zookeeper

ZooKeeper是一个分布式协调服务,用来协助解决其它分布式系统的一些共有问题,其它的分布式应用程序可以基于ZK来实现数据发布与订阅、负载均衡、命名服务、分布式协调与通知、集群管理、Leader选举、分布式锁、分布式队列等功能。

二、zookeeper集群中的角色

一个zookeeper集群中的节点有三种角色:

Leaner学习者:和leader进行状态同步的节点统称Learner,上述Follower和Observer都是Learner。

三、zookeeper的数据结构

zookeeper的数据模型和文件系统特别相似,是一个树状结构,树中的每个节点叫做ZNode,每一个节点可以通过路径来标识。Zookeeper的整个树状结构都是直接存放在内存中的,当然也会进行持久化存储到文件系统,但是持久化只是为了保证故障重启之后能够恢复到故障前的状态,在zk启动初始化结束之后不会像关系库那样检索文件系统来获取节点状态。

znode节点从大方向上来分可以分为持久节点和临时节点,当然细分的话就多了:

ZooKeeper中为数据节点引入了版本的概念,每个数据节点都具有三种类型的版本信息:
version是当前数据节点内容的版本号,cversion代表子节点的版本号,aversion代表ACL变更的版本号。这个版本号的概念和传统的版本号概念不太一样,它表示的是修改次数,比如version代表当前节点数据的修改次数,cversion表示对子节点的修改次数,aversion表示ACL的变更次数。

四、ZAB协议

Zab协议是zookeeper的崩溃恢复/原子广播协议。Zookeeper通过Zab协议来完成领导者选举以及状态同步,从而使集群始终能够提供一致性的服务,并且各节点的状态也是一致的。
从名字也可以看出来zab协议包含了两方面的内容:一种是集群启动或者崩溃恢复时候的领导者选举过程,另外一种是集群正常服务的时候领导者向学习者同步自己的状态。

4.1 一些重要概念

4.2 崩溃恢复模式

4.2.1 什么情况下会触发选举的过程?

4.2.2 选举过程

选举的过程就是投票的过程,投票大家都经历过,我们可以用投票来进行类比,投票的时候我们总是投票给能力最强的人,但如果我们不知道谁能力最强,那我们就认为自己能力最强,先投票给自己,如果之后投票过程发现了有比自己能力更强的人的话再该票,投票结束后看投票箱中谁的投票最多就选谁为领导者。

投票过程中我们总是投票给能力最强的人,在zookeeper集群中哪个节点能力最强呢?我们可以通过事务id(zxid)来进行判断,谁的事务id越大就表示数据越新,谁的能力就越强,如果事务id是一样的就比较服务器id(sid),最终要投出数据最新的那个节点来作为领导者。

每张选票至少包含两项信息:一个我投票的这个节点的服务器id(sid),另外一个就是我投票这个节点的最大的事务id(zxid),有了这两个信息其它节点就知道我投给了谁,并且我投票的这个节点的能力如何。

Zookeeper集群中的某一个节点在开始进行选举时,首先认为自己的数据是最新的,会先投自己一票,并且把这张选票发送给其他节点。与此同时这个节点也会接收其他节点的选票,当接收到其他节点的选票后,可以根据选票信息中的事务id和自己当前所投的节点的最大事务id进行比较,如果其他节点选票中的事务id较大,那自己就要改票,改投给自己接收到的这张选票中的节点,改票的结果也要发送给其它节点。

和人类投票过程不一样的是zookeeper投票过程中没有一个公共的投票箱,而是每个节点都有一个投票箱,节点会将自己的选票和接收到的其它节点的选票放入到这个投票箱,开始选举之后,一个节点会不断的收到其它节点发来的选票,每一次都会和自己当前的选票pk,将自己的选票修改为事务id最大的节点,当自己的投票加上投票箱中的和自己相同的投票超过了一半,还会最后进行验证,验证主要是看是否还会接受到其它投票,如果一段时间没有接收到其它投票,那这个节点就被选中为领导者了。选中之后要进行广播,各节点更改自己的状态。

4.2.3 状态同步(数据同步)

仅仅只是选举出领导者还不能对外提供服务,还要先进行状态的同步,使得Follower节点和新的Leader节点达成一致的状态,只有这个过程结束之后才能对外提供服务

当选举出新的Leader之后,新的Leader节点会为每一个Follower节点都准备一个队列,并将那些没有被各Follower节点同步的事务以Proposal消息的形式逐个发送给Follower节点,在每一个Proposal消息后面还会紧接着发送一个Commit消息,以表示该事务已经被提交。等到Follower节点将所有它尚未同步的事务都从Leader服务器上同步过来并成功持久化到本地日志之后,整个恢复模式就结束了,集群进入原子广播模式,对外开始提供服务。

详细同步过程如下:

4.2.4 Leader和Learner要同步哪些数据

数据的同步的目的是为了让Follower和Leader在状态上保持一致。在同步之前可能出现的情况:

在Leader上,数据会保存在几个地方:
a、 日志文件中(txnlog):数据最新
b、 快照中(snapshot):数据新度有延迟
c、 CommittedLog队列:保存的是Leader节点最近处理的请求(相当于日志,日志是持久化在文件中的,而CommittedLog是在内存中的)

当Follower节点向Leader节点发起同步数据请求时,Learner会把它目前最大的zxid发给Leader,Leader则会结合自身的信息来进行判断,然后告知Follower如何同步数据。

参考:ZAB协议恢复模式-数据同步

4.3 原子广播模式

原子广播模式就是对外提供服务的模式。每一个节点都可以接收来自客户端的请求,但是只有leader节点才能处理写请求,其它节点会把写请求转给leader节点。

4.3.1 Leader处理写请求的过程

某个ZookeeperServer在处理写请求时,主要分为以下几步:

以上是单个ZookeeperServer执行写请求的步骤,那么,集群在处理写请求时只是在这几步之上做了修改。

Zookeeper集群处理写请求时,主要分为以下几步:

4.3.1 广播过程中保证事务的顺序性

在整个消息广播过程中,Leader服务器收到一个写请求后,会为这个写请求生成对应的事务Proposal和生成一个全局单调递增的事务ID(即ZXID),并且会把事务写入自己的事务日志,然后开始进行事务广播,在广播过程中,Leader服务器会为每一个Follower服务器都各自分配一个单独的队列,然后将需要广播的事务依次放入到这些队列中去,并且按照先入先出(FIFO)策略进行消息发送。每一个Folower服务器在接收到这个事务之后,都会首先将其写入自己的事务日志之中,在成功写入后反馈给Leader服务器一个Ack响应。当 Leader服务器接收到超过半数Follower的Ack响应后,Leader自身先写日志,然后自己提交事务并响应客户端,然后广播一个Commit消息给所有的Follower服务器来通知他们进行事务提交,每一个Follower服务器在接收到Commit消息后,也会完成对事务的提交。


五、Watcher机制

zookeeper引入了watcher机制来实现发布/订阅功能,能够让多个订阅者同时监听某一个主题对象,当这个主题对象自身状态发生变化时,会通知所有订阅者。

zookeeper中实现watcher需要有三个部分,分别是客户端线程、客户端WatchManager和ZooKeeper服务器三部分,简单地讲,客户端在向ZooKeeper服务器注册Watcher的同时,会将Watcher对象存储在客户端的WatchManager中。当ZooKeeper服务器端触发Watcher事件后,会向客户端发送通知,客户端线程就从WatchManager中取出对应的Watcher对象来执行回调逻辑。

5.1 watcher机制的特点

5.2 watcher类型

从触发机制来看:老版本都是一次性watcher,新版本(zookeeper 3.6.0)增加了持久性的watcher持久性递归的watcher,通过addWatcher()方法添加,可以指定AddWatchMode。

从事件类型来看:又可以分为dataWatcher、childWatcher、exitWatcher

5.3 watch事件的类型

EventType:None,None是一种特殊的事件类型,用来描述客户端与服务器连接状态的变化。由默认的defaultWather来处理对应的事件,当连接状态发生改变之后,一次性的watchers可能清空。defaultWatcher和客户端的生命周期一样,不会进行移除。

EventType:NodeCreated //节点创建
EventType:NodeDataChanged //节点的数据变更
EventType:NodeChildrentChanged //子节点下的数据变更
EventType:NodeDeleted //节点删除

5.4 不同的Watcher事件类型会触发哪些Watche

None

NodeDataChanged和NodeCreated

NodeChildrenChanged

NodeDeleted

5.5 不同的操作触发的Watcher事件类型

创建节点

删除节点

修改节点

注意NodeChildrenChanged事件不会触发persistentRecursiveWatches

六、Zookeeper中其它值得注意的设计

6.1 ACL权限模式

ACL即访问控制列表,是一种权限模式。zookeeper中ACL机制主要包括三方面的信息:
权限模式(schema)、授权对象(ID)、权限(permission),通常使用"schema:id:permission"来标识一个有效的ACL信息。

权限模式(schema):权限模式用来确定权限验证过程中使用的检验策略。包括IP、Digest(即用户名密码的权限形式)、World(即没有权限控制,或者任何人都有权限)、Supper(即超级用户)共四种权限模式。
授权对象ID:指的是权限赋予的用户或一个指定的实体,例如IP地址或者机器等等。
权限:具体的权限,有Create、Delete、Read、Write、Admin五种权限。

zookeeper支持权限扩展,所以开发者可以实现自定义的权限控制器。

6.2 节点之间连接顺序问题

在集群启动时,一个节点需要去连另外一各节点,从而建立Socket用来进行选票传输。那么如果现在A节点去连B节点,同时B节点也去连A节点,那么就会导致建立了两条Socket,我们知道Socket是双向的,Socket的双方是可以相互发送和接收数据的,那么现在A、B两个节点建立两条Socket是没有意义的,所以ZooKeeper在实现时做了限制,只允许服务器ID较大者去连服务器ID较小者,小ID服务器去连大ID服务器会被拒绝

6.3 顺序性保证

6.4 客户端如何获取可用的服务器地址

zookeeper客户端会把服务器地址随机打散,然后拼装成一个环形链表,然后如果当前服务器不可用了,就去尝试下一个连接。

七、session(会话)

zookeeper的RPC使用的是Nio,nio底层用的是tcp/ip协议,TCP协议通过SOCKET进行通信,首先需要3次握手建立连接,然后进行数据传输,最后4次回收断开连接,如果频繁的创建、关闭,是很耗费系统资源的,所以zookeeper使用seesion来管理tcp的长连接。

客户端启动时,首先会与服务器建立一个TCP连接,从第一次连接建立开始,客户端会话的生命周期也开始了,通过这个连接,客户端能够通过心跳检测和服务器保持有效的会话,也能够向ZooKeeper服务器发送请求并接受响应,同时还能通过该连接接收来自服务器的Watch事件通知。Session的SessionTimeout值用来设置一个客户端会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,只要在SessionTimeout规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效

ZooKeeper会话在整个运行期间的生命周期中,会在不同的会话状态之间进行切换,这些状态一般可以分为CONNECTING、CONNECTED、RECONNECTING、RECONNECTED和CLOSE等。

7.1 相关概念

7.2 Session管理

7.2.1 SessionID的生成

第一次生成的时候会根据机器的当前时间和机器的SID来生成,高8位代表了机器id,后56位是当前时间的毫秒进行的随机。以后的SessionID将每次增加1。

7.2.2 时间轮与分桶策略

SessionTrackerImpl**通过分桶策略来进行会话的管理,分桶的原则是将每个会话的"下次超时时间点"(ExpirationTime)相同的会话放在同一区块中进行管理**,以便于ZooKeeper对会话进行不同区块的隔离处理,以及同一区块的统一处理。

下次超时时间点的计算并不是简单的上次时间点加上间隔时间,而是采用了时间轮,目的是计算每个超时时间的槽位,相同槽位会分配到同一个桶中进行处理。

八、zookeeper如何应对脑裂

脑裂(split-brain)就是“大脑分裂”,在分布式集群中脑裂是指因为网络分区等一些原因导致集群选举出了多个leader,每个leader都认为自己是合法的,都对外提供服务,就像大脑分裂了一样。

zookeeper采用过半机制来应对脑裂问题,也就是说不管是选举投票,还是对提议的投票都要有超过一半的选票才能决议。

zookeeper的集群节点数量可以是偶数,但不管是奇数还是偶数都要超过一半节点存活才能对外提供服务,所以在容错能力相同的情况下,奇数节点更节省资源
比如说一个zk集群有3个节点,要想对外提供服务至多只能有一个节点挂掉,但假如这个zk集群有4个节点,至多也只能有一个节点挂掉,所以在容错能力相同的情况下,奇数个节点是更节省资源的。

九、zookeeper与cap理论

ZooKeeper中保证了CAP理论中的C和P,也就是一致性和分区容错性。对可用性并没有保证。主要有两方面因素:

1、不能保证每次服务请求的可用性。任何时刻对ZooKeeper的访问请求能得到一致的数据结果,同时系统对网络分割具备容错性;但是它不能保证每次服务请求的可用性(注:也就是在极端环境下,ZooKeeper可能会丢弃一些请求,消费者程序需要重新请求才能获得结果)。所以说,ZooKeeper不能保证服务可用性。
2、进行leader选举时集群是不可用状态。zookeeper选举leader的时间比较长的,这期间不会对外进行服务,zookeeper对于网络隔离问题比较敏感,导致系统对于网络的任何风吹草动都会做出激烈反应。一旦出现网络隔离,ZK就要发起选举流程,但是选举流程有比较长,造成本来半秒一秒的网络隔离造成的不可用时间被放大为选举不可用时间,这往往是不可忍受的。

正是因为这个原因,很多应用已经不再选择zookeeper来做分布式协调了,比如最新版本的kafka。

10、zookeeper的缺陷

1、选举时间长,且不能对外提供服务
2、节点数据大小的限制:对于Zookeeper来说,数据大小直接影响了它的读写性能,如果单个节点数据越大,对网络方面的吞吐就会造成影响,而zookeeper也添加了单个节点数据内容不能超过1M的限制。如果配置数据太大就只能用多个节点来存储,这样做增加了编程和维护的复杂性。
3、watcher机制:选择使用zookeeper做配置中心关键的一点就是watcher机制,但是watcher机制有一些要注意的点,一个是节点数据的版本变化会触发NodeDataChanged,注意,这里特意说明了是版本变化。存在这样的情况,只要成功执行了setData()方法,无论内容是否和之前一致,都会触发NodeDataChanged,导致无效的网络开销。
另外最主要一点就是zookeeper并不保证每次节点的变化都会通知到客户端,原因是对于一次性的事件,触发之后需要再次进行注册,客户端再次注册watch,如果节点数据的更新频率很高,在客户端处理事件到再注册的这个过程中,可能数据已经发生了许多次的修改。

九 zookeeper应用场景

9.1 数据发布订阅(配置中心)

当用zookeeper来作为发布订阅中心的时候,首先把数据写入zookeeper的一个节点,然后每台机器从这个节点获取初始化配置并启动,然后监听这个节点的数据更改事件,一旦该节点的数据发生变更,那么zookeeper就会向相应的客户端发送Watcher事件通知,监听的服务接收到这个消息通知之后,需要主动到zookeeper获取最新的数据。可以看到基于Zookeeper的配置中心是一种推拉相结合的方式。

缺点
1、受znode数据大小限制,不能存储太多的配置
2、无效的节点版本变化也会触发事件,导致网络开销增大

9.2 负载均衡

使用Zookeeper实现负载均衡,首先每个服务将自己的信息注册到zk上,实际上就是在一个指定的持久化节下中创建一个临时子节点,这个临时子节点中记录自己的信息。客户端监听持久化节点下的子节点事件,每次事件都从zk持久化节点上获取所有的临时节点,也就是最新服务节点信息,然后在本地使用负载均衡算法,随机分配服务器。本地的负载均衡策略包括随机选择构造成环轮询,或者按请求次数模上服务节点数量等等。

缺点:一是要在客户端实现自己的负载均衡算法。二是由于是临时节点,有可能出现闪断,这样客户端获取到最新服务节点可能并不是所有正常工作的服务节点,对负载均衡策略也有一些影响。

旧版本的kafka使用zookeeper来实现负载均衡

9.3 命名服务

比如需要实现一个分布式的全局唯一递增ID,zk中有顺序节点,顺序节点的名字中就有它的序号,这个序号就是就可以作为分布式的全局唯一递增id。

顺序节点原理:在ZooKeeper中,每个父节点都会为它的第一级子节点维护一份顺序,用于记录每个子节点创建的先后顺序。基于这个顺序特性,在创建子节点的时候,可以设置上这个标记,那么在创建节点过程中,Zookeeper会自动为给定节点名加上一个数字后缀,作为一个新的、完整的节点名。另外需要注意的是,这个数字后缀的上限是整型的最大值。


9.4 分布式协调/通知

比如canal就是使用zookeeper来做分布式的协调

心跳检测:基于zookeeper来做心跳检测就比较容易了,创建一个临时节点,不同服务可以通过这个临时节点来判断对应的服务是不是还存在。当然也可能出现闪断,解决思路是监听到一个临时节点的删除操作之后设置一个延迟时间,如果过了一段时间临时节点依然没有起来再当作心跳失败。或者注册的时候使用"服务器节点"或者"时间节点"来解决这个问题。

工作汇报:在一个常见的任务分发系统中,通常任务被分发到不同的机器上执行后,需要实时地将自己的任务执行进度汇报给分发系统。这个时候就可以通过ZooKeeper来实现。在ZooKeeper上选择一个节点,每个任务客户端都在这个节点下面创建临时子节点,这样便可以实现两个功能∶

1、通过判断临时节点是否存在来确定任务机器是否存活;
2、各个任务机器会实时地将自己的任务执行进度写到这个临时节点上去,以便中心系统能够实时地获取到任务的执行进度。

系统调度:使用ZooKeeper,能够实现另一种系统调度模式∶一个分布式系统由控制台和一些客户端系统两部分组成,控制台的职责就是需要将一些指令信息发送给所有的客户端,以控制它们进行相应的业务逻辑。后台管理人员在控制台上做的一些操作,实际上就是修改了ZooKeeper上某些节点的数据,而ZooKeeper进一步把这些数据变更以事件通知的形式发送给了对应的订阅客户端。

9.5 集群管理

所谓集群管理,包括集群监控与集群控制两大块,前者侧重对集群运行时状态的收集,后者则是对集群进行操作与控制。在日常开发和运维过程中,我们经常会有类似于如下的需求。

分布式日志收集系统:
在一个典型的日志系统的架构设计中,整个日志系统会把所有需要收集的日志机器分为多个组别,每个组别对应一个收集器,这个收集器其实就是一个后台机器,用于收集日志。对于大规模的分布式日志收集系统场景,通常需要解决如下两个问题。
变化的日志源
机器
在生产环境中,伴随着机器的变动,每个应用的机器几乎每天都是在变化的(机器硬件问题、扩容、机房迁移或是网络问题等都会导致一个应用的机器变化),也就是说每个组别中的日志源机器通常是在不断变化的。
变化的收集器
机器
日志收集系统自身也会有机器的变更或扩容,于是会出现新的收集器机器加入或是老的收集器机器退出的情况。

上面两个问题,无论是日志源机器还是收集器机器的变更,最终都归结为一点∶如何快速、合理、动态地为每个收集器分配对应的日志源机器。

使用ZooKeeper来进行日志系统收集器的注册,典型做法是在ZooKeeper上创建一个节点作为收集器的根节点,每个收集器机器在启动的时候,都会在收集器节点下创建自己的节点。待所有收集器机器都创建好自己对应的节点后,系统根据收集机器的个数,将所有日志源机器分成对应的若干组,然后将分组后的机器列表分别写到这些收集器机器创建的子节点上去。这样一来,每个收集器机器都能够从自己对应的收集器节点上获取日志源机器列表,进而开始进行日志收集工作。完成收集器机器的注册以及任务分发后,我们还要考虑到这些机器随时都有挂掉的可能。因此,针对这个问题,我们需要有一个收集器的状态汇报机制∶每个收集器机器在创建完自己的专属节点后,还需要自己的节点下面创建一个状态子节点,每个收集器机器都需要定期向该节点写入自己的状态信息。我们可以把这种策略看作是一种心跳检测机制,通常收集器机器都会在这个节点中写入日志收集进度信息。日志系统根据该状态子节点的最后更新时间来判断对应的收集器机器是否存活。


如果收集器机器挂掉或是扩容了,就需要动态地进行收集任务的分配。在运行过程中,日志系统始终关注着收集器根节点下所有子节点的变更,一旦检测到有收集器机器停止汇报或是有新的收集器机器加入(这里不要用监听,因为可能会有大量的事件,所以采用定期轮询的方式),就要开始进行任务的重新分配。无论是针对收集器机器停止汇报还是新机器加入的情况,日志系统都需要将之前分配给该收集器的所有任务进行转移。为了解决这个问题,通常有两种做法:全局动态分配和局部动态分配,全局动态分配,就是把所有要收集的任务重新均分,简单粗暴但是印象范围太广。局部动态分配是指每个收集器汇报状态的时候还汇报自己的负载,如果一个收集器挂了,就把挂了的这个节点的收集任务交给其它负载低的收集机器,如果新加入一个收集机器就把负载高的机器的一部分任务交给新的机器去做。

9.6 Master选举

先创建一个持久节点,然后在持久节点中创建同名的临时节点,谁创建成功谁就是master,其它机器监听这个临时节点的删除事件。
masterr选举如何应对脑裂问题,脑裂问题是指某个节点本来已经是master了,但是因为GC时间过长,或者网络原因导致与ZK的连接中断,这个时候临时节点会被删除掉,就会有其它节点被选为master,然后当前节点恢复了,它依然认为自己是master,即产生了脑裂。比如Hdfs的namenode是怎么解决脑裂问题的,namenode创建临时节点成功了,这个namenode就变为active的状态,另外一个就会变为standby的状态,成为active的namenode还会创建一个持久节点,记录自己的信息,如果这个namenode正常结束会去删掉这个持久节点,但是如果是异常退出的,这个持久节点就不会被删掉,但是临时节点会被删掉,这个时候standby的节点就会创建临时节点,准备变为active状态,但是它发现持久化节点还在,就会先尝试通知另外一个namenode,让它进入standby状态,如果失败,默认就会调用一个脚本杀死原来的namenode进程,让它重启,或者如果用户自定义了隔离脚本也可以调用自定义的隔离脚本。

9.7 分布式锁

独占锁
先创建一个持久化节点作为锁的根节点,然后如果某个服务需要获取锁就去这个根节点下创建一个临时的顺序节点,同时判断自己是不是所有节点中序号最小的那一个,如果是就代表自己获取到锁了,开始执行业务逻辑,如果不是就监听自己的前一个节点的删除事件,同时阻塞自己。如果前一个节点被删除,监听的这个节点将获取到通知,它要再次获取所有子节点,然后看自己是不是子节点中最小的,如果是就代表获取到锁了,唤醒自己开始执行任务;如果不是最小的,说明前一个节点异常删除了(可能是服务挂了),这个时候它还得继续监听前一个节点。

如何实现可重入性,客户端本地实现可重入就可以了,本地记录重入次数,重入次数减到0就可以去zk中删掉临时节点。

共享锁
共享锁的实现稍微复杂一点,同样是先创建一个持久化节点作为锁的根节点,然后创建临时节点来作为读写锁,用节点名字来区分读写锁,比如读锁就是R_序号,写锁是W_序号。尝试获取锁的时候也是先创建对应类型临时顺序子节点,然后获取到所有节点,并进行排序,然后根据自己是需要获取读锁还是写锁来做不同处理。

如果自己要获取一个读锁,然后发现自己前面也都是读锁,那就直接获取锁,如果发现前面有写锁,就监听这个写锁的删除事件,如果收到通知再次检测前面是否有写锁,如果没有了就获取到了锁,开始执行;如果还有,就继续监听前一个写锁的删除事件。

如果自己要获取一个写锁,只要自己不是最小的节点就没有获取锁,然后监听前一个节点,前一个节点被移除,收到通知之后再判断自己是否最小,是的化就获取锁。

可重入性依然在本地实现就可以了。

十、Canal

10.1 基础原理

canal的工作原理就是模仿Mysql Slave的交互协议,将自己伪装成一个Mysql的Slave节点,然后不断向Mysql的master节点发送Dump请求,Master收到dump请求之后,开始就会推送相应的binlog的数据给这个slave节点,也就是canal服务端,canal收到binlog数据之后解析出来就可以进行消费了。

canal server的执行单位叫instance,一个instance定义了需要同步哪个mysql数据库的哪些表,以及从什么位置开始同步,instance之间彼此隔离。canal server本身是多活的,可以安装多个canal server,但是instance只会运行在一个canal server里面,采用的是抢占的模式,为了保证高可用性,如果一台canal server挂掉了,这台canal server上运行的instance就会自动切换到另外某一台canal server的节点上运行。这个切换的过程就是利用zookeeper来控制的。

10.2 instance抢占式执行的具体实现

每个canal server启动之后都会去扫描instance的配置,如果发现了一个instence,会先去zookeeper中创建这个instance的节点,这个是一个持久化节点,这个节点里面还会记录同步的位点信息,另外会创建一个临时节点,里面会记录创建这个临时节点的canal server的ip和端口号,如果有多个canal server扫描到了同一个instance,谁先创建出临时节点并记录自己的信息,谁就执行这个instance,所以这个临时节点记录的就是正在运行这个instance的canal server。而其它的canal server发现临时节点已经创建出来了,并且上面记录的信息不是自己的,就会注册一个watcher,监听这个临时节点的删除事件,然后在这个canal server上的instance会陷入阻塞状态,直到监听到临时节点被删除,会再次尝试去创建临时节点,如果创建上了,自己就读取zookeeper上记录的这个instance的位点信息,并从这个位点继续往后解析。

10.3 这样设计存在的问题

可以看到这个设计有点类似于分布式锁,但是又不太一样,它不保证公平性,也没有重入的概念。这样设计可以保证一个instance总是只有一个canal server在运行,而且一旦崩溃可以由另外一个canal server来运行。但也有问题,实际上instance的运行是抢占式的,谁先抢占到,谁就会执行,可能部署了n个canal server,但是所有instance都第一个canal server给抢占到了。解决思路有两种,一是随机canal server的扫描时间,这样可以让每个canal server都有可能抢占到instance,但是这个只能减缓不均匀的问题,不能根除。另外一种思路就是抛弃这种抢占式的思路,进行任务的分发或者负载均衡,但是要处理canal server崩溃后的instance再次分发的情况,可能需要引入一个协调者,引入协调者有可能会出现单点问题。

10.4 canal如何解决闪断问题

闪断是指一个canal server已经开始正常运行某个instance了,但是和zookeeper发生了短时间连接中断,这个时候zookeeper会删除掉临时节点,如果没有任何处理,其它canal server会直接抢占这个instance,并开始执行,这个时候当前server重新连接上了zookeeper,结果发现临时节点记录的信息不是自己的了,它就必须停止自己正在运行的instance。如和解决这个问题呢?可以让canal server监听到删除事件之后,延迟一会儿再去抢占instance,而原先正在运行的这个canal server不会延迟,这样如果正在运行的这个canal server在延迟时间内重新连上了zookeeper,将继续占有锁,继续执行。其它canal server经过延迟时间之后,会去尝试创建临时节点,如果失败就会继续阻塞,并且重新注册监听事件。

10.5 canal client

canal client会连接canal server,并且从canal server获取数据,但是并不用显示的向canal client配置canal server的地址,只要canal client和canal server使用同一个zookeeper就可以了,canal client会去配置的instance节点下面获取到正在执行的这个instance的ip和端口号,然后进行连接,并且会注册临时节点的删除事件,这样当instance发生切换之后,client也能够及时感知到切换,然后重新连接切换之后的canal server。

canal client也可以实现自己的HA,实现原理就和canal server一样。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注