<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; ZooKeeper</title>
	<atom:link href="https://blog.gmem.cc/tag/zookeeper/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Tue, 28 Apr 2026 08:15:52 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>Apache Curator学习笔记</title>
		<link>https://blog.gmem.cc/apache-curator-study-note</link>
		<comments>https://blog.gmem.cc/apache-curator-study-note#comments</comments>
		<pubDate>Thu, 10 Aug 2017 09:09:21 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[BigData]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[ZooKeeper]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=15366</guid>
		<description><![CDATA[<p>简介 Apache Curator（音标[kjʊ(ə)'reɪtə]）Framework是ZooKeeper的Keeper（动物园管理员的管理员）。它是一个Java库，提供了比ZooKeeper更加高层的API，更加易用、可靠。Curator的推荐的ZooKeeper版本是 3.5+，但它也和3.4兼容。 Curator实现了自动的连接管理，当会话过期后，你需要重新创建ZooKeeper客户端，并重新设置Watcher。Curator可以透明的重新创建、重试连接。 子项目 Curator以组标识[crayon-69f0ea68c6655370091965-i/]发布在Maven中心仓库，包含以下构件： 构件 说明 curator-recipes 对于大部分用户来说，只需要依赖此构件。包含所有recipes  curator-async 异步DSL curator-framework Curator框架的高层API  curator-client 客户端，代理ZooKeeper类  curator-x-discovery 基于 Curator框架的服务发现实现 curator-x-discovery-server 用于Curator发现的RESTful服务器  起步 <a class="read-more" href="https://blog.gmem.cc/apache-curator-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/apache-curator-study-note">Apache Curator学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Apache Curator<span style="color: #c0c0c0;"><em>（音标[kjʊ(ə)'reɪtə]）</em><span style="color: #333333;">Framework</span></span>是ZooKeeper的Keeper（动物园管理员的管理员）。它是一个Java库，提供了比ZooKeeper更加高层的API，更加易用、可靠。Curator的推荐的ZooKeeper版本是 3.5+，但它也和3.4兼容。</p>
<p>Curator实现了自动的连接管理，当会话过期后，你需要重新创建ZooKeeper客户端，并重新设置Watcher。Curator可以透明的重新创建、重试连接。</p>
<div class="blog_h2"><span class="graybg">子项目</span></div>
<p>Curator以组标识<pre class="crayon-plain-tag">org.apache.curator</pre>发布在Maven中心仓库，包含以下构件：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 28%; text-align: center;">构件</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>curator-recipes</td>
<td>对于大部分用户来说，只需要依赖此构件。包含所有recipes </td>
</tr>
<tr>
<td>curator-async</td>
<td>异步DSL</td>
</tr>
<tr>
<td>curator-framework</td>
<td>Curator框架的高层API </td>
</tr>
<tr>
<td>curator-client</td>
<td>客户端，代理ZooKeeper类 </td>
</tr>
<tr>
<td>curator-x-discovery</td>
<td>基于 Curator框架的服务发现实现</td>
</tr>
<tr>
<td>curator-x-discovery-server</td>
<td>用于Curator发现的RESTful服务器 </td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">起步</span></div>
<div class="blog_h2"><span class="graybg">Maven依赖</span></div>
<p>当前版本是4.0.0，通常你需要引用下面这个构件。此构件对curator-framework、curator-client有依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;org.apache.curator&lt;/groupId&gt;
    &lt;artifactId&gt;curator-recipes&lt;/artifactId&gt;
    &lt;version&gt;4.0.0&lt;/version&gt;
&lt;/dependency&gt;</pre>
<div class="blog_h2"><span class="graybg">连接和操控</span></div>
<p>Curator内置了重连逻辑，因此你不再需要手工Watch和管理了：</p>
<pre class="crayon-plain-tag">String zookeeperConnectionString = "172.21.0.1:2181,172.21.0.2:2181,172.21.0.3:2181";
// 重试策略，如果连接不上ZooKeeper集群如何重连
RetryPolicy retryPolicy = new ExponentialBackoffRetry( 1000, 3 );
CuratorFramework client = CuratorFrameworkFactory.newClient( zookeeperConnectionString, retryPolicy );
client.start();
// 创建znode
client.create().forPath( "/tmp", EMPTY );</pre>
<div class="blog_h2"><span class="graybg">CuratorFramework</span></div>
<div class="blog_h3"><span class="graybg">API</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">方法</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>create</td>
<td>启动一个znode创建操作，可以调用额外方法来设置节点类型、添加Watcher，使用forPath完成操作：<br />
<pre class="crayon-plain-tag">client.create().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath("/user", new byte[0]);</pre>
</td>
</tr>
<tr>
<td>delete</td>
<td>启动一个删除操作，使用forPath完成操作：<br />
<pre class="crayon-plain-tag">client.delete().inBackground().forPath("/user");</pre>
</td>
</tr>
<tr>
<td>checkExists</td>
<td>启动一个检查znode存在性的操作，使用forPath完成操作</td>
</tr>
<tr>
<td>getData</td>
<td>启动一个获取znode关联数据的操作，使用forPath完成操作：<br />
<pre class="crayon-plain-tag">client.getData().watched().inBackground().forPath("/user");</pre>
</td>
</tr>
<tr>
<td>setData</td>
<td>启动一个设置znode关联数据的操作，使用forPath完成操作</td>
</tr>
<tr>
<td>getChildren</td>
<td>启动获取znode子节点集合的操作，使用forPath完成操作</td>
</tr>
<tr>
<td>transactionOp</td>
<td>调用以生成供transaction()使用的操作条目</td>
</tr>
<tr>
<td>transaction</td>
<td>原子的提交一系列的操作条目</td>
</tr>
<tr>
<td>getACL</td>
<td>启动一个获取znode访问控制列表的操作，使用forPath完成操作</td>
</tr>
<tr>
<td>setACL</td>
<td>启动一个设置znode访问控制列表的操作，使用forPath完成操作</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Recipes</span></div>
<p>Recipes封装了很多高层语义，例如互斥锁、Leader选举，你不再需要手工实现了：</p>
<pre class="crayon-plain-tag">// 分布式互斥锁用法：
InterProcessMutex lock = new InterProcessMutex(client, lockPath);
if ( lock.acquire(maxWait, waitUnit) ) {
    try{
        // 临界区
    }
    finally{
        lock.release();
    }
}</pre><br />
<pre class="crayon-plain-tag">// 在多个进程之间选择Leader
LeaderSelectorListener listener = new LeaderSelectorListenerAdapter() {
    public void takeLeadership(CuratorFramework client) throws Exception {
        // 如果当前进程被选择为Leader则此回调被调用
        // 除非当前进程想放弃Leader地位，不要退出此方法
    }
}

LeaderSelector selector = new LeaderSelector(client, path, listener);
selector.autoRequeue();
selector.start();</pre>
<div class="blog_h2"><span class="graybg">ConnectionStateListener</span></div>
<p>Curator提供了此接口，用于处理连接中断：</p>
<pre class="crayon-plain-tag">public interface ConnectionStateListener {
    public void stateChanged(CuratorFramework client, ConnectionState newState);
} </pre>
<p>使用某些Recipe时，你应该通过Listenable.addListener注册此接口的实现。</p>
<div class="blog_h3"><span class="graybg">ConnectionState</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">状态</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>CONNECTED</td>
<td>第一次成功连接到ZooKeeper后，进入此状态。对于每个CuratorFramework对象，此状态仅出现一次</td>
</tr>
<tr>
<td>READONLY</td>
<td>连接进入只读模式，调用CuratorFrameworkFactory.Builder.canBeReadOnly(true)后导致此状态</td>
</tr>
<tr>
<td>SUSPENDED</td>
<td>到ZooKeeper的连接丢失</td>
</tr>
<tr>
<td>RECONNECTED</td>
<td>丢失的连接被重新建立</td>
</tr>
<tr>
<td>LOST</td>
<td>
<p>当Curator认为ZooKeeper会话已经过期，则进入此状态。可能的原因包括：</p>
<ol>
<li>ZooKeeper返回Watcher.Event.KeeperState.Expired或者KeeperException.Code.SESSIONEXPIRED</li>
<li>Curator关闭了内部管理的ZooKeeper客户端实例</li>
<li>由于网络中断导致的会话过期</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">Recipes</span></div>
<p>Recipes是Curator提供的高层封装。大部分Recipes都会自动创建所需要的父znode。</p>
<div class="blog_h2"><span class="graybg">选举类</span></div>
<p>在分布式计算领域，多个同类型服务器（节点）通常需要指定其中一员作为Leader，例如主从复制场景下需要指定主节点（Leader）。</p>
<p>Leader通常通过选举产生，在选举前，任何节点均不知道Leader是谁，选举完毕后，则所有节点对Leader是谁达成共识。</p>
<p>Curator提供了两个Recipes来支持选举。其中LeaderSelectorListener已经在起步一章示例过。</p>
<div class="blog_h3"><span class="graybg">LeaderLatch</span></div>
<p>用法示例：</p>
<pre class="crayon-plain-tag">String id = "websvr0";
LeaderLatch ll = new LeaderLatch( client, "/pems/websvr", id );
// 需要先启动
// 一旦启动，LeaderLatch会自动联系其它选举参与者，并随机选择一个作为Leader
ll.start();
// 任何时候，你可以调用下面的接口来判断当前进程是否为Leader
ll.hasLeadership();
// 一直等待，知道变为Leader
ll.await();
// 把当前进程从被选举人列表中移除，如果当前是Leader则放弃此权利，其它进程会被选举为Leader
ll.close();</pre>
<p>LeaderLatch会添加一个ConnectionStateListener来监控连接问题：</p>
<ol>
<li>当连接变为SUSPENDED/LOST状态后，当前Leader的hasLeadership()调用返回false（丢失Leader权限）</li>
<li>当断开的连接变为RECONNECTED，LeaderLatch会删除之前创建的znode，并创建新的</li>
</ol>
<div class="blog_h2"><span class="graybg">分布式锁</span></div>
<div class="blog_h3"><span class="graybg">InterProcessMutex</span></div>
<p>这是一个分布式的可重入（锁的持有者可以反复获得锁）的共享锁，保证任何时刻只能有一个客户端占有锁。</p>
<p>用法示例：</p>
<pre class="crayon-plain-tag">InterProcessMutex mutex = new InterProcessMutex( client, "/mutex/resource" );
// 阻塞，直到获得锁。返回true
mutex.acquire();
// 阻塞一定时间，尝试获得锁，返回true/false
mutex.acquire( 1000, TimeUnit.SECONDS );
// 释放锁，仅仅当调用线程是当初获得锁的线程时，才可以释放
mutex.release();

// 使得锁可被撤销，当其它进程/线程attemptRevoke此锁时，你会得到通知
mutex.makeRevocable( forLock -&gt; {
    // 得到通知后，你可以选择释放锁
    forLock.release();
} );

// 尝试撤销锁
Revoker.attemptRevoke(client,"/mutex/resource" );</pre>
<p>你应当设置一个ConnectionStateListener来监听SUSPENDED/LOST状态变更：</p>
<ol>
<li>当连接变为SUSPENDED状态后，你无法确定当前是否仍然持有锁，除非恢复到RECONNECTED状态</li>
<li>当连接变为LOST状态后，你肯定已经丢失了锁 </li>
</ol>
<div class="blog_h3"><span class="graybg">InterProcessSemaphoreMutex</span></div>
<p>API类似于InterProcessMutex，对应类InterProcessSemaphoreMutex。此锁不可以重入。</p>
<div class="blog_h3"><span class="graybg">InterProcessReadWriteLock</span></div>
<p>这是一个可重入的读写锁，所有JVM中的所有线程共享一个分布式的临界区域。此外，该锁是“公平的” —— 每个请求者按照其请求的顺序（ZooKeeper角度）来获得锁。</p>
<p>读写锁维护一对相关联的锁，一个用于读，一个用于写。其中读锁可以被多个进程共同持有，而写锁则是独占的（也不允许读锁被持有）。</p>
<p>持有写锁者，可以继续请求读锁，反之则不行。通过获取写锁 —— 获取读锁 —— 释放写锁，可以实现锁降级。</p>
<p>代码示例：</p>
<pre class="crayon-plain-tag">InterProcessReadWriteLock lock = new InterProcessReadWriteLock( client, "/resource" );
// 获取两个相关的锁，然后调用InterProcessMutex的API即可
InterProcessMutex readLock = lock.readLock();
InterProcessMutex writeLock = lock.writeLock();</pre>
<p>你应当设置一个ConnectionStateListener来监听SUSPENDED/LOST状态变更。 </p>
<div class="blog_h3"><span class="graybg">InterProcessSemaphoreV2</span></div>
<p>这是一个分布式的信号量。所谓信号量，是一个有限数量的资源集，进程可以租借/归还信号量。此信号量实现是“公平的” —— 每个请求者按照其请求的顺序（ZooKeeper角度）来获得信号量。</p>
<p>确定信号量资源数的方式有两种：</p>
<ol>
<li>由SharedCountReader类提供</li>
<li>静态声明信号量数量，这要求所有参与者声明相同的数值</li>
</ol>
<p>代码示例：</p>
<pre class="crayon-plain-tag">// 声明一个具有10个资源的信号量
InterProcessSemaphoreV2 semaphore = new InterProcessSemaphoreV2( client, "/semaphore", 10 );
// 通过SharedCount指定资源数量
SharedCountReader sc = new SharedCount( client, "/sharedcount", 10 );
semaphore = new InterProcessSemaphoreV2( client, "/semaphore", sc );

// 租借一个资源，可选参数等待时间
Lease lease = semaphore.acquire();
// 租借N个资源，可选参数等待时间
Collection&lt;Lease&gt; leases = semaphore.acquire( 2 );

// 归还资源
lease.close();
// 或者：
semaphore.returnLease( lease );
semaphore.returnAll( leases );</pre>
<p>你应当设置一个ConnectionStateListener来监听SUSPENDED/LOST状态变更。</p>
<div class="blog_h3"><span class="graybg">InterProcessMultiLock</span></div>
<p>持有一系列InterProcessLock的引用，客户端要么获取所有锁，要么失败。当释放时，所有锁一起被释放。</p>
<div class="blog_h2"><span class="graybg">屏障</span></div>
<div class="blog_h3"><span class="graybg">DistributedBarrier</span></div>
<p>所谓屏障，是一个同步点。每一个进程到达此点都要等待，直到所有进程都到达，则继续。</p>
<p>代码示例： </p>
<pre class="crayon-plain-tag">DistributedBarrier barrier = new DistributedBarrier( client, "/barrier" );
// 设置屏障，每个客户端设置一次
barrier.setBarrier();

// 等待所有客户端都到达，如果连接丢失，此方法会抛出异常
barrier.waitOnBarrier();</pre>
<div class="blog_h3"><span class="graybg">DistributedDoubleBarrier</span></div>
<p>双重屏障，在协作开始之前同步，当足够数量的进程加入到屏障后，开始协作，当所有进程完毕后离开屏障。</p>
<p>代码示例：</p>
<pre class="crayon-plain-tag">// 建立一个10个资源的屏障
DistributedDoubleBarrier barrier = new DistributedDoubleBarrier( client, "/barrier", 10 );
// 进入屏障，当有10个客户端进入屏障后，阻塞解除
barrier.enter();
// 这里是协作逻辑 ...
// 离开屏障，当所有客户端都尝试离开时，阻塞解除
barrier.leave();</pre>
<p>如果连接丢失，则enter/leave会抛出异常。 </p>
<div class="blog_h2"><span class="graybg">计数器</span></div>
<div class="blog_h3"><span class="graybg">SharedCount</span></div>
<p>一个共享的整数，所有客户端均看到一致性的、最新的数值。</p>
<p>代码示例：</p>
<pre class="crayon-plain-tag">// 第三个参数为初始值，假设路径不存在，则自动设置为此值
SharedCount counter = new SharedCount( client, "/counter", 0 );
// 此计数器需要启动
counter.start();
// 设置计数值
counter.setCount( 10 );
// 获取计数值
counter.getCount();
counter.addListener( new SharedCountListener() {
    @Override
    public void countHasChanged( SharedCountReader sharedCount, int newCount ) throws Exception {
        // 计数值变化后异步回调
    }

    @Override
    public void stateChanged( CuratorFramework curatorFramework, ConnectionState connectionState ) {
        // 连接数量变化后的异步回调
        // 如果连接变为SUSPENDED状态，你必须假设计数器的值不再精确
        // 如果连接变为LOST状态，则计数器永久性失效
    }
} );</pre>
<div class="blog_h2"><span class="graybg">其他</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">Recipe</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td><a href="http://curator.apache.org/curator-recipes/distributed-atomic-long.html">DistributedAtomicLong </a></td>
<td>分布式的原子的整数，支持自增、自减、加、减等操作</td>
</tr>
<tr>
<td><a href="http://curator.apache.org/curator-recipes/node-cache.html">NodeCache</a></td>
<td>拥有监控一个znode。每当数据变更或者此节点被删除，NodeCache都会更新自己的状态，反映当前数据（如果节点被删除数据为null）</td>
</tr>
<tr>
<td><a href="http://curator.apache.org/curator-recipes/path-cache.html">PathChildrenCache</a></td>
<td>用于监控一个znode。每当添加、更新、删除子节点时，PathChildrenCache都会更新自己的状态，反映最新的子节点集合、子节点数据、子节点状态</td>
</tr>
<tr>
<td><a href="http://curator.apache.org/curator-recipes/tree-cache.html">TreeCache</a></td>
<td>监控一个znode的整个子树</td>
</tr>
<tr>
<td><a href="http://curator.apache.org/curator-recipes/persistent-node.html">PersistentNode</a></td>
<td>尝试驻留ZooKeeper的节点，即使在连接/会话中断的情况下</td>
</tr>
<tr>
<td><a href="http://curator.apache.org/curator-recipes/persistent-ttl-node.html">PersistentTtlNode</a></td>
<td>如果你想创建TTL节点，但是又不愿意手工周期性设置其数据，可以使用</td>
</tr>
<tr>
<td><a href="http://curator.apache.org/curator-recipes/group-member.html">GroupMember</a></td>
<td>管理并缓存一组成员，用于构建集群成员列表</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">异步</span></div>
<div class="blog_h2"><span class="graybg">异步客户端</span></div>
<p>Curator提供了一个基于Java 8 Completion Stage的纯异步客户端实现。要使用此实现，添加依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;org.apache.curator&lt;/groupId&gt;
    &lt;artifactId&gt;curator-x-async&lt;/artifactId&gt;
    &lt;version&gt;4.0.0&lt;/version&gt;
&lt;/dependency&gt;</pre>
<div class="blog_h3"><span class="graybg">代码示例</span></div>
<pre class="crayon-plain-tag">String zookeeperConnectionString = "172.21.0.1:2181,172.21.0.2:2181,172.21.0.3:2181";
RetryPolicy retryPolicy = new ExponentialBackoffRetry( 1000, 3 );
CuratorFramework client = CuratorFrameworkFactory.newClient( zookeeperConnectionString, retryPolicy );
client.start();


// 此包装器提供异步API
AsyncCuratorFramework async = AsyncCuratorFramework.wrap( client );
//    检查znode /user是否存在             如果存在，打印状态结构
async.checkExists().forPath( "/user" ).thenAccept( stat -&gt; LOGGER.debug( stat.toString() ) );

// 获取数据
async.getData().forPath( "/user" ).thenAccept( data -&gt; LOGGER.debug( new String( data ) ) );

// 在最前面添加watched()调用，可以增加Watcher
// 使用AsyncStage（CompletionStage子类型）的event()方法，设置Watcher的回调
async.with( WatchMode.successOnly) // 不关心连接丢失类的事件
     .watched().getData().forPath( "/user" ).event().thenAccept( ev -&gt; LOGGER.debug( ev.toString() ) );

// 同步化
async.create().forPath("/user").toCompletableFuture().get();</pre>
<div class="blog_h2"><span class="graybg">强类型模型</span></div>
<p>通过串行化机制，Apache Curator支持在ZooKeeper的znode中存储类型化的数据。这一功能由Modeled Curator组件负责。</p>
<div class="blog_h3"><span class="graybg">ZPath</span></div>
<p>MC不使用原始的字符串形式的路径，而是使用ZPath来抽象ZooKeeper路径。ZPath可以是简单的字符串，也可以包含路径变量，这些变量可以根据需要替换为动态的取值：</p>
<pre class="crayon-plain-tag">// 静态路径
ZPath staticPath = ZPath.parse( "/static/path" );
// 动态路径，使用 {} 包围路径变量，路径变量又叫ID，其中可以包含任何东西，例如{ any thing}但是没有什么意义
// 路径变量（ID）总是从左向右依次解析
ZPath path = ZPath.parseWithIds( "/static/{}/{}" );
// 路径变量可以被替换，如果用于替换的对象是NodeName，则调用其nodeName()方法，否则调用toString()方法
ZPath resolvedPath = path.resolved( "path", new NodeName() {
    public String nodeName() {
        return "name";
    }
}  );
// 输出内容：/static/path/name
System.out.println( resolvedPath );

// 路径可以被部分的解析：
System.out.println( path.resolved( "path" ) );</pre>
<div class="blog_h3"><span class="graybg">ModelSpec</span></div>
<p>此类型包含对ZooKeeper路径进行操作（存取强类型对象）所需的全部元数据：</p>
<ol>
<li>一个ZPath</li>
<li>用于串行化对象的串行化器</li>
<li>关于如何创建znode的选项（顺序、压缩、TTL等）</li>
<li>针对znode的ACL</li>
<li>如何删除znode（是否删除子节点）</li>
</ol>
<div class="blog_h3"><span class="graybg">写入模型</span></div>
<p>要写入一个数据模型到znode的关联数据，参考如下代码： </p>
<pre class="crayon-plain-tag">public static class ServerInfo implements NodeName {
    private String name;
    private String type;

    @Override
    public String nodeName() {
        return getName();
    }
}

ModelSpec&lt;ServerInfo&gt; spec = ModelSpec.builder(
        ZPath.parseWithIds("/servers/{}"),
        JacksonModelSerializer.build(ServerInfo.class)
).build();
// 依赖异步客户端
ModeledFramework&lt;ServerInfo&gt; modeledClient = ModeledFramework.wrap(async, spec);

ServerInfo info = new ServerInfo();
info.setType("webserver");
info.setName("tk.gmem.cc");
// 输出：/servers/hk.gmem.cc，这个路径是把模型传入ZPath而解析得到的，因为模型实现了NodeName，因此调用其nodeName()替换路径变量
modeledClient.set(info).thenAccept(path -&gt; LOGGER.debug(path)).toCompletableFuture().get();</pre>
<p>本例使用JacksonJSON作为串行化机制的实现，需要引入依赖：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;com.fasterxml.jackson.core&lt;/groupId&gt;
    &lt;artifactId&gt;jackson-databind&lt;/artifactId&gt;
    &lt;version&gt;LATEST&lt;/version&gt;
&lt;/dependency&gt;</pre>
<div class="blog_h3"><span class="graybg">读取模型</span></div>
<pre class="crayon-plain-tag">ModelSpec&lt;ServerInfo&gt; spec = ModelSpec.builder(
        ZPath.parseWithIds("/servers/hk.gmem.cc"),
        JacksonModelSerializer.build(ServerInfo.class)
).build();
ModeledFramework&lt;ServerInfo&gt; modeledClient = ModeledFramework.wrap(async, spec);
modeledClient.read().whenComplete((info, e) -&gt; {
    if (e != null) {
        
    } else {
        LOGGER.debug(info.getType());
    }
}).toCompletableFuture().get();</pre>
<div class="blog_h2"><span class="graybg">迁移</span></div>
<p>这一功能允许你在一个事务中，执行一系列的ZooKeeper操作。你可以使用迁移来保证某个ZooKeeper子树的数据一致性。示例：</p>
<pre class="crayon-plain-tag">// 一系列操作组成一个迁移
CuratorOp op1 = async.transactionOp().create().forPath("/parent");
CuratorOp op2 = async.transactionOp().create().forPath("/parent/one");
CuratorOp op3 = async.transactionOp().create().forPath("/parent/two");
CuratorOp op4 = async.transactionOp().create().forPath("/parent/three");
// Migration是一个函数式接口，本质上就是一系列操作
Migration migration = () -&gt; Arrays.asList(op1, op2, op3, op4);
// 迁移集可以包含若干个迁移，迁移集必须有个ID
MigrationSet set = MigrationSet.build("main", Collections.singletonList(migration));

// 迁移管理器，负责执行迁移
// 迁移管理器会监控迁移集的执行状态，它只会应用那些没有应用的迁移
MigrationManager manager = new MigrationManager(client,
        lockPath,       // 迁移管理器会锁定此路径
        metaDataPath,   // 存储元数据的路径
        executor,       // 异步执行器
        lockMax         // 锁定最长时间
);
manager.migrate(set).exceptionally(e -&gt; {
    if (e instanceof MigrationException) {
        // migration checksum failed, etc.
    } else {
        // some other kind of error
    }
    return null;
});</pre>
<div class="blog_h1"><span class="graybg">服务发现</span></div>
<p>在SOA/分布式系统中，服务需要能够相互发现。例如，一个Web服务需要发现缓存服务。可以使用服务发现系统（Service Discovery system）来避免服务位置的硬编码。服务发现系统的功能包括：</p>
<ol>
<li>允许服务注册自己，便于其它服务调用之</li>
<li>定位某种服务的但个实例</li>
<li>当服务的实例发生变化，可以发出通知</li>
</ol>
<div class="blog_h2"><span class="graybg">Curator服务发现</span></div>
<p>Curator服务发现功能由单独的子项目实现：</p>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;org.apache.curator&lt;/groupId&gt;
    &lt;artifactId&gt;curator-x-discovery&lt;/artifactId&gt;
    &lt;version&gt;4.0.0&lt;/version&gt;
&lt;/dependency&gt;</pre>
<div class="blog_h3"><span class="graybg">ServiceInstance</span></div>
<p>这个类代表一个服务的实例，具有名称（同一类型服务共享）、标识符、地址、端口、可选的附加详细信息。服务实例被串行化在znode中，路径为/basePath/serviceName/id。</p>
<div class="blog_h3"><span class="graybg">ServiceProvider</span></div>
<p>基于提供策略（provider strategy）对某个特定的命名服务的发现服务进行封装，所谓提供策略，决定了如何从多个服务实例中选择一个实例。内置的策略包括循环选择（Round Robin）、随机、粘性（Sticky，总选择一个实例）。</p>
<p>ServiceProviderBuilder作为ServiceProvider的工厂，由ServiceDiscovery提供。ServiceProviderBuilder允许你设置服务的名称以及其它可选的属性。</p>
<p>ServiceProvider必须在<pre class="crayon-plain-tag">start()</pre>之后才能正常工作，当你不再需要它以后，应该调用<pre class="crayon-plain-tag">close()</pre>，要获取服务实例，调用<pre class="crayon-plain-tag">getInstance()</pre>。当无法连接到某个实例时，你应该调用<span style="color: rgba(0, 0, 0, 0.74902);"><pre class="crayon-plain-tag">noteError()</pre>，这样ServiceProvider会根据DownInstancePolicy决定何时将其标注为宕机。</span></p>
<div class="blog_h3"><span class="graybg">ServiceDiscovery</span></div>
<p>由ServiceDiscoveryBuilder创建，此类负责提供ServiceProviderBuilder。</p>
<p>ServiceProvider必须在<pre class="crayon-plain-tag">start()</pre>之后才能正常工作，当你不再需要它以后，应该调用<pre class="crayon-plain-tag">close()</pre></p>
<div class="blog_h3"><span class="graybg">示例代码</span> </div>
<pre class="crayon-plain-tag">// 当前JVM提供的服务实例
ServiceInstance&lt;InstanceDetail&gt; thisService = ServiceInstance.&lt;InstanceDetail&gt;builder()
        .name( "cacheService" )
        .payload( new InstanceDetail() )
        .address( "192.168.0.89" )
        .port( 8088 )
        .uriSpec( new UriSpec( "http://{address}:{port}" ) )
        .build();

// 服务发现
JsonInstanceSerializer&lt;InstanceDetail&gt; serializer = new JsonInstanceSerializer&lt;InstanceDetail&gt;( InstanceDetail.class );
ServiceDiscovery&lt;InstanceDetail&gt; discovery = ServiceDiscoveryBuilder
        .builder( InstanceDetail.class )
        .client( client )
        .basePath( "/sd" )
        .serializer( serializer )
        .thisInstance( thisService )
        .build();
// 需要启动
discovery.start();

// 服务提供者
ServiceProvider&lt;InstanceDetail&gt; sp = discovery
        .serviceProviderBuilder()
        .serviceName( "cacheService" )
        // 随机选取服务实例
        .providerStrategy( new RandomStrategy&lt;&gt;() )
        // 如果在一分钟内，有10此noteError则认为目标实例宕机
        .downInstancePolicy( new DownInstancePolicy( 60, TimeUnit.SECONDS, 10 ) )
        .build();
// 需要启动
sp.start();


LOGGER.debug( sp.getInstance().buildUriSpec() );</pre>
<div class="blog_h2"><span class="graybg">服务发现服务器</span></div>
<p>为了让非JVM应用使用服务发现功能，Curator提供了RESTful的WebService，你可以通过HTTP协议来注册、移除、查询服务。</p>
<p>Curator提供了JAX-RS组件，你可以在任何Web容器中，配合JAX-RS提供者（例如Jersey）使用该组件。</p>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">零散问题</span></div>
<div class="blog_h3"><span class="graybg">KeeperErrorCode = Unimplemented</span></div>
<p>可能原因是ZooKeeper服务器的版本与工程声明的ZooKeeper客户端版本不兼容导致。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/apache-curator-study-note">Apache Curator学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/apache-curator-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>ZooKeeper学习笔记</title>
		<link>https://blog.gmem.cc/zookeeper-study-note</link>
		<comments>https://blog.gmem.cc/zookeeper-study-note#comments</comments>
		<pubDate>Mon, 08 Aug 2016 09:23:19 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[BigData]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Hadoop]]></category>
		<category><![CDATA[ZooKeeper]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=15297</guid>
		<description><![CDATA[<p>基础知识 ZooKeeper是Hadoop的子项目，实现高可靠的分布式协调服务。它可以提供分布式的配置、同步、命名、集群服务。ZooKeeper暴露了一系列简单的接口，具有Java、C语言绑定。 为了正确构建复杂的服务，ZooKeeper提供以下保证： 顺序一致性：来自客户端的更新，按照它们被发送的顺序应用 原子性：更新要么成功要么失败 单一系统镜像：不管客户端连接到哪个ZooKeeper实例，它都看到服务的一致性视图 可靠性：一旦更新被写入，即是永久的 Timeliness：客户端看到的系统视图确保在一定时间范围内更新到最新状态 ZooKeeper组件 ZooKeeper由以下组件构成： 服务器：运行在ZooKeeper ensemble节点上的Java服务器 客户端：一个Java类库，用于链接到ZooKeeper集群 Native客户端：基于C实现的客户端 其它可选组件 Native客户端和可选组件仅仅支持Linux。 服务器端模块 除了Request Processor之外，所有ZooKeeper实例持有一模一样的组件： replicated database，一个内存数据库，包含整个名字空间。对此数据库的更新被刷入磁盘，以便重启后恢复 每个ZooKeeper实例都可以接受客户端请求，读请求基于本地的数据库处理，更新服务状态的写请求基于 <a class="read-more" href="https://blog.gmem.cc/zookeeper-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/zookeeper-study-note">ZooKeeper学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">基础知识</span></div>
<p>ZooKeeper是Hadoop的子项目，实现高可靠的分布式协调服务。它可以提供<span style="background-color: #c0c0c0;">分布式的配置、同步、命名、集群服务</span>。ZooKeeper暴露了一系列简单的接口，具有Java、C语言绑定。</p>
<p>为了正确构建复杂的服务，ZooKeeper提供以下保证：</p>
<ol>
<li>顺序一致性：来自客户端的更新，按照它们被发送的顺序应用</li>
<li>原子性：更新要么成功要么失败</li>
<li>单一系统镜像：不管客户端连接到哪个ZooKeeper实例，它都看到服务的一致性视图</li>
<li>可靠性：一旦更新被写入，即是永久的</li>
<li>Timeliness：客户端看到的系统视图确保在一定时间范围内更新到最新状态</li>
</ol>
<div class="blog_h2"><span class="graybg">ZooKeeper组件</span></div>
<p>ZooKeeper由以下组件构成：</p>
<ol>
<li>服务器：运行在ZooKeeper ensemble节点上的Java服务器</li>
<li>客户端：一个Java类库，用于链接到ZooKeeper集群</li>
<li>Native客户端：基于C实现的客户端</li>
<li>其它可选组件</li>
</ol>
<p>Native客户端和可选组件仅仅支持Linux。</p>
<div class="blog_h2"><span class="graybg">服务器端模块</span></div>
<p>除了Request Processor之外，所有ZooKeeper实例持有一模一样的组件：</p>
<p><img class="aligncenter size-full wp-image-15303" src="https://blog.gmem.cc/wp-content/uploads/2016/08/zkcomponents.png" alt="zkcomponents" width="611" height="248" /></p>
<ol>
<li>replicated database，一个内存数据库，包含整个名字空间。对此数据库的更新被刷入磁盘，以便重启后恢复</li>
<li>每个ZooKeeper实例都可以接受客户端请求，读请求基于本地的数据库处理，更新服务状态的写请求基于 agreement protocol处理。此协议：
<ol>
<li>将一个实例作为leader，其它实例作为followers</li>
<li>follower从leader接收消息提议，并同意消息递送</li>
<li>消息层负责leader的故障转移、follower与leader的同步</li>
</ol>
</li>
<li>ZooKeeper使用一个定制化的原子消息协议，保证消息层是原子性的。这种原子性保证了本地数据库不会出现数据不一致性 </li>
</ol>
<div class="blog_h2"><span class="graybg">基本特性</span></div>
<div class="blog_h3"><span class="graybg">简单性</span></div>
<p>ZooKeeper很简单，它允许分布式进程通过一个共享的、有层次的名字空间来相互交互。这个名字空间的组织就像文件系统一样，名字空间由数据寄存器（data registers） —— 所谓znodes组成，类似于文件系统的目录/文件，ZooKeeper的数据是驻留内存的，而不是像文件系统那样写在磁盘上。</p>
<div class="blog_h3"><span class="graybg">复制性</span></div>
<p>就像被它管理的分布式进程，ZooKeeper本身也倾向于跨越一组宿主机（所谓ensemble）复制，整体上形成一个ZooKeeper服务。只要大部分节点可用，则ZooKeeper服务可用。</p>
<p>客户端连接到单个ZooKeeper节点，通过一个TCP连接来：</p>
<ol>
<li>发送请求</li>
<li>获得响应</li>
<li>获得监听的事件</li>
<li>发送心跳</li>
</ol>
<p>如果此TCP连接丢失，客户端可能连接到另外一个节点。</p>
<div class="blog_h3"><span class="graybg">有序性</span></div>
<p>ZooKeeper使用数字标注每个update，以反应所有ZooKeeper事务的顺序。后续操作可以使用此数字序号实现高层次的抽象，例如同步原语。</p>
<div class="blog_h3"><span class="graybg">高性能</span></div>
<p>ZooKeeper非常高速，特别是在读为主的应用场景下。在上千台机器上运行的ZooKeeper应用，其性能在读写比10:1左右最好。</p>
<div class="blog_h2"><span class="graybg">名字空间</span></div>
<p>ZooKeeper提供的名字空间非常类似于Linux目录树，由一系列以 / 分隔的路径元素组成，名字空间中的每个节点以完整路径唯一识别。</p>
<div class="blog_h2"><span class="graybg">时间</span></div>
<p>ZooKeeper使用多种方式来追踪时间：</p>
<ol>
<li>Zxid：即ZooKeeper事务ID。每次对ZooKeeper的状态做出修改，都对应一个这样的ID。这个ID是单调递增的</li>
<li>版本号：对节点的修改会导致其某个版本号字段变更</li>
<li>时间单元（Ticks）：在集群中，ZooKeeper使用时间单元来界定状态上传、会话过期、Peer连接过期的最小时间间隔</li>
<li>真实时间：节点的创建、修改时间使用真实时间记录</li>
</ol>
<div class="blog_h2"><span class="graybg">会话</span></div>
<p>客户端利用C/Java语言绑定库建立会话：</p>
<ol>
<li>客户端发起调用时，可以指定连接字符串、会话超时、默认Watcher等参数，连接字符串包括逗号分隔的多个ZooKeeper实例的host:port列表</li>
<li>客户端创建一个到ZooKeeper服务集群的句柄（Handle），句柄被创建后，进入CONNECTING状态。这时，ZooKeeper会：
<ol>
<li>创建一个代表会话ID的64位整数并分配给客户端</li>
<li>同时，发送一个会话密码给客户端，客户端重连时需要用到此密码</li>
<li>根据客户端请求的会话超时ms，确定一个允许的（受限于时间单元，必须在2-20倍之间）会话超时</li>
</ol>
</li>
<li>绑定库尝试建立到某个ZooKeeper实例的TCP连接。连接建立后，句柄进入CONNECTED状态</li>
<li>如果出现不可恢复的错误，包括会话过期、身份验证失败、客户端显式的关闭句柄，则句柄进入CLOSED状态</li>
<li>如果客户端发现连接意外断开，则尝试连接列表中下一个实例，直到重新连接上。客户端重新连接时，会在握手时携带自己的会话ID、密码</li>
<li>客户端重新连接上之后，可能发现会话已经过期</li>
<li>默认Watcher可以在会话状态发生变化（例如连接断开、会话过期）后，通知客户端</li>
</ol>
<p>从3.2开始，连接字符串可以有一个chroot后缀，例如<pre class="crayon-plain-tag">127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002/pems/webmgr</pre> ，其意义是，将客户端的根znode设置为/pems/webmgr，所有绝对、相对路径均假设此znode为根。</p>
<p>客户端通过发送PING心跳来保活，这样双方都可以知道连接是否断开。</p>
<p>客户端可以变更连接字符串，也就是变更服务器列表。此变更可能触发一个负载均衡算法，并导致客户端重连到其它服务器。</p>
<div class="blog_h2"><span class="graybg">节点</span></div>
<p>名字空间的每个节点被称为znode，类似于文件系统的目录。znode有以下特点：</p>
<ol>
<li>路径是znode的唯一标识</li>
<li>znode可以具有关联的数据，还可以具有子节点</li>
<li>znode关联的数据通常很小，一般最大KB级别</li>
<li>znode具有一个状态结构（Stat Structure），存放版本号、数据长度、修改时间戳等信息</li>
<li>znode的数据发生变化，版本号就递增</li>
<li>znode同时保存数据的多个版本</li>
<li>读写znode数据的操作都是原子性的，写操作替换掉数据</li>
<li>每个节点包含一个访问控制列表（ACL）用于限制谁能做什么</li>
<li>znode可以被监控（watch），一旦数据修改、子节点变化，即可通常关注此znode的客户端。这是ZooKeeper的核心特性</li>
</ol>
<div class="blog_h3"><span class="graybg">临时节点</span></div>
<p>所谓临时（EPHEMERAL）节点，仅仅在创建它的Session存在期间存在。ZooKeeper的客户端Session基于TCP长连接，通过心跳来保活。</p>
<div class="blog_h3"><span class="graybg">有序节点</span></div>
<p>当创建一个znode时，你可以要求ZooKeeper<span style="background-color: #c0c0c0;">在路径尾部自动附加单调递增</span>的计数器。对于父节点来说，此计数器具有唯一性。计数器十位左侧补零，示例：<pre class="crayon-plain-tag">&lt;path&gt;0000000001</pre></p>
<p>计数器的下一个值，存储在父节点中。如果计数超过2147483647 会出现溢出。</p>
<div class="blog_h3"><span class="graybg">容器节点</span></div>
<p>3.6版本新增。这类节点的所有子节点都消失后，可能在未来的某个时间点被自动删除。</p>
<div class="blog_h3"><span class="graybg">状态结构</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">字段</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>czxid</td>
<td>创建此节点的zxid </td>
</tr>
<tr>
<td>mzxid</td>
<td>最后一次修改此节点的zxid </td>
</tr>
<tr>
<td>pzxid</td>
<td>最后一次修改子节点的zxid </td>
</tr>
<tr>
<td>ctime</td>
<td>此节点创建的时间</td>
</tr>
<tr>
<td>mtime</td>
<td>此节点被修改的时间</td>
</tr>
<tr>
<td>version</td>
<td>关联数据变更版本号</td>
</tr>
<tr>
<td>cversion</td>
<td>子节点变更版本号</td>
</tr>
<tr>
<td>aversion</td>
<td>ACL版本号</td>
</tr>
<tr>
<td>ephemeralOwner</td>
<td>
<p>如果此节点是临时节点，则此字段存放节点创建者的会话ID</p>
<p>如果不是临时节点，值为0</p>
</td>
</tr>
<tr>
<td>dataLength</td>
<td>关联数据的长度</td>
</tr>
<tr>
<td>numChildren</td>
<td>子节点的数量</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">监控</span></div>
<p>ZooKeeper支持监控（Watch）的概念——<span style="background-color: #c0c0c0;">一次性的事件监听器</span>。它有三个要点：</p>
<ol>
<li>一次性的：当被监控数据发生变化后，事件发送且仅一次。例如<pre class="crayon-plain-tag">getData("/znode1", true)</pre>导致当znode1的关联数据发生变化后客户端得到通知，但是后续znode1的数据再发生变化则不会自动得到通知</li>
<li>发送给客户端：事件被异步的发送给客户端，并且ZooKeeper提供顺序性保证，客户端不可能在收到通知之前就看到目标数据的变化</li>
<li>监控什么数据：getData/exists针对关联数据进行监控，getChildren()则针对子节点进行监控</li>
</ol>
<p>ZooKeeper中所有的读操作，包括<pre class="crayon-plain-tag">getData()</pre>、<pre class="crayon-plain-tag">getChildren()</pre>、<pre class="crayon-plain-tag">exists()</pre>，都可选设置Watch。</p>
<p>Watch仅仅在客户端所连接到的服务器上维护，但如果客户端发生重连，则新服务器负责维护客户端之前注册的Watch。有一种情况下，Watch会遗漏监控：</p>
<ol>
<li>客户端设置了exists监控，然后客户端断开</li>
<li>被监控节点创建，然后又被删除</li>
<li>客户端重连，此时它不会收到Watch</li>
</ol>
<p>客户端可以调用removeWatches来移除监控。</p>
<div class="blog_h3"><span class="graybg">关于监控的注意事项</span></div>
<p>ZooKeeper对Watch提供以下保证：</p>
<ol>
<li>先发生的事件，其Watcher先被通知</li>
<li>针对同一事件先注册的Watcher先被通知</li>
<li>先得到Watch通知，然后才能看到目标znode数据的变化</li>
</ol>
<p>使用Watch时需要注意：</p>
<ol>
<li>一次性，如果需要获得后续变更通知，需要再次注册Watcher</li>
<li>由于再次注册会有网络延迟，这期间目标znode可能已经发生了多次变化，这些变化是捕获不到的</li>
<li>在连接断开期间，接收不到通知</li>
</ol>
<div class="blog_h3"><span class="graybg">监控与API对照表</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">事件类型</td>
<td style="text-align: center;">可监控该事件的API</td>
</tr>
</thead>
<tbody>
<tr>
<td>节点创建</td>
<td>exists</td>
</tr>
<tr>
<td>节点删除</td>
<td>exists / getData / getChildren</td>
</tr>
<tr>
<td>节点数据改变</td>
<td>getData</td>
</tr>
<tr>
<td>子节点的创建、删除、改变</td>
<td>getChildren</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">访问控制</span></div>
<p>ZooKeeper支持基于ACL对znode进行访问控制。其实现有些类似UNIX文件模式，不同的是，ZooKeeper没有user/group/world这三种角色，它允许定义无限数量的角色（ID），并为这些角色授权。此外<span style="background-color: #c0c0c0;">ACL不是递归的</span>，也就是子节点不会继承父节点的授权设置。</p>
<p>ZooKeeper支持可拔插的身份验证方案（scheme）。ID的形式则为<pre class="crayon-plain-tag">scheme:id</pre>，例如ip:172.16.16.1表示IP验证方案下的实体172.16.16.1。</p>
<p>当客户端登录ZooKeeper并对自己进行身份验证时，ZooKeeper将所有对应此客户端的ID都分配给它。当客户端访问znode时，这些ID用来进行ACL验证。</p>
<p>ACL中每个条目的格式为<pre class="crayon-plain-tag">(scheme:expression, perms)</pre>，其中expression取决于scheme，覆盖1-N个ID。例如<pre class="crayon-plain-tag">(ip:172.21.0.0/16, READ)</pre>表示授予172.21网段所有客户端读权限。</p>
<div class="blog_h3"><span class="graybg">内置验证模式</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">Scheme</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>world</td>
<td>此模式仅包含单个id —— anyone，表示任何人</td>
</tr>
<tr>
<td>auth</td>
<td>此模式没有id，表示任何通过身份验证的人</td>
</tr>
<tr>
<td>digest</td>
<td>基于username:password字符串生产id，用在ACL条目中密码显示为Base64编码的SH1摘要</td>
</tr>
<tr>
<td>ip</td>
<td>使用客户端IP地址作为id</td>
</tr>
<tr>
<td>x509</td>
<td>使用数字整数进行验证，在ACL条目中使用X500主体名称作为id</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">ACL权限列表</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">Perm</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>CREATE</td>
<td>允许创建子节点</td>
</tr>
<tr>
<td>READ</td>
<td>允许读取数据、列出子节点</td>
</tr>
<tr>
<td>WRITE</td>
<td>允许修改节点的关联数据</td>
</tr>
<tr>
<td>DELETE</td>
<td>允许删除子节点</td>
</tr>
<tr>
<td>ADMIN</td>
<td>允许设置权限</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">安装配置</span></div>
<div class="blog_h2"><span class="graybg">安装</span></div>
<p>首先保证1.7或者更高版本的JDK已经安装，然后到<a href="https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/stable/">稳定频道</a>下载二进制压缩包，解压到合适的目录中。</p>
<div class="blog_h2"><span class="graybg">配置</span></div>
<div class="blog_h3"><span class="graybg">Standalone </span></div>
<p>要启动ZooKeeper服务，需要创建一个配置文件conf/zoo.cfg：</p>
<pre class="crayon-plain-tag"># 基本的时间单元对应的毫秒数，心跳每个时间单元发送一次，会话过期最小时间2时间单元
tickTime=2000
# 内存数据库的快照存放目录
dataDir=/data
# 监听客户端连接的端口
clientPort=2181</pre>
<div class="blog_h3"><span class="graybg">quorum</span></div>
<p>独立运行的ZooKeeper可以用户开发、测试目的。在产品环境下，你需要在复制（Replication）模式下运行ZooKeeper。一组复制的ZookKeeper称为Quorum，它们必须共享一致的配置文件：</p>
<pre class="crayon-plain-tag">tickTime=2000
dataDir=/var/lib/zookeeper
clientPort=2181
# 连接到Leader超时时间单元
initLimit=5
# Follower最多落后Leader多少时间单元
syncLimit=2
quorumListenOnAllIPs=true
# 服务器列表，server.X中的X是实例ID，在运行时，每个实例检查自己数据目录下的myid文件，其中以ASCII写着自己的ID
# 前一个端口供Follower连接到Leader使用；后一个端口用于选举Leader
server.1=172.21.0.1:2888:3888
server.2=172.21.0.2:2888:3888
server.3=172.21.0.3:2888:3888</pre>
<p>每个Quorum最少应该包含3个ZooKeeper实例，并且应当配置奇数个数的实例。</p>
<div class="blog_h3"><span class="graybg">docker</span></div>
<p>官方镜像的配置文件路径为/conf/zoo.cfg，下面的命令创建容器：</p>
<pre class="crayon-plain-tag"># ZOO_MY_ID 用于设置myid文件，此镜像假设数据目录是/data
docker run -e "ZOO_MY_ID=2" --name zookeeper-2 --network local --ip 172.21.0.2 -d docker.gmem.cc/zookeeper
docker run -e "ZOO_MY_ID=3" --name zookeeper-3 --network local --ip 172.21.0.3 -d docker.gmem.cc/zookeeper</pre>
<div class="blog_h3"><span class="graybg">日志配置</span></div>
<p>环境变量<pre class="crayon-plain-tag">ZOO_LOG_DIR</pre>用于指定ZooKeeper运行期间产生的日志的存放目录。包括log4j日志和守护程序的stdout都存放在此目录中。</p>
<p>环境变量<pre class="crayon-plain-tag">ZOO_LOG4J_PROP</pre>用于设置log4j的日志级别、启用的Appender。此变量的默认值是INFO, CONSOLE，也即是将最低INFO级别的日志输出到标准输出。可以设置为<pre class="crayon-plain-tag">ERROR,ROLLINGFILE</pre>，以使用滚动日志。</p>
<div class="blog_h2"><span class="graybg">运行</span></div>
<pre class="crayon-plain-tag"># 在后台启动ZooKeeper服务
zkServer.sh start
# 在前台启动ZooKeeper服务
zkServer.sh start-foreground
# 停止ZooKeeper服务
zkServer.sh stop

# 启动CLI客户端
zkCli -server 127.0.0.1:2181</pre>
<div class="blog_h1"><span class="graybg">开发</span></div>
<div class="blog_h2"><span class="graybg">简单的API</span></div>
<p>ZooKeeper具有非常简单的编程接口，它仅仅提供以下操作：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">操作</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>create</td>
<td>在名字空间树中创建一个节点</td>
</tr>
<tr>
<td>delete</td>
<td>删除一个节点</td>
</tr>
<tr>
<td>exists</td>
<td>测试某个路径上的节点是否存在</td>
</tr>
<tr>
<td>get data</td>
<td>读取节点数据</td>
</tr>
<tr>
<td>set data</td>
<td>写入节点数据</td>
</tr>
<tr>
<td>get children</td>
<td>获取子节点列表</td>
</tr>
<tr>
<td>sync</td>
<td>等待数据传播到整个集群</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Java绑定</span></div>
<div class="blog_h3"><span class="graybg">Maven依赖</span></div>
<pre class="crayon-plain-tag">&lt;dependency&gt;
    &lt;groupId&gt;org.apache.zookeeper&lt;/groupId&gt;
    &lt;artifactId&gt;zookeeper&lt;/artifactId&gt;
    &lt;version&gt;3.4.10&lt;/version&gt;
&lt;/dependency&gt;</pre>
<div class="blog_h3"><span class="graybg">有序节点</span></div>
<p>使用模式EPHEMERAL可以创建有序节点，反复针对同一路径创建有序节点，会自动后缀单调递增的编号。</p>
<pre class="crayon-plain-tag">ArrayList&lt;ACL&gt; acl = ZooDefs.Ids.OPEN_ACL_UNSAFE;
byte[] data = { 0 };
// 临时节点不支持子节点
try {
    // 同步创建
    zk.create( "/tmp", data, acl, CreateMode.PERSISTENT );
} catch ( KeeperException e ) {
    if ( e.code() != KeeperException.Code.NODEEXISTS ) throw e;
}
// 异步创建
zk.create( "/tmp/no", data, acl, CreateMode.EPHEMERAL_SEQUENTIAL, ( rc, path, ctx, name ) -&gt; {
    // rc: OK path: /tmp/no, name: /tmp/no0000000000
    // rc: OK path: /tmp/no, name: /tmp/no0000000001
    // rc: OK path: /tmp/no, name: /tmp/no0000000002
    LOGGER.debug( "rc: {} path: {}, name: {}", new Object[]{ KeeperException.Code.get( rc ), path, name } );
}, this );
TimeUnit.SECONDS.sleep( 1 );</pre>
<div class="blog_h3"><span class="graybg">简单Watch客户端</span></div>
<p>这个例子中，我们使用一个Agent来：</p>
<ol>
<li>管理到ZooKeeper的连接，一旦会话过期即重连</li>
<li>转发ZooKeeper发来的事件通知给客户端</li>
<li>对事件机制进行简单封装，实现了：
<ol>
<li>隔离客户端和ZooKeeper</li>
<li>会话过期后，自动重新注册Watch</li>
</ol>
</li>
</ol>
<p>Agent代码：</p>
<pre class="crayon-plain-tag">package cc.gmem.study;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Consumer;

public class ZooKeeperAgent implements Watcher {

    private static final Logger LOGGER = LoggerFactory.getLogger( ZooKeeperAgent.class );


    private ZooKeeper zk;

    private String connectString;

    private int timeout;

    private Map&lt;Event.EventType, Map&lt;String, Queue&lt;Consumer&lt;EventResolver&gt;&gt;&gt;&gt; allListeners;

    // 对WatchedEvent进行简单包装，支持获取znode数据，对客户端屏蔽ZooKeeper
    public class EventResolver {

        private WatchedEvent event;

        public EventResolver( WatchedEvent event ) {
            this.event = event;
        }

        public WatchedEvent getEvent() {
            return event;
        }

        public byte[] getData() {

            return getData( null );
        }

        public byte[] getData( Stat stat ) {
            if ( stat == null ) {
                stat = new Stat();
            }
            try {
                return zk.getData( event.getPath(), false, stat );
            } catch ( Exception e ) {
                throw new RuntimeException( e.getMessage(), e );
            }
        }
    }

    public ZooKeeperAgent( String connectString, int timeout ) {
        this.connectString = connectString;
        this.timeout = timeout;
        allListeners = new ConcurrentHashMap&lt;&gt;();
        allListeners.put( Event.EventType.NodeCreated, new ConcurrentHashMap&lt;&gt;() );
        allListeners.put( Event.EventType.NodeDataChanged, new ConcurrentHashMap&lt;&gt;() );
        allListeners.put( Event.EventType.NodeDeleted, new ConcurrentHashMap&lt;&gt;() );
        allListeners.put( Event.EventType.NodeChildrenChanged, new ConcurrentHashMap&lt;&gt;() );
        allListeners = Collections.unmodifiableMap( allListeners );
        createZooKeeper();
    }

    public void on( Event.EventType eventType, String path, Consumer&lt;EventResolver&gt; listener ) {
        Map&lt;String, Queue&lt;Consumer&lt;EventResolver&gt;&gt;&gt; listenersOfType = this.allListeners.get( eventType );
        Queue&lt;Consumer&lt;EventResolver&gt;&gt; listeners;
        synchronized ( this ) {
            listeners = listenersOfType.get( path );
            if ( listeners == null ) {
                listeners = new ConcurrentLinkedQueue&lt;&gt;();
                listenersOfType.put( path, listeners );
            }
        }
        listeners.add( listener );
        watch( eventType, path );
    }

    private void watch( Event.EventType eventType, String path ) {
        try {
            switch ( eventType ) {
                case NodeCreated:
                    // 这里可以传递一个Watcher类型，也可以传入boolean
                    // 如果为true，使用构造ZooKeeper时提供的Watcher ———— 所谓defaultWatcher
                    zk.exists( path, this );
                    break;
                case NodeDataChanged:
                    Stat stat = new Stat();
                    zk.getData( path, this, stat );
                    break;
                case NodeDeleted:
                    zk.exists( path, this );
                    break;
                case NodeChildrenChanged:
                    zk.getChildren( path, this );
                    break;
            }
        } catch ( Exception e ) {
            LOGGER.error( e.getMessage(), e );
        }
    }

    private void createZooKeeper() {
        try {
            zk = new ZooKeeper( connectString, timeout, this );
            // 新会话，Watch需要重新注册
            allListeners.entrySet().stream().flatMap( entry -&gt; {
                Event.EventType eventType = entry.getKey();
                return entry.getValue().keySet().stream().map( path -&gt; {
                    return new Object[]{ eventType, path };
                } );
            } ).forEach( args -&gt; watch( (Event.EventType) args[0], (String) args[1] ) );
        } catch ( IOException e ) {
            throw new RuntimeException( e.getMessage(), e );
        }
    }

    public void process( WatchedEvent event ) {
        switch ( event.getType() ) {
            case None:
                onKeeperStateEvent( event );
                break;
            default:
                onZnodeEvent( event );
        }
    }

    private void onKeeperStateEvent( WatchedEvent event ) {
        switch ( event.getState() ) {
            case SyncConnected:
                LOGGER.debug( "Connected to server with session id {}", zk.getSessionId() );
                break;
            case Expired:
                LOGGER.debug( "Session expired, recreating" );
                // 一旦会话过期，ZooKeeper对象就废了
                createZooKeeper();
                break;
        }
    }

    private void onZnodeEvent( WatchedEvent event ) {
        Queue&lt;Consumer&lt;EventResolver&gt;&gt; consumers = allListeners.get( event.getType() ).get( event.getPath() );
        if ( consumers != null ) {
            consumers.forEach( consumer -&gt; consumer.accept( new EventResolver( event ) ) );
            // 继续监控
            watch( event.getType(), event.getPath() );
        }
    }

}</pre>
<p>客户端代码：</p>
<pre class="crayon-plain-tag">package cc.gmem.study;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;

import static org.apache.zookeeper.Watcher.Event.EventType.*;

/**
 * Created by alex on 8/10/16.
 */
public class WatchClient {

    private static final Logger LOGGER = LoggerFactory.getLogger( WatchClient.class );

    public static void main( String[] args ) throws InterruptedException {
        String connectString = "172.21.0.1:2181,172.21.0.2:2181,172.21.0.3:2181";
        ZooKeeperAgent agent = new ZooKeeperAgent( connectString, 2000 * 20 );
        agent.on( NodeCreated, "/user", ( resolver ) -&gt; {
            Stat stat = new Stat();
            LOGGER.debug( "znode craeted with data: {}", new String( resolver.getData( stat ) ) );
        } );
        agent.on( NodeDataChanged, "/user", ( resolver ) -&gt; {
            Stat stat = new Stat();
            LOGGER.debug( "znode data: {}, version: {}", new String( resolver.getData( stat ) ), stat.getVersion() );
        } );
        agent.on( NodeDeleted, "/user", ( resolver ) -&gt; {
            LOGGER.debug( "znode deleted" );
        } );
        TimeUnit.DAYS.sleep( 1 );
    }
}</pre>
<div class="blog_h1"><span class="graybg">应用场景</span></div>
<div class="blog_h2"><span class="graybg">命名服务</span></div>
<p>ZooKeeper名字空间可以直接当做类似于JNDI的命名服务使用，ZooKeeper和JNDI都可以把目录节点关联到特定的资源（但是JNDI的资源是Java对象），ZooKeeper还具有与生俱来的高可用性。</p>
<div class="blog_h2"><span class="graybg">配置管理</span></div>
<p>ZooKeeper可以用来管理分布式系统中的配置项，如果多个应用服务实例需要共享很多系统参数，可以交由ZooKeeper来管理，通过Watch的方式，应用服务可以在配置变更后获得通知。</p>
<div class="blog_h2"><span class="graybg">集群管理</span></div>
<p>ZooKeeper可以管理服务的集群：</p>
<ol>
<li>当新增服务器、删除服务器（服务器宕机）时，客户端、ZooKeeper、集群成员可以得到通知。实现原理是：
<ol>
<li>每个集群成员启动后，都在某个znode下创建临时子节点，这个子节点依赖于TCP长连接保活</li>
<li>一旦集群成员宕机，临时子节点由于TCP连接的断开而自动删除</li>
<li>关注者可以在父节点上调用getChildren( parentPath, true)进行watch，一旦子节点发生增减，此调用即返回</li>
</ol>
</li>
<li>在集群成员中选取Leader（Master）。实现原理是：
<ol>
<li>每个成员创建不但是临时，还是有序（SEQUENTIAL）的子节点</li>
<li>序号最小的成员，总是作为Leader</li>
<li>如果当前Leader宕机，则次小的成员被选作Leader</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">分布式锁</span></div>
<p>要实现独占锁，可以把多个协作者共享的资源抽象为一个znode，然后：</p>
<ol>
<li>需要占用此资源的协作者，在znode 下创建一个临时、有序子节点</li>
<li>协作者判断自己这个节点序号是否最小：
<ol>
<li>如果是，意味着获得锁</li>
<li>如果否，则在父节点上watch。收到通知后，继续执行步骤2</li>
</ol>
</li>
<li> 当需要是否资源时，删除自己创建的子节点即可</li>
</ol>
<div class="blog_h2"><span class="graybg">屏障</span></div>
<p>所谓屏障（Barrier），即协作者到达某个状态后，等待其它协作者都到达此状态，然后一起继续。基于ZooKeeper可以这样实现屏障：</p>
<ol>
<li>每个协作者到达状态后，均创建一个子节点</li>
<li>判断子节点总数是否足够：
<ol>
<li>如果是，跨越屏障继续执行业务逻辑</li>
<li>如果否，则在父节点上watch。收到通知后，继续执行步骤2</li>
</ol>
</li>
</ol>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">零散问题</span></div>
<div class="blog_h3"><span class="graybg">在Docker桥接接口上监听失败</span></div>
<p>报错信息：</p>
<p>2017-08-09 17:53:03,750 [myid:1] - ERROR [/172.21.1.1:3888:QuorumCnxManager$Listener@763] - Exception while listening<br />java.net.BindException: Cannot assign requested address (Bind failed)</p>
<p>解决办法：配置文件添加<pre class="crayon-plain-tag">quorumListenOnAllIPs=true</pre></p>
<div class="blog_h3"><span class="graybg">如何查看集群状态</span></div>
<p>可以通过JMX查看，通过Oracle Mission Controll连接到名为 org.apache.zookeeper.server.quorum.QuorumPeerMain  的JVM，在MBean Browser选项卡中可以看到相关信息。</p>
<div class="blog_h3"><span class="graybg">zookeeper.out</span></div>
<p>此文件是ZooKeeper运行期间的标准输出，要指定它的存放位置，可以设置<pre class="crayon-plain-tag">ZOO_LOG_DIR</pre>这个环境变量。</p>
<p>注意，上述环境变量同时也作为log4j的日志输出目录。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/zookeeper-study-note">ZooKeeper学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/zookeeper-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
