2013年2月12日火曜日

Cassandraってなんじゃ?(EC2でクラスタリング:シングルリージョン編)

以前cassandraの記事が途中で終わってしまっていたため、突然復活です。
前回までは、ec2にcassandraを入れて、ローカルからThrift APIでアクセスするところまで行いました。

今回はクラスタリングです。
cassandraはread/writeを分散できるクラスタリングの機能をサポートしており、負荷分散や冗長化がしやすいため、ここで勉強したいと思います。

構成は以下の通りです。



VPCのprivateサブネットに3台でクラスタリングして、publicサブネットからAPIでアクセスしてみます。
ここでは、natインスタンスを踏み台にしてcassandraの各ノードにsshで接続して作業します。

cassandraは、新しくノード(EC2インスタンス)が追加されたときに、クラスタ上のどれか1台につながればあとは自動的にすべてのノードに新ノードの情報が伝わるようになっています。そのため新規ノードが立ち上がった時に接続するためのseedといわれるノードが1つ以上必要です。ここではそのノードをseed (10.0.1.10)とします。


cassandraインスタンスの準備


cassandraは複数立ち上げますが、設定ファイルに自分のIPなどを記載する必要があるため、インスタンスごとに設定しなくても良いよう設定を自動化したAMIを作成します。
ベースとなるインスタンスに、以下のようにインストールします。
前回の記事から変化がある部分もあるため、最初から記載します。

javaのインストール

javaは以前の記事と同じように、ブラウザでOracleのサイトからダウンロードを初めて一旦キャンセルし、通信上のURLをコピーして使用します。またcassandraではjdk7ではなくjdk6が推奨されているようなので、今回はjdk6をインストールします。
# cd /usr/local/src
# curl -o jdk-6u39-linux-x64-rpm.bin -L http://download.oracle.com/otn-pub/java/jdk/6u39-b04/jdk-6u39-linux-x64-rpm.bin?AuthParam=1360419913_e2b3080676cab457471a1ee88b4dc0c5
# chmod a+x jdk-6u39-linux-x86-rpm.bin
# ./jdk-6u39-linux-x86-rpm.bin


cassandraのインストール

# cd /usr/local/src
# curl -OL http://ftp.tsukuba.wide.ad.jp/software/apache/cassandra/1.2.1/apache-cassandra-1.2.1-bin.tar.gz
# tar xzvf apache-cassandra-1.2.1-bin.tar.gz 
# mv apache-cassandra-1.2.1 /usr/local/
# cd /usr/local
# ln -s apache-cassandra-1.2.1 cassandra


cassandra.yaml


クラスタリングの設定では、cassandra.yamlの以下の部分を変更します。
seeds: 10.0.1.10
 本来はseedも動的に取得すべきですが、ここではseedが10.0.1.10を1つだけの決め打ちでいきます。

rpc_address: 0.0.0.0
 thriftプロトコルを受け付けるIPです。ここでは自分のprivateIPを指しますが、0.0.0.0でも動きます。

endpoint_snitch: Ec2Snitch
 ノードの置かれているネットワークトポロジの情報をcassandraが判断するための方式です。
 通常のデータセンターではデータセンターやラックという単位で区分けされますが、Ec2Snitchを使用すると、
 それがリージョンやゾーンとして区分けされます。

listen_address: 自分のprivateIP
 ノード間の通信に使用するときの自分のアドレスです。
 ここでは正しくIPを指定する必要があるようです。

auto_bootstrap: 自動でクラスタ参加するかどうか
 自分がseedのときはfalse、非seedのときはtrueを設定します。

このうち、listen_addressはノードによって変わるため、起動前に動的に書き換えられるようにする必要があります。
方法は後述します。


/etc/hosts


たとえば10.0.1.10は、hostnameとしてip-10-0-1-10などと振られますが、hostsファイルに記載がないためcassandraの起動時にエラーが発生します。そのため、起動時にhostsに自動登録する必要があります。
方法は、cloud-initや起動スクリプト内で行うなどありますが、ここではcassandraの起動スクリプト内で実行してみます。


/etc/init.d/cassandra (簡易版)

ここでは最もシンプルな起動スクリプトを使います。必要であればもっと高機能のものでもよいです。
ただ、上述のcassandra.yamlと/etc/hostsへの自動登録をstart時に行うようにしておきます。

# chkconfig: 345 95 1
# description: cassandra
# processname: cassandra

#!/bin/sh

CASS_BIN=/usr/local/cassandra/bin/cassandra
CASS_PID=/var/run/cassandra.pid

case "$1" in
    start)
        # hosts1行目(127.0.0.1)への自動登録

        sed -i '1s/ip-.*//g' /etc/hosts
        sed -i "1s/$/ $(hostname)/g" /etc/hosts
        # cassandra.yamlへのlisten_addressの自動登録
        sed -i '/^listen_address:/d' /usr/local/cassandra/conf/cassandra.yaml
        echo "listen_address: `curl http://169.254.169.254/latest/meta-data/local-ipv4`" >> /usr/local/cassandra/conf/cassandra.yaml

        $CASS_BIN -p $CASS_PID
        echo "Running Cassandra"
        ;;
    stop)
        kill `cat $CASS_PID`
        rm -f $CASS_PID
        echo "Stopped Cassandra"
        ;;
    *)
        echo "Usage: $0 {start|stop}"
        exit 1
esac
exit 0

cassandra.yamlのauto_bootstrapについては、UserData→cloud-initなどで自動設定もできますが、今回は固定でseedと非seed用でtrue, falseに固定して、それぞれの状態でseed用と非seed用のAMIを作成しておきます。


セキュリティグループ




セキュリティグループを設定します。
cassandraは以下のポートを利用します。
  • 7000:ノード間の接続
  • 7199:nodetoolなどツールが使用するJMX
  • 9160:Thrift API
これらと、作業用のsshポートなどを開放します。

http
  • 80 0.0.0.0/0
ssh
  • 22 10.0.0.0/16
nat
  • 22 10.0.0.0/16
  • 22 作業者のIP
cassandra
  • 7000 10.0.0.0/16
  • 7199 10.0.0.0/16
  • 9160 10.0.0.0/16

それぞれのインスタンスには以下を割り当てます。
  • app:http
  • nat:ssh


起動と確認


ここまでできたら、cassandraのAMIを起動します。
まず、seed用のAMIから起動します。
subnetはprivate用の10.0.1.0/24を指定し、privateIPに10.0.1.10を指定します。
セキュリティグループは以下を割り当てます。
  • seed, cluster*:ssh, cassandra
次に、cluster用のAMIから同様に2台起動します。privateIPは特に指定しません。


sshで10.0.1.10に入ります。

そこでcassandra-cliで前回と同じようにkeyspaceやcolumn familyを作成します。
# /usr/local/cassandra/bin/cassanra-cli
[default@unknown]  create keyspace Hogebook;
[default@unknown]  use Hogebook;
[default@Hogebook] create column family User with comparator = UTF8Type and 
default_validation_class=UTF8Type and key_validation_class=UTF8Type and column_metadata =[
{column_name: email, validation_class: UTF8Type},
{column_name: gender, validation_class: UTF8Type, index_type: KEYS}];
[default@Hogebook] set User['memorycraft']['email'] = 'memorycraft@gmail.com';
UnavailableException

エラーが発生しました。。
cassandraは他ノードへのデータのレプリカ数と、設定された一貫性保証レベルによって書き込みの際にエラーになったりするようです。これについては別記事で触れたいと思います。

ひとまずここでは、以下のようにして、3台すべてにデータレプリケーションされるように設定しておきます。
[default@Hogebook] update keyspace Hogebook with placement_strategy = 'org.apache.cassandra.locator.NetworkTopologyStrategy' and strategy_options = {ap-northeast:3};
[default@Hogebook] set User['memorycraft']['email'] = 'memorycraft@gmail.com';
[default@Hogebook] set User['memorycraft']['gender'] = 'male';
[default@Hogebook] set User['memorycraftgirl']['gender'] = 'female';
[default@Hogebook] set User['memorycraftgirl']['email'] = 'memorycraft+girl@gmail.com';
[default@Hogebook] get User where gender = 'male';
[default@Hogebook] get User where gender = 'female';


そして、クラスタの状態を見てみます。
クラスタの管理はcassandraのインストールディレクトリに入っているnodetoolを利用します。
ringコマンドは、クラスタの状態をみることのできるコマンドです。

# /usr/local/cassandra/bin/nodetool ring
Datacenter: ap-northeast

==========
Replicas: 3

Address         Rack        Status State   Load            Owns                Token                                       
                                                                               4159756940621079776                         
10.0.1.227      1a          Up     Normal  70.45 KB        100.00%             8650976588742297378                         
10.0.1.176      1a          Up     Normal  80.79 KB        100.00%             -7564491331177403445                        
10.0.1.10       1a          Up     Normal  90.42 KB        100.00%             4159756940621079776 

Ownsが100%になっているので、3台にすべてデータがレプリケーションされている状態です。


また、非seedであるclusterインスタンスをみてみます。
# cat /etc/hosts
127.0.0.1       localhost.localdomain localhost ip-10-0-1-176

# cat /usr/local/cassandra/conf/cassandra.yaml

....
auto_bootstrap: true 
listen_address: 10.0.1.176

設定ファイルの自動登録も旨く行っているようです。
これなら何台でも同じAMIから起動できます。


APIアクセス


また、appインスタンスに前回の記事と同じように、phpcassaをインストールします。
そして、以下のようなスクリプトで10.0.1.10に対してデータを連続投入してみます。

~/app/test.php 
---
<?php
    require(dirname(__FILE__).'/lib/autoload.php');

    use phpcassa\ColumnFamily;
    use phpcassa\ColumnSlice;
    use phpcassa\Connection\ConnectionPool;

    try{
        $servers = array('10.0.1.10:9160');
        $pool = new ConnectionPool('Hogebook', $servers);
        $user = new ColumnFamily($pool, 'User');

        //データの挿入
 while(1){
          $id = md5(uniqid(rand(),1));
   echo $id."\n";
   $user->insert($id,
            array(
                'email' => uniqid().'@gmail.com',
                'gender' => 'female',
            )
          );
   sleep(1);
 }
        //$pool->close();
    }
    catch(Exception $e){
        echo 'ERROR : ' . print_r($e, true);
    }
?>
----

本来コネクションプールは閉じないと行けませんが、ここでは無限ループさせてみます

$ php test.php 
5b3e62154f94a2a669e6b77298f22272
131b1d417165fea802c1a24afbfc2e7e
b3a731d17a301c1fa4f82d89500a1f68
86d568f544470595ab72483657b04352
ab4a15d2346440cc0e089235f68032ae
553ce1a57ad18a108ccba94224c98c4c
c2cf1319c7e099d16f84ecc2802f7b43
61c1d722b1d5d6442706fd5c4e5c0077
7067cfc415def987c5f5ffa12db9879e
a21f80887c9c2d163218045cf8ba321c
d71602cdd32f2cbd0a054aebacaa3764
f4f752c1dd8edde587d6d4a77ae30ec2
12901e29685ab6c3581a4944207c2e55
394f9c09e93dd33b1d378a9b299d1573
7d547f9682592945ddc0ddf1218e1c7e
f6fa6f5c8a80130abee396c438b6a8ac
fec78b484fa4f3975e60c86c082fe9fe
b629d87134a38d487e1750b3fa631d4c
72f9e4965084f39f158853761f0c62c5
e216af506ddae295346d94292e62fb7d
1631160e0977fdd6c4264555006c84ef
aa7f18a38ff739ce4a290d1f109e77f5
709a39189cef0cf82375ea124ef1d97c
0819cf50c7ed9f22b34c5a685f349f4f
5961358ecb3b4bb15ed2e6853357900d
1982683673d01a968a7f90dac8f6fe2c
3a4515cad2ee0ae803230aa7a5da7169
1d331dbd45cbee8038bb1c550039ae31
5d24a90282342d2786ce2dc25b398737
22194a119a48dd492cc42efc6f150b5a
e9040f305bccee5a5ae9fafa47e81820
f5dd73dfb1eda2de50a6ed03ce2e6de0
db86daa2bcb97bc269f846f3a78bb606
0c8a8793d8e71f29021917d7e0441519
bbf590273829d2efb99adba810ef7f13
a41e7ed89d14174f8b896bf887b7cfdc
bad960ca18793b57ca92974dce8bf248
6d4abcef26f8a9138034208df4bbc227
9fc5abb9d63e42a86c297835e9dad6bd
d1dbe39f50628cce099e2dcc3035392b


適当な処で終了して、seedインスタンス(10.0.1.10)でnodetoolを見てみます。
cfstatsコマンドではデータの統計情報がみれます。

# /usr/local/cassandra/bin/nodetool cfstats
.....
----------------
Keyspace: Hogebook
 Read Count: 18
 Read Latency: 0.16872222222222222 ms.
 Write Count: 338
 Write Latency: 0.5308639053254437 ms.
 Pending Tasks: 0
  Column Family: User
  SSTable count: 0
  Space used (live): 0
  Space used (total): 0
  Number of Keys (estimate): 0
  Memtable Columns Count: 667
  Memtable Data Size: 346140
  Memtable Switch Count: 0
  Read Count: 18
  Read Latency: 0.169 ms.
  Write Count: 338
  Write Latency: 0.531 ms.
  Pending Tasks: 0
  Bloom Filter False Positives: 0
  Bloom Filter False Ratio: 0.00000
  Bloom Filter Space Used: 0
  Compacted row minimum size: 0
  Compacted row maximum size: 0
  Compacted row mean size: 0

データの投入は成功しているようです。

またclusterインスタンス(10.0.1.176)で上記のPHPで出力されたキーを任意に選んで取得してみます。
# /usr/local/cassandra/bin/cassandra-cli

[default@unknown]  use Hogebook;
[default@Hogebook]  get User['d1dbe39f50628cce099e2dcc3035392b'];

=> (column=email, value=5119c2f9030a6@gmail.com, timestamp=1360642809012461)
=> (column=gender, value=female, timestamp=1360642809012461)
Returned 2 results.
Elapsed time: 19 msec(s).


取得も成功しました。

とりあえず基本的なクラスタリングの設定ができたようです。
次回は、もう少し詳しく見てみたいと思います。