• 分布式协调服务 ( 服务治理 ).


    分布式协调服务 ( 服务治理 ).

    标签(空格分隔): Java


    1. 问题所在

    • 主要用于解决分布式环境中多个进程之间的同步控制, 让他们有序的去访问某种临界资源, 防止造成脏数据的后果.
    订单服务JVM1->商品服务(库存五个): 我要五个
    订单服务JVM2->商品服务(库存五个): 我要五个
    订单服务JVM3->商品服务(库存五个): 我要五个
    商品服务(库存五个)-->订单服务JVM1:给你五个
    商品服务(库存五个)-->订单服务JVM2:给你五个
    商品服务(库存五个)-->订单服务JVM3:给你五个
    

    三个JVM 同时发送清空库存,这个时候就造成了脏数据的问题, 库存变成了 (-10)

    2. 解决方案

    • 分布式锁: 在第一个订单服务访问到商品服务的时候, 我们将商品服务加锁. 这个时候 第二个订单服务去访问 商品服务的时候会被拒绝.
    • 分布式协调的核心就是 实现分布式锁, 而Zookeeper就是分布式锁的实现框架.

    分布式锁


    1. 目的

    • 为了防止分布式系统中的多个进程之间的相互干扰, 我们需要一种分布式协调技术去对这些进程进行调度, 而这个分布式协调技术的核心就是来实现这个分布式锁, 而Zookeeper 就是分布式锁的实现框架 .

    2. 完备条件

    • 在分布式系统环境下, 一个方法在同一时间只能被一个机器的一个线程执行.
    • 高可用的获取锁和释放锁.
    • 高性能的获取锁和释放锁.
    • 具备非阻塞特性 , 即没有获取到锁将直接放回获取锁失败.
    • 具备失效机制, 防止死锁. ( 在加锁以后因为发生某些意外, 这个时候可以让锁失效, 而不是一直持有锁, 造成死锁. )
    • 具备可重入特征(可以理解为重新进入, 由于多于一个任务并发适用, 而不必担心数据错误).

    3. 常用方案

    • Memcached: 利用Memcached的add 命令. 此命令是原子性操作, 只有在key不存在的情况下, 才能add 成功, 也就意味着线程得到了锁.
    • Redis: 和Memcached的方法类似, 利用Redis的setnx命令. 此命令同样是原子性操作, 只有在key不为空的情况下,才能set成功.
    • Zookeeper: 利用Zookeeper的顺序临时节点, 来实现分布式锁和等待队列, Zookeeper设计的初衷,就是为了实现分布式锁.
    • Chubby: Google公司实现的粗粒度分布式锁服务,底层利用了Paxos一致性算法.

    分布式锁实现的三个核心要素:


    1. 加锁

    • 最简单的方式是使用setnx命令. key是锁的唯一标识, 按业务来决定命名. 比如想要给一种商品的秒杀活动加锁, 可以给key 命名为 lock_sale_商品ID. 而value可以姑且设置为1. 加锁的伪代码如下:
    setnx(lock_sale_商品ID,1);
    
    • 当一个线程执行setnx返回1, 说明key原本不存在, 则该线程成功得到了锁; 当一个线程执行setnx返回0, 说明key已经存在, 该线程抢锁失败.

    2. 解锁

    • 有加锁就有解锁. 当得到锁的线程执行完任务之后,需要释放锁, 以便其他线程可以进入. 释放锁的最简单方式是执行del指令, 伪代码如下:
    del(lock_sale_商品ID);
    

    3. 锁超时

    • 锁超时是什么意思呢? 如果一个得到锁的线程在执行任务的过程中挂掉, 来不及显示的释放锁, 这块资源将会被永远的锁住(死锁), 别的线程再也别想进来. 所以setnxkey必须设置一个超时时间, 以保证及时没有被显式的释放, 这把锁也要在一定的时间后自动释放. setnx不支持超时参数, 所以需要额外的指令,伪代码如下:
    expire(lock_sale_商品ID, 30)
    

    综合伪代码如下: 如果可以获得锁的话, 先设置自动释放的时间, 然后去do something

    if(setnx(lock_sale_商品ID,1) == 1){
        expire(lock_sale_商品ID,30)
        try {
            do something ......
        } finally {
            del(lock_sale_商品ID)
        }
    }
    

    以上代码存在三个致命问题


    1. setnxexpire的非原子性

    假设一个极端的场景, 上述setnx执行完毕得到了锁, 但是在没有执行expire的时候服务器宕机了, 这个时候依然是没有过期时间的死锁, 别的线程再也无法获得锁了. setnx本身是不支持传入操作时间的, 但是set指令增加了可选参数, 其伪代码如下:

    set(lock_sale_商品ID,1,30,NX);
    

    2. del误删

    不确定到底expire到底设置为多长的时间, 如果是30s的话 ,那么万一30s内A任务没有将 something执行完毕, 这个时候 依然将锁释放掉了, 此时B任务进程成果或得到了锁. 然后A进程执行完毕, 按照del来释放锁, 这个时候就出问题了.

    3. 第一种问题已经有解决方案了, 现在是第二个问题的解决方案.

    为了避免这种情况的发生我们可以在del释放锁之前做一个判断, 验证当前的锁是不是自己加的锁. 具体的实现: 我们在加锁的时候把当前的线程ID作为锁的value,并且在删除之前验证key对应的value是不是自己的线程ID.

    // 加锁
    String threadId = Thread.currentThread().getId();
    set(key,threadId,30,NX);
    
    // 解锁
    if(threadId .equals(redisClient.get(key))){
        del(key)
    }
    

    4. 但是这样又出现 第二点的问题. 解锁的代码不是原子性操作.

    如果判断结束之后, 发现当前线程的ID, 当时在没有执行del的时候, expire了, 这样就又回到了第二种方案的致命问题.

    5. 致命大杀器

    现在可以确定的是目前的问题解决思路是存在问题的, 应该换一种思路. 应该从第二种del误删这里向下继续解决这个问题.

    第二点问题描述: 可能存在多个线程同时执行该代码块.

    第二点问题原因分析: 因为不确定代码的执行时间, 可能设置30S的话 大家都会疯狂超时.

    第二点问题解决思路: 设置一个可以动态变化,可以满足代码块运行时间的, 且可以应对宕机情况的守护进程.

    方案: 给锁开启一个守护进程, 用来给锁进行续航操作, 当时间到29S , 发现还没有执行完毕的时候, 守护进程执行expire 给锁续命, 如果宕机的话 没人给锁续命, 时间到了之后 也会自动释放锁.


    什么是Zookeeper

    主要有两个功能, 分布式锁和服务注册与发现. 以下主要说明服务注册与发现部分.


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

    Zookeeper的数据模型是一个标准的二叉树结构. 树是由节点Znode组成的, 但是不同于树的节点, Znode的饮用方式是路径引用, 类似于文件路径.

    /动物/猫
    /汽车/宝马
    

    Znode的数据结构

    // 元数据: 数据的数据, 例如数据的创建,修改时间, 大小等.
    Znode{
        data; // Znode存储的信息
        ACL;  // 记录Znode的访问权限
        stat; // 包含Znode的各种元数据, 比如事务的ID,版本号,时间戳,大小
        child;// 当前节点的子节点引用
    }
    

    Zookeeper这样的数据结构是为了读多写少的场景所设计的. Znode并不是用来存储大规模业务数据,而是用于存储少量的状态和配置信息, 每个节点的数据最大不能超过1MB.

    1. Zookeeper的基本操作

    • 创建节点
    create
    
    • 删除节点
    delete
    
    • 判断节点是否存在
    exists
    
    • 获得一个节点的数据
    getData
    
    • 设置一个节点的数据
    setData
    
    • 获取节点下的所有子节点
    getChildren
    

    其中exists,getData,getChildren属于读操作. Zookeeper客户端在请求读操作的时候,可以选择是否设置Watch.


    Zookeeper的事件通知

    根据事件通知机制, 如果某个服务下线, Zookeeper会及时发现并且将消息异步传送至客户端A(API GateWay), 这个时候网关发现服务下线会及时启用该服务的备用服务, 从而达到高可用的特性.

    整个服务注册与发现 也是基于事件通知机制.


    我们可以把Watch理解成是注册在特定Znode上的触发器, 当这个Znode发生改变, 也就是调用了该节点的create, delete, setData方法的时候, 将会出发Znode上注册的对应事件, 请求Watch的客户端会接收到异步通知.

    设置watch示例: 客户端调用getData方法, watch参数是true. 服务端接收到请求, 返回节点数据, 并且在对应的哈希表里插入被WatchZnode的路径,以及Watcher列表.
    设置/动物/猫 Znode的watch

    异步获取反馈信息watch示例: 根据上述操作WatchTable只用已经有了 /动物/猫节点的信息, 这个时候我们对其进行delete操作. 服务端会查找HashTable发现该节点的信息, 然后异步通知客户端A,并且删除哈希表中对应的Key-Value.
    异步反馈消息


    Zookeeper的一致性


    为了防止服务注册与发现(Zookeeper)挂掉的情况, 我们需要对Zookeeper的自身实现高可用, 这个时候我们需要维护一个Zookeeper集群, 假设目前集群中有 ZkA,ZkB,ZkC , 三台机器. 该项目下存在多个项目, 每个项目将自身链接到 ZkA,ZkB,ZkC中某个服务注册与发现中心. 在更新数据(包括服务注册)的时候, 先将数据更新到主节点(Leader), 然后同步到从节点(Follwer) .


    Zookeeper Atomic Broadcast


    1. ZAB协议定义的三种状态

    • Looking: 选举状态
    • Following: Follower节点所处的状态
    • Leading: Lead接待所处的状态

    最大ZXID

    最大ZXID也就是节点本地的最新事务编号, 包含epoch和计数两部分. epoch是纪元的意思, 相当于Raft算法选主时候的term.

    ZXID是一个64位的数字, 低32位代表一个单调递增计数器, 高32位代表Leader的周期. 当有新的Leader产生的时候,Leader的epoch+1, 计数器从0开始; 每当处理一个新的请求的时候, 计数器+1.

    Epoch 计数器
    Leader周期 单调递增,从0开始
    高32位 低32位

    崩溃恢复

    1. Leader Selection

    • 选举阶段,此时集群中的节点处于Looking状态( Zookeeper刚开启的时候也是这个状态 ), 他们会向其它节点发起投票, 投票中包含自己的服务器ID和最新事务ID.
    • 将自己的ZXID和其它机器的ZXID比较, 如果发现别人的ZXID比自己的大, 也就是数据比自己的新, 那么重新发起投票, 投票给目前最大的ZXID所属节点. (比较ZXID的大小的时候,前32位是一致的. 只能从后32位比较, 这样就是处理请求越多的节点的ZXID的越大)
    • 每次投票结束之后,服务器都会统计投票数量, 判断是否有某个节点得到半数以上的投票. 如果存在这样的节点, 该节点会成为准Leader,状态变为Leading. 其他节点的状态变为```Following.

    2. Discovery

    • 发现阶段, 用于在从节点中发现最新的ZXID和事务日志. 或许有人会问: 既然Leader被选为主节点, 已经是集群里面数据最新的了, 为什么还要从节点中寻找最新的事务呢?
    • 为了防止某些意外情况, 比如因为网络原因在上一个阶段产生多个Leader的情况.
    • Leader接收所有Follower发送过来各自的epoch值, Leader从中选出最大的epoch,基于此值+1, 生成新的epoch分发给各个Follower.
    • 各个Follower收到全新的epoch之后返回ACKLeader,带上各自最大的ZXID和历史事务事务日志,Leader从中选出最大的ZXID, 并更新自身的历史日志.

    3. Synchronization

    • 同步阶段, 把Leader刚才收集到的最新历史事务日志, 同步给集群中所有的Follower, 只有当半数Follower同步成功, 这个准Leader才能成为正式的Leader.
    • 自此故障恢复完成, 其大约需 30-120S , 期间服务注册与发现 集群是无法正常工作的.

    ZAB数据写入(Broadcast)

    • ZAB的数据写入涉及到Broadcast阶段, 简单来说, 就是Zookeeper常规情况下更新数据的时候, 有Leader广播到所有的Follower. 其过程如下. (Zookeeper 数据一致性的更新方式)
    1. 客户端发出写入数据请求给任意的Follower.
    2. Follower把写入数据请求转发给Leader.
    3. Leader采用二阶段提交方式, 先发送Propose广播给Follower.
    4. Follower接收到Propose消息,写入日志成功后, 返回ACK消息给Leader.( 类似数据库的insert操作 )
    5. Leader接收到半数以上的ACK(类似Http状态码)消息, 返回成功给客户端, 并且广播Commit请求给Follower. (在第四步, insert之后, 执行commit操作,进行数据持久化)

    ZAB 协议既不是强一致性也不是弱一致性, 而是处于两者之间的单调一致性(顺序一致性). 它依靠事务的ID和版本号, 保证了数据的更新和读取时有序的.


    Zookeeper的应用场景


    1. 分布式锁

    这里雅虎研究院设计Zookeeper的初衷, 利用Zookeeper的临时顺序节点可以轻松的实现 分布式锁.

    2. 服务注册与发现

    利用ZnodeWatch, 可以实现分布式服务的注册与发现. 最著名的应用就是阿里的分布式RPC框架Dubbo .

    3. 共享配置和状态信息

    Redis的分布式解决方案Codis, 就利用了Zookeeper来存放数据路由表和codis-proxy节点的元信息. 同时codis-config发起的命令都会通过Zookeeper同步到各个存活的codis-proxy.

    此外, kafka,Hbase,Hadoop也都依靠Zookeeper同步节点信息,实现高可用.

  • 相关阅读:
    符号表实现(Symbol Table Implementations)
    揭开枚举类的面纱(Unlocking the Enumeration/enum Mystery)
    玩转指针(Playing with Pointers)
    什么是空间复杂度(What is actually Space Complexity ?)
    论困于记忆之物(随笔感言)
    【未有之有】洛依文明相关
    告别
    【未有之有】洛森修炼体系整理
    【未有之有】洛森十三圣人
    复苏
  • 原文地址:https://www.cnblogs.com/A-FM/p/11435096.html
Copyright © 2020-2023  润新知