上一篇提到了php操作zookeeper的一些常用curd操作和需要注意的问题
本篇主要说下具体zookeeper的一种应用场景:集群管理(图片来自网络)
在此我们主要关注两点
监听机器的退出加入
上图中所有机器约定在父节点GroupMembers下创建临时节点,然后监听父节点的子节点。一旦有机器挂掉,该机器与zookeeper的连接断开,其所创建的临时节点被删除,所有其他机器都收到通知某个兄弟节点被删除。新机器加入也是类似,所有机器收到通知,有新增临时节点。
在此特别备注下监听父节点下的子节点只能监听到子节点的新增和删除的事件,子节点本身内容变更父节点并不能捕获,如果需要捕获子节点内容变化的事件,需要对子节点做单独监听
选举主节点
通常集群我们需要一个主节点来对外响应调度分发任务给从节点执行,所以在集群中选举主节点也是异常重要的。选举主节点也有两种主要方式
1 排它式,我们让所有集群所有机器尝试对一个未创建的节点创建临时节点,如果创建成功了则标记该机器为主节点,其他机器继续监听该节点,一旦主节点机器和zookeeper连接断开,其他机器监听到删除事件继续对该节点尝试创建来获得主节点标识,不过在这里当机器数量比较多的时候主节点机器断连会导致其他机器都接收该删除事件会导致惊群,会对服务端造成比较大的性能影响,所以我们推荐第二种方式来解决该问题。
2 共享式,如上图中我们让所有机器在父节点GroupMembers下创建临时顺序节点,机器创建节点拉取GroupMembers所有子节点判断自己编号是不是最小,如果最小则标记为主节点,如果不是最小则监听前一个节点。如果主节点和zookeeper断连则后一个顺序节点(即兄弟节点)可以监听到主节点的删除事件则此时顺利升级为新的主节点。此方式的好处的是每台机器只需要监听前一个兄弟节点,有效避免了惊群问题。下面附上此方案的示例代码,代码修改自网络测试可用。
<?php class Worker extends Zookeeper{ const CONTAINER = '/chenjie.info'; protected $acl = array( array( 'perms' => Zookeeper::PERM_ALL, 'scheme' => 'world', 'id' => 'anyone' ) ); private $isLeader = false; private $znode; public function __construct($host = '', $watcher_cb = null, $recv_timeout = 10000){ parent::__construct($host, $watcher_cb, $recv_timeout); } public function register(){ if (! $this->exists(self::CONTAINER)) { $this->create(self::CONTAINER, null, $this->acl); } $this->znode = $this->create(self::CONTAINER . '/w-', null, $this->acl, Zookeeper::EPHEMERAL | Zookeeper::SEQUENCE); $this->znode = str_replace(self::CONTAINER . '/', '', $this->znode); printf("I'm registerd as %s\n", $this->znode); $watching = $this->watchPrevious(); if ($watching == $this->znode) { printf("Nobody here , I'm the leader\n"); $this->setleader(true); } else { printf("I'm watching %s\n", $watching); } } public function watchPrevious(){ $workers = $this->getChildren(self::CONTAINER); sort($workers); $size = sizeof($workers); for ($i = 0; $i < $size; $i ++) { if ($this->znode == $workers[$i]) { if ($i > 0) { $res = $this->get(self::CONTAINER . '/' . $workers[$i - 1], array( $this, 'watchNode' )); return $workers[$i - 1]; } return $workers[$i]; } } throw new Exception(sprintf("Something went very wrong! I can't find myself: %s/%s", self::CONTAINER, $this->znode)); } public function watchNode(){ $watching = $this->watchPrevious(); if ($watching == $this->znode) { printf("I'm the new leader\n"); $this->setLeader(true); } else { printf("Now I'm watching %s\n", $watching); } } public function isLeader(){ return $this->isLeader; } public function setLeader($flag){ $this->isLeader = $flag; } public function run(){ $this->register(); while (true) { if ($this->isLeader()) { $this->doLeaderJob(); } else { $this->doWorkerJob(); } sleep(2); } } public function doLeaderJob(){ echo "Leading\n"; } public function doWorkerJob(){ echo "Working\n"; } } $worker = new Worker('localhost:2181'); $worker->run();
执行效果:
为了测试方便对比,我们启动两个客户端,第一个客户端因为刚启动只有一个它自己所以编号最小成为主节点,然后我们断开第一个客户端,观察第二个客户端监听到第一个客户端(前一个节点)断连 然后接任为新的主节点。