ZooKeeper的作用、应用场景和替代品

 

ZooKeeper 我想大家应该都略有耳闻,可能你在开发中没有直接使用过,但常用的 Hadoop、HBase、Kafka、Dubbo 等都有使用到 ZooKeeper。那 ZooKeeper 到底起到了什么样的作用,为什么这些框架、系统需要使用 ZooKeeper呢,我们在开发过程中应该如何使用 ZooKeeper,又是否有 ZooKeeper的替代品呢。本文将围绕以上问题,从以下三方面说起:

  1. 来源与作用;
  2. 经典应用场景;
  3. 替代品。

1. 来源与作用

ZooKeeper 的设计初衷是什么?这要从雅虎的一个研究小组说起。当时,研究人员发现雅虎内部的很多分布式系统都需要依赖一个组件进行分布式协调,但是这些组件往往都存在分布式单点问题。所以雅虎便组织开发了一个通用的无单点问题的分布式协调框架,那就是 ZooKeeper,一方面解决单点问题,另一方面,将分布式协调从分布式系统中抽离出来,让开发者更专注于业务逻辑。

下面分别对 “单点问题” 和 “分布式协调” 进行讲述。

1.1 单点问题

单点问题(又叫单点故障,Single Point of Failure,SPOF)是指在系统中一旦失效就会让整个系统无法运作的部件。举个例子,将系统只部署在机器 A 一台机器上,如果机器 A 失效,则整个系统将无法运作。而为了解决该问题,一般采用冗余的方式,增加多台机器,只要多台机器不同时失效,则系统将可正常运作。

ZooKeeper 也是通过集群的方式解决单点问题。看似通过集群的方式,多部署几台机器就能解决单点问题。首先我们需要把单点问题细化成无状态的单点问题有状态的单点问题。这里说的有无状态可以参考网络协议中的概念,HTTP 无状态协议,每次请求都是独立的,TCP 有状态协议,依靠记录状态完成可靠传输。

对于简单的无状态的单点问题,通过集群的方式便能解决,例如,代理节点做消息转发,这就是无状态的情况,如下图所示:

无状态的单点问题

如果消息转发服务器 X 宕机了,还有服务器 Y 能继续正常工作,而且机器数量越多可用性越高。

对于有状态的情况,则不一样了,将会面临分布式一致性问题。该问题仅靠简单的增加机器是无法解决的。常用的解决方法有:

  1. 去状态:将问题去除为无状态,例如将状态存储到可靠的DB中;
  2. 主从:由 Master 做主要的数据处理,Slaver 同步 Master 的状态,例如 MySQL 的主从复制,Master 处理写操作,Slaver 通过 binlog 同步状态。

有状态的单点问题

上述简单介绍了 MySQL 的解决方法,而 ZooKeeper 是使用的 ZAB 协议实现一致性。关于分布式一致性问题,本文将不继续叙述了,大家就先记住 ZooKeeper 解决了单点问题,能保证一致性,不用担心 ZooKeeper 集群会因为有机器宕机导致数据不一致性问题。

聊完了单点问题,下面聊下分布式协调,这也是 ZooKeeper 所需要起到的作用。

1.2 分布式协调

分布式协调,其含义正如它的字面意思,在分布式环境下进行协调。举个例子,在单机情况下,若多个线程同时想对某个资源进行修改,例如库存减一。为了保证正确性,我们会在这个资源上加锁。但是在分布式情况下,若多台机器想对某个资源进行修改,我们如何为这个资源加锁呢?这时候就需要一个协调者的出现。机器若想知道其他机器有没有加锁,不需要去和其他机器通信,只需和协调者通信就可以知道资源的加锁情况了,也可以认为锁由协调者进行管理。

分布式协调

你可能会说为什么要让 ZooKeeper 担任协调者, 为什么不直接在集群里选一台机器。是的,当然可以,但是这样的话你就需要解决协调者的单点问题(担任协调者的机器宕机)。又因为分布式系统中大多都需要这么一个协调者组件,而为了复用,将其抽离出来,于是就有了分布式协调系统。因此 Dubbo、Kafak 等框架在实现分布式时,直接把 ZooKeeper 拿来用,这样就不用再重复实现协调者组件了。而程序员的我们在分布式开发中也只需要关注业务逻辑实现即可。

总结下,ZooKeeper 的作用就是提供无单点问题的分布式协调服务。

2. 经典应用场景

本节将介绍一些 ZooKeeper 的经典应用场景,看看到底怎么使用 ZooKeeper,但首先还需要对 ZooKeeper 的数据模型、节点特性、Watcher 机制有一定认识,下面会简单介绍一下。

2.1 ZooKeeper 基础

什么是数据模型,可以理解为数据的存放形式,例如 Redis 是以键值对的形式存储数据,而 ZooKeeper 是树的形式,如下图所示:

ZooKeeper数据模型

其中根节点的名字是 “/”,根节点有两个子节点 “A” 和 “B”,“A” 又有三个子节点 “X”、“Y”、“Z”。我们可以根据需要组织树的节点,还可以在节点上存储数据,是不是有点像文件系统的感觉。

对数据模型有了基本认识,还需要知道下节点的特性,有 4 种节点,都很好理解:

  1. 持久节点:创建后将一直存在,除非主动删除;
  2. 持久顺序节点:其持久性与持久节点一样,但是它具有顺序性,节点名上加上一个数字后缀表示其创建的顺序,例如 1,2,3,4…;
  3. 临时节点:如果创建该节点的会话失效,那该节点也将被删除;
  4. 临时顺序结点:在临时节点上增加了顺序的概念。

最后再知道下 Watcher 机制,就是一种监听机制,我们可以监听某个节点数据内容的变化、子节点变化等,一旦发生了我们监听的事件,ZooKeeper 将会通知我们。

介绍完 ZooKeeper 的一些基础,下面进入本节的正题。

2.2 数据发布/订阅

通过 Watcher机制 实现数据发布/订阅。以配置中心场景为例。将配置发布到 ZooKeeper 的节点上,其他机器可通过监听 ZooKeeper 上节点的变化来实现配置的动态更新。例如我们把数据库配置放在 “/configserver/app1/database_config” 节点上,然后让每台机器在启动时从该节点上获取数据库配置信息,并且监听节点的内容变化,发生配置变更时,重新获取配置。

配置管理

2.3 命名服务

通过 ZooKeeper 的顺序节点生成全局唯一 ID。例如一个分布式任务调度系统,为任务生产全局唯一 ID,如下图所示:

命名服务

在创建完顺序节点后,通过节点名拼接可得到唯一 ID 的任务名,例如 “type1-job-000000003”。

2.4 集群管理

通过 ZooKeeper 的临时节点Watcher 机制,来监控集群的运行状态,如下图所示:

集群管理

当有新的机器加入集群时,就在 “machines” 下创建一个临时节点。当有机器宕机时,临时节点将失效被删除。通过监控 “machines” 下的子节点变化,就能得知集群机器的状态。

2.5 分布式锁

分布式锁,主要有两类,排他锁和共享锁。

排它锁,在事务对资源的加锁期间,不允许其他事务进行读写操作。通过一个临时节点便能表示一个锁,如下图所示:

排它锁

在 “resourceA\exclusive_lock” 节点下建立一个 “lock” 节点表示对资源A上了排它锁。当多台机器尝试在 “exclusive_lock” 下创建 “lock” 节点时,只会有一台能成功创建,而其他失败的机器将监听 “exclusive_lock” 的子节点变化。当锁被主动释放时或者宕机时,“lock” 节点将被删除,其他机器将被通知,继续尝试创建 “lock” 节点获取锁。

共享锁,在事务对资源的加锁期间,允许其他事务进行读操作。仍然是通过一个节点来代表锁,不同的地方在于需要区分下读写操作,读写可以通过节点的数据内容来表示,例如 R 表示读操作,W 表示写操作。如下图所示:

共享锁

读写操作都是在 “shared_lock” 节点下创建一个名为 “lock” 的临时顺序节点,只是数据内容不同。对于读操作,如果比自己序号小的子节点都是读请求,则认为自己成功获得了锁,可以进行读操作,如果序号小的节点中包含写操作,需要进行等待,监听 “shared_lock” 的子节点变化。对于写操作,如果自己不是序号最小的子节点,则进入等待,监听 “shared_lock” 的子节点变化。

3. 替代品

第二节中,我们了解了 ZooKeeper 在分布式环境下有很多的应用场景,那是不是必须使用 ZooKeeper 才能实现分布式锁、集群管理等功能呢?当然不是的,还有其他技术可供选择。例如 Redis 也是可以用于分布式协调服务,关于第二节所说的那些场景,都是能实现的,只是会因为它们的数据模型不同,需要采用不同的设计。

那我们是使用 ZooKeeper 还是 Redis 呢?这个需要根据场景来决定,我们在分布式锁的场景下来对比下这两者的优缺点:

  • Redis:支持高并发的获取、释放锁操作,不能保证 100% 的数据一致性,可能会出现问题(极少);
  • ZooKeeper:锁的模型健壮,强一致性,但是频繁的申请锁、释放锁操作对 ZooKeeper 集群的压力较大。

Redis 的数据一致性不如 ZooKeeper,而 ZooKeeper 对高并发的支持不如 Redis。

最后还想介绍下,etcd,这是一个高可用的键值存储系统,使用 Go 编写,通过 Raft 算法处理日志复制保证强一致性,被 Kubernetes 等系统所使用。与 ZooKeeper 相比的话,有较多技术细节的不同,我举几个比较宽泛的,ZooKeeper 是基于 Java 实现的,自然也具有了 Java 的缺点,例如 GC 暂停,而 etcd 使用了堆外存储,堆外是不会被 GC 管理,所以也就没有了 GC 暂停的问题。ZooKeeper 有很好的 Java 客户端库,还提供其他语言的客户端库,etcd 作为后起之秀,还没有很好的 Java 客户端库,只能通过 HTTP 方式调用。

以上列出 Redis 和 etcd 只是为了帮助读者开阔视野,不局限于 ZooKeeper。而如何进行具体的技术选型,笔者还缺乏经验,就不多说了。

4. 参考

  1. 《从Paxos到ZooKeeper》
  2. 为什么需要ZooKeeper