Zookeeper
学习文档,方便以后查看
官网文档地址大全:
OverView(概述)
http://zookeeper.apache.org/doc/r3.4.6/zookeeperOver.html
Getting Started(开始入门)
http://zookeeper.apache.org/doc/r3.4.6/zookeeperStarted.html
Tutorial(教程)
http://zookeeper.apache.org/doc/r3.4.6/zookeeperTutorial.html
Java Example(Java示例)
http://zookeeper.apache.org/doc/r3.4.6/javaExample.html
Programmer's Guide(开发人员指南)
http://zookeeper.apache.org/doc/r3.4.6/zookeeperProgrammers.html
Recipes and Solutions(技巧及解决方案)
http://zookeeper.apache.org/doc/r3.4.6/recipes.html
3.4.6 API online(在线API速查)
http://zookeeper.apache.org/doc/r3.4.6/api/index.html
另外推荐园友sunddenly的zookeeper系列
http://www.cnblogs.com/sunddenly/category/620563.html
一、java与zk
1.1 java连接与添加监听器
maven项目添加依赖
<!--zk--> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.6</version> </dependency>
最基本的连接方式
@Test public void demo() throws IOException, InterruptedException, KeeperException { //连接单个server // ZooKeeper zk = new ZooKeeper("192.168.99.100:2181", 300000, new DemoWatcher()); //集群连接 ZooKeeper zk = new ZooKeeper("192.168.99.100:2181,192.168.99.100:2182,192.168.99.100:2183", 300000, new DemoWatcher()); String node = "/app1"; Stat stat = zk.exists(node, false);//检测/app1是否存在 if (stat == null) { //创建节点 String createResult = zk.create(node, "test".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); System.out.println("create: "+createResult); } //获取节点的值 byte[] b = zk.getData(node, false, stat); System.out.println(new String(b)); zk.close(); } static class DemoWatcher implements Watcher { @Override public void process(WatchedEvent event) { System.out.println("----------->"); System.out.println("path:" + event.getPath()); System.out.println("type:" + event.getType()); System.out.println("stat:" + event.getState()); System.out.println("<-----------"); } }
利用开源项目连接,并且添加监听器
首先添加依赖
<!-- https://mvnrepository.com/artifact/com.101tec/zkclient -->
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.9</version>
</dependency>
实现
/** * Created by fengzp on 16/6/29. */ public class ZookeeperTest { private ZkClient zkClient; private String nodeName = "/zkListener"; @Before public void before(){ zkClient = new ZkClient("192.168.99.100:2181,192.168.99.100:2182,192.168.99.100:2183"); } @Test public void testZkListener() throws InterruptedException { //添加监听器 zkClient.subscribeDataChanges(nodeName, new IZkDataListener() { @Override public void handleDataChange(String s, Object o) throws Exception { System.out.println("node data changed!"); System.out.println("node=>" + s); System.out.println("data=>" + o); System.out.println("--------------"); } @Override public void handleDataDeleted(String s) throws Exception { System.out.println("node data deleted!"); System.out.println("s=>" + s); System.out.println("--------------"); } }); while (true){ TimeUnit.SECONDS.sleep(5); } } @Test public void testUpdateData(){ if (!zkClient.exists(nodeName)) { zkClient.createPersistent(nodeName); } zkClient.writeData(nodeName, "a"); zkClient.writeData(nodeName, "b"); zkClient.delete(nodeName); zkClient.delete(nodeName);//删除一个不存在的node,并不会报错 } }
1.2 zk节点的创建模式
zk节点有4种创建模式
//持久节点:节点创建后,会一直存在,不会因客户端会话失效而删除; PERSISTENT(0, false, false), //持久顺序节点:基本特性与持久节点一致,创建节点的过程中,zookeeper会在其名字后自动追加一个单调增长的数字后缀,作为新的节点名; PERSISTENT_SEQUENTIAL(2, false, true), //临时节点:客户端会话失效或连接关闭后,该节点会被自动删除,且不能再临时节点下面创建子节点,否则报如下错:org.apache.zookeeper.KeeperException$NoChildrenForEphemeralsException; EPHEMERAL(1, true, false), //临时顺序节点:基本特性与临时节点一致,创建节点的过程中,zookeeper会在其名字后自动追加一个单调增长的数字后缀,作为新的节点名; EPHEMERAL_SEQUENTIAL(3, true, true);
可通过create方法来指定创建模式
zkClient.create(note, data, CreateMode.EPHEMERAL_SEQUENTIAL);
1.3 通过zk实现统一配置管理
目标:把公共的配置抽出来单独管理,而改变配置时,能快速的把一堆已经在线上运行的子应用,通通换掉相应的配置,而且还不用停机。
实现步骤:
1、公用配置不应该分散存放到各应用中,而是应该抽出来,统一存储到一个公用的位置(最容易想到的办法,放在db中,或统一的分布式cache server中,比如Redis,或其它类似的统一存储,比如ZooKeeper中)
2、对这些公用配置的添加、修改,应该有一个统一的配置管理中心应用来处理(这个也好办,做一个web应用来对这些配置做增、删、改、查即可),这里的操作就是对zk节点的操作
3、子应用创建一个zk的监听器,监听zk存放配置的节点的变化,每当节点的数据更新时,应用就相应的更新。
二、消除单点故障
2.1 思路
关键节点的单点故障(Single Point of Failure)在大型的架构中,往往是致命的。比如:SOA架构中,服务注册中心(Server Register)统一调度所有服务,如果这个节点挂了,基本上整个SOA架构也就崩溃了,另外hadoop 1.x/2.x中的namenode节点,这是hdfs的核心节点,如果namenode宕掉,hdfs也就废了。ZooKeeper的出现,很好的解决了这一难题,其核心原理如下:
1. 关键节点的运行实例(或服务器),可以跑多个,这些实例中的数据完全是相同的(即:对等设计),每个实例启动后,向ZK注册一个临时顺序节点,比如 /core-servers/server0000001, /core-servers/server0000002 ... ,最后的顺序号是由ZK自动递增的
2. 其它应用需要访问1中的核心服务器里,可以事先约定好,从ZK的这些临时节点中,挑选一个序号最小的节点,做为主服务器(即master)
3. 当master宕掉时,超过一定的时间阈值,临时节点将由ZK自动删除,这样原来序列最小的节点也就没了,客户端应用按2中的约定找最小节点的服务器时,自动会找到原来次最小的节点,继续充为master(老大挂了,老二顶上),即实现了故障转换。如果原来出问题的master恢复了,重新加入ZK,由于顺序号是一直递增,重新加入后,它将做为备胎待命。
2.2 测试代码
import org.I0Itec.zkclient.ZkClient; import org.junit.Test; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; /** * Created by fengzp on 16/6/30. */ public class ZkSinglePointFailTest { @Test public void startServer1() throws InterruptedException { new MyServer("server1").start(); while (true) { TimeUnit.SECONDS.sleep(5); } } @Test public void startServer2() throws InterruptedException { new MyServer("server2").start(); while (true) { TimeUnit.SECONDS.sleep(5); } } @Test public void test() throws InterruptedException { MyClient clientServer = new MyClient(); clientServer.run(); //此时,手动停止coreServer1 TimeUnit.SECONDS.sleep(60); //再次运行 clientServer.run(); } private String ServerNodeName = "/servers"; public ZkClient getZkClient(){ return new ZkClient("192.168.99.100:2181"); } class MyClient{ private String getServerData(){ ZkClient zkClient = getZkClient(); List<String> servers = zkClient.getChildren(ServerNodeName); if (servers.size() <= 0) { return null; } for (String s : servers) { System.out.println("server: "+s); } Object[] arr = servers.toArray(); Arrays.sort(arr); String data = zkClient.readData(ServerNodeName + "/" + arr[0].toString()); System.out.println("node:" + arr[0].toString() + ", data:" + data); return data; } public void run(){ System.out.println("客户端应用运行中,正在调用:" + getServerData() + " 上的服务"); } } class MyServer{ private String hostName; public MyServer(String hostName){ this.hostName = hostName; } public void start() { ZkClient zk = getZkClient(); if (!zk.exists(ServerNodeName)){ zk.createPersistent(ServerNodeName); } zk.createEphemeralSequential(ServerNodeName + "/server", hostName); System.out.println(hostName + " is running..."); } } }
首先调用startServer1方法启动server1, 然后调用startServer2方法启动server2, 因为代码里加入了死循环,所以server会一直在跑。
再调用test方法,正常应该会调用server1的服务,然后在中间我加入了60秒的睡眠,这时候手动停止server1,当再次运行时应会调用server2的服务。
输出:
三、ACL访问
3.1 acl机制(转)
zk做为分布式架构中的重要中间件,通常会在上面以节点的方式存储一些关键信息,默认情况下,所有应用都可以读写任何节点,在复杂的应用中,这不太安全,ZK通过ACL机制来解决访问权限问题,详见官网文档:http://zookeeper.apache.org/doc/r3.4.6/zookeeperProgrammers.html#sc_ZooKeeperAccessControl
总体来说,ZK的节点有5种操作权限:
CREATE、READ、WRITE、DELETE、ADMIN 也就是 增、删、改、查、管理权限,这5种权限简写为crwda(即:每个单词的首字符缩写)
注:这5种权限中,delete是指对子节点的删除权限,其它4种权限指对自身节点的操作权限
身份的认证有4种方式:
world:默认方式,相当于全世界都能访问
auth:代表已经认证通过的用户(cli中可以通过addauth digest user:pwd 来添加当前上下文中的授权用户)
digest:即用户名:密码这种方式认证,这也是业务系统中最常用的
ip:使用Ip地址认证
Cli命令行下可以这样测试:
通过getAcl命令可以发现,刚创建的节点,默认是 world,anyone的认证方式,具有cdrwa所有权限
继续捣鼓:
先给/test增加了user1:+owfoSBn/am19roBPzR1/MfCblE的只读(r)权限控制,
说明:setAcl /test digest:用户名:密码:权限 给节点设置ACL访问权限时,密码必须是加密后的内容,这里的+owfoSBn/am19roBPzR1/MfCblE=,对应的原文是12345 (至于这个密文怎么得来的,后面会讲到,这里先不管这个),设置完Acl后,可以通过
getAcl /节点路径 查看Acl设置
然后get /test时,提示认证无效,说明访问控制起作用了,接下来:
addauth digest user1:12345 给"上下文"增加了一个认证用户,即对应刚才setAcl的设置
然后再 get /test 就能取到数据了
最后 delete /test 成功了!原因是:根节点/默认是world:anyone:crdwa(即:全世界都能随便折腾),所以也就是说任何人,都能对根节点/进行读、写、创建子节点、管理acl、以及删除子节点(再次映证了ACL中的delete权限应该理解为对子节点的delete权限)
刚才也提到了,setAcl /path digest这种方式,必须输入密码加密后的值,这在cli控制台上很不方便,所以下面这种方式更常用:
注意加框的部分,先用addauth digest user1:12345 增加一个认证用户,然后用 setAcl /test auth:user1:12345:r 设置权限,跟刚才的效果一样,但是密码这里输入的是明文,控制台模式下手动输入更方便。
3.2 通过java实现
import org.I0Itec.zkclient.ZkClient; import org.apache.zookeeper.ZooDefs; import org.apache.zookeeper.data.ACL; import org.apache.zookeeper.data.Id; import org.apache.zookeeper.data.Stat; import org.apache.zookeeper.server.auth.DigestAuthenticationProvider; import org.junit.Test; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * Created by fengzp on 16/6/30. */ public class ZookeeperAclTest { private static final String zkAddress = "192.168.99.100:2181"; private static final String testNode = "/test"; private static final String readAuth = "read-user:123456"; private static final String writeAuth = "write-user:123456"; private static final String deleteAuth = "delete-user:123456"; private static final String allAuth = "super-user:123456"; private static final String adminAuth = "admin-user:123456"; private static final String digest = "digest"; @Test public void test() throws NoSuchAlgorithmException { initNode(); System.out.println("---------------------"); readTest(); System.out.println("---------------------"); writeTest(); System.out.println("---------------------"); changeACLTest(); System.out.println("---------------------"); deleteTest(); } private void initNode() throws NoSuchAlgorithmException { ZkClient zkClient = new ZkClient(zkAddress); zkClient.addAuthInfo(digest, allAuth.getBytes()); if (zkClient.exists(testNode)) { zkClient.delete(testNode); System.out.println("节点删除成功!"); } List<ACL> acls = new ArrayList<>(); acls.add(new ACL(ZooDefs.Perms.ALL, new Id(digest, DigestAuthenticationProvider.generateDigest(allAuth)))); acls.add(new ACL(ZooDefs.Perms.READ, new Id(digest, DigestAuthenticationProvider.generateDigest(readAuth)))); acls.add(new ACL(ZooDefs.Perms.WRITE, new Id(digest, DigestAuthenticationProvider.generateDigest(writeAuth)))); acls.add(new ACL(ZooDefs.Perms.DELETE, new Id(digest, DigestAuthenticationProvider.generateDigest(deleteAuth)))); acls.add(new ACL(ZooDefs.Perms.ADMIN, new Id(digest, DigestAuthenticationProvider.generateDigest(adminAuth)))); zkClient.createPersistent(testNode, "test-data", acls); System.out.println(zkClient.readData(testNode).toString()); System.out.println("节点创建成功!"); zkClient.close(); } private void readTest() { ZkClient zkClient = new ZkClient(zkAddress); try { System.out.println(zkClient.readData(testNode).toString());//没有认证信息,读取会出错 } catch (Exception e) { System.err.println(e.getMessage()); } try { zkClient.addAuthInfo(digest, adminAuth.getBytes()); System.out.println(zkClient.readData(testNode).toString());//admin权限与read权限不匹配,读取也会出错 } catch (Exception e) { System.err.println(e.getMessage()); } try { zkClient.addAuthInfo(digest, readAuth.getBytes()); System.out.println(zkClient.readData(testNode).toString());//只有read权限的认证信息,才能正常读取 } catch (Exception e) { System.err.println(e.getMessage()); } zkClient.close(); } private void writeTest() { ZkClient zkClient = new ZkClient(zkAddress); try { zkClient.writeData(testNode, "new-data");//没有认证信息,写入会失败 } catch (Exception e) { System.err.println(e.getMessage()); } try { zkClient.addAuthInfo(digest, writeAuth.getBytes()); zkClient.writeData(testNode, "new-data");//加入认证信息后,写入正常 } catch (Exception e) { System.err.println(e.getMessage()); } try { zkClient.addAuthInfo(digest, readAuth.getBytes()); System.out.println(zkClient.readData(testNode).toString());//读取新值验证 } catch (Exception e) { System.err.println(e.getMessage()); } zkClient.close(); } private void deleteTest() { ZkClient zkClient = new ZkClient(zkAddress); //zkClient.addAuthInfo(digest, deleteAuth.getBytes()); try { //System.out.println(zkClient.readData(testNode)); zkClient.delete(testNode); System.out.println("节点删除成功!"); } catch (Exception e) { System.err.println(e.getMessage()); } zkClient.close(); } private static void changeACLTest() { ZkClient zkClient = new ZkClient(zkAddress); //注:zkClient.setAcl方法查看源码可以发现,调用了readData、setAcl二个方法 //所以要修改节点的ACL属性,必须同时具备read、admin二种权限 zkClient.addAuthInfo(digest, adminAuth.getBytes()); zkClient.addAuthInfo(digest, readAuth.getBytes()); try { List<ACL> acls = new ArrayList<ACL>(); acls.add(new ACL(ZooDefs.Perms.ALL, new Id(digest, DigestAuthenticationProvider.generateDigest(adminAuth)))); zkClient.setAcl(testNode, acls); Map.Entry<List<ACL>, Stat> aclResult = zkClient.getAcl(testNode); System.out.println(aclResult.getKey()); } catch (Exception e) { System.err.println(e.getMessage()); } zkClient.close(); } }
四、同步锁
4.1介绍(转)
目前分布式锁,比较成熟、主流的方案有基于redis及基于zookeeper的二种方案。
大体来讲,基于redis的分布式锁核心指令为SETNX,即如果目标key存在,写入缓存失败返回0,反之如果目标key不存在,写入缓存成功返回1,通过区分这二个不同的返回值,可以认为SETNX成功即为获得了锁。
redis分布式锁,看上去很简单,但其实要考虑周全,并不容易,网上有一篇文章讨论得很详细:http://blog.csdn.net/ugg/article/details/41894947/,有兴趣的可以阅读一下。
其主要问题在于某些异常情况下,锁的释放会有问题,比如SETNX成功,应用获得锁,这时出于某种原因,比如网络中断,或程序出异常退出,会导致锁无法及时释放,只能依赖于缓存的过期时间,但是过期时间这个值设置多大,也是一个纠结的问题,设置小了,应用处理逻辑很复杂的话,可能会导致锁提前释放,如果设置大了,又会导致锁不能及时释放,所以那篇文章中针对这些细节讨论了很多。
而基于zk的分布式锁,在锁的释放问题上处理起来要容易一些,其大体思路是利用zk的“临时顺序”节点,需要获取锁时,在某个约定节点下注册一个临时顺序节点,然后将所有临时节点按小从到大排序,如果自己注册的临时节点正好是最小的,表示获得了锁。(zk能保证临时节点序号始终递增,所以如果后面有其它应用也注册了临时节点,序号肯定比获取锁的应用更大)
当应用处理完成,或者处理过程中出现某种原因,导致与zk断开,超过时间阈值(可配置)后,zk server端会自动删除该临时节点,即:锁被释放。所有参与锁竞争的应用,只要监听父路径的子节点变化即可,有变化时(即:有应用断开或注册时),开始抢锁,抢完了大家都在一边等着,直到有新变化时,开始新一轮抢锁。
个人感觉:zk做分布式锁机制更完善,但zk抗并发的能力弱于redis,性能上略差,建议如果并发要求高,锁竞争激烈,可考虑用redis,如果抢锁的频度不高,用zk更适合。
以下是对于分布式锁所封装的一个抽象类
import org.I0Itec.zkclient.ZkClient; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import java.util.Collections; import java.util.List; /** * Created by fengzp on 16/6/30. */ public abstract class AbstractZkLock { private int lockNumber = 1; //允许获取的锁数量(默认为1,即最小节点=自身时,认为获得锁) private ZkClient zk = null; private String rootNode = "/lock"; //根节点名称 private String selfNode; private final String className = this.getClass().getSimpleName(); //当前实例的className private String selfNodeName;//自身注册的临时节点名 private boolean handling = false; private static final String SPLIT = "/"; // private String selfNodeFullName; /** * 通过Zk获取分布式锁 */ protected void getLock(int lockNumber) { setLockNumber(lockNumber); initBean(); initNode(); subscribe(); register(); // heartBeat(); remainRunning(); } protected void getLock() { getLock(1); } /** * 初始化结点 */ private void initNode() { String error; if (!rootNode.startsWith(SPLIT)) { error = "rootNode必须以" + SPLIT + "开头"; System.out.println(error); throw new RuntimeException(error); } if (rootNode.endsWith(SPLIT)) { error = "不能以" + SPLIT + "结尾"; System.out.println(error); throw new RuntimeException(error); } int start = 1; int index = rootNode.indexOf(SPLIT, start); String path; while (index != -1) { path = rootNode.substring(0, index); if (!zk.exists(path)) { zk.createPersistent(path); } start = index + 1; if (start >= rootNode.length()) { break; } index = rootNode.indexOf(SPLIT, start); } if (start < rootNode.length()) { if (!zk.exists(rootNode)) { zk.createPersistent(rootNode); } } selfNode = rootNode + SPLIT + className; if (!zk.exists(selfNode)) { zk.createPersistent(selfNode); } } /** * 向zk注册自身节点 */ private void register() { selfNodeName = zk.createEphemeralSequential(selfNode + SPLIT, StringUtils.EMPTY); if (!StringUtils.isEmpty(selfNodeName)) { // selfNodeFullName = selfNodeName; System.out.println("自身节点:" + selfNodeName + ",注册成功!"); selfNodeName = selfNodeName.substring(selfNode.length() + 1); } checkMin(); } /** * 订阅zk的节点变化 */ private void subscribe() { zk.subscribeChildChanges(selfNode, (parentPath, currentChilds) -> { checkMin(); }); } /** * 检测是否获得锁 */ private void checkMin() { List<String> list = zk.getChildren(selfNode); if (CollectionUtils.isEmpty(list)) { System.out.println(selfNode + " 无任何子节点!"); lockFail(); handling = false; return; } //按序号从小到大排 Collections.sort(list); //如果自身ID在前N个锁中,则认为获取成功 int max = Math.min(getLockNumber(), list.size()); for (int i = 0; i < max; i++) { if (list.get(i).equals(selfNodeName)) { if (!handling) { lockSuccess(); handling = true; System.out.println("获得锁成功!"); } return; } } int selfIndex = list.indexOf(selfNodeName); if (selfIndex > 0) { System.out.println("前面还有节点" + list.get(selfIndex - 1) + ",获取锁失败!"); } else { System.out.println("获取锁失败!"); } lockFail(); handling = false; } /** * 获得锁成功的处理回调 */ protected abstract void lockSuccess(); /** * 获得锁失败的处理回调 */ protected abstract void lockFail(); /** * 初始化相关的Bean对象 */ protected abstract void initBean(); protected void setZkClient(ZkClient zk) { this.zk = zk; } protected int getLockNumber() { return lockNumber; } protected void setLockNumber(int lockNumber) { this.lockNumber = lockNumber; } protected void setRootNode(String value) { this.rootNode = value; } /** * 防程序退出 */ private void remainRunning() { byte[] lock = new byte[0]; synchronized (lock) { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.out.println("remainRunning出错:" + e.getMessage()); } } } }