Zookeeper能帮我们作什么事情呢?简单的例子:假设我们我们有个20个搜索引擎的服务器(每个负责总索引中的一部分的搜索任务)和一个总服务器(负责向这20个搜索引擎的服务器发出搜索请求并合并结果集),一个备用的总服务器(负责当总服务器宕机时替换总服务器),一个web的cgi(向总服务器发出搜索请求).搜索引擎的服务器中的15个服务器现在提供搜索服务,5个服务器正在生成索引.这20个搜索引擎的服务器经常要让正在提供搜索服务的服务器停止提供服务开始生成索引,或生成索引的服务器已经把索引生成完成可以搜索提供服务了.使用Zookeeper可以保证总服务器自动感知有多少提供搜索引擎的服务器并向这些服务器发出搜索请求,备用的总服务器宕机时自动启用备用的总服务器,web的cgi能够自动地获知总服务器的网络地址变化.这些又如何做到呢?
当一个ZooKeeper实例被创建之后, 它启动一个线程连接到ZooKeeper服务器, 对构造函数的响应返回的很快, 因此在使用ZooKeeper对象钱等待建立连接非常重要, 因此借助并发包中的CountDownLatch来阻塞, 直到ZooKeeper实例准备好.
客户端连接到ZooKeeper之后, Watcher的process()方法被调用, 并收到一个事件, 表明连接已经完成. 在收到连接事件时(由Watcher.Event.KeeperState枚举型表示, 并带有值SyncConnected), CountDownLatch的countDown()方法被调用, 计数减一, 即释放等待线程, 表明连接建立, 可以自行其他操作了. 比如创建znode.
ZooKeeper可以提供一种高可用, 高性能的协作服务.
ZooKeeper是为协作而设计的(通常使用小数据文件), 不是大容量的数据存储, 因此任何一个znode的数据存储量的上限是1MB
ZooKeeper上的数据访问都是原子的. 不可能出现部分数据被客户端写入, ZooKeeper不支持追加操作.
znode的路径必须是绝对, 因此, 他们必须由反斜杠字符开头. 除此之外, 他们还必须是唯一的.
zookeeper在路径中是保留字, /zookeeper用来存储管理信息, 比如一些配额信息.
znode可以分为两种: 临时的和永久的. znode类型是在创建时指定的, 并且不能被改变. 一个临时性znode会在创建它的客户端的会话结束时由ZooKeeper删除, 一个永久的znode并不依赖客户端会话, 而且只有在客户端明确删除它的时候才会被删除(不一定是创建的客户端), 一个临时的znode不应该有子节点, 即临时性的子节点.
临时性znode绑定在客户端会话上, 它们对所有客户端都是可见的.
临时性znode对于建立那些需要知道什么时候某些分布式资源可以使用的应用非常有效.
在znode有改变时, Watch使客户端了解相应的信息. Watch由ZooKeeper服务的操作来设置, 同时由服务的其他操作来触发. 比如, 一个客户端可能调用znode上的exists操作, 同时在这个节点上加了一个Watch, 如果这个节点不存在, 返回false, 如果一段时间之后, 这个znode由另一台客户端建立了, 那么Watch将被触发, 通知第一台客户端znode被创建的消息.
Watcher只被触发一次, 为了获得多次提醒, 客户端需要再次注册Watcher.
更新ZooKeeper的操作是有限制的. delete或setData必须明确需要更新的znode的版本号, 如果版本号不匹配, 更新就会失败. 更新操作是非阻塞的, 因此客户端如果失去了一个更新, 它可以在不阻塞其他进行进程执行的情况下, 选择重新尝试或进行其他操作.
尽管ZooKeeper可以被看着是一个文件系统, 但是它处于便利性的考虑, 摒弃了一些文件系统的操作原语. 因为文件非常小并且是整体读写的, 所以不需要提供打开, 关闭或寻址操作.
ZooKeeper的异步化API使你能并行处理请求, 在某些场景下可以提供更好的吞吐量. 如果你想读取大批量znode并且独立的处理他们, 使用同步api的话, 那么每个读操作都会被阻塞, 直到它返回的那一刻, 相反使用异步化, 可以非常快的执行所有异步操作, 并且用不同的线程来处理响应.
读操作exists, getChildren和getData都被设置了watch, 并且这些watch都由写操作来触发:create, delete和setData. ACL操作并不参与到watch中.
exists操作上的watch在被监视的znode创建, 删除或数据更新时被触发
getData操作上的watch在被监视的znode删除或数据更新时触发, 在创建时不能触发, 因为只有znode一定存在, getData操作才会成功.
getChildren操作上的watch在被监视的znode的子节点创建或删除时, 或者当这个znode节点的子节点被删除时被触发. 可以通过查看watch事件类型来区分是znode, 还是它的子节点被删除.
ACL
如果我们想要客户端在example.com域中对znode进行读访问, 那么可以对这个znode的ACL进行设置, 使用host模式, 带有example.com的ID和READ许可, 在java中可以这样创建ACL对象:
new ACL(Perms.READ, new Id("host", "example.com"));
exists并不受ACL许可控制.
ZooKeeper以复合模式运行在一组叫做ensemble的集群上, ZooKeeper通过复制来获得高可用性, 同时, 只要ensemble中的大部分机器都运行正常就可以提供服务. 比如说, 在一个5节点ensemble中, 可以在任何两台机器故障的情况下服务仍在运作, 如果6节点的话, 也只能承受两台出现故障, 因此一个ensemble通常选择奇数台机器.
ZooKeeper的思想非常简单: 它所需要做的就是保证对znode树的每一次修改都复制到ensemble中的大部分机器上去.
ZooKeeper采用了Zab协议, 它分为两个阶段, 并且可能被无限制的重复.
阶段1:领导者选举
在ensemble中的机器要参与一个选择特殊成员的进程, 这个成员叫做领导者, 其他机器则叫做跟随者, 在大部分的跟随者与它们的领导者同步了状态以后, 这个阶段才算完成.
阶段2:原子广播
所有的写操作请求被传送给领导者, 并通过广播将更新信息告诉给跟随者, 当大部分跟随者执行了修改后, 领导者就会提交更新操作, 客户端将得到更新成功的回应.
如果领导者出现故障, 剩下的机器将会再次进行领导者选举, 并在新领导者被选出来之前继续执行任务. 如果在不久之后, 老的领导者恢复了. 那么它将以跟随者的身份继续运行, 领导者的选举非常快(200ms左右), 因此不会带来明显的延迟.
所有在ensemble中的机器在更新它们的内存中的znode树之前会先将更新信息写入磁盘. 读操作请求可能由任何机器服务, 同时由于它们只涉及到内存查找, 因此非常快.
一致性
在ensemble中的领导者和跟随者非常聪明, 跟随者通过来更新号来滞后领导者, 结果导致只要大部分而不是所有ensemble确认更新
每一个对znode树的更新都会给定一个全局标识, 叫zxid(ZooKeeper事务id).
ZooKeeper客户端与ensemble中的服务器列表配置相一致, 在启动时, 它尝试与表中的一个服务器相连接, 如果连接失败, 它就尝试列表中的其他服务器, 以此类推, 直到最终连接到其中一个, 或者当ZooKeeper的所有服务器都无法连接时, 连接失败.
一旦与ZooKeeper服务器连接成功, 服务器会创建与客户端的一个新的会话. 每个会话都会有超时时间, 这个是在会话创建时设定的, 如果服务器在超时时段内得到请求, 它可能中断该会话, 一旦会话中断, 它可能不再打开, 与该会话相关的临时性节点都将丢失.
在会话空闲的一定时间内, 都会由客户端发起ping请求来保持活跃(犹如心跳)(ping是由zk客户端自动发送, 不需要由程序来指定), 超时时段要设置的足够小, 以便能检测到服务器故障, 并且能在会话超时时连接到另外一台服务器.
创建复杂的临时性节点状态的应用, 应该设置更长的会话超时时间, 因为重建这些内容的代价非常昂贵, 在这种情况下, 应用程序可以有更多的时间来重启, 从而避免会话过期. 每个会话都由服务器给定一个唯一的身份和密码, 而且如果在建立连接时传递给zk的话, 它就能恢复会话(只要没有过期), 所以应用程序可以安全关闭, 同时因为存储了身份和密码, 它可以重新获得这个身份和密码并恢复会话.
一个zk实例一次只能处于一种状态, 一个zk实例在建立与zk服务器的建立时, 处于CONNECTING状态, 一旦连接建立, 它就变成CONNECTED状态了.
使用zk的客户端可以通过注册watcher的方法来获取状态的改变的消息. 一旦进入CONNECTED状态, watcher将获得一个KeeperState值为SyncConnected的WatchEvent.
zk的watcher对象有两个职责, 一个是了解zk实例的状态变化, 一个了解znode的改变, 初始化或者在在zk实例读取znode节点信息的读方法(exists方法和getData方法)上指定是否需要watch znode节点变化, 这里的watcher可以是专门的, 也可以是zk实例构造函数中指定默认的.
zk可以作为配置的高可用存储, 使应用的参与者可以恢复或更新配置文件, 使用zk的监测可以创建灵活配置服务, 有需要的客户端可以以此来了解配置的改变.
public class ConnectionWatcher implements Watcher{ private static final int SESSION_TIMEOUT = 5000; private ZooKeeper zk; private CountDownLatch connectedSignal = new CountDownLatch(1); public void connect(String hosts) throws IOException, InterruptedException { zk = new ZooKeeper(hosts, SESSION_TIMEOUT, this); connectedSignal.await(); } @Override public void process(WatchedEvent event) { if (event.getState() == KeeperState.SyncConnected) { connectedSignal.countDown(); } } public void create(String groupName) throws KeeperException, InterruptedException { String path = "/" + groupName; String createPath = zk.create(path, null, Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); System.out.println("created" + createPath); } public void close() throws InterruptedException { zk.close(); } } public class ActiveKeyValueStore extends ConnectionWatcher { private final Charset CHARSET = Charset.forName("UTF-8"); private final int MAX_RETRIES = 3; private final int RETRY_PERIOD_SECONDS = 5; public void write(String path, String value) throws KeeperException, InterruptedException { Stat stat = zk.exists(path, false); int retries = 0; while(true) { try { if (stat == null) { zk.create(path, value.getBytes(CHARSET), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); }else { zk.setData(path, value.getBytes(CHARSET), -1); } }catch(KeeperException.SessionExpiredException e) { throw e; }catch(KeeperException e) { if (retries ++ == MAX_RETRIES) { throw e; } TimeUnit.SECONDS.sleep(RETRY_PERIOD_SECONDS); } } } public String read(String path, Watcher watcher) throws KeeperException, InterruptedException { byte[] data = zk.getData(path, watcher, null); return new String(data, CHARSET); } } public class ConfigWatcher implements Watcher { private ActiveKeyValueStore store; private String path; public ConfigWatcher(String hosts) throws IOException, InterruptedException { store = new ActiveKeyValueStore(); store.connect(hosts); } @Override public void process(WatchedEvent event) { if (event.getType() == EventType.NodeDataChanged) { try { displayConfig(); } catch (KeeperException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); Thread.currentThread().interrupt(); } } } public void displayConfig() throws KeeperException, InterruptedException { System.out.println(store.read(path, this)); } public static void main(String[] args) throws Exception { ConfigWatcher watcher = new ConfigWatcher(args[0]); watcher.displayConfig(); Thread.sleep(Long.MAX_VALUE); } }
server.n=hostname:port:port
server.1=zookeeper1:2888:3888