21 ZooKeeper 分布式锁:实现和原理解析
在实战篇中,我们主要介绍在实际生成环境中应该如何使用和设计 ZooKeeper 服务,并给出一些常见的问题以及解决方案。
在基础篇第 2 课时介绍 Watch 监控机制时,我为你介绍了一个利用 ZooKeeper 中的 Watch 机制实现一个简单的分布式锁的例子。这个例子当时是为了说明 Watch 机制的主要特点和作用。但在实际生产开发的过程中,这种分布式锁作为商业系统分布式锁的解决方案,直接利用之前介绍的实现分布式锁的方式,显然过于简单,且其中也有不少缺陷。那么今天这节课就结合这段时间学习到的知识,开发一个商业级别的分布式锁。
什么是分布式锁
在开始着手开发商业级的分布式锁之前,我们首先要弄清楚什么是分布式锁,以及分布式锁在日常工作的使用场景。明确了这些,我们才能设计出一个安全稳定的分布式锁。
在日常开发中,我们最熟悉也常用的分布式锁场景是在开发多线程的时候。为了协调本地应用上多个线程对某一资源的访问,就要对该资源或数值变量进行加锁,以保证在多线程环境下系统能够正确地运行。在一台服务器上的程序内部,线程可以通过系统进行线程之间的通信,实现加锁等操作。而在分布式环境下,执行事务的线程存在于不同的网络服务器中,要想实现在分布式网络下的线程协同操作,就要用到分布式锁。
分布式死锁
在单机环境下,多线程之间会产生死锁问题。同样,在分布式系统环境下,也会产生分布式死锁的问题。
当死锁发生时,系统资源会一直被某一个线程占用,从而导致其他线程无法访问到该资源,最终使整个系统的业务处理或运行性能受到影响,严重的甚至可能导致服务器无法对外提供服务。
所以当我们在设计开发分布式系统的时候,要准备一些方案来面对可能会出现的死锁问题,当问题发生时,系统会根据我们预先设计的方案,避免死锁对整个系统的影响。常用的解决死锁问题的方法有超时方法和死锁检测。
- 超时方法 :在解决死锁问题时,超时方法可能是最简单的处理方式了。超时方式是在创建分布式线程的时候,对每个线程都设置一个超时时间。当该线程的超时时间到期后,无论该线程是否执行完毕,都要关闭该线程并释放该线程所占用的系统资源。之后其他线程就可以访问该线程释放的资源,这样就不会造成分布式死锁问题。但是这种设置超时时间的方法也有很多缺点,最主要的就是很难设置一个合适的超时时间。如果时间设置过短,可能造成线程未执行完相关的处理逻辑,就因为超时时间到期就被迫关闭,最终导致程序执行出错。
- 死锁检测 :死锁检测是处理死锁问题的另一种方法,它解决了超时方法的缺陷。与超时方法相比,死锁检测方法主动检测发现线程死锁,在控制死锁问题上更加灵活准确。你可以把死锁检测理解为一个运行在各个服务器系统上的线程或方法,该方法专门用来探索发现应用服务上的线程是否发生了死锁。如果发生死锁,就会触发相应的预设处理方案。
锁的实现
在介绍完分布式锁的基本性质和潜在问题后,接下来我们就通过 ZooKeeper 来实现两种比较常用的分布式锁。
排他锁
排他锁也叫作独占锁,从名字上就可以看出它的实现原理。当我们给某一个数据对象设置了排他锁后,只有具有该锁的事务线程可以访问该条数据对象,直到该条事务主动释放锁。否则,在这期间其他事务不能对该数据对象进行任何操作。在第二课时我们已经学习了利用 ZooKeeper 实现排他锁,这里不再赘述。
共享锁
另一种分布式锁的类型是共享锁。它在性能上要优于排他锁,这是因为在共享锁的实现中,只对数据对象的写操作加锁,而不为对象的读操作进行加锁。这样既保证了数据对象的完整性,也兼顾了多事务情况下的读取操作。可以说,共享锁是写入排他,而读取操作则没有限制。
接下来我就通过 ZooKeeper 来实现一个排他锁。
创建锁
首先,我们通过在 ZooKeeper 服务器上创建数据节点的方式来创建一个共享锁。其实无论是共享锁还是排他锁,在锁的实现方式上都是一样的。唯一的区别在于,共享锁为一个数据事务创建两个数据节点,来区分是写入操作还是读取操作。如下图所示,在 ZooKeeper 数据模型上的 Locks_shared 节点下创建临时顺序节点,临时顺序节点的名称中带有请求的操作类型分别是 R 读取操作、W 写入操作。

获取锁
当某一个事务在访问共享数据时,首先需要获取锁。ZooKeeper 中的所有客户端会在 Locks_shared 节点下创建一个临时顺序节点。根据对数据对象的操作类型创建不同的数据节点,如果是读操作,就创建名称中带有 R 标志的顺序节点,如果是写入操作就创建带有 W 标志的顺序节点。

释放锁
事务逻辑执行完毕后,需要对事物线程占有的共享锁进行释放。我们可以利用 ZooKeeper 中数据节点的性质来实现主动释放锁和被动释放锁两种方式。
主动释放锁是当客户端的逻辑执行完毕,主动调用 delete 函数删除ZooKeeper 服务上的数据节点。而被动释放锁则利用临时节点的性质,在客户端因异常而退出时,ZooKeeper 服务端会直接删除该临时节点,即释放该共享锁。
这种实现方式正好和上面介绍的死锁的两种处理方式相对应。到目前为止,我们就利用 ZooKeeper 实现了一个比较完整的共享锁。如下图所示,在这个实现逻辑中,首先通过创建数据临时数据节点的方式实现获取锁的操作。创建数据节点分为两种,分别是读操作的数据节点和写操作的数据节点。当锁节点删除时,注册了该 Watch 监控的其他客户端也会收到通知,重新发起创建临时节点尝试获取锁。当事务逻辑执行完成,客户端会主动删除该临时节点释放锁。

总结
通过本课时的学习,我们掌握了什么是分布式锁,以及分布式锁在实际生产环境中面临的问题和挑战。无论是单机上的加锁还是分布式环境下的分布式锁,都会出现死锁问题。面对死锁问题,如果我们不能很好地处理,会严重影响系统的运行。在本课时中,我为你讲解了两种处理死锁问题的方法,分别是超时设置和死锁监控。然后重点介绍了利用 ZooKeeper 实现一个共享锁。
在具体实现的过程中,我们利用 ZooKeeper 数据模型的临时顺序节点和 Watch 监控机制,在客户端通过创建数据节点的方式来获取锁,通过删除数据节点来释放锁。
这里我给你留一个问题,作为我们课后的作业:在分布式共享锁的实现中,获得锁的线程执行完释放锁后,其他等待资源的线程客户端收到 Watch 通知机制,会尝试获取锁。但是如果等待线程过多,那么频繁的 Watch 通知也会占用系统的网络资源和内存,有没有什么好的办法可以优化呢?在保证共享锁的实现下,减少 Watch 通知次数,这里留给你在本地进行优化。
22 基于 ZooKeeper 命名服务的应用:分布式 ID 生成器
在上个课时中,我们讲解了如何利用 ZooKeeper 实现一个分布式锁,并解决在分布式环境下,网络中多线程之间的事务问题。今天我们利用 ZooKeeper 来解决分布式系统环境的另一个实际应用场景:使用 ZooKeeper 实现一个分布式 ID 生成器。
无论是单机环境还是分布式环境,都有使用唯一标识符标记某一资源的使用场景。比如在淘宝、京东等购物网站下单时,系统会自动生成订单编号,这个订单编号就是一个分布式 ID 的使用。
什么是 ID 生成器
我们先来介绍一下什么是 ID 生成器。分布式 ID 生成器就是通过分布式的方式,实现自动生成分配 ID 编码的程序或服务 。在日常开发中,Java 语言中的 UUID 就是生成一个 32 位的 ID 编码生成器。根据日常使用场景,我们生成的 ID 编码一般具有唯一性、递增性、安全性、扩展性这几个特性。

唯一性:ID 编码作为标记分布式系统重要资源的标识符,在整个分布式系统环境下,生成的 ID 编码应该具有全局唯一的特性。如果产生两个重复的 ID 编码,就无法通过 ID 编码准确找到对应的资源,这也是一个 ID 编码最基本的要求。
递增性:递增性也可以说是 ID 编码的有序特性,它指一般的 ID 编码具有一定的顺序规则。比如 MySQL 数据表主键 ID,一般是一个递增的整数数字,按逐条加一的方式顺序增大。我们现在学习的 ZooKeeper 系统的 zxID 也具有递增的特性,这样在投票阶段就可以根据 zxID 的有序特性,对投票信息进行比对。
安全性:有的业务场景对 ID 的安全性有很高的要求,但这里说的安全性是指,如果按照递增的方式生成 ID 编码,那么这种规律很容易被发现。比如淘宝的订单编码,如果被恶意的生成或使用,会严重影响系统的安全性,所以 ID 编码必须保证其安全性。
扩展性:该特性是指 ID 编码规则要有一定的扩展性,按照规则生成的编码资源应该满足业务的要求。还是拿淘宝订单编码为例,假设淘宝订单的 ID 生成规则是:随机产生 4 位有效的整数组成编码,那么最多可以生成 6561 个订单编码,这显然是无法满足淘宝系统需求的。所以在设计 ID 编码的时候,要充分考虑扩展的需要,比如编码规则能够生成足够多的 ID,从而满足业务的要求,或者能够通过不同的前缀区分不同的产品或业务线 。
生成策略
UUID 方式
介绍完 ID 编码在整个应用系统的重要作用和 ID 编码自身的特性后。接下来我们看看几种在日常开发中常见的生成策略。
开发人员,尤其是 Java 程序员最为熟悉的编码生成方式就是 UUID。它是一种包含 16 个字节的数字编码。 UUID 会根据运行应用的计算机网卡 MAC 地址、时间戳、命令空间等元素,通过一定的随机算法产生。正因为 UUID 算法元素的复杂性,保证了 UUID 在一定范围内的随机性。
UUID 在本地应用中生成,速度比较快,不依赖于其他服务,网络的好坏对其没有任何影响。但从实现上来讲,使用 UUID 策略生成的代码耦合度大,不能作为单独的 ID 生成器使用。而且生成的编码不能满足递增的特性,没有任何有序性可言,在很多业务场景中都不合适。
数据库序列方式
生成 ID 编码的另一种方式是数据库序列。比如 MySQL 的自增主键就是一种有序的 ID 生成方式。随着数据变得越来越多,为了提升数据库的性能,就要对数据库进行分库分表等操作。在这种情况下,自增主键的方式不能满足系统处理海量数据的要求。
这里我给你介绍另一种性能更好的数据库序列生成方式:TDDL 中的序列化实现。TDDL 是 Taobao Distributed Data Layer 的缩写。是淘宝根据自己的业务特点开发的数据库中间件。主要应用于数据库分库分表的应用场景中。
TDDL 生成 ID 编码的大致过程如下图所示。首先,作为 ID 生成器的机器,数据库中会存在一张sequence 序列化表,用于记录当前已经被占用的 ID 最大值。之后每个需要 ID 编码的客户端在请求 ID 编码生成器后,编码服务器会返回给该客户端一段 ID 地址区间。并更新 sequence 表中的信息。

在接收一段 ID 编码后,客户端会将该编码存储在内存中。在本机需要使用 ID 编码时,会首先使用内存中的 ID 编码。如果内存中的 ID 编码已经完全被占用,则再重新向编码服务器获取。
在 TDDL 框架的内部实现中,通过分批获取 ID 编码的方式,减少了客户端访问服务器的频率,避免了网络波动所造成的影响,并减轻了服务器的内存压力。不过 TDDL 是高度依赖底层数据库的实现方式,不能作为一个独立的分布式 ID 生成器对外提供服务。
实现方式
上面介绍的几种策略,有的和底层编码耦合比较大,有的又局限在某一具体的使用场景下,并不满足作为分布式环境下一个公共 ID 生成器的要求。接下来我们就利用目前学到的 ZooKeeper 知识,动手实现一个真正的分布式 ID 生成器。
首先,我们通过 ZooKeeper 自身的客户端和服务器运行模式,来实现一个分布式网络环境下的 ID 请求和分发过程。每个需要 ID 编码的业务服务器可以看作是 ZooKeeper 的客户端。ID 编码生成器可以作为 ZooKeeper 的服务端。客户端通过发送请求到 ZooKeeper 服务器,来获取编码信息,服务端接收到请求后,发送 ID 编码给客户端。

在代码层面的实现中,如上图所示。我们可以利用 ZooKeeper 数据模型中的顺序节点作为 ID 编码。客户端通过调用 create 函数创建顺序节点。服务器成功创建节点后,会响应客户端请求,把创建好的节点信息发送给客户端。客户端用数据节点名称作为 ID 编码,进行之后的本地业务操作。
通过上面的介绍,我们发现,使用 ZooKeeper 实现一个分布式环境下的公用 ID 编码生成器很容易。利用 ZooKeeper 中的顺序节点特性,很容易使我们创建的 ID 编码具有有序的特性。并且我们也可以通过客户端传递节点的名称,根据不同的业务编码区分不同的业务系统,从而使编码的扩展能力更强。
虽然使用 ZooKeeper 的实现方式有这么多优点,但也会有一些潜在的问题。其中最主要的是,在定义编码的规则上还是强烈依赖于程序员自身的能力和对业务的深入理解。很容易出现因为考虑不周,造成设置的规则在运行一段时间后,无法满足业务要求或者安全性不够等问题。为了解决这个问题,我们继续学习一个比较常用的编码算法------snowflake 算法。
snowflake 算法
snowflake 算法是 Twitter 公司开源的一种用来生成分布式 ID 编码的算法。如下图所示,通过 snowflake 算法生成的编码是一个 64 位的长整型值。在 snowflake 算法中,是通过毫秒数、机器 ID
毫秒流水号、符号位这几个元素生成最终的编码。

在计算编码的过程中,首先获取机器的毫秒数,并存储为 41 位,之后查询机器的工作 ID,存储在后面的 10 位字节中。剩余的 12 字节就用来存储毫秒内的流水号和表示位符号值 0。
从图中可以看出,snowflake 算法最主要的实现手段就是对二进制数位的操作。从性能上说,这个算法理论上每秒可以生成 400 多万个 ID 编码,完全满足分布式环境下,对系统高并发的要求。因此,在平时的开发过程中,也尽量使用诸如 snowflake 这种业界普遍采用的分布式 ID 生成算法,避免自己闭门造车导致的性能或安全风险。
总结
通过本课时的学习,我们掌握了什么是分布式的 ID 生成器,以及如何利用 ZooKeeper 实现一个 ID 生成器。ID 编码作为一个标识符,具有全局唯一的特性。利用这种特性,我们可以在分布式系统下实现很多功能,比如数据库的分区符号以及商城订单编号等。注意,在分布式等复杂的应用环境中,要设计一个编码生成器,我们需要考虑生成编码的唯一性、安全性、递增性以及扩展性。
在本节课中,我们通过 ZooKeeper 实现了一个编码生成器,其主要原理是利用数据模型中顺序节点的特性。在具体的实现中也比较简单,并没有使用特定的算法实现 ID 编码。
这里给你留一个作业:结合上面介绍的 snowflake 算法,实现一个更加高效的编码服务器。
23 使用 ZooKeeper 实现负载均衡服务器功能
今天我们利用 ZooKeeper 的相关知识,学习如何解决分布式环境下常见的业务场景与需求。这个课时主要通过 ZooKeeper 的相关特性,实现一个负载均衡服务器。在分布式架构和集群服务器架构下,负载均衡可以提高网络的性能和可靠性。
什么是负载均衡
负载均衡可以理解为运行在网络中的服务器或软件,其主要作用是扩展网络服务器的带宽、提高服务器处理数据的吞吐量,提高网络的可用性。比如我们经常用到的网络服务器、邮件服务器以及很多商业系统的服务器,都采用负载均衡的方式来协调工作。
这些系统一般会采用集群的方式进行部署,由于这些服务器彼此所处的网络环境各不相同,在某一段时间内所接收并处理的数据有多有少,如果整个集群没有一个专门进行管理和协调的角色,随着网络请求越来越多,就会出现某一台服务器比较忙,而网络中其他服务器没有什么任务要处理的情况。
负载均衡通过监控网络中各个服务器的运行情况,对整个集群的计算资源进行合理地分配和调整,避免由于请求处理的无序性导致的短板,从而限制整个集群性能。
了解了负载均衡服务器在集群服务中的作用后,接下来再来介绍一下实现负载均衡的常用算法。
负载均衡算法
在我们平时的工作和面试中,也常被问及一些负载均衡的算法问题。常用的有轮询法、随机法、原地址哈希法、加权轮询法、加权随机法、最小连接数法,下面我来分别为你进行讲解。
轮询法
轮询法是最为简单的负载均衡算法,当接收到来自网络中的客户端请求后,负载均衡服务器会按顺序逐个分配给后端服务。比如集群中有 3 台服务器,分别是 server1、server2、server3,轮询法会按照 sever1、server2、server3 这个顺序依次分发会话请求给每个服务器。当第一次轮询结束后,会重新开始下一轮的循环。
随机法
随机算法是指负载均衡服务器在接收到来自客户端的请求后,会根据一定的随机算法选中后台集群中的一台服务器来处理这次会话请求。不过,当集群中备选机器变的越来越多时,通过统计学我们可以知道每台机器被抽中的概率基本相等,因此随机算法的实际效果越来越趋近轮询算法。
原地址哈希法
原地址哈希算法的核心思想是根据客户端的 IP 地址进行哈希计算,用计算结果进行取模后,根据最终结果选择服务器地址列表中的一台机器,处理该条会话请求。采用这种算法后,当同一 IP 的客户端再次访问服务端后,负载均衡服务器最终选举的还是上次处理该台机器会话请求的服务器,也就是每次都会分配同一台服务器给客户端。
加权轮询法
在实际的生成环境中,一个分布式或集群系统中的机器可能部署在不同的网络环境中,每台机器的配置性能也有优劣之分。因此,它们处理和响应客户端请求的能力也各不相同。采用上面几种负载均衡算法,都不太合适,这会造成能力强的服务器在处理完业务后过早进入限制状态,而性能差或网络环境不好的服务器,一直忙于处理请求,造成任务积压。
为了解决这个问题,我们可以采用加权轮询法,加权轮询的方式与轮询算法的方式很相似,唯一的不同在于选择机器的时候,不只是单纯按照顺序的方式选择,还根据机器的配置和性能高低有所侧重,配置性能好的机器往往首先分配。
加权随机法
加权随机法和我们上面提到的随机算法一样,在采用随机算法选举服务器的时候,会考虑系统性能作为权值条件。
最小连接数法
最小连接数算法是指,根据后台处理客户端的连接会话条数,计算应该把新会话分配给哪一台服务器。一般认为,连接数越少的机器,在网络带宽和计算性能上都有很大优势,会作为最优先分配的对象。
利用 ZooKeeper 实现
介绍完负载均衡的常用算法后,接下来我们利用 ZooKeeper 来实现一个分布式系统下的负载均衡服务器。从上面介绍的几种负载均衡算法中不难看出。一个负载均衡服务器的底层实现,关键在于找到网络集群中最适合处理该条会话请求的机器,并将该条会话请求分配给该台机器。因此探测和发现后台服务器的运行状态变得最为关键。
状态收集
首先我们来实现网络中服务器运行状态的收集功能,利用 ZooKeeper 中的临时节点作为标记网络中服务器的状态点位。在网络中服务器上线运行的时候,通过在 ZooKeeper 服务器中创建临时节点,向 ZooKeeper 的服务列表进行注册,表示本台服务器已经上线可以正常工作。通过删除临时节点或者在与 ZooKeeper 服务器断开连接后,删除该临时节点。
最后,通过统计临时节点的数量,来了解网络中服务器的运行情况。如下图所示,建立的 ZooKeeper 数据模型中 Severs 节点可以作为存储服务器列表的父节点。用于之后通过负载均衡算法在该列表中选择服务器。在它下面创建 servers_host1、servers_host2、servers_host3等临时节点来存储集群中的服务器运行状态信息。

在代码层面的实现中,我们首先定义一个 BlanceSever 接口类。该类规定在 ZooKeeper 服务器启动后,向服务器地址列表中,注册或注销信息以及根据接收到的会话请求,动态更新负载均衡情况等功能。如下面的代码所示:
java
public class BlanceSever {
public void register()
public void unregister()
public void addBlanceCount()
public void takeBlanceCount()
}
之后我们创建 BlanceSever 接口的实现类 BlanceSeverImpl,在 BlanceSeverImpl 类中首先定义服务器运行的 Session 超时时间、会话连接超时时间、ZooKeeper 客户端地址、服务器地址列表节点 '/Severs' 等基本参数。并通过构造函数,在类被引用时进行初始化 ZooKeeper 客户端对象实例。
java
public class BlanceSeverImpl implements BlanceSever {
private static final Integer SESSION_TIME_OUT
private static final Integer CONNECTION_TIME_OUT
private final ZkClient zkclient
private static final SERVER_PATH="/Severs"
public BlanceSeverImpl(){
init...
}
}
接下来,在定义当服务器启动时,向服务器地址列表注册信息的 register 函数。在函数的内部,通过在 SERVER_PATH 路径下创建临时子节点的方式来注册服务器信息。如下面的代码所示,首先获取服务器的 ip 地址,利用 ip 地址作为临时节点的 path 来创建临时节点。
java
public register() throws Exception {
InetAddress address = InetAddress.getLocalHost();
String serverIp=address.getHostAddress()
zkclient.createEphemeral(SERVER_PATH+serverIp)
}
register 函数在服务器启动并注册服务器信息后,我们再来定义 unregister 方法,该方法是当服务器关机或由于其他原因不再对外提供服务时,通过调用 unregister 方法,注销该台服务器在服务器列表中的信息。
注销后的机器不会被负载均衡服务器分发处理会话。如下面的代码所示,在 unregister 函数的内部,我们主要通过删除 SERVER_PATH 路径下临时节点的方式注销服务器。
java
public unregister() throws Exception {
zkclient.delete(SERVER_PATH+serverIp)
}
负载算法
实现服务器列表后,接下来我们就进入负载均衡最核心的内容:如何选择服务器。这里我们通过采用"最小连接数"算法,来确定究竟如何均衡地分配网络会话请求给后台客户端。
整个实现的过程如下图所示。首先,在接收到客户端的请求后,通过 getData 方法获取服务端 Severs 节点下的服务器列表,其中每个节点信息都存储有当前服务器的连接数。通过判断选择最少的连接数作为当前会话的处理服务器,并通过 setData 方法将该节点连接数加 1。最后,当客户端执行完毕,再调用 setData 方法将该节点信息减 1。

首先,我们定义当服务器接收到会话请求后。在 ZooKeeper 服务端增加连接数的 addBlance 方法。如下面的代码所示,首先我们通过 readData 方法获取服务器最新的连接数,之后将该连接数加 1,再通过 writeData 方法将新的连接数信息写入到服务端对应节点信息中。
java
public void addBlance() throws Exception {
InetAddress address = InetAddress.getLocalHost();
String serverIp=address.getHostAddress()
Integer con_count=zkClient.readData(SERVER_PATH+serverIp)
++con_count
zkClient.writeData(SERVER_PATH+serverIp,con_count)
}
当服务器处理完该会话请求后,需要更新服务端相关节点的连接数。具体的操作与 addBlance 方法基本一样,只是对获取的连接信息进行减一操作,这里不再赘述。
结束
本课时我们介绍了如何利用 ZooKeeper 实现一个负载均衡服务器,了解了随机、轮询、哈希等常用的负载均衡算法,并在本课的结尾利用 ZooKeeper 来创建一个负载均衡服务器的具体实现过程。
这里请你注意:我们日常用到的负载均衡器主要是选择后台处理的服务器,并给其分发请求。而通过 ZooKeeper 实现的服务器,只提供了服务器的筛选工作。在请求分发的过程中,还是通过负载算法计算出要访问的服务器,之后客户端自己连接该服务器,完成请求操作。
24 ZooKeeper 在 Kafka 和 Dubbo 中的工业级实现案例分析
在前面的课程中,我们学习了如何使用 ZooKeeper 实现分布式 ID 生成器,以及负载均衡的分布式环境下常用的解决方案。为了更进一步地提高用 ZooKeeper 解决问题的能力,我们再来分析一下在主流开源框架中如何使用 ZooKeeper。本节课主要选择业界最为流行的两个框架,一个是 RPC 框架 Dubbo,另一个是分布式发布订阅消息系统 Kafka。下面我们先来分析这两个框架都分别利用 ZooKeeper 解决了哪些问题。
Dubbo 与 ZooKeeper
Dubbo 实现过程
Dubbo 是阿里巴巴开发的一套开源的技术框架,是一款高性能、轻量级的开源 Java RPC 框架。它提供了三大核心能力:
- 面向接口的远程方法调用
- 智能容错和负载均衡
- 服务自动注册和发现
其中,远程方法调用是 Dubbo 最为核心的功能点。因为一个分布式系统是由分布在不同网络区间或节点上的计算机或服务,通过彼此之间的信息传递进行协调工作的系统。因此跨机器或网络区间的通信是实现分布式系统的核心。而 Dubbo 框架可以让我们像调用本地方法一样,调用不同机器或网络服务上的线程方法。
下图展示了整个 Dubbo 服务的连通过程。整个服务的调用过程主要分为服务的消费端 和服务的提供方。首先,服务的提供方向 Registry 注册中心注册所能提供的服务信息,接着服务的消费端会向 Registry 注册中心订阅该服务,注册中心再将服务提供者地址列表返回给消费者。如果有变更,注册中心将基于长连接将变更数据推送给消费者,从而通过服务的注册机制实现远程过程调用。

ZooKeeper 注册中心
通过上面的介绍,我们不难发现在整个 Dubbo 框架的实现过程中,注册中心是其中最为关键的一点,它保证了整个 PRC 过程中服务对外的透明性。而 Dubbo 的注册中心也是通过 ZooKeeper 来实现的。
如下图所示,在整个 Dubbo 服务的启动过程中,服务提供者会在启动时向 /dubbo/com.foo.BarService/providers 目录写入自己的 URL 地址,这个操作可以看作是一个 ZooKeeper 客户端在 ZooKeeper 服务器的数据模型上创建一个数据节点。服务消费者在启动时订阅 /dubbo/com.foo.BarService/providers 目录下的提供者 URL 地址,并向 /dubbo/com.foo.BarService/consumers 目录写入自己的 URL 地址。该操作是通过 ZooKeeper 服务器在 /consumers 节点路径下创建一个子数据节点,然后再在请求会话中发起对 /providers 节点的 watch 监控。

Kafka 与 ZooKeeper
接下来我们再看一下 ZooKeeper 在另一个开源框架 Kafka 中的应用。Kafka 是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者在网站中的所有动作流数据,经常用来解决大量数据日志的实时收集以及 Web 网站上用户 PV 数统计和访问记录等。我们可以把 Kafka 看作是一个数据的高速公路,利用这条公路,数据可以低延迟、高效地从一个地点到达另一个地点。

Kafka 实现过程
在介绍 ZooKeeper 在 Kafka 中如何使用之前,我们先来简单地了解一下 Kafka 的一些关键概念,以便之后的学习。如下图所示,整个 Kafka 的系统架构主要由 Broker、Topic、Partition、Producer、Consumer、Consumer Group 这几个核心概念组成,下面我们来分别进行介绍。

Broker
Kafka 也是一个分布式的系统架构,因此在整个系统中存在多台机器,它将每台机器定义为一个 Broker。
Topic
Kafka 的主要功能是发送和接收消息,作为一个高效的消息管道,它存在于不同的系统中。Kafka 内部,将接收到的无论何种类型的消息统一定义为 Topic 类,可以将 Topic 看作是消息的容器。
Partition
Partition 是分区的意思,与 Topic 概念相似,它也是存放消息的容器。不过 Partition 主要是物理上的分区,而 Topic 表示消息的逻辑分区。
Producer
Producer 是消息的生产者,整个 Kafka 系统遵循的是生产者和消费者模式,消息会从生产者流通到接收者。
Consumer 和 Consumer Group
Consumer 即消费者,是 Kafka 框架内部对信息对接收方的定义。Consumer Group 会将消费者分组,然后按照不同的种类进行管理。
在整个 Kafka 服务的运行过程中,信息首先通过 producer 生产者提交给 Kafka 服务器上的 Topics 消息容器。在消息容器的内部,又会根据当前系统磁盘情况选择对应的物理分区进行存储,而每台服务分区可能对应一台或多台 Broker 服务器,之后 Broker 服务器再将信息推送给 Consumer。
Zookeeper 的作用
介绍完 Kafka 的相关概念和服务运行原理后,接下来我们学习 ZooKeeper 在 Kafka 框架下的应用。在 Kafka 中 ZooKeeper 几乎存在于每个方面,如下图所示,Kafka 会将我们上面介绍的流程架构存储为一个 ZooKeeper 上的数据模型。

由于 Broker 服务器采用分布式集群的方式工作,那么在服务的运行过程中,难免出现某台机器因异常而关闭的状况。为了保证整个 Kafka 集群的可用性,需要在系统中监控整个机器的运行情况。而 Kafka 可以通过 ZooKeeper 中的数据节点,将网络中机器的运行统计存储在数据模型中的 brokers 节点下。
在 Kafka 的 Topic 信息注册中也需要使用到 ZooKeeper ,在 Kafka 中同一个Topic 消息容器可以分成多个不同片,而这些分区既可以存在于一台 Broker 服务器中,也可以存在于不同的 Broker 服务器中。
而在 Kafka 集群中,每台 Broker 服务器又相对独立。为了能够读取这些以分布式方式存储的分区信息,Kafka 会将这些分区信息在 Broker 服务器中的对应关系存储在 ZooKeeper 数据模型的 topic 节点上,每一个 topic 在 ZooKeeper 数据节点上都会以 /brokers/topics/[topic] 的形式存在。当 Broker 服务器启动的时候,会首先在 /brokers/topics 节点下创建自己的 Broker_id 节点,并将该服务器上的分区数量存储在该数据节点的信息中。之后 ,在系统运行的过程中,通过统计 /brokers/topics 下的节点信息,就能知道对应的 Broker 分区情况。
整合 ZooKeeper 到自己的系统
在 Java 中使用 ZooKeeper
通过上面的介绍,我们大致了解了比较流行的开源框架是如何利用 ZooKeeper 解决自身问题的。接下来我们学习如何在自己的项目中使用 ZooKeeper。这里我们以比较流行的 springboot 框架为例。如下面代码所示,要想在 SpringBoot 框架中使用 ZooKeeper,首先要在工程的 pom 文件中引入对应的包。
我们在 dependency 引用配置中添加了版本为 3.6.1 的 org.apache.zookeeper 开发包。通过这个配置,我们的工程就可以使用 ZooKeeper 的相关功能了。
xml
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.1</version>
</dependency>
在项目开发 ZooKeeper 服务的时候,往往都会编写大量 ZooKeeper 客户端代码,去请求 ZooKeeper 服务端来完成相关的业务处理。而 ZooKeeper 自带的客户端使用起来不是很便利。之前我们学习过 Curator 框架,该框架被誉为 ZooKeeper 客户端中的瑞士军刀。利用该框架可以大大提高我们开发 ZooKeeper 服务的效率,因此,在项目开发中也推荐你来使用。
与上面介绍的相同,要想在项目中使用 Curator,首先需要将 Curator 引入到项目中,如下图所示,我们通过在 pom 文件中添加 dependency 来完成。首先,在配置中添加 4.0.0 版本的 org.apache.curator 包,其中包含了 Curator 的基础功能。之后添加的 curator-recipes 包,其中包括了重入锁、读写锁、Leader 选举设置等高级操作功能。把这三个包引用到工程后,整个 springboot 工程就可以利用 ZooKeeper 进行开发了。
xml
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.0</version>
</dependency>
在 Python 中使用 ZooKeeper
Python 作为一门跨平台开发语言,在数据科学、微服务、服务端开发中都有很广泛的应用。可能很多开发者是使用 Python 来进行业务开发的,下面我们介绍一下在 Python 项目中如何使用 ZooKeeper 服务。要想在 Python 中使用 ZooKeeper 服务,我们首先要在 Python 的运行环境中安装 kazoo 包。
pip install kazoo
连接 ZooKeeper 服务器
安装完 kazoo 包后,我们可以着手在 Python 项目中使用 ZooKeeper 服务了。首先连接 ZooKeeper 服务器,如下面的代码所示,在代码中引入 KazooClient 包,它的作用和上面介绍的 Curator 一样,主要提供 ZooKeeper 客户端的操作功能。之后调用 KazooClient 函数并传入服务器地址来创建服务器连接会话,再调用 create 函数来创建数据节点。
java
from kazoo.client import KazooClient
zk = KazooClient(hosts='127.0.0.1:2181')
zk.start()
zk.create("/pyzk/study/node", b"a value")
在日常开发工作中,无论是在 SpringBoot 框架下使用 Java 语言开发项目,还是使用 Pyhton 语言进行开发,使用 ZooKeeper 服务的方式基本一样,都是先要引入相关的 ZooKeeper 包,之后调用相关的客户端函数来完成业务相关的创建工作。
总结
本课时我们主要介绍了 ZooKeeper 在开源框架中的使用情况,其中,我们重点讲解了 Kafka 框架和 Dubbo 框架。Kafka 作为一个开源的分布式消息服务,会利用 ZooKeeper 实现对集群 Broker 服务器的运行情况统计等。而 Dubbo 则会利用 ZooKeeper 实现一个注册机制,以保证服务的透明性。
在实际的生产中 ZooKeeper 还能解决很多其他的问题,而这些问题本质上都是围绕分布式环境下一致性、可用性和分区容错性这三个分布式环境问题产生的。
这里给你留一个作业:试着使用 ZooKeeper 来解决在你在工作中遇到的问题,并尝试提升系统的安全性和稳定性。
25 如何搭建一个高可用的 ZooKeeper 生产环境?
如何在生产环境中部署一个安全可靠的 ZooKeeper 运行环境,是每个 IT 技术人员都要掌握的知识。没有一个安全可靠的运行环境,无论开发的服务再怎么优秀,都无法为用户提供服务。因此,本课时的重点将聚焦在 ZooKeeper 生产环境下安装的相关知识和参数配置技巧上。
运行方式
首先,我们来介绍一下 ZooKeeper 服务的几种运行模式,ZooKeeper 的运行模式一般分为单机模式、伪集群模式、集群模式。其中单机模式和伪集群模式,在我们的日常开发中经常用到。
单机模式配置
在 ZooKeeper 的单机模式下,整个 ZooKeeper 服务只运行在一台服务器节点下。在 zoo.cfg 配置文件中,我们只定义了基本的 dataDir 目录和 clientPort 端口号等信息。
ini
tickTime=2000
dataDir=/var/lib/zookeeper
clientPort=2181
伪集群模式配置
与单机模式相比,伪集群模式的意思是:虽然 ZooKeeper 服务配置有多台服务器节点,但是这些集群服务器都运行在同一台机器上。 通常伪集群服务器在配置的时候,每台服务器间采用不同的端口号进行区分,多用在本地开发或测试中。
如下面的代码所示,在配置伪集群的时候,我们将每台服务器的 IP 地址都指向 127.0.0.1,即本机地址,每台 ZooKeeper 对外提供服务的端口分别是 2223、3334、4445。
ini
tickTime=2000
dataDir=/var/lib/zookeeper
clientPort=2181
sever.1=127.0.0.1:2222:2223
sever.2=127.0.0.1:3333:3334
sever.3=127.0.0.1:4444:4445
集群模式配置
集群模式在配置上与伪集群模式基本相同。不同之处在于配置服务器地址列表的时候,组成 ZooKeeper 集群的各个服务器 IP 地址列表分别指向每台服务在网络中的实际 IP 地址。
ini
tickTime=2000
dataDir=/var/lib/zookeeper
clientPort=2181
sever.1=192.168.1.101:2222:2223
sever.1=192.168.1.102:3333:3334
sever.1=192.168.1.103:4444:4445
在 ZooKeeper 集群的三种模式中,单机模式和伪集群模式经常用于开发和测试中。而分别利用不同网络上的物理机器组成的 ZooKeeper 集群经常被我们作为生成系统的环境配置方式。
容器化部署
介绍完 ZooKeeper 服务器三种模式的配置方法后,接下来我们学习如何利用容器化技术来部署 ZooKeeper 集群。
首先,我们来了解一下什么是容器化技术。在我们前面的课程中,无论是在单机模式下在 ZooKeeper 数据模型中创建数据节点,还是在集群模式中,ZooKeeper 集群进行 Leader 节点选举,它们的实现都依赖于 ZooKeeper 服务部署在真实的物理机器上运行。
随着 IT 技术的发展,人们开始设想能否通过软件的方式,在一台机器上模拟出多台机器,突破单体物理机器的限制,利用一台物理机器的计算资源模拟出多台机器,为技术开发提供更加灵活和高效的环境。因此,有了我们比较熟悉的 VMware Workstation 等虚拟化技术软件。
利用该软件,我们可以在单一的桌面系统上,同时运行多个不同的操作系统。每个操作系统都可以看作独立的计算机。可以在不同的系统上进行程序开发、测试、服务部署等工作。虽然 VMware Workstation 为我们解决了系统资源虚拟化的问题,但是这种实现方式也有自身的缺点,比如每个虚拟机实例都需要运行客户端操作系统的完整副本以及其中包含的大量应用程序。从实际运行的角度来说,这会对物理机资源产生较大占用,也不利于整个虚拟系统的扩展和维护。
接下来我们要介绍的另一种容器化解决方式叫作 Docker,在实现容器化部署的同时,避免了 VMware Workstation 的上述问题。Docker 是一个开源的应用容器引擎,基于 Go 语言并遵从 Apache2.0 协议开源。与 VMware Workstation 相比,Docker 容器更加轻量化。在 Web 网站自动化部署、持续集成与发布等使用场景中具有广泛的应用。
本课时中,我们也使用 Docker 容器化技术来实现一个生产环境中的 ZooKeeper 集群部署案例。
使用 Docker 部署
安装 Docker
为了使用 Docker 容器技术部署我们的应用服务,首先,我们要在服务器上安装 Docker 软件。以 Linux 系统中的 CentOS 7 64 位版本为例。如下面的代码所示,通过 curl 命令使用官方安装脚本自动安装。curl 通过资源地址获取资源到本地进行安装。而国内服务器由于网络等原因可能无法访问默认的 Docker 资源服务器,因此这里采用的是国内阿里云的镜像资源服务器。
bash
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
创建 Docer 服务器
安装完 Docker 后,接下来我们就开始部署 ZooKeeper 的集群环境。这里的集群环境仍然由三台 Linux 服务器组成。而与上面我们介绍的利用网络中三台实体机器不同,这三台服务器可以通过 Docker 的方式来创建。
如下面的代码所示,首先打开系统终端,输入 docker pull 获取需要的系统镜像文件,这里选择的是 3.6 版本的 ZooKeeper,当然我们也可以不指定具体版本号,系统会默认拉取最新版本的 ZooKeeper 。之后我们通过 docker run 命令来启动 ZooKeeper 镜像服务器。执行完这两个步骤,我们就拥有一台运行 ZooKeeper 服务的服务器了。
bash
docker pull zookeeper:3.6
docker run -d --name=zookeeper1 --net=host zookeeper
配置 ZooKeeper 服务
创建完 ZooKeeper 服务器,接下来就要通过 zoo.cfg 文件来配置 ZooKeeper 服务。与部署在物理机器上不同,我们通过 docker exec 命令进入 Docker 创建的 ZooKeeper 服务器中,之后通过 vim 命令打开 zoo.cfg 文件进行相关配置。
bash
docker exec -it zookeeper1 /bin/bash
vim /conf/zoo.cfg
多台服务器配置
按照上面介绍的方法,如果我们想搭建三台服务器规模的 ZooKeeper 集群服务,就需要重复上面的步骤三次,并分别在创建的三台 ZooKeeper 服务器进行配置。
不过在实际生产环境中,我们需要的 ZooKeeper 规模可能远远大于三台,而且这种逐一部署的方式不但浪费时间,在配置过程中出错率也较高。因此,这里介绍另一种配置方式,通过 Docker Compose 的方式来部署 ZooKeeper 集群。
Docker Compose 是用于定义和运行多容器 Docker 应用程序的工具。通过 Compose,你可以使用 YML 文件来配置应用程序需要的所有服务。然后,使用一个命令,就可以从 YML 文件配置中创建并启动所有服务。如下面的代码所示,我们创建了一个名为 docker-compose.yml 的配置文件。
yaml
version: '3.6'
services:
zk1:
image: zookeeper:3.6
restart: always
hostname: zk1
container_name: zk1
ports: - 2181:2181
environment:
ZOO_MY_ID: 1
ZOO_SERVERS: server.1=zk1:2888:3888 server.2=zk2:2888:3888 server.3=zk3:2888:3888
zk2:
image: zookeeper:3.6
restart: always
hostname: zk2
container_name: zk2
ports: - 2182:2181
environment:
ZOO_MY_ID: 1
ZOO_SERVERS: server.1=zk1:2888:3888 server.2=zk2:2888:3888 server.3=zk3:2888:3888
zk3:
image: zookeeper:3.6
restart: always
hostname: zk3
container_name: zk3
ports: - 2183:2181
environment:
ZOO_MY_ID: 1
ZOO_SERVERS: server.1=zk1:2888:3888 server.2=zk2:2888:3888 server.3=zk3:2888:3888
在这个文件中,我们将需要手工逐一创建的 ZooKeeper 服务器的创建过程,通过 docker-compose.yml 配置文件的方式进行了描述。在这个配置文件中,我们告诉 Docker 服务分别创建并运行三个 ZooKeeper 服务器,并分别将本地的 2181, 2182, 2183 端口绑定到对应容器的 2181 端口上。
Docker 容器化方式部署的服务默认情况下对外界隔离,默认的 Docker 容器内服务无法被外界访问,因此需要进行端口映射,将外部物理机器的端口映射到对应的 Docker 服务器端口,这样外界在对物理机器进行访问后,系统会自动映射该端口到对应的 Docker 服务上。
在 environment 节点下,我们配置了 ZooKeeper 集群需要的两个配置参数,分别是 ZOO_MY_ID 以及 ZooKeeper 集群的服务器列表 ZOO_SERVERS。ZOO_MY_ID 是 1-255 之间的整数,必须在集群中唯一。
启动服务
在编写完 docker-compose.yml 配置文件的相关信息后,接下来我们就启动 docker 创建 ZooKeeper 集群服务。如下面的代码所示,首先,我们打开系统终端,输入 docker-compose up 命令来启动服务器。之后终端会显示我们配置的三台服务器都成功启动。
docker-compose up
Name Command State Ports
----------------------------------------------------------------------
zk1 /docker-entrypoint.sh zkSe ... Up 0.0.0.0:2181->2181/tcp
zk2 /docker-entrypoint.sh zkSe ... Up 0.0.0.0:2182->2181/tcp
zk3 /docker-entrypoint.sh zkSe ... Up 0.0.0.0:2183->2181/tcp
访问服务
ZooKeeper 集群配置完成并成功启动后,我们可以通过客户端命令来访问集群服务。如下面的代码所示,通过 zkCli.sh -server 客户端命令来访问集群服务器。
zkCli.sh -server localhost:2181,localhost:2182,localhost:2183
总结
本课时我们介绍了 ZooKeeper 的三种部署方式,学习了在这三种部署方式下,zoo.cfg 的不同配置方式。之后介绍了什么是容器化技术,并重点介绍了目前最为流行的容器化技术 Docker。并利用 Docker 创建了三台 Linux 服务器,通过这三台服务器来部署 ZooKeeper 集群。
相比本课时的例子,在实际生产环境中,对于 ZooKeeper 的性能要求可能更高。为了满足性能的要求,我们可以在三台服务器的基础上对 Docker 服务器进行动态增加来满足性能要求,这也是本课时留给你的作业。
在扩展集群规模的时候,根据 ZooKeeper 集群中 Leader 节点的选举原则,整个 ZooKeeper 集群服务器在数量上,尽量采用奇数原则,从而满足当 Leader 节点选举时,能够最终产生大多数的投票结果,避免偶数服务器一直存在票数相等的问题,从而出现脑裂等问题。
26 JConsole 与四字母命令:如何监控服务器上 ZooKeeper 的运行状态?
在上节课中我们学习了在生产环境中,如何部署 ZooKeeper 集群服务。为了我们的程序服务能够持续稳定地对外提供服务,除了在部署的时候尽量采用分布式、集群服务等方式提高 ZooKeeper 服务的可靠性外,在服务上线运行的时候,我们还可以通过对 ZooKeeper 服务的运行状态进行监控,如运行 ZooKeeper 服务的生产服务器的 CPU 、内存、磁盘等使用情况来达到目的。在系统性能达到瓶颈的时候,可以增加服务器资源,以保证服务的稳定性。
JConsole 介绍
通常使用 Java 语言进行开发的技术人员对 JConsole 并不陌生。JConsole 是 JDK 自带的工具,用来监控程序运行的状态信息。如下图所示,我们打开系统的控制终端,输入 JConsole 就会弹出一个这样的监控界面。

JConsole 使用
介绍完 JConsole 的基本信息后,接下来我们来了解如何利用 JConsole 对远程 ZooKeeper 集群服务进行监控。之所以能够通过 JConsole 连接 ZooKeeper 服务进行监控,是因为 ZooKeeper 支持 JMX(Java Management Extensions),即 Java 管理扩展,它是一个为应用程序、设备、系统等植入管理功能的框架。
JMX 可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活地开发无缝集成的系统、网络和服务管理应用。我们可以通过 JMX 来访问和管理 ZooKeeper 服务集群。接下来我们就来介绍一下监控 ZooKeeper 集群服务的相关配置操作。
在 JConsole 配置信息中,连接我们要进行监控的 ZooKeeper 集群服务器。如下面的流程所示,在配置文件中输入 ZooKeeper 服务器的地址端口等相关信息。
开启 JMX
首先,我们先开启 ZooKeeper 的 JMX 功能。在 ZooKeeper 安装目录下找到 bin 文件夹,在 bin 文件夹中 ,通过 vim 命令来编辑 zkServer.sh 文件。如下代码所示,输入 JMX 服务的端口号并禁止身份认证等配置。
ini
-Dcom.sun.management.jmxremote.port=50000
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
连接 ZooKeeper
配置完 JMX 的开启功能后,接下来我们通过系统终端启动 JConsole ,再在弹出的对话框中选择远程连接,然后在远程连接的地址中输入要监控的 ZooKeeper 服务器地址,之后就可以通过 JConsole 监控 ZooKeeper 服务器了。

四字母命令
除了上面介绍的 JConsole 监控控制台之外,ZooKeeper 还提供了一些命令,可使我们更加灵活地统计监控 ZooKeeper 服务的状态信息。 ZooKeeper 提供的这些命令也叫作四字母命令,如它们的名字一样,每一个命令都是由四个字母组成的。如下代码所示,在操作时,我们会打开系统的控制台,并输入相关的命令来查询 ZooKeeper 服务,比如我们可以输入 stat 命令来查看数据节点等信息。
bash
echo {command} | nc 127.0.0.1 2181
介绍完四字母命令的调用方式和执行格式后,接下来我们介绍几种常见的四字母命令,分别是 stat 、srvr,以及 cons 等。
stat
stat 命令的作用是监控 ZooKeeper 服务器的状态,我们通过 stat 命令统计 ZooKeeper 服务器的 ZooKeeper 版本信息、集群数节点等信息,如下面的代码所示,我们在操作时会输入 echo stat 命令来输出查询到的服务状态信息到控制台。
bash
$ echo stat | nc localhost 2181
Zookeeper version: 3.4.13- built on 06/29/2018 04:05 GMT
Clients:
/0:0:0:0:0:0:0:1:40598[0](queued=0,recved=1,sent=0)
Latency min/avg/max: 0/0/0
Received: 17
Sent: 16
Connections: 1
Outstanding: 0
Zxid: 0x0
Mode: follower
Node count: 4
srvr
srvr 命令与 stat 命令的功能十分相似,唯一不同的地方是 srvr 命令不会将与客户端的连接情况输出,通过 srvr 命令只会查询服务器的自身信息。
bash
$ echo srvr | nc localhost 2181
Zookeeper version: 3.4.13- built on 06/29/2018 04:05 GMT
Latency min/avg/max: 0/0/0
Received: 26
Sent: 25
Connections: 1
Outstanding: 0
Zxid: 0x0
Mode: follower
Node count: 4
cons
cons 命令用于输出当前这台服务器上所有客户端连接的详细信息,包括每个客户端的客户端 IP 、会话 ID 和最后一次与服务器交互的操作类型等。
shell
$ echo cons | nc localhost 2181
/0:0:0:0:0:0:0:1:31569[0](queued=0,recved=1,sent=0)
ruok
ruok 命令的主要作用是查询 ZooKeeper 服务器是否正常运行。如果 ZooKeeper 服务器正常运行,执行完 ruok 命令后,会得到 "imok" 返回值。如果 ZooKeeper 服务没有正常运行,则不会有任何返回值。在使用 ruok 命令的时候,在这里我们要注意的一点是,有些时候即使返回了 "imok" 字段,ZooKeeper 服务也可能没有正常运行,唯一能确定的是该台服务器的 2181 端口是打开的,如下代码所示。
shell
$ echo ruok | nc localhost 2181
监控集群信息
介绍完系统监控工具 JConsole 以及常用的命令后,接下来我们就从实际的生产角度出发,来看一下在 ZooKeeper 集群生产环境中如何监控系统集群运行情况,以及如何利用我们监控的数据诊断 ZooKeeper 服务的运行问题并解决问题。
虽然 ZooKeeper 服务提供了丰富的四字母命令,让我们可以通过命令来获得 ZooKeeper 服务相关的运行信息,但是在实际的生产环境中, ZooKeeper 集群的规模可能很大,逐一通过命令的方式监控 ZooKeeper 服务显然不可行。因此,这里我们会介绍一种自动的监控 ZooKeeper 集群运行服务的方式。
为了编写自动化监控 ZooKeeper 集群服务,首先我们要明确需要监控哪些数据类型,在这里我们主要对最小会话超时、最大会话超时、最大连接数、发送的数据包、接收的数据包进行监控,而具体的我们则会通过 Zabbix 来实现。
Zabbix
Zabbix 是一个性能监控的管理工具,它基于 Web 界面提供分布式系统监视,以及网络监视功能的企业级开源解决方案。
安装
我们可以通过 Maven 或 Gradle 项目管理工具下载 Zabbix,这里我们主要以 Maven 工程为例,如下代码所示,需要在 pom 文件中引入相关的配置信息。
xml
<dependency>
<groupId>io.github.cgi</groupId>
<artifactId>zabbix-api</artifactId>
<version>0.0.5</version>
</dependency>
配置项
将 Zabbix 引入到我们的工程项目后,接下来,就可以编写一个程序来自动化地获取 ZooKeeper 服务的相关信息。这里我们创建一个 ZooKeeperInfo 脚本,如下代码所示,在脚本文件中我们创建了一个 mntr 数组变量用来设置我们想要监控的服务参数,比如 minSessionTimeout 最小超时时间、maxSessionTimeout 最大超时时间等。
java
public class ZooKeepInfo(){
Static Final String ZookeeperServer = '127.0.0.1'
Static Final String ZookeeperPort = 2181
Static Final String ZookeeperCommand = 'mntr'
Static Final String ZookeeperKey = 'zk_version
CommandKey={
'conf':['clientPort','dataDir','dataLogDir','tickTime','maxClientCnxns','minSessionTimeout','maxSessionTimeout','serverId','initLimit','syncLimit','electionAlg','electionPort','quorumPort','peerType'],
'ruok':['state'],
'mntr':['zk_version','zk_avg_latency','zk_max_latency','zk_min_latency','zk_packets_received','zk_packets_sent','zk_num_alive_connections','zk_outstanding_requests','zk_server_state','zk_znode_count','zk_watch_count','zk_ephemerals_count','zk_approximate_data_size','zk_open_file_descriptor_count','zk_max_file_descriptor_count','zk_followers','zk_synced_followers','zk_pending_syncs']
}
class ZooKeeperCommands(object):
def ZooKeeperCommands(self,server,port,zkCommand,zkKey):
self._server = server
self._port = port
self._zkCommand = zkCommand
self._zkKey = zkKey
self._value_raw = None
self._value = None
void zkExec(this):
self._exec_command()
self._parse_value()
return self._value
void _exec_command(this):
Telnet tn = Telnet(self._server, self._port, timeout=30)
tn.read_until('login: ')
tn.write(username + '\n')
tn.read_until('password: ')
tn.write(password + '\n')
tn.read_until(finish)
}
结束
本节课我们主要学习了 ZooKeeper 集群在日常生产环境中的维护问题。首先介绍了 ZooKeeper 集群通过 JMX 方式进行远程监控的方法,然后学习了 JConsole 以及四字母命令的使用方式,最后介绍了在实际工作中,面对大规模的 ZooKeeper 集群时,我们如何做到自动化的获取监控数据。
本节课在实现自动化的数据获取时,利用了一个开源的性能监控工具 Zabbix 。除了课中提到的一些性能监控参数外,我们也可以利用 Zabbix 监控一些和自身业务相关的数据信息,比如在对数据节点的创建数量有严格要求的情况下,我们可以编写相关的脚本对某一个数据节点下子节点的创建个数进行监控,当该子节点个数大于我们设置的某一个临界值时,会给出报警或禁止该节点再进行创建操作。
27 crontab 与 PurgeTxnLog:线上系统日志清理的最佳时间和方式
本节课,我们主要学习对线上 ZooKeeper 服务器日志进行维护的操作,主要维护方式是备份和清理。几乎所有的生产系统都会产生日志文件,用来记录服务的运行状态,在服务发生异常的时候,可以用来作为分析问题原因的依据。ZooKeeper 作为分布式系统下的重要组件,在分布式网络中会处理大量的客户端请求,因此也会产生大量的日志文件,对这些问题的维护关系到整个 ZooKeeper 服务的运行质量。接下来我们就来学习如何维护这些日志文件。
日志类型
首先,我们先来介绍线上生产环境中的 ZooKeeper 集群在对外提供服务的过程中,都会产生哪些日志类型。我们在之前的课程中也介绍过了,在 ZooKeeper 服务运行的时候,一般会产生数据快照和日志文件,数据快照用于集群服务中的数据同步,而数据日志则记录了 ZooKeeper 服务运行的相关状态信息。其中,数据日志是我们在生产环境中需要定期维护和管理的文件。
清理方案
如上面所介绍的,面对生产系统中产生的日志,一般的维护操作是备份和清理。备份是为了之后对系统的运行情况进行排查和优化,而清理主要因为随着系统日志的增加,日志会逐渐占用系统的存储空间,如果一直不进行清理,可能耗尽系统的磁盘存储空间,并最终影响服务的运行。但在实际工作中,我们不能 24 小时监控系统日志情况,因此这里我们介绍一种定时任务,可以自动清理和备份 ZooKeeper 服务运行产生的相关日志。
清理工具
corntab
首先,我们介绍的是 Linux corntab ,它是 Linux 系统下的软件,可以自动地按照我们设定的时间,周期性地执行我们编写的相关脚本。下面我们就用它来写一个定时任务,实现每周定期清理 ZooKeeper 服务日志。
创建脚本
我们通过 Linux 系统下的 Vim 文本编辑器,来创建一个叫作 " logsCleanWeek " 的定时脚本,该脚本是一个 shell 格式的可执行文件。如下面的代码所示,我们在 usr/bin/ 文件夹下创建该文件,该脚本的主要内容是设定 ZooKeeper 快照和数据日志的对应文件夹路径,并通过 shell 脚本和管道和 find 命令 查询对应的日志下的日志文件,这里我们保留最新的 10 条数据日志,其余的全部清理。
bash
#!/bin/bash
dataDir=/home/zk/zk_data/version-2
dataLogDir=/home/zk/zk_log/version-2
ls -t $dataLogDir/log.* | tail -n +$count | xargs rm -f
ls -t $dataDir/snapshot.* | tail -n +$count | xargs rm -f
ls -t $logDir/zookeeper.log.* | tail -n +$count | xargs rm -f
find /home/home/zk/zk_data/version-2 -name "snap*" -mtime +1 | xargs rm -f
find /home/home/zk/zk_data/version-2 -name "snap*" -mtime +1 | xargs rm -f
find /home/home/zk/zk_data/logs/ -name "zookeeper.log.*" -mtime +1 | xargs rm --f
创建定时任务
创建完定时脚本后,我们接下来就利用 corntab 来设置脚本的启动时间,如下面的代码所示。corntab 命令的语法比较简单,其中 -u 表示设定指定的用户,因为 Linux 系统是一个多用户操作系统,而 crontab 的本质就是根据使用系统的用户来设定程序执行的时间计划表。因此当命令的执行者具有管理员 root 账号的权限时,可以通过 -u 为特定用户设定某一个程序的具体执行时间。
bash
crontab [ -u user ] { -l | -r | -e }
接下来我们打开系统的控制台,并输入 crontab -e 命令,开启定时任务的编辑功能。如下图所示,系统会显示出当前已有的定时任务列表。整个 crontab 界面的操作逻辑和 Vim 相同,为了新建一个定时任务,我们首先将光标移动到文件的最后一行,并敲击 i 键来开启编辑模式。

这个 crontab 定时脚本由两部分组成,第一部分是定时时间,第二部分是要执行的脚本。如下代码所示,脚本的执行时间是按照 f1 分、 f2 小时、f3 日、f4 月、f5 一个星期中的第几天这种固定顺序格式编写的。
f1 f2 f3 f4 f5 program
当对应的时间位上为 * 时,表示每间隔一段时间都要执行。例如,当 f1 分上设定的是 * 时,表示每分钟都要执行对应的脚本。而如果我们想在每天的特定时间执行对应的脚本,则可以通过在对应的时间位置设定一个时间段实现,以下代码所演示的就是将脚本清理时间设定为每天早上的 6 点到 8 点。
0 6-8 * * * /usr/bin/logsCleanWeek.sh>/dev/null 2>&1
查看定时任务
当我们设定完定时任务后,就可以打开控制台,并输入 crontab -l 命令查询系统当前的定时任务。

到目前为止我们就完成了用 crontab 创建定时任务来自动清理和维护 ZooKeeper 服务产生的相关日志和数据的过程。
crontab 定时脚本的方式相对灵活,可以按照我们的业务需求来设置处理日志的维护方式,比如这里我们希望定期清除 ZooKeeper 服务运行的日志,而不想清除数据快照的文件,则可以通过脚本设置,达到只对数据日志文件进行清理的目的。
PurgeTxnLog
除了上面所介绍的,通过编写 crontab 脚本定时清理 ZooKeeper 服务的相关日志外, ZooKeeper 自身还提供了 PurgeTxnLog 工具类,用来清理 snapshot 数据快照文件和系统日志。
PurgeTxnLog 清理方式和我们上面介绍的方式十分相似,也是通过定时脚本执行任务,唯一的不同是,上面提到在编写日志清除 logsCleanWeek 的时候 ,我们使用的是原生 shell 脚本自己手动编写的数据日志清理逻辑,而使用 PurgeTxnLog 则可以在编写清除脚本的时候调用 ZooKeeper 为我们提供的工具类完成日志清理工作。
如下面的代码所示,首先,我们在 /usr/bin 目录下创建一个 PurgeLogsClean 脚本。注意这里的脚本也是一个 shell 文件。在脚本中我们只需要编写 PurgeTxnLog 类的调用程序,系统就会自动通过 PurgeTxnLog 工具类为我们完成对应日志文件的清理工作。
bash
#!/bin/sh
java -cp "$CLASSPATH" org.apache.zookeeper.server.PurgeTxnLog
echo "清理完成"
PurgeTxnLog 方式与 crontab 相比,使用起来更加容易而且也更加稳定安全,不过 crontab 方式更加灵活,我们可以根据不同的业务需求编写自己的清理逻辑。
结束
本节课我们介绍了线上 ZooKeeper 服务日志和数据快照的清理和维护工作,可以通过 crontab 和 PurgeTxnLog 两种方式实现。这两种方式唯一的不同在清理日志脚本的实现方式上,crontab 是通过我们自己手动编写的 shell 脚本实现的,在执行上需要考虑脚本权限相关的问题,而 PurgeTxnLog 则是 ZooKeeper 提供的专门用来处理日志清除相关的工具类,使用起来更加容易,开发人员不用考虑底层的实现细节。这里希望你结合自身工作中的生产环境来选择一种适合自己的 ZooKeeper 数据维护方式。