Zookeeper 集群模式一共有三种类型的角色
Leader: 处理所有的事务请求(写请求),可以处理读请求,集群中只能有一个Leader
Follower: 只能处理读请求,同时作为 Leader的候选节点,即如果Leader宕机,Follower节点要参与到新的Leader选举中,有可能成为新的Leader节点。Folllower节点接收到写请求后会将请求转发到Leader节点上。
Observer:只能处理读请求。不能参与选举
observer节点存在的意义:和ZK的一致性协议有关系,写数据的时候并不是只写到leader节点上就说明写成功了,存在一个过半机制。即Leader节点数据写成功后,需要将数据同步到其他follower节点上,只有集群中过半的机器上都保存了这个数据,ZK才会通知客户端数据保存成功。
注意,Observer节点不参与过半机制,只提供读请求。
一、配置zookeeper集群
1、集群搭建
本例搭建的是伪集群模式,即一台机器上启动四个zookeeper实例组成集群,真正的集群模式无非就是实例IP地址不同,搭建方法没有区别。
Step1:配置JAVA环境,检验环境:保证是jdk7 及以上即可
java -version
Step2:下载并解压zookeeper
wget https://mirror.bit.edu.cn/apache/zookeeper/zookeeper-3.5.9/apache-zookeeper-3.5.9-bin.tar.gz
tar -zxvf apache-zookeeper-3.5.9-bin.tar.gz
cd apache-zookeeper-3.5.9-bin
Step3:重命名 zoo_sample.cfg文件
cp conf/zoo_sample.cfg conf/zoo-1.cfg
Step4:修改配置文件zoo-1.cfg,原配置文件里有的,修改成下面的值,没有的则加上
# vim conf/zoo-1.cfg
dataDir=/usr/local/zookeeper/apache-zookeeper-3.5.9-bin/data/zookeeper-1
clientPort=2181
# // participant 可以不用写,默认就是participant
# 第一个端口是通信端口
server.1=192.168.131.171:2001:3001
server.2=192.168.131.171:2002:3002
server.3=192.168.131.171:2003:3003
server.4=192.168.131.171:2004:3004:observer
配置完成后,将zoo-1.cfg拷贝三份,分别是zoo-2.cfg、zoo-3.cfg、zoo-4.cfg, 修改里面的dataDir目录和端口号:
dataDir=/usr/local/zookeeper/apache-zookeeper-3.5.9-bin/data/zookeeper-2
clientPort=2182
dataDir=/usr/local/zookeeper/apache-zookeeper-3.5.9-bin/data/zookeeper-2
clientPort=2183
dataDir=/usr/local/zookeeper/apache-zookeeper-3.5.9-bin/data/zookeeper-4
clientPort=2184
注意,在配置的ip和端口号后面不能加空格!!!否则可能出错说无法解析IP地址!
配置说明
- tickTime:用于配置Zookeeper中最小时间单位的长度,很多运行时的时间间隔都是使用tickTime的倍数来表示的。
- initLimit:该参数用于配置Leader服务器等待Follower启动,并完成数据同步的时间。Follower服务器再启动过程中,会与Leader建立连接并
- 完成数据的同步,从而确定自己对外提供服务的起始状态。Leader服务器允许Follower再initLimit 时间内完成这个工作。
- syncLimit:Leader 与Follower心跳检测的最大延时时间
- dataDir:顾名思义就是 Zookeeper 保存数据的目录,默认情况下,Zookeeper 将写数据的日志文件也保存在这个目录里。
- clientPort:这个端口就是客户端连接 Zookeeper 服务器的端口,Zookeeper 会监听这个端口,接受客户端的访问请求。
- server.A=B:C:D:E 其中 A 是一个数字,表示这个是第几号服务器;B 是这个服务器的 ip 地址;C
表示的是这个服务器与集群中的 Leader 服务器交换信息的端口;D 表示的是万一集群中的 Leader
服务器挂了,需要一个端口来重新进行选举,选出一个新的
Leader,而这个端口就是用来执行选举时服务器相互通信的端口。如果是伪集群的配置方式,由于 B 都是一样,所以不同的 Zookeeper
实例通信端口号不能一样,所以要给它们分配不同的端口号。如果需要通过添加不参与集群选举以及事务请求的过半机制的
Observer节点,可以在E的位置,添加observer标识。
Step5:标识Server ID
创建四个文件夹/usr/local/zookeeper/apache-zookeeper-3.5.9-bin/data/zookeeper-1,/usr/local/zookeeper/apache-zookeeper-3.5.9-bin/data/zookeeper-2,/usr/local/zookeeper/apache-zookeeper-3.5.9-bin/data/zookeeper-3,/usr/local/zookeeper/apache-zookeeper-3.5.9-bin/data/zookeeper-4,在每个目录中创建文件myid 文件,写入当前实例的server id,即1,2,3,4
echo 1 > zookeeper-1/myid
echo 2 > zookeeper-2/myid
echo 3 > zookeeper-3/myid
echo 4 > zookeeper-4/myid
// ===========或者使用下面的方法创建===========================
cd /usr/local/zookeeper/apache-zookeeper-3.5.9-bin/data/zookeeper-1
vim myid
1
cd /usr/local/zookeeper/apache-zookeeper-3.5.9-bin/data/zookeeper-2
vim myid
2
cd /usr/local/zookeeper/apache-zookeeper-3.5.9-bin/data/zookeeper-3
vim myid
3
cd /usr/local/zookeeper/apache-zookeeper-3.5.9-bin/data/zookeeper-4
vim myid
4
Step6:启动四个zookeeper实例
/usr/local/zookeeper/apache-zookeeper-3.5.9-bin/bin/zkServer.sh start /usr/local/zookeeper/apache-zookeeper-3.5.9-bin/conf/zoo-1.cfg
/usr/local/zookeeper/apache-zookeeper-3.5.9-bin/bin/zkServer.sh start /usr/local/zookeeper/apache-zookeeper-3.5.9-bin/conf/zoo-2.cfg
/usr/local/zookeeper/apache-zookeeper-3.5.9-bin/bin/zkServer.sh start /usr/local/zookeeper/apache-zookeeper-3.5.9-bin/conf/zoo-3.cfg
/usr/local/zookeeper/apache-zookeeper-3.5.9-bin/bin/zkServer.sh start /usr/local/zookeeper/apache-zookeeper-3.5.9-bin/conf/zoo-4.cfg
Step7:检测集群状态,也可以直接用命令 zkServer.sh status conf/zoo1.cfg 进行每台服务的状态查询
/usr/local/zookeeper/apache-zookeeper-3.5.9-bin/bin/zkServer.sh status /usr/local/zookeeper/apache-zookeeper-3.5.9-bin/conf/zoo-1.cfg
......
/usr/local/zookeeper/apache-zookeeper-3.5.9-bin/bin/zkServer.sh status /usr/local/zookeeper/apache-zookeeper-3.5.9-bin/conf/zoo-4.cfg
可以看到节点的信息,2181是leader节点,2182和2183是follower节点,2184是observer节点。
因为ZK是使用java写的,也可以使用jps命令查看进程:
也可以通过 查看/zookeeper/config 节点数据来查看集群配置。
zk: 192.168.131.171:2181,192.168.131.171:2182,192.168.131.171:2183(CONNECTED) 1] get /zookeeper/config
2、登录集群客户端
可以连接集群或者只连接某一个服务。连接某一个服务的缺点是一旦这个服务宕机,客户端连接也将不可用。使用集群模式连接的话,只要leader和follower服务个数过半,就都可以连接。
过半指的是可以参与选举的服务个数要超过半数。如果此时leader加follower节点个数是3, 3除以2是1,没有达到过半,所以还需要加上1,即为2. 所以需要有2个服务是正常的,集群客户端才可用。
所以再集群中,推荐使用奇数台服务。因为偶数台服务会导致服务不可用的风险加高。比如3台服务,根据过半原则,只需要有2台服务正常就可用。如果是4台服务,根据过半原则,必须有3台服务都正常,服务才可用。但是如果是5台机器的话,也是只需要3台服务可用,就可以保证可用性。而且,服务个数越多,需要同步的节点也就越多,这样会影响性能。
bin/zkCli.sh -server ip1:port1,ip2:port2,ip3:port3
// 实例
/usr/local/zookeeper/apache-zookeeper-3.5.9-bin/bin/zkCli.sh -server 192.168.131.171:2181,192.168.131.171:2182,192.168.131.171:2183
二、zookeeper客户端连接集群
zk客户端支持连接集群和服务宕机后的重连机制。但是对于重连,需要在捕获异常后自处理:
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@Slf4j
public class ZkClusterTest {
private static final String CLUSTER_CONNECT_STR = "192.168.131.171:2181,192.168.131.171:2182,192.168.131.171:2183,192.168.131.171:2184";
private static final Integer SESSION_TIMEOUT = 60 * 1000;
private static ZooKeeper zooKeeper = null;
private static CountDownLatch countDownLatch = new CountDownLatch(1);
private static ObjectMapper objectMapper = new ObjectMapper();
// 初始化zk
@Before
public void init() throws IOException {
log.info("try to connect to zk server");
zooKeeper = new ZooKeeper(CLUSTER_CONNECT_STR, SESSION_TIMEOUT, new Watcher() {
@Override
public void process(WatchedEvent event) {
// 根据事件类型判断是否连接成功
if (event != null && event.getType().equals(Event.EventType.None)
&& event.getState().equals(Event.KeeperState.SyncConnected)) {
// 当连接建立好之后
countDownLatch.countDown();
log.info("zk 连接成功...");
}
}
});
}
@Test
public void testReconnect() throws InterruptedException {
// 不断的去拿这个节点的数据
while (true) {
Stat stat = new Stat();
try {
byte[] data = zooKeeper.getData("/testReconnect", false, stat);
log.info("get data: {}", new String(data));
// 每隔5s拉取一次数据
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {
log.error("数据获取异常: {}", e.getMessage());
log.info("开始重连...");
while (true) {
log.info("zookeeper status: {}", zooKeeper.getState().name());
if (zooKeeper.getState().isConnected()) {
break;
}
TimeUnit.SECONDS.sleep(3);
}
}
}
}
@After
public void holdOnzk() throws InterruptedException {
// 不要让程序结束. 然后我们在客户端中去修改数据
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
}
}
启动上面的测试方法testReconnect后,zk客户端会不断的去查询数据。这时候我们先确定出此时客户端连接的server是哪一个:
从上面的端口号2181这个连接,我们推测现在客户端连接的应该是2182的服务。其实从IDEA的日志中我们也看出来了,连接的是2181.
[main-SendThread(192.168.131.171:2181)]
那我们此时将2181这个服务停掉,然后再看IDEA中的日志:
// 2181对应的配置文件是zoo-1.cfg
/usr/local/zookeeper/apache-zookeeper-3.5.9-bin/bin/zkServer.sh stop /usr/local/zookeeper/apache-zookeeper-3.5.9-bin/conf/zoo-1.cfg
可以看到,当2181被管的时候,ZK客户端马上进行了重连,连接到了2182这个节点上!!!
此时我们再来看看这些服务的状态:
发现,2183已经被选举为新的leader节点了。
注意,并不是说客户端连接的一定是leader节点!连接到follower节点之后的写操作,会由这个follower节点转发到leader节点去执行,leader节点执行后,将数据同步到follower节点,并判断是否已经有半数的服务同步了这个数据。如果是,则将保存成功的消息发动到客户端。
ZK是基于sessionId连接的,只有sessin过期后,才无法访问访问临时数据。
三、Curator连接集群
ZK客户端连接集群后,需要自己的异常中去判断连接状态,代码不是很优雅。
不过我们可以使用Curator去连接集群。
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.state.ConnectionState;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Slf4j
public class CuratorClusterTest {
private static final String CLUSTER_CONNECT_STR = "192.168.131.171:2181,192.168.131.171:2182,192.168.131.171:2183,192.168.131.171:2184";
private static final Integer CONNECTION_TIMEOUT_MS = 5000;
private static CuratorFramework curatorFramework;
@Before
public void init () {
ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(5000, 30);
curatorFramework = CuratorFrameworkFactory.builder()
.connectString(CLUSTER_CONNECT_STR)
.sessionTimeoutMs(CONNECTION_TIMEOUT_MS)
.canBeReadOnly(true)
.retryPolicy(retryPolicy)
.build();
curatorFramework.getConnectionStateListenable().addListener((client, newState) -> {
if (newState == ConnectionState.CONNECTED) {
log.info("连接成功!");
}
});
log.info("连接中...");
curatorFramework.start();
}
@Test
public void testCluster() throws Exception {
String path = "/test-curator-cluster";
byte[] data = curatorFramework.getData().forPath(path);
log.info("get data: {}", new String(data));
// 不断的获取数据
while (true) {
try {
byte[] bytes = curatorFramework.getData().forPath(path);
log.info("get data 2 {}", new String(bytes));
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {
log.error("获取数据异常: {}", e.getMessage());
testCluster();
}
}
}
@After
public void holdOnzk() throws InterruptedException {
// 不要让程序结束. 然后我们在客户端中去修改数据
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
}
}
此时可以看到curator已经连接到了zk集群,目前连接到2184这个节点上。为了测试高可用,我们现在停掉2184.
/usr/local/zookeeper/apache-zookeeper-3.5.9-bin/bin/zkServer.sh stop /usr/local/zookeeper/apache-zookeeper-3.5.9-bin/conf/zoo-4.cfg
可以看到,curator客户端已经帮我们处理了重试,发现连接异常后重新连接到了2182这个节点上获取数据。
四、Zookeeper 3.5.0 新特性: 集群动态配置
Zookeeper 3.5.0 以前,Zookeeper集群角色要发生改变的话,只能通过停掉所有的Zookeeper服务,修改集群配置,重启服务来完成,这样集群服务将有一段不可用的状态,为了应对高可用需求,Zookeeper 3.5.0 提供了支持动态扩容/缩容的 新特性。
但是通过客户端API可以变更服务端集群状态是件很危险的事情,所以在zookeeper 3.5.3 版本要用动态配置,需要开启超级管理员身份验证模式 ACLs。如果是在一个安全的环境也可以通过配置 系统参数 -Dzookeeper.skipACL=yes 来避免配置维护acl 权限配置。
1、配置超级管理员
第一步,先配置一个超级管理员(如果不配管理员,也可以设置系统参数 -Dzookeeper.skipACL=yes):如:
在zookeeper启动脚本中添加 超级管理员授权模式:
// 生成一个加密的用户名为admin, 密码为admin的账号信息
// ======= 首先新加一个用户,并生成加密后的密文
[root@localhost bin]# echo -n user2:pass2 | openssl dgst -binary -sha1 | openssl base64
0RphDIvIZHLcNxLVT/hhi2uckEs=
// 将下面这个信息配置到zk的zkServer.sh文件中
-Dzookeeper.DigestAuthenticationProvider.superDigest=user2:0RphDIvIZHLcNxLVT/hhi2uckEs=
修改bin目录下的zkServer.sh文件:
使用上面生成的密文,将用户user2设置成一个super用户:
2、配置动态文件
修改配置 zoo1.cfg
注意这里去除了端口号,添加了
reconfigEnabled : 设置为true 开启动态配置
dynamicConfigFile : 指定动态配置文件的路径
3、创建文件 zoo_replicated1.cfg.dynamic
动态配置文件,加入集群信息
server.A=B.C.D.E;F
A: 服务的唯一标识
B: 服务对应的IP地址,
C: 集群通信端口
D: 集群选举端口
E: 角色, 默认是 participant,即参与过半机制的角色,选举,事务请求过半提交,还有一个是observer, 观察者,不参与选举以及过半机制。
之后是一个分号,一定是分号,
F:服务IP:端口
server.1=192.168.131.171:2001:3001:participant;192.168.131.171:2181
server.2=192.168.131.171:2002:3002:participant;192.168.131.171:2182
server.3=192.168.131.171:2003:3003:participant;192.168.131.171:2183
server.4=192.168.131.171:2004:3004:participant;192.168.131.171:2184
依次配置其他服务 zoo2.cfg ,zoo3.cfg注意数据文件的路径。
依次启动所有服务:
如: ./bin/zkServer.sh start conf/zoo1.cfg
查看集群状态:
./bin/zkServer.sh status conf/zoo1.cfg
连上任意一台服务器:
查看集群配置
config // 将会把动态配置打印出来
也可以直接查看目录
/zookeeper/config
该节点存储了集群信息
如果要修改集群状态,需要授权登录
addauth digest gj:123
reconfig -remove 3 // 移除serverId为 3 的机器
// 把对应的机器加进来
reconfig -add server.3=192.168.109.200:2003:3003:participant;192.168.109.200:2183
如果要变更/或者添加新的服务需要将服务加到配置文件 zoo_replicated1.cfg.dynamic 中,启动服务。
然后通过reconfig 命令进行添加或者变更服务角色,但是需要保证服务列表中 participant 角色能够形成集群(过半机制)。
客户端可以通过监听 /zookeeper/confg 节点,来感知集群的变化。从而实现集群的动态变更。
Zookeeper 类提供了对应的API 用来更新服务列表 : updateServerList
Watcher watcher = new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.None
&& event.getState() == Event.KeeperState.SyncConnected){
countDownLatch.countDown();
log.info(" 连接建立");
// start to watch config
try {
log.info(" 开始监听:{}",ZooDefs.CONFIG_NODE);
zookeeper.getConfig(true,null);
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else if( event.getPath()!=null && event.getPath().equals(ZooDefs.CONFIG_NODE)){
try {
byte[] config = zookeeper.getConfig(this, null);
String clientConfigStr = ConfigUtils.getClientConfigStr(new String(config));
log.info(" 配置发生变更: {}",clientConfigStr);
zookeeper.updateServerList(clientConfigStr.split(" ")[1]);
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
};
Curator 也自带了动态配置的监听,不需要额外的配置和代码实现监听更新;
五、问题
1、ZK没有实现分片,所有的数据在各个节点中都是一致的;