2013年2月14日木曜日

Cassandraってなんじゃ?(EC2でDataStaxOpsCenterを動かす)

前回の記事で、Cassandraでクラスタリングをした場合、クラスタの情報をみるためのnodetoolの紹介をしました。
ただ、nodetoolやcassandra-cliでは、ノードの構成や配置が複雑だったりデータの分布を直感的に確認することは大変です。

そんな場合、DataStax社が提供しているOpsCenterという管理画面ツールを使うと便利なようです。DataStaxはApache Cassandraのプロジェクトリーダーが起こした会社でcassandraのコミッタの多くが在籍しているそうです。

DataStax OpsCenterに関しては、以下のサイトで紹介されています。
Cassandraクラスタと、DataStax OpsCenterの構築

また、DataStaxの公式サイトに詳細なガイドがあります。
DataStax OpsCenter Documentation

これらのサイトを参考に、OpsCenterをEC2にインストールしてみました。いくつかEC2ならではの部分やはまった点に触れながら手順を紹介しようと思います。


OpsCenterのインストール


前回の記事でAPIアクセスをしたpublicサブネットのappサーバーにインストールします。

# cd /usr/local/src/
# rpm -Uvh http://ftp-srv2.kddilabs.jp/Linux/distributions/fedora/epel/6/x86_64/epel-release-6-8.noarch.rpm
# cat /etc/yum.repos.d/datastax.repo
[datastax]
name= DataStax Repository
baseurl=http://rpm.datastax.com/community
enabled=1
gpgcheck=0
# yum install opscenter-free -y

接続先の設定をします。
opscenterd.confで設定します。
ここで、seed_hostsには、前回設定したseedインスタンスのIPを指定します。
また、ここではuse_sslをfalseにしてHTTPアクセスにしておきます。

# vi /etc/opscenter/opscenterd.conf
---

[jmx]
port = 7199
[webserver]
port = 8888
interface = 0.0.0.0
[cassandra]
seed_hosts = 10.0.1.10

[agents]
ssh_port = 22
use_ssl = false
https_port = 61621
incoming_port = 61620

---


セキュリティグループの設定


OpsCenterでは、以下のサイトに説明があるように接続に複数のポートを使用するようです。
OpsCenter and OpsCenter agent ports

ここでは以下のようにポートを設定します。

app(OpsCenter)インスタンス
  • 22  10.0.0.0/16 →ssh(natから入る想定)
  • 80  0.0.0.0/0  →通常のWEB接続
  • 8888  0.0.0.0/0  →OpsCenterコンソールのUI画面
  • 61620  10.0.0.0/16 →cassandraの各ノードからOpsCenterへ情報を送るポート

cassandraインスタンス
  • 22  10.0.0.0/16 →ssh(natから入る想定)、agentのインストールに必要
  • 7000  10.0.0.0/16 →cassandraノード同士の通信
  • 7199  10.0.0.0/16 →JMXポート
  • 9160  10.0.0.0/16 →Thrift API
  • 61621  10.0.0.0/16 →OpsCenterから各ノードへの情報取得用ポート


赤字が前回から新たに付与する設定です。


ここまでできたらOpsCenterを起動します。
# /etc/init.d/opscenterd start



ブラウザで確認


それでは、ブラウザでOpsCenterでUIを確認してみます。
app(OpsCenter)インスタンスにEIPをつけて、
http://インスタンスのEIP:8888/
を確認してみます。


おお!表示されました。
seedインスタンスのIPしか設定しませんでしたが、ノードが3つあることは理解されているようです。

しかし現時点では、情報が空のようです。
これは、cassandra側にOpsCenterのエージェントツールがないためです。


エージェントのインストール


次にエージェントをインストールします。
適切な設定がしてあると、OpsCenter側からcassandraにインストールすることが出来るようです。

タイトル部分の「Fix」というリンクをクリックします。



すると、以下のようなダイアログが開き、3つのノードのIPが表示されます。



ここで、「Edit Credentials」というリンクをクリックします。
すると、別のダイアログが更に開き、これらのインスタンスに入るためのユーザーとパスワードなどを聞かれます。


ここでは、rootユーザーか、もしくはsudoersに登録されているユーザーとパスワードを指定します。
sshがパスワードログインを許可していない場合は、「Private Key」に鍵ファイルのテキストをペーストします。
また、ここで登録された情報は永続化されることはない旨の説明が表示されています。
入力を終えたら「Done」ボタンをクリックします。

元のダイアログに戻るので、「Install on all nodes」ボタンをクリックします。
すると、SSHのフィンガープリントを確認されるので、「Accept Fingerprint」ボタンをクリックします。



エラー


しばらくすると、エラーダイアログが表示されてしまいました。



そこで、cassandra側を見てみます。
# ls -l /etc/init.d/ | grep ops
なにもインストールされていないようです。

インストールログがあるようなので、確認します。
# tail -1000f /var/log/opscenter-agent/installer.log
2013-02-13 23:12:09 +0900  Started: Wed Feb 13 23:10:26 2013 - 04:15 ago
2013-02-13 23:12:09 +0900  State  : Sleeping, pid: 25343
2013-02-13 23:10:27 +0900  Could not retrieve mirrorlist http://www.atomicorp.com/mirrorlist/atomic/centos-6-x86_64 error was
2013-02-13 23:10:27 +0900  12: Timeout on http://www.atomicorp.com/mirrorlist/atomic/centos-6-x86_64: (28, 'connect() timed out!')
2013-02-13 23:10:27 +0900  Could not retrieve mirrorlist http://mirrorlist.centos.org/?release=6&arch=x86_64&repo=os error was
2013-02-13 23:10:27 +0900  12: Timeout on http://mirrorlist.centos.org/?release=6&arch=x86_64&repo=os: (28, 'connect() timed out!')
2013-02-13 23:10:27 +0900  Could not get metalink https://mirrors.fedoraproject.org/metalink?repo=epel-6&arch=x86_64 error was
2013-02-13 23:10:27 +0900  12: Timeout on https://mirrors.fedoraproject.org/metalink?repo=epel-6&arch=x86_64: (28, 'connect() timed out!')
これは、VPCでEIPをつけずにyumインストールをするときによく見るログです。
OpsCenterはSSHでcassandraノードに入ってyumでエージェントをインストールしようとしているようです。


なので、エージェントをインストールするときだけ、各ノードにEIPを付与してあげます。



では同じ手順でリトライしてみます。
OpsCenterの画面で再度「Install on all nodes」をクリックします。



。。。。やはりエラーになります。


くわしい原因が書かれていないので、OpsCenterのログを見てみます。
# tail -1000f /var/log/opscenter/opscenterd.log
2013-02-13 23:30:41+0900 [Test_Cluster]  INFO: Beginning install of OpsCenter agent to 10.0.1.176
2013-02-13 23:30:42+0900 [Test_Cluster]  INFO: Installing rpm package on 10.0.1.176
2013-02-13 23:31:12+0900 [Test_Cluster]  WARN: HTTP request http://10.0.1.227:61621/cluster/datacenter?node_ip=10.0.1.176 failed: Connection was refused by other side: 111: Connection refused.
2013-02-13 23:31:12+0900 [Test_Cluster]  WARN: Unable to collect datacenter, rack information: Failed query to http://10.0.1.227:61621/cluster/datacenter?node_ip=10.0.1.176 : Connection was refused by other side: 111: Connection refused.
2013-02-13 23:31:12+0900 [Test_Cluster]  WARN: HTTP request http://10.0.1.227:61621/cluster/datacenter?node_ip=10.0.1.10 failed: Connection was refused by other side: 111: Connection refused.
2013-02-13 23:31:12+0900 [Test_Cluster]  WARN: Unable to collect datacenter, rack information: Failed query to http://10.0.1.227:61621/cluster/datacenter?node_ip=10.0.1.10 : Connection was refused by other side: 111: Connection refused.
2013-02-13 23:31:12+0900 [Test_Cluster]  WARN: HTTP request http://10.0.1.10:61621/cluster/datacenter?node_ip=10.0.1.227 failed: Connection was refused by other side: 111: Connection refused.


どうやら61621ポートでHTTPリクエストが拒否されているようです。
再度cassandra側を見てみます。
# ls -l /etc/init.d/ | grep ops
-rwxr-xr-x 1 opscenter-agent opscenter-agent  3197  7月  3 05:39 2012 opscenter-agent


今度はインストール自体はできているようです。
このファイルの中を見てみます。
# cat /etc/init.d/opscenter-agent
...
OPSC_ADDR_DIR="/var/lib/opscenter-agent/conf"
...


/var/lib/opscenter-agent/confがcassandra側のエージェント設定ディレクトリのようです。
confディレクトリにはaddress.yamlしかないので、中身を見てみます。
# cat /var/lib/opscenter-agent/conf/address.yaml
stomp_interface: "10.0.0.39"


ググってみたところ、OpsCenter側でSSLをOFFにしてある場合エージェント側でもHTTPSをHTTPに切り替える必要があるようで、use_sslを0に設定するといいようです。
そこで3つのcassandraノードに以下のように設定しました。
# cat /var/lib/opscenter-agent/conf/address.yaml
stomp_interface: "10.0.0.39"
use_ssl: 0



そして、再びOpsCenterの画面をみると、以下の状態になっているので同じ手順で再度「Install on all nodes」をクリックします。





すると、うまくいったようで、OpsCenterの画面が更新されて、情報が表示されるようになりました!





左ペインのメニューを適当にクリックしてみると、、、

CLUSTER:RING VIEW」:ハッシュの分散状態を確認できます。




CLUSTER:PHYSICAL VIEW」:ap-north-eastにまとまっていることがわかります。(Ec2Snitchの場合)




CLUSTER:LIST VIEW」:リスト状にノードが表示されます。




PERFORMANCE」:各ノードや全体平均のパフォーマンス統計情報が表示されます。




Data Modeling」:KeySpaceやColumnFamilyなど、データモデルを追加編集できたりします。




Data Explorer」:登録されているデータも見ることができます。




EVENT LOG」:クラスタに起こったイベントのログを確認できます。



EDIT CLUSTER...」:クラスタの簡単な編集もできるようです。



CLUSTER」の各ビューでは各ノードのアクション(CLEAR、DISCOMMISION、REPAIRなど)もできるようです。一部は有償のEnterprise版にしかできない機能もあるようです。


まとめ



cassandraクラスタの状態がかなり直感的にわかりとても便利そうです。
そして、ここまでできて無料というのはすごいです。

これから勉強しながらちょっとずつ利用してみたいと思います。


2013年2月12日火曜日

S3ってなんじゃ?(CloudFrontでアクセス制御:Origin Access Identity × 署名付きURL)

CloudFrontでアプリを通してのアクセス以外にはコンテンツを配信させたくないという場合があります。
ここでは、CDPデザインパターンに以下のようなパターンがありました。

CDP:Private Cache Distributionパターン


このパターンによると、CloudFrontに対して署名付きURLを送信することで、アクセス条件を制限することができるようです。今回はこれを使ってみたいと思います。



S3バケットの作成


まず、S3バケットをひとつ用意して適当にファイルをアップします。




ちなみにindex.htmlは以下の様な内容です。
<html>
  <head>
    <title>private</title>
  </head>
  <body>
    <h1>This is Private Contents</h1>
  </body>
</html>



CloudFrontディストリビューションの作成


次にCloudFrontでディストリビューションを追加します。「Create new Distribution」をクリックします。
すると、以下のようにディストリビューションの設定画面が表示されるので、以下のように入力します。



Origin Settings


  • Origin Domain Name:先ほど追加したS3バケットを選択
  • Origin ID:自動で設定されるので今回はこのまま
  • Restrict Bucket Access:Yes(YesにするとCloudFrontからしかS3にアクセスできないようにできます)
  • Origin Access Identity:Create a New Identity(S3にアクセスするこのCloudFrontの接続子です)
  • Comment:コメントです
  • Grant Read Permissions on Bucket:Yes(S3に対してCloudFrontからのBucketPolicyを設定します。)

Default Cache Behavior Settings


  • PathPattern:Default
  • View Protocol Policy:HTTP and HTTPS
  • Object Caching:Use Origin Cache Headers(Customizeにすると次の3項目が設定出来ます。)
  • Minimum TTL:0(最短TTL、デフォルト24時間)
  • Forward Cookies:None(キャッシュURLにCookieを含めることができます)
  • Whitelist Cookies:Forward Cookiesを有効にすると、Whitelist内のCookieだけをオリジンに転送することができます。
  • Forward Query Strings:No(YesにするとQueryParameter付きURLでキャッシュできます)
  • Restrict View Access:Yes(署名付きURLでしかアクセスできないようにします)
  • Trusted Signers:Self (Specify Accoutsにチェックを入れると別アカウントの署名も有効になります。)




Distribution Settings


ここの項目はひとまずデフォルトのままにしておきます。



ここまで設定できたら「Create Distribution」をクリックします。
すると以下のように、CloudFrontをprivateアクセスするために必要な次の手順が表示されます。


ざっくり訳すと以下のような内容です。

Step1:S3バケットへのアクセス制限

  •  ディストリビューションを新規作成したままS3の設定をいじっていないのであれば、S3バケットはCloudFrontとバケットオーナーからしかアクセスを受け付けないように正しく設定されています。

Step2:署名付きURL

  •  信頼された署名者用にCloudFrontのキーペアを作成します。
  •  署名付きURLを作成するには、コーディングするかサードパーティツールを使用する必要があります。
  •  ディストリビューションへ信頼された署名者を追加します。

どうやら自動でS3のパーミッションが変更されたようです。
S3のパーミッションを見てみます。
S3のバケットのPermissionsからEdit Bucket Policyをクリックします。
すると以下のように設定されています。

{
 "Version": "2008-10-17",
 "Id": "PolicyForCloudFrontPrivateContent",
 "Statement": [
  {
   "Sid": "1",
   "Effect": "Allow",
   "Principal": {
    "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E366VCXL4Q6CB9"
   },
   "Action": "s3:GetObject",
   "Resource": "arn:aws:s3:::memorycraft-private/*"
  }
 ]
}

Cloud Front Origin Access Identityとして設定されています。


また、作成されたディストリビューションを確認すると、以下のように無事登録されているのがわかります。
ディストリビューションのホスト名は
dapubd7a26puj.cloudfront.net
となっています。



この段階でブラウザでアクセスすると以下のようになります。


これは、すでに署名付きURLしか受け付けないため、通常のURLではエラーになるためです。



署名付きURLの作成


次に署名付きURLを作成してみたいと思います。

まず、署名をするためのCloudFront用のキーペアを取得します。
AWSの証明書のページで「一対の鍵」のタブをクリックします。


CloudFrontの一対の鍵という項目があるので、「新しい一対の鍵を作成する」をクリックします。


作成完了のモーダルウィンドウが表示されるので、確認して閉じます。


すると、「一対の鍵」にCloudFront用のキーペアが追加されているのがわかります。
「一対の鍵ID」というキーペアIDが表示され、下にダウンロードリンクが現れます。


「(公開鍵をダウンロード)」をクリックすると、rsa-キーペアID.pem, pk-キーペアID.pemというファイルがダウンロードされます。


どこか適当な環境にSDKをダウンロードします。
ここではPHPのSDKを利用します。
また、keysというディレクトリを作りpk-キーペアID.pemを配置します。
$tree -L 2 .

.
├── app
│   ├── key.php
│   ├── sdk -> sdk-1.6.0
│   ├── sdk-1.6.0
│   └── sdk-latest.zip
└── keys
    └── pk-キーペアID.pem


そして、以下のようにAmazonCloudFrontクラスを使って署名付きURLを生成します。
cat key.php

<?php
require_once('sdk/sdk.class.php');
date_default_timezone_set('Asia/Tokyo');

//CloudFrontクラスを初期化
$cf = new AmazonCloudFront(array('key'=>'通常のAWSアクセスキー', 'secret'=>'通常のAWSシークレットキー'));

//キーペアIDをセット
$cf->set_keypair_id('CloudFrontのキーペアID');

//配置した鍵ファイルの中身をセット
$cf->set_private_key(file_get_contents(dirname(__FILE__).'/../keys/pk-キーペアID.pem'));

//index.htmlのURLを期限付きで取得します(expireはstrtotimeが解釈できる文字列かUnixTimeならOK)
$url = $cf->get_private_object_url('dapubd7a26puj.cloudfront.net', 'index.html', strtotime('+5 minutes'));
echo $url;


確認


これを実行します。
# php app/key.php

http://dapubd7a26puj.cloudfront.net/index.html?Expires=1360671363&Key-Pair-Id=APKAIGNAXKEEXGUYEZ5A&Signature=UQantZ1gIp5-mm6d6Lb-HKQr1jxKDRW2NfFyC-E2dDx33ekBbFjLKmO6vHCOhv7liBrfcaTFc7~yKZCd4P1tfZE4TltU6TilhnAUFT6mCC-Db-dmnrU7XbcWSjt29~yOVVhLTv5RoPtIjoW~iPFR2BTyoM~4vlV40y9ypbO5M1U_


URLが出力されました。
これをつかってアクセスしてみます。




表示されました!

先ほどのコードでは5分後がアクセス期限でした。
5分以上経過してからアクセスすると、、、



ちゃんとアクセス拒否になりました。


おまけ


ユーザーがアクセスするごとにURLの期限を設定したい場合があります。
その場合は、以下のように元のURLにクエリーパラメータを付与してあげるといいかもしれません。
$url = $cf->get_private_object_url('dapubd7a26puj.cloudfront.net', 'index.html?u='.session_id(), strtotime('+5 minutes'));


以上の手順の多くはAWSのAPI経由でも設定可能です。必要に応じてプログラムで設定てもよいかもしれません。
また、署名の仕方や署名ポリシーにはいくつかパターンがあるため、機会があればもっといろいろな方法を試してみたいと思います。

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).


取得も成功しました。

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

2013年2月7日木曜日

Node.jsってなんじゃ?(Socket.IOとELBのまとめ)

過去の記事でnode.jsの話題をいくつか取り上げて来ましたが、node.jsではAmazonのELBと併用した時の問題があり、その注意点をまとめました。

Socket.IO


Socket.IOはnode.jsでwebsocketを使用するときのデファクトといっていいライブラリです。
他のSocket.IOが人気になったのは、以下のような利点のためです。

  • websocketをサポートしていないブラウザでは、自動的にxhrなどのポーリング使い通信できる
  • 接続に失敗しても再接続などを自動的に行う


ここではSocket.IOを使用する前提で、ELBを経由して複数のnodeサーバーをホストする場合についてまとめてみました。

インストールに関しては以下に書いてありますので割愛します。
WebSocketってなんじゃ?(Node編2 Socket.IOでプッシュ通信)

nodeサーバーには以下のようにファイルを配置します。上記の記事などで使用したチャットアプリです。
publicをhttpdのドキュメントルートにしておきます。
app
├── node
│   └── server.js
└── public
    ├── assets
    │   └── js
    │       └── client.js
    ├── health.txt
    └── index.html

public/health.txtはELB用のヘルスチェックファイルで、中身はありません。
その他の各ファイルの内容は以下の通りです。

server.js
index.html
client.js



ポートは3000番を利用します。
ここで、httpdを起動しておきます。
また、
node server.js

などで、nodeを起動しておきます。


AWS


単体構成



まず最初の例として、AWSは例として以下のような構成だとします。
nodeサーバーのEC2インスタンスはhtmlのホストとwebsocketの両方を行うので、80と3000のポートをセキュリティグループで開放しておきます。

画面を開いてみます。

普通に成功します。
通信をみてみると、websocketで通信されていることがわかります。


ELB


次に、nodeサーバーのインスタンスをもう一台追加し、新規作成したELB配下に2つのインスタンスをおきます。



ELBのリスナーを以下のように80番と3000番を設定します。


そしてELBのエンドポイントのURLをブラウザで開きます。


すると、xhr-pollingになり、接続と切断が繰り返されます。
また、画面を2つ開いてメッセージを送信しても相手に届かない場合があります。

注意点1:ELBはhttpではなくtcpでポートを設定


websocket通信ではクライアントとサーバーの間のハンドシェイクにUpgradeヘッダを送信します。
しかし、ELBはhttpリスナーの場合Upgradeヘッダを削ってしまうようです。
RFC6455 — The WebSocket Protocol 日本語訳
ELBでHTTPリスナーだとWebSocketは使えない

そのため、Socket.IOはwebsocketが使えないと判断し、次善策の一つとして通常のhttp通信でxhr-pollingで接続することになります。これはajaxの通信と同じです。
そこで、ELBでは上記リンクの通り、websocket用のリスナーはhttpの3000番ではなくtcpの3000番を設定する必要があります。



注意点2:Redisでセッション共有を行う


また、接続や切断が繰り返されるのはxhrのポーリングのたびにハンドシェイクが確立したサーバーとは別のサーバーに接続にいくからのようです。これはELBのリスナー設定をtcpにした場合も同様で、一度websocket通信が確立したかのように見えても、次に接続した時に別のサーバーにつながると、接続が切れたもしくはwebsocketに失敗したと判断し xhr-pollingなど他の方法で通信しようとするようです。

また、そもそもの問題として2つのnodeサーバーの間で接続情報(セッション)が共有されないので、ELBを通してnode1とnode2にそれぞれ接続したクライアント間ではメッセージのやりとりができません。

そこで、nodeサーバーのバックエンドとして、redisを利用してセッション共有を行う方法が有効です。
socket.ioはセッションを保持する方式としてローカルメモリを使用するMemoryStoreを使用しますが、オプションでRedisにを使用RedisStoreが選べます
Configuring Socket.IO

これを使います。
redisサーバーを追加し、redisを起動しておきます。
インスタンスにはセキュリティグループなどでredisで使用するポートを開放しておきます。



そして、以下のようにserver.jsを変更します。

server.js

これで、redisサーバーでnode1,node2のセッションを共有できます。


何度か試しましたが、うまく通信できているようです。


まとめ


注意点としては、ELBをつかった場合はtcpでリッスンすることと、ELBにかぎらずnodeサーバーをスケールする場合は、redisサーバーでセッション共有する必要があります。

以上です。

2013年2月5日火曜日

S3ってなんじゃ?(s3cmd syncではなくinotify+s3cmd sync (or put))

追記 2012/02/06:1ファイルずつなので普通にアップするだけならinotify + s3cmd putでいいかもと思い、題名変更しました。

以前に s3cmdの記事 を書きましたが、先日s3cmdで困った場面に遭遇したので、そのことを書きたいと思います。


問題



WEBサーバーからS3へのファイル同期にs3cmdを使ったプロジェクトがありました。
linuxのサーバーからS3へファイルを同期するのにs3cmdのsyncコマンドを使用しており、その同期がとても遅いというのです。ローカルで1ファイルを追加したとしても同期の度に数時間かかっているようでした。

状況を確認すると、同期対象のS3バケットの中には数百万ファイル存在していて、どうやらそれが問題のようです。
今回はそれを調べてみたいと思います。


再現


まず、手元のEC2環境でそれを再現してみます。
適当なディレクトリにgitからある程度のサイズのあるプロジェクトをいくつかcloneします。
# mkdir /root/dist
# cd /root/dist
# git clone https://github.com/mirrors/perl.git
# git clone https://github.com/apache/cassandra.git
# git clone https://github.com/v8/v8.git
# git clone https://github.com/symfony/symfony.git
# git clone https://github.com/torvalds/linux.git
# ls -lR | wc -l
72694


ファイル数がこの位あれば、ある程度のシミュレートができそうです。
s3cmdをインストールします。今回はyumで入れます。
# cd /etc/yum.repos.d
# curl -OL http://s3tools.org/repo/RHEL_6/s3tools.repo
# sed -i  -e s/enabled=1/enabled=0/ s3tools.repo
# yum install s3cmd --enablerepo=s3tools
# s3cmd --configure

configureの内容は前回と同じです。


次にテスト用のバケットを作成します。




それではsyncしてみます。
# /usr/bin/s3cmd sync -P /root/dist/ s3://memorycraft-sync/
.....

続々と同期のログが標準出力に吐かれていきますが、それが30分たっても全然終わる気配がありません。
まぁ7万ファイルもあるのだから仕方ない。結局全部同期させるのに1時間30分以上かかりました。

さらにその後、1ファイル追加して再度syncしてみます。
# echo "hoge" > /root/dist/symfony/src/Symfony/Component/Process/hoge.txt
# /usr/bin/s3cmd sync -P /root/dist/ s3://memorycraft-sync/
.....

まったく何の反応もありません。。。
--skip-existingをつけてみたら、3分ほどで同期が終了しました。
1ファイルの同期に3分かかるのでは使いものにならなそうです。原因を探ってみます。


調査


今回s3cmdはyumで入れてみましたが、結局ソースを落として見てみます。
# cd /usr/local/src/
# curl -OL http://ftp.jaist.ac.jp/pub/sourceforge/s/s3/s3tools/s3cmd/1.0.1/s3cmd-1.0.1.tar.gz
# tar xzvf s3cmd-1.0.1.tar.gz
# cd s3cmd-1.0.1
$ tree .
.
├── INSTALL
├── NEWS
├── PKG-INFO
├── README
├── S3
│   ├── ACL.py
│   ├── AccessLog.py
│   ├── BidirMap.py
│   ├── CloudFront.py
│   ├── Config.py
│   ├── Exceptions.py
│   ├── PkgInfo.py
│   ├── Progress.py
│   ├── S3.py
│   ├── S3Uri.py
│   ├── SimpleDB.py
│   ├── SortedDict.py
│   ├── Utils.py
│   └── __init__.py
├── s3cmd
├── s3cmd.1
├── setup.cfg
└── setup.py

中身をみてみると、S3配下がS3への接続とユーティリティ部分。s3cmdがコマンドのロジック的な部分のようです。

s3cmdをみてみると、各コマンドのメソッドがcmd_...となっており、ローカルからリモートのsyncはcmd_sync_remote2remoteというメソッドで処理されているようです。

def cmd_sync_local2remote(args):
  ##〜略〜
  s3 = S3(cfg)

  if cfg.encrypt:
    error(u"S3cmd 'sync' doesn't yet support GPG encryption, sorry.")
    error(u"Either use unconditional 's3cmd put --recursive'")
    error(u"or disable encryption with --no-encrypt parameter.")
    sys.exit(1)

  ## Normalize URI to convert s3://bkt to s3://bkt/ (trailing slash)
  destination_base_uri = S3Uri(args[-1])
  if destination_base_uri.type != 's3':
    raise ParameterError("Destination must be S3Uri. Got: %s" % destination_base_uri)
  destination_base = str(destination_base_uri)

  local_list, single_file_local = fetch_local_list(args[:-1], recursive = True)
  remote_list = fetch_remote_list(destination_base, recursive = True, require_attribs = True)                          
  ##〜略〜
                    
どうやらdst_list = fetch_remote_listというところで同期先S3バケットの何かのリストを再帰的に持ってきているようです。

さらにfetch_remote_listメソッドをみてみると、

def fetch_remote_list(args, require_attribs = False, recursive = None):
  ##〜略〜
  if recursive:
    for uri in remote_uris:
      objectlist = _get_filelist_remote(uri)
      for key in objectlist:
        remote_list[key] = objectlist[key]
  ##〜略〜


def _get_filelist_remote(remote_uri, recursive = True):
  ##〜略〜
  s3 = S3(Config())
  response = s3.bucket_list(remote_uri.bucket(), prefix = remote_uri.object(), recursive = recursive)
  ##〜略〜

S3.pyをみると、bucket_listは、特定パス配下のオブジェクトリストをS3のAPIで取得しているようです。 つまり、syncでは同期しようとしているフォルダ配下のオブジェクトすべてを取得しているということになります。 S3のオブジェクトリストの取得にはページネーションも発生するので、これでは配下にS3オブジェクトが数百万あれば、同期の前にファイルリストの取得の時点でものすごい時間がかかってしまいます。

対策


せめて変更があったファイルだけローカル→リモートで同期できたら、ということで、inotifyを使ってみました。
inotifyはファイルシステムのイベント監視のためのツールで、それを利用したinotify-toolsというものがあります。
ファイルの変更などを検知して、それをトリガーにプログラムを実行出来ます。

さっそくインストールします。

# cat /etc/yum.repos.d/dag.repo
[dag]
name=Dag RPM Repository for Red Hat Enterprise Linux
baseurl=http://ftp.riken.jp/Linux/dag/redhat/el$releasever/en/$basearch/dag
gpgcheck=1
gpgkey=http://ftp.riken.go.jp/pub/Linux/dag/RPM-GPG-KEY.dag.txt
enabled=0

# yum install inotify-tools
# yum -y --enablerepo=dag install inotify-tools


ここでは、inotify-toolsのinotifywaitというコマンドを使います。
inotifywaitはこのサイトで詳しく説明されていますが、指定したパス配下でイベントが起こると標準出力にイベントとそれが発生したファイルパスが出力されます。監視するイベントは指定することができます。

上のサイトにあるサンプルを参考にして、先ほどのソースファイル群のディレクトリの変更を検知してS3にsyncするようにしてみました。
ポイントは下記のとおりです。

  • 変更のあったファイルたちを5秒間バッファしてリスト化
  • ファイル追加時に属性変更のイベントも同時に発生したりするので、リストから重複を除去
  • S3でフォルダ内を検索しないように、s3cmd syncはファイル単位で1ファイルごとに呼び出し
  • 通常の呼び出しだとシリアルになってしまうので、&でs3cmdを非同期呼び出し


inotify.sh
#!/bin/sh
TIMEOUT=5
NOTIFYPATH=/root/dist/
BUCKETPATH=s3://memorycraft-sync/
/usr/bin/inotifywait -e create,delete,modify,move,attrib \
  -mrq $NOTIFYPATH | while [ 1 ]; do
  paths="";
  while read -t $TIMEOUT line; do
    path=`echo $line | /usr/bin/awk '{print $1$3}'`
    paths="${paths}$path@RET@"
  done
  if [ -n "$paths" ]; then
     echo $paths | sed 's/@RET@/\n/g' | sort | uniq | while read path; do
     if [ $path -a -f $path ]; then
       filename="`basename $path`"
       dpath="`dirname $path`/"
       rpath=`echo $dpath | sed "s:$NOTIFYPATH::"`
       if [ "$rpath" == $NOTIFYPATH ]; then
         rpath=""
       fi
       echo "/usr/bin/s3cmd sync -P $path $BUCKETPATH$rpath$filename"
       /usr/bin/s3cmd sync -P $path $BUCKETPATH$rpath$filename &
     fi
    done
  fi
done
※1ファイルずつ行うので、普通にアップするだけならs3cmd putで良いかもしれません。

これを実行します。
# cd  /root/bin/
# chmod 755 inotify.sh
# ./inotify.sh

別ターミナルで、監視配下の適当な場所にファイルをつくってみます。
# echo "moge" > /root/dist/symfony/src/Symfony/Component/Process/moge.txt

するとバッファタイムアウトで設定した5秒後にinotifyのターミナルで、同期が実行された旨のログがすぐ出力されました。

# ./inotify.sh
start 2013年  2月  5日 火曜日 19:11:04 JST
path=
end 2013年  2月  5日 火曜日 19:11:04 JST
path=/root/dist/symfony/src/Symfony/Component/Process/moge.txt
/usr/bin/s3cmd sync -P /root/dist/symfony/src/Symfony/Component/Process/moge.txt s3://memorycraft-sync/symfony/src/Symfony/Component/Process/moge.txt
end 2013年  2月  5日 火曜日 19:11:04 JST
/root/dist/symfony/src/Symfony/Component/Process/moge.txt -> s3://memorycraft-sync/symfony/src/Symfony/Component/Process/moge.txt  [1 of 1]
 5 of 5   100% in    0s    72.34 B/s  done
Done. Uploaded 5 bytes in 0.1 seconds, 70.15 B/s



S3をみると、無事にアップされています。


また1ファイルずつsyncするので、一度に追加や変更するファイル量が大量になる場合や空のバケットに対する初期同期の場合は通常のsyncの方が良いと思います。ただ、今回のように同期先のS3にファイルが大量にあったり、更新ファイル数がすくない場合などはinotifyで1つずつファイルを指定してsyncまたはputするほうがはるかに高速です。

S3の困った場面に遭遇する度に思いますが、やはりディレクトリ/ファイルを模したサービスなので、やはりファイルシステムとしての振る舞いを求めてしまうのが人情ってものかと思いました。

以上です。