Zookeeper应用及原理分析实战

1、Zookeeper介绍

1.1 什么是Zookeeper

ZooKeeper是一种分布式协调服务,用于管理大型主机。在分布式环境中协调和管理服务是一 个复杂的过程。ZooKeeper 通过其简单的架构和API解决了这个问题。ZooKeeper允许开发人员专注于核心应用程序逻辑,而不必担心应用程序的分布式特性。

2.2 Zookeeper的应用场景

  • 分布式协调组件

例如,以下案例中,用户修改了服务A-2的标志flag的值,但是服务A-1的没改,这里就可以使用zookeeper将服务A-2的标志flag的值同步到服务A-1上。

请添加图片描述

在分布式系统中,需要有zookeeper作为分布式协调组件,协调分布式系统中的状态。

  • 分布式锁

zk在实现分布式锁上,可以做到强一致性,关于分布式锁相关的知识,在之后的ZAB协议中介绍。

  • 无状态化的实现

客户端登录分布式系统,登录信息不用在每个分布式系统的登录系统中各维持一份,只需要一份,放在zookeeper中即可

请添加图片描述

2、搭建Zookeeper服务器

2.1 zoo.cfg配置文件说明

#zookeeper时间配置中的基本单位(毫秒)
tickTime=2000
#允许fo1lower初始化连接到leader最大时长,它表示tickTime时间倍数即:initLimit*tickTime
initLimit=10
#允许fo1lower与1eader数据同步最大时长,它表示tickTime时间倍数
syncLimit=5
#zookeper 数据存储目录及日志保存目录(如果没有指明dataLogDir,则日志也保存在这个文件中)
dataDir=/usr/local/zookeeper/zkdata
#对客户端提供的端口号
clientPort=2181
#单个客户端与zookeeper最大并发连接数
maxClientCnxns=60
#保存的数据快照数量,之外的将会被清除
autopurge.snapRetainCount=3
#自动触发清除任务时间间隔,小时为单位。默认为0,表示不自动清除。
autopurge.purgeInterval=1

2.2 Zookeeper服务器的操作命令

重命名conf中的文件zoo_sample.cfg->zoo.cfg

启动zk服务器:

/bin/zkServer.sh start ./conf/zoo.cfg

查看zk服务器状态:

/bin/zkServer.sh status ./conf/zoo.cfg

停止zk服务器:

/bin/zkServer.sh stop ./conf/zoo.cfg

使用zkCli.sh 查看zookeeper中存储的数据,使用 ls / 可以查看zookeeper中存储的数据

./zkCli.sh
...
[zk: localhost:2181(CONNECTED) 0] ls /
[zookeeper]

3、Zookeeper内部的数据模型

3.1 zk是如何保存数据的

zk中的数据是保存在节点上的,节点就是znode,多个znode之间构成一颗树的目录结构。
Zookeeper的数据模型是什么样子呢?它很像数据结构当中的树,也很像文件系统的目录。

请添加图片描述

这样的层级结构,让每一个Znode节点拥有唯一的路径,就像命名空间一样对不同信息作出清晰的隔离。

先感受下如何创建节点,获取节点数据

./zkCli.sh
...
[zk: localhost:2181(CONNECTED) 3] create /test1			# 创建节点
Created /test1
[zk: localhost:2181(CONNECTED) 4] ls /					# 获取节点数据
[test1, zookeeper]
[zk: localhost:2181(CONNECTED) 5] create /test2 abc		# 创建有数据的节点
Created /test2
[zk: localhost:2181(CONNECTED) 6] get /test2			# 获取节点数据
abc

3.2 zk中的znode是什么样的结构

zk中的znode,包含了四个部分:

  • data:保存数据

  • acl:权限,定义了什么样的用户能够操作这个节点,且能够进行怎样的操作。

    • c:create创建权限,允许在该节点下创建子节点
    • w:write更新权限,允许更新该节点的数据
    • r:read读取权限,允许读取该节点的内容以及子节点的列表信息
    • d:delete删除权限,允许删除该节点的子节点
    • a:admin管理者权限,允许对该节点进行acl权限设置
  • stat:描述当前znode的元数据

  • child:当前节点的子节点

3.3 zk中节点znode的类型

  • 持久节点:创建出的节点,在会话结束后依然存在。保存数据
  • 持久序号节点:创建出的节点,根据先后顺序,会在节点之后带上一个数值,越后执行数值越大,适用于分布式锁的应用场景-单调递增,在并发场景中,序号可以告知当前被处理的顺序。
  • 临时节点:临时节点是在会话结束后,自动被删除的,通过这个特性,zk可以实现服务注册与发现的效果。那么临时节点是如何维持心跳呢?

请添加图片描述

对于临时节点,zk客户端会使用持续会话续约session id,会话断开后,zk客户端就无法续约session id时间,zk服务器有定时任务,清理没有续约session id的临时节点,这样临时节点就被删除了。

临时节点作用:在实现服务注册与发现中,临时节点就很有作用,服务producer向zk注册服务,生成一个临时节点,只要服务producer在线,临时节点一直存在,服务comsumer就可以找到该临时节点,进而访问服务producer;服务producer下线,回话断开,临时节点别删除,服务comsumer就无法使用了。

  • 临时序号节点:跟持久序号节点相同,适用于临时的分布式锁。
  • Container节点(3.5.3版本新增):Container容器节点,当容器中没有任何子节点,该容器节点会被zk定期删除(60s)。
  • TTL节点:可以指定节点的到期时间,到期后被zk定时删除。只能通过系统配置zookeeper.extendedTypesEnabled=true开启

下面我们使用zkCli.sh来创建上面说的几种节点

友情提示:不记得命令怎么办,输入错误命令zk就是显示帮助文档,告知命令、参数等信息

# 创建节点的命令
create [-s] [-e] [-c] [-t ttl] path [data] [acl]

# 创建持久节点
create /xxx [value] 

# 创建持久序号节点
create -s /xxx [value] 

# 创建临时节点
create -e /xxx [value] 

# 创建容器节点
create -c /xxx [value]

# 获取节点详细信息
get -s /xxx

下面来尝试创建同类型的节点

./zkCli.sh
...
[zk: localhost:2181(CONNECTED) 1] create /test3				#创建持久节点test3,节点无数据
[zk: localhost:2181(CONNECTED) 1] create -s /test2 abc		# 创建持久序号节点,节点数据为abc
[zk: localhost:2181(CONNECTED) 2] create -e /test3			# 创建临时节点
[zk: localhost:2181(CONNECTED) 3] get -s /test3				# 获取节点test3详细信息
null
cZxid = 0x10
ctime = Sun Feb 20 07:04:36 CST 2022
mZxid = 0x10
mtime = Sun Feb 20 07:04:36 CST 2022
pZxid = 0x10
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x100002febed0004
dataLength = 0
numChildren = 0
[zk: localhost:2181(CONNECTED) 14] create -c /mycontainer	# 创建容器节点
[zk: localhost:2181(CONNECTED) 16] create /mycontainer/sub1 # 创建容器子节点,容器节点无子节点会被自动删除

3.4 zk的数据持久化

zk的数据是运行在内存中,zk提供了两种持久化机制:

  • 事务日志

    zk把执行的命令以日志形式保存在dataLogDir指定的路径中的文件中(如果没有指定dataLogDir,则按dataDir指定的路径)。

  • 数据快照

    zk会在一定的时间间隔内做一次内存数据的快照,把该时刻的内存数据保存在快照文件中。

zk通过两种形式的持久化,在恢复时先恢复快照文件中的数据到内存中,再用日志文件中的数据做增量恢复,这样的恢复速度更快。

请添加图片描述

4、Zookeeper客户端(zkCli)的使用

4.1 多节点类型创建

  • 创建持久节点
  • 创建持久序号节点
  • 创建临时节
  • 创建临时序号节点
  • 创建容器节点

4.2 查询节点

  • 普通查询

  • 查询节点详细信息

    • cZxid:创建节点的事务ID
    • ctime:节点创建的时间
    • mZxid:修改节点的事务ID
    • mtime:节点最近修改的时间
    • pZxid:添加和删除子节点的事务ID
    • dataVersion:节点内数据的版本,每更新一次数据,版本会+1
    • aclVersion:此节点的权限版本
    • ephemeralOwner:如果当前节点是临时节点,该值是当前节点所有者的session id。如果节点不是临时节点,则该值为零。
    • dataLength:节点内数据的长度0
    • numChildren:该节点的子节点个数

4.3 删除节点

  • 普通删除
[zk: localhost:2181(CONNECTED) 5] ls /test1 			# test1下面还有子节点
[sub1, sub2]
[zk: localhost:2181(CONNECTED) 6] ls /test2				# test2下面没有子节点
[]
[zk: localhost:2181(CONNECTED) 7] deleteall /test1 		# 普通删除,删除节点/test1及所以子节点
[zk: localhost:2181(CONNECTED) 8] delete /test2			# 普通删除,删除节点/test2
  • 乐观锁删除

    [zk: localhost:2181(CONNECTED) 9] get -s /test1 
    aaa
    cZxid = 0x40
    ctime = Thu Feb 24 23:37:40 CST 2022
    mZxid = 0x41
    mtime = Thu Feb 24 23:37:53 CST 2022
    pZxid = 0x40
    cversion = 0
    dataVersion = 1									# 当前的version,每修改一次数据就会增加1
    aclVersion = 0
    ephemeralOwner = 0x0
    dataLength = 3
    numChildren = 0
    [zk: localhost:2181(CONNECTED) 10] delete -v 0 /test1 # 删除时会先指定的version是否与dataVersion相同。不相同无法删除
    version No is not valid : /test1
    [zk: localhost:2181(CONNECTED) 11] delete -v 1 /test1
    

    如果在删除节点的时候其他客户端修改了该节点的数据,dataVersion就会增加,指定的version和实际的dataVersion不相同,节点就会无法删除。

补充知识:乐观锁和悲观锁

  • 悲观锁:又称为悲观并发控制(Pessimistic Concurrency Control,PCC),是数据库中一种非常典型且非常严格的并发控制策略。悲观锁具有强烈的独占和排他特性,能够有效地避免不同事务对同一数据并发更新而造成的数据一致性问题。悲观锁的实现原理中,如果一个事务(假定事务A)正在对数据进行处理,那么在整个处理过程中,都会将数据处于锁定状态,在这期间,其他事务将无法对这个数据进行更新操作,直到事务A完成对该数据的处理,释放了对应的锁之后,其他事务才能够重新竞争来对数据进行更新操作。也就是说,对于一份独立的数据,系统只分配了一把唯一的钥匙,谁获得了这把钥匙,谁就有权利更新这份数据。悲观锁策略适合解决那些对于数据更新竞争十分激烈的场景,在这类场景中,通常采用简单粗暴的悲观锁机制来解决并发控制的问题。

  • 乐观锁:又称为乐观并发控制(Optimistic Concurrency Control,OCC),也是一种常见的并发控制策略。相对于悲观锁而言,乐观锁机制显得更加宽松与友好。悲观锁假定不同事务之间的处理一定会出现互相干扰,从而需要在一个事务从头到尾的过程中都对数据进行加锁处理。而乐观锁则不会对多个进程产生影响。因此在事务处理的绝大部分时间里不需要进行加锁处理。乐观锁机制中,在更新请求提交前,每个事务都会首先检查当前事务读取数据之后,是否会有其他事务对此数据进行了修改。如果其他事务有更新,则正在提交的数据必须要回滚。乐观锁通常适用于使用在数据并发不大,事务冲突较少的应用场景中。

    乐观锁有三个阶段,分别是数据读取、写入校验和数据写入,其中写入校验是整个乐观锁控制的关键所在。在写入校验阶段,事务会检查数据在读取阶段后是否有其他事务对数据进行过更新,以确保数据更新的一致性。

  • zookeeper中的乐观锁
    在zookeeper中,是通过version版本号来控制实现乐观锁中的“写入校验”机制。在zookeeper之内,在处理器中,是用如下方法实现:

    version = setDataRequest.getVersion();
    int currentVersion = nodeRecord.stat.getVersion();
    if(version != -1 && version != currentVersion)
    {
        throw new KeeperException.BadVersionException(path);
    }
    version = currentVersion + 1; 
    

    在进行一次setDataRequest请求处理时,首先进行了版本检查:zookeeper会从setDataRequest请求中获取当前请求的版本version,同时从数据记录noderecord中获取到当前服务器上该数据的最新版本currentversion。如果version为-1,那么说明客户端并不要求使用乐观锁,可以忽略版本对比;如果version不是-1,那么就对比version和currentversion,如果两个版本不匹配,那么就会抛出BadVersionException异常。

4.4 权限设置

  • 注册当前会话的账号和密码:
addauth digest xiaowang:123456
  • 创建节点并设置权限
# cdwra 含义为有 创建、删除、可写、可读、权限设置的权限
create /test-node abcd auth:xiaowang:123456:cdwra
  • 在另一个会话中必须先使用账号密码,才能拥有操作该节点的权限

当前节点

[zk: localhost:2181(CONNECTED) 16] get /test-node 
abcd

重开一个client,直接查看节点,会说权限不足,添加权限后即刻

[zk: localhost:2181(CONNECTED) 0] get /test-node 							# 直接获取节点数据,权限不足
org.apache.zookeeper.KeeperException$NoAuthException: KeeperErrorCode = NoAuth for /test-node
[zk: localhost:2181(CONNECTED) 1] addauth digest xiaowang:123456			#添加权限
[zk: localhost:2181(CONNECTED) 2] get /test-node 
abcd

5、kazoo客户端的使用

kazoo是一个Python库,旨在使得Python能够轻松、便捷的使用zookeeper。

5.1 连接处理

使用kazoo,首先需要实例化一个KazooClient对象并建立连接,代码如下:

from kazoo.client import KazooClient

zk = KazooClient(hosts="127.0.0.1:2181")
zk.start()

默认情况下,KazooClient会连接本地zookeeper服务,端口号为2181。

当zookeeper服务异常时(服务异常或服务未启动等),zk.start()会不断尝试重新连接,直到连接超时。

连接一旦建立,无论是间歇性连接丢失(网络闪断等)或zookeeper会话过期,KazooClient会不断尝试重新连接。

我们可以通过stop命令显式的中断连接:

zk.stop()

5.2 会话状态

Kazoo的客户端在与zookeeper服务会话的过程中,通常会在以下三种状态之间相互切换:CONNECTED、 SUSPENDED、LOST。

当KazooClient实例第一次被创建时,它的状态为LOST,一旦连接建立成功,状态随即被切换为CONNECTED。

在整个会话的生命周期里,伴随着网络闪断、服务端zookeeper异常或是其他什么原因,导致客户端与服务端出现断开的情况,KazooClient的状态切换成SUSPENDED,与此同时,KazooClient会不断尝试重新连接服务端,一旦连接成功,状态再次回到CONNECTED。

5.2 zookeeper的增删改查

5.2.1 创建节点

方法:

  • ensure_path():递归创建节点路径,只能设置权限,不能添加数据。
  • create():创建节点,并同时可以添加数据和监听事件,前提是其父节点必须存在,不能递归创建。

用法:

zk.ensure_path("/china/henan")
zk.create("/china", b"this is china node.")
5.2.2 读取数据

方法:

  • exists():检查节点是否存在
  • get():获取节点数据以及节点状态的详细信息
  • get_children():获取指定节点的所有子节点
5.2.3 更新数据

方法:

  • set():更新指定节点的信息。
5.2.4 删除节点

方法:

  • delete():删除指定节点。
# encoding=utf-8
from kazoo.client import KazooClient

zk = KazooClient(hosts='192.168.228.132:2181')
zk.start()

# 递归创建节点路径,只能设置权限,不能添加数据
zk.ensure_path("/skx/node1")

# 创建节点,可以添加数据和监听事件,不能递归创建。ephemeral表示是否是临时节点,sequence表示是否是顺序节点
zk.create("/skx/node2", b"I am child node2", ephemeral=False, sequence=False)

node_child1 = "/skx/node1"
node_child2 = "/skx/node2"

if zk.exists(node_child1):
    print("node %s exists." % node_child1)

node_parent = "/skx"
zk.set(node_parent, b"I am father")       # 设置节点数据
data, stat = zk.get(node_parent)          # 获取节点数据data 和节点状态stat
print("data:%s" % data)
print("stat:{}".format(stat))

children = zk.get_children(node_parent)   # 获取所有子节点
print("children of %s is %s" % (node_parent, children))

zk.delete(node_parent, recursive=True)    # 删除节点及所有子节点
zk.stop()

5.3 监听器

kazoo可以在节点上添加监听,使得在节点或节点的子节点发生变化时进行触发。

kazoo支持两种类型的添加监听器的方式,一种是zookeeper原生支持的,其用法如下:

def test_watch_data(event):
    print("this is a watcher for node data.")
zk.get_children("/china", watch=test_watch_children)

另一种方式是通过python修饰器的原理实现的,支持该功能的方法有:

  • ChildrenWatch:当子节点发生变化时触发
  • DataWatch:当节点数据发生变化时触发

其用法如下:

@zk.ChildrenWatch("/china")
def watch_china_children(children):
    print("this is watch_china_children %s" % children)
    
@zk.DataWatch("/china")
def watch_china_node(data, state):
    print("china node is %s" % data)

5.4 kazoo事务

自v3.4以后,zookeeper支持一次发送多个命令,这些命令作为一个原子进行提交,要么全部执行成功,要么全部失败。

使用方法如下:

transaction = zk.transaction()
transaction.check('/china/hebei', version=3)
transaction.create('/china/shanxi', b"thi is shanxi.")
results = transaction.commit()

6、zk的watch机制

6.1 Watch机制介绍

我们可以把Watch理解成是注册在特定Znode上的触发器。当这个Znode发生改变,也就是调用了create,delete,setpata方法的时候,将会触发Znode上注册的对应事件,请求Watch的客户端会接收到异步通知。
具体交互过程如下:

  • 客户端调用getpata方法,watch参数是true。服务端接到请求,返回节点数据,并且在对应的哈希表里插入被Watch的Znode路径,以及Watcher列表。
  • 当被Watch的Znode已删除,服务端会查找哈希表,找到该Znode对应的所有Watcher,异步通知客户端,并且删除哈希表中对应的Key-Value。

请添加图片描述

6.2 zkCli客户端使用watch

create /test xxx
get -w /test		# 一次性监听节点数据变化
ls -w /test 		# 监听目录,创建和删除子节点会收到通知。不监听节点内容变化
ls -R -w /test		# 递归监听所有子节点的变化,包含创建删除。不监听节点内容变化

6.3 kazoo客户端使用watch

6.3.1 监控节点数量的变化

监控节点数量的变化,可以应用到相关的场景:

  • 把节点名称设为服务器IP,可以实现服务器集群管理,服务上、下线通知
  • 主备切换,把最小临时节点设为master角色,其他临时节点为salve角色
  • 独占锁,若只监听一个固定临时节点,当该临时节点创建,则获得锁,否则释放锁
  • 分布式锁,不同客户端创建不同临时顺序节点,链式监听节点是否删除事件
import time
from kazoo.client import KazooClient
from kazoo.client import DataWatch
from kazoo.client import ChildrenWatch


def watch_child_node(zk_path):
    zkc = KazooClient(hosts='192.168.228.132:2181', timeout=5)
    zkc.start(timeout=5)

    # 直接用装饰器完成监听
    @ChildrenWatch(client=zkc, path=zk_path, send_event=True)
    def get_changes_with_event(children, event):
        print("Children nodes are %s" % children)
        if event:
            print("catched nodes a children nodes event ", event)

    @ChildrenWatch(client=zkc, path=zk_path)
    def get_changes_without_event(children):
        print("Children are %s" % children)

    while True:
        time.sleep(5)
        print('watching children node changes.....')


def main():
    watch_child_node("/skx")


if __name__ == '__main__':
    main()

请添加图片描述

注意:在注册监听环节,可以监听当前节点本身是否删除的事件,以及子节点的增、删事件,若需要zk返回event,那么需要将send_event设为True,才可以在watch函数传入event位置参数,这个逻辑可以在kazoo的源码看到

if self._send_event:
    result = self._func(children, event)
else:
    result = self._func(children)

装饰器有两种写法,一种是从引用kazoo import的ChildrenWatch

@ChildrenWatch(client=zkc,path=zk_path,send_event=True)
def get_changes_with_event(children,event):
	pass

另外一种是从已创建的zk实例中调用ChildrenWatch

@zkc.ChildrenWatch(path=zk_path,send_event=True)
def get_changes_with_event(children,event):
	pass
6.3.2 监听节点自身的数据变化

节点数据变化的应用场景:

  • 集中配置管理,各个客户端监听放置配置文件内容的节点,若配置有变化,则可以各个客户端拉取配置更新
import time
from kazoo.client import KazooClient
from kazoo.client import DataWatch
from kazoo.client import ChildrenWatch


def watch_data(zk_path):
    zkc = KazooClient(hosts='192.168.228.132:2181', timeout=5)
    zkc.start(timeout=5)

    # 直接用装饰器完成监听,节点值的监听还可以拿到zk的事件
    # 使用@DataWatch(client=zkc,path=zk_path)或者两种写法都可以
    @zkc.DataWatch(path=zk_path)
    def my_watch(data, stat, event):
        if not data:
            pass

        print("Data is {0} and data type is {1}".format(data, type(data)))
        print("Version is %s" % stat.version)
        if event:
            print("catching a data event ", event)

    while True:
        time.sleep(5)
        print('watching current node data changes.....')


def main():
    watch_data('/test')


if __name__ == "__main__":
    main()

请添加图片描述

6.3.3 同时监听子节点变化和当前节点数据变化
import time
from kazoo.client import KazooClient
from kazoo.client import ChildrenWatch
from kazoo.client import DataWatch


class ZKWatcher(object):
    def __init__(self, host, port, timeout=5):
        self._old_node_list = []
        self._host = host
        self._port = port
        self._time_out = timeout
        self._ip_port = self._host + ':' + self._port
        self._zkc = KazooClient(hosts=self._ip_port, timeout=self._time_out)
        self._zkc.start(self._time_out)

    def watcher(self, zk_path):
        # 获取原子节点列表
        self._old_node_list = self._zkc.get_children(zk_path)
        try:
            # 为所要监听的节点开启一个子节点监听器
            ChildrenWatch(client=self._zkc, path=zk_path, func=self._node_change, send_event=True)

            # 为所要监听的节点开启一个该节点值变化的监听器
            DataWatch(client=self._zkc, path=zk_path, func=self._data_change)
        except Exception:
            raise

    def _node_change(self, new_node_list, event):
        # 这里的new_node_list是指当前最新的子节点列表
        if not event:
            print("无节点数量变化事件")
            return
        # 当前节点列表与上次拿到的节点列表相等,注意不是长度相等,是列表值和长度都要相等
        if new_node_list == self._old_node_list:
            print('子节点列表未发生变化')
            return

        if len(new_node_list) > len(self._old_node_list):
            for new_node in new_node_list:
                if new_node not in self._old_node_list:
                    print('监听到一个新的节点:%s' % str(new_node))
                    self._old_node_list = new_node_list

        else:
            for old_node in self._old_node_list:
                if old_node not in new_node_list:
                    print('监听到一个删除的节点:%s' % str(old_node))
                    self._old_node_list = new_node_list

    def _data_change(self, data, stat, event):
        if not data:
            print('节点已删除,无法获取数据')
            return
        if not event:
            print('无节点数据变化事件')
            return

        print('监听到数据变化,事件:{}'.format(event))
        print('数据为%s, 数据版本号:%s' % (data, stat.version))
        print('子节点数量%s, 子节点数据版本号:%s' % (stat.numChildren, stat.cversion))


def main():
    try:
        zk = ZKWatcher(host='192.168.228.132', port='2181')
        zk.watcher('/skx')

        while True:
            time.sleep(5)
            print('watching...')
    except Exception as e:
        print(e)


if __name__ == "__main__":
    main()

7、zk实现分布式锁

什么叫做分布式锁呢?
比如说"进程 1"在使用该资源的时候,会先去获得锁,"进程 1"获得锁以后会对该资源保持独占,这样其他进程就无法访问该资源,"进程 1"用完该资源以后就将锁释放掉,让其他进程来获得锁,那么通过这个锁机制,我们就能保证了分布式系统中多个进程能够有序的访问该临界资源。那么我们把这个分布式环境下的这个锁叫作分布式锁。

7.1 zk中锁的种类

互斥锁:同一时间只有一个客户端能拿到所锁

读锁:大家都可以读,要想上读锁的前提:之前的锁没有写锁

写锁:只有得到写锁的才能写。要想上写锁的前提是,之前没有任何锁。

7.2 zk如何上互斥锁

上互斥锁流程

  • 假设对节点/skx上锁。接收到请求后,在/skx节点下创建一个临时顺序节点
  • 判断自己是不是当前节点下最小的节点:是,获取到锁;不是,对前一个节点进行监听
  • 获取到锁,处理完业务后,delete临时顺序节点释放锁,然后下面的节点将收到通知,重复第二步判断

请添加图片描述

7.3 zk如何上读锁

假设对节点/skx 上锁,在/skx 下创建一个临时序号节点,获取当前zk中序号比自己小的所有节点

判断最小节点是否是读锁:

  • 如果不是读锁的话,则上锁失败,为最小节点设置监听。阻塞等待,zk的watch机制会当最小节点发生变化时通知当前节点,于是再执行第二步的流程。
  • 如果是读锁的话,则上锁成功

7.4 zk如何上写锁

创建一个临时序号节点,获取zk中所有的子节点

判断自己是否是最小的节点:

  • 如果是,则上写锁成功。
  • 如果不是,说明前面还有锁,则上锁失败,监听前一个节点。

仅仅使用写锁和互斥锁的效果是一样的。

7.5 羊群效应

为什么 写锁上锁失败要监听前一个节点,而不是监听最小节点。如果有多个写锁同时加锁,最小节点释放锁时,会触发所有其他等待加写锁的节点。这样的话对zk的压力非常大,称为羊群效应。监听前一个节点调整为了链式监听。解决了这个问题。

7.6 kazoo实现读写锁

下面实际看下锁的用法

import threading
import time

from kazoo.client import KazooClient
from kazoo.recipe.lock import Lock			# 互斥锁
from kazoo.recipe.lock import ReadLock		# 读锁
from kazoo.recipe.lock import WriteLock		# 写锁

SelectLock = WriteLock              # 这里更换想要的锁,ReadLock、WriteLock


class ZooKeeperLock(object):
    def __init__(self, hosts, timeout=1):
        """ 封装了初始化zk客户端,获取锁,释放锁,支持读锁,写锁

        :param hosts: 需要连接的zk,格式:ip:port
        :param zk_client: zk客户端实例
        :param timeout: 等待zk连接的最长时间
        :param lock_handle: 锁对象的实例
        """
        self.hosts = hosts
        self.zk_client = None
        self.timeout = timeout
        self.lock_handle = None
        self.create_lock()

    def create_lock(self):
        try:
            self.zk_client = KazooClient(hosts=self.hosts, timeout=self.timeout)
            self.zk_client.start(timeout=self.timeout)
        except Exception as e:
            print("Create KazooClient failed! Exception: %s" % str(e))
            return

        try:
            lock_path = "/skx"          # 对节点/skx上锁
            self.lock_handle = SelectLock(self.zk_client, lock_path)
        except Exception as e:
            print("Create lock failed! Exception: %s" % str(e))
            return

    def destroy_lock(self):
        # self.release()
        if self.zk_client is not None:
            self.zk_client.stop()
            self.zk_client = None

    def acquire(self, blocking=True, timeout=None):
        if self.lock_handle is None:
            return None

        try:
            return self.lock_handle.acquire(blocking=blocking, timeout=timeout)
        except Exception as e:
            print("Acquire lock failed! Exception: %s" % str(e))
            return None

    def release(self):
        if self.lock_handle is None:
            return None
        return self.lock_handle.release()

    def __del__(self):
        self.destroy_lock()


class MyThread(threading.Thread):
    def __init__(self, thread_id):
        threading.Thread.__init__(self)
        self.thread_id = thread_id

    def run(self):
        print("开始线程:%s" % self.thread_id)
        get_lock()
        print("退出线程:%s" % self.thread_id)


def get_lock():
    zookeeper_hosts = "192.168.228.132:2181"
    lock = ZooKeeperLock(zookeeper_hosts)

    try:
        lock.acquire()
        print("Get lock! Do something!")
        time.sleep(5)
    except Exception as e:
        print("error in acquire lock:%s" % e)
    finally:
        try:
            lock.release()
        except Exception as e:
            print("error in release error:%s" % e)


def main():
    for i in range(5):
        thread = MyThread(i)        # 多个线程同时获取锁
        thread.start()


if __name__ == "__main__":
    main()

上写锁,查看节点/skx的子节点,可以看到,按照节点序号,从小到大,依次获取写锁。

[zk: localhost:2181(CONNECTED) 45] ls /skx
[0d332c66120547f3a04b65f8324b8a49__lock__0000000001, 3a697578c11645ef850fde920902801b__lock__0000000000, 60a2c660b4374ec98033ba10c68003d3__lock__0000000004, 7dbe2432203247e6a345446cebbfe598__lock__0000000002, e9a560b6ef83419cb3129a9ac91fd6ab__lock__0000000003]
[zk: localhost:2181(CONNECTED) 46] ls /skx
[0d332c66120547f3a04b65f8324b8a49__lock__0000000001, 60a2c660b4374ec98033ba10c68003d3__lock__0000000004, 7dbe2432203247e6a345446cebbfe598__lock__0000000002, e9a560b6ef83419cb3129a9ac91fd6ab__lock__0000000003]
[zk: localhost:2181(CONNECTED) 47] ls /skx
[60a2c660b4374ec98033ba10c68003d3__lock__0000000004, 7dbe2432203247e6a345446cebbfe598__lock__0000000002, e9a560b6ef83419cb3129a9ac91fd6ab__lock__0000000003]
[zk: localhost:2181(CONNECTED) 48] ls /skx
[60a2c660b4374ec98033ba10c68003d3__lock__0000000004, e9a560b6ef83419cb3129a9ac91fd6ab__lock__0000000003]
[zk: localhost:2181(CONNECTED) 49] ls /skx
[60a2c660b4374ec98033ba10c68003d3__lock__0000000004]
[zk: localhost:2181(CONNECTED) 50] ls /skx
[]

将 SelectLock = WriteLock 改为 SelectLock = ReadLock,上读锁,查看节点/skx的子节点,可以看到,节点几乎同时获取了锁,然后同时释放。

[zk: localhost:2181(CONNECTED) 60] ls /skx
[2197f4e920044daba9e5fbb84ebf0d97__rlock__0000000003, 31ffd1826f474d56ab544a6cb1004035__rlock__0000000001, 6474d68ca356474ab3ea5f1ff7a5a694__rlock__0000000004, 9e7159e7dd9e4c91abb095f03afaeca3__rlock__0000000002, b914f482aa6f45c9a011e1887de04c61__rlock__0000000000]
[zk: localhost:2181(CONNECTED) 61] ls /skx
[]

8、Zookeeper集群实战

8.1 Zookeeper集群角色

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

  • Leader:处理集群的所有事务请求,集群中只有一个Leader。
  • Follower:只能处理读请求,参与Leader选举。
  • Observer:只能处理读请求,提升集群读的性能,但不能参与Leader选举。

8.2 集群搭建

搭建4个节点,其中一个节点为Observer

1)创建4个节点的myid,并设值在

/usr/local/zookeeper中创建四个文件夹,zk1,zk2,zk3,zk4;每个文件夹中创建一个文件myid

/usr/local/zookeeper/zkdata/zk1# echo 1 > myid
/usr/local/zookeeper/zkdata/zk2# echo 2 > myid
/usr/local/zookeeper/zkdata/zk3# echo 3 > myid
/usr/local/zookeeper/zkdata/zk4# echo 4 > myid

2)编写4个zoo.cfg

# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# 修改对应的zk1 zk2 zk3 zk4
dataDir=/usr/local/zookeeper/zkdata/zk1
# 修改对应的端口 2181 2182 2183 2184
clientPort=2181
# 2001为集群通信端口,3001为集群选举端口,observer表示不参会集群选举
server.1=192.168.228.132:2001:3001
server.2=192.168.228.132:2002:3002
server.3=192.168.228.132:2003:3003
server.4=192.168.228.132:2004:3004:observer

3)启动4台zookeeper

./bin/zkServer.sh start ./conf/zoo1.cfg
./bin/zkServer.sh start ./conf/zoo2.cfg
./bin/zkServer.sh start ./conf/zoo3.cfg
./bin/zkServer.sh start ./conf/zoo4.cfg

通过以下命令查看每个节点的状态

./bin/zkServer.sh status ./conf/zoo1.cfg
./bin/zkServer.sh status ./conf/zoo2.cfg
./bin/zkServer.sh status ./conf/zoo3.cfg
./bin/zkServer.sh status ./conf/zoo4.cfg
# ./bin/zkServer.sh status conf/zoo2.cfg 
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: conf/zoo2.cfg
Client port found: 2182. Client address: localhost. Client SSL: false.
Mode: leader

节点1,2,3,4四个zookeeper的角色分别为follower,leader,follower,observer

8.3 连接zookeeper集群

./bin/zkCli.sh -server
192.168.228.132:2181,192.168.228.132:2182,192.168.228.132:2183

9、ZAB协议

9.1 什么是ZAB协议

zookeeper作为非常重要的分布式协调组件,需要进行集群部署,集群中会以一主多从的形式进行部署。zookeeper为了保证数据的一致性,使用了ZAB(Zookeeper AtomicBroadcast)协议,这个协议解决了Zookeeper的崩溃恢复和主从数据同步的问题。

请添加图片描述

9.2 ZAB协议定义的四种节点状态

  • Looking:选举状态。
  • Following:Follower节点(从节点)所处的状态。
  • Leading:Leader节点(主节点)所处状态。
  • Observing:观察者节点所处的状态

9.3 集群上线时的Leader选举过程

先介绍三个概念:

  • SID:服务器ID 。用来唯一标识一台ZooKeeper集群中的机器,每台机器不能重复,和myid一致。
  • ZXID:事务ID。ZXID是一个事务ID,用来标识一次服务器状态的变更 。在某一时刻,集群中的每台机器的ZXID值不一定完全一致,这和ZooKeeper服务器对于客户端“更新请求”的处理逻辑有关。
  • Epoch:每个Leader任期的代号 。没有Leader时同一轮投票过程中的逻辑时钟值是相同的。每投完一次票这个数据就会增加

Zookeeper集群中的节点在上线时,将会进入到Looking状态,也就是选举Leader的状态,这个状态具体会发生什么?

请添加图片描述

(1)服务器1启动,发起一次选举。服务器1投自己一票。此时服务器1票数一票,不够半数以上(2票),选举无法完成,服务器1状态保持为 LOOKING;
(2)服务器2启动,再发起一次选举。服务器1和2分别投自己一票并交换选票信息:此时服务器1发现服务器2的myid比自己目前投票推举的(服务器1)大,更改选票为推举服务器2 。此时服务器2的票数已经超过半数(2 > 3/2),服务器2当选Leader。服务器1更改状态为FOLLOWING,服务器2更改状态为LEADING;
(3)服务器3启动,发起一次选举。此时服务器1,2已经不是LOOKING状态,不会更改选票信息。交换选票信息结果:服务器2为2票,服务器3为1票。此时服务器3服从多数,更改选票信息为服务器2,并更改状态为FOLLOWING;

9.4 崩溃恢复时的Leader选举

Leader建立完后,Leader周期性地不断向Follower发送心跳(ping命令,没有内容的socket)。当Leader崩溃后,Follower发现socket通道已关闭,于是Follower开始进入到Looking状态,重新回到上一节中的Leader选举过程,此时集群不能对外提供服务。

每个节点存在以下数据(EPOCH,ZXID,SID)

选举Leader规则:①EPOCH大的直接胜出
②EPOCH相同,事务id大的胜出
③事务id相同,服务器id大的胜出

9.5 主从服务器之间的数据同步

所有的写数据都由主节点负责

请添加图片描述

9.6 Zookeeper中的NI0与BIO的应用

NIO

  • 用于被客户端连接的2181端口,使用的是NIO模式与客户端建立连接
  • 客户端开启Watch时,也使用NIO,等待Zookeeper服务器的回调

BIO

  • 集群在选举时,多个节点之间的投票通信端口,使用BIO进行通信。

这部分只做了解,这里不进行深入。

10、CAP理论

10.1 CAP定理

2000年7月,加州大学伯克利分校的EricBrewer教授在ACMPODC会议上提出CAP猜想。2年后,麻省理工学院的SethGilbert和NancyLynch从理论上证明了CAP。之后,CAP理论正式成为分布式计算领域的公认定理。

CAP理论为:一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。

  • 一致性(Consistency)

    一致性指“all nodes see the same data at the same time",即更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致。

  • 可用性(Availability)

    可用性指“Read sand writes always succeed",即服务一直可用,而且是正常响应时间。

  • 分区容错性(Partitiontolerance)

    分区容错性指"the system continues to operate despite arbitrary message loss or failure of part of the system",即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性或可用性的服务。——避免单点故障,就要进行冗余部署,冗余部署相当于是服务的分区,这样的分区就具备了容错性。

10.2 CAP权衡

通过CAP理论,我们知道无法同时满足一致性、可用性和分区容错性这三个特性,那要舍弃哪个呢?

对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到N个9,即保证P和A,舍弃C(退而求其次保证最终一致性)。虽然某些地方会影响客户体验,但没达到造成用户流程的严重程度。

对于涉及到钱财这样不能有一丝让步的场景,C必须保证。网络发生故障宁可停止服务,这是保证CA,舍弃P。貌似这几年国内银行业发生了不下10起事故,但影响面不大,报到也不多,广大群众知道的少。还有一种是保证CP,舍弃A。例如网络故障是只读不写。
孰优孰略,没有定论,只能根据场景定夺,适合的才是最好的。

请添加图片描述

10.3 BASE理论

eBay的架构师DanPritchett源于对大规模分布式系统的实践总结,在ACM上发表文章提出BASE理论,BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性(Strong Consistency,CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。

  • 基本可用(BasicallyAvailable)

基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。

电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。比如只能付款,不能退款。

  • 软状态(SoftState)

软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。mysql replication的异步复制也是一种体现。

  • 最终一致性(EventualConsistency)

最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

10.4 Zookeeper追求的一致性

Zookeeper在数据同步时,追求的并不是强一致性,而是顺序一致性(事务id的单调递增)。

这就是本篇文档对于zookeeper的介绍的全部内容了,谢谢!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值