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里面运行这个脚本


1

$ php zookeeperdemo1.php

返回刚才的那个终端A里面,改变节点“/test”存储的数据


1

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 );

    }

  

    $this->znode = $this->create( self::CONTAINER . '/w-',

                                  null,

                                  $this->acl,

                                  Zookeeper::<span class="KSFIND_CLASS" id="0KSFindDIV">EPHEMER</span>AL | 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 ) {

          $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( $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";

  }

  

}

  

$worker = new Worker( '127.0.0.1:2181' );

$worker->run();

=====================================================

打开多个终端运行这个脚本。使用Ctrl+c或其他方法退出第一个脚本。刚开始不会有任何变化,worker可以继续工作。后来,ZooKeeper会发现超时,并选举出新的leader。


除此之外,利用这个扩展还可以实现一下其他的应用场景,比如排他锁和共享锁:php-zookeeper-recipes。