老吴的学习笔记-Zookeeper

摘要

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

引言

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

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

  • 高可靠性。集群中所有节点持久化存储同一份配置。不存在单点故障。
  • 执行的顺序性。多个修改配置的请求会严格按照顺序被串行执行。从而保证配置结果的正确。
  • 操作的原子性。配置的更改操作是原子的。要么在集群的所有节点上都成功,要么都失败。不会出现只更改了一半的情况。操作之间互不干扰。
  • 时效性。可从zk集群中的任意节点读取配置,zk会保证各节点的配置与最新配置之间的时差不会过大。
  • 服务一致性。连接zk集群中的任何一台机器都会获得相同的配置服务,连接zk集群中的任何一台不会有功能上的差别。
  • 存活监控。与zk建立会话的机器如果down机,会被zk检测到,并移除与此会话相关联的配置。例如在远程调用中,被调用机器down机后,其ip地址配置将被移除,从而避免被其它机器继续访问。
  • 配置变更通知。如果client关心某个配置的实时变化,可以在zk相应的配置上设置watcher,配置变化时,zk将会发送配置变化的通知。client可以立刻拉取最新的配置。

用redis自己实现ZooKeeper

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

老吴的学习笔记-Zookeeper_第1张图片
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中写入如下配置:

  • ServiceA.ip=xxx.xxx.x.x
  • ServiceA.port=123

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

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

配置变更

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

变更通知

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

小结

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

  • 要区别各服务的配置,需要命名空间机制。
  • 可靠性通过多机备份并持久化来实现。在redis-zk的实现中,一方面redis集群备份机制可靠性不高,另一方面redis持久化只会定期执行,而非立刻执行。因此,在可靠性上,redis-zk并不完美。
  • 在redis-zk中,服务一致性是通过让所有节点采用完全一样的访问方式完成的:所有节点通过hash值计算数据存储的原始节点,从而从中获取数据。
  • 存活检测必须通过某种心跳机制来达成。
  • 配置的变更需要具有原子性。为保证可靠性,一份配置数据会重复存储在多台机器上,一次修改请求要么更改所有zk主机上的配置,要么放弃所有修改,不允许有中间状态(各机器的配置更改允许有一定延迟,但不允许始终不一致)。
  • 配置的改变需要按其实际发生的时间顺序完成。否则会出现最终状态不正确。
  • 配置变更时,需要通过发送事件来通知相关节点。以保证实效性,避免忙检查。

ZooKeeper是如何设计的

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

  • 树状名称空间。以的数据结构组织配置数据。树上每个节点(称作Znode)就是配置数据存放的地方。通过节点的路径来访问每个znode。
老吴的学习笔记-Zookeeper_第2张图片
图片来自:http://zookeeper.majunwei.com/document/3.4.6/OverView.html
  • 集群中的配置修改操作由leader统一进行管理,其它机器称作follower。当zk集群中的任何follower收到修改请求时,都必须请求 leader来完成修改。修改操作严格按顺序串行执行。leader是通过选举产生的,一个集群中只有一个。
老吴的学习笔记-Zookeeper_第3张图片
图片来自:https://cwiki.apache.org/confluence/display/ZOOKEEPER/ProjectDescription
  • 原子广播技术(ZAB)。此过程相当于通过二阶段提交的方法把配置提交集群中所有的follower。具体流程如下:首先由leader节点向集群中的follower节点广播发送修改请求(proposal). follower收到请求后将最新配置持久化,并回复ack. 当leader收到半数follower节点的ack后,认为广播操作成功,并开始向集群中回复过ack的follower节点逐个确认提交(commit)。如果此过程因leader失效而被中断,那么整个集群将进入崩溃恢复阶段。将重新选举新的leader以完成上述过程。对于一个修改请求(proposal), 如果没有在任何follower中被commit,则认为操作未成功,proposal被丢弃。如果存在commit,则必然有一半的节点已经持久化了这个修改(proposal),新的leader将继续完成之前的流程。一次原子广播的流程如下图所示:
老吴的学习笔记-Zookeeper_第4张图片
zab广播二阶段提交流程(*序号代表发生先后顺序*)
  • watcher机制。数据观察者在其关心的配置上附加一个watcher,即可在配置变化时收到一个变更通知。
  • Session机制。client在发布新配置时,可以选择与zk之间建立session. 当session失效时(往往意味着机器down了),其关联的znode将被移除。其中的配置数据也跟着移除。session将通过心跳机制检测服务器的存活。

数据结构

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

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

数据的读取与时效性

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

配置的修改机制

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

leader的选举机制

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

  • 所有节点根据自身条件决定是否要参加选举。
  • 每个节点生成一个随机数,并将此随机数广播到所有节点。
  • 每个节点将收到的所有随机数进行比较。生成最大随机数节点即为leader.
  • 当某节点发现自己是leader时,向所有人广播它为leader.
  • 所有其它节点向leader注册自己,并记录leader.
  • 完成选举。如果有平局的情况,则重复上述步骤。

评价Zookeeper

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

zk的优势

  • 高可靠性。集群中的所有节点复制并持久化同一份数据。当集群中超过一半机器失效时,整个集群失效(ZAB广播协议的要求)。假设每台机器一周内失效的概率为0.01,一个5台机器的集群需要在3台机器同时失效的情况下才会失效。那么整个集群在一周内失效的可能性约为:
    ![](http://latex.codecogs.com/png.latex?C_{5}{3}(0.01)3(0.99)^2 \approx 0.00001)
    在有人工干预的情况下,在一天以内三台机器失几乎不可能同时失效。

  • 由于数据复制到了集群中每个节点,因此配置的读操作是完全并发的。配置存储于内存中,因此读取性能极佳。

  • watcher机制与session机制,使得配置数据的变更/机器的失效能很快被发现,并通知到相关方。

  • 数据更改的致性好。所有的修改操作由leader统一集中式处理。

zk的劣势

  • 正由于所有配置复制到所有节点,且存储在内存中,因此其存储效率是极低的。不宜在配置中存储大量的数据。
  • 每次的修改操作由zk成员通过网络交由leader处理。leader需要向全网发送广播,并得到半数以上节点的确认。这使得它的数据变更效率非常低。不宜进行高频操作。随着集群规规模的增大,其广播的代价也越来越高。然而即便如此,其写性能还相当可观的:


    老吴的学习笔记-Zookeeper_第5张图片
    图片来自:http://zookeeper.majunwei.com/document/3.4.6/Overview.html

zk的另类用法

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

  • 命名服务
  • 成员管理
  • 分布式屏障
  • 分布式队列
  • 分布式锁
  • 两阶段提交
  • leader选举

命名服务

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

成员管理

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

分布式屏障

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

  • 在zk中设置一个指定路径的znode,用于代表一个屏障。
  • 需要被阻塞的进程,判断如果此znode存在,则代表屏障存在,阻塞执行,并znode上注册一个watcher,以便在znode消失时得到通知。
  • 得到znode消失的通知,核实确实如此,则可执行之后的流程。

分布式队列

这个实现实际上不现实。有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的如下特性:

  • 树状名称空间
  • 可靠的存储能力
  • 原子性
  • 顺序性
  • 存活检测
  • 变更通知

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

你可能感兴趣的:(老吴的学习笔记-Zookeeper)