こんにちわ、7月に入社したばかりの@emorinsです。
題名の通りですが分散データベース『Apache Cassandra』を紹介したいと思います。
少し前はHadoop(とHBase)と比較されることの多かったCassandraですが、最近はHadoopの人気に押されつつあるようにも感じます。
しかし、CassandraとHadoopは特徴が異なり、よく言われるのがCassandraはリアルタイム処理に向き、一貫性のかわりに可用性を重視し、またHadoopとは違って単一障害点もありません。
今日はそんなHadoopとは違った魅力のある分散データベース『Apache Cassandra』をはじめてみましょう。
目次
- Cassandraとは
- アーキテクチャ
- Cassandraの特徴
- コンシステンシレベル
- データモデル
- MemtableとSSTable
- セットアップ
- storage-conf.xmlの設定
- Keyspace
- ColumnFamily
- MemtableThroughputInMB
- MemtableFlushAfterMinutes
- Cassandra-CLIの使い方
- ThriftAPIによるクライアントからの操作
- PHPによるクライアントアプリケーションを作る
- データを取得する簡単なプログラム
- ThriftAPI
- 最後に
Cassandraとは
そもそもCassandraとは何でしょうか。
長い間データベースと言えばRDBMS(MySQL、PostgreSQL、Oracle Databaseなど)と、それを操作するSQL言語を使用するのが主流でした。しかし、大規模データ処理を必要としたGoogleやAmazonは、リレーショナルデータベースより(ペタバイトクラスの膨大なデータ処理に対して)高速で可用性のある、スケールアウトしやすいデータベースを必要としました。そこで生まれたのがGoogleのBigtableと、AmazonのDynamoです。
Cassandraは、そんなBigtableやDynamoを参考にFacebookが開発したNoSQLデータベースです。現在はApacheソフトウェア財団に寄贈され、オープンソースとして開発が進められています。
アーキテクチャ
Cassandraの特徴
Cassandraは、
CAP定理のうちAP(可用性:Availabilityと分割耐性:Partition Tolerance)を重視しています。また一貫性:Consistencyについては、どのレベルの整合性にするか調整可能です(後述)。
Cassandraの主な特徴として
などが言えます。
特に可用性の高さとSPOFが無いのが特筆すべき点です。
コンシステンシレベル
Cassandraは可用性の代わりに一貫性を犠牲にしていますが、遅延とのトレードオフで一貫性のレベル設定ができます。
- W=書き込みを保証するレプリケート数
- R=読み込むレプリケート数
- N=レプリケートしているノード数(storage-conf.xml内の<ReplicationFactor>)
- Q=QUORUM (Q = N / 2 + 1)
R + W > N
の条件を満たすとき、一貫性があると言えます。
設定できるレベルは以下の4つです。
- Zero:保証なし
- One:W=1,R=1
- [書き込み]1つのノードの書き込みを保証
- [読み込み]最初に読み込んだノードから返す
- Quorum:w=Q,R=Q
- [書き込み](N+1)/2数のノードに書き込まれたことを保証
- [読み込み](N+1)/2数のノードから読み込み、最新を返す
- All:w=N,R=N
- [書き込み]N個のノードに書き込まれたことを保証する
- [読み込み]N個ののノードから読み込み、最新を返す
データモデル
基本はKey/Value型ですが、階層的な構造を持つ4次元or5次元のデータモデルになっています。単純なKVSよりも高機能かつ表現力豊かなデータをモデリングできます。
それぞれのデータ構造をRDBMSと照らし合わせながら見ていきましょう。
- キースペース
- RDBMSでいうデータベースに当たるものです。Cassandraデータモデルの一次元目で、カラムファミリのコンテナになります。一般に1アプリケーションに1キースペースとされます。
- カラムファミリ
- RDBMSでいうテーブルに当たるものです。カラムのコンテナです。カラムファミリは後述するstorage-conf.xmlで設定して作成します。複数の行(Row)から構成されます。
- 行(Row)
- キーと複数のカラムを持つ。RDBMSでいえば、行はレコード、キーは主キーのイメージです。
- カラム
- Cassandraでの最小のデータ構造。名前(name)、値(value)、タイムスタンプ(timestamp)を持ちます。
- スーパーカラム
- 値(value)にソート済みの複数のカラムのリストを持つ。スーパーカラムは必ずしも必要ではありません。
分かりにくいと思うので、以下のCassandra-CLIでのデータ挿入の命令文を、少し無理にSQL文に置き換えてみましょう(Cassandra-CLIの使い方については後述します)。
この場合、Keyspace1がキースペース、Standard2がカラムファミリ、['jsmith']が行(キーは'jsmith')、そして['job']がカラムで'job'が名前(name)です。
MemtableとSSTable
ハードウェア上のデータの流れはどうなっているでしょうか。
まずCassandraに書き込み操作がされるとCommitLogに書き込まれます。その後、カラムファミリ毎にMemtableというメモリ空間に対して書き込まれます。Memtableが一杯になると、ディスク上にSSTableという形式で保存(フラッシュ)されます。
MemtableではSSTableにフラッシュする際に使用するキーを設定してソートしておくので、MySQLでのランダムアクセスとは違い、HDDにシーケンシャルに書き込みむので、高速です。
ここまで話を聞くと、MemtableからSSTableにフラッシュする回数を極力減らすため、Memtableの容量を設定したくなるかと思いますが、もちろん可能です。後述するstorage-conf.xmlという設定ファイルで、Memtableに使うメモリ容量や保存時間などが指定できます。
セットアップ
今回使用した環境は以下です。
- FreeBSD 8.1
- JDK 1.6
- Cassandra 0.6.3
- PHP 5.3.2
まずはCassandraを動かすために必要なJava環境を作るため、JDK 1.6をインストール。
# wget http://www.FreeBSDFoundation.org/cgi-bin/download?download=diablo-caffe-freebsd7-i386-1.6.0_07-b02.tar.bz2
# make install clean
Cassandraのインストール。
# cd /usr/ports/databases/cassandra
# make install clean
続いて初期設定。取りあえずサンプルで用意されているlog4j.properties.sampleとstorage-conf.xml.sampleをリネームして作成。設定ファイルの説明は後述します。
# cd /usr/local/share/cassandra
# cp conf/log4j.properties.sample conf/log4j.properties
# cp conf/storage-conf.xml.sample conf/storage-conf.xml
準備完了なので、起動します。これでシングルノードではありますがCassandraクラスタのできあがりです。
# /usr/local/share/cassandra/bin/cassandra
起動オプション
- -f:フォアグラウンドで起動
- -h:使用できるオプションを表示
- -p :PIDをに出力。
storage-conf.xmlの設定
Cassandraの最も重要な設定ファイルがstorage-conf.xmlです。このファイル内で、クラスタの各設定や、カラムファミリの作成、Memtableの容量など設定できます。
全ては紹介できませんが、いくつか重要な設定箇所をピックアップします。詳しい情報や設定例はstorage-conf.xml.sampleを見てください。
Keyspace
キースペースを作成します。Name属性を指定して、カラムファミリはこのKeyspaceタグ内に記述します。
今回の設定例:
<Keyspace Name="Keyspace1">
...
</Keyspace>
ColumnFamily
カラムファミリの設定です。下記のような属性を指定できます。
- Name:カラムファミリ名
- ColumnType:カラムタイプ。デフォルトは"Standard"。スーパーカラムを持ちたい場合は"Super"にする。
- CompareWith:カラムのソート方法
- KeysCached:キーをキャッシュする個数(%をつけると割合)
今回の設定例:
<ColumnFamily Name="Standard2" CompareWith="UTF8Type" KeysCached="100%"/>
MemtableThroughputInMB
Memtableのデータ容量
MemtableFlushAfterMinutes
フラッシュまでの最大時間。この時間を超えると強制的にフラッシュする。
その他詳しい設定例などは以下をご参考ください。
StorageConfiguration - Cassandra Wiki
Cassandra-CLIの使い方
データモデルの項でチラっと出てきましたが、Cassandra-CLIというクライアントツールを使って、Cassandraのデータを操作してみましょう。
Cassandraを起動した状態で、Cassandra-CLIを立ち上げます。
/usr/local/share/cassandra/bin/cassandra-cli --host localhost --port 9160
すると
Connected to: "Test Cluster" on localhost/9160
Welcome to cassandra CLI.
Type 'help' or '?' for help. Type 'quit' or 'exit' to quit.
cassandra>
と、Cassandra-CLIのコンソールに入ります。
では、手始めにデータを書き込んでみましょう。今回はKeyspace1のStandard2カラムファミリを利用します。Keyspace1と Standard2自体はstorage-conf.xml.sampleに初めから定義されているキースペース・カラムファミリですので、特別なことをしなくてもそのまま利用できるはずです。
cassandra> set Keyspace1.Standard2['unoh']['age']='25'
set文で、Standard2カラムファミリに'unoh'キーを追加した上で、'age'という名前のカラムに25という値を挿入しました。
実際にちゃんとデータの追加ができているかget文で確認してみます。
cassandra> get Keyspace1.Standard2['unoh']
=> (column=age, value=25, timestamp=1281559772819000)
Returned 1 results.
Standard2カラムファミリの'unoh'キーにあるカラムが取得できました。カラム名、カラム値、タイムスタンプがきちんと挿入されているのが確認できます。
さらに追加してみましょう。
cassandra> set Keyspace1.Standard2['unoh']['phone']='03-5766-3911'
Value inserted.
cassandra> set Keyspace1.Standard2['unoh']['web']='www.unoh.net'
Value inserted.
で、再度取り出してみる。
cassandra> get Keyspace1.Standard2['unoh']
=> (column=web, value=www.unoh.net, timestamp=1281560104883000)
=> (column=phone, value=03-5766-3911, timestamp=1281560057868000)
=> (column=age, value=25, timestamp=1281559772819000)
Returned 3 results.
完璧ですね。
cassandra> help
とすれば、実行可能なコマンド一覧がでますので、各自試してみてください。Cassandra-CLIはこれくらいにして次に進みます。
ThriftAPIによるクライアントからの操作
Cassandraをクライアントプログラムから操作するときには、RPCフレームワークである『Thrift』が利用できます。
Thriftとは
Thriftは、同じくFacebookが開発した言語間サービス開発のためのRPCフレームワークです。C++、C#、Java、OCaml、Perl、Python、PHP、Rubyなどで書かれたプログラム間を通信可能にしてくれます。
CassandraはJavaで書かれているわけですが、Thriftを使うことで、異なる言語からCassandraを操作することができるようになります。CassandraにはThiftインターフェースがあらかじめ用意されているので、Thrift(とCassandra)がサポートしている言語ならどの言語のクライアントコードからでも操作可能です。今回はタイトル通りPHPから利用してみましょう。
※CassandraのAPI/RPC/Thriftポートは9160番です。
Thriftのインストール
Thriftの大まかな使い方の流れは、
- Thriftインターフェース(.thrift)の作成
- そのインターフェースに沿った各言語のスケルトンを生成
- スケルトンを元にサーバー・クライアントのコーディング
という流れになります。Thriftインターフェース(.thrift)は、Cassandraのインストール先ディレクトリ下のinterfaceディレクトリに"cassandra.thrift"ファイルを利用するので、1の作業は必要ありません。また、2,3のサーバー(Cassandra)側のコーディングも必要ありません。
では、Thriftをインストールします。
# cd /usr/ports/devel/thrift/
# make install clean
インストールが完了したら、まずはThriftプロジェクト用のディレクトリを適当な場所に作成しましょう。
# mkdir ~/thrift
次に、Thriftプロトコルが実装されたPHPコードをThriftパッケージ内からコピーしてきます。このコードはPHPからThriftを扱うために必要な全プロジェクトで共通して使うライブラリです。
# wget http://ftp.riken.jp/net/apache/incubator/thrift/0.2.0-incubating/thrift-0.2.0-incubating.tar.gz
# tar zxvf thrift-0.2.0-incubating.tar.gz
# cp -R thrift-0.2.0/lib/php/src ~/thrift
次に"cassandra.thrift"を使ってCassandra用Thriftクライントを生成します。サーバーごとのThriftクライントはsrcディレクトリ内のpackagesディレクトリに分けて入れておきましょう。
# cd thrift
# mkdir src/packages
# cp /usr/local/share/cassandra/interface/cassandra.thrift ~/thrift/cassandra.thrift
# thrift --gen php cassandra.thrift
# mv -R gen-php/cassandra src/packages
これでCassandraをThriftから扱うための準備が整いました。ディレクトリ構成を以下にまとめます。
/thrift
├ /src・・・Thriftプロトコルの実装ファイル
├ Thrift.php
├ autoload.php
├ /ext
├ /packages・・・各サーバごとのThriftクライアント(gen-phpとして生成されたコード)
└ /cassandra・・・cassandra用Thriftクライアントが入ったディレクトリ
├ Cassandra.php
├ cassandra_constants.php
└ cassandra_types.php
├ /protocol
└ /transport
└ /unoh・・・プロジェクトディレクトリ
└sample.php・・・クライアントアプリケーション(ここで具体的にCassandraを操作するコードを書きます)
PHPによるクライアントアプリケーションを作る
では、いよいよPHPでCassandraを使ったアプリケーションを書いていきましょう。
データを取得する簡単なプログラム
・~/thrift/unoh/sample.php
<?php
$GLOBALS['THRIFT_ROOT'] = '../src';
require_once $GLOBALS['THRIFT_ROOT'].'/packages/cassandra/Cassandra.php';
require_once $GLOBALS['THRIFT_ROOT'].'/packages/cassandra/cassandra_types.php';
require_once $GLOBALS['THRIFT_ROOT'].'/transport/TSocket.php';
require_once $GLOBALS['THRIFT_ROOT'].'/protocol/TBinaryProtocol.php';
require_once $GLOBALS['THRIFT_ROOT'].'/transport/TFramedTransport.php';
require_once $GLOBALS['THRIFT_ROOT'].'/transport/TBufferedTransport.php';
try {
// Thriftの接続用インスタンスの作成
$socket = new TSocket('localhost', 9160);
$transport = new TBufferedTransport($socket, 1024, 1024);
$protocol = new TBinaryProtocolAccelerated($transport);
//Cassandraクライアントの作成
$client = new CassandraClient($protocol);
//接続開始
$transport->open();
// ColumnFamilyの指定
$columnParent = new cassandra_ColumnParent();
$columnParent->column_family = "Standard2";
//スライスの指定。切り出すColumnの範囲を指定できる。
$predicate = new cassandra_SlicePredicate();
$sliceRange = new cassandra_SliceRange();
//例えば、'age'から'phone'までのColumnを切り出す(Columnは昇順でソートされる)
$sliceRange->start = "age";
$sliceRange->finish = "phone";
$predicate->slice_range = $sliceRange;
//クエリを投げる
$result = $client->get_slice('Keyspace1', 'unoh', $columnParent,$predicate, cassandra_ConsistencyLevel::ONE);
//中身を出力してみる
var_dump($result);
//接続終了
$transport->close();
}
catch (TException $tx) {
print 'TException: '.$tx->why. ' Error: '.$tx->getMessage() . "\n";
}
出力結果
# php ~/thrift/unoh/sample.php
array(2) {
[0]=>
object(cassandra_ColumnOrSuperColumn)#9 (2) {
["column"]=>
object(cassandra_Column)#10 (3) {
["name"]=>
string(3) "age"
["value"]=>
string(2) "25"
["timestamp"]=>
float(1.281559772819E+15)
}
["super_column"]=>
NULL
}
[1]=>
object(cassandra_ColumnOrSuperColumn)#11 (2) {
["column"]=>
object(cassandra_Column)#12 (3) {
["name"]=>
string(5) "phone"
["value"]=>
string(12) "03-5766-3911"
["timestamp"]=>
float(1.281560057868E+15)
}
["super_column"]=>
NULL
}
}
Cassandraからデータを取り出すだけの簡単なコードですが、Cassandraとうまく連携できているのが確認できました。
ThriftAPI
先程のコード中でnew cassandra_SliceRange()を使ってスライスを作成したり、$client->get_slice()でクエリを投げて結果を受け取ったりしていました。これらはThriftAPIで利用できます。
最後に
本エントリで取り上げなかったこと
かけ足になりましたが、Cassandraの概要とシングルノードによる基本的な操作をご紹介しました。マルチノードクラスタ、ノードの追加・削除方法、チューニング、モニタリング、実用性など、また次の機会に突っ込めたらと思います。
参考