Zookeeper 基础学习
# ZK 是干什么的
分布式解决了用户量不断增加的问题,但是分布式所带来的问题也不少,比如在一个分布式系统中如何确保被 RPC 调用的机器存活呢?使用 TCP 三次握手来解决?不行,速度太慢。为此我们去使用一些机器来通知调用方被调用方是否存活
那这些机器需要什么功能呢,首先它们应该保证高可用,如果自身的高可用都不能保证又如何去维持分布式程序的高可用呢,因此它们应该进行集群配置,并且集群应该保证强一致性
为了管理分布式程序的各个机器,它们需要存放一些代表各个机器的数据,并且读数据的速度要快,因此他们的数据应该存放在内存中
最后它应该有一些通知回调功能,当被调用方挂了应该及时告知调用方发生了什么事情
最后,我们得出结论,ZK 提供分布式协同服务,解决出现在分布式系统中出现的一系列问题,它的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用
ZooKeeper 将数据保存在内存中,性能是非常棒的。 在读多于写的应用程序中尤其地高性能,因为写会导致所有的服务器间同步状态
拥有这些特性,它不止能做注册中心,还可以做分布式锁、命名服务等
# 数据结构
# 结构
使用多叉树结构,每个节点都可以存放数据,不过可以存放的数据比较少,最多只有1mb。它的数据存放在内存中,因此性能很好
zookeeper 中的节点叫 znode,根节点叫/,下面的节点一般以 /XXX,/XXX/XX 来命名,类似文件系统
# 类型
znode 分为两大类型(持久节点、短暂节点),四种类型,分别是:
- 持久:这个节点一直存放在 zookeeper 中,直到被 delete
- 短暂:这个节点被某个会话写入,当连接断开,这个节点消失,这个节点不可以创建子节点(session 被看作一个 tcp 长连接)
- 持久编号:这个节点有自己的 id 和持久节点的性质
- 短暂编号:这个节点有自己的 id 和短暂节点的性质
# stat 与 data
在 Linux 中这个命令用来获取文件属性,在 ZK 中做为一个名词也是相似的功能
每个 znode 除了存放数据,还会自动维持一个叫 stat 的数据结构,stat 中存放了这个节点的所有信息,比如事务 ID、节点创建时间、当前节点的子节点个数等
data 指的是节点存放数据的具体内容
# 监听器 watcher
监听器是观察者模式的一种实现,观察者模式指在出现一对多关系依赖的时候,会有一个观察者或者一群观察者在旁边观察,如果某个被依赖对象出现修改时,观察者会自动通知依赖它的对象
zkClent 代表客户端,它会创建一个 connect 连接 zookeeper,还有一个端口用来接受 zookeeper 传来的消息
客户端可以指定它要监听哪一个节点,当这个节点或者子节点发生变化时(可能是节点中数据变化或者节点直接变化),zookeeper 返回消息给客户端
zookeeper 使用一个链表来存放客户端以及它对应监听的节点
以上就是 zk 作为注册中心的原理,很简单吧,服务提供者在 zk 中对应一个个暂时节点,服务调用者会起一个 watcher 来监听这些节点,如果节点挂掉,暂时节点就会被删除,zk 会通知调用者
# 集群
为了保证高可用,可以以集群形态来部署 zookeeper,集群中使用 ZAB 协议保证数据一致性
# 架构
与传统的主从模式不同,ZK 使用 Leader、Follower、Observer 来实现集群
- Leader 用来实现数据的写入和读取,主要负责写入,一个集群中只有一个 leader
- Follower 只能进行数据的读取,如果客户端需要实现写入请求时,它会转交给 Leader 实现
- Observer 用来实现数据的读取,它不参加选举以及半数相关的操作,它用来提高集群数据读取效率。它可以处理客户端的读请求,并且会将写请求转发给 Leader,用于提高 zk 的吞吐量
# 选举机制
ZK 优先保证一致性(C)和分区容错性(P),而非可用性(A)
当 Leader 挂掉或者集群建立的时候需要选择 Leader,只有当前集群存活大于一半的主机才会执行此步骤,此时所有的服务器会按一定顺序进行选举,此时所有的机器处于 LOOKING 状态
选举机制的过程如下:节点进入 LOOKING 状态,向其他节点发送投票信息,这里包含自己的 myid、zxid、epoch,这三玩意是啥呢
- myid:每个节点的唯一标识(数字)
- zxid:事务 ID(64 位数字,高 32 位是 epoch,低 32 位是计数器)
- epoch:逻辑时钟,每次选举递增
然后每台机器都接收其他节点的投票,根据选举规则比较投票。优先比较 epoch,如果 epoch 一样比较 zxid,然后是 myid。这里也体现了 paxos 的思路,最大事务优先,然后比较机器 id
选举完毕 Leader 全量复制自己的数据给所有 Follower,复制完成后对外提供服务,如果有数据写入会进行增量复制
zk 集群如果出现了分区,过半机器会重新选举,同时只有超过半数节点存活的集群分区才能继续提供服务
# 一定要配置奇数台机器
zookeeper 集群中有这样一个特性,如果这个集群中超过一半的机器可用,这个集群可用。此外,选举中半数投票、半数写成功策略也是一半可以进行
因为这些特性,配置5台机器和配置6台机器允许的出错数量是一样的,所以配置奇数台机器可以省下一台机器的钱
同时,ZooKeeper 选举的过半机制还防止了脑裂。比如现在有一个由 6 台服务器所组成的一个集群,部署在了 2 个机房,每个机房 3 台。正常情况下只有 1 个 leader,但是当两个机房中间网络断开的时候,每个机房的 3 台服务器都会认为另一个机房的 3 台服务器下线,而选出自己的 leader 并对外提供服务。若没有过半机制,当网络恢复的时候会发现有 2 个 leader。仿佛是 1 个大脑(leader)分散成了 2 个大脑,这就发生了脑裂现象。脑裂期间 2 个大脑都可能对外提供了服务,这将会带来数据一致性等问题
ZooKeeper 的过半机制导致不可能产生 2 个 leader,因为少于等于一半是不可能产生 leader 的
# 一致性协议
在分布式环境中,一致性是指数据在多个副本之间是否能够保持一致的特性。 如果对第一个节点的数据进行了更新操作并且更新成功后,却没有使得第二个节点上的数据得到相应的更新,于是在对第二个节点的数据进行读取操作时,获取的依然是老数据(或称为脏数据),这就是典型的分布式数据不一致情况
一致性协议有一个重要前提,在不可靠信道上试图通过消息传递的方式达到一致性是不可能的
# 如何知道对方机器是否出现故障
zookeeper 有一个心跳帧(可以在配置文件中修改),默认为2秒,当机器之间发送消息的时候重新计时
在初始化连接时,超过10个心跳没有回应则认为对方挂了,在配置文件的对应参数为 initlimit
在正常连接时,超过5个心跳没有回应则认为对方挂了,在配置文件的对应参数为 synlimit
# Paxos 算法
Paxos 算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一
这个算法主要由提案者、表决者、学习者组成
首先每个提案者在提出提案时都会首先获取到一个具有全局唯一性的、递增的提案编号 N,然后将这个编号发送给所有的表决者
每个表决者中保存的已经被接受的提案中会存在一个编号最大的提案,将这个编号返回给提案者
如果提案者接受到了半数以上的回应,将这个提案的内容发送给所有表决者,表决者只会同意大于等于自身编号最大的提案,如果提案者接受到了半数以上的同意,向未批准的表决者发送提案内容和提案编号并让它无条件执行和提交
如果这个过程失败会递增该提案的编号
# ZAB 协议
ZAB 协议由 paxos 算法改编而来
简单来说就是进行修改时 Leader 问 Followers 是否同意更新,如果超过半数以上的同意那么就进行 Follower 和 Observer 的更新
Leader 内部维持着一个队列,在数据更新时会按照先进先出的形式发送数据给 Follower,这是因为请求处理的顺序不同就会导致数据的不同,从而产生数据不一致问题
ZAB 协议有两种模式:
- 崩溃恢复:当 Leader 崩溃后的选主过程,当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该 Leader 服务器完成了状态同步之后,ZAB 协议就会退出恢复模式。上文已经说过了
- 消息广播:集群中对数据同步的过程,是 2PC 的优化版本
消息广播的基本流程如下,客户端请求接收后会路由到 leader 节点,Leader 节点接收客户端写请求后再让,每个请求被赋予一个全局唯一且递增的 ZXID(事务 ID),然后向所有 follower 节点发送这个提案
从节点收到后会先写入本地事务日志,然后向 Leader 发送 ACK 确认。这里的事务日志是每个 ZK 节点上的二进制文件,不是 znode,数据仅在内存树(DataTree)中生效后才对客户端可见。事务日志只是预写日志(WAL),不直接影响服务数据
当 Leader 收到超过半数 Follower 的 ACK 后,向所有 Follower 发送 COMMIT 指令,同时自己也提交该事务。这时候访问 zk 就可以观察到这个新 znode 了
# 如何保证事务的顺序一致性
ZK 需要保证事务的顺序一致性,这是因为 ZK 是一个分布式系统,事务的执行顺序不能被打乱。举个例子,ZK 有很多的 Znode,每个 Znode 都有一个唯一的路径,比如 /a/b/c,当客户端对 /a/b/c 进行写操作时,ZK 会先检查 /a/b 是否存在,如果不存在就会创建 /a/b,然后再创建 /a/b/c。这时候如果有其他客户端对 /a/b 进行删除操作,那么 /a/b/c 就不能被创建,否则就会导致数据不一致问题
那 ZK 是如何处理该问题的呢?
Zookeeper 采用了全局递增的事务 Id 来标识,所有的 proposal(提议)都在被提出的时候加上了 zxid,zxid 实际上是一个 64 位的数字,高 32 位是 epoch 用来标识 leader 周期,如果有新的 leader 产生出来,epoch 会自增
低 32 位用来递增计数,代表了提案 ID。当新产生 proposal 的时候,会依据数据库的两阶段过程,首先会向其他的 server 发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就会开始执行
此外,ZK 还有一个 FIFO 队列,Leader 对每个 Follower 都建立了一个 FIFO 队列。所有的 proposal 都按照顺序进入这个队列,等待被处理。这个队列底层是使用 TCP 的连接来实现的,这就保证了事务的顺序一致性。
# 应用场景
# 分布式锁
zookeeper 在多线程情况下只会创建全局的唯一节点
利用这个特性,将某个临时节点当作锁,一个连接创建这个节点代表这个连接抢到了锁,其他链接需要等待,当这个连接下线或者删除节点时(redis 中需要考虑连接下线情况),锁被释放
# 注册中心
可以作为 dubbo、spring cloud、kafka 等集群的注册中心使用,每个服务器创建临时节点,里面存放可提供的服务,web 网页作为客户端连接 zookeeper,在需要时查找自己想要的服务,将服务器的IP、方法等信息存放到自己的缓存中
客户端使用 watcher 监视 zookeeper 中的节点,如果服务器挂掉,zookeeper 会发送消息给客户端(不使用 watcher 的话 zookeeper 也可以自动发送替代的服务器信息给客户端)
那是不是只要是个可以存数据的地方都可以做注册中心呢,非也:
ZK 使用 ZAB 协议(Zookeeper Atomic Broadcast)确保所有节点的数据一致,即使部分节点宕机,只要集群存活过半,仍能保证数据正确性
并且 zk 的 watch 监控功能,有回调机制。服务注册后,如果服务宕机,消费者能立即感知服务下线
对比一下 Redis,redis 无原生临时键,需依赖 Key+TTL 模拟临时节点,但 TTL 到期后仍需消费者主动轮询或依赖额外机制比如轮询通知集群变化,复杂度较高
这里 redis 默认是 ap 架构,因为主从同步默认是异步的,但是可以设置成全同步,改成 cp 架构
# 命名服务
对于每个节点的全路径,它必定是唯一的,我们可以使用节点的全路径作为命名方式了。而且更重要的是,路径是我们可以自己定义的,这对于我们对有些有语意的对象的 ID 设置可以更加便于理解