FANCOMI Ad-Tech Blog

株式会社ファンコミュニケーションズ nend・新規事業のエンジニア・技術ブログ

Cassandraクラスタを作成してみました

こんにちは、t_endoです。
今回はCassandraクラスタをAmazon EC2上に作成したいと思います。

以前何か良さそうなKVSがないものか探していたところ、
「Cassandraは、非常に高いスケーラビリティーを持ち、イベンチュアルコンシステントな分散システム構造のKVS(Key Value Store)です。 」
Cassandra Wikiより抜粋
との事で
・PHPのクライアントがあること
・key:valueを1対多で取得できること
・クラスタ構成が組めること
こんな感じの条件で探していたので選んでみました。

各ソフトウェアのバージョンは
Amazon Linux : 2014.03
Cassandra : 2.0.6
phpcassa : 1.1.0
PHP : 5.3.0
です。

Cassandraインストール

インストールと言っても、公式ページからダウンロードしてきて、解凍してあげるだけでインストール完了です。
Cassandra公式ページ

Cassandra設定

解凍したディレクトリに
「conf/cassandra.yaml」
という設定ファイルがあるので、編集していきます。

変更前
cluster_name: 'Test Cluster'
↓
変更後
cluster_name: 'Blog Cluster'

所属するクラスタ名を指定します

変更前
seed_provider:
    # Addresses of hosts that are deemed contact points.
    # Cassandra nodes use this list of hosts to find each other and learn
    # the topology of the ring.  You must change this if you are running
    # multiple nodes!
    - class_name: org.apache.cassandra.locator.SimpleSeedProvider
      parameters:
          # seeds is actually a comma-delimited list of addresses.
          # Ex: "<ip1>,<ip2>,<ip3>"
          - seeds: "127.0.0.1"
↓
変更後(seedノードの場合)
seed_provider:
    # Addresses of hosts that are deemed contact points.
    # Cassandra nodes use this list of hosts to find each other and learn
    # the topology of the ring.  You must change this if you are running
    # multiple nodes!
    - class_name: org.apache.cassandra.locator.SimpleSeedProvider
      parameters:
          # seeds is actually a comma-delimited list of addresses.
          # Ex: "<ip1>,<ip2>,<ip3>"
          - seeds: "10.0.250.151"

seedノードのIPアドレスを指定します。
seedノードとして設定する場合は自分のIPアドレスを指定します。

※seedノードとは
各ノードはseedノードから他のノードの状態を取得したりします。
Cassandraクラスタ内のハブみたいなものになります。
Cassandra Wiki参照

変更前
listen_address: localhost
↓
変更後
listen_address: 10.0.250.151

自分のローカルIPアドレスを指定します。

変更前
endpoint_snitch: SimpleSnitch
↓
変更後
endpoint_snitch: Ec2Snitch

Cassandraは各ノードがどこのデータセンター、ラックにあるかというのを設定するのですがその設定の仕方を指定します。
今回はEC2上の「ap-northeast-1」リージョンのみに構成するので、「Ec2Snitch」を指定しました。

今回設定した項目は以上になります。
その他詳しい設定等はDATASTAX Documentを参照してください。
DATASTAX Document

Cassandra起動~column familyを作成

・Cassandraの起動は「bin/cassandra」で起動する事ができます。
※「-p ファイル名」を付けるとプロセスIDを出力する事ができます。

sudo /opt/cassandra/bin/cassandra -p /var/run/cassandra.pid

INFO 19:06:33,103 Logging initialized
INFO 19:06:33,232 Loading settings from file:/opt/apache-cassandra-2.0.6/conf/cassandra.yaml
|大量のログが流れていきます
INFO 19:11:11,030 No gossip backlog; proceeding

・Cassandraの停止はプロセスをkillする事で停止できます。

sudo kill `cat /var/run/cassandra.pid`
sudo rm -f /var/run/cassandra.pid

・全てのノードを起動したら「bin/nodetool status」を実行してみましょう。
※コマンドを実行するのは、ノードが起動している所ならどこでも大丈夫です。

/opt/cassandra/bin/nodetool status
Datacenter: ap-northeast
========================
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
--  Address       Load       Tokens  Owns   Host ID                               Rack
UN  10.0.250.151  7.16 GB    256     24.9%  xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx  1a
UN  10.0.251.152  6.5 GB     256     25.2%  xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx  1c
UN  10.0.250.152  6.3 GB     256     23.7%  xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx  1a
UN  10.0.251.151  6.74 GB    256     26.2%  xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx  1c

ちゃんと全てのノードが起動して同じクラスタに所属している事が確認できました。
※データセンターやラックもちゃんと自動で判別してくれています

・無事起動できたので早速Cassandraへ接続して遊んでみます。
接続のコマンドは「bin/cassandra-cli」です。

/opt/cassandra/bin/cassandra-cli
Connected to: "Blog Cluster" on 127.0.0.1/9160
Welcome to Cassandra CLI version 2.0.6

The CLI is deprecated and will be removed in Cassandra 3.0.  Consider migrating to cqlsh.
CQL is fully backwards compatible with Thrift data; see http://www.datastax.com/dev/blog/thrift-to-cql3

Type 'help;' or '?' for help.
Type 'quit;' or 'exit;' to quit.

[default@unknown]

・まずkeyspaceを作成します。
※RDBのdbと同じようなものです

create keyspace blog_keyspace with
placement_strategy='org.apache.cassandra.locator.NetworkTopologyStrategy'
and strategy_options={ap-northeast:2};

blog_keyspace:keyspace名です。好きな名前を入力してください。
placement_strategy:レプリカの保存方法を指定します。(データセンターは1つですが、ラックがわかれているのでNetworkTopologyStrategyを指定しました)
strategy_options:レプリカの保存先と個数を指定します。(ap-northeastのデータセンターへ2個作成するようにしました)

※placement_strategyの保存方法
SimpleStrategy:単一のデータセンターで、ラックを考慮せずに保存します
NetworkTopologyStrategy:複数のデータセンターで、ラックを分けて保存します

・keyspaceが作成できたので、そのkeyspaceを使います

use blog_keyspace;

・column familyを作成します。
※RDBのtableと同じような物です

create column family blog_column_family with comparator='UTF8Type' and key_validation_class='UTF8Type' and default_validation_class='UTF8Type';

blog_column_family:column family名です。好きな名前を入力してください。
以下3つは文字列を入れようとしたらエラーがでてしまったので、UTF8Typeを指定しました。
comparator、key_validation_class、default_validation_class

※デフォルトだと16進数でしか入れられないみたいです。

set blog_column_family['test1']['test2'] = 'test3';
org.apache.cassandra.serializers.MarshalException: cannot parse 'test2' as hex bytes
set blog_column_family['01']['02'] = '03';
Value inserted.
Elapsed time: 2.05 msec(s).

基本コマンド

・キーにデータセット

set COLUMN_FAMILY_NAME['キー名']['カラム名'] = '値';

例:

[default@blog_keyspace] set blog_column_family ['1']['service'] = 'nend';
Value inserted.
Elapsed time: 134 msec(s).
[default@blog_keyspace] set blog_column_family ['2']['service'] = 'nex8';
Value inserted.
Elapsed time: 2.32 msec(s).

・キー名のデータ取得

get COLUMN_FAMILY_NAME['キー名'];

例:

[default@blog_keyspace] get blog_column_family ['2'];
=> (name=service, value=nex8, timestamp=1425023634514000)
Returned 1 results.
Elapsed time: 57 msec(s).

・100件データ表示

list COLUMN_FAMILY_NAME;

例:

[default@blog_keyspace] list blog_column_family ;
Using default limit of 100
Using default cell limit of 100
-------------------
RowKey: 2
=> (name=service, value=nex8, timestamp=1425023775107000)
-------------------
RowKey: 1
=> (name=service, value=nend, timestamp=1425023768572000)

2 Rows Returned.
Elapsed time: 1686 msec(s).

・キーのvalue数取得

count COLUMN_FAMILY_NAME ['キー名'];

例:

[default@blog_keyspace] count blog_column_family ['1'];
1 cells

・データ削除

del COLUMN_FAMILY_NAME ['キー名'];

例:

[default@blog_keyspace] del blog_column_family ['1'];
row removed.
Elapsed time: 1.4 msec(s).
[default@blog_keyspace] list blog_column_family ;
Using default limit of 100
Using default cell limit of 100
-------------------
RowKey: 2
=> (name=service, value=nex8, timestamp=1425023775107000)
-------------------
RowKey: 1

2 Rows Returned.
Elapsed time: 1547 msec(s).

※キーは残ります

・全データ削除

truncate COLUMN_FAMILY_NAME;

例:

[default@blog_keyspace] truncate blog_column_family ;
blog_column_family truncated.
[default@blog_keyspace] list blog_column_family ;
Using default limit of 100
Using default cell limit of 100

0 Row Returned.
Elapsed time: 1683 msec(s).

※コマンドの最後は必ず「;」を付けます。
付けないと

[default@blog_keyspace] set blog_column_family ['test']['test'] = 1
...

こんな感じで改行しただけになります。
もちろんこの後「;」を打ってあげれば進むのですが、
初めてさわった時、処理中かと思って10分ぐらい待っていました…

PHPから操作

phpcassaを使用してPHPからCassandraの操作をします。
GitHubからダウンロードしてきて、lib以下のファイルをコピーしてあげれば動きます。
phpcassa

・Cの拡張モジュール追加
phpcassaの作成者がパフォーマンスアップの為にCの拡張モジュールを用意してくれています。
phpcassaをダウンロードしてきたページの「Using the C Extension」に書いてある通りに追加していきます。
解凍したディレクトリへ移動後下記コマンド実行

cd ext/thrift_protocol
phpize
./configure
make
sudo make install

php.iniへ下記項目追加

extension=thrift_protocol.so

apacheの再起動でCの拡張モジュールを追加する事ができます。
※Cの拡張モジュールは入れなくても動作します
※追加して頂いたインフラチームの検証では、50万回の値取得で3倍近く早くなったみたいですので、どうしても入れられない場合を除いて入れておいた方がいいと思います

サンプルプログラム

<?php
// ライブラリ読み込み
require_once ('./phpcassa/lib/autoload.php');

use phpcassa\ColumnFamily;
use cassandra\ConsistencyLevel;
use phpcassa\Connection\ConnectionPool;
use phpcassa\SystemManager;
use phpcassa\Schema\StrategyClass;

define("TTL", (60*60));

/**
 * keyspace接続
 * @param  string keyspace名
 * @param  array  ホスト名を配列で渡します
 * @return object keyspace接続オブジェクト
 */
$connection_pool = new ConnectionPool('blog_keyspace', array(
    // ip        :port
    '10.0.250.151:9160'
  , '10.0.250.152:9160'
  , '10.0.251.151:9160'
  , '10.0.251.152:9160'
));

/**
 * column family接続
 * @param  object keyspace接続オブジェクト
 * @param  string column family名
 * @return object column family接続オブジェクト
 */
$column_family = new ColumnFamily($connection_pool, 'blog_column_family');

/**
 * データ登録(既に存在する場合は更新)
 * @param string  キー名
 * @param array   array(カラム名 => 値)
 * @param int     timestamp
 * @param int     有効期間(キーは残ります)
 */
$column_family->insert('1', array(10 => date("Y/m/d")), null, TTL);

/**
 * データ取得
 * 取得するキーがないと例外が発生してしまうので、try-catchした方がいいです
 * @param  string  キー名
 * @return array   array(カラム名 => 値)
 */
$column_family->get('1');

/**
 * データ件数取得
 * @param  string  キー名
 * @return int     件数
 */
$column_family->get_count('1');

// 一括データ作成
for ($i = 0; $i < 10; $i++) {
    $key = sprintf("%d", $i);
    $data[$key] = array('rand' => rand(), 'access_data' => date("Y/m/d H:i:s"));
}
/**
 * 一括データ登録 or 更新
 * @param array   array(キー名 => array(カラム名 => 値))
 * @param int     timestamp
 * @param int     有効期間(キーは残ります)
 */
$column_family->batch_insert($data, null, TTL);

/**
 * データ削除(キーは残ります)
 * @param  string  キー名
 */
$column_family->remove('1');

/**
 * 全データ削除
 */
$column_family->truncate();
?>

終わりに

以上でEC2上にCassandraクラスタを作成しPHPから操作をする事ができました。
まだ作成しただけで運用していくとなると課題は沢山ありますが、作成する上で参考になれば幸いです。