学习笔记—Zookeeper

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/huxiutao/article/details/90038992

一、 入门简介

ZooKeeper是一个分布式协调技术、高性能的、开源的分布式系统的协调(Coordination)服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用程序一致性和分布式协调技术服务的软件。
是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper就将负责通知已经在Zookeeper上注册的那些观察者做出相应的反应,从而实现集群中类似Master/Slave管理模式
一句话:zookeeper=类似unix文件系统+通知机制+Znode节点
作用:服务注册+分布式系统的一致性通知协调

能干什么:
命名服务:是将一个名称映射到与该名称有关联的一些信息的服务。DNS服务也是一个名称服务,它将一个域名映射到一个 IP 地址。
配置维护
集群管理
分布式消息同步和协调机制
对Dubbo的支持

官网:https://zookeeper.apache.org/

统一命名服务(Name Service如Dubbo服务注册中心):
Dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,是阿里巴巴SOA服务化治理方案的核心框架,每天为2,000+个服务提供3,000,000,000+次访问量支持,并被广泛应用于阿里巴巴集团的各成员站点。

在Dubbo实现中:
服务提供者在启动的时候,向ZK上的指定节点/dubbo/ s e r v i c e N a m e / p r o v i d e r s U R L / d u b b o / {serviceName}/providers目录下写入自己的URL地址,这个操作就完成了服务的发布。 服务消费者启动的时候,订阅/dubbo/ {serviceName}/providers目录下的提供者URL地址, 并向/dubbo/ s e r v i c e N a m e / c o n s u m e r s U R L Z K D u b b o / d u b b o / {serviceName} /consumers目录下写入自己的URL地址。 注意,所有向ZK上注册的地址都是临时节点,这样就能够保证服务提供者和消费者能够自动感应资源的变化。另外,Dubbo还有针对服务粒度的监控,方法是订阅/dubbo/ {serviceName}目录下所有提供者和消费者的信息。

配置管理(Configuration Management如淘宝开源配置管理框架Diamond):
在大型的分布式系统中,为了服务海量的请求,同一个应用常常需要多个实例。如果存在配置更新的需求,常常需要逐台更新,给运维增加了很大的负担同时带来一定的风险(配置会存在不一致的窗口期,或者个别节点忘记更新)。zookeeper可以用来做集中的配置管理,存储在zookeeper集群中的配置,如果发生变更会主动推送到连接配置中心的应用节点,实现一处更新处处更新的效果。现在把这些配置全部放到zookeeper上去,保存在 Zookeeper 的某个目录节点中,然后所有相关应用程序对这个目录节点进行监听,一旦配置信息发生变化,每个应用程序就会收到 Zookeeper 的通知,然后从 Zookeeper 获取新的配置信息应用到系统中就好。

Java操作API。

二、 安装配置

在这里插入图片描述
修改IP地址:
Vim /etc/sysconfig/network-scripts/ifcfg-etho
重启网卡:service network restart
关闭防火墙:service iptable stop/start
在这里插入图片描述
新建专属zookeeper目录,mkdir /myzookeeper,
随后将上一步解压的zookeeper内容拷贝进/myzookeeper目录内

在这里插入图片描述
在这里插入图片描述
进入conf文件夹,拷贝zoo_sample.cfg改为zoo.cfg(vi后,输入冒号 set nu可以显示出行号)
zoo.cfg配置文件参数说明:
tickTime:通信心跳数,Zookeeper服务器心跳时间,单位毫秒。它用于心跳机制,并且设置最小的session超时时间为两倍心跳时间.(session的最小超时时间是2*tickTime。)
initLimit:这个配置项是用来配置Zookeeper接收Follower客户端(这里所说的客户端不是用户链接Zookeeper服务器的客户端,而是Zookeeper服务器集群中连接到leader的Follower服务器,Follower在启动过程中,会从Leader同步所有最新数据,然后确定自己能够对外服务的起始状态。Leader允许F在 initLimit 时间内完成这个工作)初始化连接是最长能忍受多少个心跳的时间间隔数。
syncLimit:LF同步通信时限。集群中Leader与Follower之间的最大响应时间单位。如果L发出心跳包在syncLimit之后,还没有从F那收到响应,那么就认为这个F已经不在线了。
dataDir:数据文件目录+数据持久化路径。保存内存数据库快照信息的位置,如果没有其他说明,更新的事务日志也保存到数据库。
ClientPort:客户端连接端口。

开启服务+客户端连接:启、连、退、停
启:
查看是否已经启动:(三种查服务的方法)
ps -ef|grep zookeeper
或者
netstat -anp|grep 2181
或者
lsof -i:2181

进入bin目录启动:[root@localhost bin]# ./zkServer.sh start
检查是否启动成功:上面的三种方法
在这里插入图片描述
连:在bin目录下执行“./zkCli.sh”出现“Welcome to ZooKeeper!”表示成功。
在这里插入图片描述
退

在bin目录下:
在这里插入图片描述
ls /zookeeper下有个qutoa节点(该节点也叫znode节点,其他客户端的节点都是挂到这个节点上的。)zookeeper=类似unix文件系统+通知机制+Znode节点(顺藤摸瓜)
客户端:quit
服务器端:./zkServer.sh stop
在Zookeeper服务器成功启动的前提下,在Linux侧的shell命令端口执行下面的ruok四字命令,
如果能够显示imok
表示zk服务器端成功启动。
# echo ruok | nc 127.0.0.1 2181
在这里插入图片描述

HelloWord:
查看+获得zookeeper服务器上的数据存储信息 在这里插入图片描述
Help可以查看具体操作命令:create/get/set/delete
在这里插入图片描述
Zookeeper维护一个类似文件系统的数据结构
可以看做一个树形结构的数据库,分布在不同的机器上做名称管理

初识Znode(zookeeper node): ZooKeeper数据模型的结构与Unix文件系统很类似,整体上可以看作是一棵树,每个节点称做一个ZNode,很显然zookeeper集群自身维护了一套数据结构。这个存储结构是一个树形结构,其上的每一个节点,我们称之为"znode",每一个znode默认能够存储1MB的数据(主要存储一个名字),每个ZNode都可以通过其路径唯一标识

三、 数据模型/Znode节点深入

ZNode的数据模型:
Znode维护了一个stat结构,这个stat包含数据变化的版本号、访问控制列表变化、还有时间戳。版本号和时间戳一起,可让Zookeeper验证缓存和协调更新。每次znode的数据发生了变化,版本号就增加。
Zookeeper的Stat结构体:
在这里插入图片描述
czxid - 引起这个znode创建的zxid,创建节点的事务的zxid(ZooKeeper Transaction Id)
每次修改ZooKeeper状态都会收到一个zxid形式的时间戳,也就是ZooKeeper事务ID。事务ID是ZooKeeper中所有修改总的次序。每个修改都有唯一的zxid,如果zxid1小于zxid2,那么zxid1在zxid2之前发生。
ctime - znode被创建的毫秒数(从1970年开始)
mzxid - znode最后更新的zxid
mtime - znode最后修改的毫秒数(从1970年开始)
pZxid - znode最后更新的子节点zxid
cversion - znode子节点变化号,znode子节点修改次数
dataversion - znode数据变化号
aclVersion - znode访问控制列表的变化号
ephemeralOwner - 如果是临时节点,这个是znode拥有者的session id。如果不是临时节点则是0。
dataLength - znode的数据长度
numChildren - znode子节点数量
zookeeper内部维护了一套类似UNIX的树形数据结构:由znode构成的集合,znode的集合又是一个树形结构。每一个znode又有很多属性进行描述。

Znode = path + data + Stat

Znode中的存在类型:持久化节点和临时节点。
细分分为四种
PERSISTENT-持久化目录节点:客户端与zookeeper断开连接后,该节点依旧存在
PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点:客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号
EPHEMERAL-临时目录节点:客户端与zookeeper断开连接后,该节点被删除
EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点:客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号
在这里插入图片描述
znode是由客户端创建的,它和创建它的客户端的内在联系,决定了它的存在性:
PERSISTENT-持久化节点:创建这个节点的客户端在与zookeeper服务的连接断开后,这个节点也不会被删除(除非您使用API强制删除)。
PERSISTENT_SEQUENTIAL-持久化顺序编号节点:当客户端请求创建这个节点A后,zookeeper会根据parent-znode的zxid状态,为这个A节点编写一个全目录唯一的编号(这个编号只会一直增长)。当客户端与zookeeper服务的连接断开后,这个节点也不会被删除。
EPHEMERAL-临时目录节点:创建这个节点的客户端在与zookeeper服务的连接断开后,这个节点(还有涉及到的子节点)就会被删除。
EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点:当客户端请求创建这个节点A后,zookeeper会根据parent-znode的zxid状态,为这个A节点编写一个全目录唯一的编号(这个编号只会一直增长)。当创建这个节点的客户端与zookeeper服务的连接断开后,这个节点被删除。
另外,无论是EPHEMERAL还是EPHEMERAL_SEQUENTIAL节点类型,在zookeeper的client异常终止后,节点也会被删除

四、 基础命令和Java客户端操作

ZkCli的常用命令操作:
一句话:和redis的KV键值对类似,只不过key变成了一个路径节点值,v就是data
常用命令
在这里插入图片描述
Ls:使用 ls 命令来查看当前znode中所包含的内容
Ls2:查看当前节点数据并能看到更新次数等数据,相当于:ls+stat
Stat:查看节点状态
Set:设置节点具体值(用法:set 节点 value值)
在这里插入图片描述
Get:获得节点的值(用法:get 节点)
Create:普通创建。
Delete:删除节点(只能删除空节点
Rmr:删除节点(可以删除非空节点
四字命令:zookeeper支持某些特定的四字命令,他们大多是用来查询ZK服务的当前状态及相关信息的,通过telnet或nc向zookeeper提交相应命令,如:echo ruok | nc 127.0.0.1 2181。

运行公式echo 四字命令 | nc 主机IP zookeeper端口

Ruok:测试服务是否处于正确状态,如果正常返回“imok”,否则没有任何响应。
Stat:输出关于性能和连接的客户端的列表(可以看到是follower还是leader,Node count)
Conf:输出相关服务配置的详细信息
Cons:列出所有连接到服务器的客户端的完全的连接/回话的详细信息。包括“接受/发送”的包数量、回话id、操作延迟、最后的操作执行等信息。
Dump:列出未经处理的回话和临时节点
Envi:输出关于环境的详细信息(区别于conf命令)
Reqs:列出未经处理的请求
Wchs:列出服务器Watch的详细信息
Wchc:通过session列出服务器watch的详细信息,它的输出是一个与watch相关的回话列表
Wchp:通过路径列出服务器watch的详细信息,它输出一个与session相关的路径。
在这里插入图片描述

Java客户端操作

新建Maven工程,必备依赖:

<!-- https://mvnrepository.com/artifact/com.101tec/zkclient -->
   <dependency>
     <groupId>com.101tec</groupId>
     <artifactId>zkclient</artifactId>
     <version>0.10</version>
   </dependency>
   <!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper -->
   <dependency>
     <groupId>org.apache.zookeeper</groupId>
     <artifactId>zookeeper</artifactId>
     <version>3.4.9</version>
   </dependency>
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
		<dependency>
		    <groupId>log4j</groupId>
		    <artifactId>log4j</artifactId>
		    <version>1.2.17</version>
		</dependency>
可选依赖:lombok用来简化代码
	<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
		<dependency>
		    <groupId>org.projectlombok</groupId>
		    <artifactId>lombok</artifactId>
		    <version>1.18.2</version>
		    <scope>provided</scope>
		</dependency>

设置日志控制文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">

<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">

	<appender name="log.console" class="org.apache.log4j.ConsoleAppender">
		<layout class="org.apache.log4j.PatternLayout">
			<param name="ConversionPattern" value="%d{HH:mm:ss,SSS} %5p (%C{1}:%M) - %m%n" />
		</layout>
		<!-- <filter class="org.apache.log4j.varia.LevelRangeFilter"> <param name="levelMin" 
			value="debug" /> <param name="levelMax" value="warn" /> <param name="AcceptOnMatch" 
			value="true" /> </filter> -->
	</appender>

	<appender name="log.file" class="org.apache.log4j.DailyRollingFileAppender">
		<param name="File" value="logs\\run.log" />
		<param name="Append" value="true" />
		<param name="DatePattern" value="'.'yyyy-MM-dd" />
		<layout class="org.apache.log4j.PatternLayout">
			<param name="ConversionPattern" value="%d{HH:mm:ss,SSS} %5p (%C{1}:%M) - %m%n" />
		</layout>
		<!-- <filter class="org.apache.log4j.varia.LevelRangeFilter"> <param name="levelMin" 
			value="info" /> <param name="levelMax" value="info" /> <param name="AcceptOnMatch" 
			value="true" /> </filter> -->
	</appender>
	<!-- <logger name="com.atguigu" > <level value="info" /> <appender-ref ref="log.console" 
		/> <appender-ref ref="log.file" /> </logger> <logger name="com.atguigu.dao" 
		> <level value="debug" /> <appender-ref ref="log.console" /> <appender-ref 
		ref="log.file" /> </logger> -->

	<!-- -->
	<root>
		<level value="info" />
		<appender-ref ref="log.console" />
		<appender-ref ref="log.file" />
	</root>

</log4j:configuration>

Java操作:

package com.delta.my.learn;

import java.io.IOException;

import org.apache.log4j.Logger;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

import lombok.Getter;
import lombok.Setter;

/**
 * @Description: 1 初始化ZK的多个操作 1.1 建立ZK的链接 1.2 创建/atguigu节点并赋值 1.3 获得该节点的值
 * 
 *               2 watchmore 2.1
 *               获得值之后设置一个观察者watcher,如果/atguigu该节点的值发生了变化,要求通知Client端,一次性通知
 * 
 *               3 watchMore 3.1
 *               获得值之后设置一个观察者watcher,如果/atguigu该节点的值发生了变化,要求通知Client端,继续观察 3.2
 *               又再次获得新的值的同时再新设置一个观察者,继续观察并获得值 3.3
 *               又再次获得新的值的同时再新设置一个观察者,继续观察并获得值.。。。。。重复上述过程
 * @author zzyy
 * @date 2018年3月21日
 */
public class WatchMore {
	private static final Logger logger = Logger.getLogger(WatchMore.class);
	// 实例常量
	private static final String CONNECTION_STRING = "192.168.23.167:2181";
	private static final String PATH = "/atguigu";
	private static final int SESSION_TIMEOUT = 20 * 1000;
	// 实例变量
	private @Setter @Getter ZooKeeper zk = null;

	private @Setter @Getter String oldValue = "";
	private @Setter @Getter String newValue = "";

	/**
	 * @Title: startZK @Description:
	 * 获得ZK的session连接对象实例 @param @return @param @throws IOException 参数 @return
	 * ZooKeeper 返回类型 @throws
	 */
	public ZooKeeper startZK() throws IOException {
		return new ZooKeeper(CONNECTION_STRING, SESSION_TIMEOUT, new Watcher() {
			@Override
			public void process(WatchedEvent event) {

			}
		});
	}

	/**
	 * @Title: createZnode @Description: 再给定的路径下创建znode节点并赋值 @param @param
	 * zk @param @param path @param @param data @param @throws
	 * KeeperException @param @throws InterruptedException 参数 @return void
	 * 返回类型 @throws
	 */
	public void createZnode(String path, String data) throws KeeperException, InterruptedException {
		zk.create(path, data.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
	}

	/**
	 * @Title: getZnode @Description: 获取我们对应节点的值 @param @param zk @param @param
	 * path @param @return @param @throws KeeperException @param @throws
	 * InterruptedException 参数 @return String 返回类型 @throws
	 */
	public String getZnode(String path) throws KeeperException, InterruptedException {
		String result = "";

		byte[] byteArray = zk.getData(path, new Watcher() {
			@Override
			public void process(WatchedEvent event) {
				try {
					triggerValue(path);
				} catch (KeeperException | InterruptedException e) {
					e.printStackTrace();
				}
			}
		}, new Stat());
		result = new String(byteArray);
		oldValue = result;

		return result;
	}

	public boolean triggerValue(String path) throws KeeperException, InterruptedException {
		String result = "";

		byte[] byteArray = zk.getData(path, new Watcher() {
			@Override
			public void process(WatchedEvent event) {
				try {
					triggerValue(path);
				} catch (KeeperException | InterruptedException e) {
					e.printStackTrace();
				}
			}
		}, new Stat());
		result = new String(byteArray);
		newValue = result;

		if (newValue.equals(oldValue)) {
			logger.info("*********The value no changes*******");
			return false;
		} else {
			logger.info("********oldValue: " + oldValue + "\t newValue: " + newValue);
			oldValue = newValue;
			return true;
		}
	}

	public static void main(String[] args) throws Exception {
		WatchMore watchOne = new WatchMore();
		watchOne.setZk(watchOne.startZK());

		if (watchOne.getZk().exists(PATH, false) == null) {
			watchOne.createZnode(PATH, "AAA");

			String result = watchOne.getZnode(PATH);// AAA

			if (logger.isInfoEnabled()) {
				logger.info("main(String[]) --------init String result=" + result);
			}
		} else {
			logger.info("this node has already exists!!!!");
		}

		Thread.sleep(Long.MAX_VALUE);
	}

}

五、 通知机制

客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、被删除、子目录节点增加删除)时,zookeeper会通知客户端。

观察者功能:
ZooKeeper 支持watch(观察)的概念。客户端可以在每个znode结点上设置一个观察。如果被观察服务端的znode结点有变更,那么watch就会被触发,这个watch所属的客户端将接收到一个通知包被告知结点已经发生变化,把相应的事件通知给设置过Watcher的Client。

一句话:异步回调的触发机制

Watch事件:
一次触发:

  • 发送客户端。当数据有了变化时zkserver向客户端发送一个watch,它是一次性的动作,即触发一次就不再有效。如果想继续Watch的话,需要客户端重新设置Watcher。因此如果你得到一个watch事件且想在将来的变化得到通知,必须新设置另一个watch。

  • 发往客户端:Watches是异步发往客户端的,Zookeeper提供一个顺序保证:在看到watch事件之前绝不会看到变化,这样不同客户端看到的是一致性的顺序。

    在(导致观察事件被触发的)修改操作的成功返回码到达客户端之前,事件可能在去往客户端的路上,但是可能不会到达客户端。观察事件是异步地发送给观察者(客户端)的。ZooKeeper会保证次序:在收到观察事件之前,客户端不会看到已经为之设置观察的节点的改动。网络延迟或者其他因素可能会让不同的客户端在不同的时间收到观察事件和更新操作的返回码。这里的要点是:不同客户端看到的事情都有一致的次序。

  • 为数据设置watch:节点有不同的改动方式。可以认为ZooKeeper维护两个观察列表:数据观察和子节点观察。getData()和exists()设置数据观察。getChildren()设置子节点观察。此外,还可以认为不同的返回数据有不同的观察。getData()和exists()返回节点的数据,而getChildren()返回子节点列表。所以,setData()将为znode触发数据观察。成功的create()将为新创建的节点触发数据观察,为其父节点触发子节点观察。成功的delete()将会为被删除的节点触发数据观察以及子节点观察(因为节点不能再有子节点了),为其父节点触发子节点观察。(常考面试考题:Zookeeper里的所有读取操作:getData(),getChildren()和exists()都有设置watch的选项)

  • 时序性和一致性:Watches是在client连接到Zookeeper服务端的本地维护,这可让watches成为轻量的,可维护的和派发的。当一个client连接到新server,watch将会触发任何session事件,断开连接后不能接收到。当客户端重连,先前注册的watches将会被重新注册并触发。

关于watches,Zookeeper维护这些保证:
(1)Watches和其他事件、watches和异步恢复都是有序的。Zookeeper客户端保证每件事都是有序派发
(2)客户端在看到新数据之前先看到watch事件
(3)对应更新顺序的watches事件顺序由Zookeeper服务所见

六、 Zookeeper集群

1、伪分布式集群
服务器名称与地址:集群信息(服务器编号,服务器地址,LF通信端口,选举端口)。
这个配置项的书写格式比较特殊,规则如下:server.N=YYY:A:B 其中,
N表示服务器编号,
YYY表示服务器的IP地址,
A为LF通信端口,表示该服务器与集群中的leader交换的信息的端口。
B为选举端口,表示选举新leader时服务器间相互通信的端口(当leader挂掉时,其余服务器会相互通信,选择出新的leader)
一般来说,集群中每个服务器的A端口都是一样,每个服务器的B端口也是一样。
下面是一个集群的例子:
server.0=233.34.9.144:2008:6008
server.1=233.34.9.145:2008:6008
server.2=233.34.9.146:2008:6008
server.3=233.34.9.147:2008:6008
但是当所采用的为伪集群时,IP地址都一样,只能是A端口和B端口不一样。
下面是一个伪集群的例子:
server.0=127.0.0.1:2008:6008
server.1=127.0.0.1:2007:6007
server.2=127.0.0.1:2006:6006
server.3=127.0.0.1:2005:6005
说明:
initLimit是Zookeeper用它来限定集群中的Zookeeper服务器连接到Leader的时限。
syncLimit限制了follower服务器与leader服务器之间请求和应答之间的时限
2、配置步骤
1.zookeeper-3.4.9.tar.gz解压后拷贝到/myzookeeper目录下并重新名为zk01,再复制zk01形成zk02、zk03,共计3份。
2.进入zk01/02/03分别新建文件夹:mydata、mylog
在这里插入图片描述
3.分别进入zk01-zk03各自的conf文件夹,新建zoo.cfg在这里插入图片描述
4.编辑zoo.cfg
设置自己的数据和log路径:
dataDir=/myzookeeper/zk01/mydata
dataLogDir=/myzookeeper/zk01/mylog
修改各自的clientPort
在最后面添加server的列表
在这里插入图片描述
5.在各自mydata下面创建myid的文件,在里面写入server的数字
在这里插入图片描述
6.分别启动三个服务器
在这里插入图片描述
7.zkCli连接server,带参数指定-server
在这里插入图片描述
2191/2192/2193任意用客户端链接一台,会发现只需要有一个改变了,整个集群的内容自动一致性同步。
另外参考:
https://www.cnblogs.com/linuxprobe/p/5851699.html
https://blog.csdn.net/wzsy_ll/article/details/82752307

七、 常见应用案例

1、命名服务(NameService)
前面所演示的每个nodepath就是一个命名服务
一句话:path就是命名服务,客户端能够根据指定节点的名字来获取资源或者服务地址,然后进行下一步操作
2、软负载均衡:(轮询算法)
分布式:不同的主机上部署不同的服务,组成一个整体对外提供服务。
集群:不同的主机上部署相同的服务,组成一个整体对外提供服务。

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.log4j.Logger;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

public class BalanceTest {
	/**
	 * Logger for this class
	 */
	private static final Logger logger = Logger.getLogger(BalanceTest.class);
	// 定义常量
	private static final String CONNECTSTRING = "192.168.67.167:2181";
	private static final int SESSION_TIMEOUT = 50 * 1000;
	private static final String PATH = "/atguigu";
	private static final String SUB_PREFIX = "sub";
	// 定义实例变量
	private ZooKeeper zk = null;
	private int subCount = 5;
	private List<String> serviceNodeLists = new ArrayList<String>();
	private int serviceNum = 0;

	// 以下为业务方法
	public ZooKeeper startZK() throws IOException {
		return new ZooKeeper(CONNECTSTRING, SESSION_TIMEOUT, new Watcher() {
			@Override
			public void process(WatchedEvent event) {
				try {
					serviceNodeLists = zk.getChildren(PATH, true);
				} catch (KeeperException | InterruptedException e) {
					e.printStackTrace();
				}
			}
		});
	}

	public String dealRequest() throws KeeperException, InterruptedException {
		serviceNum = serviceNum + 1;

		for (int i = serviceNum; i <= subCount; i++) {
			if (serviceNodeLists.contains(SUB_PREFIX + serviceNum)) {
				return new String(zk.getData(PATH + "/" + SUB_PREFIX + serviceNum, false, new Stat()));
			} else {
				serviceNum = serviceNum + 1;
			}
		}
		for (int i = 1; i <= subCount; i++) {
			if (serviceNodeLists.contains(SUB_PREFIX + i)) {
				serviceNum = i;
				return new String(zk.getData(PATH + "/" + SUB_PREFIX + serviceNum, false, new Stat()));
			}
		}
		return "null node~~~~~";
	}

	public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
		BalanceTest test = new BalanceTest();

		test.setZk(test.startZK());
		Thread.sleep(3000);
		String result = null;
		// 以轮询的方式访问15次,共计5个节点来应付实现负载均衡
		for (int i = 1; i <= 15; i++) {
			result = test.dealRequest();
			System.out.println("****loop:" + i + "\t" + test.serviceNum + "\t" + result);
			Thread.sleep(2000);
		}
	}

	// setter---getter
	public ZooKeeper getZk() {
		return zk;
	}

	public void setZk(ZooKeeper zk) {
		this.zk = zk;
	}

}

八、负载均衡算法

在分布式系统中,多台服务器同时提供一个服务,并统一被服务配置中心进行管理。消费者通过查询服务配置中心,获取到服务地址列表,需要选取一台来发起RPC远程调用。如何选择,则取决于具体的负载均衡算法,对应不同的场景,选择的负载均衡算法也不尽相同。负载均衡算法的种类很多,常见的算法包括:轮询法、随机法、源地址哈希法、加权轮询法、加权随机法、最小连接法等。
在这里插入图片描述
算法一:轮询法
轮询法很容易实现,将请求按顺序轮流分配到后台服务器上,均衡的对待每一台服务器,而不关心服务器实际的连接数和单签的系统负载。
这里通过实例化一个serviceWeightMap的Map变量来服务器地址和权重的映射,以此来模拟轮询算法的实现,其中设置的权重值在以后的加权算法中会使用到,这里先不做过多介绍,该变量初始化如下:

private static Map<String, Integer> serviceWeightMap = new HashMap<String, Integer>();
static {
    serviceWeightMap.put("192.168.1.100", 1);
    serviceWeightMap.put("192.168.1.101", 1);
    serviceWeightMap.put("192.168.1.102", 4);      //权重为4
    serviceWeightMap.put("192.168.1.103", 1);
    serviceWeightMap.put("192.168.1.104", 1);
    serviceWeightMap.put("192.168.1.105", 3);        //权重为3
    serviceWeightMap.put("192.168.1.106", 1);
    serviceWeightMap.put("192.168.1.107", 2);        //权重为2
    serviceWeightMap.put("192.168.1.108", 1);
    serviceWeightMap.put("192.168.1.109", 1);
    serviceWeightMap.put("192.168.1.110", 1);
}

通过该地址列表,实现的轮询算法的部分关键代码如下:

private static Integer pos = 0;
public static String testRoundRobin() {
     // 重新创建一个map,避免出现由于服务器上线和下线导致的并发问题
    Map<String, Integer> serverMap = new HashMap<String, Integer>();
    serverMap.putAll(serviceWeightMap);
     
    //取得IP地址list
    Set<String> keySet = serverMap.keySet();
    ArrayList<String> keyList = new ArrayList<String>();
    keyList.addAll(keySet);
     
    String server = null;
     
    synchronized (pos) {
        if (pos > keySet.size()) {
            pos = 0;
        }
         server = keyList.get(pos);
         pos++;
    }
     return server;
}

由于serviceWeightMap中的地址列表是动态的,随时可能由机器上线、下线或者宕机,因此,为了避免可能出现的并发问题,比如数组越界,通过在方法内新建局部变量serverMap,先将域变量拷贝到线程本地,避免被其他线程修改。这样可能会引入新的问题,当被拷贝之后,serviceWeightMap的修改将无法被serverMap感知,也就是说,在这一轮的选择服务器中,新增服务器或者下线服务器,负载均衡算法中将无法获知。新增比较好处理,而当服务器下线或者宕机时,服务消费者将有可能访问不到不存在的地址。因此,在服务消费者服务端需要考虑该问题,并且进行相应的容错处理,比如重新发起一次调用。
对于当前轮询的位置变量pos,为了保证服务器选择的顺序性,需要对其在操作时加上synchronized锁,使得同一时刻只有一个线程能够修改pos的值,否则当pos变量被并发修改,将无法保证服务器选择的顺序性,甚至有可能导致keyList数组越界。

使用轮询策略的目的是,希望做到请求转移的绝对均衡,但付出的代价性能也是相当大的。为了保证pos变量的并发互斥,引入了重量级悲观锁synchronized,将会导致该轮询代码的并发吞吐量明显下降。

算法二:随机法
通过系统随机函数,根据后台服务器列表的大小值来随机选取其中一台进行访问。由概率统计理论可以得知,随着调用量的增大,其实际效果越来越接近于平均分配流量到和后台的每一台服务器,也就是轮询法的效果。
随机算法的关键代码:

public static String testRandom() {
 
    // 重新创建一个map,避免出现由于服务器上线和下线导致的并发问题
    Map<String, Integer> serverMap = new HashMap<String, Integer>();
    serverMap.putAll(serviceWeightMap);
 
    //取得IP地址list
    Set<String> keySet = serverMap.keySet();
    ArrayList<String> keyList = new ArrayList<String>();
    keyList.addAll(keySet);
 
    Random random = new Random();
    int randomPos = random.nextInt(keyList.size());
     
    String server = keyList.get(randomPos);
     
    return server;
}

跟前面类似,为了避免并发的问题,需要将serviceWeightMap拷贝到serverMap中。通过Random的nextInt函数,取到0~keyList.size之间的随机值, 从而从服务器列表中随机取到一台服务器的地址,进行返回。根据概率统计理论,吞吐量越大,随机算法的效果越接近于轮询算法的效果。

算法三:源地址哈希法
源地址哈希法的思想是根据消费者请求客户端的IP地址,通过哈希函数计算得到一个哈希值,将此哈希值和服务器列表的大小进行取模运算,得到的结果便是要访问的服务器地址序号。采用源地址哈希法进行负载均衡,如果服务器列表不变的话,相同的IP客户端始终映射到同一台服务器。
源地址哈希值部分关键代码:

public static String testConsumerHash(String remoteIp) {
 
    // 重新创建一个map,避免出现由于服务器上线和下线导致的并发问题
    Map<String, Integer> serverMap = new HashMap<String, Integer>();
    serverMap.putAll(serviceWeightMap);
 
    //取得IP地址list
    Set<String> keySet = serverMap.keySet();
    ArrayList<String> keyList = new ArrayList<String>();
    keyList.addAll(keySet);
     
    int hashCode = remoteIp.hashCode();
    int pos = hashCode % keyList.size();
     
    return keyList.get(pos);
}

算法四:加权轮询法
不同的后台服务器的配置和当前系统的负载不相同,因此他们的抗压能力也不一样。配置高、负载底的机器分配更高权重,使其处理更多的请求,反之。
加权轮询法的关键代码:

public static String testWeightRoundRobin() {
 
    // 重新创建一个map,避免出现由于服务器上线和下线导致的并发问题
    Map<String, Integer> serverMap = new HashMap<String, Integer>();
    serverMap.putAll(serviceWeightMap);
 
    //取得IP地址list
    Set<String> keySet = serverMap.keySet();
    Iterator<String> it = keySet.iterator();
 
    List<String> serverList = new ArrayList<String>();
 
    while (it.hasNext()) {
        String server = it.next();
        Integer weight = serverMap.get(server);
        for (int i=0; i<weight; i++) {
            serverList.add(server);
        }
    }
 
    String server = null;
 
    synchronized (pos) {
        if (pos > serverList.size()) {
            pos = 0;
        }
         
        server = serverList.get(pos);
        pos++;
    }
     
    return server;
}

与轮询算法类似,只是在获取服务器地址之前增加了一段权重计算代码,根据权重的大小,将地址重复增加到服务器地址列表中,权重越大,该服务器每轮所获得的请求数量越多。

算法五:加权随机法
加权随机法与加权轮询法类似,根据后台服务器的配置不和负载情况的不同,配置不同的权重。不同的是,他是按照权重来随机选取服务器的。关键代码:

public static String testWeightRandom() {
    // 重新创建一个map,避免出现由于服务器上线和下线导致的并发问题
    Map<String, Integer> serverMap = new HashMap<String, Integer>();
    serverMap.putAll(serviceWeightMap);
 
    //取得IP地址list
    Set<String> keySet = serverMap.keySet();
    List<String> serverList = new ArrayList<String>();
    Iterator<String> it = keySet.iterator();
     
    while (it.hasNext()) {
        String server = it.next();
        Integer weight = serverMap.get(server);
        for (int i=0; i<weight; i++) {
            serverList.add(server);
        }
    }
 
    Random random = new Random();
    int randomPos = random.nextInt(serverList.size());
 
    String server = serverList.get(randomPos);
 
    return server;
}

算法六:最小连接数法
最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。

扩展:Nginx的5中负载均衡算法
(1)默认是轮询法
每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。
(2)weight
指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。
(3)ip_hash
每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。
(4)fair(第三方)
按后端服务器的响应时间来分配请求,响应时间短的优先分配。
(5)url_hash(第三方)
按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。

每个设备的状态设置为:
1.down表示单前的server暂时不参与负载
2.weight默认为1.weight越大,负载的权重就越大。
3.max_fails:允许请求失败的次数默认为1.当超过最大次数时,返回proxy_next_upstream 模块定义的错误
4.fail_timeout:max_fails次失败后,暂停的时间。
5.backup: 其它所有的非backup机器down或者忙的时候,请求backup机器。所以这台机器压力会最轻。
nginx支持同时设置多组的负载均衡,用来给不用的server来使用。

九、Zookeeper的权限设置

一般情况下,在开发阶段用的zk都是在内网中,当把项目发布/部署到外网中时,就需要考虑将zookeeper加入访问权限。

请注意,这里说的权限是针对znode节点而言的,不是对zookeeper这个服务而说的。我刚开始想这个问题时,简单的以为就像mysql数据库,只要设置一下统一的用户名和密码就可以。但是对于zookeeper不是这样的,你只能通过对其中节点的权限进行设置。
(自己的疑惑:是不是知道zk地址就可以无限建自己的节点和操作呢?目前想不明白,为什么要这样设置,难道在服务端设置用户名和密码不行么?不同的用户设置不同的角色就可以进行控制访问列表吧。)

ZooKeeper提供了ACL(Access Control List)的权限控制机制,说简单点,就是通过设置ZooKeeper服务器上数据节点的ACL,来控制客户端对该数据节点的访问权限,针对这种控制方式,zookeeper提供了针对服务器上节点的多种权限控制模式(Schema)。

权限控制模式共有四种:

  • world:默认方式,相当于全世界都能访问
  • auth:代表已经认证通过的用户(cli中可以通过addauth digest user:pwd 来添加当前上下文中的授权用户)
  • digest:即用户名:密码这种方式认证,这也是业务系统中最常用的
  • ip:使用Ip地址认证

当然,在说这些权限控制模式前,还需要首先了解对节点的操作权限包括哪些:
CREATEREADWRITEDELETEADMIN 也就是 增、查、改、删、管理权限,这5种权限简写为crwda(即:每个单词的首字符缩写)。还有一个需要注意的地方:这5种权限中,delete是指对子节点的删除权限,其它4种权限只对自身节点的操作权限。

开发者要使用zookeeper的权限控制功能,则需要连接zookeeper后进行设置(客户端进行设置):
1、推荐使用digest模式:
(1)首先要增加一个认证用户

addauth digest 用户名:密码明文
比如:
>[zk: ……] addauth digest userName:123456

(2)设置权限

setAcl /path auth:用户名:密码明文:权限
比如:
>[zk: ……] setAcl /testNode auth:userName:123456:cdrwa

(3)查看Acl设置

getAcl /path

当然可以将上面的三步合并为一步:(不推荐)

setAcl /path digest:用户名:密码密文:权限

这里的加密规则是SHA1加密,然后base64编码。

// 将要设置的密码进行base64编码
/**
 * 以下命令说明:
 *  -n:代表不换行
 * dgst:是openssl的参数,代表生成信息摘要
 * -binary:二进制
 * -sha1:消息摘要算法
 * base64:编码算法
 **/
>echo -n userName:password | openssl dgst -binary -sha1 | openssl base64
输出:
>fEqNCco3Yq9h5zUg1Djt3lBs=
>create /digest userName
>setAcl /mytest digest:userName:fEqNCco3Yq9h5zUg1Djt3lBs=:rwadc
>getAcl /mytest 
输出:
>addauth digest userName:password

另外,zookeeper也已经提供了生成密码的工具类:

[root@root zookeeper-server01]# java -cp ./zookeeper-3.4.6.jar:./lib/log4j-1.2.16.jar:./lib/slf4j-log4j12-1.6.1.jar:./lib/slf4j-api-1.6.1.jar org.apache.zookeeper.server.auth.DigestAuthenticationProvider userName:password

>userName:dadT/grgRyRGDsdsfFgGh34frsfdaV28qNyn=

2、world schema固定id为anyone,表示对所有Client端开放权限

[zk: localhost:2181(CONNECTED) 14] getAcl /test

'world,'anyone

: cdrwa

3、ip scheme设置可以访问的ip地址或ip地址段(192.168.1.0/16)

[zk: 197.33.0.1:2181(CONNECTED) 1] setAcl /test ip:197.33.0.1:crwda

如果在别的电脑上访问,则出现:
Authentication is not valid : /test

4、SuperDigest超级管理员

如果节点的ACL权限密码忘记了怎么办?所以zookeeper是有超级管理员的概念的:SuperDigest超级管理员

设置步骤:
(1)修改zkServer.sh,加入super权限设置
(密码需要加密处理,同上)

SUPER_ACL="-Dzookeeper.DigestAuthenticationProvider.superDigest=super:gG7s8t3oDEtIqF6DM9LlI/R+9Ss="

(2)重新启动Zookeeper:

>sh zkServer.sh restart

(3)使用super:super进行认证:

>ls /test
Authentication is not valid : /test
>addauth digest super:super
>ls /test
[]
>get /test
Helloworld

推荐一篇文章:Zookeeper权限管理与Quota管理
https://www.cnblogs.com/linuxbug/p/5023677.html

精简如下:
(1)Zookeeper的ACL功能需要zk服务端和客户端协调完成。
(2)创建一个znode就会自动产生一个ACL,ACL中包括:验证模式、具体内容、权限。
(3)exists操作和getAcl操作并不受ACL许可控制,因此任何客户端可以查询节点的状态和节点的ACL。
(4)节点的权限主要有以下几种:

Create 允许对子节点Create操作
Read 允许对本节点GetChildren和GetData操作
Write 允许对本节点SetData操作
Delete 允许对子节点Delete操作
Admin 允许对本节点setAcl操作

Znode ACL权限用一个int型数字perms表示,perms的5个二进制位分别表示setacl、delete、create、write、read。比如0x1f=adcwr,0x1=----r,0x15=a-c-r。

(5)ACL缺陷:ACL毕竟仅仅是访问控制,并非完善的权限管理,通过这种方式做多集群隔离,还有很多局限性。ACL并无递归机制,任何一个znode创建后,都需要单独设置ACL,无法继承父节点的ACL设置;很多依赖zookeeper的开源框架并没有加入ACL的支持(例如:storm)。
(6)关于Zookeeper quota,参考:https://www.cnblogs.com/cc11001100/p/9971808.html

关于ZooKeeper之Java客户端API使用—权限控制,参考以下链接,不再赘述:
https://blog.csdn.net/en_joker/article/details/78771936

猜你喜欢

转载自blog.csdn.net/huxiutao/article/details/90038992