2012年9月19日水曜日

Cassandraってなんじゃ?(API編)

Cassandraを利用するケースは、Webアプリケーションやバッチアプリケーションなど、プログラムからのアクセスが多いと思います。
そこで今回は、Cassandraにphpからアクセスしてみたいと思います。

CassandraにphpからアクセスするためのAPIライブラリとして以下のようなものがあるようです。
このうち、Thriftは、Cassandra専用でもPHP専用でもなく、複数の言語間でRPC通信を可能にする汎用フレームワークで、設定コードを元に各言語用のプログラムを生成します。他のライブラリはThriftで生成されたCassandraのlow-level APIをラップしたものになります。

もともとThriftを使おうと思っていましたが、Thriftの最新版 0.8.0は、Cassandra1.1.5では出力されたコードが不十分だったため、一度断念しました。機会があれば再度試してみたいと思います。

今回はphpcassaを試してみました。
phpcassaもThriftを使用しているので、Thriftが組み込まれ、すでにコード生成もされているので、インストールも非常に簡単です。

それでは早速インストールしてみます。
$ cd /usr/local/src
$ wget https://github.com/downloads/thobbs/phpcassa/phpcassa-1.0.a.5.tar.gz
$ tar xzvf phpcassa-1.0.a.5.tar.gz
$ cd phpcassa-1.0.a.5
$ phpize
$ ./configure
$ make
# make install

これでインストールは完了です。

次にアプリケーション用ディレクトリを作成し、phpcassaのライブラリをコピーします。
$ mkdir -p /home/memorycraft/app
$ cd /usr/local/src/phpcassa-1.0.a.5
$ cp -r lib /home/memorycraft/app/
$ cd /home/memorycraft/app/
$ touch test.php
$ tree -L 2
.
|-- lib
|   |-- autoload.php
|   |-- phpcassa
|   `-- thrift
`-- test.php


これでライブラリがそろったので、これらを利用してcassandraにアクセスするコードを書いてみます。
$ vim test.php
<?php
    require(dirname(__FILE__).'/lib/autoload.php');

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

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

        //データの挿入
        $user->insert('memorycraftgirl',
            array(
                'email' => 'ng@gmail.com',
                'gender' => 'female',
            )
        );

        //データの更新
        $user->insert('memorycraftgirl', array('email'=>'memorycraft+girl@gmail.com'));

        //キーで全カラムを取得  
        $girl = $user->get('memorycraftgirl');
        echo 'girl = ' . print_r($girl, true);

        //キーでカラムを指定して取得
        $email = $user->get('memorycraftgirl', null, array('email'));
        echo 'email = ' . print_r($email, true);

        //複数キーを指定して取得
        $users = $user->multiget(array('memorycraft', 'memorycraftgirl'));
        echo 'users = ' . print_r($users, true);


        //カラムの削除
        $user->remove('memorycraftgirl', array('email'));
        echo 'girl after remove column = ' . print_r($user->get('memorycraftgirl'), true);

        //行の削除
        $user->remove('memorycraftgirl');
        echo 'girl after remove row = ' . print_r($user->get('memorycraftgirl'), true);

        $pool->close();
    }
    catch(Exception $e){
        echo 'ERROR : ' . print_r($e, true);
    }
?>

このコードでは前回作成したHogebookのUserというColumn Familyに対して、挿入/更新/取得/削除を行い、その都度結果や、Column Familyの状態を表示するようにしました。

それでは実行してみます。
$ php test.php
girl = Array
(
    [email] => memorycraft+girl@gmail.com
    [gender] => female
)
email = Array
(
    [email] => memorycraft+girl@gmail.com
)
users = Array
(
    [memorycraft] => Array
        (
            [email] => memorycraft@gmail.com
            [gender] => male
        )

    [memorycraftgirl] => Array
        (
            [email] => memorycraft+girl@gmail.com
            [gender] => female
        )

)
girl after remove column = Array
(
    [gender] => female
)
ERROR : cassandra\NotFoundException Object
(
    [message:protected] => 
    [string:Exception:private] => 
    [code:protected] => 0
    [file:protected] => /home/memorycraft/app/lib/phpcassa/ColumnFamily.php
    [line:protected] => 308
    [trace:Exception:private] => Array
        (
            [0] => Array
                (
                    [file] => /home/memorycraft/app/lib/phpcassa/ColumnFamily.php
                    [line] => 299
                    [function] => _get
                    [class] => phpcassa\ColumnFamily
                    [type] => ->
                    [args] => Array
                        (
                            [0] => memorycraftgirl
                            [1] => cassandra\ColumnParent Object
                                (
                                    [column_family] => User
                                    [super_column] => 
                                )

                            [2] => cassandra\SlicePredicate Object
                                (
                                    [column_names] => 
                                    [slice_range] => phpcassa\ColumnSlice Object
                                        (
                                            [start] => 
                                            [finish] => 
                                            [reversed] => 
                                            [count] => 100
                                        )

                                )

                            [3] => 
                        )

                )

            [1] => Array
                (
                    [file] => /home/memorycraft/app/test.php
                    [line] => 43
                    [function] => get
                    [class] => phpcassa\ColumnFamily
                    [type] => ->
                    [args] => Array
                        (
                            [0] => memorycraftgirl
                        )

                )

        )

    [previous:Exception:private] => 
)

概ね期待通りの動作になっています。
また、存在しないキーを取得しようとすると例外が返る仕様のようです。

前回少し触れましたが、Cassandraはメモリテーブルを使用するため、このテストプログラムを実行したときの感覚としてはmemcacheへアクセスしたときと同じような体感速度でした。
今回は、localhostへのアクセスでしたが、クラスタを形成した場合のパフォーマンスなど、今後さらに調査してみたいと思います。

2012年9月17日月曜日

Cassandraってなんじゃ?

今回はCassandraをさわってみました。
Cassandraは、もともとFacebookが開発したKVSで、現在はApache Cassandraとしてオープンソース化されています。

Cassandraには以下の特徴があります。
  • 分散方式はAmazonのDynamoと同様なConsistentHashing方式
  • ストレージ方式はGoogleのBigTagleと同様なコミットログ/メモリテーブル/ディスクテーブル方式
  • CAP定理の3つの要素では、一貫性よりも可用性と分割耐性を重視
  • データモデルは列指向
  • アプリケーションからはThrift APIを使用

KVSのモデルはどれも一長一短で、Cassandraの場合デフォルトでは一貫性保証が弱いとされていますが、一貫性オプションの選択ができ、すべてのサーバーが最新の値に更新されてから読み出しができるようにすることも可能です。

Cassandraのデータの保持構造は以下のような階層で保存されます。
  • Keyspace:RDBのデータベースにあたるもの
  • Column Family:RDBのテーブルにあたるもの
  • Row:RDBのレコードにあたるもの
  • Column:RDBのカラムにあたるもの、name, value, timestampから構成される
この他にもSuper Columnがありますが必須要素ではありません。

それではインストールしてみます。

JDKのインストール

CassandraはJVM上で動作するため、まずはJDKをインストールします。
OpenJDKはCassandra1.1.5ではセグメンテーション違反が起こったため、Sun SDKを使用しました。
また、RPM版はインストールエラーがでたのでTAR版でインストールします。

ダウンロードはOracleのダウンロードページから行いますが、linuxシェル上では、Acceptボタンの操作ができないので、ブラウザでAcceptをチェックしダウンロードを開始した直後に一時停止した後、通信コンソール上のURLをコピーしてターミナル上でwgetしました。
今回は記事の都合上すべてrootで作業しましたが、適宜専用ユーザーで行なってください。

# yum install -y wget
# cd /usr/local/src
# wget http://download.oracle.com/otn-pub/java/jdk/7u7-b10/jdk-7u7-linux-i586.tar.gz?AuthParam=1347660783_4d176083902db0eb4404c8b5fcd542d0
# mv jdk-7u7-linux-i586.tar.gz\?AuthParam\=1347660783_4d176083902db0eb4404c8b5fcd542d0 jdk-7u7-linux-i586.tar.gz
# tar xzvf jdk-7u7-linux-i586.tar.gz 
# mkdir -p /usr/java/
# mv jdk1.7.0_07  /usr/java/jdk1.7.0_07

JAVA用の環境変数を定義します。
# vim /etc/profile
-----
JAVA_HOME=/usr/java/jdk1.7.0_07
PATH=$PATH:$JAVA_HOME/bin
CLASSPATH=.:$JAVA_HOME/jre/lib:$JAVA_HOME/lib/tools.jar
export JAVA_HOME CLASSPATH
------
# source /etc/profile
# cd /usr/local/src

簡単なコードを書き、コンパイルして動作確認します。
# vim HellowWorld.java
public class HelloWorld
{
    public static void main(String args[])
    {   
        System.out.println("Hello World!");
    }   
}
# javac HelloWorld.java
# java HelloWorld
Hello World!
これでJDKがインストールされました。


Cassandraのインストール

それではCassandraをインストールします。 CassandraはApache Cassandraのダウンロードページのリンクからwgetします。

# cd /usr/local/src/
# wget http://ftp.riken.jp/net/apache/cassandra/1.1.5/apache-cassandra-1.1.5-bin.tar.gz
# tar xzvf apache-cassandra-1.1.5-bin.tar.gz 
# mv apache-cassandra-1.1.5 /usr/local/
# cd /usr/local/
# ln -s apache-cassandra-1.1.5/ cassandra


Cassandraの起動

 今回はフォアグラウンドで起動してみます。 正常に動作すれば起動ログが出力されます。
# cd /usr/local/cassandra
# ./bin/cassandra -f


Cassandraのクライアントコンソールの起動

別のシェルからクライアントを起動します。
# ./bin/cassandra-cli --host localhost

[default@unknown]

[default@unknown]と表示されれば、Cassandraのコンソールに入れたことになります。 ここで、keyspaceの操作や、データのCRUDを行います。

以降、コンソール内での操作になります。
本来であればRDBとは概念が異なるため、するべきではないかも知れませんが、 とっつきやすくするために、対応するMySQLの操作を参考に記載します。


Keyspaceの作成

Hogebookという名前でkeyspaceを登録してみます。
MySQLでいうところのCREATE DATABASEにあたります。
[default@unknown] create keyspace Hogebook;
983a327c-bab3-37c3-958d-d856986f4f2b
Waiting for schema agreement...
... schemas agree across the cluster


Keyspaceの切り替え

使用したいKeyspaceをHogebookに切り替えます。
MySQLのUSE databaseにあたります。
[default@unknown] use Hogebook;
Authenticated to keyspace: Hogebook


Column Familyの作成

Column Familyを作成します。
UserというColumn Familyを作成してみます。
MySQLではCREATE TABLEに相当します。
作成する際にColumnのソート順を決めるComparatorを指定します。 ここではUTF8Typeを指定しますが、他にBytesType、AsciiType、UTF8Type、LexicalUUIDType、TimeUUIDType、LongTypeがあります。
[default@Hogebook] create column family User with comparator = UTF8Type;      
4ce3585d-9f66-36cb-8433-76bd1acf7827
Waiting for schema agreement...
... schemas agree across the cluster


Column Familyの設定の変更

emailとgenderというカラムのデータ型を定義します。
Column Familyの設定を変更します。
MySQLでのALTER TABLEに該当します。
[default@Hogebook] update column family User with column_metadata =  
... [
... {column_name: email, validation_class: UTF8Type},
... {column_name: gender, validation_class: UTF8Type} 
... ]
... ;
86ed51a4-e3b1-36b7-9cb9-93e4bf1a5fde
Waiting for schema agreement...
... schemas agree across the cluster

※追記:org.apache.cassandra.db.marshal.MarshalException: cannot parse ...とエラーになる場合は、
create column family User with comparator = UTF8Type and default_validation_class=UTF8Type and key_validation_class=UTF8Type;
のようにするといいようです。

データの投入

それではデータを保存してみます。
MySQLでのINSERTやUPDATEにあたります。
この例では'memorycraft'というキーでemail,genderというcolumnを保存します。
このようにCassandraでは、連想配列をつくるようにコマンドをセットするため、INSERTとUPDATEは同じ処理になります。
[default@Hogebook] set User['memorycraft']['email'] = 'memorycraft@gmail.com';
Value inserted.
Elapsed time: 47 msec(s).
[default@Hogebook] set User['memorycraft']['gender'] = 'male';
Value inserted.
Elapsed time: 2 msec(s).
[default@Hogebook] set User['memorycraftgirl']['gender'] = 'female';
Value inserted.
Elapsed time: 3 msec(s).
[default@Hogebook] set User['memorycraftgirl']['email'] = 'memorycraft+girl@gmail.com';
Value inserted.
Elapsed time: 17 msec(s).


データの取得

ここで、投入したデータを取得してみます。
MySQLでいうPKを条件にしたSELECTにあたります。
 
[default@Hogebook] get User['memorycraft'];                   
=> (column=email, value=memorycraft@gmail.com, timestamp=1347799163739000)
=> (column=gender, value=male, timestamp=1347799233322000)
Returned 2 results.
Elapsed time: 2 msec(s).


データの検索

キー以外のカラムの条件でデータを検索したい場合があります。
以下のように行います。
 
[default@Hogebook] get User where gender = 'male';
No indexed columns present in index clause with operator EQ
しかし、エラーになりました。インデックスがないことが原因のようです。
Cassandraではキー以外で検索をかける場合、そのカラムにインデックスを張る必要があります。
そこで、カラムの設定にインデックスを設定します。
[default@Hogebook] update column family User with column_metadata =  
... [
... {column_name: email, validation_class: UTF8Type},
... {column_name: gender, validation_class: UTF8Type, index_type: KEYS} 
... ]
... ;
86ed51a4-e3b1-36b7-9cb9-93e4bf1a5fde
Waiting for schema agreement...
... schemas agree across the cluster

このように、index_type: KEYSとすることで、genderカラムで検索をかけることが可能になります。

[default@Hogebook] get User where gender = 'male';
-------------------
RowKey: memorycraft
=> (column=email, value=memorycraft@gmail.com, timestamp=1347799163739000)
=> (column=gender, value=male, timestamp=1347799233322000)

1 Row Returned.
Elapsed time: 119 msec(s).


データの削除

データを削除してみます。
MySQLでのDELETEに該当します。
[default@Hogebook] del User['memorycraftgirl'];   
row removed.


とりあえず、駆け足で簡単なCRUD処理をさらってみました。
ここまでは、一風変わったデータストアというところまでしかわかりません。次はもう少しCassandraらしい部分を調べてみます。

2012年9月5日水曜日

S3ってなんじゃ?(CORS対応)

ごぶさたしております。
S3がCORS対応になったので、少し触れてみたいと思います。

CORSはCrossDomainResourceSharingの略です。
JSONPなどの特殊なケースを除いて通常ajaxなどではクロスドメイン通信は認められていません。
CORSは、通信先のサーバーで条件付きで許可をすることでクロスドメインアクセスを可能にするための仕組みで、W3Cで策定されいてる仕様です。モダンブラウザであればほぼサポートされているかと思います。

CORSではブラウザがクロスドメインのサーバーにリクエストする際に、事前にそのサーバーがこれから行おうとしているリクエストを許可しているかどうかをHTTPメソッドのひとつであるOPTIONSメソッドといくつかのHTTPリクエストヘッダを用いて問い合わせます。これをPreflightリクエストと言います。
そしてそのレスポンスをもってブラウザは通信可能かどうかを判断します。

Preflightリクエストで使用されるヘッダは
  • Origin 
    • リクエスト元のオリジン
    • (http://www.example.com/foo/barからリクエストされる場合、http://www.example.com/)
  • Access-Control-Request-Method 
    • これからリクエストするときに使用するHTTPメソッド
  • Access-Control-Request-Headers
    • これからリクエストするときに使用するHTTPヘッダー

などです。これらは通常ブラウザが自動で判別してリクエストヘッダに付与します。
それに対してレスポンスヘッダには以下の内容がセットされます。
  • Access-Control-Allow-Origin
    • この項目で指定されたOriginにリクエストが許可されます。
  • Access-Control-Allow-Credentials
    • trueならCookieなどユーザーの資格情報をレスポンスに含めることを許可します
  • Access-Control-Expose-Headers
    • CORS API仕様として公開して良いヘッダーフィールド
  • Access-Control-Max-Age
    • このPreflightリクエストの結果のキャッシュ保持秒数
  • Access-Control-Allow-Methods
    • 許可するHTTPメソッドが含まれます
  • Access-Control-Allow-Headers
    • 許可するリクエストヘッダーが含まれます

ブラウザはこのレスポンスヘッダを見てリクエスト可能かどうかを判断します。
通常REST APIなどを実装しWEBサービスを行うサーバーでは、このCORSに対応するような実装をすることで許可した相手にのみAPIを利用できるように実装を行う必要がありますが、今回S3ではそれが簡単な設定を行うだけで利用できるようになりました。

それでは実際にS3のCORS機能を使って具体的にどんなことが行われているのかを見てみます。

まずS3にバケットをつくります。
そこに例として以下のJSONファイルをアップロードします。

{
   "id": "1405028029",
   "name": "Satoru Miura",
   "first_name": "Satoru",
   "last_name": "Miura",
   "link": "https://www.facebook.com/memocra",
   "username": "memocra",
   "gender": "male",
   "locale": "ja_JP"
}

そしてEC2などで立ち上げた別のホストにこのjsonファイルをajaxで読み込む以下のようなHTMLを配置します。

<html>
  <head>
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" ></script>
    <script type="text/javascript">
    $(function(){
      $.ajax({
        url:'http://s3-ap-northeast-1.amazonaws.com/memorycraft/memocra.json',
        type:'GET',
        contentType:"application/json",
        error:function(XMLHttpRequest, textStatus, errorThrown){
          alert("ERROR " + textStatus+ " " + errorThrown);
        },
        success:function(data){
          for(var p in data){
            $("#data").append("<li>"+p+" : "+data[p]+"</li>");
          }
        },
        dataType:'json',
        processData: false
      });
    });
    </script>
  </head>
  <body>
    <h1>Graph</h1>
    <ul id="data"></ul>
  </body>
</html>


WEBブラウザのアドレスバーに直接
http://s3-ap-northeast-1.amazonaws.com/memorycraft/memocra.json
と入力するとロードできますが、別ホストで上記のようなHTMLページからのajaxではドメインが異なるためロードに失敗します。



これはchromeブラウザですが、通信をみるとOPTIONSメソッドで送信しており、以下のヘッダを送信していることがわかります。

  • Access-Control-Request-Headers
    • origin, content-type, accept
  • Access-Control-Request-Method
    • GET
  • Host
    • s3-ap-northeast-1.amazonaws.com
  • Origin
    • http://176.34.47.135


これがPreflightリクエストです。
このあとOrigin、Content-Type、AcceptヘッダをGETメソッドで送りますよと伝えています。
結果は403で返ってきました。

バケットのプロパティを見るとPermissionsタブに、「Edit CORS Configuration」というボタンがあるのでクリックすると以下のようなXMLの設定が表示されます。



<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>Authorization</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

CORSRuleにある設定がこのS3バケットで受け付けている設定になります。
これらは前述の
  • Access-Control-Allow-Origin
  • Access-Control-Allow-Methods
  • Access-Control-Max-Age
  • Access-Control-Allow-Headers

レスポンスヘッダに該当し、許可する条件を示しています。
このデフォルト設定をみると、アクセス元はどこでもよく、許可するHTTPでメソッドはGETのみ、認証キャッシュは3000秒で、Authorizationヘッダーのみを許可しています。

Authorizationヘッダーのみの許可なので、本リクエストで送る予定のAccept, Content-Type, Originヘッダーが許可されず403エラーとなっているようです。
ここで、AllowdHeaderを増やしてみます。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>Authorization</AllowedHeader>
        <AllowedHeader>Accept</AllowedHeader>
        <AllowedHeader>Content-Type</AllowedHeader>
        <AllowedHeader>Origin</AllowedHeader>
</CORSRule>
</CORSConfiguration>

もしくはすべてのヘッダーを受け付けるようにワイルドカード*を指定します。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

すると、GETであればアクセス可能になるので、ajaxリクエストは成功します。



この状態では、どのホストからでもGETリクエストであればアクセスできてしまうので、
接続するOriginを特定してみます。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>http://176.34.47.135</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

ここで先ほどとはまた別のホストから同様にajaxでリクエストしてみると、Preflightリクエストで403が返り、アクセスできないことがわかります。



いままではS3のパーミッションはAWSアカウントを基準としたアクセス制御でしたが、
このようにHTTPレベルでもアクセス制御ができるようになりました。

これによって、S3の使用の仕方がとても広がるのではないかと思います。
以上です。