Cassandraに入門した

概要

Facebookが開発したNoSQLデータベースであるCassandraに入門しました. テーブルの使い方とCRUDの使い方、その時の注意点を調べてまとめました。 そして、実際に手を動かして動作を確認しました。

事前準備

以下がすでにインストールされているとします。

  • Docker

環境構築

環境構築はDockerHubのCassandraのページを参考にして行いました。

Cassandraはネットワークを通じて他のノードと通信し、クラスタを作成します。 まず、Dockerのネットワークを作成します。ネットワークの名前はなんでも良いのですが、cassandra-nwとしました。

docker network create cassandra-nw

次に、Dockerのコンテナを削除してもデータが消えないようにローカルにデータを保存するディレクトリを作成します。

mkdir $HOME/cassandra
mkdir $HOME/cassandra2

2つのノードを作成する予定なので、2つのディレクトリを作成しました。

以下のコマンドを実行して、1つ目のCassandraのノードを作成します。

docker run --name cassandra1 --network cassandra-nw -v $HOME/cassandra:/var/lib/cassandra -p 9042:9042 cassandra:latest

次に、同じマシン上で2つ目のCassandraのノードを作成します。

docker run --name cassandra2 --network cassandra-nw -e CASSANDRA_SEEDS=cassandra1 -v $HOME/cassandra2:/var/lib/cassandra -p 9043:9042  cassandra:latest

2つ目のノードを作成する際、CASSANDRA_SEEDSという環境変数を用いてシードとなるノードを指定しました。シードノードは新しいノードをCassandraのリングに加える際、ノードのゴシッププロセスをブートストラップするために利用されるようです。各ノードとP2Pで通信し、シードノードが新しいノードの親となる訳ではないので、単一障害点を持たない分散DBとなっているようです。

以上の操作で、2つのノードからなるCassandraクラスタを作成することができました。

入門 - CLIでテーブルを作る

ここでは、MySQLでDatabaseに相当するKeyspaceを作成し、テーブルを作成し、CRUD(Create, Read, Update, Delete)を行います。 また、操作中に出てくる概念について調べてまとます。

CQLでクエリを叩く

Dockerを用いてcqlを起動するために、以下のコマンドを実行します。

docker run -it --network cassandra-nw --rm cassandra cqlsh cassandra1
Connected to Test Cluster at cassandra1:9042.
[cqlsh 5.0.1 | Cassandra 3.11.4 | CQL spec 3.4.4 | Native protocol v4]
Use HELP for help.
cqlsh>

cqlsh>と表示されていれば、CQLでクエリを実行する環境が作成できています。

Keyspaceを作る

まず、keyspaceという、MySQLなどでdatabaseに相当する概念を表示します。

cqlsh> DESCRIBE keyspaces

system_schema  system_auth  system  system_distributed  system_traces

次に、今回使用するKeyspaceを作成します。

cqlsh> CREATE KEYSPACE test WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }; 

cqlsh> DESCRIBE keyspaces

system_schema  system              test           system_auth
system_distributed  system_traces

testというKeyspaceを作成しました。 ここで、classSimpleStrategyを指定しました。 他に指定できるものと、その意味は下のようになっています。

class 意味
SimpleStrategy 1つのデータセンターと1つのラックを使うときに指定する。replication_factorはクラスターの中にいくつRowをコピーするかを指定します。
NetworkTopologyStrategy 複数のデータセンターまたは複数のラックを使うときに指定します。replication_factorの代わりにデータセンター名とそのデータセンターのreplication_factorを指定します。

https://docs.apigee.com/private-cloud/v4.17.09/about-cassandra-replication-factor-and-consistency-level

Tableを作る

Keyspaceを作成することができたので、次にテーブルを作成します。 Cassandraでは,テーブルをどのように作るかによって投げられるクエリーがほとんど決まってしまうので,テーブル作成が大変重要です.

Key Value Store では,カラムの値でソートしたデータを取得できないものが多いです. しかし,Cassandraでは予めテーブル作成時に決めたカラムの値でソートしたデータを取得することができます. ここでは,テーブル定義によってどのようなデータ構造でCassandraにデータが保持されるのか,そのときどのようなクエリーを投げることができて,どのようなクエリーを投げることができないのかを見ていきます.

まず、フィードというテーブルを作成します。 FacebookやTwitterのホーム画面に表示されるアレです。

cqlsh> CREATE TABLE test.feed (
    user_id  text,
    timestamp    timestamp,
    message text,
    primary key((user_id), timestamp)
) WITH CLUSTERING ORDER BY (timestamp DESC);

cqlsh> DESC test.feed;

CREATE TABLE test.feed (
    user_id text,
    timestamp timestamp,
    message text,
    PRIMARY KEY (user_id, timestamp)
) WITH CLUSTERING ORDER BY (timestamp DESC)
    AND bloom_filter_fp_chance = 0.01
    AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'}
    AND comment = ''
    AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'}
    AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'}
    AND crc_check_chance = 1.0
    AND dclocal_read_repair_chance = 0.1
    AND default_time_to_live = 0
    AND gc_grace_seconds = 864000
    AND max_index_interval = 2048
    AND memtable_flush_period_in_ms = 0
    AND min_index_interval = 128
    AND read_repair_chance = 0.0
    AND speculative_retry = '99PERCENTILE';

色々と出てきました。順を追って見ていきます。

  1. Data Typeについて
  2. Primary KeyとClustering Orderについて
  3. DESC test.feedでテーブル定義のWITHいかにある設定値について

Cassandraのデータタイプ

色々あります。見ればわかるのでいかにリンクを貼るだけにします. 文字列型の文字コードがUTF-8だったり、list, set, mapなどのコレクション型があるのが個人的に嬉しいです。

https://docs.datastax.com/en/cql/3.3/cql/cql_reference/cql_data_types_c.html

Primary KeyとClustering Keyについて

CassandraではMap<PartitionKey, SortedMap<ColumnKey, Value>>という形でデータを保持しています。 上で作成したフィードテーブルでは、Primary Keyとして((user_id), timestamp)、Clustering Keyとしてtimestamp DESCを指定しました。

これによりClustering Keyに指定されていないuser_idはPartition Keyとなります. Partition Key単位でノードにレコードが配置されます.

また,timestampがClustering Keyと指定していました.このClustering Keyを工夫して持つことによって,ソートされたレコードを取得できるようにしています. どのようなデータ構造でデータを持つことで,timestampによってソートできるようにしているのかをこれから見ていきます.

まずはじめに,Clustring Keyがなかった場合,どのようなデータの持ち方をするのかを見てみます. 以下のようなテーブルがあったとします.

cqlsh> CREATE TABLE test.feed (
    user_id  text,
    timestamp    timestamp,
    message text,
    primary key(user_id, timestamp)
);

このように,Clustring Keyが指定されていないとき,以下のようなフィードデータを保存したとします.

user_id timestamp message
letitbe_or_not 2019-03-25 00:00:000
letitbe_or_not 2019-03-25 00:01:000
letitbe_or_not 2019-03-25 00:02:000
twitter 2019-03-25 00:00:000 Work, work, work, work, work, work
twitter 2019-03-25 00:01:000 Hahahahahahahaha
twitter 2019-03-25 00:02:000 Go to sleep

このとき,CassandraのMap<PartitionKey, SortedMap<ColumnKey, Value>>というデータ構造に対して,データの持ち方のイメージは以下のようになっています.

{
    'letitbe_or_not:2019-03-25 00:00:00': {
        'message': 'あ'
    },
    'letitbe_or_not:2019-03-25 00:01:00': {
        'message': 'い'
    },
    'letitbe_or_not:2019-03-25 00:02:00': {
        'message': 'う'
    },
    'twitter:2019-03-25 00:00:00': {
        'message': 'Work, work, work, work, work, work'
    },
    'twitter:2019-03-25 00:01:00': {
        'message': 'Hahahahahahahaha'
    },
    'twitter:2019-03-25 00:02:00': {    
        'message': 'Go to sleep'
    }
}

この例だと,user_id+timestampがPartition Keyとなっていて,Column Keyはmessageというカラム名がキーになっています. この例からデータを取得することを考えると,Partition Keyはどちらも指定してクエリを投げる必要があると考えられます.そして,実際に指定する必要があります. messageというカラム名によってソートされていますが,ソートされていることで得られるメリットはありません.

次に,今回作成したフィードテーブルではどのようなレコードを取得できるのかを考えます. テーブル定義を再掲します.

cqlsh> CREATE TABLE test.feed (
    user_id  text,
    timestamp    timestamp,
    message text,
    primary key((user_id), timestamp)
) WITH CLUSTERING ORDER BY (timestamp DESC);

Clustring Keyとして,timestampが指定されています. Clustering Keyとして指定されたカラムはカラム名ではなく、カラムの値がColumnKeyとなります。そして,Clustring Keyの値は,他のPrimary Keyでないカラム名のプレフィックスとなります。 フィードの例ではClustering Keyの値がtimestampの値:messageとなります.

以下のデータを保存したとき,今回はどのようにCassandraがデータを持つのかを考えます.

user_id timestamp message
letitbe_or_not 2019-03-25 00:00:000
letitbe_or_not 2019-03-25 00:01:000
letitbe_or_not 2019-03-25 00:02:000
twitter 2019-03-25 00:00:000 Work, work, work, work, work, work
twitter 2019-03-25 00:01:000 Hahahahahahahaha
twitter 2019-03-25 00:02:000 Go to sleep

Cassandraでの持ち方イメージ

{
    'letitbe_or_not': {
        '2019-03-25 00:00:00': '',
        '2019-03-25 00:00:00:message': 'あ',
        '2019-03-25 00:01:00': '',
        '2019-03-25 00:01:00:message': 'い',
        '2019-03-25 00:02:00': '',
        '2019-03-25 00:02:00:message': 'う'
    },
    'twitter': {
        '2019-03-25 00:00:00': '',
        '2019-03-25 00:00:00:message': 'Work, work, work, work, work, work',
        '2019-03-25 00:01:00': '',
        '2019-03-25 00:01:00:message': 'Hahahahahahahaha',
        '2019-03-25 00:02:00': '',
        '2019-03-25 00:02:00:message': 'Go to sleep'
    }
}

この場合ではPartion Keyはuser_idの値となっています. 大きく変わったのはtimestampの値がすべてのColumn Keyに入っていることです.そして,Column Keyによってソートされていることで,timestampの値によって昇順にソートされていることが重要です. 前の例ではPrimary Keyのカラムを指定したクエリしか投げられませんでした.つまり,普通のKey Value Storeのような使い方しかできませんでした. 今回の例ではPartition Keyを指定し,Clustring Keyについてソートした結果を効率的に得ることができそうです. そして,実際にColumn Keyが連続しているレコードを取得することができ,連続して取得できるようなレコードのみを取得できます(SELECT句でフィールドを指定する,IN句を使うのような例外を除いて).

参考文献

https://qiita.com/mtakahashi-ivi/items/f9fab96675d1963be9a0

DESC test.feedでテーブル定義のWITHいかにある設定値について

あとでまとめます。

名前 意味
bloom_filter_fp_chance
caching
comment テーブルを説明するコメント
compaction
compression
crc_check_chance
dclocal_read_repair_chance
default_time_to_live
gc_grace_seconds Cassandraでは削除されたRowはまず論理削除されます。論理削除されたRowはTombstone(お墓)と呼ばれます。このお墓はSELECT時にスキャンされます。CassandraではスキャンするRow数に上限があるのですが、お墓のRowも含まれるため必要な結果を得る前にスキャンの上限に達して結果が得られないことがあります。gc_grace_secondsはお墓を物理削除する間隔を指定します。デフォルトでは10日となっています。
max_index_interval
memtable_flush_period_in_ms
min_index_interval
read_repair_chance
speculative_retry

https://docs.datastax.com/ja/cql-jajp/3.1/cql/cql_reference/tabProp.html

入門 - CLIでCRUD

今、testというKeyspaceとfeedというテーブルを作成できました。 ここでは、CRUD(CREATE, READ, UPDATE, DELETE)のやっていきます。

CREATE

ここでは、データの作成しについて見てみます。

cqlsh> INSERT INTO test.feed(user_id, timestamp, message) VALUES ('letitbe_or_not', '2019-03-25 00:00:000', 'あ');
cqlsh> INSERT INTO test.feed(user_id, timestamp, message) VALUES ('letitbe_or_not', '2019-03-25 00:01:000', 'い');
cqlsh> INSERT INTO test.feed(user_id, timestamp, message) VALUES ('letitbe_or_not', '2019-03-25 00:02:000', 'う');
cqlsh> INSERT INTO test.feed(user_id, timestamp, message) VALUES ('twitter', '2019-03-25 00:00:000', 'Work, work, work, work, work, work');
cqlsh> INSERT INTO test.feed(user_id, timestamp, message) VALUES ('twitter', '2019-03-25 00:01:000', 'Hahahahahahahaha');
cqlsh> INSERT INTO test.feed(user_id, timestamp, message) VALUES ('twitter', '2019-03-25 00:02:000', 'Go to sleep');

CREATEは特に難しいとことはないです。複数挿入はできるのかわかりませんでした。MySQLのような文法ではできませんでした。

https://docs.datastax.com/en/cql/3.3/cql/cql_reference/cqlInsert.html

READ

ここでは、データの読み取りについてみてみます。

cqlsh> SELECT * FROM test.feed;

 user_id        | timestamp                       | message
----------------+---------------------------------+------------------------------------
        twitter | 2019-03-25 00:02:00.000000+0000 |                        Go to sleep
        twitter | 2019-03-25 00:01:00.000000+0000 |                   Hahahahahahahaha
        twitter | 2019-03-25 00:00:00.000000+0000 | Work, work, work, work, work, work
 letitbe_or_not | 2019-03-25 00:02:00.000000+0000 |                                 う
 letitbe_or_not | 2019-03-25 00:01:00.000000+0000 |                                 い
 letitbe_or_not | 2019-03-25 00:00:00.000000+0000 |                                 あ

(6 rows)

READは難しいところしかないです。前節の「Tableを作る」で見たようなデータの持ち方をしている時、連続したColumnしか読めないという制限を考える必要があります。そのような制限が具体的にはどうなるのかは以下の記事が大変参考になりました。

https://qiita.com/mizuka/items/3fb7ce3a7e631f2f7b29

Cassandraの制限を緩和するALLOW FILTERING句があるのですが、フルスキャンが走るので実用的には使えなそうです。

また、IN句は連続していないColumnを読むことができますが、コレクションを取ってこれないという制限があるようです。

https://docs.datastax.com/en/cql/3.3/cql/cql_reference/cqlSelect.html

UPDATE

ここでは、データの更新についてみてみます。

cqlsh> UPDATE test.feed SET message='え' WHERE user_id = 'letitbe_or_not' AND timestamp= '2019-03-25 00:01:000';
cqlsh> SELECT * FROM test.feed;

 user_id        | timestamp                       | message
----------------+---------------------------------+------------------------------------
        twitter | 2019-03-25 00:02:00.000000+0000 |                        Go to sleep
        twitter | 2019-03-25 00:01:00.000000+0000 |                   Hahahahahahahaha
        twitter | 2019-03-25 00:00:00.000000+0000 | Work, work, work, work, work, work
 letitbe_or_not | 2019-03-25 00:02:00.000000+0000 |                                 う
 letitbe_or_not | 2019-03-25 00:01:00.000000+0000 |                                 え
 letitbe_or_not | 2019-03-25 00:00:00.000000+0000 |                                 あ

UPDATEは難しいところはないと思います。たぶん。

https://docs.datastax.com/en/cql/3.3/cql/cql_reference/cqlUpdate.html

DELETE

ここでは、データの削除についてみてみます。

cqlsh> DELETE FROM test.feed WHERE user_id = 'letitbe_or_not' AND timestamp = '2019-03-25 00:00:00';
cqlsh> SELECT * FROM test.feed;

 user_id        | timestamp                       | message
----------------+---------------------------------+------------------------------------
        twitter | 2019-03-25 00:02:00.000000+0000 |                        Go to sleep
        twitter | 2019-03-25 00:01:00.000000+0000 |                   Hahahahahahahaha
        twitter | 2019-03-25 00:00:00.000000+0000 | Work, work, work, work, work, work
 letitbe_or_not | 2019-03-25 00:02:00.000000+0000 |                                 う
 letitbe_or_not | 2019-03-25 00:01:00.000000+0000 |                                 え
 
cqlsh> INSERT INTO test.feed(user_id, timestamp, message) VALUES ('letitbe_or_not', '2019-03-25 00:01:000', 'い');
cqlsh> SELECT * FROM test.feed;

 user_id        | timestamp                       | message
----------------+---------------------------------+------------------------------------
        twitter | 2019-03-25 00:02:00.000000+0000 |                        Go to sleep
        twitter | 2019-03-25 00:01:00.000000+0000 |                   Hahahahahahahaha
        twitter | 2019-03-25 00:00:00.000000+0000 | Work, work, work, work, work, work
 letitbe_or_not | 2019-03-25 00:02:00.000000+0000 |                                 う
 letitbe_or_not | 2019-03-25 00:01:00.000000+0000 |                                 い

(5 rows)
cqlsh>  DELETE FROM test.feed WHERE user_id = 'letitbe_or_not' AND timestamp = '2019-03-25 00:01:00' IF message = 'え';

 [applied] | message
-----------+---------
     False |      い

cqlsh>  SELECT * FROM test.feed;

 user_id        | timestamp                       | message
----------------+---------------------------------+------------------------------------
        twitter | 2019-03-25 00:02:00.000000+0000 |                        Go to sleep
        twitter | 2019-03-25 00:01:00.000000+0000 |                   Hahahahahahahaha
        twitter | 2019-03-25 00:00:00.000000+0000 | Work, work, work, work, work, work
 letitbe_or_not | 2019-03-25 00:02:00.000000+0000 |                                 う
 letitbe_or_not | 2019-03-25 00:01:00.000000+0000 |                                 い

(5 rows)

PartitionKeyとClusteringKeyを指定して削除する、PartitionKeyを指定して、特定Partitionのデータを全て削除するという操作ができます。 さらにPartitionKeyとClusteringKeyを等号で指定して、IF句を用いることでPrimaryKeyでないカラムに対しても条件を指定して削除することができます。

DESC test.feedでテーブル定義のWITHいかにある設定値について」でも書いたのですが、削除ではまず論理削除されます。 論理削除されたRowがガベージコレクションにより物理削除されない場合、読み取り操作ではスキャンされる対象となるので注意が必要です。

終わりに

CassandraのKeyspace、テーブルの作り方とCRUDを見てきました。 水平スケールするハイパフォーマンスな分散DBであるCassandraは使える機会が多いと思うので、これからも調べてまとめていきたいと思います。

Share Comments