Zookeeper 集群

对于要提供可靠的 Zookeeper 服务,那么应该以集群模式部署 Zookeeper 服务。只要半数以上的集群节点在线,那么服务就是可用的。因为Zk服务可用需要超过半数以上(n/2 + 1)的节点,所以Zk的集群最好是使用奇数个机器。比如:对于四个机器的Zk集群,只能处理单台机器的故障,如果两台机器故障,则剩下的两台不能占半数以上(3)了。但是,如果有了5个机器,Zk可以处理2个机器的故障。

所以,对于生产环境,3个Zk服务器是推荐的最小的配置,并且还建议它运行在不同的机器上。

Note 通常来说,三台服务器对于生产环境应该足够了,但是为了在维护期间也要获得最大的可靠性,你可以安装五台服务器。使用三台服务器时,如果你拿一台服务器去维护,则在维护期间,剩下两台服务器可能会发生故障导致Zk不可用,如果有5台,拿一台去维护,剩下的再故障一台也能提供Zk服务。

通常,集群有两种角色:leaderfollower,但是为了快速拓展集群,还有一个可以启用的角色 : Observer

搭建

下面开始搭建Zk集群。Zk集群最新版3.5需要java7及以上的环境。这里的话演示在一台机器上,根据端口不同的划分搭建三个服务端。

服务 客户端端口 服务通信端口 leader选举端口
server1 2181 2888 3888
server2 2182 2889 3889
server3 2183 2890 3890

上面是三个服务的端口划分。客户端端口是用来客户端连接的;服务通信端口提供各个实例间通信,leader才会监听此端口,follower从此端口同步信息;leader选举端口是为了重新选举leader通信用的。

创建配置文件:

# zoo_server1.cfg
tickTime=2000
dataDir=/tmp/zookeeper/server1
clientPort=2181
initLimit=5
syncLimit=2
admin.enableServer=false # 禁用AdminServer
server.1=localhost:2888:3888
server.2=localhost:2889:3889
server.3=localhost:2890:3890

# zoo_server2.cfg
tickTime=2000
dataDir=/tmp/zookeeper/server2
clientPort=2182
initLimit=5
syncLimit=2
admin.enableServer=false # 禁用AdminServer
server.1=localhost:2888:3888
server.2=localhost:2889:3889
server.3=localhost:2890:3890

# zoo_server3.cfg
tickTime=2000
dataDir=/tmp/zookeeper/server3
clientPort=2183
initLimit=5
syncLimit=2
admin.enableServer=false # 禁用AdminServer
server.1=localhost:2888:3888
server.2=localhost:2889:3889
server.3=localhost:2890:3890

每个服务的配置都需要知道每个服务的地址: server.x=ip:server-port:leader-election-portx 是每个服务的id,这个id由 myid 文件指定,myid 是一个文本文件,每个服务器对应一个文件,存放在 dataDir 目录,文件内容只包含这个服务的id,id在集群中是唯一的,其值应该介于1-255之间。重要信息: 如果启用ttl节点等拓展功能,由于内部限制,id必须介于1-254之间。

创建myid:

mkdir -p /tmp/zookeeper/server{1,2,3}

echo 1 > /tmp/zookeeper/server1/myid

echo 2 > /tmp/zookeeper/server2/myid

echo 3 > /tmp/zookeeper/server3/myid

接下来一个一个启动,先启动server1:

zkServer.sh start ./conf/zoo_server1.cfg

后面跟的是server1配置的地址,如果没有指定,默认使用的是 zoo.cfg。启动server1后使用 zkServer.sh status ./conf/zoo_server1.cfg 发现提示可能没有运行:

zkServer.sh status conf/zoo_server1.cfg
ZooKeeper JMX enabled by default
Using config: conf/zoo_server1.cfg
Client port found: 2181. Client address: localhost.
Error contacting service. It is probably not running.

# 查看日志
Cannot open channel to 3 at election address localhost/127.0.0.1:3890
java.net.ConnectException: 拒绝连接 (Connection refused)

Cannot open channel to 2 at election address localhost/127.0.0.1:3889
java.net.ConnectException: 拒绝连接 (Connection refused)

所以不用着急,这是因为以集群模式启动,现在只启动了一个服务,然后我们指定的是3个服务,不到半数以上,集群是不可能提供服务的,并且日志上说和另外两个服务的选举端口连接不上,一直不能发生选举,导致服务不可用。

现在启动server2:

zkServer.sh start conf/zoo_server2.cfg

查看server2的状态,发现server2变成了leader,而server1则是follower:

zkServer.sh status conf/zoo_server2.cfg

ZooKeeper JMX enabled by default
Using config: conf/zoo_server2.cfg
Client port found: 2182. Client address: localhost.
Mode: leader

zkServer.sh status conf/zoo_server1.cfg
ZooKeeper JMX enabled by default
Using config: conf/zoo_server1.cfg
Client port found: 2181. Client address: localhost.
Mode: follower

这里有一个点,为什么server2是leader而不是server1是leader,明明server1比server2更早启动?这里涉及到选举的知识和算法,后面会讲。

按照同样的方法启动server3,发现server3也是follower。

我们可以稍微验证一下集群的可靠性。

我们刚刚看到,server2是leader,我们连接到server2,创建一个节点:

[zk: localhost:2182(CONNECTED) 1] create /leader-create "server2 is leader"
Created /leader-create
[zk: localhost:2182(CONNECTED) 2] get /leader-create
server2 is leader

然后我们停止server2,通过查看状态,发现server3变成了leader,server1还是follower,我们连接server1,查看之前的leader server2创建的节点存不存在:

[zk: localhost:2181(CONNECTED) 0] ls /
[leader-create, zookeeper]
[zk: localhost:2181(CONNECTED) 1] get /leader-create
server2 is leader

显然,很明显的获取到了数据,在一般的zk 客户端,通常我们是使用 ip:port,ip2:port,ip3:port/chroot,其实我们使用集群中的一个ip:port就可以自动的服务发现,但是写一个是不推荐的,因为你并不能确定某个故障的服务恰好不是你写的连接,不然就会造成单点故障,所以写多个是必要的,因为会有重连功能。

集群搭建就到了这里,后面会讲一些运维和实践。

上次在讲客户端命令的时候,遗留了两个命令没有讲。(这里依然没有讲清楚,后面会再涉及到)

config

用于打印当前集群的配置。用法: config [-c] [-w] [-s]

config -s 
server.1=localhost:2888:3888:participant
server.2=localhost:2889:3889:participant
server.3=localhost:2890:3890:participant
version=200000000
cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x200000000
mtime = Tue Aug 27 19:43:20 CST 2019
pZxid = 0x0
cversion = 0
dataVersion = -1
aclVersion = -1
ephemeralOwner = 0x0
dataLength = 140
numChildren = 0

-s 表示输出 stat 的统计信息,-c 表示只输出版本号和当前配置的连接,-w 增加一个监听器。

reconfig

用法:

reconfig [-s] [-v version] [[-file path] | [-members serverID=host:port1:port2;port3[,...]*]] | [-add serverId=host:port1:port2;port3[,...]]* [-remove serverId[,...]*]

-s 打印stat,-v 指定版本对了的才能修改成功。

reconfig -add server.4=localhost:2891:3891:observer;2184

reconfig -members server.1=125.23.63.23:2780:2783:participant;2791,server.2=125.23.63.24:2781:2784:participant;2792,server.3=125.23.63.25:2782:2785:participant;2793

reconfig -remove 3 -add server.5=125.23.63.23:1234:1235;1236

 reconfig -remove 3,4 -add server.5=localhost:2111:2112;2113,6=localhost:2114:2115:observer;2116

 reconfig -file newconfig.cfg ///newconfig.cfg 是动态配置文件

observer

一个集群只能有一个 leader,可以有多个 follower,但是当我们需要拓展集群的时候,如果我们只能添加 follower,那么写入性能会降低,因为写操作需要集群半数以上的节点写成功则写操作成功,因此随着更多选民的增加,投票成本就会相应增加。

Zk 引入了一种名为 Observer 的新型 Zk 服务端,它有助于解决这个问题并且进一步的提高 Zk 的可拓展性。观察者是集群中的非投票成员,它只听取投票结果,而不参与投票,除了这个区别,observerfollower 完全相同,观察者可以接收读写请求,并和 follower 一样,将写请求转发给 leader。由于观察者不参与投票,那么 leader 写操作的时候,半数以上的节点并不包括 observer,所以可以在不影响投票性能的前提下,可以增加观察者的数量以提高读性能。

观察者还有其他的有点。因为他们不投票,所以他们不是Zk集群的重要组成部分,所以,那么观察者断开连接、宕机也不会影响整个集群的可用性。对用户的好处是,observer 可以用比 follower 更不可靠的网络连接。实际上,Observers 可能由于与另外一个数据中心的Zk服务器通信,对于连接到Observer的客户端来说,读取可能会更快速,写也只会花费少量的网络流量,因为在没有投票的情况下所需要的消息会更少。

把一个服务以 observer 运行,需要修改配置文件:

peerType=observer

server.x=localhost:2181:3181:observer

peerType 说明以observer的身份运行,server.x=localhost:2181:3181:observer则告诉集群中的其它成员,x 是一个观察者,不应该期望它能进行投票。

集群怎么处理读写?

现在我们有一个集群,集群中有三个角色 leaderobserverfollower

那么,客户单的读写请求,集群是怎么处理的呢?

我们分几种情况来说明。

读 observer

对于客户端发送到 observer 的读请求,由 observer 直接响应客户端

读 follower

对于客户端发送到 follower 的读请求,由 follower 直接响应客户端

读 leader

对于客户端发送到 leader 的读请求,由 leader 直接响应客户端

写 observer

对于客户端发送到 observer 的写请求,observer 会将请求转发到 leaderleader 对集群中的 follower 广而告之,超过半数的 follower 响应 leaderleader 写成功,响应 observerobserver 响应客户端。

写 follower

对于客户端发送到 follower 的写请求,follower 会将请求转发到 leaderleader 对集群中的 follower 广而告之,超过半数的 follower 响应 leaderleader 写成功,响应 followerfollower 响应客户端。

写 leader

对于客户端发送到 leader 的写请求,leader 对集群中的 follower 广而告之,超过半数的 follower 响应 leaderleader 写成功,响应客户端。

所以,对于Zk来说,读请求不管哪个服务收到都直接返回,写请求为了一致性都交由 leader 来写。那么Zk是怎么保持一致性的呢?我们会在 Zookeeper的zab协议 中讲到。