<hadoop in action>翻译的比较烂, 不过zookeeper相对来说比较简单, 勉强看完, 做个笔记先^_^
当一个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);
}
}
zk实例方法大多都带有一个InterruptException异常, 可以通过调用被阻塞线程的interrupt()方法来抛出一个InterruptException异常来取消一个zk操作.
如果zk服务器发送错误信息或者服务器发生通信故障时, KeeperException将抛出, KeeperException用不同的子类来对应不同的出错, 比如KeeperException.NoNoeException在不存在的znode上执行操作将会被抛出
KeeperException包括三种明确的种类:
状态异常
通常出现在另一个进程在改变znode, 而当前进程没有感知到该改变. 例如调用setData方法对znode进行更新时, 另一个进程也在更新, 此时将抛出BadVersionException, 对于这种可能发生的情况, 必须通过编码重试来避免. 还有一些可能是程序错误, 比如在创建一个临时节点时可能发生NoChildrenEphemeralsException.
可恢复异常
比如在一个会话中, 可能连接丢失, 将触发ConnectionLossException, 此时可以通过重连来恢复会话, 保证会话的完整性.
幂等操作是指相同的结果可以被一次又一次应用的操作, 比如读请求或者无条件的setData, 它可以简单的被重新尝试.
不可恢复异常
比如创建连接时出现验证失败, 会抛出AuthFailedException. 另外一种就是会话过期, 会抛出SessionExpireException, 此时状态为CLOSED, 永远无法重连. 对于这种情况, 可以通过在watcher中判断KeeperState的状态是否为Expired, 如果是则尝试重新建立连接, 从而保证write方法的重试.
如果zk实例连接到zk服务器失败, 会尝试ensemble中的另一台, 如果所有服务器都无法连接, 将抛出IOException.
zookeeper应该只运行在只负责zookeeper的机器上, 有其他应用程序竞争资源会显著影响zookeeper的性能.
每一个在ensemble集群中的zookeeper都有一个在集群中唯一的数字, 这个数字必须在1~255之间, 这个号码在dataDir路径下的纯文本文件myId中.
在zookeeper配置文件中有这样一行:
引用
server.n=hostname:port:port
n表示服务器号, 有两个端口, 第一个是跟随者连接到领导者的端口, 第二个是用来选举领导者的.
比如:
引用
server.1=zookeeper1:2888:3888
zookeeper监听三个端口: 2181用来监听zookeeper客户端连接, 2888, 如果是领导者, 用来监听跟随者连接, 3888用来在选举领导者阶段, 用来监听其他服务器的连接.
当zookeeper服务器启动时, 它读取myid文件来判断这是哪个服务器, 然后读取配置文件来确定它需要监听哪个端口, 以及ensemble中其他服务器的网络地址
连接到zookeeper服务器的客户端实例, 应该使用"zookeeper1:2181, zookeeper2:2181,zookeeper3:2181"来作为zookeeper实例的构造参数.
在复制模式中, 还有另外两个属性: initLimit, syncLimit
initLimit表示跟随者连接到领导者可以与其同步的时间, 如果在这个时间内跟随者无法与领导者进行同步, 那么领导者将放弃领导地位, 重新选举, 通过查看日志, 如果发现这种事情经常发生, 说明这个时间设置短了, 需要加长一些.
syncLimit是允许一个跟随者与领导者同步的时间, 如果跟随者在这段时间内不能与领导者同步, 将重启, 而与该跟随者的连接将被连接到另外一台机器上.
参考:
Apache ZooKeeper
zookeeper使用和原理探究(一)
分布式服务框架 Zookeeper -- 管理分布式环境中的数据