Zookeeper是一个开源的分布式的,为分布式应用提供协调服务的Apache项目。
Zookeeper工作机制:文件系统+通知机制
Zookeeper从设计模式角度理解:是一个基于观察者设计模式的分布式服务管理框架,它负责存储和管理大家都关系的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper就将负责通知已经在Zookeeper上注册的那些观察者做出相应的回应。
二、Zookeeper特点
-
Zookeeper:一个领导者(leader),多个跟随者(follower)组成的集群。
-
Leader负责进行投票的发起和决议,更新系统状态,
-
Follower用于接收客户请求并向客户端返回结果,在选举Leader过程中参与投票
-
集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。
-
全局数据一致:每个server保存一份相同的数据副本,client无论连接到哪个server,数据都是一致的。
-
更新请求顺序进行,来自同一个client的更新请求按其发送顺序依次执行。
-
数据更新原子性,一次数据更新要么成功,要么失败。
-
实时性,在一定时间范围内,client能读到最新数据。
三、工作机制
3.1、文件系统+通知机制
1、被管理的分布式服务器一旦启动就会向Zookeeper发送上线状态信息(数据)
2、Zookeeper接受观察者(客户端)的注册,并根据服务器注册的信息变化进行实时通知
3、观察者对于Zookeeper的通知进行响应的反应。
3.2、投用选举机制
追随者得到半数追随者的票数就可以成为领导者,一般服务器上线就会给自己投票,因为票数没有达到一般,所以无法成为领导者,等下一台服务器上线,也会也给自己投一票,前面上线的服务器如果Id比这一台小,就会给自己投票,.....就这样一直投票,等到哪一台的服务器一上线得到的票数超过半数,就会成为领导者。
3.3、Zookeeper的监听原理
1、在zookeeper客户端被创建,这时候就会创建出两个线程,一个是负责网络通信(connet),一个是负责监听(listener)。
2、通过connet线程将注册的监听事件发送给zookeeper(getChildren("监听的路径",true))
3、在zookeeper的监听器列表,会将注册的监听事件添加到列表
4、zookeeper监听到有数据或路径变化,就会将者消息发送给listener
5、listener线程内部调用了process()方法进行处理。
3.4、Zookeeper的工作流程
1、客户端向zk服务器发送写入请求的数据
2、该服务器如果不是Leader,就会将请求转发给Leader
3、Leader处理好请求,对所有的follower进行数据广播,follower执行写的操作。
4、只要半数follower(追随者)反馈ok,领导者就向所有follewer发送commit消息。
四、zookeeper的设计目标
zooKeeper致力于为分布式应用提供一个高性能、高可用,且具有严格顺序访问控制能力的分布式协调服务
-
高性能
Zookeeper将全量数据存储在内存中,并直接服务于客户端的所有非事务请求,尤其适用于以读为主的应用场景
- 高可用
zooKeeper一般以集群的方式对外提供服务,一般3 ~ 5台机器就可以组成一个可用的Zookeeper集群了,每台机器都会在内存中维护当前的服务器状态,并且每台机器之间都相互保持着通信。只要集群中超过一半的机器都能够正常工作,那么整个集群就能够正常对外服务
- 严格顺序访问
对于来自客户端的每个更新请求,ZooKeeper都会分配一个全局唯一的递增编号,这个编号反映了所有事务操作的先后顺序。
五、zookeeper数据模型
zookeeper的数据节点可以视为树状结构(或者目录),树中的各节点被称为znode(即zookeeper node),一个znode可以有多个子节点。zookeeper节点在结构上表现为树状;使用路径path来定位某个znode。
znode,兼具文件和目录两种特点。既像文件一样维护着数据、元信息、ACL、时间戳等数据结构,又像目录一样可以作为路径标识的一部分。
那么如何描述一个znode呢?一个znode大体上分为3各部分:
-
节点的数据:即znode data(节点path, 节点data)的关系就像是java map中(key, value)的关系
-
节点的子节点children
-
节点的状态stat:用来描述当前节点的创建、修改记录,包括cZxid、ctime等
在zookeeper shell中使用get命令查看指定路径节点的data、stat信息:
[zk: localhost:2181(CONNECTED) 7] get /a cZxid = 0x6a0000000a #数据节点创建时的事务 ID ctime = Wed Mar 27 09:56:44 CST 2019 #数据节点创建时的时间 mZxid = 0x6a0000000a #数据节点最后一次更新时的事务 ID mtime = Wed Mar 27 09:56:44 CST 2019 #数据节点最后一次更新时的时间 pZxid = 0x6a0000000e #数据节点的子节点最后一次被修改时的事务 ID cversion = 2 #子节点的更改次数 dataVersion = 0 #节点数据的更改次数 aclVersion = 0 #节点的 ACL 的更改次数 ephemeralOwner = 0x0 #ephemeralOwner:如果节点是临时节点,则表示创建该节点的会话的SessionID;如果节点是持久节点,则该属性值为 0 dataLength = 0 #数据内容的长度 numChildren = 2 #数据节点当前的子节点个数
5.2、节点类型
zookeeper中的节点有两种,分别为临时节点和永久节点。节点的类型在创建时即被确定,并且不能改变。
-
-
临时节点:该节点的生命周期依赖于创建它们的会话。一旦会话(Session)结束,临时节点将被自动删除,当然可以也可以手动删除。虽然每个临时的Znode都会绑定到一个客户端会话,但他们对所有的客户端还是可见的。另外,ZooKeeper的临时节点不允许拥有子节点。
-
-
持久化节点:该节点的生命周期不依赖于会话,并且只有在客户端显示执行删除操作的时候,他们才能被删除。
六、zookeeper应用场景
ZooKeeper 是一个高可用的分布式数据管理与系统协调框架。基于对 Paxos 算法的实现,使该框架保证了分布式环境中数据的强一致性,也正是基于这样的特性,使得 ZooKeeper 解决很多分布式问题。
下面对 ZK 的应用场景迚行一个分门归类的介绍。
6.1、数据发布与订阅(配置中心)
发布与订阅模型,即所谓的配置中心,顾名思义就是发布者将数据发布到 ZK 节点上,供订阅者动态获取数据,实现配置信息的集中式管理和动态更新。例如全局的配置信息,服务式服务框架的服务地址列表等就非常适合使用。
- 应用中用到的一些配置信息放到ZK上进行集中管理。这类场景通常是这样:应用在启动的时候会主动来获取一次配置,同时,在节点上注册 一个 Watcher,这样一来,以后每次配置有更新的时候,都会实时通知到订阅的客户端,从来达到获取最新配置信息的目的。
- 分布式搜索服务中,索引的元信息和服务器集群机器的节点状态存放在 ZK 的一些指定节点,供各个客户端订阅使用。
- 分布式日志收集系统。这个系统的核心工作是收集分布在不同机器的日志。收集器通常是按照应用来分配收集任务单元,因此需要在 ZK上创建一个以应用名作为 path 的节点 P,并将这个应用的所有机器 ip,以子节点的形式注册到节点 P 上,这样一来就能够实现机器变动的时候, 能够实时通知到收集器调整任务分配。
- 系统中有些信息需要动态获取,并且还会存在人工手动去修改这个信息的发问。通常是暴露出接口,例如 JMX 接口,来获取一些运行时的信息。 引入ZK之后,就不用自己实现一套方案了,只要将这些信息存放到指定的ZK节点上即可。
注意:在上面提到的应用场景中,有个默认前提是:数据量很小,但是数据更新可能会比较快的场景。
分布式锁,这个主要得益于ZooKeeper 为我们保证了数据的强一致性。锁服务可以分为两类,一个是保持独占,另一个是控制时序。
所谓保持独占,就是所有试图来获取这个锁的客户端,最终只有一个可以成功获得这把锁。通常的做法是把 zk 上的一个 znode 看作是一把锁, 通过 create znode 的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。
控制时序,就是所有视图来获取这个锁的客户端,最终都是会被安排执行,只是有个全局时序了。做法和上面基本类似,只是这里 /distribute_lock 已经预先存在,客户端在它下面创建临时有序节点( 这个可以通过节点的 属性控制: CreateMode.EPHEMERAL_SEQUENTIAL 来指定)。ZK的父节点(/distribute_lock)维持一份 sequence,保证子节点创建的时序性,从而也形成了每个客户端的全局时序。
一个集群有时会因为各种软硬件故障或者网络故障,出现某些服务器挂掉而被移除集群,而某些服务器加入到集群中的情况,zookeeper会将这些服务器加入/移出的情况通知给集群中的其他正常工作的服务器,以及时调整存储和计算等任务的分配和执行等。此外zookeeper还会对故障的服务器做出诊断并尝试修复。
6.4、生成分布式唯一ID
在过去的单库单表型系统中,通常可以使用数据库字段自带的auto_increment属性来自动为每条记录生成一个唯一的ID。但是分库分表后,就无法在依靠数据库的auto_increment属性来唯一标识一条记录了。此时我们就可以用zookeeper在分布式环境下生成全局唯一ID。做法如下:每次要生成一个新Id时,创建一个持久顺序节点,创建操作返回的节点序号,即为新Id,然后把比自己节点小的删除即可。
6.5、负载均衡
这里说的负载均衡是指软负载均衡。在分布式环境中,为了保证高可用性,通常同一个应用或同一个服务的提供方都会部署多份,达到对等服务。 而消费者就须要在这些对等的服务器中选择一个来执行相关的业务逻辑,其中比较典型的是消息中间件中的生产者,消费者负载均衡。
-
消息中间件中发布者和订阅者的负载均衡,linkedin 开源的KafkaMQ 和阿里开源的 metaq都是通过 zookeeper 来做到生产者、消费者的负载均衡。这里以 metaq 为例如讲下:
生产者负载均衡
- metaq 发送消息的时候,生产者在发送消息的时候必须选择一台 broker 上的一个分区来发送消息,因此 metaq 在运行过程中,会把所有 broker 和对应的分区信息全部注册到 ZK 指定定节点上,默认的策略是一个依次轮询的过程,生产者在通过ZK获取分区列之后,会按照 brokerId和partition的顺序排列组织成一个有序的分区列表,发送的时候按照从头到尾循环往复的方式选择一个分区来发送消息。
消费负载均衡
在消费过程中,一个消费者会消费一个或多个分区中的消息,但是一个分区只会由一个消费者来消费。MetaQ 的消费策略是:
-
每个分区针对同一个 group 只挂载一个消费者。
-
如果同一个 group 的消费者数目大于分区数目,则多出来的消费者将不参不消费。
-
如果同一个 group 的消费者数目小于分区数目,则有部分消费者需要额外承担消费任务。
在某个消费者故障或者重启等情况下,其他消费者会感知到这一变化(通过 zookeeper watch 消费者列表),然后重新迚行负载均衡,保证 所有的分区都有消费者迚行消费。
在Zookeeper中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求。
6.6、命名服务(Naming Service)
命名服务也是分布式系统中比较常见的一类场景。在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址, 提供者等信息。被命名的实体通常可以是集群中的机器,提供的服务地址,进程对象等等——这些我们都可以统称他们为名字(Name)。其中较为常见的就是一些分布式服务框架中的服务地址列表。通过调用 ZK 提供的创建节点的 API,能够很容易创建一个全局唯一的 path,这个 path 就 可以作为一个名称。
阿里开源的分布式服务框架 Dubbo 中使用 ZooKeeper 来作为其命名服务,维护全局的服务地址列表,
Dubbo 实现中:
-
服务提供者在启劢的时候,向 ZK 上的挃定节点/dubbo/${serviceName}/providers 目彔下写入自己的 URL 地址,这个操作就完成了服务的 发布。
-
服务消费者启劢的时候,订阅/dubbo/${serviceName}/providers 目彔下的提供者 URL 地址, 并向/dubbo/${serviceName} /consumers 目彔下写入自己的 URL 地址。
注意,所有向 ZK 上注册的地址都是临时节点,这样就能够保证服务提供者和消费者能够自劢感应资源的变化。
另外,Dubbo 还有针对服务粒度的监控,方法是订阅/dubbo/${serviceName}目彔下所有提供者和消费者的信息。