老吴的学习笔记-Zookeeper

2016-06-28  本文已影响557人  老吴学技术

摘要

要学习系统构架,ZooKeeper (下文简称zk)是无法绕开的开源技术。大型网站后台成百上千的分布式服务节点通常要依赖于zk来组织协调系统间依赖关系。分布式消息队列如kafka、分布式缓存如redis集群、远程调用协议dubbo等,都是通过zk来完成系统各部分间的协调工作。虽然zk也可以有多种其它用法,例如成员管理、leader选举、命名服务等等,但zk的核心仍然只是一个发布配置与订阅配置的服务。例如,要协调服务的调用方与被调用方,被调用方先发布自己的ip和端口到zk中,服务调用方在zk中订阅被调用方的ip地址与端口配置,即可获得相应的ip地址和端口,并获得及时的更新。本文将重点为读者阐明zk完成配置协调功能的原理,以及它的优势与劣势。最后讲一讲zk的其它用法到底是怎么回事。

引言

zk设计的出发点,是要解决分布式系统的配置协调问题(文献链接)。在zk出现之前,许多大型系统都为此功能不断做重复开发。由于这些系统的主要目标都不是实现一个专门用于协调配置的系统,因此其实现多少都有过于简化、缺乏通用性、容易单点故障的特点,并且存在难于发现的bug.

Yahoo!的工程师们开发了zk。开源出来之后,迅速得到广泛的应用。得益于它有以下优点:

用redis自己实现ZooKeeper

为加深你对zk的理解,我们先尝试自己设计zk系统,看看有哪些核心问题需要解决。下面我们就来实现配置发布/订阅功能。配置的通用形式就是key-value,因此想到使用redis来实现(不了解redis的读者可以认为它是一个巨型的HashMap)。系统结构如下:

redis-zk

任意机器可以通过向redis中写入配置项完成配置的发布。订阅的机器直接从redis读取配置即可。细心的读者可能会问:redis集群是如何构建的呢?实际上,许多redis集群本身也是基于zk来做的。不过redis官方只规定了redis集群规范,并没有指定实现的方式。所以当然也可以使用别的方式来实现。这里的例子只是为了方便大家理解,不必去深究这个问题。

解决配置重名问题-名称空间

系统日渐复杂,需要配置的项目也越来越多了。为了避免不同服务配置的名称冲突,借鉴java包结构的思路,我们需要为各服务的配置指定命名空间。这样,在向redis中发布配置时,重名的配置通过不同的命名空间加以区分,就像这样:service1.config=val1, service2.config=val2.

解决配置丢失问题-多机备份

redis通过将同一份数据在集群中复制多份来保证数据在其中任意一台机器down机后数据不丢失。其不可靠之处(相对于zk)在于,数据放入redis后,首先只存在于其中一台机器。如果数据成功复制到其它机器之前,此台机器失效,那么数据就永远丢失了。

服务一致性

在redis集群中,每台机器地位相同。一项配置会存储在集群中的多个机器上,一台为原始主机,其它作为备份主机。读取时优先从原始主机读取,当原始主机失效时,从备份主机读取。因此,无论client连接的是哪台机器,都需要先从原始主机尝试取出配置,再尝试从备份主机读取。所以服务是一致的。

解决down机问题-存活监控

redis并不会检测client的存活状况。我们来看看不支持存活检测会引发什么问题。以远程调用为例,服务ServiceA对外提供远程调用服务。在ServiceA启动后,向redis-zk中写入如下配置:

此时,突然SeviceA断电了。服务ServiceB需要调用ServiceA,当它从redis中取出ServiceA的ip地址并调用时,才发现ServiceA已经down机了。

要解决这个问题,我们为每个配置设置过期时间,并由ServiceA定期延长其过期时间。当ServiceA断电时,redis中的配置将因为过期而被移除。这本质上是一个心跳检测的方法。由于redis过期时间精确到秒,因此这里的心跳时间间隔大于等于1秒。

配置变更

得益于redis本身的设计,更改操作是原子的。而所有的更改操作都在同一个存储主机上发生,因此其顺序性也必然满足。

变更通知

当redis中的配置发生变更时,配置读取方无法得知此事件。只能通过周期性访问的方法来得到最新的配置。

小结

从我们自行设计的分布式配置发布/订阅系统中,我们总结出如下几个要点:

ZooKeeper是如何设计的

为达成前述的各种目标,zk 的设计有如下5个要点:

图片来自:http://zookeeper.majunwei.com/document/3.4.6/OverView.html zab广播二阶段提交流程(*序号代表发生先后顺序*)

数据结构

所有的配置数据通过树状数据结构组织。每个znode通过路径进行访问。因此,znode的路径就是天然的命名空间,可用于隔离不同服务的配置。

znode有两种模式。一种是持久型,设置了就永久存在。一种是挥发型,在创建此节点时,zk会与相应的服务器之间建立session. 当sesson结束时,此节点将被删除。如果此节点或者此节点的父节点上被设置了watcher,当此结点被创建和删除时,watcher的持有者都将获得事件通知。

数据的读取与时效性

连接zk的服务器可以连接集群中的任意结点。配置数据的更改都是从leader节点广播出来。因此,从广播开始到每个节点收到最新的数据有一定的时差。为保证数据的时效性,如果成员节点超过指定时间还未更新配置,将被视为无效节点。迫使连接此无效成员节点的client重新从zk集群中选择可用节点。

配置的修改机制

每个client独立选择连接的zk集群成员节点,并从中读取数据。当需要修改配置数据时,则由各成员节点向leader发起更改请求。由leader通过原子广播完成数据的修改。当集群中超过一半的节点完成配置更新并持久化,leader才返回修改成功。因此,zk对配置修改成功的承诺具有极高的可靠性。

leader的选举机制

分布式选举算法有很多。具体读者可以查找相关资料。这里用一个简单的算法说明其原理:

评价Zookeeper

任何设计是为特定的需求设计的。总会做出一些权衡。

zk的优势

zk的劣势

zk的另类用法

zk本是用于分布式配置服务。但它还可以这样用:(参考链接):

命名服务

命名服务是把给定的字符串映射到对应实体的服务。比如,给定域名从而取得ip地址(DNS)。由于zk内部的数据结构为树,因此其天然支持将给定的路径字段串映射到一个具体的znode,只需将路径要映射到的实体配置到相应路径下的znode上即可。

成员管理

成员管理利用了zk对client的存活检测。首先使用一个znode代表一个集群,称其为group node。集群的所有的成员在此znode下建立对应的子znode, 称其为child node。要获得一个集群的所有存活成员,只需获取group node下的所有child node即可。当成员机器失效时,其child node会从group node下移除。

分布式屏障

此屏障的意义在于阻塞其它机器的执行过程。当屏障移除后,可继续执行。原理也很简单:

分布式队列

这个实现实际上不现实。有kafka这样专门的消息队列,就不用在zk中实现了。况且zk的写效率这么低,存储效率这么低。。。

分布式锁

用一个znode代表一个锁,称为lock node。加锁时,使用create()方法在lock node下创建子节点,并指定sequence flag以及 ephemeral flag。sequence flag的意思是创建的结点名称后面自动加上一个序号。 ephemeral flag的意思是节点为挥发型节点。当有多个client竞争上锁时,序号最小的client持有锁。

虽然zk可以完成分布式锁的功能,还是不建议这样使用zk。从其使用过程就知道有多纠结了。。。

二阶段提交

用一个znode代表一个分布式事务。所有要提交的参与者都分别在此节点下创建对应于自己的子节点。各节点将自己的commit结果或者放弃commit的决定放在自己对应的事务子节点中。

本质上,是将zk当作一个中间通信者来使用。还是那句话,不建议这样使用。

leader选举

使用一个znode代表一次选举,称其为election node。参选者在此election node下,通过创建子节点的方式参与竞选。在使用create()方法时,通过指定sequence flag,zk将会为生成的znode的名称后面自动加上一个序号。序号最小的当选为leader.

小结

以上的各种用法,实际上都在利用zk的如下特性:

如您发现本文中的错误或者不清楚之处,请您留言,我会尽快修正,以免误导他人。

上一篇 下一篇

猜你喜欢

热点阅读