Hadoop 之 ZooKeeper (二)

继  Hadoop 之 ZooKeeper (一)

4. 使用 ZooKeeper 构建应用 (Building Applications with ZooKeeper)

-----------------------------------------------------------------------------------------------------------------------------------------


4.1  一个配置服务 (A Configuration Service)
-----------------------------------------------------------------------------------------------------------------------------------------
分布式应用所需要的基本服务之一是配置服务,它使配置信息中那些公共的部分可以由集群中的机器共享。简单来说,ZooKeeper 可以作为配置信息的高可
用性存储,允许应用程序参与者获取或更新配置文件。使用 ZooKeeper watch, 可以创建一个活动的配置服务,而让感兴趣的客户端在配置发生变化时获得
通知。

让我们来编写这样一个服务。为了简化实现,我们做几个假设(稍加修改就可以移除这些假设)。第一,唯一需要存储的的配置数据为字符串,关键字是 znode
路径,因此我们使用一个 znode 来存储每个 key-value 对。第二,任何时间,只能有一个客户端执行更新。除了别的之外,这个模型符合 master 思想(
类似于 HDFS 中的 namenode), 即 master 更新信息,workers 需要遵循这些信息(follow)。

将代码封装到 ActiveKeyValueStore 类中:


public class ActiveKeyValueStore extends ConnectionWatcher {
    private static final Charset CHARSET = Charset.forName("UTF-8");
    
    public void write(String path, String value) throws InterruptedException, KeeperException {
        
        Stat stat = zk.exists(path, false);
        if (stat == null) {
            zk.create(path, value.getBytes(CHARSET), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        } else {
            zk.setData(path, value.getBytes(CHARSET), -1);
        }
    }
}

write() 方法的功能是将一个 key 和其给定的 value 写入到 ZooKeeper. 它隐藏了创建一个新 znode 和用一个新值更新已有 znode 的区别,先用 znode 的
exists 操作测试,然后执行相应的操作。

为了演示使用 ActiveKeyValueStore, 考虑 ConfigUpdater 类来用一个新值更新一个配置属性:

//An application that updates a property in ZooKeeper at random times
public class ConfigUpdater {
    public static final String PATH = "/config";
    
    private ActiveKeyValueStore store;
    private Random random = new Random();
    
    public ConfigUpdater(String hosts) throws IOException, InterruptedException {
        store = new ActiveKeyValueStore();
        store.connect(hosts);
    }
    
    public void run() throws InterruptedException, KeeperException {
        while (true) {
            String value = random.nextInt(100) + "";
            store.write(PATH, value);
            System.out.printf("Set %s to %s\n", PATH, value);
            TimeUnit.SECONDS.sleep(random.nextInt(10));
        }
    }
    public static void main(String[] args) throws Exception {
        ConfigUpdater configUpdater = new ConfigUpdater(args[0]);
        configUpdater.run();
    }
}

下一步,看看如何读取 /config 配置属性。首先,为 ActiveKeyValueStore 添加一个 read method:

    public String read(String path, Watcher watcher) throws InterruptedException,
    KeeperException {
        byte[] data = zk.getData(path, watcher, null/*stat*/);
        return new String(data, CHARSET);
    }

作为服务的消费者,ConfigWatcher 创建了一个 ActiveKeyValueStore, 并且在启动之后,调用 store 的 read() method(在它的 displayConfig() method
中) 把它自己的引用作为 watcher 传递进去。它显示出读取到的配置信息初始值。

//An application that watches for updates of a property in ZooKeeper and prints them to the console
public class ConfigWatcher implements Watcher {
    
    private ActiveKeyValueStore store;
    
    public ConfigWatcher(String hosts) throws IOException, InterruptedException {
        store = new ActiveKeyValueStore();
        store.connect(hosts);
    }
    
    public void displayConfig() throws InterruptedException, KeeperException {
        String value = store.read(ConfigUpdater.PATH, this);
        System.out.printf("Read %s as %s\n", ConfigUpdater.PATH, value);
    }
    
    @Override
    public void process(WatchedEvent event) {
        if (event.getType() == EventType.NodeDataChanged) {
            try {
                displayConfig();
            } catch (InterruptedException e) {
                System.err.println("Interrupted. Exiting.");
                Thread.currentThread().interrupt();
            } catch (KeeperException e) {
                System.err.printf("KeeperException: %s. Exiting.\n", e);
            }
        }
    }
    
    public static void main(String[] args) throws Exception {
        ConfigWatcher configWatcher = new ConfigWatcher(args[0]);
        configWatcher.displayConfig();
        // stay alive until process is killed or thread is interrupted
        Thread.sleep(Long.MAX_VALUE);
    }
}

当 ConfigUpdater 更新 znode 时,ZooKeeper 使 watcher 发生一个类型为 EventType.NodeDataChanged 的事件。ConfigWatcher 在其 process() 中
处理该事件,读取并显示配置信息的最新版本。

因为 watch 是一次性信号,因此每次调用 ActiveKeyValueStore 的 read() 方法时让 ZooKeeper 有一个新的 watch, 保证能看到之后的更新。尽管如此,
我们还是不能保证接收到每一个更新,因为在收到 watch 事件通知与下一次读取之间,znode 可能已经被更新过,而且可能是很多次更新,由于客户端
在这段时间内没有注册任何 watcher , 因此不会收到通知。对于示例中配置的服务,这不是问题,因为客户端只关心属性的最新值,最新值优先于之前的
值。但一般情况下,这个潜在的问题是不容忽视的。

运行: 在一个终端窗口启动 ConfigUpdater

    % java ConfigUpdater localhost
    Set /config to 79
    Set /config to 14
    Set /config to 78

之后立刻在另一个窗口启动 ConfigWatcher

    % java ConfigWatcher localhost
    Read /config as 79
    Read /config as 14
    Read /config as 78
    
    


4.2 可恢复 ZooKeeper 应用 (The Resilient ZooKeeper Application)
-----------------------------------------------------------------------------------------------------------------------------------------
关于分布式计算的第一个误区是 "网络是可靠的(the network is reliable)"。按照他们的观点,程序总是假定有一个可靠的网络,因此当程序运行在真正
的网络上时,往往会出现各种故障。让我们看看各种可能的故障模式,以及能够解决故障的措施,使我们的程序在面对故障时能够及时恢复。

Java API 中每个 ZooKeeper 操作在其 throws 字句中声明两个类型的异常:InterruptedException 和 KeeperException.


    ■ InterruptedException 异常 (InterruptedException)
    -------------------------------------------------------------------------------------------------------------------------------------
    如果有操作中断(interrupt)会抛出 InterruptedException 异常。有一个标准的 Java 机制用于取消阻塞的方法(blocking methods), 在阻塞的方法
    调用线程上调用 interrupt()。一个成功的取消操作将产生一个 InterruptedException 异常。ZooKeeper 也遵循这一机制,因此可以使用这种方法来
    取消一个 ZooKeeper 操作。ZooKeeper 的类或库通常会传播 InterruptedException 异常,因此客户端可以取消它们的操作。

    一个 InterruptedException 异常并不意味着故障,而是表明相应的操作已经被取消,所以在配置服务的案例中,可以通过传播异常来终止应用程序的
    运行。
    
    
    ■ KeeperException 异常 (KeeperException)
    -------------------------------------------------------------------------------------------------------------------------------------
    如果 ZooKeeper server 发出发生错误的信号(signals an error)或者与服务器通信存在问题会抛出一个 KeeperException 异常。针对不同的错误情况,
    有各种 KeeperException 的子类。例如, KeeperException.NoNodeException 是 KeeperException 的一个子类,如果试图在一个不存在的 znode 上执行
    操作就会抛出这个类型的异常。
    
    每个 KeeperException 子类都有关于此类型错误信息相应的代码。例如,对于 KeeperException.NoNodeException, 相应的代码为:
    KeeperException.Code.NONODE (一个 enum 值)。
    
    有两种处理 KeeperException 异常的方法:一种是捕获 KeeperException 异常然后通过检测它的代码来决定采取什么补救操作;或者捕获 KeeperException
    子类然后在每个 catch 块中执行相应的操作。
    
    KeeperExceptions 异常归为三大类(fall into three broad categories)。


        
        ● 状态异常 (State exceptions)
        ---------------------------------------------------------------------------------------------------------------------------------
        因为操作不能应用到 znode 树而导致失败时引发状态异常。状态异常经常发生在同一时刻有另一个进程在修改同一个 znode 时。例如,使用一个
        版本号        执行 setData 操作,如果这个 znode 被另一个进程先更新了,就会抛出 KeeperException.BadVersionException 异常而失败,因为
        版本号不匹配了。程序员通常都知道这种冲突总是可能的,并编写代码来处理此类异常。
        
        有些状态异常指明程序中的一个错误,例如 KeeperException.NoChildrenForEphemeralsException, 当试图为一个短暂 znode (an ephemeral
        znode)创建子节点时抛出。
        
        
        
        ● 可恢复异常 (Recoverable exceptions)
        ---------------------------------------------------------------------------------------------------------------------------------
        可恢复异常是指那些应用程序能够在同一个 ZooKeeper 会话中恢复(can recover within the same ZooKeeper session)的异常。一个可恢复异常
        通过 KeeperException.ConnectionLossException 表示,意味着到 ZooKeeper 连接丢失了。ZooKeeper 会尝试重新连接,并且在多数情况下重连
        会成功并且保证会话完好。
        
        但是,ZooKeeper 不能辨明 由于 KeeperException.ConnectionLossException 异常抛出而失败的操作是否执行。这是一个部分失败(partial
        failure) 的例子。因此程序员有责任来处理这种不确定性,根据应用的情况采取适当的操作。
        
        在这一点上,需要对幂等(idempotent)和非幂等(nonidempotent)操作进行区分。幂等操作是指那些一次或多次执行都会产生相同结果的操作,例如,
        读请求或无条件执行的 setData 操作。对于幂等操作,只需要简单地进行重试即可。
        
        对于非幂等操作,就不能盲目地进行重试,因为多次执行的效果与一次执行的效果是不同的。程序可以通过在 znode 的路径名或其数据中编码信息
        来检测是否非幂等操作的更新已经完成。
        
        
        ● 不可恢复异常 (Unrecoverable exceptions)
        ---------------------------------------------------------------------------------------------------------------------------------
        在某些情况下, ZooKeeper 会话会失效 —— 或许因为超时或者因为会话关闭了(两种情况都会抛出 KeeperException.SessionExpiredException
        异常) 或许因为身份验证失败(抛出 (KeeperException.AuthFailedException)。不管是哪种情形,所有与该会话相关联的暂态节点都会丢失,因此
        应用程序在重新连接 ZooKeeper 之前需要重建它的状态。
        


    ■ 一个可靠的配置服务 (A reliable configuration service)
    -------------------------------------------------------------------------------------------------------------------------------------
    回到 ActiveKeyValueStore 的 write() method, 回顾它是有一个 exists 操作后跟随一个 create 或 setData 组成:

    public void write(String path, String value) throws InterruptedException,
    KeeperException {
        Stat stat = zk.exists(path, false);
        if (stat == null) {
            zk.create(path, value.getBytes(CHARSET), Ids.OPEN_ACL_UNSAFE,
            CreateMode.PERSISTENT);
        } else {
            zk.setData(path, value.getBytes(CHARSET), -1);
        }
    }

    作为一个整体,write() 方法是一个幂等操作,所以可以对它进行无条件重做。下面是一个修改版本的 write() method , 在一个循环中重复操作。其中
    设置了重复操作的最大次数 MAX_RETRIES 和每次重试之间休眠的时间值 RETRY_PERIOD_SECONDS

    public void write(String path, String value) throws InterruptedException,
    KeeperException {
        int retries = 0;
        while (true) {
            try {
                Stat stat = zk.exists(path, false);
                if (stat == null) {
                zk.create(path, value.getBytes(CHARSET), Ids.OPEN_ACL_UNSAFE,
                CreateMode.PERSISTENT);
            } else {
                zk.setData(path, value.getBytes(CHARSET), stat.getVersion());
            }
            return;
            
            } catch (KeeperException.SessionExpiredException e) {
                throw e;
            } catch (KeeperException e) {
                if (retries++ == MAX_RETRIES) {
                throw e;
            }
                // sleep then retry
                TimeUnit.SECONDS.sleep(RETRY_PERIOD_SECONDS);
            }
        }
    }

    代码很小心地在 KeeperException.SessionExpiredException 异常时没有重试,因为当一个会话过期时,ZooKeeper 对象进入 CLOSED 状态,这种情况它
    永远不会重新连接。我们简单地再次抛出异常,让调用者创建一个新的 ZooKeeper 实例,这样整个 write() method 可以重试。一个简单的创建新实例的
    方法是创建一个新的 ConfigUpdater (实际上我们已经重新命名为 ResilientConfigUpdater) 来恢复一个过期的会话。
    
    public static void main(String[] args) throws Exception {
        while (true) {
            try {
                ResilientConfigUpdater configUpdater =    new ResilientConfigUpdater(args[0]);
                configUpdater.run();
            } catch (KeeperException.SessionExpiredException e) {
                // start a new session
            } catch (KeeperException e) {
                // already retried, so exit
                e.printStackTrace();
                break;
            }
        }
    }
    
    处理会话过期的另一个方法是检测 watcher (本例中应该是 ConnectionWatcher)中值为 Expired 的 KeeperState, 然后在检测到时创建一个新的连接。
    这种方法,即便得到 KeeperException.SessionExpiredException 异常,由于连接最终会重建,我们仅仅保持重试 write() 方法。不管使用哪种机制
    从过期的会话恢复,重点是需要对这种不同于连接丢失的故障类型进行不同的处理。

        
        ● NOTE
        ---------------------------------------------------------------------------------------------------------------------------------
        实际上还有另外一种故障模式我们这里忽略了。在 ZooKeeper 对象创建时,它尝试连接到一个 ZooKeeper server. 如果连接失败或超时了,然后
        它会尝试集合体中其他的服务器(in the ensemble)。如果,尝试完集合体内所有的服务器之后,不能建立连接,那么它会抛出 IOException. 所有
        ZooKeeper 服务器都不可用的可能性是很低的,然而,有些应用会选择在一个循环中重试操作直到 ZooKeeper 服务可用为止。

    
    这仅仅是重试处理的一个策略。还有很多其他策略,例如,使用指数补偿,每次将重试的间隔乘以一个常数。


4.3 一个锁服务 (A Lock Service)
-----------------------------------------------------------------------------------------------------------------------------------------    
分布式锁是一个在一组进程间提供互斥的机制,在任何时间,只有一个进程可以持有锁。分布式锁可用于大型分布式系统上进行领导者选举(leader election)
leader 就是在任何时间点上持有锁的进程。


    NOTE
    -------------------------------------------------------------------------------------------------------------------------------------
    不要将 ZooKeeper 自己的领导者选举和使用 ZooKeeper 原生操作实现的一般的领导者选举服务搞混淆(事实上,ZooKeeper 中包含有一个领导者选举
    服务的实现)。ZooKeeper 自己的领导者选举机制是不对外公开的,我们这里所描述的一般领导者选举服务则不同,它是为那些需要所有进程与主进程保持
    一致的分布式系统设计的。

为了使用 ZooKeeper 实现分布式锁,我们使用顺序 znode (sequential znodes) 来为那些竞争锁进程强制一个次序(impose an order)。思路很简单,首先,
指定一个锁 znode, 通常描述为被锁定的实体(比方说 /leader),然后,要获得锁的客户端创建顺序的暂态 znode (sequential ephemeral znodes) 作为锁
znode 的子节点。具有较低顺序号的客户端持有该锁。例如,如果有两个客户端大约同时在 /leader 下创建 znode:/leader/lock-1 和 /leader/lock-2 ,
则创建 /leader/lock-1 的客户端持有此锁,因为它的 znode 具有较低的顺序号。ZooKeeper 服务是次序的仲裁者因为它分配顺序号。

可以通过删除 /leader/lock-1 znode 简单地被释放锁;另一方面,如果客户端进程死掉了,其对应的暂态 znode 会被删除,锁也会释放。然后创建 /leader/lock-2
的客户端会持有该锁,因为它有下一个较低的顺序号。通过创建一个 znode 删除时的 watch, 可以使客户端在获得锁时得到通知。

如下是获取锁的伪代码:

    ① 在锁 znode 下创建一个暂态的顺序 znode, 名称为 lock- , 记下它的实际路径名(pathname, 由 create 操作返回的值)
    ② 得到锁 znode 的子节点并设置一个 watch
    ③ 如果在第一步创建的 znode 的 pathname 具有比在第二步返回的子节点更低的号码,则已经得到了锁,退出。
    ④ 等待在第二步中设置的 watch 的通知,然后返回第二步。



    ■ 羊群效应 (The herd effect)
    -------------------------------------------------------------------------------------------------------------------------------------
    虽然这个算法是正确的,但还是存在一些问题。第一个问题是这个实现受到羊群效应(herd effect)的影响。考虑有成百上千的客户端,所有客户端都
    尝试获得该锁。每个客户端都在锁 znode 上设置了一个 watch 来捕捉子节点的变化。每次锁被释放或另一个进程开始申请锁的时候,watch 会被触发
    并且每个客户端都会收到一个通知。羊群效应是指这种大量客户端收到同一事件的通知,但实际上只有很少一部分需要处理这一事件。这种情况下,只有
    一个客户端会成功获得锁,但维护的过程以及向所有客户端发送 watch 事件会产生峰值流量,这会对 ZooKeeper 服务器造成压力。
    
    为了避免羊群效应,需要对发送通知的条件进行优化。关键在于仅当前一个顺序号的子节点消失时才需要通知下一个客户端,而不是删除(或创建)任何
    子节点时都进行通知。在我们的例子中,如果客户端创建了 znode: /leader/lock-1, /leader/lock-2, and /leader/lock-3, 那么只有当 /leader/lock-2
    消失时才需要通知  /leader/lock-3 对应的客户端;/leader/lock-1 消失或有新节点 /leader/lock-4 加入时不需要通知该客户端。


    ■ 可恢复异常 (Recoverable exceptions)
    -------------------------------------------------------------------------------------------------------------------------------------
    这个申请锁的算法目前还存在另一问题,它不能处理因连接丢失而导致的 create 操作失败。如前所述,这种情况下我们不知道操作是成功还是失败。
    创建一个顺序 znode 是一个非幂等操作,因此不能简单地重试操作,因为如果第一次创建已经成功,重试操作会多出一个永远删不掉的孤儿 znode(an
    orphaned znode), 至少要等到这个客户端会话结束。不幸的结果是还会出现死锁。
    
    问题是在重新连接之后,客户端不能判别它是否已经创建了子节点。通过在 znode 名称中嵌入一个标识符,如果遇到了连接丢失,它可以检查锁节点下
    是否有名称中含有其标识符的子节点存在。如果一个子节点名称含有它的标识符,它知道它的 create 操作成功执行了,并且它不需要创建另一个子节点
    如果没有子节点在其名称中含有该标识符,客户端可以安全地创建一个新的顺序子节点 znode 。
    
    客户端的会话标识符(identifier) 是一个长整型(long integer)值,在 ZooKeeper 服务中是唯一的,因此是连接丢失事件后用于重新识别客户端的理想
    工具。会话标识符可通过调用 ZooKeeper Java 类的 getSessionId() method 获得。

    暂态的顺序 znode (ephemeral sequential znode) 应该采用 lock-<sessionId>-  这样的命名方式创建,ZooKeeper 在其尾部追加顺序号之后,znode
    的名称会变成 lock-<sessionId>-<sequenceNumber> 的形式。顺序号对于父节点是唯一的,因此采用这样的命名方式可以让子节点在保持创建顺序的同时
    确定自己的创建者。



    ■ 不可恢复异常 (Unrecoverable exceptions)
    -------------------------------------------------------------------------------------------------------------------------------------
    如果一个客户端的 ZooKeeper 会话过期,由客户端创建的暂态 znode 会被删除,效果上会释放持有的锁。使用锁的应用应该认识到它已经不再持有该锁,
    应当清理它的状态,然后通过创建一个新的锁对象并尝试获取它来再次启动。注意,控制这个过程的是应用程序,而不是锁实现,因为锁不能预知应用程
    序需要如何清理它的状态。



    ■ 实现 (Implementation)
    -------------------------------------------------------------------------------------------------------------------------------------
    很难对所有的故障模型都进行正确地解释处理,因此正确地实现一个分布式锁是件棘手的事。ZooKeeper 带有一个 Java 语言编写的生产级别的锁实现,
    名称为 WriteLock , 客户端可以很容易使用。(a production-quality lock implementation in Java)



4.4 更多的分布式数据结构和协议 (More Distributed Data Structures and Protocols)
-----------------------------------------------------------------------------------------------------------------------------------------
有很多的分布式数据结构和协议可以使用 ZooKeeper 构建,例如 barriers, queues, and two-phase commit. 一个有趣的事情是它们都是同步协议,但我们
可以使用异步 ZooKeeper 的原生操作(如通知)来实现它们。

ZooKeeper web 站点提供了一些用于实现分布式数据结构和协议的伪代码。ZooKeeper 本身也带有一些标准方法的实现,包括 locks, leader election, 以及
queues , 它们可以在 recipes 目录下找到。

Apache Curator project(http://curator.apache.org/) 也提供了 ZooKeeper 方法的扩展集,以及一个简单的 ZooKeeper 客户端。


    ■ BookKeeper and Hedwig
    -------------------------------------------------------------------------------------------------------------------------------------
    BookKeeper 是一个高可用性和稳定性的日志服务。它用于提供预写式日志(write-ahead logging),这是一项在存储系统中用于保证数据完整性的常用技术。
    在一个使用预写式日志的系统中,每个写操作在被应用前都先要写入事务日志。使用这个技术,我们不必在每个写操作之后都将数据写到永久性存储器上,
    因为即使出现系统故障,也可以通过重新执行事务日志中尚未应用的写操作来恢复系统的最后状态。
    
    BookKeeper 客户端所创建的日志称为 ledgers, 每一个追加到 ledger 的记录被称为 ledger entry, 每个 leader entry 就是一个简单的字节数组。
    ledger 由保存有 ledger 数据复本的 bookie 服务器组进行管理。注意, ledger 数据不存储在 ZooKeeper 中,只有元数据保存在 Zookeepr 中。
    
    传统上,为了让预写式日志的系统更加稳定,必须解决保存有事务日志的节点故障问题,这通常是通过某种方式复制事务日志来解决这个问题。例如,
    Hadoop HDFS 中的 namenode 会将它的编辑日志写到多个磁盘上,每个磁盘是一个典型的 NFS 挂载盘。尽管如此,当主节点放生故障时,还是需要手动完成
    故障恢复。通过提供具有高可用性的日志服务,BookKeeper 承诺提供透明的故障恢复,因为它可以容忍 Bookie 服务器的故障。

    Hedwig 是利用 BookKeeper 实现的一个基于主题的发布-订阅系统。以 ZooKeeper 作为基础,Hedwig 也提供了一个具有高可用性的服务,即使在订阅者
    长时间离线的情况下它也能保证消息的传递。

    BookKeeper 是 ZooKeeper 的一个子项目。





5. 生产环境中的 ZooKeeper (ZooKeeper in Production)
-----------------------------------------------------------------------------------------------------------------------------------------
在生产环境中,应当以复制模式运行 ZooKeeper。这里,将讨论使用 ZooKeeper 服务器集合体时需要考虑的问题。建议参考 《ZooKeeper Administrator’s
Guide》获得更详细的最新操作指南。

    http://zookeeper.apache.org/doc/r3.4.9/zookeeperAdmin.html
    

5.1 可恢复性和性能 (Resilience and Performance)
-----------------------------------------------------------------------------------------------------------------------------------------
ZooKeeper 的机器应放置于机器和网络故障影响最小的位置。实践中,这意味着服务器应该跨机架,电源和交换机来安放,这样,这些设备中的任何一个出现
故障都不会使集合体损失半数以上的服务器。

对于那些需要低延迟服务(毫秒级别)的应用来说,最好将所有服务器都放在同一个数据中心的同一个集合体中。也有一些应用不需要低延迟的服务,它们可以
通过跨数据中心(每个数据中心至少两台服务器)放置服务器来获得更好的可恢复性,领导者选举和分布式粗粒度锁是这类应用的代表。这两个应用的状态变化
都相对较少,因此相对于整个服务来说,数据中心之间传递状态变化消息所需的几十毫米的开销是可以承受的。


    ■ NOTE
    -------------------------------------------------------------------------------------------------------------------------------------
    ZooKeeper 中有一个观察者节点(observer node)的概念,类似一个没有投票权的跟随者(a non-voting follower)。因为它们不参与写请求过程中达成
    共识的投票,因此使用观察者节点可以让 ZooKeeper 集群在不影响写性能的情况系提高读操作性能。使用观察者节点可以让 Zookeeper 集群跨越多个
    数据中心,同时不会增加正常投票节点的延迟。可以通过将投票节点放到一个数据中心,将观察节点放置到另一数据中心来实现这一点。

Zookeeper 是高可用性系统,对它来说,最关键的是能够及时地履行其职能。因此,Zookeeper 应当运行在专用的机器上,如果有其他应用竞争资源,可能会
导致 Zookeeper 性能下降。
    
通过对 Zookeeper 进行配置,可以使他的事务日志和数据快照分别保存到不同的磁盘驱动器上。默认情况下,两者都保存在 dataDir 属性指定的目录中,但
通过为 dataLogDir 属性设置一个值,便可以将事务日志写在指定的位置。通过指定一个专用的设备(不只是一个分区),Zookeeper 服务器就可以最大速率将
日志记录写到磁盘,因为写日志是顺序写,并且没有寻址操作。由于所有的写操作都是通过领导者来完成的,增加服务器并不能提高写操作的吞吐量,所以提
高性能的关键是写操作的速度。
    
如果写操作被交换到磁盘上,则性能会收到不利的影响。这是可以避免的,将 Java 堆内存的大小设置为小于机器上空闲的物理内存即可。Zookeeper 脚本可
以从它的配置目录中获取一个名为 java.env 的文件,这个文件被用来设置 JVMFLAGS 环境变量,包括 Java 堆内存的大小(以及任何其他所需的JVM 参数).



5.2 配置 (Configuration)
-----------------------------------------------------------------------------------------------------------------------------------------
ZooKeeper 集合体中每个服务器有一个数字标识 ID, 服务器 ID 在集合体中是唯一的,并且取值范围在 1 到 255 之间。可以通过一个名为 myid 的纯文本
文件设定服务器的 ID, 这个文件保存在 dataDir 参数所指定的目录中。

为每个服务器设置 ID 只是完成了工作的一半。还需要将集合体中每个服务器的 ID 和网络位置告诉所有的服务器。ZooKeeper 的配置文件必须为每个服务器
包含一行,如下形式:

    server.n=hostname:port:port

n 的值替换为服务器号码。有两个端口设置:第一个是跟随者用于连接领导者的端口,第二个端口被用于领导者选举。这里是一个包含三台机器的复制模式
ZooKeeper 集合体的配置例子:


    tickTime=2000
    dataDir=/disk1/zookeeper
    dataLogDir=/disk2/zookeeper
    clientPort=2181
    initLimit=5
    syncLimit=2
    server.1=zookeeper1:2888:3888
    server.2=zookeeper2:2888:3888
    server.3=zookeeper3:2888:3888


服务器在 3 个端口上监听:2181 端口用于客户端连接;对于领导者来说, 2888 端口用于跟随者连接; 3888 端口用于领导者选举阶段其他服务的连接。
当一个 ZooKeeper 服务器启动时,它读取 myid 文件用于确定自己的服务器 ID, 然后通过读取配置文件来确定应当在哪个端口上进行监听,同时确定集合体中
其他服务器的网络地址。

连接到这个 ZooKeeper 集合体的客户端在 ZooKeeper 对象的构造器中应该使用 zookeeper1:2181,zookeeper2:2181,zookeeper3:2181 作为主机字符串。

在复制模式下,有两个额外的强制属性:initLimit 和 syncLimit, 两者都以 tickTime 的倍数作为度量。

initLimit 属性设定了所有跟随者与领导者进行连接并同步的时间。如果多数的跟随者在这个期间内同步失败,这个领导者放弃它的领导状态,然后进行另外
一次领导选举。如果这种情况经常发生(可以通过日志中的记录发现这种情况),则表明设定的值太小。

syncLimit 是运行跟随者与领导者同步的时间。如果一个跟随者在此期间内同步失败,它会重启自己。连接到这个跟随者的客户端会连接到另一个跟随者服务器。

这些是一个 ZooKeeper 服务器集群建立和运行所需的最少设置。《 ZooKeeper Administrator’s Guide 》中列出更多的配置选项,特别是性能调优方面的。
        

        http://zookeeper.apache.org/doc/r3.4.9/zookeeperAdmin.html


参考 Hadoop 之 ZooKeeper (一)

(本篇完)

猜你喜欢

转载自blog.csdn.net/devalone/article/details/80906993