分布式协调服务框架——zookeeper

文章内容输出来源:拉勾教育大数据高薪训练营

第一部分 zookeeper简介

1.1 zookeeper是什么?

Zookeeper 是一个分布式协调服务的开源框架。 主要用来解决分布式集群中应用系统的一致性问题, 例如怎样避免同时操作同⼀数据造成脏读的问题。分布式系统中数据存在一致性的问题!!

  • ZooKeeper本质上是一个分布式的小文件存储系统。 提供基于类似于文件系统的目录树方式的数据存储,并且可以对树中的节点进行有效管理理。
  • ZooKeeper提供给客户端监控存储在zk内部数据的功能,从而可以达到基于数据的集群管理。 诸如: 统一命名服务(dubbo)分布式配置管理(solr的配置集中管理理)、分布式消息队列 (sub/pub)、分布式锁分布式协调等功能。

简而言之,zookeeper=文件系统+通知机制。

1.2 zookeeper的架构组成

image.png

Leader

  • Zookeeper 集群工作的核⼼角⾊
  • 集群内部各个服务器的调度者
  • 事务请求(写操作) 的唯一调度和处理者,保证集群事务处理的顺序性;对于 create, setData, delete 等有写操作的请求,则需要统一转发给leader 处理, leader 需要决定编号、执行操作,这个过程称为一个事务

Follower

  • 处理客户端非事务(读操作) 请求, 转发事务请求给Leader
  • 参与集群 Leader 选举投票 2n-1台可以做集群投票。

此外,针对访问量比较大的 zookeeper 集群, 还可新增观察者⻆色。

Observer

  • 观察者⻆色,观察 Zookeeper 集群的最新状态变化并将这些状态同步过来,其对于⾮事务请求可以进行独立处理,对于事务请求,则会转发给 Leader服务器进行处理。
  • 不会参与任何形式的投票只提供非事务服务,通常用于在不影响集群事务处理能力的前提下提升集群的⾮事务处理能力。增加了集群增加并发的读请求。

ZK也是Master/slave架构,但是与之前不同的是zk集群中的Leader不是指定而来,而是通过选举产生。

1.3 zookeeper的特点

  1. Zookeeper:一个领导者(leader:⽼大),多个跟随者(follower:小弟)组成的集群。
  2. Leader负责进行投票的发起和决议,更新系统状态。
  3. Follower⽤于接收客户请求并向客户端返回结果,在选举Leader过程中参与投票。
  4. 集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。
  5. 全局数据一致:每个server保存⼀份相同的数据副本,Client无论连接到哪个server,数据都是⼀致的。
  6. 更新请求顺序进行。
  7. 数据更新原⼦性,⼀次数据更新要么成功,要么失败。

第二部分 zookeeper环境搭建

2.1 zookeeper的搭建方式

Zookeeper安装⽅式有三种,单机模式集群模式以及伪集群模式

■ 单机模式:Zookeeper只运行在一台服务器上,适合测试环境;

■ 伪集群模式:就是在⼀台服务器上运⾏多个Zookeeper 实例;

■ 集群模式:Zookeeper运行于一个集群上,适合生产环境,这个计算机集群被称为一个“集合体”

2.2 zookeeper集群搭建

下载

⾸先我们下载稳定版本的zookeeper:http://zookeeper.apache.org/releases.html

上传

下载完成后,将zookeeper压缩包 zookeeper-3.4.14.tar.gz上传到linux系统/opt/lagou/software 解压缩包

tar -zxvf zookeeper-3.4.14.tar.gz -C ../servers/

修改配置⽂件创建data与log⽬录

#创建zk存储数据目录
mkdir -p /opt/lagou/servers/zookeeper-3.4.14/data 
#创建zk⽇志文件目录
mkdir -p /opt/lagou/servers/zookeeper-3.4.14/data/logs 
#修改zk配置文件
cd /opt/lagou/servers/zookeeper-3.4.14/conf
#⽂件改名
mv zoo_sample.cfg zoo.cfg

vim zoo.cfg
#更新datadir
dataDir=/opt/lagou/servers/zookeeper-3.4.14/data
#增加logdir 
dataLogDir=/opt/lagou/servers/zookeeper-3.4.14/data/logs 
#增加集群配置
##server.服务器ID=服务器IP地址:服务器之间通信端口:服务器之间投票选举端口
server.1=linux121:2888:3888
server.2=linux122:2888:3888
server.3=linux123:2888:3888
#打开注释 
#ZK提供了了自动清理理事务日志和快照⽂件的功能,这个参数指定了清理频率,单位是小时
autopurge.purgeInterval=1

添加myid配置

在zookeeper的 data 目录下创建一个 myid文件,内容为1,这个⽂件就是记录每个服务器的ID

cd /opt/lagou/servers/zookeeper-3.4.14/data
echo 1 > myid

安装包分发并修改myid的值

# 安装包分发
rsync-script /opt/lagou/servers/zookeeper-3.4.14
# 修改myid值 linux122
echo 2 >/opt/lagou/servers/zookeeper-3.4.14/data/myid
# 修改myid值 linux123
echo 3 >/opt/lagou/servers/zookeeper-3.4.14/data/myid

依次启动三个zk实例(三个节点都执行启动命令)

/opt/lagou/servers/zookeeper-3.4.14/bin/zkServer.sh start

查看启动情况

/opt/lagou/servers/zookeeper-3.4.14/bin/zkServer.sh status

zk集群启动停止脚本

vim zk.sh

#!/bin/sh
echo "start zookeeper server..."
if(($#==0));then
echo "no params";
exit;
fi

hosts="linux121 linux122 linux123"
for host in $hosts
do
ssh $host "source /etc/profile; /opt/lagou/servers/zookeeper-3.4.14/bin/zkServer.sh $1"
done

第三部分 zookeeper数据结构与监听机制

ZooKeeper数据模型Znode

在ZooKeeper中,数据信息被保存在⼀个个数据节点上,这些节点被称为znode。ZNode 是 Zookeeper 中最⼩数据单位,在 ZNode 下面又可以再挂 ZNode,这样⼀层层下去就形成了一个层次化命名空间ZNode树,我们称为ZNode Tree,它采⽤了类似⽂件系统的层级树状结构进行管理。见下图示例:

  image.png

在Zookeeper中,每⼀个数据节点都是一个ZNode,上图根⽬录下有两个节点,分别是app1 和 app2,其中 app1 下⾯又有三个子节点,所有ZNode按层次化进行组织,形成这么⼀颗树,ZNode的节点路径标识⽅式和Unix文件系统路径非常相似,都是由⼀系列使用斜杠(/)进行分割的路径表示,开发人员可以向这个节点写⼊数据,也可以在这个节点下面创建子节点。

3.1 ZNode的类型

Zookeeper 节点类型可以分为三大类:

  • 持久性节点(Persistent)
  • 临时性节点(Ephemeral)
  • 顺序性节点(Sequential)

在开发中在创建节点的时候通过组合可以生成以下四种节点类型:持久节点、持久顺序节点、临时节点、临时顺序节点。

不同类型的节点则会有不同的生命周期。

持久节点:是Zookeeper中最常见的⼀种节点类型,所谓持久节点,就是指节点被创建后会一直存在服务器,直到删除操作主动清除。

持久顺序节点:就是有顺序的持久节点,节点特性和持久节点是一样的,只是额外特性表现在顺序上。 顺序特性实质是在创建节点的时候,会在节点名后面加上⼀个数字后缀,来表示其顺序。

临时节点:就是会被⾃动清理掉的节点,它的生命周期和客户端会话绑在一起,客户端会话结束,节点会被删除掉。与持久性节点不同的是,临时节点不能创建⼦节点。

临时顺序节点:就是有顺序的临时节点,和持久顺序节点相同,在其创建的时候会在名字后面加上数字后缀。

 

事务ID

⾸先,先了解,事务是对物理和抽象的应用状态上的操作集合。往往在现在的概念中,狭义上的事务通常指的是数据库事务,⼀般包含了一系列对数据库有序的读写操作,这些数据库事务具有所谓的ACID特性,即原子性(Atomic)、⼀致性(Consistency)、隔离性(Isolation)和持久性(Durability)。

而在ZooKeeper中,事务是指能够改变ZooKeeper服务器状态的操作,我们也称之为事务操作或更新操作,一般包括数据节点创建与删除、数据节点内容更新等操作。对于每一个事务请求,ZooKeeper都会为其分配一个全局唯一的事务ID,⽤ZXID来表示,通常是一个64位的数字。每一个ZXID 对应一次更新操作,从这些ZXID中可以间接地识别出ZooKeeper处理这些更新操作请求的全局顺序

zk中的事务指的是对zk服务器状态改变的操作(create,update data,更新字节点);zk对这些事务操作都会编号,这个编号是⾃增长的被称为ZXID。

3.2 ZNode的状态信息

#使用bin/zkCli.sh 连接到zk集群
[zk: localhost:2181(CONNECTED) 2] get /zookeeper

cZxid = 0x0
ctime = Wed Dec 31 19:00:00 EST 1969
mZxid = 0x0
mtime = Wed Dec 31 19:00:00 EST 1969
pZxid = 0x0
cversion = -1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1

整个 ZNode 节点内容包括两部分:节点数据内容和节点状态信息。数据内容是空,其他的属于状态信息。

cZxid 就是 Create ZXID,表示节点被创建时的事务ID。
ctime 就是 Create Time,表示节点创建时间。
mZxid 就是 Modified ZXID,表示节点最后一次被修改时的事务ID。
mtime 就是 Modified Time,表示节点最后⼀次被修改的时间。
pZxid 表示该节点的子节点列表最后⼀次被修改时的事务 ID。只有子节点列表变更才会更新 pZxid,子节点内容变更不会更新。
cversion 表示子节点的版本号。
dataVersion 表示内容版本号。
aclVersion 标识acl版本
ephemeralOwner 表示创建该临时节点时的会话 sessionID,如果是持久性节点那么值为 0 
dataLength 表示数据⻓长度。
numChildren 表示直系⼦节点数。

3.3 Watcher机制

Zookeeper使用Watcher机制实现分布式数据的发布/订阅功能

一个典型的发布/订阅模型系统定义了一种一对多的订阅关系,能够让多个订阅者同时监听某一个主题对象,当这个主题对象⾃身状态变化时,会通知所有订阅者,使它们能够做出相应的处理。

在 ZooKeeper 中,引⼊了 Watcher 机制来实现这种分布式的通知功能。ZooKeeper 允许客户端向服务端注册一个 Watcher 监听,当服务端的⼀些指定事件触发了这个Watcher,那么Zk就会向指定客户端发送⼀个事件通知来实现分布式的通知功能。

整个Watcher注册与通知过程如图所示。

image.png

Zookeeper的Watcher机制主要包括客户端线程、客户端WatcherManager、Zookeeper服务器三部分。

具体⼯作流程为:

  • 客户端在向Zookeeper服务器注册的同时,会将Watcher对象存储在客户端的WatcherManager当中
  • 当Zookeeper服务器触发Watcher事件后,会向客户端发送通知
  • 客户端线程从WatcherManager中取出对应的Watcher对象来执行回调逻辑

第四部分 zookeeper的基本使用

4.1 zookeeper命令行操作

⾸先,进⼊到zookeeper的bin⽬录:cd /opt/lagou/servers/zookeeper-3.4.14/bin/

# 连接本地的zookeeper服务器
./zkcli.sh
# 连接指定的服务器
./zkCli.sh -server ip:port(2181)

连接成功之后,系统会输出Zookeeper的相关环境及配置信息等信息。输入help之后,屏幕会输出可用的Zookeeper命令,如下图所示

image.png

创建节点

create [-s][-e] path data

-s或-e分别指定节点特性,顺序或临时节点,若不指定,则创建持久节点。path为节点路径名称,data为节点内容。

  1. 创建顺序节点
[zk: localhost:2181(CONNECTED) 6] create -s /zk-test 123
Created /zk-test0000000002
[zk: localhost:2181(CONNECTED) 7] ls /
[zookeeper, zk-test0000000002, hadoop-ha]
  1. 创建临时节点
[zk: localhost:2181(CONNECTED) 8] create -e /zk-temp 123
Created /zk-temp
[zk: localhost:2181(CONNECTED) 9] ls /
[zookeeper, zk-test0000000002, hadoop-ha, zk-temp]

临时节点在客户端会话结束后,就会⾃动删除。使用quit退出客户端再重新连接客户端后会发现zk-temp节点被删除了。

  1. 创建永久节点
[zk: localhost:2181(CONNECTED) 1] create /zk-persistent 123
Created /zk-persistent
[zk: localhost:2181(CONNECTED) 2] ls /
[zk-persistent, zookeeper, zk-test0000000002, hadoop-ha]

读取节点

与读取相关的命令有ls命令和get命令

ls命令可以列出Zookeeper指定节点下的所有子节点,但只能查看指定节点下的第一级的所有子节点。

ls path

get命令可以获取Zookeeper指定节点的数据内容和属性信息。

get path

例如:

[zk: localhost:2181(CONNECTED) 3] get /zk-persistent
123
cZxid = 0x200000009
ctime = Wed Jul 29 02:19:31 PDT 2020
mZxid = 0x200000009
mtime = Wed Jul 29 02:19:31 PDT 2020
pZxid = 0x200000009
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 3
numChildren = 0

更新节点

使用set命令,可以更新指定节点的数据内容,⽤法如下

set path data

[zk: localhost:2181(CONNECTED) 4] set /zk-persistent 456
cZxid = 0x200000009
ctime = Wed Jul 29 02:19:31 PDT 2020
mZxid = 0x20000000a
mtime = Wed Jul 29 02:26:15 PDT 2020
pZxid = 0x200000009
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 3
numChildren = 0

删除节点

使用delete命令可以删除Zookeeper上的指定节点,用法如下

delete path

[zk: localhost:2181(CONNECTED) 5] delete /zk-persistent
[zk: localhost:2181(CONNECTED) 6] ls /
[zookeeper, zk-test0000000002, hadoop-ha]

可以看到,已经成功删除/zk-persistent节点。值得注意的是,若删除节点存在子节点,那么无法删除该节点,必须先删除子节点,再删除父节点

4.2 zookeeper开源客户端

ZkClient

ZkClient是Github上一个开源的zookeeper客户端,在Zookeeper原⽣API接口之上进行了包装,是⼀个更易用的Zookeeper客户端,同时,zkClient在内部还实现了诸如Session超时重连、Watcher反复注册等功能

接下来,还是从创建会话、创建节点、读取数据、更新数据、删除节点等方面来介绍如何使用zkClient这个zookeeper客户端

添加依赖:

在pom.xml⽂件中添加如下内容

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.14</version>
</dependency>
<dependency>
    <groupId>com.101tec</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.2</version>
</dependency>

1.创建会话

@Test
public void createSession(){
    ZkClient zkClient = new ZkClient("linux121:2181");
    System.out.println("zookeeper client created.");
}

2.创建节点

@Test
public void createNode(){
    ZkClient zkClient = new ZkClient("linux121:2181");
    zkClient.create("/test", "test", CreateMode.EPHEMERAL);
    zkClient.createEphemeral("/test2");
    zkClient.createEphemeralSequential("/test3", "test3");
    zkClient.createPersistent("/persistent-test", "123");
}

ZkClient通过设置createParents参数为true可以递归的先创建⽗节点,再创建子节点。

3.删除节点

@Test
public void deleteNode(){
    ZkClient zkClient = new ZkClient("linux121:2181");
    zkClient.delete("/persistent-test");
    // 递归删除
    zkClient.deleteRecursive("/persistent-test");
}

4.监听节点变化

@Test
public void watchNodeChange() throws InterruptedException {
    ZkClient zkClient = new ZkClient("linux121:2181");
    zkClient.subscribeChildChanges("/persistent-test", new IZkChildListener() {
        // 该方法执行收到通知后的业务逻辑
        public void handleChildChange(String path, List<String> childs) throws Exception {
            System.out.println("childs change, current childs" + childs);
        }
    });
    zkClient.createPersistent("/persistent-test");
    Thread.sleep(1000); //只是为了方便观察结果数据
    zkClient.createPersistent("/persistent-test/c1");
    Thread.sleep(1000);
    zkClient.delete("/persistent-test/c1");
    Thread.sleep(1000);
    zkClient.delete("/persistent-test");
    Thread.sleep(Integer.MAX_VALUE);
}

运行结果

childs change, current childs[]
childs change, current childs[c1]
childs change, current childs[]
childs change, current childsnull
  • 监听器可以对不存在的目录进行监听
  • 监听⽬录下⼦节点发⽣改变,可以接收到通知,携带数据有⼦节点列表
  • 监听⽬录创建和删除本身也会被监听到

5.监听数据变化

@Test
public void watchDataChange() throws InterruptedException {
    ZkClient zkClient = new ZkClient("linux121:2181");
    //zkClient.setZkSerializer(xxx); //设置自定义的序列化类型
    boolean exists = zkClient.exists("/test");
    if (!exists){
        zkClient.createEphemeral("/test","123");
    }
    zkClient.subscribeDataChanges("/test", new IZkDataListener() {
        public void handleDataChange(String path, Object data) throws Exception {
            //定义接收通知之后的处理逻辑
            System.out.println(path + " data is changed ,new data " +data);
        }

        public void handleDataDeleted(String path) throws Exception {
            // 数据删除、节点删除时执行的业务逻辑
            System.out.println(path + " is deleted!!");
        }
    });
    //更新节点的数据,删除节点,验证监听器是否正常运行
    final Object o = zkClient.readData("/test");
    System.out.println(o);
    zkClient.writeData("/test", "new data");
    Thread.sleep(1000);
    //删除节点
    zkClient.delete("/test");
    Thread.sleep(Integer.MAX_VALUE);
}

运行结果

123
/test data is changed ,new data new data
/test is deleted!!

第五部分 zookeeper内部原理

5.1 Leader选举

选举机制

  • 半数机制:集群中半数以上机器存活,集群可用。所以Zookeeper适合安装奇数台服务器。
  • Zookeeper虽然在配置文件中并没有指定Master和Slave。但是,Zookeeper⼯作时,是有一个节点为Leader,其它为Follower,Leader是通过内部的选举机制产生的。

集群⾸次启动

假设有五台服务器组成的Zookeeper集群,它们的id从1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点上,都是一样的。假设这些服务器依序启动,来看看会发生什么

image.png

Zookeeper的选举机制

(1)服务器1启动,此时只有它⼀台服务器启动了,它发出去的报文没有任何响应,所以它的选举状态一直是LOOKING状态。

(2)服务器2启动,它与最开始启动的服务器1进行通信,互相交换⾃己的选举结果,由于两者都没有历史数据,所以id值较大的服务器2胜出,但是由于没有达到超过半数以上的服务器都同意选举它(这个例⼦中的半数以上是3),所以服务器1、2还是继续保持LOOKING状态。

(3)服务器3启动,根据前面的理论分析,服务器3成为服务器1、2、3中的⽼大,⽽与上面不同的是,此时有三台服务器选举了它,所以它成为了这次选举的Leader。

(4)服务器4启动,根据前面的分析,理理论上服务器4应该是服务器1、2、3、4中最大的,但是由于前面已经有半数以上的服务器选举了服务器3,所以它只能接受当⼩弟的命了。

(5)服务器5启动,同4⼀样称为follower。

集群⾮首次启动

每个节点在选举时都会参考自身节点的zxid值(事务ID),优先选择zxid值大的节点称为Leader!!

5.2 ZAB一致性协议

1. 分布式数据一致性问题

为什么会出现分布式数据一致性问题?

  • 将数据复制到分布式部署的多台机器中,可以消除单点故障,防⽌系统由于某台(些)机器宕机导致的不可⽤。
  • 通过负载均衡技术,能够让分布在不同地方的数据副本全都对外提供服务。有效提⾼系统性能。

在分布式系统中引⼊数据复制机制后,多台数据节点之间由于网络等原因很容易产生数据不一致的情况。

举例:当客户端Client1将系统中的一个值K1由V1更新为V2,但是客户端Client2读取的是一个还没有同步更新的副本,K1的值依然是V1,这就导致了数据的不一致性。其中,常见的就是主从数据库之间的复制延时问题。

image.png

2. ZAB协议

ZK就是分布式一致性问题的⼯业解决方案,paxos是其底层理论算法(晦涩难懂著名),其中zab,raft和众多开源算法是对paxos的工业级实现。ZK没有完全采用paxos算法,⽽是使⽤了一种称为Zookeeper Atomic Broadcast(ZAB,Zookeeper原子消息广播协议)的协议作为其数据一致性的核心算法。

ZAB协议

ZAB 协议是为分布式协调服务 Zookeeper 专⻔设计的一种支持崩溃恢复原子广播协议

主备模式保证一致性

image.png

ZK怎么处理集群中的数据?所有客户端写入数据都是写⼊Leader中,然后,由 Leader复制到Follower中。ZAB会将服务器数据的状态变更以事务Proposal的形式广播到所有的副本进程上,ZAB协议能够保证了事务操作的⼀个全局的变更序号(ZXID)。

⼴播消息

ZAB协议的消息广播过程类似于 二阶段提交过程。对于客户端发送的写请求,全部由 Leader 接收, Leader 将请求封装成⼀个事务 Proposal(提议),将其发送给所有 Follwer ,如果收到超过半数反馈 ACK,则执行 Commit 操作(先提交⾃己,再发送 Commit 给所有 Follwer)。

1. 发送Proposal到Follower

image.png

2. Leader接收Follower的ACK

image.png

3. 超过半数ACK则Commit

image.png

不能正常反馈Follower恢复正常后会进入数据同步阶段最终与Leader保持一致!!

细节

  • Leader接收到Client请求之后,会将这个请求封装成一个事务,并给这个事务分配一个全局递增的唯⼀ID,称为事务ID(ZXID),ZAB 协议要求保证事务的顺序,因此必须将每⼀个事务按照 ZXID 进行先后排序然后处理。
  • ZK集群为了保证任何事务操作能够有序的顺序执行,只能是 Leader 服务器接受写请求,即使是 Follower 服务器接受到客户端的请求,也会转发到 Leader 服务器进⾏处理。

zk提供的应该是最终一致性的标准。zk所有节点接收写请求之后可以在一定时间内保证所有节点都能看到该条数据!!

Leader 崩溃问题

Leader宕机后,ZK集群⽆法正常工作,ZAB协议提供了一个⾼效且可靠的leader选举算法。

Leader宕机后,被选举的新Leader需要解决的问题

  • ZAB 协议确保那些已经在 Leader 提交的事务最终会被所有服务器提交。
  • ZAB 协议确保丢弃那些只在 Leader 提出/复制,但没有提交的事务。

基于上面的目的,ZAB协议设计了一个选举算法:能够确保已经被Leader提交的事务被集群接受,丢弃还没有提交的事务

这个选举算法的关键点:保证选举出的新Leader拥有集群中所有节点最大编号(ZXID)的事务!!

第六部分 Zookeeper应用实践

ZooKeeper是一个典型的发布/订阅模式的分布式数据管理与协调框架,我们可以使用它来进⾏分布式数据的发布与订阅。另一⽅面,通过对ZooKeeper中丰富的数据节点类型进行交叉使用,配合Watcher 事件通知机制,可以⾮常⽅便地构建一系列分布式应⽤中都会涉及的核心功能,如数据发布/订阅、命名服务、集群管理理、Master选举、分布式锁和分布式队列等。那接下来就针对这些典型的分布式应用场景来做下介绍

Zookeeper的两大特性:  

  1. 客户端如果对Zookeeper的数据节点注册Watcher监听,那么当该数据节点的内容或是其子节点列表发生变更时,Zookeeper服务器就会向订阅的客户端发送变更通知。
  2. 对在Zookeeper上创建的临时节点,⼀旦客户端与服务器之间的会话失效,那么临时节点也会被自动删除。

利用其两大特性,可以实现集群机器存活监控系统,若监控系统在/clusterServers节点上注册一个 Watcher监听,那么但凡进行动态添加机器的操作,就会在/clusterServers节点下创建⼀个临时节点:/clusterServers/[Hostname],这样,监控系统就能够实时监测机器的变动情况。

6.1 服务器动态上下线监听

分布式系统中,主节点会有多台,主节点可能因为任何原因出现宕机或者下线,⽽任意⼀台客户端都要能实时感知到主节点服务器的上下线。

思路分析

image.png

具体实现:

服务端

package com.lagou.zk;

import org.I0Itec.zkclient.ZkClient;

public class Server {
    public ZkClient zkClient = null;

    // 获取zk连接对象
    private void connectZk(){
        zkClient = new ZkClient("linux121:2181,linux122:2181,linux123:2181");
        if (!zkClient.exists("/servers")){
            zkClient.createPersistent("/servers");
        }
    }

    // 服务器向ZK注册
    public void registerServer(String ip, String port){
        // 创建临时顺序节点
        String path = zkClient.createEphemeralSequential("/servers/server", ip + ":" +port);
        System.out.println("服务器注册成功。ip=" + ip + ", port=" + port + ", 节点路径:" + path);
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.connectZk();
        server.registerServer(args[0],args[1] );
        //启动⼀个服务线程提供时间查询
        new TimeServer(Integer.parseInt(args[1])).start();
    }

}

服务端提供时间查询线程类

package com.lagou.zk;

import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;

public class TimeServer extends Thread {
    private int port;

    public TimeServer(int port) {
        this.port = port;
    }

    @Override
    public void run() {
        //启动serversocket监听一个端⼝
        try {
            ServerSocket serverSocket = new ServerSocket(port);
            while (true) {
                Socket socket = serverSocket.accept();
                OutputStream out = socket.getOutputStream();
                out.write(new Date().toString().getBytes());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端

package com.lagou.zk;

import org.I0Itec.zkclient.IZkChildListener;
import org.I0Itec.zkclient.ZkClient;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class Client {
    //获取zkclient
    ZkClient zkClient = null;
    //维护⼀个servers信息集合
    ArrayList<String> infos = new ArrayList<String>();

    private void connectZk() {
        // 创建zkclient
        zkClient = new ZkClient("linux121:2181,linux122:2181");
        //第一次获取服务器信息,所有的⼦节点
        List<String> childs = zkClient.getChildren("/servers");
        for (String child : childs) {
            //存储着ip+port
            Object o = zkClient.readData("/servers/" + child);
            infos.add(String.valueOf(o));
        }

        //对servers⽬录进行监听
        zkClient.subscribeChildChanges("/servers", new IZkChildListener() {
            public void handleChildChange(String s, List<String> children) throws Exception {
                //接收到通知,说明节点发⽣了变化,client需要更新infos集合中的数据
                ArrayList<String> list = new ArrayList<String>();
                //遍历更新过后的所有节点信息
                for (String path : children) {
                    Object o = zkClient.readData("/servers/" + path);
                    list.add(String.valueOf(o));
                }
                //最新数据覆盖老数据
                infos = list;
                System.out.println("接收到通知,最新服务器信息为:" + infos);
            }
        });
    }

    //发送时间查询的请求
    public void sendRequest() throws IOException {
        //⽬标服务器地址
        Random random = new Random();
        int i = random.nextInt(infos.size());
        String ipPort = infos.get(i);
        String[] arr = ipPort.split(":");
        //建立socket连接
        Socket socket = new Socket(arr[0], Integer.parseInt(arr[1]));
        OutputStream out = socket.getOutputStream();
        InputStream in = socket.getInputStream();
        //发送数据
        out.write("query time".getBytes());
        out.flush();
        //接收返回结果
        byte[] b = new byte[1024];
        in.read(b); //读取服务端返回数据
        System.out.println("client端接收到server:+" + ipPort + "+返回结果:" + new String(b));
        //释放资源
        in.close();
        out.close();
        socket.close();
    }

    public static void main(String[] args) throws InterruptedException {
        Client client = new Client();
        client.connectZk(); //监听器逻辑
        while (true) {
            try {
                client.sendRequest(); //发送请求
            } catch (IOException e) {
                e.printStackTrace();
                try {
                    client.sendRequest();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
            //每隔⼏秒发送⼀次请求到服务端
            Thread.sleep(2000);
        }
    }
}

6.2 分布式锁

1. 什么是锁

在单机程序中,当存在多个线程可以同时改变某个变量(可变共享变量)时,为了保证线程安全 (数据不能出现脏数据)就需要对变量或代码块做同步,使其在修改这种变量时能够串行执⾏消除并发修改变量。 对变量或者堆代码块做同步本质上就是加锁。目的就是实现多个线程在一个时刻同一个代码块只能有⼀个线程可执⾏

2. 分布式锁

分布式的环境中会不会出现脏数据的情况呢?类似单机程序中线程安全的问题。观察下面的例子

image.png

上⾯的设计是存在线程安全问题

问题

假设Redis ⾥面的某个商品库存为1;此时两个用户同时下单,其中⼀个下单请求执⾏到第 3 步,更新数据库的库存为0,但是第 4 步还没有执行。

⽽另外⼀个用户下单执行到了第 2 步,发现库存还是 1,就继续执行第 3 步。但是商品库存已经为0, 所以如果数据库没有限制就会出现超卖的问题。

解决方法:⽤锁把 2、3、4 步锁住,让他们执行完之后,另一个线程才能进来执行。

image.png

公司业务发展迅速,系统应对并发不断提高,解决方案是要增加⼀台机器,结果会出现更大的问题

image.png

假设有两个下单请求同时到来,分别由两个机器执行,那么这两个请求是可以同时执⾏了,依然存在超卖的问题。

因为如图所示系统是运行在两个不同的 JVM ⾥面,不同的机器上,增加的锁只对⾃己当前 JVM ⾥面的线程有效,对于其他 JVM 的线程是⽆效的。所以现在已经不是线程安全问题。需要保证两台机器加的锁是同⼀个锁,此时分布式锁就能解决该问题。

分布式锁的作用:在整个系统提供一个全局、唯⼀的锁,在分布式系统中每个系统在进行相关操作的时候需要获取到该锁,才能执行相应操作。

3.zk实现分布式锁

利用Zookeeper可以创建临时带序号节点的特性来实现⼀个分布式锁

实现思路

锁就是zk指定目录下序号最⼩的临时顺序节点,多个系统的多个线程都要在此目录下创建临时的顺序节点,因为Zk会为我们保证节点的顺序性,所以可以利用节点的顺序进行锁的判断。

每个线程都是先创建临时顺序节点,然后获取当前⽬录下最小的节点(序号),判断最⼩节点是不是当前节点,如果是那么获取锁成功,如果不是那么获取锁失败。

获取锁失败的线程获取当前节点上一个临时顺序节点,并对此节点进行监听,当该节点删除的时候(上⼀个线程执行结束删除或者是掉线zk删除临时节点)这个线程会获取到通知,代表获取到了锁。

流程图

image.png

Java代码实现分布式锁

main方法

package com.lagou.zk.lock;

/**
 * 使用多线程模拟分布式集群,实现zookeeper分布式锁的使用
 */
public class DisLockTest {
    public static void main(String[] args) {
        //使用10个线程模拟分布式环境
        for (int i = 0; i < 10; i++) {
            new Thread(new DisLockRunnable()).start();//启动线程
        }
    }

    static class DisLockRunnable implements Runnable {
        public void run() {
            //每个线程具体的任务,每个线程就是抢锁,
            final DisClient client = new DisClient();
            client.getDisLock();
            try {
                //模拟获取锁之后的其它动作
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //释放锁
            client.deleteLock();
        }
    }
}

客户端,核心实现

package com.lagou.zk.lock;

import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * 客户端抢锁:
 * 1.客户端在zk创建临时顺序节点,并获取到序号
 * 2.判断自己创建的节点序号是否是最小节点序号,如果是则获取锁,执行相关操作,最后释放锁
 * 3.如果不是最小节点,当前线程需要等待,并监听你前一个序号节点被删除(释放锁),然后再次判断自己是否是最小节点。
 */
public class DisClient {
    // 获取zk对象
    private ZkClient zkClient = new ZkClient("linux121:2181,linux122:2181");
    private String basePath = "/distriLock";

    public DisClient() {
        synchronized (DisClient.class){
            if (!zkClient.exists(basePath)) {
                zkClient.createPersistent(basePath);
            }
        }
    }

    // 获取分布式锁
    public void getDisLock() {
        String threadName = Thread.currentThread().getName();
        if (tryGetLock()) {
            System.out.println(threadName + "获取到了锁.");
        } else {
            System.out.println(threadName + "获取锁失败,进入等待状态.");
            waitForLock();
            // 递归获取锁
            getDisLock();
        }
    }

    String currentNodePath = null;
    String beforeNodePath = null;

    // 尝试获取锁
    private boolean tryGetLock() {
        // 创建临时顺序节点:/distriLock/序号
        if (currentNodePath == null || "".equals(currentNodePath)) {
            currentNodePath = zkClient.createEphemeralSequential(basePath + "/", "lock");
        }
        // 获取basePath下所有的子节点
        List<String> children = zkClient.getChildren(basePath);
        // 对所有子节点排序(默认升序)
        Collections.sort(children);
        // 获取第一个节点即为最小节点
        String minNode = children.get(0);
        // 判断自己是否与最小节点一致
        if (currentNodePath.equals(basePath + "/" + minNode)) {
            return true;
        } else {
            // 自己不是最小节点,需要监听自己前一个节点,获取前一个节点
            int i = Collections.binarySearch(children, currentNodePath.substring("/distriLock/".length()));
            String preNode = children.get(i - 1);
            beforeNodePath = basePath + "/" + preNode;
            return false;
        }
    }

    CountDownLatch countDownLatch = null;

    // 等待前一个节点释放锁
    private void waitForLock() {
        // 创建前一个节点的监听器
        IZkDataListener iZkDataListener = new IZkDataListener() {

            public void handleDataChange(String s, Object o) throws Exception {

            }

            public void handleDataDeleted(String s) throws Exception {
                // 提醒当前线程再次获取锁
                countDownLatch.countDown();//把值减1变为0,唤醒之前await线程
            }
        };

        // 监听前一个节点
        zkClient.subscribeDataChanges(beforeNodePath, iZkDataListener);
        //在监听的通知没来之前,该线程应该是等待状态,先判断一次上一个节点是否还存在
        if (zkClient.exists(beforeNodePath)) {
            //开始等待,CountDownLatch:线程同步计数器
            countDownLatch = new CountDownLatch(1);
            try {
                countDownLatch.await();//阻塞,countDownLatch值变为0
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 解除监听
        zkClient.unsubscribeDataChanges(beforeNodePath, iZkDataListener);
    }

    // 释放锁
    public void deleteLock() {
        if (zkClient != null) {
            zkClient.delete(currentNodePath);
            zkClient.close();
        }
    }

}

分布式锁的实现可以是 Redis、Zookeeper,相对来说生产环境如果使用分布式锁可以考虑使用Redis实现⽽非Zk。

第七部分 Hadoop HA

7.1 HA概述

  1. 所谓HA(High Available),即⾼可用(7*24⼩时不中断服务)。
  2. 实现⾼可用最关键的策略是消除单点故障。Hadoop-HA严格来说应该分成各个组件的HA机制:HDFS的HA和YARN的HA。
  3. Hadoop2.0之前,在HDFS集群中NameNode存在单点故障(SPOF)。
  4. NameNode主要在以下两个⽅方⾯面影响HDFS集群

NameNode机器发生意外,如宕机,集群将无法使用,直到管理员重启

NameNode机器需要升级,包括软件、硬件升级,此时集群也将无法使用

HDFS HA功能通过配置Active/Standby两个NameNode实现在集群中对NameNode的热备来解决上述问题。如果出现故障,如机器崩溃或机器需要升级维护,这时可通过此种⽅式将NameNode很快的切换到另外一台机器。

7.2 HDFS-HA 工作机制

通过双NameNode消除单点故障(Active/Standby)

1. HDFS-HA⼯作要点

1. 元数据管理方式需要改变

内存中各⾃保存一份元数据; Edits⽇志只有Active状态的NameNode节点可以做写操作; 两个NameNode都可以读取Edits; 共享的Edits放在一个共享存储中管理(qjournal和NFS两个主流实现);

2. 需要一个状态管理功能模块

实现了一个zkfailover,常驻在每一个namenode所在的节点,每一个zkfailover负责监控⾃己所在NameNode节点,利用zk进行状态标识,当需要进行状态切换时,由zkfailover来负责切换,切换时需要防⽌brain split现象的发生(集群中出现两个Active的Namenode)。

3. 必须保证两个NameNode之间能够ssh无密码登录

4. 隔离(Fence),即同一时刻仅有一个NameNode对外提供服务

2. HDFS-HA工作机制

配置部署HDFS-HA进行自动故障转移。⾃动故障转移为HDFS部署增加了两个新组件:ZooKeeper和 ZKFailoverController(ZKFC)进程,ZooKeeper是维护少量协调数据,通知客户端这些数据的改变和监视客户端故障的高可用服务。HA的自动故障转移依赖于ZooKeeper的以下功能:

  • 故障检测

集群中的每个NameNode在ZooKeeper中维护了一个临时会话,如果机器崩溃,ZooKeeper中的会话将终止,ZooKeeper通知另一个NameNode需要触发故障转移。

  • 现役NameNode选择

ZooKeeper提供了一个简单的机制⽤于唯一的选择一个节点为active状态。如果目前现役NameNode崩溃,另⼀个节点可能从ZooKeeper获得特殊的排外锁以表明它应该成为现役NameNode。

 

ZKFC是自动故障转移中的另⼀个新组件,是ZooKeeper的客户端,也监视和管理NameNode的状态。 每个运行NameNode的主机也运行了一个ZKFC进程,ZKFC负责:

  • 健康监测

ZKFC使⽤一个健康检查命令定期地ping与之在相同主机的NameNode,只要该NameNode及时地回复健康状态,ZKFC认为该节点是健康的。如果该节点崩溃,冻结或进⼊不健康状态,健康监测器标识该节点为⾮健康的。

  • ZooKeeper会话管理

当本地NameNode是健康的,ZKFC保持一个在ZooKeeper中打开的会话。如果本地NameNode 处于active状态,ZKFC也保持⼀个特殊的znode锁,该锁使⽤了ZooKeeper对短暂节点的支持,如果会话终止,锁节点将⾃动删除。

  • 基于ZooKeeper的选择

如果本地NameNode是健康的,且ZKFC发现没有其它的节点当前持有znode锁,它将为⾃己获取该锁。如果成功,则它已经赢得了选择,并负责运行故障转移进程以使它的本地NameNode为Active。故障转移进程与前面描述的手动故障转移相似,首先如果必要保护之前的现役NameNode,然后本地NameNode转换为Active状态。

image.png

7.3 HDFS-HA集群配置

https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/HDFSHighAvailabilityWithQJM.html

3.1 环境准备

1. 修改IP

2. 修改主机名及主机名和IP地址的映射

3. 关闭防火墙

4. ssh免密登录

5. 安装JDK,配置环境变量等

3.2 集群规划

image.png

3.3 启动Zookeeper集群

# 启动zk
zk.sh start
# 查看状态
zk.sh status

3.4 配置HDFS-HA集群

1. 停止原先HDFS集群

stop-dfs.sh

2. 在所有节点,/opt/lagou/servers目录下创建一个ha文件夹

 mkdir /opt/lagou/servers/ha

3. 将/opt/lagou/servers/目录下的hadoop-2.9.2拷贝到ha目录下

cp -r hadoop-2.9.2 ha

4. 删除原集群data目录

rm -rf /opt/lagou/servers/ha/hadoop-2.9.2/data

5. 配置hdfs-site.xml

<configuration>
  <property>
      <name>dfs.nameservices</name>
      <value>lagoucluster</value>
  </property>
  <property>
      <name>dfs.ha.namenodes.lagoucluster</name>
      <value>nn1,nn2</value>
  </property>
  <property>
      <name>dfs.namenode.rpc-address.lagoucluster.nn1</name>
      <value>linux121:9000</value>
  </property>
  <property>
      <name>dfs.namenode.rpc-address.lagoucluster.nn2</name>
      <value>linux122:9000</value>
  </property>
  <property>
      <name>dfs.namenode.http-address.lagoucluster.nn1</name>
      <value>linux121:50070</value>
  </property>
  <property>
      <name>dfs.namenode.http-address.lagoucluster.nn2</name>
      <value>linux122:50070</value>
  </property>
  <property>
      <name>dfs.namenode.shared.edits.dir</name>
      <value>qjournal://linux121:8485;linux122:8485;linux123:8485/lagou</value>
  </property>
  <property>
      <name>dfs.client.failover.proxy.provider.lagoucluster</name>
      <value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value>
  </property>
  <property>
      <name>dfs.ha.fencing.methods</name>
      <value>sshfence</value>
  </property>
  <property>
      <name>dfs.ha.fencing.ssh.private-key-files</name>
      <value>/root/.ssh/id_rsa</value>
  </property>
  <property>
      <name>dfs.journalnode.edits.dir</name>
      <value>/opt/journalnode</value>
  </property>
  <property>
      <name>dfs.ha.automatic-failover.enabled</name>
      <value>true</value>
  </property>
</configuration>

6.配置core-site.xml

<property>
    <name>fs.defaultFS</name>
  <value>hdfs://lagoucluster</value>
</property>
<property>
    <name>hadoop.tmp.dir</name>
    <value>/opt/lagou/servers/ha/hadoop-2.9.2/data/tmp</value>
</property>
<property>
    <name>ha.zookeeper.quorum</name>
    <value>linux121:2181,linux122:2181,linux123:2181</value>
</property>

7. 拷⻉配置好的hadoop环境到其他节点

rsync-script /opt/lagou/servers/ha/hadoop-2.9.2/

3.5 启动HDFS-HA集群

1. 在各个JournalNode节点上,输入以下命令启动journalnode服务(去往HA安装目录,不要使⽤环境变量中命令)

/opt/lagou/servers/ha/hadoop-2.9.2/sbin/hadoop-daemon.sh start journalnode

2. 在[nn1]上,对其进行格式化,并启动

/opt/lagou/servers/ha/hadoop-2.9.2/bin/hdfs namenode -format
/opt/lagou/servers/ha/hadoop-2.9.2/sbin/hadoop-daemon.sh start namenode

3. 在[nn2]上,同步nn1的元数据信息

/opt/lagou/servers/ha/hadoop-2.9.2/bin/hdfs namenode -bootstrapStandby

4. 在[nn1]上初始化zkfc

/opt/lagou/servers/ha/hadoop-2.9.2/bin/hdfs zkfc -formatZK

5. 在[nn1]上,启动集群

/opt/lagou/servers/ha/hadoop-2.9.2/sbin/start-dfs.sh

6. 验证

将Active NameNode进程kill,standby NameNode会变为Active NameNode。

7.4 YARN-HA配置

4.1 YARN-HA⼯作机制

https://hadoop.apache.org/docs/stable/hadoop-yarn/hadoop-yarn-site/ResourceManagerHA.html

image.png

4.2 配置YARN-HA集群

1. 环境准备

  • 修改IP
  • 修改主机名及主机名和IP地址的映射
  • 关闭防火墙
  • ssh免密登录
  • 安装JDK,配置环境变量等
  • 配置Zookeeper集群

2. 规划集群

image.png

3.具体配置

yarn-site.xml

<configuration>
    <property>
        <name>yarn.nodemanager.aux-services</name>
        <value>mapreduce_shuffle</value>
    </property>
    <!--启⽤用resourcemanager ha--> 
    <property>
        <name>yarn.resourcemanager.ha.enabled</name>
        <value>true</value>
    </property>
    <!--声明两台resourcemanager的地址--> 
    <property>
        <name>yarn.resourcemanager.cluster-id</name>
        <value>cluster-yarn</value>
    </property>
    <property>
        <name>yarn.resourcemanager.ha.rm-ids</name>
        <value>rm1,rm2</value>
    </property>
    <property>
        <name>yarn.resourcemanager.hostname.rm1</name>
        <value>linux122</value>
    </property>
    <property>
        <name>yarn.resourcemanager.hostname.rm2</name>
        <value>linux123</value>
    </property>
    <!--指定zookeeper集群的地址-->
    <property>
        <name>yarn.resourcemanager.zk-address</name>
        <value>linux121:2181,linux122:2181,linux123:2181</value>
    </property>
    <!--启⽤自动恢复-->
    <property>
        <name>yarn.resourcemanager.recovery.enabled</name>
        <value>true</value>
    </property>
    <!--指定resourcemanager的状态信息存储在zookeeper集群--> 
    <property>
        <name>yarn.resourcemanager.store.class</name>
        <value>org.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateStore</value>
    </property>
</configuration>

同步到其他节点

rsync-script yarn-site.xml

4. 启动yarn

sbin/start-yarn.sh

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值