大数据基础学习-4.Zookeeper-3.4.5

一、Zookeeper-3.4.5

1.分布式协调技术

在开始zookeeper前,首先了解分布式协调技术。分布式协调技术主要用来解决分布式环境中多个进程之间的同步控制,让他们有序的去访问某个资源,防止造成"脏数据"。


在图中有三台机器,每台机器各跑一个应用程序。这三台机器通过网络连接起来,构成一个系统来为用户提供服务,这种系统称作分布式系统。

假设在第一台机器上挂载了一个资源,三个物理分布的进程都要获得这个资源,但同时进行访问就会造成“脏数据”结果。这时候就需要一个协调器,来让他们有序的来访问这个资源。这个协调器就是,比如说"进程-1"在使用该资源的时候,会先去获得锁,"进程1"获得锁以后会对该资源保持独占,这样其他进程就无法访问该资源,"进程1"用完该资源以后就将锁释放掉,让其他进程来获得锁,那么通过这个锁机制,就能保证了分布式系统中多个进程能够有序的访问该资源。分布式环境下的这个锁叫作分布式锁,也就是分布式协调技术实现的核心内容。

分布式协调远比在同一台机器上对多个进程的调度要难得多,而且如果为每一个分布式应用都开发一个独立的协调程序。一方面,协调程序的反复编写浪费,且难以形成通用、伸缩性好的协调器。另一方面,协调程序开销比较大,会影响系统原有的性能。所以,急需一种高可靠、高可用的通用协调机制来协调分布式应用。

我们所需要的分布式锁服务概括起来就是:一个松散耦合的分布式系统中粗粒度锁以及可靠性存储(低容量)的系统 。松散耦合是指对硬件性能的依赖性不强;分布式系统即分布式锁工作在分布式系统中;进程空间里的锁为细粒度锁,属于线程级的锁,粗粒度锁相对于细粒度锁来说,只需保证机器之间能顺利的协调工作就可以,而没必要细致到线程级别,因此说它是粗粒度的;可靠性存储是指这个系统能够存一些数据。

针对以上的需求,Google内部的分布式锁服务实现叫Chubby,基于Paxos协议,Apache的分布式锁服务叫ZooKeeper,基于Zab协议(ZooKeeper Atomic Broadcast 。Chubby是非开源的,Google自家用,雅虎模仿Chubby开发出了ZooKeeper,也实现了类似的分布式锁的功能,并且将ZooKeeper作为开源程序捐献给 Apache。

2.Zookeeper概述

ZooKeeper是一种为分布式应用所设计的高可用、高性能且一致的开源协调服务,它提供了一项基本服务:分布式锁服务。由于ZooKeeper的开源特性,在分布式锁的基础上,摸索了出了其他的使用方法:配置维护、组服务、分布式消息队列、分布式通知/协调等

ZooKeeper性能上的特点决定了它能够用在大型的、分布式的系统当中。从可靠性方面来说,它并不会因为一个节点的错误而崩溃。除此之外,它严格的序列访问控制意味着复杂的控制原语可以应用在客户端上。ZooKeeper在一致性、可用性、容错性的保证,也是ZooKeeper的成功之处,它获得的一切成功都与它采用的协议——Zab协议是密不可分的

ZooKeeper设计一种新的数据结构——Znode,然后在该数据结构的基础上定义了一些原语,也就是一些关于该数据结构的一些操作。有了这些数据结构和原语还不够,因为ZooKeeper是工作在一个分布式的环境下,服务是通过消息以网络的形式发送给分布式应用程序,所以还需要一个通知机制——Watcher机制。那么总结一下,ZooKeeper所提供的服务主要是通过:数据结构+原语+watcher机制,三个部分来实现的。

3.Zookeeper核心概念

ZooKeeper拥有一个层次的命名空间,这个和标准的文件系统非常相似


从图中可以看出ZooKeeper的数据模型,在结构上和标准文件系统的非常相似,都是采用这种树形层次结构,ZooKeeper树中的每个节点被称为—Znode。和文件系统的目录树一样,ZooKeeper树中的每个节点可以拥有子节点。但也有不同之处:

1) 引用方式

Znode通过路径引用,如同Unix中的文件路径。路径必须是绝对的,因此他们必须由斜杠字符来开头。除此以外,他们必须是唯一的,也就是说每一个路径只有一个表示,因此这些路径不能改变。在ZooKeeper中,路径由Unicode字符串组成,并且有一些限制。字符串"/zookeeper"用以保存管理信息,比如关键配置信息。

2) Znode结构

ZooKeeper命名空间中的Znode,兼具文件和目录两种特点。既像文件一样维护着数据、元信息、ACL、时间戳等数据结构,又像目录一样可以作为路径标识的一部分。图中的每个节点称为一个Znode。每个Znode由3部分组成:

① stat:此为状态信息, 描述该Znode的版本, 权限等信息

② data:与该Znode关联的数据

③ children:该Znode下的子节点

ZooKeeper虽然可以关联一些数据,但并没有被设计为常规的数据库或者大数据存储,相反的是,它用来管理调度数据,比如分布式应用中的配置文件信息、状态信息、汇集位置等等。这些数据的共同特性就是它们都是很小的数据,通常以KB为大小单位。ZooKeeper的服务器和客户端都被设计为严格检查并限制每个Znode的数据大小至多1M,但常规使用中应该远小于此值。

3) 数据访问

ZooKeeper中的每个节点存储的数据要被原子性的操作。也就是说读操作将获取与节点相关的所有数据,写操作也将替换掉节点的所有数据。另外,每一个节点都拥有自己的ACL(访问控制列表),这个列表规定了用户的权限,即限定了特定用户对目标节点可以执行的操作。


当客户端请求读取特定 znode 的内容时,读取操作是在客户端所连接的服务器上进行的。因此,由于只涉及集合体中的一个服务器,所以读取是快速和可扩展的。然而,为了成功完成写入操作,要求 ZooKeeper 集合体的严格意义上的多数节点都是可用的。在启动 ZooKeeper 服务时,集合体中的某个节点被选举为领导者。当客户端发出一个写入请求时,所连接的服务器会将请求传递给领导者。此领导者对集合体的所有节点发出相同的写入请求。如果严格意义上的多数节点(也被称为法定数量(quorum))成功响应该写入请求,那么写入请求被视为已成功完成。然后,一个成功的返回代码会返回给发起写入请求的客户端。如果集合体中的可用节点数量未达到法定数量,那么 ZooKeeper 服务将不起作用。

ZooKeeper有这样一个特性:集群中只要有超过过半的机器是正常工作的,那么整个集群对外就是可用的, 也就是说如果有2个ZooKeeper,那么只要有1个死了zookeeper就不能用了,因为1没有过半,所以2个ZooKeeper的死亡容忍度为0;同理,要是有3个ZooKeeper,一个死了,还剩下2个正常的,过半了,所以3个ZooKeeper的容忍度为1;同理你多列举几个:2->0;3->1;4->1;5->2;6->2会发现一个规律,2n和2n-1的容忍度是一样的,都是n-1,所以为了更加高效,通常都使用技术个ZooKeeper。  

【如果运行一台服务器,从ZooKeeper 的角度来看是没问题的;只是系统不再是高可靠或高可用的。三个节点的 ZooKeeper 集合体支持在一个节点故障的情况下不丢失服务,这对于大多数用户而言,这可能是没问题的,也可以说是最常见的部署拓扑。不过,为了安全起见,可以在集合体中使用五个节点。五个节点的集合体可以拿出一台服务器进行维护或滚动升级,并能够在不中断服务的情况下承受第二台服务器的意外故障。因此,在 ZooKeeper 集合体中,三、五或七是最典型的节点数量。ZooKeeper 集合体的大小与分布式系统中的节点大小没有什么关系。分布式系统中的节点将是 ZooKeeper 集合体的客户端,每个 ZooKeeper 服务器都能够以可扩展的方式处理大量客户端。例如,HBase(Hadoop 上的分布式数据库)依赖​​于ZooKeeper 实现区域服务器的领导者选举和租赁管理。可以利用一个相对较少(比如说,五个)节点的 ZooKeeper 集合体运行有 50 个节点的大型 HBase 集群。】

4) 节点类型

ZooKeeper中的节点有两种,分别为临时节点和永久节点。节点的类型在创建时即被确定,并且不能改变。

① 临时节点Ephemeral Nodes:该节点的生命周期依赖于创建它们的会话。一旦会话(Session)结束,临时节点将被自动删除,当然可以也可以手动删除。虽然每个临时的Znode都会绑定到一个客户端会话,但他们对所有的客户端还是可见的。如果会话又重新恢复,临时节点不会恢复,需要再进行注册。另外,ZooKeeper的临时节点不允许拥有子节点

② 永久节点Persistent Nodes:该节点的生命周期不依赖于会话,并且只有在客户端执行删除操作的时候,他们才能被删除。

③ 顺序节点Sequence Nodes(不能单独存在,需与上面两种节点组合),client申请创建该节点时,zk会自动在节点路径末尾添加递增序号,这种类型是实现分布式锁,分布式queue等特殊功能的关键。

5) watch

客户端可以在节点上设置watch,称之为监视器。当节点状态发生改变时(Znode的增、删、改)将会触发watch所对应的操作。当watch被触发时,ZooKeeper将会向客户端发送且仅发送一条通知,客户端被动收到通知,watch只能被触发一次,这样可以减少网络流量,触发监控后需要重新设置。

4.Zookeeper的角色

5.节点重要属性

属性 描述
czxid 节点被创建的Zxid值
mzxid   节点被修改的Zxid值
pzxid 子节点(或自身)被修改的时间
ctime 节点被创建的时间
mtime 节点最后一次被修改的时间
version 节点被修改的版本号
cversion 节点的所拥有子节点被修改的版本号
aversion
节点的ACL被修改的版本号
emphemeralOwner 如果此节点为临时节点,那么它的值为这个节点拥有者的会话ID;否则,它的值为0
dataLength 节点数据域的长度
numChildren 节点拥有的子节点个数

1) Zxid

致使ZooKeeper节点状态改变的每一个操作都将使节点接收到一个Zxid格式的时间戳,并且这个时间戳全局有序。也就是说,每个对节点的改变都将产生一个唯一的Zxid。如果Zxid1的值小于Zxid2的值,那么Zxid1所对应的事件发生在Zxid2所对应的事件之前。实际上,ZooKeeper的每个节点维护者三个Zxid值,为别为:cZxid、mZxid、pZxid。

① cZxid:是节点的创建时间所对应的Zxid格式时间戳。

② mZxid:是节点的修改时间所对应的Zxid格式时间戳。

③ pZxid:这个值只和该节点的子节点有关,是该节点的子节点(如果没有子节点及时节点本身)最近一次 【创建/删除 】的时间戳。【只与本节点 或者该节点的子节点有关,与孙子节点无关】

实现中Zxid是一个64为的数字,它高32位是epoch,用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch。低32位是个递增计数。

2)版本号

对节点的每一个操作都将致使这个节点的版本号增加。每个节点维护着三个版本号,他们分别为:

① version:节点被修改的版本号

② cversion:子节点被修改的版本号

③ aversion:节点所拥有的ACL版本号

6.数据访问

– 每个节点上的“访问控制链” (ACL,AccessControl List)保存了各客户端对于该节点的访问权限。

– 用一个三元组来定义客户端的访问权限:(scheme模式,expression,permissions权限)

• ip:19.22.0.0/16,READ   表示IP地址以19.22开头的主机有该数据节点的读权限。permissions权限列表如下。

权限 

描述
CREATE  有创建子节点的权限
READ  有读取节点数据和子节点列表的权限
WRITE  有修改节点数据的权限 无创建和删除子节点的权限
DELETE  有删除子节点的权限
ADMIN  有设置节点权限的权限

ZooKeeper本身提供了ACL机制,表示为scheme:id,permissions,第一个字段表示采用哪一种机制,第二个id表示用户,permissions表示相关权限(如只读,读写,管理等)。scheme列表如下。

模式

描述
World  它下面只有一个id, 叫anyone,world:anyone代表任何人,zookeepe中对所有人有权限的结点就是属于world:anyone的r
Auth  已经被认证的用户
Digest 通过username:password字符串的MD5编码认证用户
Host 匹配主机名后缀,如,host:corp.com匹配host:host1.corp.com,host:host2.corp.com,但不能匹配host:host1.store.com
IP  通过IP识别用户,表达式格式为addr/bits
public class NewDigest {
    public static void main(String[] args) throws Exception {
        List<ACL> acls = newArrayList<ACL>();
        //添加第一个id,采用用户名密码形式
        Id id1 = new Id("digest",
        DigestAuthenticationProvider.generateDigest("admin:admin"));
        ACL acl1 = new ACL(ZooDefs.Perms.ALL,id1);
        acls.add(acl1);
        //添加第二个id,所有用户可读权限
        Id id2 = new Id("world","anyone");
        ACL acl2 = new ACL(ZooDefs.Perms.READ,id2);
        acls.add(acl2);
        // zk用admin认证,创建/testZNode。
        ZooKeeper zk = newZooKeeper("host1:2181,host2:2181,host3:2181", 2000, null);
        zk.addAuthInfo("digest","admin:admin".getBytes());
        zk.create("/test","data".getBytes(), acls, CreateMode.PERSISTENT);
    }
}

代码分析:

先创建了两个ACL访问的方式(id1/id2),id1采用的是digest,即用户密码模式,拥有所有的权限;id2对所有用户提供read权限。然后创建/test节点,数据是“data”,访问该节点的权限有两种(id1/id2),类型是永久性节点。

7.关键代码

客户端要连接 Zookeeper 服务器可以通过创建 org.apache.zookeeper. ZooKeeper 的一个实例对象,然后调用这个类提供的接口来和服务器交互。

• String create(String path, byte[] data, List<ACL> acl, CreateMode createMode)

– 创建一个给定的目录节点 path, 并给它设置数据,CreateMode标识有四种形式的目录节点,分别是 PERSISTENT:持久化目录节点,这个目录节点存储的数据不会丢失;PERSISTENT_SEQUENTIAL:顺序自动编号的目录节点,这种目录节点会根据当前已近存在的节点数自动加 1,然后返回给客户端已经成功创建的目录节点名;EPHEMERAL:临时目录节点,一旦创建这个节点的客户端与服务器端口也就是 session 超时,这种节点会被自动删除;EPHEMERAL_SEQUENTIAL:临时自动编号节点。

• Stat exists(String path, boolean watch)

– 判断某个 path 是否存在,并设置是否监控这个目录节点,这里的 watcher 是在创建ZooKeeper 实例时指定的 watcher。

• Stat exists(String path, Watcher watcher)

– 重载方法,这里给某个目录节点设置特定的watcher,Watcher 在 ZooKeeper 是一个核心功能,Watcher 可以监控目录节点的数据变化以及子目录的变化,一旦这些状态发生变化,服务器就会通知所有设置在这个目录节点上的 Watcher,从而每个客户端都很快知道它所关注的目录节点的状态发生变化,而做出相应的反应。

• void delete(String path, int version)

– 删除 path 对应的目录节点,version为 -1 可以匹配任何版本,也就删除了这个目录节点所有数据。

• List<String> getChildren(String path, boolean watch)

– 获取指定 path 下的所有子目录节点,同样getChildren方法也有一个重载方法可以设置特定的 watcher 监控子节点的状态。

• Stat setData(String path, byte[] data, int version)

– 给 path 设置数据,可以指定这个数据的版本号,如果 version 为 -1 怎可以匹配任何版本。

• byte[] getData(String path, boolean watch, Stat stat)

– 获取这个 path 对应的目录节点存储的数据,数据的版本等信息可以通过 stat 来指定,同时还可以设置是否监控这个目录节点数据的状态。

• void addAuthInfo(String scheme, byte[] auth)

– 客户端将自己的授权信息提交给服务器,服务器将根据这个授权信息验证客户端的访问权限。

•List<ACL> getACL(String path, Stat stat)

– 获取某个目录节点的访问权限列表

• Stat setACL(String path, List<ACL> acl, int version)

– 给某个目录节点重新设置访问权限,需要注意的是 Zookeeper 中的目录节点权限不具有传递性,父目录节点的权限不能传递给子目录节点。目录节点 ACL 由两部分组成:perms 和id。 Perms 有 ALL、 READ、 WRITE、 CREATE、 DELETE、 ADMIN 几种而 id 标识了访问目录节点的身份列表,默认情况下有以下两种: ANYONE_ID_UNSAFE = new Id("world","anyone") 和 AUTH_IDS =new Id("auth", "") 分别表示任何人都可以访问和创建者拥有访问权限。

除了以上这些上表中列出的方法之外还有一些重载方法,如都提供了一个回调类的重载方法以及可以设置特定 Watcher 的重载方法,具体的方法可以参考 org.apache.zookeeper. ZooKeeper 类的 API 说明。

二、zookeeper使用

1.创建目录

在zookeeper根目录下,创建一个目录,用它来存储与 ZooKeeper 服务器有关联的一些状态:mkdir data。

2.配置conf/zoo.cfg 

tickTime=2000
dataDir=/usr/local/src/zookeeper-3.4.5-cdh5.7.0/data
clientPort=2181
initLimit=5
syncLimit=2
server.1=masteractive:2888:3888
server.2=slave1:2888:3888
server.3=slave2:2888:3888

值得重点注意的一点是,所有三个机器都应该打开端口 2181、2888 和 3888。在本例中,端口 2181 由 ZooKeeper 客户端使用,用于连接到 ZooKeeper 服务器;端口 2888 由对等 ZooKeeper 服务器使用,用于互相通信;而端口 3888 用于领导者选举。可以选择自己喜欢的任何端口。通常建议在所有 ZooKeeper 服务器上使用相同的端口。

【注意如果配置了三台机器,则进行练习时就要开启三台机器,否则zookeeper会认为集群不可用,将会报错,在学习时,可以将另外两台机子注释掉】

3.myid

创建一个 touch –p /usr/local/src/zookeeper-3.4.5-cdh5.7.0/data/myid 文件。此文件的内容将只包含一个全局唯一的数字,作为识别码,和server.后面的数字要对应起来。例如对于masteractive主机,myid的值是1。

4.启动

• zkServer.sh start(每台机器都要启动,否则报错)

• zkServer.sh status 查看状态

• zkServer.sh stop 停止zookeeper

• zkCli.sh -server masteractive:2181    连接到masteractive的2181端口,即连接zookeeper服务

 • 执行客户端 zkCli.sh

– ls / 查看当前目录
– create /text "test"     创建节点,名称为test
– create -e /text "test" 创建临时节点
– create -s /text "test" 创建序列节点
– get /test 查看节点
– rmr /test 删除节点
– delete /test 删除节点(只能删除无子节点的节点)
– stat /test 查看元数据

5.watch编程

编写watch,根据znode的情况触发相应的事件。

package testZK;

import java.io.IOException;

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;

public class testZK implements Watcher {
	public ZooKeeper zk = null;
	boolean bk = true;

	public void CreateConnection(String connectString, int timeout) {
		try {
			zk = new ZooKeeper(connectString, timeout, null);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public void releaseConnection() {
		try {
			this.zk.close();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public void createPath(String path, String data) {
		try {
//			this.zk.create(path, data.getBytes(), Ids.OPEN_ACL_UNSAFE,
//					CreateMode.EPHEMERAL);
			this.zk.create(path, data.getBytes(), Ids.OPEN_ACL_UNSAFE,
					CreateMode.PERSISTENT);
		} catch (KeeperException e) {
			e.printStackTrace();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public String readData(String path) {
		String ret = null;
		try {
			ret = new String(this.zk.getData(path, false, null));
		} catch (KeeperException e) {
			e.printStackTrace();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return ret;
	}

	public void writeData(String path, String data) {
		try {
			this.zk.setData(path, data.getBytes(), -1);
		} catch (KeeperException e) {
			e.printStackTrace();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public void deleteNode(String path) {
		try {
			this.zk.delete(path, -1);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (KeeperException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public void isExist(String path) {
		try {
			this.zk.exists(path, this);
		} catch (KeeperException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public void getChildren(String path) {
		try {
			this.zk.getChildren(path, this);
		} catch (KeeperException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public void getData(String path) {
		try {
			this.zk.getData(path, this, null);
		} catch (KeeperException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		String zkpath = "/testzk";
		testZK test = new testZK();
		test.CreateConnection("192.168.101.101:2181", 100000);
		test.createPath(zkpath, "123"); 

		String ret = test.readData(zkpath);
		System.out.println("get data :" + ret);
				
		test.writeData(zkpath, "321");

		ret = test.readData(zkpath);
		System.out.println("get data :" + ret);

		test.createPath("/test2", "123");
		test.getData("/test2"); //这里可以设置监控,一旦test2被操作,就会触发process
		test.getChildren("/test2");
		test.isExist("/test2/node1");
		
		test.createPath("/test2/test3", "123");

		while (test.bk) {
			System.out.print(".");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		test.releaseConnection();
	}

	@Override
	public void process(WatchedEvent arg0) {
		// TODO Auto-generated method stub这里编写自己的业务实现
		System.out.println("get event" + arg0.getState() + " # "+ arg0.getType() + "\n");
		this.bk = false;
	}

}

可在process中编写代码实现具体的业务。

三、应用场景

Zookeeper 从设计模式角度来看,是一个基于观察者模式设计(watch)的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper 就将负责通知已经在 Zookeeper 上注册的那些观察者做出相应的反应,从而实现集群中类似 Master/Slave 管理模式,关于 Zookeeper 的详细架构等内部细节可以阅读 Zookeeper 的源码。下面详细介绍这些典型的应用场景,也就是 Zookeeper 到底能帮我们解决那些问题。

1.配置管理(Configuration Management)


配置的管理在分布式应用环境中很常见,例如同一个应用系统需要多台 PC Server 运行,但是它们运行的应用系统的某些配置项是相同的,如果要修改这些相同的配置项,那么就必须同时修改每台运行这个应用系统的 PC Server,这样非常麻烦而且容易出错。像这样的配置信息完全可以交给 Zookeeper 来管理,将配置信息保存在 Zookeeper 的某个节点中,然后将所有需要修改的应用机器监控配置信息的状态,一旦配置信息发生变化,每台应用机器就会收到 Zookeeper 的通知,然后从 Zookeeper 获取新的配置信息应用到系统中。

2.统一命名服务(Name Service)

分布式应用中,通常需要有一套完整的命名规则,既能够产生唯一的名称又便于人识别和记住,通常情况下用树形的名称结构是一个理想的选择,树形的名称结构是一个有层次的目录结构,既对人友好又不会重复。说到这里你可能想到了 JNDI,没错 Zookeeper 的 Name Service 与 JNDI 能够完成的功能是差不多的,它们都是将有层次的目录结构关联到一定资源上,但是 Zookeeper 的 Name Service 更加是广泛意义上的关联,也许你并不需要将名称关联到特定资源上,你可能只需要一个不会重复名称,就像数据库中产生一个唯一的数字主键一样。Name Service 已经是 Zookeeper 内置的功能,你只要调用 Zookeeper 的 API 就能实现。如调用 create 接口就可以很容易创建一个目录节点。

3.集群管理(Group Membership)

Zookeeper 能够很容易的实现集群管理的功能,如有多台 Server 组成一个服务集群,那么必须要一个“总管”知道当前集群中每台机器的服务状态,一旦有机器不能提供服务,集群中其它集群必须知道,从而做出调整重新分配服务策略。同样当增加集群的服务能力时,就会增加一台或多台 Server,同样也必须让“总管”知道。

Zookeeper 不仅能够帮你维护当前的集群中机器的服务状态,而且能够帮你选出一个“总管”,让这个总管来管理集群,这就是 Zookeeper 的另一个功能 Leader Election

它们的实现方式都是在 Zookeeper 上创建一个 EPHEMERAL 类型的目录节点,然后每个 Server 在它们创建目录节点的父目录节点上调用 getChildren(String path, boolean watch) 方法并设置 watch 为 true,由于是 EPHEMERAL 目录节点,当创建它的 Server 死去,这个目录节点也随之被删除,所以 Children 将会变化,这时 getChildren上的 Watch 将会被调用,所以其它 Server 就知道已经有某台 Server 死去了。新增 Server 也是同样的原理。

Zookeeper 如何实现 Leader Election,也就是选出一个 Master Server。和前面的一样每台 Server 创建一个 EPHEMERAL 目录节点,不同的是它还是一个 SEQUENTIAL 目录节点,所以它是个 EPHEMERAL_SEQUENTIAL 目录节点。之所以它是 EPHEMERAL_SEQUENTIAL 目录节点,是因为我们可以给每台 Server 编号,我们可以选择当前是最小编号的 Server 为 Master,假如这个最小编号的 Server 死去,由于是 EPHEMERAL 节点,死去的 Server 对应的节点也被删除,所以当前的节点列表中又出现一个最小编号的节点,我们就选择这个节点为当前 Master。这样就实现了动态选择 Master,避免了传统意义上单 Master 容易出现单点故障的问题。


4.共享锁

共享锁在同一个进程中很容易实现,但是在跨进程或者在不同 Server 之间就不好实现了。Zookeeper 却很容易实现这个功能,实现方式也是需要获得锁的 Server 创建一个 EPHEMERAL_SEQUENTIAL 目录节点,然后调用 getChildren方法获取当前的目录节点列表中最小的目录节点是不是就是自己创建的目录节点,如果正是自己创建的,那么它就获得了这个锁,如果不是那么它就调用exists(String path, boolean watch) 方法并监控 Zookeeper 上目录节点列表的变化,一直到自己创建的节点是列表中最小编号的目录节点,从而获得锁,释放锁很简单,只要删除前面它自己所创建的目录节点就行了。


5.队列管理

Zookeeper 可以处理两种类型的队列:

1)当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达,这种是同步队列。

2)队列按照 FIFO 方式进行入队和出队操作,例如实现生产者和消费者模型。

同步队列用 Zookeeper 实现的实现思路如下:创建一个父目录 /synchronizing,每个成员都监控标志(Set Watch)位目录 /synchronizing/start 是否存在,然后每个成员都加入这个队列,加入队列的方式就是创建 /synchronizing/member_i 的临时目录节点,然后每个成员获取 / synchronizing 目录的所有目录节点,也就是 member_i。判断 i 的值是否已经是成员的个数,如果小于成员个数等待 /synchronizing/start 的出现,如果已经相等就创建 /synchronizing/start。

6.FIFO

– 创建 SEQUENTIAL 类型的子目录 /queue_i,这样就能保证所有成员加入队列时都是有编号的,出队列时通过getChildren( ) 方法可以返回当前所有的队列中的元素,然后消费其中最小的一个,这样就能保证 FIFO。

四、zab协议

参考:http://www.cnblogs.com/hongdada/p/8145075.html

为了保证写操作的一致性与可用性,Zookeeper专门设计了一种名为原子广播(ZAB)的支持崩溃恢复的一致性协议。基于该协议,Zookeeper实现了一种主从模式的系统架构来保持集群中各个副本之间的数据一致性。根据ZAB协议,所有的写操作都必须通过Leader完成,Leader写入本地日志后再复制到所有的Follower节点。一旦Leader节点无法工作,ZAB协议能够自动从Follower节点中重新选出一个合适的替代者,即新的Leader,该过程即为领导选举。该领导选举过程,是ZAB协议中最为重要和复杂的过程。下面对zab进行学习。

 1.概述

ZooKeeper为高可用的一致性协调框架,自然的ZooKeeper也有着一致性算法的实现,ZooKeeper使用的是ZAB协议作为数据一致性的算法, ZAB(ZooKeeper Atomic Broadcast ) 全称为:原子消息广播协议。

ZAB可以说是在Paxos算法基础上进行了扩展改造而来的,ZAB协议设计了支持崩溃恢复,ZooKeeper使用单一主进程Leader用于处理客户端所有事务请求,采用ZAB协议将服务器数状态以事务形式广播到所有Follower上;由于事务间可能存在着依赖关系,ZAB协议保证Leader广播的变更序列被顺序的处理:一个状态被处理那么它所依赖的状态也已经提前被处理。

ZAB协议支持的崩溃恢复可以保证在Leader进程崩溃的时候可以重新选出Leader并且保证数据的完整性。在ZooKeeper中所有的事务请求都由一个主服务器也就是Leader来处理,其他服务器为Follower,Leader将客户端的事务请求转换为事务Proposal,并且将Proposal分发给集群中其他所有的Follower,然后Leader等待Follwer反馈,当有过半数(>=N/2+1) 的Follower反馈信息后,Leader将再次向集群内Follower广播Commit信息,Commit为将之前的Proposal提交。

1)写leader

通过Leader进行写操作流程如下图所示

由上图可见,通过Leader进行写操作,主要分为五步:
1.客户端向Leader发起写请求
2.Leader将写请求以Proposal的形式发给所有Follower并等待ACK
3.Follower收到Leader的Proposal后返回ACK

4Leader得到过半数的ACK(Leader对自己默认有一个ACK)后向所有的Follower和Observer发送Commmit
5.Leader将处理结果返回给客户端

这里要注意:

Leader并不需要得到Observer的ACK,即Observer无投票权。Leader不需要得到所有Follower的ACK,只要收到过半的ACK即可,同时Leader本身对自己有一个ACK。上图中有4个Follower,只需其中两个返回ACK即可,因为(2+1) / (4+1) > 1/2。Observer虽然无投票权,但仍须同步Leader的数据从而在处理读请求时可以返回尽可能新的数据。

2)写follower/observer

通过Follower/Observer进行写操作流程:Follower/Observer均可接受写请求,但不能直接处理,而需要将写请求转发给Leader处理除了多了一步请求转发,其它流程与直接写Leader无任何区别。

3)读

Leader/Follower/Observer都可直接处理读请求,从本地内存中读取数据并返回给客户端即可。

由于处理读请求不需要服务器之间的交互,Follower/Observer越多,整体可处理的读请求量越大,也即读性能越好。

2.数据一致性

ZooKeeper从以下几点保证了数据的一致性

① 顺序一致性:来自任意特定客户端的更新都会按其发送顺序被提交。也就是说,如果一个客户端将Znode z的值更新为a,在之后的操作中,它又将z的值更新为b,则没有客户端能够在看到z的值是b之后再看到值a(如果没有其他对z的更新)。

② 原子性:每个更新要么成功,要么失败。这意味着如果一个更新失败,则不会有客户端会看到这个更新的结果。

③ 单一系统映像:一个客户端无论连接到哪一台服务器,它看到的都是同样的系统视图。这意味着,如果一个客户端在同一个会话中连接到一台新的服务器,它所看到的系统状态不会比在之前服务器上所看到的更老。当一台服务器出现故障,导致它的一个客户端需要尝试连接集合体中其他的服务器时,所有滞后于故障服务器的服务器都不会接受该连接请求,除非这些服务器赶上故障服务器。

④ 持久性:一个更新一旦成功,其结果就会持久存在并且不会被撤销。这表明更新不会受到服务器故障的影响。

3.FastLeaderElection

参考:http://www.jasongj.com/zookeeper/fastleaderelection/

4.zab模式

ZAB协议的两个基本模式:恢复模式和广播模式

1)恢复模式:(选举)

当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数server完成了和leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和server具有相同的系统状态。崩溃恢复过程中,为了保证数据一致性需要处理特殊情况:

1、已经被leader提交的proposal确保最终被所有的服务器follower提交

2、确保那些只在leader被提出的proposal被丢弃

针对这个要求,如果让leader选举算法能够保证新选举出来的Leader服务器拥有集群中所有机器最高的ZXID事务proposal,就可以保证这个新选举出来的Leader一定具有所有已经提交的proposal,也可以省去Leader服务器检查proposal的提交与丢弃的工作。

2)广播模式:(数据同步)

一旦Leader已经和多数的Follower进行了状态同步后,他就可以开始广播消息了,即进入广播状态。

这时候当一个Server加入ZooKeeper服务中,它会在恢复模式下启动,发现Leader,并和Leader进行状态同步。待到同步结束,它也参与消息广播。ZooKeeper服务一直维持在广播状态,直到Leader崩溃了或者Leader失去了大部分的Followers支持。

ZAB协议简化了2PC事务提交:

1、去除中断逻辑移除,follower要么ack,要么抛弃Leader;

2、leader不需要所有的Follower都响应成功,只要一个多数派ACK即可。

丢弃的事务proposal处理过程:ZAB协议中使用ZXID作为事务编号,ZXID为64位数字,低32位为一个递增的计数器,每一个客户端的一个事务请求时Leader产生新的事务后该计数器都会加1,高32位为Leader周期epoch编号,当新选举出一个Leader节点时Leader会取出本地日志中最大事务Proposal的ZXID解析出对应的epoch把该值加1作为新的epoch,将低32位从0开始生成新的ZXID;ZAB使用epoch来区分不同的Leader周期,能有效避免了不同的leader服务器错误的使用相同的ZXID编号提出不同的事务proposal的异常情况,大大简化了提升了数据恢复流程。所以这个崩溃的机器启动时,也无法成为新一轮的Leader,因为当前集群中的机器一定包含了更高的epoch的事务proposal。


猜你喜欢

转载自blog.csdn.net/qq_25560849/article/details/80087097