Cassandra実践入門―Twitter、Facebookが採用するNoSQLシステム

はじめに

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インフラとして機能する時代に入ると、SNSSocial Networking ServiceやEC(電子商取引)など世界規模で運営されるWebサイトでは、データ量や同時アクセス数の増大からRDBMSではパフォーマンスの問題が生じるようになってきました。

サーバを増やすことでリニアに性能をスケールできるフロントエンドのWebサーバと違い、バックエンドを引き受けるRDBMSは負荷分散の手法に制約があり、ボトルネックになることが多くなってきたのです。

GoogleのBigtableとAmazonのDynamo

そのような状況の中、2006年にGoogleのBigtable2007年に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システムの位置づけ
図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のデータモデル
図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に示します。クラスタ内の各ノードとなるサーバは論理的なリング(ノードを一定の順序で並べ、先頭ノードを末尾ノードの後続とみなして終端をなくす構造)を構成し、キーに対応するデータを分担して保持します。ノード同士はそれぞれ対等で、マスタやスレーブといった役割の違いはなく、お互いに通信し合って自律的に動作するP2PPeer to Peer型のシステムです。

このようなアーキテクチャは、リングの構成に関するアルゴリズムやノード同士の通信プロトコルなどの理論基礎を含め、すべてDynamoから受け継いでいます。

図3 Cassandraの分散アーキテクチャ
図3 Cassandraの分散アーキテクチャ

一方Bigtableは、クラスタ全体を統括する1台のマスタサーバと、データの実体をタブレットというファイルに分割し管理するタブレットサーバと呼ばれるサーバ群によって構成されています。

CassandraやDynamoはリング上のどのノードが障害を起こしても全体としては動き続けますが、Bigtableはマスタサーバがダウンするとクラスタ全体が止まってしまいます。これはSPOFSingle Point of Failure単一障害点)と呼ばれるアーキテクチャ上の弱点です。

ノードとトークン

Cassandraではノード同士はお互いにGossipと呼ばれるプロトコルで通信し、クラスタ内の各ノードの状態を共有します。

また、ノードはそれぞれトークンTokenと呼ばれる固有番号を割り振られます。トークンはシステムによって自動で割り振ることも、管理者が手動で割り振ることもできます。トークンはリングの並び順や、ノードとキーの対応関係を決めるのに使われます。

パーティショニングとレプリケーション

Cassandraでは、データをキーに基づいて各ノードに分散して格納します。このことをパーティショニングPartitioningと呼びます。どのキーがどのノードの受け持ちになるかは、分散ハッシュテーブルDistributed Hash Table, DHTの一種であるコンシステントハッシュ法Consistent Hashingと呼ばれるアルゴリズムによって決定されます。

また冗長化のために、データを別のノードにレプリケーションさせることができます。レプリカ(複製)の数を決めるパラメータをレプリケーションファクタReplication Factorと呼び、レプリカ数+1(本来のデータ分)を指定します。

パーティショニングとレプリケーションはクラスタの構成を決める重要な要素で、Cassandraではデザインパターンの一つであるストラテジーパターンを使ってアルゴリズムを選択できるようになっています。詳しい設定方法については後述します。


Cassandraのアーキテクチャについて概観してきました。以降はCassandraを使用したシステム構築に必要な事柄について見てみましょう。

Cassandraのクライアントライブラリ

Thrift

Cassandraでは、クライアントプログラムとの通信にThriftというRPCRemote Procedure Call遠隔手続き呼び出し)ライブラリを採用しています。ThriftもFacebookが開発しApache Foundationに寄贈したライブラリで、独自文法のIDLInterface Description Languageインタフェース定義言語)ファイルからさまざまなプログラミング言語のライブラリを生成できます図4⁠。Java、C++、C#、Perl、PHP、Python、Rubyなど、数多くの言語に対応しています。

図4 Thrift IDLから各種言語用ライブラリの生成
図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のランタイム環境JREJava Runtime Environmentさえそろっていれば、次の4つのステップでプロセスを立ち上げることができます。

  1. Cassandraの公式サイトから最新リリースをダウンロードする
  2. アーカイブを展開する
  3. README.txtに従いデータディレクトリとログディレクトリを作成する
  4. 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.propertiesCassandraのログファイルに関する設定を記述する
bin/cassandra.in.shCassandra起動時に必要な環境変数や、JVMJava Virtual MachineJava仮想マシン)に渡すオプションを記述する

Cassandraバージョン0.7以降ではcassandra.yamlというYAMLYAML 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 ショッピングモールと履歴情報の保存
図5 ショッピングモールと履歴情報の保存

データ構造

まず履歴情報を格納するカラムファミリを定義します。名前はUserHistoryとします。Cassandraでは、カラムファミリの定義時にソート順を表す型を指定すると、読み出し時に必ずその順序で返されることがシステムによって保証されています表5⁠。

表5 カラムファミリの定義で指定可能なソートタイプ
タイプ説明
BytesType値によって単純にソートする。値の型によるチェックは行われない。指定しない場合これがデフォルトとなる
AsciiTypeBytesTypeと同じソートが行われるが、US-ASCIIとしてパースできないとインサートできない
UTF8TypeUTF-8文字列としてソートする
LongType64ビットlong値としてソートする
LexicalUUIDType128ビットのUUID(Universal Unique ID)を、バイト値でソートする。UUIDはRFC-4122で定義されている
TimeUUIDTypeversion1と呼ばれる時間ベースの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回書き込みされたことが通知されるまで待ってから処理を返す。本来担当すべきノードがダウンしている場合は最寄りの別のノードがデータを一時保管し、本来のノードへの伝達はキューイングされる
ONE1つのノードでの書き込み成功が通知されてから処理を返す(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 + R > N
  • 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⁠。また、JMXJava Management Extensionsの標準的なGUIツールであるJConsoleからも同等のオペレーションが行えます。JConsoleはJDKJava Development Kitに付属しています。

表10 nodetoolの主な管理用コマンド
コマンド機能
ringクラスタのリング構成と各ノードのトークン値を表示
tpstatsノード内の各スレッドの統計情報を表示
cfstats各カラムファミリの統計情報を表示
infoノードの稼働情報を表示

まだ筆者のプロジェクトでは検証していませんが、監視ツールMuninプラグインもあるようです。

おわりに

駆け足になりましたが、NoSQLとその一実装であるCassandraについて、概要から実践までを解説しました。CassandraはRDBMSとはまったく異なる概念で動いていることもあり、実際に触って動かしてみないと実感できない部分も多いでしょう。公式Wikiの日本語への翻訳や勉強会の開催など、国内でのコミュニティ活動も立ち上がりつつある今、みなさんも積極的に取り組んでみてはいかがでしょうか。

最後に、本記事の執筆にあたっては、大谷晋平さん、冨田和孝さん、桑野章弘さん、terurouさんにレビューして頂きました。お忙しい中ご協力くださった皆様に御礼申し上げます。

おすすめ記事

記事・ニュース一覧