[关闭]
@x-power 2019-09-16T06:47:33.000000Z 字数 6262 阅读 630

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

操作系统 分布式 Zookeeper


1. 问题所在

订单服务JVM1商品服务(库存五个)订单服务JVM2订单服务JVM3我要五个我要五个我要五个给你五个给你五个给你五个订单服务JVM1商品服务(库存五个)订单服务JVM2订单服务JVM3

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

2. 解决方案


分布式锁


1. 目的

2. 完备条件

3. 常用方案


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


1. 加锁

  1. setnx(lock_sale_商品ID,1);

2. 解锁

  1. del(lock_sale_商品ID);

3. 锁超时

  1. expirelock_sale_商品ID 30

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

  1. ifsetnxlock_sale_商品ID1 == 1){
  2. expirelock_sale_商品ID30
  3. try {
  4. do something ......
  5. } finally {
  6. dellock_sale_商品ID
  7. }
  8. }

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


1. setnxexpire的非原子性

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

  1. set(lock_sale_商品ID130NX);

2. del误删

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

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

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

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

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

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

5. 致命大杀器

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

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

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

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

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


什么是Zookeeper

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


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

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

  1. /动物/
  2. /汽车/宝马

Znode的数据结构

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

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

1. Zookeeper的基本操作

  1. create
  1. delete
  1. exists
  1. getData
  1. setData
  1. 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协议定义的三种状态


最大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同步节点信息,实现高可用.

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注