Do You PHP はてブロ

Do You PHPはてなからはてブロに移動しました

PHP+Thrift+HBaseを試してみた

使ってる人にとっては何周目かの今さら感漂いますが、ひょんなことから調べる必要が出てきたのでざっくりまとめてみました。

環境

  • CentOS6.3
  • PHP5.5.3
  • JDK1.6.0-45
  • HBase0.94.11
  • Thrift0.9.1

HBaseとは

HBaseはKVS(Key-Value Store)の1つで、ASF(Apache Software Foundation)のHadoopプロジェクトの一環として作られたオープンソースソフトウェアです。本家はApache HBase – Apache HBase™ Homeです。

とりあえず、以下の記事をざっと読むと良いかと。

HBaseの論理データモデルは「多次元ソートマップ」で、テーブルやカラムといった概念がありますので、RDBMSを触ったことがある人は馴染みやすいかも。また、カラムをグルーピングした"カラムファミリー"という概念があります。カラムファミリーはテーブルに1つ以上存在し、カラムはいずれかのカラムファミリーに属します。

HBaseのインストール

"インストール"と言っても、ダウンロードして展開するだけです。

$ wget http://ftp.riken.jp/net/apache/hbase/stable/hbase-0.94.11.tar.gz
$ tar zxf hbase-0.94.11.tar.gz
$ cd hbase-0.94.11/
$ 

HBaseを起動・停止してみる

起動と停止は、直下のbinディレクトリにあるstart-hbase.shとstop-hbase.shを使います。環境変数JAVA_HOMEの設定を忘れずに。

$ export JAVA_HOME=/path/to/java_home
$ bin/start-hbase.sh
starting master, logging to /path/to/hbase-0.94.11/bin/../logs/hbase-hoge-master-mobylog64.out
$ 
$ bin/stop-hbase.sh
stopping hbase..................
$

HBase shell

HBaseにはJRuby製のshellが用意されていて、テーブルの作成やデータの登録などが可能です。helpを表示させると、大体どんなことができそうか分かると思います。

$ bin/hbase shell
HBase Shell; enter 'help<RETURN>' for list of supported commands.
Type "exit<RETURN>" to leave the HBase Shell
Version 0.94.11, r1513697, Wed Aug 14 04:54:46 UTC 2013

hbase(main):001:0> help
HBase Shell, version 0.94.11, r1513697, Wed Aug 14 04:54:46 UTC 2013
Type 'help "COMMAND"', (e.g. 'help "get"' -- the quotes are necessary) for help on a specific command.
Commands are grouped. Type 'help "COMMAND_GROUP"', (e.g. 'help "general"') for help on a command group.

COMMAND GROUPS:
  Group name: general
  Commands: status, version, whoami

  Group name: ddl
  Commands: alter, alter_async, alter_status, create, describe, disable, disable_all, drop, drop_all, enable, enable_all, exists, is_disabled, is_enabled, list, show_filters

  Group name: dml
  Commands: count, delete, deleteall, get, get_counter, incr, put, scan, truncate

  Group name: tools
  Commands: assign, balance_switch, balancer, close_region, compact, flush, hlog_roll, major_compact, move, split, unassign, zk_dump

  Group name: replication
  Commands: add_peer, disable_peer, enable_peer, list_peers, list_replicated_tables, remove_peer, start_replication, stop_replication

  Group name: snapshot
  Commands: clone_snapshot, delete_snapshot, list_snapshots, restore_snapshot, snapshot

  Group name: security
  Commands: grant, revoke, user_permission

SHELL USAGE:
Quote all names in HBase Shell such as table and column names.  Commas delimit
command parameters.  Type <RETURN> after entering a command to run it.
Dictionaries of configuration used in the creation and alteration of tables are
Ruby Hashes. They look like this:

  {'key1' => 'value1', 'key2' => 'value2', ...}

and are opened and closed with curley-braces.  Key/values are delimited by the
'=>' character combination.  Usually keys are predefined constants such as
NAME, VERSIONS, COMPRESSION, etc.  Constants do not need to be quoted.  Type
'Object.constants' to see a (messy) list of all constants in the environment.

If you are using binary keys or values and need to enter them in the shell, use
double-quote'd hexadecimal representation. For example:

  hbase> get 't1', "key\x03\x3f\xcd"
  hbase> get 't1', "key\003\023\011"
  hbase> put 't1', "test\xef\xff", 'f1:', "\x01\x33\x40"

The HBase shell is the (J)Ruby IRB with the above HBase-specific commands added.
For more on the HBase Shell, see http://hbase.apache.org/docs/current/book.html
hbase(main):002:0> exit
$ 

ここで、接続テストに使うためのテーブル"tbl"を作ってデータを突っ込んでおきます。定義は唯一のカラムファミリー"family"にカラム"column1"〜"column3"を所属させる感じ。
以下のコマンドで、putはSQLで言うところのINSERT文で、カラム単位にデータを登録します。scanはSELECT文に相当します。

$ bin/hbase shell
HBase Shell; enter 'help<RETURN>' for list of supported commands.
Type "exit<RETURN>" to leave the HBase Shell
Version 0.94.11, r1513697, Wed Aug 14 04:54:46 UTC 2013

hbase(main):001:0> create "tbl", "family", {NAME => "column1"}, {NAME=>"column2"}, {NAME=>"column3"}
0 row(s) in 1.2370 seconds

hbase(main):002:0> put 'tbl', 'test1', 'family:column1', 'value1'
0 row(s) in 3.1850 seconds

hbase(main):003:0> put 'tbl', 'test1', 'family:column2', 'value2'
0 row(s) in 0.0200 seconds

hbase(main):004:0> put 'tbl', 'test1', 'family:column3', 'value3'
0 row(s) in 0.0370 seconds

hbase(main):011:0> put 'tbl', 'test2', 'family:column1', 'value1'
0 row(s) in 0.0500 seconds

hbase(main):012:0> put 'tbl', 'test3', 'family:column1', 'value1'
0 row(s) in 0.0100 seconds

hbase(main):005:0> scan 'tbl'
ROW                                       COLUMN+CELL
 test1                                    column=family:column1, timestamp=1377683232461, value=value1
 test1                                    column=family:column2, timestamp=1377683293228, value=value2
 test1                                    column=family:column3, timestamp=1377683298505, value=value3
 test2                                    column=family:column1, timestamp=1377683807442, value=value1
 test3                                    column=family:column1, timestamp=1377683810364, value=value1
3 row(s) in 0.0930 seconds

hbase(main):006:0> exit
$ 

RDBMSとは異なり、カラムごとに1行ずつ出力されます。

Thriftとは

ThriftはFacebookにて開発されたRPCフレームワークで、本家はApache Thrift - Homeです。また、PHPとHBaseを仲介するProxyサーバーとして起動することで、PHPとHBase間でデータをやりとりすることができます。
この時、.thriftファイルと呼ばれるファイルをThriftが持っているコード生成エンジンに食わせることで、PHPを含む様々な言語でのソースコード(クライアント、サーバー)を出力することができます。SOAPで言うwsdlファイルと生成されるProxyコードみたいな感じですかね。

Thriftのインストール

Thriftのインストールの前に事前準備。必要となるパッケージ郡をインストールします。

$ sudo yum install -y automake libtool flex bison pkgconfig gcc-c++ boost-devel libevent-devel zlib-devel python-devel ruby-devel
$

Thriftのアーカイブをダウンロードし、configure・make・make installすればOK。。。と言いたいところですが、Thriftにはthrift_protocolというPHP拡張モジュールが含まれていますが、buildに必要なファイルが含まれていないっぽいです。。。このため、1つ前のバージョンのThrift0.9.0から拝借してきます。

$ wget http://ftp.riken.jp/net/apache/thrift/0.9.0/thrift-0.9.0.tar.gz
$ tar zxf thrift-0.9.0.tar.gz
$ wget http://ftp.riken.jp/net/apache/thrift/0.9.1/thrift-0.9.1.tar.gz
$ tar zxf thrift-0.9.1.tar.gz
$ cp -rp thrift-0.9.0/lib/php/src/ext/thrift_protocol thrift-0.9.1/lib/php/src/ext/
$ tar zxf thrift-0.9.1.tar.gz  # 再度展開
$ ./configure
$ make
$ sudo make install
$ 

インストールすると、PHPからの接続に必要となるライブラリもPEARディレクトリにコピーされます。
なお、PHP拡張のインストール先などはRPMPHPのものを基準としているようなので、ソースからインストールした場合でconfigure時に--prefixを指定したりデフォルトのままの場合、そのインストール先ディレクトリを環境変数PATHに含めておく必要があります。また、必要に応じてPHP_CONFIG_PREFIX()を指定してください。さらに、make install時にもPATHが通っている必要があります。

$ ./configure PHP_CONFIG_PREFIX=/usr/local/lib/php/lib/
$ make
$ su
# make install
# exit
$ 

Thriftを起動・停止してみる

Thriftをサーバーとして起動するためには、HBaseのbinディレクトリにあるhbaseコマンド、もしくは、hbase-daemon.shを使います。いずれも環境変数JAVA_HOMEの設定を忘れずに。
まずは、hbaseコマンドの場合の例。

$ export JAVA_HOME=/path/to/java_home
$ cd ../hbase-0.94.11/
$ bin/hbase thrift start

なお、ログが標準エラー出力にそのまま出力されますので、適宜リダイレクトやteeコマンドを使いましょう。停止はCTRL-Cで。
次にhbase-daemon.shコマンドの場合の例。

$ export JAVA_HOME=/path/to/java_home
$ cd ../hbase-0.94.11/
$ bin/hbase-daemon.sh start thrift
starting thrift, logging to /path/to/hbase-0.94.11/bin/../logs/hbase-hoge-thrift-mobylog64.out
$ 
$ bin/hbase-daemon.sh stop thrift
stopping thrift.
$ 

PHP+Thrift+HBase

まずは、HBaseの.thriftファイルからPHP用のコードを作成します。

$ cd ../
$ thrift --gen php hbase-0.94.11/src/main/resources/org/apache/hadoop/hbase/thrift/Hbase.thrift
$ 

直下にgen-phpディレクトリと、その中に

が作成されていることを確認します。ただし、namespaceがイケてないので、ここではコメントアウトしておきます。

$ perl -i -p -s -e "s#^namespace ;#//namespace ;#g" gen-php/*.php
$ 

次に、gen-phpディレクトリにテスト用スクリプト(test.php)を作成します。

<?php
function autoload($className)
{
    $className = ltrim($className, '\\');
    $fileName  = '';
    $namespace = '';
    if($lastNsPos = strrpos($className, '\\')) {
        $namespace = substr($className, 0, $lastNsPos);
        $className = substr($className, $lastNsPos + 1);
        $fileName  = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
    }
    $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';

    require $fileName;
}
spl_autoload_register('autoload');

include 'Hbase.php';
include 'Types.php';

use \Thrift\Transport\TSocket;
use \Thrift\Transport\TBufferedTransport;
use \Thrift\Protocol\TBinaryProtocol;

$server = 'localhost';
$port = 9090;

try {
    $socket = new TSocket($server, $port);
    $socket->setRecvTimeout(5000);
    $transport = new TBufferedTransport($socket);
    $protocol = new TBinaryProtocol($transport);
    $client = new HbaseClient($protocol);

    $transport->open();

    /**
     * 利用可能なテーブルの一覧を取得
     */
    var_dump($client->getTableNames());

    /**
     * SQLで言うと"SELECT * FROM tbl LIMIT 2"に相当
     */
    $scan = new TScan();
    $scan = $client->scannerOpenWithScan('tbl', $scan, null);
    var_dump($client->scannerGetList($scan, 2));

} catch (TException $e) {
    error_log('TException');
    error_log($e);
} catch (Exception $e) {
    error_log('Exception');
    error_log($e);
}

で、実行。データが無いカラムは出力されないことに注意です。

$ export JAVA_HOME=/path/to/java_home
$ cd ../hbase-0.94.11/
$ bin/start-hbase.sh
$ bin/hbase-daemon.sh start thrift
$ php -v
PHP 5.5.3 (cli) (built: Aug 23 2013 19:04:46)
Copyright (c) 1997-2013 The PHP Group
Zend Engine v2.5.0, Copyright (c) 1998-2013 Zend Technologies
$ php test.php
$ php test.php
array(1) {
  [0]=>
  string(3) "tbl"
}
array(2) {
  [0]=>
  object(TRowResult)#7 (3) {
    ["row"]=>
    string(5) "test1"
    ["columns"]=>
    array(3) {
      ["family:column1"]=>
      object(TCell)#9 (2) {
        ["value"]=>
        string(6) "value1"
        ["timestamp"]=>
        int(1377683232461)
      }
      ["family:column2"]=>
      object(TCell)#10 (2) {
        ["value"]=>
        string(6) "value2"
        ["timestamp"]=>
        int(1377683293228)
      }
      ["family:column3"]=>
      object(TCell)#11 (2) {
        ["value"]=>
        string(6) "value3"
        ["timestamp"]=>
        int(1377683298505)
      }
    }
    ["sortedColumns"]=>
    NULL
  }
  [1]=>
  object(TRowResult)#8 (3) {
    ["row"]=>
    string(5) "test2"
    ["columns"]=>
    array(1) {
      ["family:column1"]=>
      object(TCell)#13 (2) {
        ["value"]=>
        string(6) "value1"
        ["timestamp"]=>
        int(1377683807442)
      }
    }
    ["sortedColumns"]=>
    NULL
  }
}
$ 

SQLで言うところのWHERE句

"フィルター"と呼ばれるものがそれに相当します。色々なフィルターが用意されていますので、詳細は以下のURLを読んでみてください。

フィルターはPHP側でも利用可能で、以下はcolumn1の値を正規表現マッチしたカラムを取得する例です。PostgreSQLだと

SELECT column1 FROM tbl WHERE column1 ~ 'e[13]$'

な感じですかね?

<?php$scan = new TScan();
    $scan->columns = array('family:column1');
    $scan->filterString = "ValueFilter(=,'regexstring:e[13]$')";
    $scan = $client->scannerOpenWithScan($tablename, $scan, null);
    :

その他メソッドについて

thriftコマンドで生成されたHbase.phpの先頭で定義されているHbaseIfインターフェースを眺めてもいいんですが、大元のHbase.thriftファイルを見たほうが型定義がハッキリするので、こちらの方がオススメです。

名前空間をサポートしないPHP5.2系はどうすれば?

Thrift0.9.0以降で生成されるPHPコードや同梱されているPHPライブラリでは名前空間を使用しています。このため、PHP5.2系などではエラーになります。1つ前のThrift0.8.0であれば名前空間を使用していないので、PHP5.2系でも動作します。試したところ、HBase+ThriftサーバはThrift0.9系、PHP側のみThrift0.8.0でも問題なく動作しました。

まとめ

またもや「ちゃんと繋げられましたよー」で終わっちゃうわけですが。。。
個人的には(RDBでなはいけど)DB系の新ネタだったので、「SQLで○○○なのはどうやるんだろ?」とかいろいろ試してました。

ちなみに、scannerOpenWithScanメソッドの第4引数にnullを指定していますが、ここって何を指定するんですかね。。。?もし、ご存じの方がいらっしゃいましたら教えてもらえるとありがたいです:-)