はじめに
2010年のはじめ、TwitterがApache Cassandra というJavaで実装された分散型のデータストアシステムを採用しつつあるというニュース が話題を呼びました。このことでCassandraは、NoSQLと呼ばれるシステムの中で最も注目を集めるものの一つになったと言えるでしょう。
2010年7月の時点で、Twitterは、位置情報のデータストレージ、トップツイート(トップページに表示される人気ツイート一覧)などのリアルタイム分析、データマイニング処理など、多くの用途でCassandraを活用しています 。また、Cassandraを生み出し、のちにApache Foundationに寄贈したFacebookでは、5億人規模・150Tバイト以上のデータ量を持つユーザメッセージの検索機能(Inbox Search )を、150ノードのCassandraクラスタで処理しています 。
本稿では、まずNoSQLとCassandraについて、その全体像や位置づけを概観します。次にRuby on Rails(以下Rails)からCassandraを利用する方法について解説します。最後にCassandraクラスタ環境の構築と運用方法について説明します。
NoSQL小史
まず、NoSQLの簡単な歴史を紹介しましょう。
RDBMSの限界
RDBMSは長年にわたって広く使われ、特に大規模なシステム開発においては必ずと言ってよいほど採用されています。しかしWebが社会のITインフラとして機能する時代に入ると、SNS(Social Networking Service )やEC(電子商取引)など世界規模で運営されるWebサイトでは、データ量や同時アクセス数の増大からRDBMSではパフォーマンスの問題が生じるようになってきました。
サーバを増やすことでリニアに性能をスケールできるフロントエンドのWebサーバと違い、バックエンドを引き受けるRDBMSは負荷分散の手法に制約があり、ボトルネックになることが多くなってきたのです。
GoogleのBigtableとAmazonのDynamo
そのような状況の中、2006年にGoogleのBigtable 、2007年にAmazonのDynamo と、2つの巨大Webサービス企業が、自社の開発した分散ストレージシステムについての技術論文を相次いで発表しました。両者は分散コンピューティング技術を基礎とし、データ量や処理の負荷を多数のサーバに分散できるスケールアウト可能なシステムという共通点を持っていました。
NoSQLという言葉の誕生
これらの論文は多くの人々に影響を与え、BigtableやDynamoの直接的なクローンや、アイデアを積極的に取り入れたシステムの開発プロジェクトが、数多く立ち上げられました。
そして2009年6月11日、サンフランシスコにおいて、それらを取り上げた「NOSQL meetup 」というイベントが開催されました。これが「NOSQL」というキーワードを世界に広めたきっかけだと言われています。
NOSQLは「SQL/RDBMSの否定」というネガティブなイメージを持つため、のちに「Not only SQL = NoSQL」と再定義され 、現在ではRDBMSと相補的な役割を持つシステムというニュアンスが強調されています。
NoSQLを理解する2つの軸
現在NoSQLとみなされる多くのシステムが存在します。それぞれ多様な特徴を持っており、どのように評価・分類すればよいのか難しいところです。そこで、NoSQLシステムの全体像の理解に役立つ2つの概念を紹介します。
CAP定理
エリック・ブリュワーが2000年に提唱した「CAP定理」( The CAP Theorem ) と呼ばれる概念があります[1] 。CAP定理は、ネットワーク上の分散システムは、次のC、A、Pの3つの要件のうち、同時に2つしか保証できない、というシステムデザイン上のトレードオフを論じたものです。
Consistency(整合性)
Availability(可用性)
Partition Tolerance(ネットワーク分割耐性)
Cの整合性は、各クライアントが同時に同じデータを読み出すと必ず同じ値が返されることを意味します。Aの可用性は、サービスが恒常的に提供される(停止しない)ことを意味します。Pのネットワーク分割耐性は、分散システム内のネットワークが分断された場合もサービスを継続できることを意味します。
C、A、Pの3要件のうち同時に2つしか保証できないとは、整合性、可用性、ネットワーク分割耐性の3つを兼ね備えたシステムは構築できないことを意味します。たとえば整合性と可用性を重視したシステムは、ネットワーク分割耐性に弱点を持ってしまうというのがCAP定理の主張です。
では、C、A、Pの組み合わせCA/AP/CPは、それぞれどのような処理に向いているのでしょうか。参考までに筆者の考えを表1 にまとめました。
表1 C、A、Pの各組み合わせに適した処理
種類 適した処理
CA(整合性、可用性) 銀行口座など、データの矛盾が許されないリアルタイム処理
AP(可用性、ネットワーク分割耐性) ショッピングカートなど、ダウンタイム短縮への要求が厳しいリアルタイム処理
CP(整合性、ネットワーク分割耐性) 検索エンジンのインデックス処理など、データの矛盾が許されない非リアルタイム処理
データモデル
NoSQLシステムがデータをどのようなモデルで取り扱うかは、大きく分類して表2 の3つの方向性にまとめられます。表2には、筆者が考えるそれぞれのデータモデルに適した用途も掲載しました。
表2 NoSQLのデータモデル分類と、適した用途
データモデル 説明 適した用途
キー・バリュー型 キーに1つの値を対応づけるシンプルなデータモデル キャッシュなど加工済みデータの保存
カラム指向 キーに対する値として{名前:値} の集合を扱うデータモデル ユーザアカウントなど多数のプロパティを持つオブジェクトの保存
ドキュメント指向 XMLやJSONなどのツリー構造を扱うデータモデル ディレクトリ・フォルダのような不定形で深い構造を持つデータの保存
2つの軸と各NoSQLシステムの位置づけ
米国の起業家ネイザン・ハーストは、NoSQLの各種システムをCAP定理とデータモデルの2つの観点から調査し、2010年3月に自身のブログエントリでわかりやすく図解しました。その図を和訳したものを図1 に掲載します。
Cassandraは、AP型(可用性とネットワーク分割耐性は備えるが、整合性は備えない)かつカラム指向なデータモデルのシステムと位置づけられています[2] 。表1と表2に挙げた適した処理・用途から、Cassandraは停止が許されない高いサービスレベルを要求される場面で、複雑なデータを管理するのに向いたシステムだと言えます。
図1 CAP定理と各NoSQLシステムの位置づけ
※Nathan Hurst, "Visual Guide to NoSQL Systems", 2010
結果整合性
AP型のNoSQLシステムでは、1つのクライアントから書き込まれた変更をすべてのクライアントから参照できるようになるまで、ある程度時間がかかる可能性があります。その間、データの整合性が守られていないことを、システムの要件として許容しているのです。このような弱い整合性のことを「結果整合性」( Eventual Consistency ) と呼び、NoSQLシステムにおいて中心的な概念の一つとされています。
CassandraとBigtable、Dynamo
CassandraはBigtableとDynamo双方の影響を強く受けたシステムです。どのような影響を受けているのかを見ていきましょう。
データモデル
Cassandraのデータモデルは「カラムファミリ」( Column Family )と呼ばれる構造を基本としています(図2 ) 。この構造はカラムファミリという名前も含めてBigtableから取り入れられたものですが、後述するように、より複雑なデータを扱えるように拡張されています。図2の一番外側の「キースペース」( Keyspace )はカラムファミリの属する名前空間を管理するものですが、これはCassandra独自の機構です。
図2 Cassandraのデータモデル
※http://d.hatena.ne.jp/terurou/20100411/1270912571 に掲載されている図の改訂版をterurouさんより提供いただき、許可を得て掲載
一方Dynamoは単純なキーと値のペアであるキー・バリュー型のデータモデルを採用しています。Cassandraのほうが、より高度なデータ管理機能を持っていると言えます。
キースペースとカラムファミリ
Cassandraではクラスタ内に複数のキースペースを定義することができ、RDBMSでいうデータベースと同じような役割を果たします。カラムファミリはRDBMSにおけるテーブルにあたります。キーとそれに紐づく行(Row )は、RDBMSでのプライマリキーや行と同じような位置づけになります。
カラムファミリの行部分は、プログラミング言語におけるHashMapや連想配列などのように、{名前:値} の組を複数格納するデータ構造です[3] 。1つの{名前:値} の組を「カラム」( Column )と呼びます。RDBMSとは異なり、カラム名を事前に定義する必要がなく、任意に追加や削除ができます。
カラムファミリのデータ構造は次のようなJSON形式で表現できます。
AddressBook = {
"yamada taro": {
"name": "山田太郎",
"address": "東京都品川区****",
"phone": "03-xxxx-xxxx"
},
"sato hanako": {
"name": "佐藤花子",
"address": "東京都墨田区****",
"mobile": "090-xxx-xxxx",
"email": "[email protected] "
}
};
この例ではAddressBookというカラムファミリを表現しています。山田さんの行にはmobile、emailというカラムがなく、佐藤さんの行にはphoneというカラムがありません。このように、行によってカラムが不ぞろいなデータ構造を表現できます。
スーパーカラム
AddressBookの例で示したデータ構造はBigtableとほぼ同様ですが、Cassandraではさらに「Super」と呼ばれるタイプのカラムファミリを定義できます。
AddressBook2 = {
"仕事": {
"yamada taro": {
"name": "山田太郎",
"address": "東京都品川区****",
"phone": "03-xxxx-xxxx"
}
},
"プライベート": {
"sato hanako": {
"name": "佐藤花子",
"address": "東京都墨田区****",
"mobile": "090-xxx-xxxx",
"email": "[email protected] "
}
}
};
AddressBook2では、データを「仕事」と「プライベート」に分け、ネストを一段深くしています。このような二重のハッシュ構造を「スーパーカラム」( Super Column )と呼びます。
分散アーキテクチャ
Cassandraの分散システムとしてのアーキテクチャを図3 に示します。クラスタ内の各ノードとなるサーバは論理的なリング(ノードを一定の順序で並べ、先頭ノードを末尾ノードの後続とみなして終端をなくす構造)を構成し、キーに対応するデータを分担して保持します。ノード同士はそれぞれ対等で、マスタやスレーブといった役割の違いはなく、お互いに通信し合って自律的に動作するP2P(Peer to Peer )型のシステムです。
このようなアーキテクチャは、リングの構成に関するアルゴリズムやノード同士の通信プロトコルなどの理論基礎を含め、すべてDynamoから受け継いでいます。
図3 Cassandraの分散アーキテクチャ
一方Bigtableは、クラスタ全体を統括する1台のマスタサーバと、データの実体をタブレットというファイルに分割し管理するタブレットサーバと呼ばれるサーバ群によって構成されています。
CassandraやDynamoはリング上のどのノードが障害を起こしても全体としては動き続けますが、Bigtableはマスタサーバがダウンするとクラスタ全体が止まってしまいます。これはSPOF(Single Point of Failure 、単一障害点)と呼ばれるアーキテクチャ上の弱点です。
ノードとトークン
Cassandraではノード同士はお互いに「Gossip 」と呼ばれるプロトコルで通信し、クラスタ内の各ノードの状態を共有します。
また、ノードはそれぞれトークン(Token )と呼ばれる固有番号を割り振られます。トークンはシステムによって自動で割り振ることも、管理者が手動で割り振ることもできます。トークンはリングの並び順や、ノードとキーの対応関係を決めるのに使われます。
パーティショニングとレプリケーション
Cassandraでは、データをキーに基づいて各ノードに分散して格納します。このことをパーティショニング(Partitioning )と呼びます。どのキーがどのノードの受け持ちになるかは、分散ハッシュテーブル(Distributed Hash Table, DHT )の一種であるコンシステントハッシュ法(Consistent Hashing ) と呼ばれるアルゴリズムによって決定されます。
また冗長化のために、データを別のノードにレプリケーションさせることができます。レプリカ(複製)の数を決めるパラメータをレプリケーションファクタ(Replication Factor )と呼び、レプリカ数+1(本来のデータ分)を指定します。
パーティショニングとレプリケーションはクラスタの構成を決める重要な要素で、Cassandraではデザインパターンの一つであるストラテジーパターンを使ってアルゴリズムを選択できるようになっています。詳しい設定方法については後述します。
Cassandraのアーキテクチャについて概観してきました。以降はCassandraを使用したシステム構築に必要な事柄について見てみましょう。
Cassandraのクライアントライブラリ
Thrift
Cassandraでは、クライアントプログラムとの通信にThrift というRPC(Remote Procedure Call 、遠隔手続き呼び出し)ライブラリを採用しています。ThriftもFacebookが開発しApache Foundationに寄贈したライブラリで、独自文法のIDL(Interface Description Language 、インタフェース定義言語)ファイルからさまざまなプログラミング言語のライブラリを生成できます(図4 ) 。Java、C++、C#、Perl、PHP、Python、Rubyなど、数多くの言語に対応しています。
図4 Thrift IDLから各種言語用ライブラリの生成
高水準ライブラリ
Thriftを使って生成される各種言語用のライブラリは、RPCのAPIに沿ってメッセージを組み立て、そのまま送受信するだけの、いわゆる低水準なものです。
より高水準な、たとえばクラスタ内の複数のノードに対して、接続のリトライやロードバランシングなどの機能を持つライブラリが各種言語用に開発されています(表3 ) 。そのほかのライブラリの情報については、CassandraプロジェクトのWiki にまとめられています。
表3 主な高水準ライブラリ
今後はThriftからAvroに移行
Cassandraプロジェクトでは、Thriftに関して次のような問題点があるとし、Apache Avro という別のRPCライブラリに移行していく方針を示しています。
開発やメンテナンスの活動があまり活発ではない
RPCのAPIが変わると各言語用のライブラリを生成しなおさなければならない
Avroは2009年にApacheプロジェクトに登録されたライブラリで、同じくApacheプロジェクトの分散データストアシステムHBase や、Dynamoを参考に開発された分散キー・バリューストアシステムVoldemort などでの採用が進められています 。
開発環境の構築
開発用ノードのインストール
開発環境で単体のCassandraプロセスを動作させることはそれほど難しくはありません。Java 6のランタイム環境JRE(Java Runtime Environment )さえそろっていれば、次の4つのステップでプロセスを立ち上げることができます。
Cassandraの公式サイトから最新リリースをダウンロードする
アーカイブを展開する
README.txtに従いデータディレクトリとログディレクトリを作成する
Cassandraをフォアグラウンドモードで起動する(-fオプション)
以下にREADME.txtから手順を抜粋します[4] 。「 $VERSION」はダウンロードしたバージョン番号に読み替えてください。
$ tar -zxvf apache-cassandra-$VERSION.tar.gz
$ cd apache-cassandra-$VERSION
$ sudo mkdir -p /var/log/cassandra
$ sudo chown -R `whoami` /var/log/cassandra
$ sudo mkdir -p /var/lib/cassandra
$ sudo chown -R `whoami` /var/lib/cassandra
起動と停止
Cassandraは、開発に必要なときだけ起動し、不要なときは停止しておくことができます。付属の設定ファイルは単体構成の開発用サーバを前提として記述されており、多くの場合そのままで動作させることができます。
-fオプションを指定してフォアグラウンドモードで起動したCassandraは、標準出力にログを出力しながら動作します。
$ apache-cassandra-$VERSION/bin/cassandra -f
停止は[Ctrl]+[C]で行います。
設定ファイル
Apache.orgが配布するCassandraのバイナリリリースでは、Cassandraの動作を決める設定ファイルは表4 のようになっています。レプリケーションファクタなどの設定項目はほぼすべて、1番めのstorage-conf.xmlに記述します。
表4 Cassandraの設定ファイル
ディレクトリ/ファイル名 設定内容
conf/storage-conf.xml(※ 参加するクラスタや接続先、格納するデータ構造のスキーマ、使用するディスク領域やバッファなど、Cassandraの動作に関わるほとんどを記述する
conf/passwd.properties クライアントからの接続にユーザ認証を行う場合に、ユーザ名とパスワードを記述する
conf/access.properties クライアントからの接続にユーザ認証を行う場合に、キースペースごとにアクセスを許可するユーザを記述する
conf/log4j.properties Cassandraのログファイルに関する設定を記述する
bin/cassandra.in.sh Cassandra起動時に必要な環境変数や、JVM(Java Virtual Machine 、Java仮想マシン)に渡すオプションを記述する
※ Cassandraバージョン0.7以降ではcassandra.yamlというYAML(YAML Ain't Markup Language )形式のファイルに変更され、スキーマ定義はAPIを通じた動的操作となる予定
Ruby on Railsでのプログラミング
筆者が所属するケイビーエムジェイの製品「パーソナライズド・レコメンダー 」では、一部の機能にCassandraを使用しています。その事例をベースに、Cassandraを使うコードを書いてみましょう。
筆者が動作検証したバージョンはそれぞれ、Ruby 1.8.7、Rails 2.3.8、Cassandra 0.6.3です。
例題「最近チェックしたアイテム」
Railsで実装されたショッピングモールアプリケーションに、Cassandraを使って「ユーザが最近チェックしたアイテム」を表示する機能を追加します。ユーザがアイテムの詳細ページにアクセスする際、Cassandraに履歴情報を記録します。またショップの各画面を表示する際、Cassandraから履歴情報を参照し、最新10件のアイテム情報を表示します(図5 ) 。
図5 ショッピングモールと履歴情報の保存
データ構造
まず履歴情報を格納するカラムファミリを定義します。名前はUserHistoryとします。Cassandraでは、カラムファミリの定義時にソート順を表す型を指定すると、読み出し時に必ずその順序で返されることがシステムによって保証されています(表5 ) 。
表5 カラムファミリの定義で指定可能なソートタイプ
タイプ 説明
BytesType 値によって単純にソートする。値の型によるチェックは行われない。指定しない場合これがデフォルトとなる
AsciiType BytesTypeと同じソートが行われるが、US-ASCIIとしてパースできないとインサートできない
UTF8Type UTF-8文字列としてソートする
LongType 64ビットlong値としてソートする
LexicalUUIDType 128ビットのUUID(Universal Unique ID)を、バイト値でソートする。UUIDはRFC-4122 で定義されている
TimeUUIDType version1と呼ばれる時間ベースのUUIDを、埋め込まれたタイムスタンプによって時間順にソートする
今回扱う情報は履歴データですので、TimeUUID Typeを使うと時系列でソートされた状態で取り出すことができます。バージョン0.6系列のCassandraでは、conf/storage-conf.xmlでスキーマを定義していますので、このファイルを編集して次の記述を追加します。
<ColumnFamily Name="UserHistory"
CompareWith="TimeUUIDType" />
属性Nameがカラムファミリの名前、属性CompareWithがソート順を意味します。ファイルを編集してCassandraを再起動すれば設定が反映され、UserHistoryが使用可能になります。
また、個々のユーザの履歴情報は、ショップのIDとユーザのIDの複合キーになることがわかっていますので、両者をデリミタ@で連結して1つのキーとします。データ構造をJSON形式で表現すると次のようになります。
UserHisotry = {
"user_id@shop_id": {
TimeUUIDのバイト表現: "item_id", ...
}, ...
};
cassandra gemのインストール
RubyからCassandraを使うためのライブラリは、少し紛らわしいですが表3で紹介したように「Cassandra」です。gemの名称が「cassandra」で、クラス名も「Cassandra」です。このライブラリはGitHubで開発が進められており、Twitterのエンジニアであるライアン・キングもプロジェクトに参加しています。
cassandraはgemコマンドでインストールできます[5] 。
$ sudo gem install cassandra
UserHistoryクラス
RailsからCassandraを利用するにあたっては、UserHistoryというシングルトンクラスを作成し、そのインスタンスを通じてアクセスすることにします。ここでは初期化部分の実装を説明します(リスト1 ) 。
リスト1 RAILS_ROOT/lib/user_history.rb(初期化部)
class UserHistory
include Singleton
def self.setup(options)
@@host = options.delete(:host)
@@keyspace = options.delete(:keyspace)
@@options = options
end
def initialize
@cassandra = Cassandra.new(@@keyspace, @@host, @@options)
end
end
UserHistoryでは、① でSingletonモジュールをインクルードし、② のクラスメソッドsetup()で初期化に必要なパラメータを受け取ります。インスタンス生成時に③ のinitialize()メソッドでCassandraクラスをnew()し、メンバ変数に保持します。
アプリケーションからは次のような記述でUserHistoryクラスのインスタンスを参照できます。
user_history = UserHistory.instance
Ruby on Railsへの組み込み
Railsの初期化ファイルenvironment.rbの中で、CassandraクラスとUserHistoryクラスを使えるように設定します(リスト2 ) 。
リスト2 RAILS_ROOT/cong/environment.rb(初期化部)
Rails::Initializer.run do |config|
config.gem "cassandra"
end
cassandra_options = {
:host => "127.0.0.1:9160",
:keyspace => "Keyspace1",
:timeout => 0.5
}
UserHistory.setup(cassandra_options)
まず① でcassandra gemを読み込みます。
次にCassandraのインスタンス生成に必要なパラメータを用意します。② で接続先としてローカルで動作するCassandraを指定します。9160はCassandraのデフォルトのポート番号です。③ で使用するキースペースを指定します。ここではリリースファイル添付のstorage-conf.xmlで定義されている「Keyspace1」を指定しています。
最後に④ でUserHistoryクラスの初期化メソッドを呼び出します。
履歴データの書き込み
チェックされたアイテムを1件Cassandraに記録するUserHistory#add()メソッドの実装について見てみます(リスト3 ) 。
リスト3 RAILS_ROOT/lib/user_history.rb(書き込み部)
class UserHistory
include Cassandra::Constants
def add(shop_id, user_id, item_id)
column = {SimpleUUID::UUID.new => item_id.to_s}
option = {:consistency => ONE}
@cassandra.insert(:UserHistory,
row_key(shop_id, user_id),
column, option)
end
private
def row_key(sid, uid)
uid.to_s + "@" + sid.to_s
end
end
④ の部分でCassandra#insert()メソッドを使って実際の書き込み処理を行っています。1番目の引数はカラムファミリで、文字列ではなくシンボルを与えることになっています。2番めのパラメータは行キーです。⑤ のrow_key()メソッドを使ってキーとなる文字列を生成しています。
カラムデータはRubyのハッシュで表現します。ソートタイプがTimeUUIDTypeなので、② のようにSimpleUUID::UUIDクラスのインスタンスを使います[6] 。
書き込み時の整合性レベル
Cassandraは結果整合性を持つシステムですが、書き込みや読み込みの際に、整合性のレベルを変化させることができます。
ここでは書き込み時の整合性レベルとして提供されているオプションを見てみます(表6 ) 。表6の下のほうにいくほど整合性のレベルは高くなりますが、その分処理速度は遅くなります。今回はRuby Cassandraのデフォルト値であるONEを指定してみましょう。Cassandra#insert()メソッドの4番めの引数にリスト3③ のようなハッシュを渡します。ONEという定数はリスト3① でインクルードしたモジュールによって使えるようになります[7] 。
なお、Cassandraでの書き込み処理は、ディスクへのシーケンシャルな書き出し(ジャーナリング)とメモリ上での記録が終わった時点で成功を返すため、非常に高速です。
表6 書き込み要求における整合性レベル
レベル 動作
ZERO 書き込み要求を受け取った時点で処理を返し、その後バックグラウンドで書き込み処理を行う。失敗は報告されない
ANY どこかで1回書き込みされたことが通知されるまで待ってから処理を返す。本来担当すべきノードがダウンしている場合は最寄りの別のノードがデータを一時保管し、本来のノードへの伝達はキューイングされる
ONE 1つのノードでの書き込み成功が通知されてから処理を返す(Ruby Cassandraでの省略時デフォルト)
QUORUM レプリケーションファクタ値の過半数(N/2+1)のノードが書き込みに成功するのを待ってから処理を返す
ALL レプリケーションファクタで指定されたすべてのノードの書き込みに成功するのを待ってから処理を返す
履歴データの読み込み
次に、履歴情報の読み込み処理を行うUserHistory #get()メソッドの実装について見てみます(リスト4 ) 。Cassandraから返ってくるデータは、デフォルトでは昇順に並んでいますが、パラメータによって降順にすることができます。今回必要なのは最新分からのデータですから、ここでは① のようにして降順を指定します。
④ のCassandra#get()メソッドの返り値は、Cassandra::OrderedHashクラス[8] のインスタンスです。Hashクラスと同等のAPIを持ち、順序を保持する性質を持っているため、⑤ のようにvalues()メソッドを使うとCassandraの返したとおりの順序でカラムの値を取り出すことができます。
リスト4 RAILS_ROOT/lib/user_history.rb(読み込み部)
class UserHistory
MAX_COUNT = 100
COUNT = 10
def get(shop_id, user_id)
options = {
:reversed => true,
:consistency => QUORUM,
:count => MAX_COUNT
}
columns = @cassandra.get(:UserHistory,
row_key(shop_id, user_id),
options)
item_ids = columns.values.map{|s| s.to_i}
return item_ids.uniq[0, COUNT]
end
end
読み込み時の整合性レベル
Cassandraの読み込み時の整合性レベルを表7に示します。先ほどと同様に、表7 の下のほうにいくほど整合性のレベルは高くなりますが、その分処理速度は遅くなります。
表7 読み込み要求における整合性レベル
レベル 動作
ONE 読み込み要求に対して最初にレスポンスされてきたデータを返す(Ruby Cassandraでの省略時デフォルト)
QUORUM レプリケーションファクタ値の過半数(N/2+1)からのレスポンスが返ってきた時点で、タイムスタンプが最も新しいデータを返す
ALL データを持つすべてのノードからのレスポンスを待ち、タイムスタンプが最も新しいデータを返す
Cassandraでは、次の式を満たすことで強い整合性を得ることができます。
W:書き込み(insert())時の整合性レベル
R:読み込み(get())時の整合性レベル
N:レプリケーションファクタ
リスト3ではinsert()メソッドでONEを指定しました。この場合W=1となり、get()メソッドの整合性レベルに、レプリケーションファクタが1ならばONE以上(N=1、R=1) 、2ならばQUORUM以上(N=2、R=N/2+1=2) 、3以上ならばALL(N=R≧3)を指定すると式を満たします。リスト4② ではレプリケーションファクタが2であるという想定で、QUORUMを指定しています。
なお、Cassandraでの読み込み処理はディスクのランダムアクセスを伴うため、書き込み処理よりも遅いという特性を持っています。
読み込み件数の制限
履歴情報が蓄積されるにつれて、行にあたるカラムデータは大きくなっていきます。巨大なデータの読み込みはサーバとクライアント双方のメモリを圧迫してしまうため、履歴の表示に不要なデータを読み込まないように制限します。表示すべき件数は10件ですが、履歴情報には重複があるため、それを見越した件数を読み込む必要があります。既存の統計データからユーザのアイテム詳細ページへの再訪率を調べて取得件数を決めます。今回の例題では、リスト4③ のように最大100件という値を指定しています。
これで、RailsアプリケーションからCassandraを使って「最近チェックしたアイテム」を読み書きするクラスが完成しました。なお、ここで解説したコードとRailsアプリケーションのひな形一式を、本誌サポートサイト からダウンロードできますので、参考にしてください。
クラスタ環境の構築と運用
ここまで開発環境として単体構成のCassandraを扱ってきました。以降では、複数ノードで構成するクラスタ環境の構築と運用について簡単に説明します。
クラスタの構築
cas1、cas2、cas3という3台のLinuxマシンを使い、3ノード、レプリカ数1のクラスタを構築します。Apache.orgにdebパッケージが用意されているため、Debian GNU/Linux系のディストリビューションではAPTコマンドでインストールできます。公式Wikiの記述 を参考にインストールしてください。インストールが完了するとCassandraが自動的に起動します。
続いて、/etc/cassandra/storage-conf.xmlを編集して次の項目を変更します。
ReplicationFactorを2に
Seedの項目を3つに増やしてそれぞれの値をcas1、cas2、cas3に
ListenAddressを各サーバのホスト名に
ThriftAddressを0.0.0.0に
次に、/etc/default/cassandraというDebian GNU/Linux独自の設定ファイルを編集して、JVMの最大ヒープサイズを変更します。JVM_MAX_MEM="1G"
を、メモリ搭載量に合わせて4G~16G程度のサイズにしてください。
パーティショナの設定
行キーからそのデータを格納する担当ノードを決定するためのアルゴリズムはパーティショナ(Partitioner )と呼ばれます。Cassandraにはパーティショナとして表8 のクラスが用意されています。
表8 Cassandraで用意されているパーティショナ
クラス名 説明
RandomPartitioner 行キーのMD5値をもとに対応するノードを決める
OrderPreservingPartitioner 行キーの値をそのまま使って対応するノードを決める
CollatingOrderPreservingPartitioner 上記と同じだが並び順をバイトではなくアルファベット辞書順として扱う
※ ユーザが独自に実装したクラスも指定可能
RandomPartitionerは各ノードにデータがランダムに分配されます。残り2つのパーティショナでは行キーの範囲を指定して複数の行データをまとめて取得する、レンジスキャンと呼ばれる便利な操作を行うことができますが、データを偏りなく各ノードに分配するためには、各ノードのトークンを適切に設定する必要があります。レンジスキャンを必要とする要件が特にない場合、RandomPartitionerをお勧めします。
パーティショナはstorage-conf.xmlに設定します。Partitioner項目にクラス名を記述してください。
レプリカ配置戦略の設定
担当ノードからデータの複製先ノードを決定するレプリカ配置戦略(Replica Placement Strategy )のアルゴリズムも設定できます。Cassandra 0.6系列で用意されているクラスは表9 のとおりです。
表9 Cassandraで用意されているレプリカ配置戦略
クラス名 説明
RackUnawareStrategy トークン値に基づいてリング上の後続するノードにレプリカを配置する
RackAwareStrategy リング上の後続するノード、異なるデータセンターで稼働するノード、同一データセン
ター内の異なるラックで稼働するノードの順にレプリカを配置する
※ ユーザが独自に実装したクラスも指定可能
レプリカ配置戦略もstorage-conf.xmlに設定します。ReplicaPlacementStrategy 項目にクラス名を記述してください。
以上の設定が終わったら、次のコマンドでCassandraを再起動します。
$ sudo /etc/init.d/cassandra restart
運用ツール
Cassandraには、nodetoolというコマンドラインベースの管理ツールが付属しています。各種ステータスの確認やリング上のデータの再配置、ノードの削除など、クラスタの管理に関するさまざまな操作が行えます(表10 ) 。また、JMX(Java Management Extensions )の標準的なGUIツールであるJConsoleからも同等のオペレーションが行えます。JConsoleはJDK(Java Development Kit )に付属しています。
表10 nodetoolの主な管理用コマンド
コマンド 機能
ring クラスタのリング構成と各ノードのトークン値を表示
tpstats ノード内の各スレッドの統計情報を表示
cfstats 各カラムファミリの統計情報を表示
info ノードの稼働情報を表示
まだ筆者のプロジェクトでは検証していませんが、監視ツールMunin のプラグイン もあるようです。
おわりに
駆け足になりましたが、NoSQLとその一実装であるCassandraについて、概要から実践までを解説しました。CassandraはRDBMSとはまったく異なる概念で動いていることもあり、実際に触って動かしてみないと実感できない部分も多いでしょう。公式Wikiの日本語への翻訳 や勉強会の開催など、国内でのコミュニティ活動も立ち上がりつつある今、みなさんも積極的に取り組んでみてはいかがでしょうか。
最後に、本記事の執筆にあたっては、大谷晋平さん、冨田和孝さん、桑野章弘さん、terurouさんにレビューして頂きました。お忙しい中ご協力くださった皆様に御礼申し上げます。