学习zookeeper,看看这一篇

zookeeper学习总结

  • zookeeper是一个服务于分布式应用程序的中间件协调服务

单体架构到微服务架构

  • 单体架构下,一个完整的流程比如"购物",可以在单体架构中完成整个流程。
  • 分布式架构下,服务进行细化,拆分,涉及到服务之间的通信。

SpringBoot+RestTemplate

  • SpringBoot提供RestTemplate支持远程通信,其简化了http服务的通信
  • 使用RestTemplate,只需要使用者提供URL
  • RestTemplate本质上是对现有技术进行了封装,如JDK、Apache HttpClient、OkHttp等

分布式一致性问题

  • 在一个分布式系统中,有多个节点,每个节点都会提出一个请求,
  • 但是所有节点中只有一个请求会被通过
  • 被通过的请求是所有节点达成一致的结果
  • 所谓一致性:提出的所有请求中能够选出最终一个确定的请求,并且选出之后,所有节点都要知道

分布式锁服务

  • zookeeper是作为分布式锁服务的一设计
  • 注册中心是zookeeper是其能够实现的功能之一

zookeeper集群部署

  • 进入zk安装的conf目录下,复制zoo.sample.cfg文件为zoo.cfg文件,并配置dirData目录存储相关数据
  • 在配置的dirData路径下创建myid文件,文件内容自定义,为每一台zk服务器的标识
  • 在conf目录下的zoo.cfg配置文件下配置集群
    • zk启动的时候会根据myid的内容与配置文件中的配置相匹配
  • 集群启动后,会进行leader选举,选出leader节点
#2181:访问zookeeper的端口     3888:重新选举后leadeer的端口
server.1=192.168.1.1:2181:3888
server.2=192.168.1.1:2181:3888
server.3=192.168.1.1:2181:3888
#解包
tar -zxvf z..
#启动ZK服务
./zkServer.sh start
#查看zk服务状态
./zkServer.sh status
#停止zk服务
./zkServer.sh stop
#重启zk服务
./zkServer.sh restart
#连接zk
sh zkCli.sh

问题记录

  • 记录创建zookeeper集群时遇到的坑
  • 本地虚拟机同时启动三台总有一台无法运行——暂未解决
  • zk启动之后查看启动状态和日志发现,节点之间未建立连接
    • 通过修改服务器上的hosts文件,去掉ip解决

zookeeper集群角色

  • 在zookeeper集群中一共有三种角色:Leader、follower、Observer
  • leader节点是被选举出的主节点,这里称为leader选举
  • follow节点参与leader选举,选举失败为follower节点
  • Observer为观察者节点,不参与选举

zookeeper数据结构

  • zk的数据结构类似于标准的文件系统,其内部维护了一系列的节点称为zNode
  • 每个节点上都可以保存数据,数据形式是以key-value的形式存储的
  • 节点上都可以保存数据以及挂载子节点,构成层次化的树形结构

节点类型

  • 持久化节点:PERSISTENT 创建完成后一直存在zk服务器上,直到主动删除
  • 持久有序节点:PERSISTENT_SEQUENTIAL 会为其子节点维护一个顺序
  • 临时节点:EPHEMERAL 生命周期与会话绑定在一起,当客户端失效,自动清理
  • 临时有序节点:在临时节点的基础上多了一个有序性

zookeeper会话

  • 初始化或连接未建立的状态是connecting

  • 建立连接的状态时connected

  • 关闭连接的状态是closed

zookeeper节点状态信息

  • zk的每个节点除了存储数据内容之外,还存储了节点本身的状态信息,通过get命令可获得详细内容
状态属性 解释说明
czxid Create ZXID 表示该节点被创建时的事务ID
mzxid Modify ZXID 表示该节点最后一次被更新时的事务ID
ctime Create Time 表示节点被创建的时间
mtime Modify Time 节点最后一次被更新的时间
version 数据节点的版本号
cversion 子节点的版本号
aversion 节点的ACL版本号
ephemeralOwner 创建临时节点的会话sessionID若为持久化节点,该属性值为0
dataLength 数据内容的长度
numChildren 当前节点的子节点个数
pzxid 表示当前节点的子节点列表最后一次被修改时的事务ID(这里特指子节点列表变更)

zk-分布式数据的原子性

  • zk为数据节点引入了版本的概念,每个数据节点对应三类版本信息
  • 对数据节点的任何更新操作都会引起版本号的变化
  • 通过版本实现分布式数据的原子性类似于悲观锁和乐观锁的概念
    • 悲观锁、乐观锁概念回顾
    • 悲观锁是数据库中一种并发控制策略,当一个事务A正在对数据进行处理
    • 数据将会处于锁定状态,这期间其它事务无法对数据进行操作
    • 乐观锁,多个事务在处理过程中不受影响,在同时对一个数据进行修改时
    • 在更新请求提交之前,会对数据是否冲突进行检测,若发生冲突则提交被拒绝
  • zk就是通过version来实现乐观锁机制的——”写入校验“

watcher

  • zk提供了分布式数据的发布/订阅功能
  • zk允许客户端向服务端注册一个watcher监听,当服务端一些事件触发了watcher
  • 服务端向客户端发送一个事件通知
  • watcher通知是一次性的,一旦触发一次通知后,watcher就会失效,可以通过循环注册实现持续监听

zk重试策略

  • 在和zk服务器建立连接的时候,有提供三种连接策略 retryPolicy
RetryOneTime 仅仅重试一次
RetryUntilElapsed 一直重试直到规定时间结束
RetryNTimes 指定最大重试次数

zk节点权限控制

  • zk提供ACL权限控制机制(Access Control List)来保证数据的安全性
权限模式 授权对象
IP IP地址或IP段,被赋予权限的用户可在指定IP或IP段进行操作
Digest 最常用控制模式,设置的时候需要DigestAuthenticationProvider.generateDigest() SHA- 加 密和 base64 编码
World 最开放的控制模式,数据访问权限对所用用户开放
Surper 超级用户,可对节点进行任何操作

zk——watcher机制

  • zk提供watcher监听机制,可对节点数据变更、删除、子节点状态变更等事件进行监听
  • 通过监听机制,可以基于zookeeper实现分布式锁、集群管理等功能
zookeeper事件 事件含义
EventType.NodeCreated 当节点被创建,事件触发
EventType.NodeChildrenChanged 监听当前节点的直接子节点
EventType.NodeDataChanged 节点数据发生变更,事件触发
EventType.NodeDeleted 节点删除,事件触发
EventType.None 客户端连接状态变更,事件触发
  • watcher监听是一次性的,可通过循环注册实现永久监听

  • Curator是对zookeeper的封装,提供了三种watcher来监听节点的变化

    • PathChildCache:监视一个路径下目标子结点的创建、删除、更新

    • NodeCache:监视当前结点的创建、更新、删除,并将结点的数据缓存在本地

    • TreeCache:PathChildCache 和 NodeCache 的“合体”,监视路径下的创建、更新、删除事件,

      并缓存路径下所有孩子结点的数据

zk实现分布式锁

  • 基于Curator实现分布式锁
  • 在分布式架构下,涉及到多个进程访问同一个共享资源的情况
  • 这个时候没有办法使用synchronized和lock之类的锁实现数据安全
  • zookeeper实现分布式锁
  • zk的同级节点具有唯一性,可利用此特性实现独占锁
  • 即多个进程在zk的指定节点下创建一个名称相同的节点,只有一个进程能够创建成功,认为其获取了锁
  • 创建失败的节点可通过zookeeper的watcher机制来监听子节点的变化,监听到删除事件,触发所有进程去竞争锁
  • 通过有序节点实现分布式锁,可避免大量进程同时去竞争锁,每个节点只需要监听比自己小的节点,避免了资源浪费
  • curator基于zk提供了分布式锁的基本使用

Curator 实现分布式锁的基本原理

  • 利用path创建临时有序节点,实现公平锁核心,1代表了能获得分布式锁的数量,即为互斥锁

在这里插入图片描述

  • 由LockInternals对象执行分布式锁的申请和释放

在这里插入图片描述

  • internaLock(-1, null):不限时等待 internaLock:限时等待

在这里插入图片描述

private boolean internalLock(long time, TimeUnit unit) throws Exception
{
    Thread currentThread = Thread.currentThread();
    //  这里给定的 LockData 实例只能由一个线程操作
    LockData lockData = threadData.get(currentThread);
    if ( lockData != null )
    {
        // 实现可重入,lockData映射表存放线程的锁信息
        lockData.lockCount.incrementAndGet();
        return true;
    }

    ///  映射表 中没有对应的锁信息, ,通过 attempLock获得锁    
    String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
    if ( lockPath != null )
    {
        //  获取锁成功记录  锁信息到映射表  即  thread
        //  private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();
        LockData newLockData = new LockData(currentThread, lockPath);
        threadData.put(currentThread, newLockData);
        return true;
    }

    return false;
}
  • lockCount:分布式锁重入次数

在这里插入图片描述

  • attempLock 尝试获取锁,并返回锁对应的 Zookeeper 临时顺序节点的路径
String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception
{
    final long      startMillis = System.currentTimeMillis();
    //  无限时等待  millisToWait 为null
    final Long      millisToWait = (unit != null) ? unit.toMillis(time) : null;
    final byte[]    localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes;
    int             retryCount = 0;
    //  临时节点路径
    String          ourPath = null;
    // 是否已经持有分布式锁
    boolean         hasTheLock = false;
    //  是否已经完成获取锁操作
    boolean         isDone = false;
    while ( !isDone )
    {
        isDone = true;
        try
        {
            //  在 zk 中创建临时顺序节点
            ourPath = driver.createsTheLock(client, path, localLockNodeBytes);
            //  循环等待来激活分布式锁,实现锁的公平性
            hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);
        }
        catch ( KeeperException.NoNodeException e )
        {
            // gets thrown by StandardLockInternalsDriver when it can't find the lock node
            // this can happen when the session expires, etc. So, if the retry allows, just try it all again
            if ( client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper()) )
            {
                isDone = false;
            }
            else
            {
                throw e;
            }
        }
    }
    if ( hasTheLock )
    {
        // 成功获得分布式锁,返回临时顺序节点的路径上层将其封装成锁信息,记录在映射表,方便锁重入
        return ourPath;
    }
    return null;
}
  • 创建临时顺序节点即锁节点,createsTheLock
public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception
{
    String ourPath;
    //  lockNodeBytes为空的话,采用默认(IP地址)创建节点
    if ( lockNodeBytes != null )
    {
         ourPath=client.create()
             .creatingParentContainersIfNeeded()
             .withProtection()
             //  保证临时节点的有序性
             .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
             .forPath(path, lockNodeBytes);
    }
    else
    {
        ourPath = client.create()
            .creatingParentContainersIfNeeded()
            .withProtection()
            .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
            .forPath(path);
    }
    return ourPath;
}
  • internalLockLoop 判断是否已经持有分布式锁:循环等待激活,实现锁的公平性
private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception
{
    //  是否已经持有锁
    boolean     haveTheLock = false;
    //  是否删除子节点
    boolean     doDelete = false;
    try
    {
        if ( revocable.get() != null )
        {
            client.getData().usingWatcher(revocableWatcher).forPath(ourPath);
        }

        while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock )
        {
            //  获取排序后的子节点列表
            List<String> children = getSortedChildren();
            // +1 to include the slash  获取自己创建的临时子节点的名称
            String sequenceNodeName = ourPath.substring(basePath.length() + 1); 
            PredicateResults predicateResults = driver
                .getsTheLock(client, children, sequenceNodeName, maxLeases);
            if ( predicateResults.getsTheLock() )
            {
                // 获得了锁,中断循环
                haveTheLock = true;
            }
            else
            {
                //  没有获得锁,,监听上一临时顺序节点
                String  previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();

                synchronized(this)
                {
                    try 
                    {
    // use getData() instead of exists() to avoid leaving unneeded watchers which is a type of resource leak                         //  使用 getData() 避免资源泄露,exists会创建不必要的监听器
                        //  这里 如果监听到上一顺序节点被删除,,则会获得锁
                        client.getData().usingWatcher(watcher).forPath(previousSequencePath);
                        if ( millisToWait != null )
                        {
                            millisToWait -= (System.currentTimeMillis() - startMillis);
                            startMillis = System.currentTimeMillis();
                            if ( millisToWait <= 0 )
                            {
                                //  获取锁超时 删除属于当前进程的临时顺序节点
                                doDelete = true;    // timed out - delete our node
                                break;
                            }

                            wait(millisToWait);
                        }
                        else
                        {
                            wait();
                        }
                    }
                    catch ( KeeperException.NoNodeException e ) 
                    {
                        // it has been deleted (i.e. lock released). Try to acquire again
                    }
                }
            }
        }
    }
    catch ( Exception e )
    {
        ThreadUtils.checkInterrupted(e);
        doDelete = true;
        throw e;
    }
    finally
    {
        if ( doDelete )
        {
            deleteOurPath(ourPath);
        }
    }
    return haveTheLock;
}
  • getsTheLock获得锁的逻辑
    • 获取临时有序节点在排序后的列表中的索引
    • 校验创建的临时有序节点是否有效
      在这里插入图片描述
  • 锁公平性核心逻辑
    • 已经了解到maxLeases为1,即当ourIndex为零的时候,线程才能获得锁
    • zk临时有序节点特性,保证了跨多个JVM线程并发创建节点时的顺序性,越早创建成功越早激活锁
    • 如果已经获取了锁,无需监听任何节点,否则需要监听上一顺序节点,,即 ourIndex-1
    • 锁是公平的,因此无需监听除了 ourIndex-1 以外的任何节点
    • 如果 ourIndex < 0,则表示会话过期或连接丢失等原因,该线程创建的临时有序节点被zk删除,抛出异常
      • 如果在重试策略允许的范围内重新尝试获取锁,然后重新生成新的临时有序节点
  • 释放锁逻辑
    • newLockCount记录了锁重入次数,初始值为1,当 newLockCount=0,锁释放
    • 然后从映射表中移除对应线程的锁信息

zookeeper的leader选举原理

  • 在zk的分布式集群中,有着三种角色,leader、follower、observer

  • curator提供了两种选举 recipe -> Leader Latch 和 Leader Selector

    • Leader Latch:参与选举的所有节点都会创建一个顺序节点最小的节点会被设置为master节点

      没有抢占到Leader节点的节点都会对前一个节点的删除事件进行监听,

      当其删除或master节点手动调用close()或master节点挂掉,后续节点会抢占master

    • Leader Selector:和Leader Latch的差别在于leader节点在释放领导权之后会继续参与竞争

zookeeper数据同步

  • zookeeper通过三种不同的集群角色(leader、follower、observer),组成整个高性能集群
  • zk集群中,客户端会随机连接到zookeeper集群中的一个节点,如果是读请求,直接从当前节点读取数据
  • 如果是写请求,请求将会被转发给leader提交事务
  • 然后leader会广播事务,只要有超过半数节点写入成功,那么写请求就会被提交

在这里插入图片描述

流程详解

  • 1.客户端发出写事务请求到zk集群中的follower节点
  • 2.follower节点转发事务请求到leader节点进行处理
  • 3.leader节点发起proposal事务广播
  • 4.follower节点发回ack消息确认
  • 5.commit提交事务
  • 6.response返回客户端

zookeeper——ZAB协议

  • ZAB(zookeeper Atomic Broadcast)协议是为分布式协调服务zookeeper专门设计的
  • 支持 崩溃恢复 的原子广播协议
  • 在zk中,主要依赖ZAB协议来实现分布式数据的一致性
  • 基于该协议,zk实现了主备模式的系统架构中集群中各个副本之间的数据一致性
  • zab协议包含两种基本模式:崩溃恢复和原子广播

崩溃恢复和原子广播

  • 当zk集群启动后,leader节点出现网络中断、崩溃等情况时
  • 基于zab协议zk会自动进入恢复模式并选举出新的leader
  • 当leader选举出来之后,并且集群中有过半机器和leader节点的数据完成同步后
  • zab协议就会退出恢复模式,然后整个集群进入消息广播模式
  • 此时当一台同样遵守ZAB协议的服务器启动后加入到集群中时,如果集群中存在leader服务器进行消息广播
  • 新加入的服务器会自动进入数据恢复模式,进行数据同步,然后一起参与到消息广播流程中去
  • 注意点:如果一个事务proposal在一台机器上处理成功,那么该事务应该在所有机器上都被处理成功
  • 即,已经被处理的消息不能丢弃。被丢弃的消息不能再次出现
  • 判断依据是:消息是否被commit成功

消息广播实现原理

  • 消息广播的过程实际上是一个简化版本的二阶段提交过程,类似于分布式事务的2pc协议
  • 流程分析
  • leader接收到事务请求后,会生成一个全局唯一的64位的自增id,即zxid通过其大小保证消息因果顺序
  • leader为每一个follower节点准备了一个FIFO的队列(通过TCP协议实现,以实现全局有序的的特点)
  • 将带有zxid的消息作为一个提案(Proposal)分发给所有的follower
  • follower节点收到proposal后,会先把proposal写到磁盘,写入成功后返回给leader一个ack
  • 当leader接收到合法数量即超过半数节点的ACK之后,leader就会向这些follower发送commit命令,同时在本地执行目标消息
  • follower节点收到commit命令之后,会提交该消息(leader的投票过程不需要Observer的ack,仅做数据同步即可)

数据一致性保证

  • leader选举算法能够保证重新选举出来的leader拥有集群中所有机器最高编号zxid——zxid最大的事务proposal
  • 那么就可以保证重新选举的leader一定具有已经提交的proposal
  • 这是因为所有提案被commit之前必须有超过半数的follower节点发出ack确认,即拥有消息提案
  • 因此只要有合法数量的节点正常工作,就必然有一个节点保存了所有被commit消息的proposal状态

关于zxid

  • zxid构成以及作用

  • zxid是一个64位的数据,高32位时epoch编号,每经过一次leader选举产生一个新的leader,新的leader会将epoch号加1

  • 低32位是消息计数器,每收到一条消息,该值+1,新的leader选举后,消息被重置为0

    • 基于此设计,保证了挂掉的leader重新启动之后不会被选举为leader,它的zxid小于当前新的leader
    • 当旧的leader作为follower接入之后,新的leader会让其清除所有违背commit的proposal的epoch号
  • zxid说明

  • 为了保证事务的顺序一致性,zk采用了递增的事务id号(zxid)来标识事务

  • 所有的提案proposal都会被加上zxid

zookeeper的一致性

  • zk集群内部的数据副本同步是基于半提交策略的,意味着它是最终一致性,而不满足强一致性要求
  • zookeeper基于zxid以及阻塞队列的方式实现请求的顺序一致性
    • 如果一个client请求连接到一台follower节点,读取到最新数据,由于网络原因连接到其它节点
    • 若是重新连接到的follower节点还没有完成数据同步,将会读到旧的数据
    • 针对这种情况,client会记录自己已经读取到的最大的zxid,如果重新连接到server
    • 发现client持有的zxid比server的zxid要大,则连接会失败

zookeeper——leader选举原理

  • leader选举存在于两个阶段中,一个是服务器启动时的leader选举
  • 其二是运行过程中leader节点宕机导致的leader选举

重要参数

  • 服务器ID:myid:myid是服务器的编号,编号越大在选择算法中的权重越大
  • zxid:事务id:值越大说明数据越新,在选举算法中权重越大
  • 逻辑时钟也叫投票次数:epoch – logicalclock
    • 在同一轮投票过程中epoch-logicalclock的值是相同的,每次投票结束,该数值会增加
    • 然后接收其它服务器返回的投票信息中的数值进行比较,根据不同的值做出判断
  • 选举状态
LOOKING 竞选状态
FOLLOWING 随从状态,同步 leader 状态,参与投票
OBSERVING 观察状态,同步 leader 状态,不参与投票
LEADING 领导者状态

服务启动时的leader选举

  • 每个节点启动时的状态都是LOOKING
  • 在集群初始化阶段,当有一台服务器server1启动的时候,无法单独进行和完成leader选举
  • 当有多台服务器启动的时候,服务器之间此时可以进行相互通信,开始进入leader选举
|-每个server发起一次投票,首次投票,会将自身作为Leader服务器来进行投票
|-票据信息中会包含服务器的myid、zxid和epoch,使用vote进行封装(myid,zxid,epoch)
|-然后各自的投票信息发送给其它服务器
|-收到来自其它服务器的投票信息后,会首先判断投票是否有效(epoch),是否来自looking状态的服务器
|-然后进行pk,
——比较epoch
——检查zxid,zxid较大的服务器优先作为Leader,zxid相同比较myid
——myid较大的服务器作为leader服务器
|-投票结束后会统计投票信息,当有过半服务器接受了相同的投票信息,即选出了leader节点
|-确定leader之后,每个服务器会更新自己的状态,follower->followering   leader->leadering

运行过中的leader选举

  • 当集群中的 leader 服务器出现宕机或者不可用的情况时,进入新一轮leader选举
  • leader宕机之后,剩余follower服务器,将自己的状态改变为looking,然后开始进入leader选举过程
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章