带你搞明白Zookeeper的内部原理

今天来看下zookeeper是干什么的,以及zookeeper的内部原理。

Zookeeper简介

概述

首先zookeeper是一个开源分布式的协调服务项目,主要为集群提供数据一致的协调服务,在整个集群中负责各个节点的数据复制和同步。

如果把集群中每个节点比喻成各种小动物,那么zookeeper就是动物园管理员,这也是zookeeper名字的由来。

zookeeper底层是基于类似文件系统的目录节点树的方式进行数据的存储,同时维护和监控存储数据的状态变化,通过监控这些数据状态变化,从而达到基于数据的集群管理。

下面来看下zookeeper的工作机制:

如上图,从设计模式来看的话,zookeeper是基于观察者模式设计的分布式服务管理框架,它负责存储和管理数据,然后可以接受观察者注册,当数据的状态变化后,zookeeper就通知那些在zookeeper上注册的观察者,从而让观察者能及时的做出相应的反应。

特点

zookeeper的特点有:

  1. 一个领导者(leader),多个跟随者(follower)。
  2. 集群中只要有半数以上的节点存活zookeeper集群就能正常服务。
  3. 每个server保存一份相同的数据副本,client无论连接到哪个server,数据都是一致的。
  4. 更新的请求是顺序进行的,即来自同一个client的更新请求按发送的顺序依次执行。
  5. 数据更新具有原子性,即一次数据更新要么成功要么失败。
  6. 具有实时性,在一定时间范围内,client能读到最新的数据。

数据结构

zookeeper的数据模型结构与Unix文件系统的类似,整体上可以看作是一棵树,每个节点称作一个ZNode,每一个ZNode默认能存储1MB的数据,每个ZNode都可以通过路径唯一标识。如下图所示:

:节点不是目录,可以类比文件,目录相当于箱子可以存东西,节点相当于文件,通过这个节点(文件)可以找到下一个节点(文件)。

应用场景

zookeeper提供的服务包括:命名服务、配置管理、集群管理、服务器节点动态上下线、软负载均衡等。

命名服务:在分布式环境下,经常需要对应用或服务进行统一命名便于识别。比如访问百度的域名www.baidu.com时其实是访问的不同的服务器地址,但这些不同的服务器地址的域名都是相同的。

配置管理:分布式环境下,要求集群中的配置信息是一致的,配置文件修改后希望能快速同步到各个节点上,配置文件修改后的同步就可以交给zookeeper来管理。

集群管理:集群中每个节点都在zookeeper上进行注册,可以实现监控节点的状态。可以把zookeeper想象成班长,把客户端想象成学生,老师可以通过班长知道学生的实时状态。

服务器动态上下线:刚开始已经介绍过了,就是客户端能实时洞察到服务器上下线的变化,就是通过zookeeper实现的。

软负载均衡:在zookeeper中记录每台服务器的访问量,让访问量少的服务器去处理最新的客户端请求,类似于Nginx的负载均衡功能。

Zookeeper安装

zookeeper的安装有两种模式,一种是本地模式另一种是分布式模式。

本地部署

# 必须有JDK
# 上传zookeeper并解压
tar -zxvf zookeeper-3.5.7.tar.gz -C /opt/programfiles/
# 配置环境变量(记得source一下)
# 将zookeeper根目录下conf文件中zoo_sample.cfg修改成zoo.cfg
mv zoo_sample.cfg zoo.cfg
# 在zoo.cfg中修改datadir
vim zoo.cfg
# zoo.cfg修改内容如下
dataDir=/opt/programfiles/zookeeper-3.5.7/zkData
# 在zookeeper根目中创建zkData(因为datadir指向它)
mkdir /opt/programfiles/zookeeper-3.5.7/zkData
# 启动zookeeper服务 : zkServer.sh start|stop|status|restart
/opt/programfiles/zookeeper-3.5.7/bin/zkServer.sh start
# 启动zookeeper客户端 :zkCli.sh;使用quit退出客户端。
/opt/programfiles/zookeeper-3.5.7/bin/zkCli.sh

部分配置参数解读

tickTime =2000:
通信心跳时间,zookeeper服务器与客户端的心跳时间,单位为毫秒。它用于心跳机制,并且设置最小的session超时时间为两倍心跳时间(session的最小超时时间是2 x tickTime),即超过两个心跳服务器和客户端连接就会断开表示这次会话结束。

initLimit =10:
LF初始通信时限,集群中的follower与leader之间初始连接时能容忍的最多心跳数(tickTime的数量为2000时,最多容忍20S没有心跳),用它来限定集群中的zookeeper服务器连接到leader的时限。

syncLimit =5:
LF同步通信时限,集群中leader与follower之间的最大响应时间,假如响应超过syncLimit x tickTime,leader认为follwer死掉,从服务器列表中删除follwer。

dataDir:
数据文件目录+数据持久化路径,主要用于保存zookeeper中的数据。

clientPort =2181:
客户端连接端口,监听客户端连接的端口。

分布式部署

集群规划:在node1、node2和node3三个节点上部署zookeeper。

# 注:先把zookeeper服务停掉,再将logs和zkData中的数据全部删除
# 在zkData中创建myid文件,并在文件中写一个数值2,该值代表zookeeper节点Id
[Robofly@node1 zkData]$ vim myid
# 添加myid文件,注意一定要在linux里面创建,在notepad++等文本编辑器里面很可能出现乱码
# 注:该值在整个集群中必须是唯一的
# 修改配置文件zoo.cfg
# 配置datadir(已经配置过了)
# 添加如下内容
server.2=node1:2888:3888
server.3=node2:2888:3888
server.4=node3:2888:3888
# 说明:2指的是myid的值,node1指的是myid为2的机器是哪台,2888是zk通信的端口,3888是leader选举端口
# 使用脚本分发文件
[Robofly@node1 zkData]$ sendout myid
# 修改node2和node3的myid值3,4
# 启动各节点的zookeeper服务:zkServer.sh start
[Robofly@node1 zookeeper-3.5.7]$ bin/zkServer.sh start
[Robofly@node2 zookeeper-3.5.7]$ bin/zkServer.sh start
[Robofly@node3 zookeeper-3.5.7]$ bin/zkServer.sh start

分别在每台节点上启动比较麻烦,可以写个脚本一次启动,以下脚本仅供参考:

#!/bin/bash
if [ $# -lt 1 ]
then
    echo "请输入参数,参数可选start、stop、status。"
    exit ;
fi
case $1 in
"start"){
        echo "==================== 启动 node1 zookeeper ===================="
        ssh node1 "/opt/programfiles/zookeeper-3.5.7/bin/zkServer.sh start"
        echo "==================== 启动 node2 zookeeper ===================="
        ssh node2 "/opt/programfiles/zookeeper-3.5.7/bin/zkServer.sh start"
        echo "==================== 启动 node3 zookeeper ===================="
        ssh node3 "/opt/programfiles/zookeeper-3.5.7/bin/zkServer.sh start"
};;
"stop"){
        echo "==================== 停止 node1 zookeeper ===================="
        ssh node1 "/opt/programfiles/zookeeper-3.5.7/bin/zkServer.sh stop"
        echo "==================== 停止 node2 zookeeper ===================="
        ssh node2 "/opt/programfiles/zookeeper-3.5.7/bin/zkServer.sh stop"
        echo "==================== 停止 node3 zookeeper ===================="
        ssh node3 "/opt/programfiles/zookeeper-3.5.7/bin/zkServer.sh stop"
};;
"status"){
        echo "==================== node1 zookeeper 状态 ===================="
        ssh node1 "/opt/programfiles/zookeeper-3.5.7/bin/zkServer.sh status"
        echo "==================== node2 zookeeper 状态 ===================="
        ssh node2 "/opt/programfiles/zookeeper-3.5.7/bin/zkServer.sh status"
        echo "==================== node3 zookeeper 状态 ===================="
        ssh node3 "/opt/programfiles/zookeeper-3.5.7/bin/zkServer.sh status"
};;
esac

以上就启动了zookeeper集群,此集群三个节点,多于三个节点也可以,但最好为奇数个节点。

zookeeper的命令行操作

命令基本语法 功能描述
help 显示所有操作命令
ls path 使用 ls 命令来查看当前znode的子节点 -w 监听子节点变化 -s 附加次级信息
create 普通创建 -s 含有序列 -e 临时(重启或者超时消失)
get path 获得节点的值 -w 监听节点内容变化 -s 附加次级信息
set 设置节点的具体值
stat 查看节点状态
delete 删除节点
deleteall 递归删除节点

下面举例来操作下zookeeper的命令:

# 启动客户端
bin/zkCli.sh
# 远程连接Zookeeper
bin/zkCli.sh -server node2:2181
# 显示所有操作命令
[zk: localhost:2181(CONNECTED) 1] help
# 查看当前znode中所包含的内容
[zk: localhost:2181(CONNECTED) 1] ls /
# 查看当前节点详细数据
[zk: localhost:2181(CONNECTED) 1] ls2 /
# 创建普通节点
[zk: localhost:2181(CONNECTED) 1] create /robofly "gaofei"
# 获得节点的值
[zk: localhost:2181(CONNECTED) 1] get /robofly
# 创建短暂节点,在当前客户端是可以查看到的,退出当前客户端后再重启客户端短暂节点已经删除
[zk: localhost:2181(CONNECTED) 1] create -e /robofly/gaofei
# 创建带序号的节点,如果原来没有序号节点,则序号从0开始依次递增,如果原节点下已有2个节点,则再排序时从2开始,以此类推
[zk: localhost:2181(CONNECTED) 1] create -s /gao/fei
# 修改节点的值
[zk: localhost:2181(CONNECTED) 1] set /robofly "666"
# 监听节点值的变化,在node3上注册监听/robofly节点的数据变化
[zk: localhost:2181(CONNECTED) 1] get /robofly watch
# 在node2上修改/robofly节点的数据,观察node3收到数据变化的监听
[zk: localhost:2181(CONNECTED) 1] set /robofly "888"
# 在node3上注册监听/robofly节点的子节点变化
[zk: localhost:2181(CONNECTED) 1] ls /robofly watch
# 在node2上/robofly节点上创建子节点,观察node3上收到子节点变化的监听
[zk: localhost:2181(CONNECTED) 1] create /robofly/gao "fei"
# 删除节点
[zk: localhost:2181(CONNECTED) 1] delete /robofly/gao
# 递归删除节点
[zk: localhost:2181(CONNECTED) 1] rmr /robofly/gaofei
# 查看节点状态
[zk: localhost:2181(CONNECTED) 1] stat /robofly

关于zookeeper的api应用,只需要在项目中加入相应的依赖就可以调用,这里不再赘述。

Zookeeper内部原理

节点类型

zookeeper的节点类型常用有四种,分别为:

  1. 持久化目录节点
    客户端与zookeeper断开连接后,该类型节点依旧存在。
  2. 持久化顺序编号目录节点
    客户端与zookeeper断开连接后,该类型节点依旧存在,只是zookeeper给该类型节点名称进行了顺序编号。
  3. 临时目录节点
    客户端与zookeeper断开连接后,该类型节点被删除。
  4. 临时顺序编号目录节点
    客户端与zookeeper断开连接后,该类型节点被删除,只是zookeeper给该类型节点名称进行了顺序编号。

:创建带顺序编号的节点时,znode名称后会附加一个顺序号,顺序号是一个单调递增的计数器,由父节点进行维护。在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端就可以通过顺序号来推断事件的顺序。

Stat结构体

每个znode都维护stat的数据结构,里面存储了该节点的全部状态信息。比如上面用get命令获取节点的值时,会返回节点的全部状态信息。

下面解释下stat结构体中每个参数的含义:

  • czxid:创建节点时的事务id
  • ctime:创建节点时间(毫秒,从1970年开始)
  • mzxid:最后一次修改数据节点时的事务id
  • mtime:最后一次修改节点的时间(毫秒,从1970年开始)
  • pzxid:子节点列表最后一次修改事务的id
  • cversion:子节点版本号/修改次数,每次修改加1
  • dataversion:数据版本号,数据每次修改加1
  • aclversion:权限版本号,修改加1
  • ephemeralowner:临时节点的sessionId,持久节点为0
  • datalength:节点数据长度
  • numchildren:拥有子节点数量

监听器原理(面试重点)

如上图,监听器步骤详解为:

  1. 首先要有一个main线程。
  2. 在main线程中创建zookeeper客户端,这时就会创建两个线程,一个负责网络连接通信的connect,一个负责监听listener。
  3. 通过connect线程将注册的监听事件发送给zookeeper。
  4. 在zookeeper的注册监听器列表中将注册的监听事件添加到列表中。
  5. zookeeper监听到有数据或路径变化,就会将这个消息发送给listener线程。
  6. listener线程内部会调用process方法。

选举机制(面试重点)

zookeeper的选举机制为半数机制,即集群中半数以上的节点存活则集群就可用,所以zookeeper适合安装奇数台节点。

下面用一个简单的例子来说明选举的过程:

假设有五台服务器组成的zookeeper集群,它们的id从1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点上,都是一样的。

这些服务器是依序启动的,服务器1启动投自己一票发起一次选举,此时服务器1票数一票,不够半数以上(3票),选举无法完成,服务器1状态保持为LOOKING。

服务器2启动投自己一票发起一次选举,然后服务器1和服务器2交换选票信息,此时服务器1发现服务器2的ID比自己目前投票推举的(服务器1)大,更改选票为服务器2。此时服务器1票数0票,服务器2票数2票,没有半数以上结果,选举无法完成,服务器1,2状态保持LOOKING。

服务器3启动,发起一次选举。此时服务器1和2都会更改选票为服务器3。此次投票结果:服务器1为0票,服务器2为0票,服务器3为3票。此时服务器3的票数已经超过半数,服务器3当选Leader。服务器1,2更改状态为FOLLOWING,服务器3更改状态为LEADING。

服务器4启动,发起一次选举。此时服务器1,2,3已经不是LOOKING状态,不会更改选票信息。交换选票信息的结果为:服务器3为3票,服务器4为1票。此时服务器4服从多数,更改选票信息为服务器3,并更改状态为FOLLOWING。

服务器5启动,同4一样更改状态为FOLLOWING。

写数据流程

如上图所示,具体步骤为:

  1. client向zookeeper的server1上写数据,发送一个写数据请求。
  2. 如果server1不是leader,那么server1会把接收到的请求进一步转发给leader,因为每个zookeeper的server里面有一个是leader。这个leader会将写数据请求广播给各个server,比如server1和server2,各个server会将写数据请求加入待写队列,并向leader发送成功信息。
  3. 当leader收到半数以上server的成功信息,说明该写操作可以执行。leader会向各个server发送提交信息,各个server收到信息后会落实队列里的写数据请求,此时写数据成功。
  4. server1会进一步通知client写数据成功,这时认为整个写操作成功。

最后,以上就是今天的内容,介绍了zookeeper的一些原理,关于zookeeper的Paxos算法没有介绍,Paxos算法是一种基于消息传递且具有高度容错特性的一致性算法,这里先挖个坑,放到之后来详解~

posted @ 2023-03-12 20:17  码农高飞  阅读(3)  评论(0编辑  收藏  举报