搭建一个高可用的 ZooKeeper 生产环境

运行方式

首先,我们来介绍一下 ZooKeeper 服务的几种运行模式,ZooKeeper 的运行模式一般分为单机模式、伪集群模式、集群模式
其中单机模式和伪集群模式,在我们的日常开发中经常用到。

单机模式配置

在 ZooKeeper 的单机模式下,整个 ZooKeeper 服务只运行在一台服务器节点下。在 zoo.cfg 配置文件中,我们只定义了基本的 dataDir 目录和 clientPort 端口号等信息。
tickTime=2000 dataDir=/var/lib/zookeeper clientPort=2181

伪集群模式配置

与单机模式相比,伪集群模式的意思是:虽然 ZooKeeper 服务配置有多台服务器节点,但是这些集群服务器都运行在同一台机器上。
 通常伪集群服务器在配置的时候,每台服务器间采用不同的端口号进行区分,多用在本地开发或测试中。
如下面的代码所示,在配置伪集群的时候,我们将每台服务器的 IP 地址都指向 127.0.0.1,即本机地址,每台 ZooKeeper 对外提供服务的端口分别是 2223、3334、4445。
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 地址。
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 资源服务器,因此这里采用的是国内阿里云的镜像资源服务器。
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 服务的服务器了。
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 文件进行相关配置。
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 的配置文件。
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 集群中 Leader 节点的选举原则,整个 ZooKeeper 集群服务器在数量上,尽量采用奇数原则,从而满足当 Leader 节点选举时,能够最终产生大多数的投票结果,避免偶数服务器一直存在票数相等的问题,从而出现脑裂等问题。

监控服务器上 ZooKeeper 的运行状态

了我们的程序服务能够持续稳定地对外提供服务,除了在部署的时候尽量采用分布式、集群服务等方式提高 ZooKeeper 服务的可靠性外,在服务上线运行的时候,我们还可以通过对 ZooKeeper 服务的运行状态进行监控,如运行 ZooKeeper 服务的生产服务器的 CPU 、内存、磁盘等使用情况来达到目的。
在系统性能达到瓶颈的时候,可以增加服务器资源,以保证服务的稳定性。

JConsole 介绍

通常使用 Java 语言进行开发的技术人员对 JConsole 并不陌生。JConsole 是 JDK 自带的工具,用来监控程序运行的状态信息。如下图所示,我们打开系统的控制终端,输入 JConsole 就会弹出一个这样的监控界面。
notion image

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 服务的端口号并禁止身份认证等配置。
-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 命令来查看数据节点等信息。
echo {command} | nc 127.0.0.1 2181
介绍完四字母命令的调用方式和执行格式后,接下来我们介绍几种常见的四字母命令,分别是 stat 、srvr,以及 cons 等。

stat

stat 命令的作用是监控 ZooKeeper 服务器的状态,我们通过 stat 命令统计 ZooKeeper 服务器的 ZooKeeper 版本信息、集群数节点等信息,如下面的代码所示,我们在操作时会输入 echo stat 命令来输出查询到的服务状态信息到控制台。
$ 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 命令只会查询服务器的自身信息。
$ 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 和最后一次与服务器交互的操作类型等。
$ 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 端口是打开的,如下代码所示。
$ echo ruok | nc localhost 2181

监控集群信息

介绍完系统监控工具 JConsole 以及常用的命令后,接下来我们就从实际的生产角度出发,来看一下在 ZooKeeper 集群生产环境中如何监控系统集群运行情况,以及如何利用我们监控的数据诊断 ZooKeeper 服务的运行问题并解决问题。
虽然 ZooKeeper 服务提供了丰富的四字母命令,让我们可以通过命令来获得 ZooKeeper 服务相关的运行信息,但是在实际的生产环境中, ZooKeeper 集群的规模可能很大,逐一通过命令的方式监控 ZooKeeper 服务显然不可行。因此,这里我们会介绍一种自动的监控 ZooKeeper 集群运行服务的方式。
为了编写自动化监控 ZooKeeper 集群服务,首先我们要明确需要监控哪些数据类型,在这里我们主要对最小会话超时、最大会话超时、最大连接数、发送的数据包、接收的数据包进行监控,而具体的我们则会通过 Zabbix 来实现。

Zabbix

Zabbix 是一个性能监控的管理工具,它基于 Web 界面提供分布式系统监视,以及网络监视功能的企业级开源解决方案。

安装

我们可以通过 Maven 或 Gradle 项目管理工具下载 Zabbix,这里我们主要以 Maven 工程为例,如下代码所示,需要在 pom 文件中引入相关的配置信息。
<dependency>     <groupId>io.github.cgi</groupId>     <artifactId>zabbix-api</artifactId>     <version>0.0.5</version> </dependency>

配置项

将 Zabbix 引入到我们的工程项目后,接下来,就可以编写一个程序来自动化地获取 ZooKeeper 服务的相关信息。这里我们创建一个 ZooKeeperInfo 脚本,如下代码所示,在脚本文件中我们创建了一个 mntr 数组变量用来设置我们想要监控的服务参数,比如 minSessionTimeout 最小超时时间、maxSessionTimeout 最大超时时间等。
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) }
我们也可以利用 Zabbix 监控一些和自身业务相关的数据信息,比如在对数据节点的创建数量有严格要求的情况下,我们可以编写相关的脚本对某一个数据节点下子节点的创建个数进行监控,当该子节点个数大于我们设置的某一个临界值时,会给出报警或禁止该节点再进行创建操作。