概要
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を作成しました。
ここで、class
にSimpleStrategy
を指定しました。
他に指定できるものと、その意味は下のようになっています。
class | 意味 |
---|---|
SimpleStrategy | 1つのデータセンターと1つのラックを使うときに指定する。replication_factor はクラスターの中にいくつRowをコピーするかを指定します。 |
NetworkTopologyStrategy | 複数のデータセンターまたは複数のラックを使うときに指定します。replication_factor の代わりにデータセンター名とそのデータセンターのreplication_factorを指定します。 |
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';
色々と出てきました。順を追って見ていきます。
- Data Typeについて
- Primary KeyとClustering Orderについて
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 | う |
2019-03-25 00:00:000 | Work, work, work, work, work, work | |
2019-03-25 00:01:000 | Hahahahahahahaha | |
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 | う |
2019-03-25 00:00:000 | Work, work, work, work, work, work | |
2019-03-25 00:01:000 | Hahahahahahahaha | |
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
句を使うのような例外を除いて).
参考文献
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しか読めないという制限を考える必要があります。そのような制限が具体的にはどうなるのかは以下の記事が大変参考になりました。
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は使える機会が多いと思うので、これからも調べてまとめていきたいと思います。