Apache Cassandraの探索 – 1編

(出典:Wikipedia

1.はじめに

今回はCassandraについて紹介したいと思います。最近はCassandraがそれなりにホットになっていて、勉強しながら経験した色々な問題点や特徴をみなさんにも共有したいと思います。また、Cassandraの詳しい内容は、DataStaxの公式文書やLearning Apache Cassandra, Mastering Apache Cassandra, Cassandra High Availabilityのような外国書籍をお勧めします。

2. Cassandraの特徴

Cassandraはscalabilityとhigh availabilityに最適化された、代表的な分散型Data storageです。Consistent hashingを利用したRing構造とGossip protocolを実装していることから、各ノード装置の追加・削除などが自由に操作できます。また、データセンターまで考慮できるデータレプリケーションポリシーを保有しており、信頼性の面でも大きなメリットがあります。Cassandraを利用すると、Shardingを検討する必要もなく、Master-Slaveのようなポリシーがなくても障害対応ができ、必要に応じて機器を増減したとしても大きなコストはかかりません。
とはいえ、Cassandraは完璧なソリューションではありません。すべてのものにtrade offがあるように、このような強力な機能が、逆に多くの欠点になることもあります。CassandraはJoinやTransactionに対応していないので、Indexなどの検索機能も非常に簡素で、さらには、Cassandraの構造上、RDBMSのようなPagingを実装することも難しいです。また、Keyspace(RDBMSのDBなど)やTableなどを過度に生成すると、Memory Overflowになることが予想されます。
このようなことから、Cassandraは既存のRDBMSの完全な代替品であるとは言えません。開発対象の製品の機能と特徴によって、Cassandraを使用するか、RDBMSを使用するか、慎重に決定する必要があるでしょう。

3. Cassandra Data Structure

(図1 Cassandra Data Layer)

Cassandraのデータ構造は比較的簡単です。最上位に論理Dataの保存場所であるKeyspaceがあり、Keyspaceの下にTableが存在します。Tableは多数のRowで構成され、各RowはKey-ValueからなるColumnで構成されます。RDBMSのDB-Table-Row-Columnの形態と同様な構造であることが分かります。しかもCassandraは、現在CQL(Cassandra Query Language)に対応しており、RDBMSを使用した経験があれば、大きな抵抗もなくCassandraが使えるでしょう。

(図2:Cassandra Ring. アルファベットはtoken範囲を意味する。 出典:Datastax Documents

まず、Cassandraを正しく使用するために、Cassandraがどのようにデータを保存しているか理解しましょう。
Cassandraは、基本的にRing構造を帯びています。そしてRingを構成する各ノードにDataを分散して保存します。このとき、Partition Keyと呼ばれる(Cassandra Data LayerではRow Keyと呼ばれる)データのhash値に基づいてDataを分散します。
はじめに各ノードがRingに参加すると、Cassandraのconf/cassandra.yamlに定義された設定によって、ノード別に固有のhash値の範囲を与えます。その上で、外部からdataのrequestが届くと、当該データのpartition key(Row key)のhash値を計算して、当該データがどのノードに保存されているかを把握してアクセスします。このように計算されたhashの値をCassandraではtokenと呼びます。
では次に、partition keyとRow keyについて確認しよう。両方の違いは、前者がCQLで使用される用語、後者がCassandra Data Layerで使用される用語です。さらに詳しくCassandraのHistoryを確認してみよう。

現在、CassandraはCQL(Cassandra Query Language)の使用を推奨していますが、CQLは最初から提供されたものではありません。初期のCassandraはThrift protocolを使ったclient APIを提供し、Cassandraに直接アクセスしたい場合は、bin/cassandra-cliユーティリティを使ってデータを確認することができました。このような既存のThrift基盤のAPIとcliユーティリティは、Cassandra Data layerをそのまま表現してくれましたが、Thriftが持つ様々なしきい値を共に保有していました。そのためCassandra 1.2バージョン以降では、Native Protocolを基盤にしたAPIとCQL文法が追加され、バージョン3.0から従来のThrift基盤のbin/cassandra-cliユーティリティはDeprecatedされて消えてしまいました。
またこの時期にCassandraには多くの変化がありました。Super ColumnというColumn内にColumn型を持つデータ構造がスペックから除外され、Collectionという新しい機能に代替されました。従来のColumn Familyと呼ばれるデータ構造は、Tableへと名称が変更されました。この過程で、CQLはこのように新しく構成されたCassandra Data Layerを抽象的に表現する文法で構成したことによって、実際のData Layerの用語とCQLでの表現が1:1にマッチングされず、変わることになったのです。

(図3:Ringに保存されているData)

初期のCassandra Data Structureは、上図のようにKeyspace> Column Family> Row> Column(Column Name + Column Value)形式で構成されていました。KeyspaceとColumn Familyは、すべてのCassandra nodeのmemoryに保存されます。実際のユーザーのデータが保存されるRowは、それぞれにRow keyを持ち、このhash値であるtokenを基準にして各ノードに分散保存されます。そしてRowに属するColumnは、Column nameを基準にソートされて保存されます。

(図4:Ringに保存さるData。Column FamilyがTableに変更された)

このような形態はCassandra 1.2から、Keyspace> Table> Row> Column(Column Name + Column Value)に名称が変わります。
しかし、このとき登場したCQLは、これをそのまま表現せずに一段階抽象化して表現します。

(図5:CQLのTable型。ここでのRowとColumnはCassandra Data LayerのRowとColumnを意味しない)

依然として同じ意味で使用されるKeyspace、Tableとは異なり、CQLにおけるRowとColumnは、実際のデータが保存されるCassandra Data LayerでのRow、Columnと意味が異なります。図から分かるように、CQLでのRowとColumnは、RDBMSのTuple、Attributeと類似しています。テーブルの行と列です。しかし、このように構成されたCQL Tableは、少なくとも1つ以上のColumnをprimary keyとして指定する必要があり、Cassandraはprimary keyに指定されたcolumnの中で、partition keyに指定されたcolumnのvalueを基準にしてデータを分散しています。

実際に、bin/cqlshユーティリティを使ったサンプルを見てみよう。

 CREATE TABLE test_keyspace.test_table_ex1 ( 
    code text, 
    location text, 
    sequence text, 
    description text, 
    PRIMARY KEY (code, location)
);

「test_keyspace」Keyspaceに「test_table_ex1」というTableを作成します。このとき「test_table_ex1」というTableは、それぞれ「code」、「location」、「sequence」、「description」というColumnを持ち、個々の型はすべてtextです。primary keyで「code」と「location」を指定しましたが、このときCQLの文法に基づいて、最初の「code」はpartition keyで、2番目の「location」は自動で「cluster key」が指定されます。(cluster keyとは、Cassandra Data LayerでRowの内部Columnの配置を担当するもので、詳細は後で説明します。)このようにtableが生成されたら、5つのデータを入力してデータを確認してください。

INSERT INTO test_keyspace.test_table_ex1 (code, location, sequence, description ) VALUES ('N1', 'Seoul', 'first', 'AA');
INSERT INTO test_keyspace.test_table_ex1 (code, location, sequence, description ) VALUES ('N1', 'Gangnam', 'second', 'BB');
INSERT INTO test_keyspace.test_table_ex1 (code, location, sequence, description ) VALUES ('N2', 'Seongnam', 'third', 'CC');
INSERT INTO test_keyspace.test_table_ex1 (code, location, sequence, description ) VALUES ('N2', 'Pangyo', 'fourth', 'DD');
INSERT INTO test_keyspace.test_table_ex1 (code, location, sequence, description ) VALUES ('N2', 'Jungja', 'fifth', 'EE');

Select * from test_keyspace.test_table_ex1;

5つのRowと4つのColumnからなるデータが正常に出力されました。
ではこのTableを、bin/cassandra-cliに出力すると、どうなるでしょうか?
(valueの値はbyteに変換されて保存されるので、以下のように整数で表現されます。)

use test_keyspace;
list test_table_ex1;

Using default limit of 100
Using default cell limit of 100
-------------------
RowKey: N1
=> (name=Gangnam:, value=, timestamp=1452481808817684)
=> (name=Gangnam:description, value=4242, timestamp=1452481808817684)
=> (name=Gangnam:sequence, value=7365636f6e64, timestamp=1452481808817684)
=> (name=Seoul:, value=, timestamp=1452481808814357)
=> (name=Seoul:description, value=4141, timestamp=1452481808814357)
=> (name=Seoul:sequence, value=6669727374, timestamp=1452481808814357)
-------------------
RowKey: N2
=> (name=Jungja:, value=, timestamp=1452481808833644)
=> (name=Jungja:description, value=4545, timestamp=1452481808833644)
=> (name=Jungja:sequence, value=6669667468, timestamp=1452481808833644)
=> (name=Pangyo:, value=, timestamp=1452481808829751)
=> (name=Pangyo:description, value=4444, timestamp=1452481808829751)
=> (name=Pangyo:sequence, value=666f75727468, timestamp=1452481808829751)
=> (name=Seongnam:, value=, timestamp=1452481808823137)
=> (name=Seongnam:description, value=4343, timestamp=1452481808823137)
=> (name=Seongnam:sequence, value=7468697264, timestamp=1452481808823137)

2 Rows Returned.
Elapsed time: 67 ms.

このときvalueに何もないColumnは無効なデータではなく、CassandraがCQLから入力されたデータを内部的に変換して使用するデータColumnです。したがって、この部分を除いてもう一度整理してみよう。

Using default limit of 100
Using default cell limit of 100
-------------------
RowKey: N1
=> (name=Gangnam:description, value=4242, timestamp=1452481808817684)
=> (name=Gangnam:sequence, value=7365636f6e64, timestamp=1452481808817684)
=> (name=Seoul:description, value=4141, timestamp=1452481808814357)
=> (name=Seoul:sequence, value=6669727374, timestamp=1452481808814357)
-------------------
RowKey: N2
=> (name=Jungja:description, value=4545, timestamp=1452481808833644)
=> (name=Jungja:sequence, value=6669667468, timestamp=1452481808833644)
=> (name=Pangyo:description, value=4444, timestamp=1452481808829751)
=> (name=Pangyo:sequence, value=666f75727468, timestamp=1452481808829751)
=> (name=Seongnam:description, value=4343, timestamp=1452481808823137)
=> (name=Seongnam:sequence, value=7468697264, timestamp=1452481808823137)

2 Rows Returned.
Elapsed time: 67 ms.

CQLとは異なり、単に2つのRowがあって、Row毎に属しているColumnの個数が違うことが分かります。
Cassandraは「N1」と「N2」をそれぞれHashingしてTokenを計算し、当該TokenがCassandraノードの中から範囲に対応するノードを見つけて、データをCRUDします。
CQLの出力画面と比較してprimary keyの組み合わせを変更し、他のケースをテストしてみると、次のような結論になります。

(1)CassandraはRow KeyのHash値を利用して、データを分散する。
(2)このとき、Cassandra Data LayerのRow key = CQL partition keyのvalue(複数のpartition keyならば、該当Column valueと” : “の組合せ)
(3)Cassandra Data LayerのColumn Name = CQL cluster keyのColumn valueと、primary keyに属さないColumn Nameと” : “の組合せ

4. Virtual Node

ここまでの内容で、ひとまずCassandraがどのようにデータを分散するか大体分かりましたね。ここからは、initial token、num tokens、virtual nodeについて紹介します。これらはCassandraのバージョンアップの過程で生じた用語です。

今までの内容を復習しよう。
ユーザーはCassandraにDataをCRUDします。このとき、当該データからPartition Keyに指定されたColumnのValueの組み合わせがRow Keyになり、このRow KeyをHashingしてtokenを計算した後、当該tokenの範囲に属するノードを見つけてCRUDします。ここでまず、ノード別にtokenの範囲が割り当てられているという前提条件が必要になります。

(図6:3 ReplicationポリシーでのRingとノードの構成。1つのノードで連続した3つのToken範囲に対する保存義務を負う。出典:Datastax Documents

すでに説明したように、CassandraはRing構造で構成されています。Cassandra初期では、手動またはスクリプトを用いてHash値の範囲を求め、Cassandraのノード別にconf/cassandra.yamlから「initial_token」というオプションに対応するHash値を指定する必要がありました。つまり、Cassandraが駆動する際、initial tokenに指定された値を読み込み、自分が担当するhash範囲を計算します。また特定ノードの追加や削除が必要な場合は、特定ノードにデータが集中しないように、手作業でinitial tokenを再計算し、conf/cassandra.yamlを更新した後、Cassandraを駆動する必要がありました。bin/nodetoolユーティリティのmove、remove、decommission、cleanupのようなコマンドを使って、手動で既存データをリバランシングを行います。そしてCassandra 1.2からは、virtual nodeと呼ばれる機能が追加されました。

(図7:3 ReplicationでのVirtual Ring. 1つのノードで多数の仮想ノードと仮想ノードが定められたtoken範囲の保存義務を負う。出典:Datastax Documents

virtual nodeは、1つのCassandraノードの中に数台の仮想ノードを設置して、細分化されたtokenの範囲を仮想ノードに割り当て、データを分散するという概念です。virtual nodeは、多数の仮想ノードを用いてデータをより均一に分散できる利点があります。また、データがすでに複数ノードに分散されているので、ノードの追加、削除の際のデータの移動、複製、リバランシングに高いパフォーマンスを発揮します。1つのノードに何台の仮想ノードを運営するか設定できるオプションが、conf/cassandra.yamlの「num_tokens」の項目です。virtual nodeを使用する場合、ノードの追加/削除に伴うtokenを毎回手動で作成し、オプションに入れる必要がありません。Ringに属するすべてのノードが自動的にgossip protocolを使ってtoken範囲を決定し、リバランシングまで処理してくれる、強力な機能です。

5. CQL key用語集

CQLはSQLと似通った形態をしているので、使用時に大きな困難はないでしょう。ここではCassandraで適切なデータモデリングを実現できるように、CQLの主要なkeyを簡単に紹介します。

(1)partition key
:CQL文法でCassandraにdataを分散保存するためのuniqueなkeyです。
partition keyは特定tableを構成するとき、1つ以上指定する必要があり、複数指定することもあります。
partition keyが1つだけなら、当該partition keyに指定されたCQL Columnのvalueが、実際のCassandra Data LayerのRow keyに保存されます。
partition keyが複数あれば、各partition keyに指定されたCQL Columnのvalueと” : “を組み合わせた値が、実際のCassandra Data LayerのRow keyに保存されます。

(2)cluster key
:ご存知のように、Cassandra Data LayerでRowに属するすべてのColumnは、常にソートされた状態で保存されます。
したがってcluster keyは、このような整列に対する基準の役割を持ちます。
CQLでcluster keyに指定されたCQL Columnのvalueは、残りのColumnのnameと” : “を組み合わせて、当該値をCassandra Data LayerのColumn Nameとして保存します。cluster keyが全くなければ、CQL ColumnのnameがそのままCassandra Data LayerのColumn Nameになります。

(3)primary key
:CQL tableにある各rowを一意に識別するための項目です。
primary keyは、少なくとも1つ以上のpartition keyと0以上のcluster keyで構成されます。

(4)composite key(= compound key)
:1つ以上のCQL Columnで構成されたprimary keyをcomposite keyあるいはcompound keyと呼びます。

(5)composite partition key
:2つ以上の多数のCQL Columnからなるpartition keyを意味します。

6. 1編を終えて

2編では1編の追加内容とCassandraのデータを読み書きするときに発生するプロセス、Cassandraで提供している機能、Cassandraでよく使われる機能や、回避すべきパターンなどについて紹介したいと思います。

TOAST Meetup 編集部

TOASTの技術ナレッジやお得なイベント情報を発信していきます
pagetop