ZooKeeper分布式专题与Dubbo微服务入门

zookeeper基本数据类型

  • zookeeper是一个树形结构,类似于前端开发中的 tree.js 组件;

    image.png

  • zk的数据模型也可以理解为 linux/unix 的文件目录:/usr/local

  • 每一个节点都称之为 znode,它可以有子节点,也可以有数据

    子节点: 就是父目录下的一个子目录,在 zk中称之为节点,每一个节点中都有一些相应的数据,就像目录下有一些文件数据一样

  • 每个节点分为临时节点和永久节点,临时节点在客户端断开后消失

    永久节点:其实就是一个持久化的过程,我们存了一些数据,只有人为的情况在才会删除 如是session超时或者是session丢失,数据还是会一直存在的。临时节点生命周期依赖创建它的会话,一旦会话结束,临时节点将会被删除。临时节点不允许有子节点。

  • 每个zk节点都是有各自的版本号,可以通过命令行来显示节点信息;

    节点的信息其实就是节点的详情,详情中包含了一些版本号,版本号是累加的,每当节点中的数据发生变化,版本号就会累加(乐观锁)

  • 删除/修改过时节点,版本号不匹配则会报错

    例如我们在查询某一个节点的时候,比如说它的节点是1,经过两个人进行删除或者修改之后,那么它的节点会由1变为2,在变为3,此时我们需要去修改或者删除这个节点,那么删除这个节点的时候,我们传入的版本号是一个老的版本号,那么这个时候就会报一个版本号不匹配的异常,这也是在数据库中是使用乐观锁的一种表现

  • 每一个zk节点存储的数据不宜过大,几k即可(官方推荐);

  • 节点是可以设置权限acl ,可以通过权限来限定用户的访问

    acl:权限控制列表,后续会讲到;

zookeeper应用场景

Master选举

在分布式系统中,Master往往用来协调集群中其他系统单元,具有对分布式系统状态变更的决定权,如在读写分离的应用场景中,客户端的写请求往往是由Master来处理,或者其常常处理一些复杂的逻辑并将处理结果同步给其他系统单元。利用Zookeeper的强一致性,能够很好地保证在分布式高并发情况下节点的创建一定能够保证全局唯一性,即Zookeeper将会保证客户端无法重复创建一个已经存在的数据节点。

首先创建/master_election/2019-07-05节点,客户端集群每天会定时往该节点下创建临时节点,如/master_election/2019-07-05/binding,这个过程中,只有一个客户端能够成功创建,此时其变成master,其他节点都会在节点/master_election/2019-07-05上注册一个子节点变更的Watcher,用于监控当前的Master机器是否存活,一旦发现当前Master挂了,其余客户端将会重新进行Master选举。

这也就是所谓的首脑模式,从而保证我们的集群是高可用的;

image.png

数据发布/订阅(以Dubbo注册中心为例)

数据发布/订阅系统,即配置中心。需要发布者将数据发布到Zookeeper的节点上,供订阅者进行数据订阅,进而达到动态获取数据的目的,实现配置信息的集中式管理和数据的动态更新。发布/订阅一般有两种设计模式:推模式和拉模式,服务端主动将数据更新发送给所有订阅的客户端称为推模式;客户端主动请求获取最新数据称为拉模式,Zookeeper采用了推拉相结合的模式,客户端向服务端注册自己需要关注的节点,一旦该节点数据发生变更,那么服务端就会向相应的客户端推送Watcher事件通知,客户端接收到此通知后,主动到服务端获取最新的数据。

  若将配置信息存放到Zookeeper上进行集中管理,在通常情况下,应用在启动时会主动到Zookeeper服务端上进行一次配置信息的获取,同时,在指定节点上注册一个Watcher监听,这样在配置信息发生变更,服务端都会实时通知所有订阅的客户端,从而达到实时获取最新配置的目的。

Dubbo是集团开源的分布式服务框架,致力于提供高性能和透明化的远程服务调用解决方案和基于服务框架展开的完整SOA服务治理方案。

其中服务自动发现是最核心的模块之一,该模块提供基于注册中心的目录服务,使服务消费方能够动态的查找服务提供方,让服务地址透明化,同时服务提供方可以平滑的对机器进行扩容和缩容,其注册中心可以基于提供的外部接口来实现各种不同类型的注册中心,例如数据库、ZK和Redis等。接下来看一下基于ZK实现的Dubbo注册中心

image.png

/dubbo: 这是Dubbo在ZK上创建的根节点。

/dubbo/com.foo.BarService:这是服务节点,代表了Dubbo的一个服务。

/dubbo/com.foo.BarService/Providers:这是服务提供者的根节点,其子节点代表了每个服务的真正的提供者。

/dubbo/com.foo.BarService/Comsumers:这是服务消费者的根节点,其子节点代表了每一个服务的真正的消费者。

Dubbo基于ZK实现注册中心的工作流程:

服务提供者:在初始化启动的时候首先在/dubbo/com.foo.BarService/Providers节点下创建一个子节点,同时写入自己的URL地址,代表这个服务的一个提供者。

服务消费者 : 在启动的时候读图并订阅zookeeper上/dubbo/com.foo.BarService/Providers节点下的所有节点,并解析所有提供者的URL地址作为该服务类的地址列表,开始发起正常的调用。同时在Consumers节点下创建一个临时节点,写入自己的URL地址,代表自己是BarService的一个消费者

监控中心 : 监控中心是Dubbo服务治理体系的重要一部分,它需要知道一个服务的所有提供者和订阅者及变化情况。监控中心在启动的时候会通过ZK的/dubbo/com.foo.BarService节点来获取所有提供者和消费者的url地址,并注册Watcher来监听其子节点变化情况。

所有服务提供者在ZK上创建的节点都是临时节点,利用的是临时节点的生命周期和客户端会话绑定的特性,一旦提供者机器挂掉无法对外提供服务时该临时节点就会从ZK上摘除,这样服务消费者和监控中心都能感知到服务提供者的变化。

命名服务

命名服务也是分布式系统中比较常见的一类场景,被命名的实体通常可以是集群中的机器、提供的服务地址或远程对象,其中较为常见的是一些分布式服务框架中的服务地址列表,通过使用命名服务客户端应用能够制定名字来获取资源的实体、服务地址和提供者的信息等。

上层应用使用命名服务时可能仅需要一个全局唯一的名字,类似于数据库中的唯一主键,用数据库自增id是可以的,但分库分表的情况下就无法依靠数据库的自增属性来唯一标识一条记录了。另外UUID也是一种广泛应用的ID实现方式,但如果是用UUID对服务进行命名的话就太不直观了,从字面意思根本看不出其表达的含义。下面看下用ZK如何实现全局唯一ID的生成。

之前在ZNode介绍时提过,创建节点时可以设定为SEQUENTIAL顺序节点,创建后API会返回这个节点的完整名字,利用这个特性我们就可以来生成全局唯一ID了。

image.png

所有客户端根据自己的任务类型,在指定类型的任务下创建一个顺序节点,例如“Job-”节点

节点创建完毕后会返回一个完整的节点名称,如Job-0000000001

客户端拿到这个返回值后拼接上type类型,例如type1-Job-000000001,这样就可以作为一个全局唯一的ID了

在ZK中每个数据节点都能维护一份子节点的顺序序列,当客户端对其创建一个顺序子节点时ZK会自动以后缀的形式在其子节点上添加一个序号,该场景就利用了ZK的这个特性。

分布式锁

分布式锁用于控制分布式系统之间同步访问共享资源的一种方式,可以保证不同系统访问一个或一组资源时的一致性,主要分为排它锁和共享锁。

排它锁又称为写锁或独占锁,若事务T1对数据对象O1加上了排它锁,那么在整个加锁期间,只允许事务T1对O1进行读取和更新操作,其他任何事务都不能再对这个数据对象进行任何类型的操作,直到T1释放了排它锁。

如果不同系统或同一系统不同机器之间共享了同一资源,那访问这些资源时通常需要一些互斥手段来保证一致性,这种情况下就需要用到分布式锁了。

使用关系型数据库是一种简单、广泛的实现方案,但大多数大型分布式系统中数据库已经是性能瓶颈了,如果再给数据库添加额外的锁会更加不堪重负;另外,使用数据库做分布式锁,当抢到锁的机器挂掉的话如何释放锁也是个头疼的问题。

接下来看下使用ZK如何实现排他锁。排他锁的核心是如何保证当前有且只有一个事务获得锁,并且锁被释放后所有等待获取锁的事务能够被通知到。

image.png

  • 获取锁,在需要获取排它锁时,所有客户端通过调用接口,在/exclusive_lock节点下创建临时子节点/exclusive_lock/lock。Zookeeper可以保证只有一个客户端能够创建成功,没有成功的客户端需要注册/exclusive_lock节点监听。

  • 释放锁,当获取锁的客户端宕机或者正常完成业务逻辑都会导致临时节点的删除,此时,所有在/exclusive_lock节点上注册监听的客户端都会收到通知,可以重新发起分布式锁获取。

共享锁又称为读锁,若事务T1对数据对象O1加上共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁,直到该数据对象上的所有共享锁都被释放。

image.png

  • 获取锁: 在需要获取共享锁时,所有客户端都会到/shared_lock下面创建一个临时顺序节点,如果是读请求,那么就创建例如/shared_lock/host1-R-00000001的节点,如果是写请求,那么就创建例如/shared_lock/host2-W-00000002的节点。

  • 判断读写顺序:不同事务可以同时对一个数据对象进行读写操作,而更新操作必须在当前没有任何事务进行读写情况下进行,通过zookeeper来确定分布式读写顺序,大致分为四步。

    • 创建完节点后,获取/shared_lock节点下所有字节点,并对该节点变更注册监听。

    • 确定自己的节点序号在所有子节点中的顺序

    • 对于读请求:若没有比自己序号小的子节点或者所有比自己序号小的子节点都是读请求,那么表明自己已经成功获取共享锁,同时开始执行读取逻辑,若有写请求,则需要等待。对于写请求:若自己不是序号最小的子节点,那么需要等待。

    • 接收到Watcher通知后,重复步骤1.

  • 释放锁:其释放锁的流程与独占锁的流程一致。

ZooKeeper分布式专题与Dubbo微服务入门