ZooKeeper是一个中心化服务,用于分布式应用下的配置同步和协调,提供统一配置服务,统一命名服务,分布式同步,集群管理等。Zookeeper 从设计模式角度来看,是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper 就将负责通知已经在 Zookeeper 上注册的那些观察者做出相应的反应。ZooKeeper的应用场景包括:统一命名服务;配置管理;集群管理;队列管理等。
ZooKeeper作为一个Java应用程序,有大神开发了PHP的扩展:php-zookeeper。利用ZooKeeper,我们可以让分布式的PHP应用程序协调产生leader,为woker分配任务,当leader崩溃时,自动选举产生leader;也可以作分布式的锁和队列。
ZooKeeper本身是一个集群,至少需要表示3台,只要超过半数节点正常就可以工作,避免单点故障。首先需要安装JDK环境
yum search java | grep 'java-' sudo yum install java-1.8.0-openjdk-devel
然后安装ZooKeeper,从官网下载
tar zxfv zookeeper-3.4.6.tar.gz cd zookeeper-3.4.6/src/c ./configure --prefix=/usr/ make sudo make install #创建libzookeeper.conf,内容为/usr/lib,以便编译扩展使用 sudo vim /etc/ld.so.conf.d/libzookeeper.conf #使配置生效 sudo ldconfig
然后安装PHP的扩展
cd git clone https://github.com/andreiz/php-zookeeper.git cd php-zookeeper phpize ./configure make sudo make install
更改php.ini配置,增加以下内容
[zookeeper] extension = zookeeper.so
查看是否加载成功
php -m | grep zookeeper
更改ZooKeeper配置,可以改变里面的DataDir熟悉,默认在/tmp下面
cp conf/zoo_sample.cfg conf/zoo.cfg vim conf/zoo.cfg
然后终端A里面运行ZooKeeper,通过shell进行交互
cd zookeeper-3.4.6/bin ./zkServer.sh start ./zkCli.sh -server 127.0.0.1:2181 create /test hello ;Created /test ls / ;[test, zookeeper]
这时便已成功连到了ZooKeeper,并创建了一个名为“/test”的znode。ZooKeeper以树形结构保存数据。这很类似于文件系统,但“文件夹”又和文件很像。znode是ZooKeeper保存的实体。
新建一个PHP脚本来测试一下
<?php class ZookeeperDemo extends Zookeeper { public function watcher( $i, $type, $key ) { echo "Insider Watcher\n"; // Watcher gets consumed so we need to set a new one $this->get( '/test', array($this, 'watcher' ) ); } } $zoo = new ZookeeperDemo('127.0.0.1:2181'); $zoo->get( '/test', array($zoo, 'watcher' ) ); while( true ) { echo '.'; sleep(2); }
在新的终端B里面运行这个脚本
$ php zookeeperdemo1.php
返回刚才的那个终端A里面,改变节点“/test”存储的数据
set /test world
这时候在终端B里面变化打印“Insider Watcher”。注意:这里注册的回到函数仅支持对象的方法,不支持普通的函数。
前面说过,ZooKeeper是一个基于观察者模式设计的分布式服务管理框架。Zookeeper提供了绑定在znode上的监听器,一旦监听到znode数据发生变化,便会通知所有注册的客户端。所以也可以应用于发布订阅模式。
这篇文章还举例,如何让多个PHP脚本自动选举leader,分配工作。
<?php class Worker extends Zookeeper { const CONTAINER = '/cluster'; 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 ); } //Zookeeper::EPHEMERAL - auto remove if client session goes away //Zookeeper::EPHEMERAL - auto increasing sequence number $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 registred 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 ) { //for node path change event $this->get( self::CONTAINER . '/' . $workers[ $i - 1 ], array( $this, 'watchNode' ) ); //for node path exist event $this->exists( 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( $i, $type, $name ) { $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"; } } //host can be multiple, e.g '127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183' $worker = new Worker( '127.0.0.1:2181' ); $worker->run();
打开多个终端运行这个脚本。使用Ctrl+c或其他方法退出第一个脚本。刚开始不会有任何变化,worker可以继续工作。后来,ZooKeeper会发现超时,并选举出新的leader。
除此之外,利用这个扩展还可以实现一下其他的应用场景,比如排他锁和共享锁:php-zookeeper-recipes。
参考链接:
Distributed application in PHP with Apache Zookeeper
分布式服务框架 Zookeeper — 管理分布式环境中的数据
使用Apache Zookeeper分布式部署PHP应用程序
分布式服务框架:Zookeeper
Pingback引用通告: 日志系统设计 | 勇气