Zookeeper详细介绍

ZooKeeper是什么?

ZooKeeper是一个分布式应用程序协调服务,提供的功能包括:配置维护、命名服务、分布式同步、组服务等。最终封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。

ZooKeeper数据模型

ZooKeeper包含一个树形的数据模型,我们叫做znode。一个znode中包含了存储的数据和ACL(Access Control List)。ZooKeeper的设计适合存储少量的数据,并不适合存储大量数据,所以znode的存储限制最大不超过1M。

数据节点(znode)类型

每个子目录项如 NameService 都被称作为znode,和文件系统一样,我们能够自由的增加、删除znode,在一个znode下增加、删除子znode,唯一的不同在于znode是可以存储数据的。 有四种类型的znode:

  1. PERSISTENT-持久化目录节点:客户端与zookeeper断开连接后,该节点依旧存在
  2. PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点: 客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号
  3. EPHEMERAL-临时目录节点:客户端与zookeeper断开连接后,该节点被删除
  4. EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点:客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号

其中:如果在创建znode时,我们使用PERSISTENT_SEQUENTIAL或者EPHEMERAL_SEQUENTIAL,ZooKeeper会在我们指定的znode名字后面增加一个数字。我们继续加入相同名字的znode时,这个数字会不断增加。这个序号的计数器是由这些排序znode的父节点来维护的。如:

访问控制(Access Control List)

ZooKeeper树形的数据模型znode外。还有访问控制ACL(Access Control List)。

znode的创建时,我们会给他一个ACL(Access Control List),来决定谁可以对znode做哪些操作。

ACL 权限控制,使用:schema:Id:permission 来标识,主要涵盖 3 个方面:

  • 权限模式(Schema):鉴权方式
  • 授权对象(ID)
  • 权限(Permission)

鉴权方式

ZooKeeper通过鉴权来获得客户端的身份,然后通过ACL来控制客户端的访问。鉴权方式有4种: (1)world:默认方式,只有一个用户:anyone,代表所有人 (2)auth:代表已经认证通过的用户(cli中可以通过addauth digest user:pwd 来添加当前上下文中的授权用户) (3)digest:即用户名:密码(密文)这种方式认证,这也是业务系统中最常用的 (4)ip:使用ip地址认证

ID

授权对象ID是指,权限赋予的用户或者一个实体,例如:IP 地址或者机器。授权模式 schema 与 授权对象 ID 之间关系

权限

zookeeper的节点有5种操作权限:CREATE、READ、WRITE、DELETE、ADMIN 也就是 增、删、改、查、管理权限,这5种权限简写为crwda(即:每个单词的首字符缩写)

ACL permission Permitted operations
CREATE create (a child znode) READ getChildren,getData
WRITE setData
DELETE delete (a child znode) ADMIN setACL

在ZooDefs.Ids类中,有一些ACL的预定义变量,包括OPEN_ACL_UNSAFE,这个设置表示将赋予所有的许可给客户端(除了ADMIN的许可)。

权限相关命令

命令 使用方式 描述
getAcl getAcl <path> 读取ACL权限 setAcl setAcl <path> <acl> 设置ACL权限 addauth addauth <scheme> <auth> 添加认证用户

Cli命令行方式

设置world方式

setAcl <path> world:anyone:<acl>

Cli方式实例

[zk: localhost:2181(CONNECTED) 0] create /node1 1
Created /node1
 
[zk: localhost:2181(CONNECTED) 1] getAcl /node1
'world,'anyone  #默认为world方案
: cdrwa #任何人都拥有所有权限
 
#可以用以下方式设置:
[zk: localhost:2181(CONNECTED) 2] setAcl /node1 world:anyone:cdrwa
cZxid = 0x19000002a1
ctime = Thu May 11 22:00:00 CST 2017
mZxid = 0x19000002a1
mtime = Thu May 11 22:00:00 CST 2017
pZxid = 0x19000002a1
cversion = 0
dataVersion = 0
aclVersion = 1
ephemeralOwner = 0x0
dataLength = 1
numChildren = 0

设置IP方案

  • 设置方式

    setAcl <path> ip:<ip>:<acl>

<ip>:可以是具体IP也可以是IP/bit格式,即IP转换为二进制,匹配前bit位,如192.168.0.0/16匹配192.168..

  • 客户端实例

    [zk: localhost:2181(CONNECTED) 0] create /node2 1 Created /node2

    [zk: localhost:2181(CONNECTED) 1] setAcl /node2 ip:192.168.100.1:cdrwa #设置IP:192.168.100.1 拥有所有权限 cZxid = 0x1900000239 ctime = Thu May 11 22:00:00 CST 2017 mZxid = 0x1900000239 mtime = Thu May 11 22:00:00 CST 2017 pZxid = 0x1900000239 cversion = 0 dataVersion = 0 aclVersion = 1 ephemeralOwner = 0x0 dataLength = 1 numChildren = 0

    [zk: localhost:2181(CONNECTED) 2] getAcl /node2 'ip,'192.168.100.1 : cdrwa

    #使用IP非 192.168.100.1 的机器 [zk: localhost:2181(CONNECTED) 0] get /node2 Authentication is not valid : /node2 #没有权限

    [zk: localhost:2181(CONNECTED) 1] delete /node2

Auth方案

设置方式

addauth digest <user>:<password> #添加认证用户
setAcl <path> auth:<user>:<acl>

设置步骤

  • 创建节点

create /test1 'test-data'

  • 获取默认访问权限

getAcl /test1

  • 添加认证用户

addauth digest user1:12345

  • 设置访问权限

setAcl /test1 auth:user1:12345:r

设置已认证通过的用户权限

Digest方案

设置方式

setAcl <path> digest:<user>:<password>:<acl>

设置步骤

  • 创建节点

create /test 'test-data'

  • 获取默认访问权限

getAcl /test

通过getAcl命令可以发现,刚创建的节点,默认是 world,anyone的认证方式,具有cdrwa所有权限

  • 设置访问权限

setAcl /test digest:user1:+owfoSBn/am19roBPzR1/MfCblE=:r

说明:setAcl /test digest:用户名:密码:权限 给节点设置ACL访问权限时,密码必须是加密后的内容,这里的+owfoSBn/am19roBPzR1/MfCblE=,对应的原文是12345 (至于这个密文怎么得来的,后面会讲到,这里先不管这个),设置完Acl后,可以通过getAcl查看结果

设置后通过get /test返回没有权限

  • 添加认证用户

addauth digest user1:12345 给"上下文"增加了一个认证用户,即对应刚才setAcl的设置,然后再 get /test 就能取到数据了

  • 删除节点

delete /test

秘钥加密规则说明:

这里的密码是经过SHA1及BASE64处理的密文,在SHELL中可以通过以下命令计算:

echo -n <user>:<password> | openssl dgst -binary -sha1 | openssl base64

先来计算一个密文

echo -n yoonper:123456 | openssl dgst -binary -sha1 | openssl base64

static public String generateDigest(String idPassword)
        throws NoSuchAlgorithmException {
    String parts[] = idPassword.split(":", 2);
    byte digest[] = MessageDigest.getInstance("SHA1").digest(
            idPassword.getBytes());
    return parts[0] + ":" + base64Encode(digest);
}

就是SHA1加密,然后base64编码,即username:BASE64(SHA-1(username:password))

客户端可以在与ZooKeeper建立会话连接后,自己给自己授权。授权是并不是必须的,虽然znode的ACL要求客户端必须是身份合法的,在这种情况下,客户端可以自己授权来访问znode。下面的例子,客户端使用用户名和密码为自己授权:

`zk.addAuthInfo(``"digest"``, ``"tom:secret"``.getBytes());`

ACL是由鉴权方式、鉴权方式的ID和一个许可(permession)的集合组成。例如,我们想通过一个ip地址为10.0.0.1的客户端访问一个znode。那么,我们需要为znode设置一个ACL,鉴权方式使用IP鉴权方式,鉴权方式的ID为10.0.0.1,只允许读权限。使用JAVA我们将像如下方式创建一个ACL对象:

`new` `ACL(Perms.READ,``new` `Id(``"ip"``, ``"10.0.0.1"``));`

所有的许可权限将在下表中列出。请注意,exists操作不受ACL的控制,所以任何一个客户端都可以通过exists操作来获得任何znode的状态,从而得知znode是否真的存在。

另外,我们可以使用ZooKeeper鉴权的插件机制,来整合第三方的鉴权系统。

Zookeeper通知机制

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

观察模式 Watcher

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

观察模式可以使客户端在某一个znode发生变化时得到通知。观察模式有ZooKeeper服务的某些操作启动,并由其他的一些操作来触发。例如,一个客户端对一个znode进行了exists操作,来判断目标znode是否存在,同时在znode上开启了观察模式。如果znode不存在,这exists将返回false。如果稍后,另外一个客户端创建了这个znode,观察模式将被触发,将znode的创建事件通知之前开启观察模式的客户端。我们将在以后详细介绍其他的操作和触发。

观察模式只能被触发一次。如果要一直获得znode的创建和删除的通知,那么就需要不断的在znode上开启观察模式。在上面的例子中,如果客户端还继续需要获得znode被删除的通知,那么在获得创建通知后,客户端还需要继续对这个znode进行exists操作,再开启一次观察模式。

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

在上图中:

  • ZooKeeper :部署在远程主机上的 ZooKeeper 集群,当然,也可能是单机的。
  • Client :分布在各处的 ZooKeeper 的 jar 包程序,被引用在各个独立应用程序中。
  • WatchManager :一个接口,用于管理各个监听器,只有一个方法 materialize(),返回一个 Watcher 的 set。 在具体流程上,简单讲,客户端在向 ZooKeeper 服务器注册 Watcher 的同时,会将 Watcher 对象存储在客户端的 WatchManager 中。当ZooKeeper 服务器触发 Watcher 事件后,会向客户端发送通知,客户端线程从 WatchManager 的实现类中取出对应的 Watcher 对象来执行回调逻辑。

客户端注册Watcher

在创建一个 ZooKeeper 客户端对象实例时,可以向构造方法中传入一个默认的 Watcher:

public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)

Stat	exists(java.lang.String path, Watcher watcher)
Return the stat of the node of the given path.

java.util.List<java.lang.String>	getChildren(java.lang.String path, Watcher watcher)
Return the list of the children of the node of the given path.

void	getData(java.lang.String path, Watcher watcher, AsyncCallback.DataCallback cb, java.lang.Object ctx)
The asynchronous version of getData.

ZooKeeper构造函数参数 Watcher 将作为整个 ZooKeeper会话期间的默认 Watcher,会一直被保存在客户端 ZKWatchManager 的 defaultWatcher 中。另外,ZooKeeper 客户端也可以通过 getData、exists 和 getChildren 三个接口来向 ZooKeeper 服务器注册 Watcher,无论哪种方式,注册 Watcher 的工作原理都是一致的。其中:

  • exists启动的观察模式,由创建znode,删除znode和更新znode操作来触发。
  • getData启动的观察模式,由删除znode和更新znode操作触发。创建znode不会触发,是因为getData操作成功的前提是znode必须已经存在。
  • getChildren启动的观察模式,由子节点创建和删除,或者本节点被删除时才会被触发。我们可以通过事件的类型来判断是本节点被删除还是子节点被删除:NodeChildrenChanged表示子节点被删除,而NodeDeleted表示本节点删除。

Watcher--ZK状态,事件类型

zookeeper有watch事件,是一次性触发的,当watch监视的数据发生变化时,通知设置了该watch的client.即watcher. 同样:其watcher是监听数据发送了某些变化,那就一定会有对应的事件类型和状态类型。

状态类型:(是跟客户端实例相关的)

  • KeeperState:Disconneced //连接失败 KeeperState:SyncConnected //连接成功
  • KeeperState:AuthFailed //认证失败
  • KeeperState:Expired //会话过期

事件类型:(znode节点相关的)

  • EventType:NodeCreated //节点创建
  • EventType:NodeDataChanged //节点的数据变更
  • EventType:NodeChildrentChanged //子节点下的数据变更
  • EventType:NodeDeleted

分别对应WatchedEvent

event.getState() -- ZK连接状态

event.getType() -- 时间类型

通知的状态类型与事件类型之间关系

在Watcher接口类中,已经定义了所有的状态类型和事件类型,这里把各个状态和事件类型之间的关系整理一下。

  • 状态:KeeperState.Disconnected(0) 此时客户端处于断开连接状态,和ZK集群都没有建立连接。 1)事件:EventType.None(-1) 触发条件:一般是在与服务器断开连接的时候,客户端会收到这个事件。
  • 状态:KeeperState. SyncConnected(3) 1)事件:EventType.None(-1) 触发条件:客户端与服务器成功建立会话之后,会收到这个通知。 2)事件:EventType. NodeCreated (1) 触发条件:所关注的节点被创建。 3)事件:EventType. NodeDeleted (2) 触发条件:所关注的节点被删除。 4)事件:EventType. NodeDataChanged (3) 触发条件:所关注的节点的内容有更新。注意,这个地方说的内容是指数据的版本号dataVersion。因此,即使使用相同的数据内容来更新,还是会收到这个事件通知的。无论如何,调用了更新接口,就一定会更新dataVersion的。 5)事件:EventType. NodeChildrenChanged (4) 触发条件:所关注的节点的子节点有变化。这里说的变化是指子节点的个数和组成,具体到子节点内容的变化是不会通知的。
  • 状态 KeeperState. AuthFailed(4) 1)事件:EventType.None(-1)
  • 状态 KeeperState. Expired(-112) 1)事件:EventType.None(-1)

Watcher 特性总结

ZooKeeper 的 Watcher 具有以下几个特性。

  • 一次性触发 客户端在Znode设置了Watch时,如果Znode内容发生改变,那么客户端就会获得Watch事件。例如:客户端设置getData("/znode1", true)后,如果/znode1发生改变或者删除,那么客户端就会得到一个/znode1的Watch事件,但是/znode1再次发生变化,那客户端是无法收到Watch事件的,除非客户端设置了新的Watch。
  • 客户端串行执行 Watch事件是异步发送到Client。Zookeeper可以保证客户端发送过去的更新顺序是有序的。客户端 Watcher 回调的过程是一个串行同步的过程,这为我们保证了顺序。
  • 轻量 Watch是轻量级的,其实就是本地JVM的Callback,服务器端只是存了是否有设置了Watcher的布尔类型。Zookeeper客户端接收到服务端通知信息后,找到对应变化path的watcher列表,挨个进行触发回调。

ZooKeeper开发

Zookeeper客户端的使用

  • zkCli.sh-Zookeeper命令行

可使用./zkCli.sh -server localhost来连接到Zookeeper服务上。

命令行:

  • h(help):查看帮助

  • ls:列出某一节点下的子节点信息

  • ls2:列出当前节点的子节点,同时列出节点状态

  • stat:查看节点的状态信息 在zookeeper中,每一次对节点的写操作都认为是一次事务,每一个事务,系统都会分配一个唯一的事务ID。   czxid:节点被创建的事务ID   ctime: 创建时间   mzxid: 最后一次被更新的事务ID   mtime: 修改时间   pzxid:子节点列表最后一次被更新的事务ID   cversion:子节点的版本号   dataversion:数据版本号   aclversion:权限版本号   ephemeralOwner:用于临时节点,代表临时节点的事务ID,如果为持久节点则为0   dataLength:节点存储的数据的长度   numChildren:当前节点的子节点个数

  • get:获取当前节点存储的数据内容 如:get /zk

  • create:创建节点 -s:顺序节点   -e:临时节点   如:create /zk "myData"

  • set:修改节点数据,可携带版本号 如:set /zk "myData"

  • delete:删除节点,只能删除没有子节点的节点 如:delete /zk

  • rmr:递归删除节点(含子节点) 如:rmr /zk

  • setquota:设置配额 给节点限制值,比如限制子节点个数、节点数据的长度(当创建节点超出配额时,zookeeper不会抛出异常,会在zookeeper.out记录警告信息)   -n:限制子节点个数   -b:限制值的长度

  • listquota:查看配额,以及节点的配额状态

  • delquota:删除配额

  • close:关闭当前连接

  • history:查看历史执行指令

  • redo:重复执行指令

  • addauth:添加认证用户,例子 addauth digest user:pasword

  • setAcl:设置访问权限,用户名:密码明文:权限,如:setAcl /test auth:user1:password1:cdrwa

  • getAcl:查看访问权限,getAcl /path

ZooKeeper原生Java客户端

Jar客户端的引入

<dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.4.6</version>
</dependency>

常用方法列表

方法名 参数说明
String create(final String path, byte data[], List acl, CreateMode createMode) 参数: 路径、 znode内容,ACL(访问控制列表)、 znode创建类型;用途:创建znode节点 void delete(final String path, int version) 参数: 路径、版本号;如果版本号与znode的版本号不一致,将无法删除,是一种乐观加锁机制;如果将版本号设置为-1,不会去检测版本,直接删除;用途:删除节点 Stat exists(final String path, Watcher watcher) 参数: 路径、Watcher(监视器);当这个znode节点被改变时,将会触发当前Watcher用途:判断znode节点是否存在 Stat exists(String path, boolean watch) 参数: 路径、并设置是否监控这个目录节点,这里的 watcher 是在创建 ZooKeeper 实例时指定的 watcher;判断znode节点是否存在 Stat setData(final String path, byte data[], int version) 参数: 路径、数据、版本号;如果为-1,跳过版本检查用途:设置znode上的数据 byte[] getData(final String path, Watcher watcher, Stat stat) 参数: 路径、监视器、数据版本等信息用途:获取znode上的数据
List getChildren(final String path, Watcher watcher) 参数: 路径、监视器;该方法有多个重载用途:获取节点下的所有子节点
getACL
setACL
sync

创建zookeeper连接

ZooKeeper(java.lang.String connectString, int sessionTimeout, org.apache.zookeeper.Watcher watcher)

  • connectString:zookeeper服务地址,例如“192.168.117.128:2181”
  • sessionTimeout :超时时间,单位为毫秒
  • watcher:实现org.apache.zookeeper.Watcher接口的实现类,需实现process(WatchedEvent watchedEvent) 方法

ZooKeeper zooKeeper = new ZooKeeper("192.168.117.128:2181",5000, new MyWatcher());

package com.funo.oeip.regcenter.zkcli;

import java.io.IOException;
import java.io.UnsupportedEncodingException;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

public class ZkClientWatcher implements Watcher {
	public static final String NODE_NAME = "/auth/node01"; 
	public static final String ZK_HOST = "localhost:2181";
	
    private static ZooKeeper zooKeeper;

	public static void main(String[] args) {
        try {
            zooKeeper = new ZooKeeper(ZK_HOST,5000, new ZkClientWatcher());
            //System.out.println("State:" + zooKeeper.getState());
            Thread.sleep(10000);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

	}

	public void process(WatchedEvent event) {
		KeeperState state = event.getState();
		
		//判断状态
		if (state.equals(Event.KeeperState.SyncConnected)) { 
			doBus();
		} else {
			System.out.println("Keeper state is:" + state.toString());
		}

	}
	
	private void doBus() {
        System.out.println("doBus!");
        try {
            if(null != zooKeeper.exists(NODE_NAME,false)) {
            	Stat stat = null;
            	byte[] data = zooKeeper.getData(NODE_NAME, this, stat);
                System.out.println(NODE_NAME+ "节点已存在,值:" + new String(data, "UTF-8"));
                return;
            }
            String path = zooKeeper.create(NODE_NAME,"001".getBytes(),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);

            System.out.println("zookeeper return:" + path);
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}        
    }
}

节点创建

同步创建

create(java.lang.String path, byte[] data, java.util.List<org.apache.zookeeper.data.ACL> acl, org.apache.zookeeper.CreateMode createMode)

path:创建节点路径,需保证父节点已存在

data:节点数据

acl:权限列表

  • 提供默认的权限OPEN_ACL_UNSAFE、CREATOR_ALL_ACL、READ_ACL_UNSAFE
    • OPEN_ACL_UNSAFE:完全开放
    • CREATOR_ALL_ACL:创建该znode的连接拥有所有权限
    • READ_ACL_UNSAFE:所有的客户端都可读
  • 自定义权限   ACL aclIp = new ACL(ZooDefs.Perms.READ,new Id("ip","127.0.0.1")); ACL aclDigest = new ACL(ZooDefs.Perms.READ| ZooDefs.Perms.WRITE, new Id("digest", DigestAuthenticationProvider.generateDigest("id:pass")));
  • session设置权限  zk.addAuthInfo("digest", "id:pass".getBytes());  

createMode:节点类型

  • PERSISTENT:持久化节点
  • PERSISTENT_SEQUENTIAL:持久化有序节点
  • EPHEMERAL:临时节点(连接断开自动删除)
  • EPHEMERAL_SEQUENTIAL:临时有序节点(连接断开自动删除)

创建成果,返回节点路径。

异步创建

create(java.lang.String path, byte[] data, java.util.List<org.apache.zookeeper.data.ACL> acl, org.apache.zookeeper.CreateMode createMode, org.apache.zookeeper.AsyncCallback.StringCallback cb, java.lang.Object ctx)

  • StringCallback cb:回调接口,执行创建操作后,结果以及数据发送到此接口的实现类中
  • Object ctx:自定义回调数据,在回调实现类可以获取此数据

无返回值,返回值体现在回调函数StringCallback里。

StringCallback的processResult参数含义如下:

其中int rc -- 返回码,具体参见《附录-错误码定义》

    static class MyStringCallBack implements AsyncCallback.StringCallback {

        public void processResult(int rc, String path, Object ctx, String name) {
            System.out.println("resultcode="+rc);//创建成功返回0
            System.out.println("path="+path);//自定义节点名称
            System.out.println("ctx="+ctx);//自定义回调数据
            System.out.println("name="+name);//最终节点名称(顺序节点最终名称与自定义名称不同)

        }
    }	

ACL权限控制

package com.funo.oeip.regcenter.zkcli;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

public class AclCreateWatcher implements Watcher {
	private static final String CONNECTION_IP = "localhost:2181";
	private static final String ZNODE_NAME = "/node01";
	private static final String ZNODE_VALUE = "node01-value";

	private static CountDownLatch latch = new CountDownLatch(1);

	private static ZooKeeper zk = null;

	public void syncInit() {
		try {
			zk = new ZooKeeper(CONNECTION_IP, 5000, new AclCreateWatcher());
			latch.await();
			zk.addAuthInfo("digest", "user1:12345".getBytes());
			Stat stat = zk.exists(ZNODE_NAME, false);
			if ( null == stat) {
				zk.create(ZNODE_NAME, ZNODE_VALUE.getBytes(), Ids.CREATOR_ALL_ACL, CreateMode.PERSISTENT);
			}
			String value1 = new String(zk.getData(ZNODE_NAME, false, null));
			System.out.println("zk有权限进行数据的获取:" + value1);

			ZooKeeper zk3 = new ZooKeeper(CONNECTION_IP, 5000, null);
			zk3.addAuthInfo("digest", "user2:12345".getBytes());
			String value2 = new String(zk3.getData(ZNODE_NAME, false, null));
			System.out.println("zk3有权限进行数据的获取" + value2);

			// ZooKeeper zk2 = new ZooKeeper(CONNECTION_IP, 5000, null);
			// zk2.addAuthInfo("digest", "super:123".getBytes());
			// zk2.getData(ZNODE_NAME, false, null);
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (KeeperException e) {
			System.out.println("异常:" + e.getMessage());
		}
	}

	public void process(WatchedEvent event) {
		if (KeeperState.SyncConnected == event.getState()) {
			if (event.getType() == EventType.None && null == event.getPath()) {
				latch.countDown();
			}
		}
	}

	public static void main(String[] args) throws InterruptedException {
		AclCreateWatcher acl_Create = new AclCreateWatcher();
		acl_Create.syncInit();
	}

}

输出

zk有权限进行数据的获取:node01-value
异常:KeeperErrorCode = NoAuth for /node01

user1有权限读取数据

user2没有权限读取数据

判断是否节点是否存在

	/**
	 * <p>
	 * 判断某个zNode节点是否存在, Stat exists(path<节点路径>, watch<并设置是否监控这个目录节点,这里的 watcher 是在创建
	 * ZooKeeper 实例时指定的 watcher>)
	 * </p>
	 * 
	 * [@param](https://my.oschina.net/u/2303379) path
	 *            zNode节点路径
	 * [@return](https://my.oschina.net/u/556800) 存在返回true,反之返回false
	 */
	public boolean isExists(String path, boolean needWatch) {
		try {
			Stat stat = this.zk.exists(path, needWatch);
			return null != stat;
		} catch (KeeperException e) {
			LOG.error("读取数据失败,发生KeeperException! path: " + path + ", errMsg:" + e.getMessage(), e);
		} catch (InterruptedException e) {
			LOG.error("读取数据失败,发生InterruptedException! path: " + path + ", errMsg:" + e.getMessage(), e);
		}
		return false;
	}	

读取节点值

	/**
	 * <p>
	 * 读取指定节点数据内容,byte[] getData(path<节点路径>, watcher<监视器>, stat<数据版本号>)
	 * </p>
	 * 
	 * [@param](https://my.oschina.net/u/2303379) path
	 *            zNode节点路径
	 * [@return](https://my.oschina.net/u/556800) 节点存储的值,有值返回,无值返回null
	 */
	public String readData(String path) {
		String data = null;
		try {
			data = new String(this.zk.getData(path, false, null));
			LOG.info("读取数据成功, path:" + path + ", content:" + data);
		} catch (KeeperException e) {
			LOG.error("读取数据失败,发生KeeperException! path: " + path + ", errMsg:" + e.getMessage(), e);
		} catch (InterruptedException e) {
			LOG.error("读取数据失败,发生InterruptedException! path: " + path + ", errMsg:" + e.getMessage(), e);
		}
		return data;
	}

更新节点值

	/**
	 * <p>
	 * 更新指定节点数据内容, Stat setData(path<节点路径>, data[]<节点内容>, stat<数据版本号>)
	 * </p>
	 * 
	 * <pre>
	 *     设置某个znode上的数据时如果为-1,跳过版本检查
	 * </pre>
	 * 
	 * [@param](https://my.oschina.net/u/2303379) path
	 *            zNode节点路径
	 * @param data
	 *            zNode数据内容
	 * @return 更新成功返回true,返回返回false
	 */
	public boolean writeData(String path, String data) {
		try {
			Stat stat = this.zk.setData(path, data.getBytes(), -1);
			LOG.info("更新数据成功, path:" + path + ", stat: " + stat);
			return true;
		} catch (KeeperException e) {
			LOG.error("更新数据失败, 发生KeeperException! path: " + path + ", data:" + data + ", errMsg:" + e.getMessage(), e);
		} catch (InterruptedException e) {
			LOG.error("更新数据失败, 发生InterruptedException! path: " + path + ", data:" + data + ", errMsg:" + e.getMessage(),
					e);
		}
		return false;
	}

获取子节点

	/**
	 * <p>
	 * 获取某个节点下的所有子节点,List getChildren(path<节点路径>, watcher<监视器>)该方法有多个重载
	 * </p>
	 * 
	 * @param path
	 *            zNode节点路径
	 * @return 子节点路径集合 说明,这里返回的值为节点名
	 * 
	 *         <pre>
	 *     eg.
	 *     /node
	 *     /node/child1
	 *     /node/child2
	 *     getChild( "node" )户的集合中的值为["child1","child2"]
	 *         </pre>
	 *
	 * @throws KeeperException
	 * @throws InterruptedException
	 */
	public List<String> getChild(String path) {
		try {
			List<String> list = this.zk.getChildren(path, false);
			if (list.isEmpty()) {
				LOG.info("中没有节点" + path);
			}
			return list;
		} catch (KeeperException e) {
			LOG.error("读取子节点数据失败,发生KeeperException! path: " + path + ", errMsg:" + e.getMessage(), e);
		} catch (InterruptedException e) {
			LOG.error("读取子节点数据失败,发生InterruptedException! path: " + path + ", errMsg:" + e.getMessage(), e);
		}
		return null;
	}

删除节点

	/**
	 * <p>
	 * 删除一个zMode节点, void delete(path<节点路径>, stat<数据版本号>)
	 * </p>
	 * <br/>
	 * 
	 * <pre>
	 *     说明
	 *     1、版本号不一致,无法进行数据删除操作.
	 *     2、如果版本号与znode的版本号不一致,将无法删除,是一种乐观加锁机制;如果将版本号设置为-1,不会去检测版本,直接删除.
	 * </pre>
	 * 
	 * @param path
	 *            zNode节点路径
	 * @return 删除成功返回true,反之返回false.
	 */
	public boolean deletePath(String path) {
		try {
			zk.delete(path, -1);
			LOG.info("节点删除成功, Path: " + path);
			return true;
		} catch (KeeperException e) {
			LOG.error("节点删除失败, 发生KeeperException! path: " + path + ", errMsg:" + e.getMessage(), e);
		} catch (InterruptedException e) {
			LOG.error("节点删除失败, 发生 InterruptedException! path: " + path + ", errMsg:" + e.getMessage(), e);
		}
		return false;
	}

通过ZooKeeper的api访问ZooKeeper集群

public static final String CONNECTION_STR = "172.16.16.193:2181,172.16.16.216:2181,172.16.16.212:2181";

zk = new ZooKeeper(CONNECTION_STR , sessionTimeout, watcher);

ZooKeeper监控

  • 远程JMX配置

默认情况下,zookeeper是支持本地的jmx监控的。若需要远程监控zookeeper,则需要进行进行如下配置。

默认的配置(zkServer.sh)有这么一行:

ZOOMAIN="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=$JMXLOCALONLY org.apache.zookeeper.server.quorum.QuorumPeerMain"

咱们在$JMXLOCALONLY后边添加jmx的相关参数配置:

ZOOMAIN="-Dcom.sun.management.jmxremote
        -Dcom.sun.management.jmxremote.local.only=$JMXLOCALONLY
                -Djava.rmi.server.hostname=127.0.0.1
                -Dcom.sun.management.jmxremote.port=1911
                -Dcom.sun.management.jmxremote.ssl=false
                -Dcom.sun.management.jmxremote.authenticate=false
                 org.apache.zookeeper.server.quorum.QuorumPeerMain"

这样就可以远程监控了,可以用jconsole.exe或jvisualvm.exe等工具对其进行监控。

ZooKeeper安装

独立模式(standalone mode):只运行在一台服务器上,适合测试环境。

复制模式(replicated mode):运行于一个集群上,适合生产环境,这个计算机集群被称为一个“集合体”(ensemble)

独立模式(standlone mode)

Zookeeper的运行环境是需要java的,建议安装oracle的java6.

可去官网下载一个稳定的版本,然后进行安装:http://zookeeper.apache.org/

解压后在zookeeper的conf目录下创建配置文件zoo.cfg,里面的配置信息可参考统计目录下的zoo_sample.cfg文件,我们这里配置为:

tickTime=2000
initLimit=10
syncLimit=5
dataDir=/opt/zookeeper-data/
clientPort=2181

tickTime:指定了ZooKeeper的基本时间单位(以毫秒为单位),默认值:3000(ms)。不支持系统属性方式配置。用于配置zookeeper中最小时间单元长度,很多运行时的时间间隔都是使用它的倍数来表示的。

initLimit:指定了启动zookeeper时,zookeeper实例中的随从实例同步到领导实例的初始化连接时间限制,超出时间限制则连接失败(以tickTime为时间单位)。Leader Zookeeper接收集群其他服务器初始化等待最大时间(10*tickTime)。

syncLimit:指定了zookeeper正常运行时,主从节点之间同步数据的时间限制,若超过这个时间限制,那么随从实例将会被丢弃。Leader Zookeeper和集群其他服务器通信最大时间(5*tickTime)。

dataDir:zookeeper存放数据的目录;

clientPort:用于连接客户端的端口。

复制模式(replicated mode)

集群配置的环境与单机配置的环境相同,唯一不同的就是集群是在多台服务器之间配置,当然也有伪集群的配置,也就是在同一台机器上配置多台服务,通过端口号的不同来进行区分。

一般zookeeper集群由3~5台服务器组成,即2n+1台机器。

zoo.cfg配置

集群部署zoo.cfg样例

tickTime=2000
initLimit=10
syncLimit=5
dataDir=/tmp/zookeeper
clientPort=2181
server.1=IP1:2888:3888
server.2=IP2:2888:3888
server.3=IP3:2888:3888

其中:

server.id=host:port:port解析 每一行此配置表示一个集群中的一台服务器。其中id为Server ID,用来标识该机器在集群中的编号。同时,在所在服务器的数据目录(${data_dir})下创建一个myid文件,该文件只有一行内容,并且是一个数字,就是对应每台服务器的Server ID数字。

添加myid文件

除了修改 zoo.cfg 配置文件,集群模式下还要配置一个文件 myid,这个文件在zoo.cfg里dataDir指定的目录下,这个文件里面就只有一个数字,这个数字和server.n的n保持一致,Zookeeper 启动时会读取这个文件,拿到里面的数据与 zoo.cfg 里面的配置信息比较从而判断到底是那个 server。

即server.x:用于集群中发现彼此,这里的1,2,3需要跟${data_dir}/myid里数字对应

比如server.1=IP1:2888:3888的myid中的内容就是1。不同服务器的ID需要保持不同,并且和zoo.cfg文件中server.id中的id和myid文件的内容保持一致。id的取值范围为1~255。

其中,server.id中配置参数的第一个port是集群中其他机器与Leader之间通信的端口,第二个port为当Leader宕机或其他故障时,集群进行重新选举Leader时使用的端口。

状态查看

服务进程查看

使用jps -m可以查询进程,Zookeeper的进程为QuorumPeerMain

[root@zk-2-82-0001 bin]# jps -m
1744 WrapperSimpleApp CloudResetPwdUpdateAgent
2528 QuorumPeerMain /app/zookeeper/bin/../conf/zoo.cfg
21745 Jps -m
[root@zk-2-82-0001 bin]#

集群状态查看

./zkServer.sh status

[root@zk-2-82-0001 bin]# ./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /app/zookeeper/bin/../conf/zoo.cfg
Mode: leader
[root@zk-2-82-0001 bin]#

[root@zk-2-81 bin]# ./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /app/zookeeper/bin/../conf/zoo.cfg
Mode: follower
[root@zk-2-81 bin]# 

[root@zk-2-82-0002 bin]# ./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /app/zookeeper/bin/../conf/zoo.cfg
Mode: follower
[root@zk-2-82-0002 bin]# 

附录

错误码定义

public interface CodeDeprecated {
    /**
     * @deprecated deprecated in 3.1.0, use {@link Code#OK} instead
     */
    @Deprecated
    public static final int Ok = 0;

    /**
     * @deprecated deprecated in 3.1.0, use {@link Code#SYSTEMERROR} instead
     */
    @Deprecated
    public static final int SystemError = -1;
    /**
     * @deprecated deprecated in 3.1.0, use
     * {@link Code#RUNTIMEINCONSISTENCY} instead
     */
    @Deprecated
    public static final int RuntimeInconsistency = -2;
    /**
     * @deprecated deprecated in 3.1.0, use {@link Code#DATAINCONSISTENCY}
     * instead
     */
    @Deprecated
    public static final int DataInconsistency = -3;
    /**
     * @deprecated deprecated in 3.1.0, use {@link Code#CONNECTIONLOSS}
     * instead
     */
    @Deprecated
    public static final int ConnectionLoss = -4;
    /**
     * @deprecated deprecated in 3.1.0, use {@link Code#MARSHALLINGERROR}
     * instead
     */
    @Deprecated
    public static final int MarshallingError = -5;
    /**
     * @deprecated deprecated in 3.1.0, use {@link Code#UNIMPLEMENTED}
     * instead
     */
    @Deprecated
    public static final int Unimplemented = -6;
    /**
     * @deprecated deprecated in 3.1.0, use {@link Code#OPERATIONTIMEOUT}
     * instead
     */
    @Deprecated
    public static final int OperationTimeout = -7;
    /**
     * @deprecated deprecated in 3.1.0, use {@link Code#BADARGUMENTS}
     * instead
     */
    @Deprecated
    public static final int BadArguments = -8;

    /**
     * @deprecated deprecated in 3.1.0, use {@link Code#APIERROR} instead
     */
    @Deprecated
    public static final int APIError = -100;

    /**
     * @deprecated deprecated in 3.1.0, use {@link Code#NONODE} instead
     */
    @Deprecated
    public static final int NoNode = -101;
    /**
     * @deprecated deprecated in 3.1.0, use {@link Code#NOAUTH} instead
     */
    @Deprecated
    public static final int NoAuth = -102;
    /**
     * @deprecated deprecated in 3.1.0, use {@link Code#BADVERSION} instead
     */
    @Deprecated
    public static final int BadVersion = -103;
    /**
     * @deprecated deprecated in 3.1.0, use
     * {@link Code#NOCHILDRENFOREPHEMERALS}
     * instead
     */
    @Deprecated
    public static final int NoChildrenForEphemerals = -108;
    /**
     * @deprecated deprecated in 3.1.0, use {@link Code#NODEEXISTS} instead
     */
    @Deprecated
    public static final int NodeExists = -110;
    /**
     * @deprecated deprecated in 3.1.0, use {@link Code#NOTEMPTY} instead
     */
    @Deprecated
    public static final int NotEmpty = -111;
    /**
     * @deprecated deprecated in 3.1.0, use {@link Code#SESSIONEXPIRED} instead
     */
    @Deprecated
    public static final int SessionExpired = -112;
    /**
     * @deprecated deprecated in 3.1.0, use {@link Code#INVALIDCALLBACK}
     * instead
     */
    @Deprecated
    public static final int InvalidCallback = -113;
    /**
     * @deprecated deprecated in 3.1.0, use {@link Code#INVALIDACL} instead
     */
    @Deprecated
    public static final int InvalidACL = -114;
    /**
     * @deprecated deprecated in 3.1.0, use {@link Code#AUTHFAILED} instead
     */
    @Deprecated
    public static final int AuthFailed = -115;
    /**
     * This value will be used directly in {@link CODE#SESSIONMOVED}
     */
    // public static final int SessionMoved = -118;
}

Fast Paxos算法

Fast Paxos概览

Lamport在40多页的论文中不仅提出了Fast Paxos算法,并且还从工程实践的角度重新描述了Paxos,使其更贴近应用场景。从一般的Client/Server来考虑,Client其实承担了Proposer和Learner的作用,而Server则扮演Acceptor的角色,因此下面重新描述了Paxos算法中的几个角色:

  • Client/Proposer/Learner:负责提案并执行提案
  • Coordinator:Proposer协调者,可为多个,Client通过Coordinator进行提案
  • Leader:在众多的Coordinator中指定一个作为Leader
  • Acceptor:负责对Proposal进行投票表决

就是Client的提案由Coordinator进行,Coordinator存在多个,但只能通过其中被选定Leader进行;提案由Leader交由Server进行表决,之后Client作为Learner学习决议的结果。 这种方式更多地考虑了Client/Server这种通用架构,更清楚地注意到了Client既作为Proposer又作为Learner这一事实。

同样要注意到的是,如果Leader宕机了,为了保证算法的正确性需要一个Leader的选举算法,但与之前一样,Lamport并不关心这个Leader选举算法,他认为可以简单地通过随机或超时机制实现。

另外在Classic Paxos中,从每次Proposer提案到决议被学习,需要三个通信步骤: Proposer-----Leader-----Acceptor-----Learner 从直观上来说,Proposer其实更“知道”提交那个Value,如果能让Proposer直接提交value到Acceptor,则可以把通信步骤减少到2个。Fast Paxos便是基于此而产生。

Make Paxos Faster

我们再回顾下Classic Paxos的几个阶段: Phase1a:Leader提交proposal到Acceptor Phase2b:Acceptor回应已经参与投票的最大Proposer编号和选择的Value Phase2a:Leader收集Acceptor的返回值 Phase2a.1:如果Acceptor无返回值,则自由决定一个 Phase2a.2: 如果有返回值,则选择Proposer编号最大的一个 Phase2b:Acceptor把表决结果发送到Learner 很明显,在Phase2a.1中,如果Leader可以自由决定一个Value,则可以让Proposer提交这个Value,自己则退出通信过程。只要之后的过程运行正常,Leader始终不参与通信,一直有Proposer直接提交Value到Acceptor,从而把Classic Paxos的三阶段通信减少为两阶段,这便是Fast Paxos的由来。因此,我们更加形式化下Fast Paxos的几个阶段: Phase1a:与之前相同 Phase1b:与之前相同 Phase2a:Leader收集Acceptor的返回值 Phase2a.1:如果Acceptor无返回值,则发送一个Any消息给Acceptor,之后Acceptor便等待Proposer提交Value Phase2a.2:如果有返回值,则根据规则选取一个 Phase2b:Acceptor把表决结果发送到Learner(包括Leader) 算法主要变化在Phase2a阶段,即: 若Leader可以自由决定一个Value,则发送一条Any消息,Acceptor便等待Proposer提交Value 若Acceptor有返回值,则Acceptor需选择某个Value

先不考虑实现,从形式上消息仅需在Proposer-----Acceptor-----Learner之间传递即可,也即仅需2个通信步骤。下面我们详细说明算法过程:

一些定义

  • Quorum 在Classic Paxos中一直通过多数派(Majority)来保证算法的正确性,对多数派再进一步抽象化,称为“Quorum(法定人数)”,要求任意两个Quorum之间有交集(从而间接表达了majority的含义)
  • Round 在Classic Paxos中,Proposer每次提案都用一个全序的编号表示,如果执行顺利,该编号的Proposal在经历Phase1,Phase2后最终会执行成功。 在Fast Paxos称这个带编号的Proposal的执行过程为“Round”
  • i-Quorum 在Classic Paxos执行过程中,一般不会明确区分每次Round执行的Quorum,虽然也可以为每个Round指定一个Quorum。在Fast Paxos中会通过i-Quorum明确指定Round i需要的Quorum
  • Classic Round 执行Classic Paxos的Round称为Classic Round
  • Fast Round 如果Leader发送了Any消息,则认为后续通信是一个Fast Round;若Leader未发送Any消息,还是跟之前一样通信,则后续行为仍然是Classic Round。

根据Lamport描述,Classic Round和Fast Round可通过Round Number进行加以区分。

Any消息 在正常情况下,Leader若可以自由决定一个Value,应该发生一条Phase2a消息,其中包含了选择的Value,但此时却发送了一条无Value的Any消息。Acceptor在接收到Any消息后可做一些开始Fast Round的初始化工作,等待Proposer提交真正的Value。Any消息的意思是Acceptor可以做任意的处理。 因此,一个Fast Round包括两个阶段:由Any消息开始的阶段,和由Proposer提交Value的结束阶段,而Leader只是起到一个初始化过程的作用,如果没有错误发生,Leader将退出之后的通信中过程。

下面是Classic Paxos交互图:

下面是Fast Paxos的交互图:

冲突 在Classic Paxos中,Acceptor投票的value都是Leader选择好的,所以不存在同一Round中投票多个Value的场景,从而保证了一致性。但在Fast Round中因为允许多个Proposer同时提交不同的Value到Acceptor,这将导致在Fast Round中没有任何value被作为最终决议,这也称为“冲突”(Collision) Proposer提交的Round是全序的,不同的Proposer提交的Round肯定不一样,同一Proposer不可能在同一Round中提交不同的Value,那为什么还会有同一Fast Round中有多个Value的情况?原因在于Fast Round与Round区别,当Fast Round开始后,会被分配一个唯一的Round Number,之后无论多少个Proposer提交Value都是基于这个Round Number,而不管Proposer提交的Round是否全序。 比如,Fast Round Number为10,Proposer1提交了(11,1),Proposer2提交了(12,2),但对Fast Round来说存在(10,1,2)两个Value。

因为冲突的存在,会导致Phase2a.2的选择非常困难,原因是: 在Classic Paxos中,如果Acceptor返回多个Value,只要排序,选择最高的编号对应的Value即可,因为Classic Paxos中的Value都是有Leader选择后在Phase2a中发送的,因此最高编号的Value肯定只有一个。但在Fast Paxos中,最高编号的Value会发现多个,比如(10,1,2)。 假如当前Leader正在执行第i个Classic Round(i-Quorum为Q) ,得到Acceptor反馈的最高编号为k,有两个value:v、w,说明Fast Round k存在两个k-Quorum,Rv,Rw。 O4(v):下面定义在Round k中v或w被选择的条件: 如果v在Round k中被选择,那么存在一个k-Quorum R,使得对任意的Acceptor a∈Q∩R,都对v作出投票。 这个问题也可表述为:R中的所有Acceptor都对v作出投票,并且Q∩R≠φ,因为如果Q∩R=φ,则Round i将无法得知投票结果

因此如果保证下面两个条件: 每个Acceptor在同一Fast Round中仅投票一个Value Q∩Rv∩Rw≠φ 则v、w不可能同时被选择 确定Quorum 根据上面描述,为了防止一次Fast Round选择多个Value,Quorum需要满足下面两个条件: 任意两个Classic Quorum有交集 任意一个Classic Quorum与任意两个Fast Quorum有交集 不妨设总Acceptor数为N,Classic Round运行的最大失败Acceptor数为F,Fast Round允许的失败数为E,即N-F构成Classic Round的一个Quorum,N-E构成Fast Round的一个Quorum。 上面两个条件等价于: N>2F N>2E+F 设Qc,Qf分别为Classic和Fast Round的Quorum大小,经过整理可得两个下限结果: |Qc| = |Qf| ≥ N − ⌈N/3⌉ + 1 ≥ ⌊2N/3⌋ + 1 |Qc| ≥N-⌈N/2⌉+1 = ⌈N/2⌉+1 |Qf|≥N-⌈N/4⌉≥⌈3N/4⌉ 证明请参考:一致性算法中的节点下限 冲突Recovery 作为优化,Acceptor在投票Value时也应该发送到Leader,这样Leader就很容易能发现冲突。Leader如果在Round i发现冲突,可以很容易地开始Roun i+1,从Phase1a开始重新执行Classic Paxos过程,但这个其实可以进一步优化,我们首先考虑下面这个事实: 如果Leader重启了Round i+1,并且收到了i-Quorum个Acceptor发送的Phase1b消息,则该消息明确了两件事情: 报告Acceptor a参与投票的最大Round和对应的Value 承诺不会对小于i+1的Round作出投票 假如Acceptor a也参与了Round i的投票,则a的Phase1b消息同样明确了上述两件事情,并且会把对应的Round,Value在Phase2b中发送给Leader(当然还有Learner),一旦Acceptor a执行了Phase2b,则也同时表明a将不会再对小于i+1的Round进行投票。 也就是说,Round i的Phase2b与Round i+1的Phase1b有同样的含义,也暗含着如果Leader收到了Round i的Phase2b,则可直接开始Round i+1的Phase2a。经过整理,产生了两种解决冲突(Recovery)的方法: 7.1 基于协调者的Recovery 如果Leader在Round i 中收到了(i+1)-Quorum个Acceptor的Phase2b消息,并且发现冲突,则根据O4(v)选取一个value,直接执行Round i+1的Phase2a;否则,从Phase1a开始重新执行Round i+1 7.2 基于非协调的Recovery 作为基于协调Recovery的扩展,非协调要求Acceptor把Phase2b消息同时发送给其他Quorum Acceptor,由每个Acceptor直接执行Round i+1的Phase2a,但这要求i-Quorum与(i+1)-Quorum必须相同,并且遵循相同选择value的规则。 这种方式的好处是Acceptor直接执行Round i+1的Phase2a,无需经过Leader,节省了一个通信步骤,缺点是Acceptor同时也作为Proposer,搞的过于复杂。 Fast Paxos Progress 至此,再完整地总结下Fast Paxos的Progress: Phase1a:与之前相同 Phase1b:与之前相同 Phase2a:Leader收集Acceptor的返回值 Phase2a.1:如果Acceptor无返回值,则发送一个Any消息给Acceptor,之后Acceptor便等待Proposer提交Value Phase2a.2:如果有返回值 2.1 如果仅存在一个Value,则作为结果提交 2.2 如果存在多个Value,则根据O4(v)选取符合条件的一个 2.3 如果存在多个结果并且没有符合O4(v)的Value,则自由决定一个 Phase2b:Acceptor把表决结果发送到Learner(包括Leader)

总结 Fast Paxos基本是本着乐观锁的思路:如果存在冲突,则进行补偿。其中Leader起到一个初始化Progress和解决冲突的作用,如果Progress一直执行良好,则Leader将始终不参与一致性过程。 因此Fast Paxos理论上只需要2个通信步骤,而Classic Paxos需要3个,但Fast Paxos在解决冲突时有至少需要1个通信步骤,在高并发的场景下,冲突的概率会非常高,冲突解决的成本也会很大。 另外,Fast Paxos把Client深度引入算法中,致使其架构远没Classic Paxos那么清晰,也没Classic Paxos容易扩展。 还有一点要注意的是,Fast Quorum的大小比Classic的要大,一般Fast Quorum至少需要4个节点(3E+1),而Classic Paxos需要3个(2F+1)(请参考:一致性算法中的节点下限)。

总之,在我看来Fast Paxos是一个理论上可行,但实际中很难操作的算法,实际中用的比较多的还是Classic Paxos的各种简化形式

Zookeeper开源客户端ZKClient和Curator简介

Zookeeper客户端提供了基本的操作,比如,创建会话、创建节点、读取节点、更新数据、删除节点和检查节点是否存在等。但对于开发人员来说,Zookeeper提供的基本操纵还是有一些不足之处。本篇就聊聊这些不足之处和两款开源框架ZKClient和Curator。

Zookeeper 原生API不足之处

  1. Zookeeper的Watcher是一次性的,每次触发之后都需要重新进行注册;
  2. Session超时之后没有实现重连机制;
  3. 异常处理繁琐,Zookeeper提供了很多异常,对于开发人员来说可能根本不知道该如何处理这些异常信息;
  4. 只提供了简单的byte[]数组的接口,没有提供针对对象级别的序列化;
  5. 创建节点时如果节点存在抛出异常,需要自行检查节点是否存在;
  6. 删除节点无法实现级联删除;

ZkClient简介 ZkClient是一个开源客户端,在Zookeeper原生API接口的基础上进行了包装,更便于开发人员使用。内部实现了Session超时重连,Watcher反复注册等功能。像dubbo等框架对其也进行了集成使用。

虽然ZkClient对原生API进行了封装,但也有它自身的不足之处:

  • 几乎没有参考文档;
  • 异常处理简化(抛出RuntimeException);
  • 重试机制比较难用;
  • 没有提供各种使用场景的实现;

Curator简介 Curator是Netflix公司开源的一套Zookeeper客户端框架,和ZkClient一样,解决了非常底层的细节开发工作,包括连接重连、反复注册Watcher和NodeExistsException异常等。目前已经成为Apache的顶级项目。另外还提供了一套易用性和可读性更强的Fluent风格的客户端API框架。 除此之外,Curator中还提供了Zookeeper各种应用场景(Recipe,如共享锁服务、Master选举机制和分布式计算器等)的抽象封装。 小结 上面介绍了Zookeeper原生API的不足和两款开源客户端的简介。后面的章节将会针对两款开源客户端进行详细的介绍。

转载于:https://my.oschina.net/simonfj/blog/2979013

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值