事件监听机制
zookeeper
提供了数据的发布/订阅
功能,多个订阅者可同时监听某一特定主题对象,当该主题对象的自身状态发生变化时例如节点内容改变、节点下的子节点列表改变等,会实时、主动通知所有订阅者zookeeper
采用了Watcher
机制实现数据的发布订阅功能。该机制在被订阅对象发生变化时会异步通知客户端,因此客户端不必在Watcher
注册后轮询阻塞,从而减轻了客户端压力watcher
机制事件上与观察者模式类似,也可看作是一种观察者模式在分布式场景下的实现方式
1.watcher架构
watcher
实现由三个部分组成
zookeeper
服务端zookeeper
客户端- 客户端的
ZKWatchManager对象
客户端首先将 Watcher
注册到服务端,同时将 Watcher
对象保存到客户端的watch
管理器中。当Zookeeper
服务端监听的数据状态发生变化时,服务端会主动通知客户端,接着客户端的 Watch
管理器会触发相关 Watcher
来回调相应处理逻辑,从而完成整体的数据 发布/订阅
流程
2.watcher特性
特性 | 说明 |
---|---|
一次性 | watcher 是一次性的,一旦被触发就会移除,再次使用时需要重新注册 |
客户端顺序回调 | watcher 回调是顺序串行执行的,只有回调后客户端才能看到最新的数据状态。一个watcher 回调逻辑不应该太多,以免影响别的watcher 执行 |
轻量级 | WatchEvent 是最小的通信单位,结构上只包含通知状态、事件类型和节点路径,并不会告诉数据节点变化前后的具体内容 |
时效性 | watcher 只有在当前session 彻底失效时才会无效,若在session 有效期内快速重连成功,则watcher 依然存在,仍可接收到通知; |
3.watcher接口设计
Watcher
是一个接口,任何实现了Watcher
接口的类就算一个新的Watcher
。Watcher
内部包含了两个枚举类:KeeperState
、EventType
4.Watcher通知状态(KeeperState)
KeeperState
是客户端与服务端连接状态发生变化时对应的通知类型。路径为org.apache.zookeeper.Watcher.EventKeeperState
,是一个枚举类,其枚举属性如下:
枚举属性 | 说明 |
---|---|
SyncConnected | 客户端与服务器正常连接时 |
Disconnected | 客户端与服务器断开连接时 |
Expired | 会话session 失效时 |
AuthFailed | 身份认证失败时 |
5.Watcher事件类型(EventType)
EventType
是数据节点znode
发生变化时对应的通知类型。EventType
变化时KeeperState
永远处于SyncConnected
通知状态下;当keeperState
发生变化时,EventType
永远为None
。其路径为org.apache.zookeeper.Watcher.Event.EventType
,是一个枚举类,枚举属性如下:
枚举属性 | 说明 |
---|---|
None | 无 |
NodeCreated | Watcher 监听的数据节点被创建时 |
NodeDeleted | Watcher 监听的数据节点被删除时 |
NodeDataChanged | Watcher 监听的数据节点内容发生更改时(无论数据是否真的变化) |
NodeChildrenChanged | Watcher 监听的数据节点的子节点列表发生变更时 |
- 注意:客户端接收到的相关事件通知中只包含状态以及类型等信息,不包含节点变化前后的具体内容,变化前的数据需业务自身存储,变化后的数据需要调用
get
等方法重新获取
6.捕获相应的事件
上面讲到zookeeper
客户端连接的状态和zookeeper
对znode
节点监听的事件类型,下面我们来讲解如何建立zookeeper
的watcher
监听。在zookeeper
中采用zk.getChildren(path,watch)、zk.exists(path,watch)、zk.getData(path,watcher,stat)
这样的方式来为某个znode
注册监听
下表以node-x
节点为例,说明调用的注册方法和可用监听事件间的关系:
注册方式 | created | childrenChanged | Changed | Deleted |
---|---|---|---|---|
zk.exists("/node-x",watcher) | 可监控 | 可监控 | 可监控 | |
zk.getData("/node-x",watcher) | 可监控 | 可监控 | ||
zk.getChildren("/node-x",watcher) | 可监控 | 可监控 |
7.注册watcher的方法
客户端与服务器端的连接状态
-
KeeperState
:通知状态 -
SyncConnected
:客户端与服务器正常连接时 -
Disconnected
:客户端与服务器断开连接时 -
Expired
:会话session
失效时 -
AuthFailed
:身份认证失败时 -
事件类型为:
None
public class ZkConnectionWatcher implements Watcher {
@Override
public void process(WatchedEvent watchedEvent) {
Event.KeeperState state = watchedEvent.getState();
if(state == Event.KeeperState.SyncConnected){
// 正常
System.out.println("正常连接");
}else if (state == Event.KeeperState.Disconnected){
// 可以用Windows断开虚拟机网卡的方式模拟
// 当会话断开会出现,断开连接不代表不能重连,在会话超时时间内重连可以恢复正常
System.out.println("断开连接");
}else if (state == Event.KeeperState.Expired){
// 没有在会话超时时间内重新连接,而是当会话超时被移除的时候重连会走进这里
System.out.println("连接过期");
}else if (state == Event.KeeperState.AuthFailed){
// 在操作的时候权限不够会出现
System.out.println("授权失败");
}
countDownLatch.countDown();
}
private static final String IP = "192.168.133.133:2181"
;
private static CountDownLatch countDownLatch = new CountDownLatch(1);
public static void main(String[] args) throws Exception {
// 5000为会话超时时间
ZooKeeper zooKeeper = new ZooKeeper(IP, 5000, new ZkConnectionWatcher());
countDownLatch.await();
// 模拟授权失败,用户名密码错误
zooKeeper.addAuthInfo("digest1","itcast1:123451".getBytes());
byte[] data = zooKeeper.getData("/hadoop", false, null);
System.out.println(new String(data));
TimeUnit.SECONDS.sleep(50);
}
}
8.watcher检查节点
exists:
-
exists(String path, boolean b)
//使用连接对象监视器 -
exists(String path, Watcher w)
//使用自定义监视器 -
NodeCreated
:节点创建 -
NodeDeleted
:节点删除 -
NodeDataChanged
:节点内容
public class EventTypeTest {
private static final String IP = "192.168.133.133:2181";
private static CountDownLatch countDownLatch = new CountDownLatch(1);
private static ZooKeeper zooKeeper;
// 采用zookeeper连接创建时的监听器
public static void exists1() throws Exception{
zooKeeper.exists("/watcher1",true);
}
// 自定义监听器
public static void exists2() throws Exception{
zooKeeper.exists("/watcher1",(WatchedEvent w) -> {
System.out.println("自定义" + w.getType());
});
}
// 演示使用多次的监听器
public static void exists3() throws Exception{
zooKeeper.exists("/watcher1", new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
try {
System.out.println("自定义的" + watchedEvent.getType());
} finally {
try {
zooKeeper.exists("/watcher1",this);
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
}
// 演示一节点注册多个监听器
public static void exists4() throws Exception{
zooKeeper.exists("/watcher1",(WatchedEvent w) -> {
System.out.println("自定义1" + w.getType());
});
zooKeeper.exists("/watcher1", new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
try {
System.out.println("自定义2" + watchedEvent.getType());
} finally {
try {
zooKeeper.exists("/watcher1",this);
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
}
// 测试
public static void main(String[] args) throws Exception {
zooKeeper = new ZooKeeper(IP, 5000, new ZKWatcher());
countDownLatch.await();
exists4();
TimeUnit.SECONDS.sleep(50);
}
static class ZKWatcher implements Watcher{
@Override
public void process(WatchedEvent watchedEvent) {
countDownLatch.countDown();
System.out.println("zk的监听器" + watchedEvent.getType());
}
}
}
getData:
getData(String path, boolean b, Stat stat)
getData(String path, Watcher w, Stat stat)
NodeDeleted
:节点删除NodeDataChange
:节点内容发生变化
getChildren:
getChildren(String path, boolean b)
getChildren(String path, Watcher w)
NodeChildrenChanged
:子节点发生变化NodeDeleted
:节点删除
配置中心案例
-
工作中有这样的一个场景:数据库用户名和密码信息放在一个配置文件中,应用读取该配置文件,配置文件信息放入缓存
-
若数据库的用户名和密码改变时候,还需要重新加载媛存,比较麻烦,通过
Zookeeper
可以轻松完成,当数据库发生变化时自动完成缓存同步 -
使用事件监听机制可以做出一个简单的配置中心
设计思路:
- 连接
zookeeper
服务器 - 读取
zookeeper
中的配置信息,注册watcher
监听器,存入本地变量 - 当
zookeeper
中的配置信息发生变化时,通过watcher
的回调方法捕获数据变化事件 - 重新获取配置信息
分布式唯一id案例
-
在过去的单库单表型系统中,通常第可以使用数据库字段自带的
auto_ increment
属性来自动为每条记录生成个唯一的ID
。但是分库分表后,就无法在依靠数据库的auto_ increment
属性来唯一标识一条记录了。此时我们就可以用zookeeper
在分布式环境下生成全局唯一ID
-
连接Zookeeper服务器
-
指定路径生成临时有序节点
-
取序列号即为分布式环境下的唯一ID
public class IdGenerate {
private static final String IP = "192.168.133.133:2181";
private static CountDownLatch countDownLatch = new CountDownLatch(1);
private static ZooKeeper zooKeeper;
public static String generateId() throws Exception {
//创建临时有序节点
return zooKeeper.create("/id", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
}
public static void main(String[] args) throws Exception {
zooKeeper = new ZooKeeper(IP, 5000, new ZKWatcher());
//阻塞程序等待创建成功
countDownLatch.await();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
for (int i = 0; i < 10; i++) {
threadPoolExecutor.execute(() -> {
try {
System.out.println(generateId());
} catch (Exception e) {
e.printStackTrace();
}
});
}
TimeUnit.SECONDS.sleep(50);
threadPoolExecutor.shutdown();
}
static class ZKWatcher implements Watcher {
@Override
public void process(WatchedEvent watchedEvent) {
countDownLatch.countDown();
System.out.println("zk的监听器" + watchedEvent.getType());
}
}
}
分布式锁
分布式锁有多种实现方式,比如通过数据库、redis都可实现。作为分布式协同工具Zookeeper
,当然也有着标准的实现方式。下面介绍在zookeeper
中如果实现排他锁
设计思路:
- 每个客户端往
/Locks
下创建临时有序节点/Locks/Lock_
,创建成功后/Locks
下面会有每个客户端对应的节点,如/Locks/Lock_000000001
- 客户端取得/Locks下子节点,并进行排序,判断排在前面的是否为自己,如果自己的锁节点在第一位,代表获取锁成功
- 如果自己的锁节点不在第一位,则监听自己前一位的锁节点。例如,自己锁节点
Lock_000000002
,那么则监听Lock_000000001
- 当前一位锁节点
(Lock_000000001)
对应的客户端执行完成,释放了锁,将会触发监听客户端(Lock_000000002)
的逻辑 - 监听客户端重新执行第
2
步逻辑,判断自己是否获得了锁
// 线程测试类
public class ThreadTest {
public static void delayOperation(){
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static interface Runable{
void run();
}
public static void run(Runable runable,int threadNum){
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(30, 30,
0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
for (int i = 0; i < threadNum; i++) {
threadPoolExecutor.execute(runable::run);
}
threadPoolExecutor.shutdown();
}
public static void main(String[] args) {
// DistributedLock distributedLock = new DistributedLock();
// distributedLock.acquireLock();
// delayOperation();
// distributedLock.releaseLock();
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 每秒打印信息
run(() -> {
for (int i = 0; i < 999999999; i++) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
String format = dateTimeFormatter.format(LocalDateTime.now());
System.out.println(format);
}
},1);
// 线程测试
run(() -> {
DistributedLock distributedLock = new DistributedLock();
distributedLock.acquireLock();
delayOperation();
distributedLock.releaseLock();
},30);
}
}
public class DistributedLock {
private String IP = "192.168.133.133:2181";
private final String ROOT_LOCK = "/Root_Lock";
private final String LOCK_PREFIX = "/Lock_";
private final CountDownLatch countDownLatch = new CountDownLatch(1);
private final byte[] DATA = new byte[0];
private ZooKeeper zookeeper;
private String path;
private void init(){
// 初始化
try {
zookeeper = new ZooKeeper(IP, 200000, w -> {
if(w.getState() == Watcher.Event.KeeperState.SyncConnected){
System.out.println("连接成功");
}
countDownLatch.countDown();
});
countDownLatch.await();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
// 暴露的外部方法,主逻辑
public void acquireLock(){
init();
createLock();
attemptLock();
}
// 暴露的外部方法,主逻辑
public void releaseLock(){
try {
zookeeper.delete(path,-1);
System.out.println("锁释放了" + path);
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
private void createLock(){
try {
// 创建一个目录节点,不存在则创建。
Stat root = zookeeper.exists(ROOT_LOCK, false);
if(root == null)
zookeeper.create(ROOT_LOCK, DATA, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
// 目录下创建子节点,临时有序节点
path = zookeeper.create(ROOT_LOCK + LOCK_PREFIX, DATA, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
private Watcher watcher = new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
if (watchedEvent.getType() == Event.EventType.NodeDeleted){
synchronized (this){
this.notifyAll();
}
}
}
};
private void attemptLock(){
try {
// 获取正在排队的节点,由于是zookeeper生成的临时节点,不会出错,这里不能加监视器
// 因为添加了监视器后,任何子节点的变化都会触发监视器
List<String> nodes = zookeeper.getChildren(ROOT_LOCK,false);
nodes.sort(String::compareTo);
// 获取自身节点的排名
int ranking = nodes.indexOf(path.substring(ROOT_LOCK.length() + 1));
// 已经是最靠前的节点了,获取锁
if(ranking == 0){
return;
}else {
// 并不是靠前的锁,*监视*自身节点的前一个节点
Stat status = zookeeper.exists(ROOT_LOCK+"/"+nodes.get(ranking - 1), watcher);
// 有可能这这个判断的瞬间,0号完成了操作(此时我们应该判断成功自旋才对),但是上面的status变量已经获取了值并且不为空,1号沉睡
// 但是,请注意自行测试,虽然1号表面上沉睡了,但是实际上watcher.wait()是瞬间唤醒的
if(status == null){
attemptLock();
}else {
synchronized (watcher){
watcher.wait();
}
attemptLock();
}
}
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
}
集群搭建
说明:对于复制模式,至少需要三个服务器,并且强烈建议您使用奇数个服务器。如果只有两台服务器,那么您将处于一种情况,如果其中一台服务器发生故障,则没有足够的计算机构成多数仲裁(zk
采用的是过半数仲裁。因此,搭建的集群要容忍n个节点的故障,就必须有2n+1
台计算机,这是因为宕掉n台后,集群还残余n+1
台计算机,n+1
台计算机中必定有一个最完整最接近leader
的follower
,假如宕掉的n台都是有完整信息的,剩下的一台就会出现在残余的zk
集群中。也就是说:zk
为了安全,必须达到多数仲裁,否则没有leader
,集群失败,具体体现在**leader
选举-章**)。由于存在两个单点故障,因此两个服务器还不如单个服务器稳定。
多数仲裁的设计是为了避免脑裂(zk,已经采用了多数仲裁,所以不会出现),和数据一致性的问题
- 脑裂:由于网络延迟等各种因素,最终导致集群一分为二,各自独立运行(两个
leader
),集群就是坏的 - 如果有两台服务器,两台都认为另外的
zk
宕掉,各自成为leader
运行(假设可以,实际上选不出leader
,可以实际搭建一个集群,看看一台zk是否能够成功集群,详见**leader
选举**),就会导致数据不一致。 - 如果有三台服务器,一台因为网络分区,无法连接,剩下两台网络正常,选举出了
leader
,集群正常
tickTime=2000
dataDir=/var/lib/zookeeper
clientPort=2181
initLimit=5
syncLimit=2
server.1=zoo1:2888:3888 # 这是多机部署
server.2=zoo2:2888:3888
server.3=zoo3:2888:3888
- 新的键值**
initLimit
是zookeeper
用于限制选举中zookeeper
服务连接到leader
的时间,syncLimit
**限制服务器与leader
的过期时间 - 对于这两个超时,您都可以使用tickTime指定时间单位。在此示例中,
initLimit
的超时为5个滴答声,即2000
毫秒/滴答声,即10
秒 - 表格*
server.X
的条目列出了组成ZooKeeper
服务的服务器。服务器启动时,它通过在数据目录中查找文件myid
*来知道它是哪台服务器。该文件包含ASCII
的服务器号。 - 最后,记下每个服务器名称后面的两个端口号:
“ 2888”
和“ 3888”
。对等方使用前一个端口连接到其他对等方。这种连接是必需的,以便对等方可以进行通信,例如,以商定更新顺序。更具体地说,ZooKeeper
服务器使用此端口将follower
连接到leader
。当出现新的leader
者时,follower
使用此端口打开与leader
的TCP
连接。因为默认的leader
选举也使用TCP
,所以我们当前需要另一个端口来进行leader
选举。这是第二个端口。
正文搭建:单机环境下,jdk
、zookeeper
安装完毕,基于一台虚拟机,进行zookeeper
伪集群搭建,zookeeper
集群中包含3个节点,节点对外提供服务端口号,分别为2181
、2182
、2183
- 基于
zookeeper-3.4.10
复制三份zookeeper
安装好的服务器文件,目录名称分别为zookeeper2181
、zookeeper2182
、zookeeper2183
cp -r zookeeper-3.4.10 zookeeper2181
cp -r zookeeper-3.4.10 zookeeper2182
cp -r zookeeper-3.4.10 zookeeper2183
# cp -r zookeeper-3.1.10 ./zookeeper218{1..3}
- 修改
zookeeper2181
服务器对应配置文件
# 服务器对应端口号
clientPort=2181
# 数据快照文件所在路径
dataDir=/opt/zookeeper2181/data
# 集群配置信息
# server.A=B:C:D
# A:是一个数字,表示这个是服务器的编号
# B:是这个服务器的ip地址
# C:Zookeeper服务器之间通信的端口(数据互通,必须的)
# D:Leader选举的端口
server.1=192.168.133.133:2287:3387 # 这是伪集群部署,注意端口号
server.2=192.168.133.133:2288:3388
server.3=192.168.133.133:2289:3389
# 对,这些都是2181的配置文件
- 在上一步
dataDir
指定的目录下,创建myid
文件,然后在该文件添加上一步server
配置的对应A
数字
# zookeeper2181对应的数字为1
# /opt/zookeeper2181/data目录(即dataDir的目录下)下执行命令,就是把1写入文件
echo "1" > myid
zookeeper2182、2183
参照2/3进行相应配置- 分别启动三台服务器,检验集群状态
检查:cd
进入bin
目录./zkServer status
登录命令:
./zkCli.sh -server 192.168.60.130:2181
./zkCli.sh -server 192.168.60.130:2182
./zkCli.sh -server 192.168.60.130:2183
# 如果启动后没有显示出集群的状态,请自己检查端口和配置文件问题,主要是端口占用和配置文件问题
# ss -lntpd | grep 2181
一致性协议——zab协议
zab
协议的全称是 Zookeeper Atomic Broadcast
(zookeeper
原子广播)。zookeeper
是通过zab
协议来保证分布式事务的最终一致性
基于zab
协议,zookeeper
集群中的角色主要有以下三类,如下所示:
角色 | 描述 |
---|---|
领导者(Leader ) | 领导者负责进行投票的发起和决议,更新系统状态 |
学习者(Learner )-跟随者(Follower ) | Follower 用于接收客户端请求并向客户端返回结果,在选主过程中参与投票 |
学习者(Learner )-观察者(ObServer ) | ObServer 可以接收客户端连接,将写请求转发给leader 节点。但ObServer 不参加投票过程,只同步leader 的状态。ObServer 的目的是为了扩展系统,提高读取速度 |
客户端(Client ) | 请求发起方 |
·zab
广播模式工作原理,通过类似两端式提交协议的方式解决数据一致性:
leader
从客户端收到一个写请求leader
生成一个新的事务并为这个事务生成一个唯一的ZXID
leader
将事务提议(propose
)发送给所有的follows
节点follower
节点将收到的事务请求加入到本地历史队列(history queue
)中,并发送ack
给leader
,表示确认提议- 当
leader
收到大多数follower
(半数以上节点)的ack(acknowledgement)
确认消息,leader
会本地提交,并发送commit
请求 - 当
follower
收到commit
请求时,从历史队列中将事务请求commit
leader选举
服务器状态
looking
:寻找leader
状态。当服务器处于该状态时,它会认为当前集群中没有leader
,因此需要进入leader
选举状态following
:跟随着状态。表明当前服务器角色是follower
observing
:观察者状态。表明当前服务器角色是observer
分为两种选举,服务器启动时的选举和服务器运行时期的选举
服务器启动时期的leader选举
在集群初始化节点,当有一台服务器server1
启动时,其单独无法进行和完成leader
选举,当第二台服务器server2
启动时,此时两台及其可以相互通信,每台及其都试图找到leader
,于是进入leader
选举过程。选举过程如下:
-
每个
server
发出一个投票。由于是初始状态,server1
和server2
都会将自己作为leader
服务器来进行投票,每次投票都会包含所推举的myid
和zxid
,使用(myid,zxid
),此时server1
的投票为(1,0),server2
的投票为(2,0),然后各自将这个投票发给集群中的其它机器 -
集群中的每台服务器都接收来自集群中各个服务器的投票
-
处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行pk,规则如下
-
优先检查
zxid
。zxid
比较大的服务器优先作为leader
(zxid
较大者保存的数据更多) -
如果
zxid
相同。那么就比较myid
。myid
较大的服务器作为leader
服务器
对于Server1
而言,它的投票是(1,0),接收Server2
的投票为(2,0),首先会比较两者的zxid
,均为0,再比较myid
,此时server2
的myid
最大,于是更新自己的投票为(2,0),然后重新投票,对于server2而言,无需更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可
-
-
统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,对于
server1、server2
而言,都统计出集群中已经有两台机器接受了(2,0)的投票信息,此时便认为已经选举出了leader
-
改变服务器状态。一旦确定了
leader
,每个服务器就会更新自己的状态,如果是follower
,那么就变更为following
,如果是leader
,就变更为leading
举例:如果我们有三个节点的集群,1,2,3,启动 1 和 2 后,2 一定会是 leader
,3 再加入不会进行选举,而是直接成为follower
—— 仔细观察 一台zk
无法集群,没有leader
服务器运行时期选举
在zookeeper
运行期间,leader
与非leader
服务器各司其职,即使当有非leader
服务器宕机或者新加入,此时也不会影响leader
,但是一旦leader
服务器挂了,那么整个集群将暂停对外服务,进入新一轮leader
选举,其过程和启动时期的leader
选举过程基本一致
假设正在运行的有server1
、server2
、server3
三台服务器,当前leader
是server2
,若某一时刻leader
挂了,此时便开始Leader
选举。选举过程如下
- 变更状态。
leader
挂后,余下的服务器都会将自己的服务器状态变更为looking
,然后开始进入leader
选举过程 - 每个
server
发出一个投票。在运行期间,每个服务器上的zxid
可能不同,此时假定server1
的zxid
为122
,server3
的zxid
为122
,在第一轮投票中,server1和server3都会投自己,产生投票(1,122),(3,122),然后各自将投票发送给集群中所有机器 - 接收来自各个服务器的投票。与启动时过程相同
- 处理投票。与启动时过程相同,此时,
server3
将会成为leader
- 统计投票。与启动时过程相同
- 改变服务器的状态。与启动时过程相同
observer角色及其配置
-
尽管
ZooKeeper
通过使用客户端直接连接到该集合的投票成员表现良好,但是此体系结构使其很难扩展到大量客户端。问题在于,随着我们添加更多的投票成员,写入性能会下降。这是由于以下事实:写操作需要(通常)集合中至少一半节点的同意,因此,随着添加更多的投票者,投票的成本可能会显着增加。 -
我们引入了一种称为Observer的新型
ZooKeeper
节点,该节点有助于解决此问题并进一步提高ZooKeeper
的可伸缩性。观察员是合法的非投票成员,他们仅听取投票结果,而听不到投票结果。除了这种简单的区别之外,观察者的功能与跟随者的功能完全相同-客户端可以连接到观察者,并向其发送读写请求。观察者像追随者一样将这些请求转发给领导者,但是他们只是等待听取投票结果。因此,我们可以在不影响投票效果的情况下尽可能增加观察员的数量。 -
观察者还有其他优点。因为他们不投票,所以它们不是
ZooKeeper
选举中的关键部分。因此,它们可以在不损害ZooKeeper
服务可用性的情况下发生故障或与群集断开连接。给用户带来的好处是,观察者可以通过比跟随者更不可靠的网络链接进行连接。实际上,观察者可用于与另一个数据中心的ZooKeeper
服务器进行对话。观察者的客户端将看到快速读取,因为所有读取均在本地提供,并且由于缺少表决协议而需要的消息数量较小,因此写入会导致网络流量最小
ovserver
角色特点:
- 不参与集群的
leader
选举 - 不参与集群中写数据时的
ack
反馈
为了使用observer
角色,在任何想变成observer
角色的配置文件中加入如下配置:
peerType=observer
并在所有server
的配置文件中,配置成observer
模式的server
的那行配置追加:observer
,例如
server.1=192.168.133.133:2287:3387 # 注意端口号
server.2=192.168.133.133:2288:3388
server.3=192.168.133.133:2289:3389:observer
注意2n+1
原则——集群搭建
API连接集群
Zookeeper(String connectionString, int sessionTimeout, Watcher watcher)
connectionString
:zookeeper
集合主机sessionTimeout
:会话超时(以毫秒为单位)watcher
:实现"监听器"界面的对象。zookeeper
集合通过监视器对象返回连接状态
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(1);
ZooKeeper connection = new ZooKeeper("192.168.133.133:2181,192.168.133.133:2182,192.168.133.133:2183", 5000, watchedEvent -> {
if (watchedEvent.getState() == Watcher.Event.KeeperState.SyncConnected)
System.out.println("连接成功");
countDownLatch.countDown();
});
countDownLatch.await();
connection.create("/hadoop",new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
System.out.println(connection.getSessionId());
}