NoSQLのCassandraをRDBのPostgreSQL感覚で触ってみた

こんにちは。技術部の髙岡です。

NoSQLのCassandraを初めて触ってみました。
RDBの経験はあるのですが、NoSQLを触ってみたのも初めてです。

CassandraやNoSQLについての説明はいろんなところに書いてあるので、記載しません。
ここではRDBのPostgreSQL感覚でCassandraを触るとどうかという観点で、Cassandraを簡単に検証した結果を書いてみたいと思います。Bigなデータを扱うに相応いのでしょうか?

導入

社内の検証用の仮想マシンを使いました。
とりあえず触ってみたいだけなので、1ノード構成です。

CPU:Intel(R) Xeon(R) CPU E5-2630 0 @ 2.30GHz
メモリ:1G
OS:CentOS 7.5

インストールはとても簡単でした。
yumのリポジトリにcassandraを手動で追加して、以下の2行を実行するだけ。

# yum install java
# yum install cassandra

cassandra-3.11.4-1をインストールしました。

起動停止もこれだけです。

# systemctl stop cassandra
# systemctl start cassandra

各種のパラメータはデフォルトのままです。

テーブル作成、挿入、更新

まずは、ログインしてみます。
PostgreSQLのpsqlなんかと同じ感覚でした。

# /usr/bin/cqlsh
Connected to Test Cluster at 127.0.0.1:9042.
[cqlsh 5.0.1 | Cassandra 3.11.4 | CQL spec 3.4.4 | Native protocol v4]
Use HELP for help.
cqlsh>

最初にキースペースを作成します。

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

作成できたかどうかを確認します。

cqlsh> SELECT * FROM system_schema.keyspaces ;

 keyspace_name      | durable_writes | replication
--------------------+----------------+-------------------------------------------------------------------------------------
        system_auth |           True | {'class': 'org.apache.cassandra.locator.SimpleStrategy', 'replication_factor': '1'}
      system_schema |           True |                             {'class': 'org.apache.cassandra.locator.LocalStrategy'}
                ks1 |           True | {'class': 'org.apache.cassandra.locator.SimpleStrategy', 'replication_factor': '1'}
 system_distributed |           True | {'class': 'org.apache.cassandra.locator.SimpleStrategy', 'replication_factor': '3'}
             system |           True |                             {'class': 'org.apache.cassandra.locator.LocalStrategy'}
      system_traces |           True | {'class': 'org.apache.cassandra.locator.SimpleStrategy', 'replication_factor': '2'}

(6 rows)

キースペースに接続します。

cqlsh> use ks1 ;

テーブルを作成して、データを挿入、検索、更新、削除してみます。
ここで使っているコマンドはSQLと同じですが、CQLというcassandra独自のコマンドです。

cqlsh:ks1> CREATE TABLE emp (empno int, ename text, deptno int,PRIMARY KEY (empno));
cqlsh:ks1> INSERT INTO emp (empno, ename, deptno ) values (001,'ttakaoka',10);
cqlsh:ks1> SELECT * FROM emp;

 empno | deptno | ename
-------+--------+----------
     1 |     10 | ttakaoka

(1 rows)
cqlsh:ks1> UPDATE emp SET deptno=20 WHERE empno=001 ;
cqlsh:ks1> DELETE FROM emp;
SyntaxException: line 1:15 mismatched input ';' expecting K_WHERE

全件削除はできないようです。

cqlsh:ks1> DELETE FROM emp WHERE empno=001;
cqlsh:ks1> SELECT * FROM emp;

 empno | deptno | ename
-------+--------+-------

(0 rows)

ここままでは、特に違和感がありませんでした。
しかし、この後説明する大量のデータロード、大量のデータ検索を実施したところ、RDB感覚では違和感を覚えたのです。

大量データのロードと検索

100万件のCSVを作成し、cassandraにCSVデータをロードしてみました。

# cat sampledata1000000.csv
1,"ttakaoka1",80
2,"ttakaoka2",80
・・・・
1000000,"ttakaoka1000000",80

CSVデータをロードするコマンドとして、PostgreSQLのCOPYコマンドそっくりのコマンドがありました。

cqlsh:ks1> COPY emp FROM 'sampledata1000000.csv' ;
Using 1 child processes

Starting copy of ks1.emp with columns [empno, deptno, ename].
Failed to import 1 rows: ParseError - Failed to parse takaoka$i : invalid literal for int() with base 10: 'takaoka',  given up without retries
・・・

エラーが出ました。
なぜ、int型で定義した列に、文字型のデータが挿入されようとしているのでしょうか?
csvは、

1,”ttakaoka1″,80

の順番で記載してるので、列の定義順と同じ筈です。

テーブルは、

 CREATE TABLE emp (empno int, ename text, deptno int,PRIMARY KEY (empno));

で作成したのです。

そこでテーブルの定義を調べてみました。
DESCRIBEという、どっかの商用DBにありそうな便利なコマンドがありました。

cqlsh:ks1> DESCRIBE TABLE emp;

CREATE TABLE ks1.emp (
    empno int PRIMARY KEY,
    deptno int,
    ename text
) WITH bloom_filter_fp_chance = 0.01
    AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'}
  ・・・・・

理由はわからないのですが、
CREATE TABLEで指定した
「empno int, ename text, deptno int」
の順でなく、
「empno int, deptno int, ename text」
の順で列が定義されていました。

理由はさておき、COPYコマンドで列の順番を指定して、再びロードしてみました。

cqlsh:ks1> COPY emp(empno,ename,deptno) FROM 'sampledata1000000.csv' ;
Using 1 child processes

Starting copy of ks1.emp with columns [empno, ename, deptno].
Processed: 770000 rows; Rate:    2731 rows/s; Avg. rate:    2733 rows/s
Processed: 775000 rows; Rate:    9222 rows/s; Avg. rate:    2747 rows/s
・・・・・
Processed: 1000000 rows; Rate:    4324 rows/s; Avg. rate:    3041 rows/s
1000000 rows imported from 1 files in 5 minutes and 28.880 seconds (0 skipped).

なんと、28秒もかかりました!
同じことをPostgreSQLで実行すると3秒程度で終わったので、正直驚きました。

PostgreSQLでの実行結果

testdb=# \COPY emp FROM 'sampledata1000000.csv' WITH csv
COPY 1000000
時間: 3290.005 ミリ秒(00:03.290)
testdb=# SELECT count(*) FROM emp;
  count
---------
 1000000
(1 行)

次に、ロードした100万件の行を検索してみました。

まずは、特定の1行の検索です。

cqlsh:ks1> tracing on
Now Tracing is enabled
cqlsh:ks1> SELECT * FROM emp WHERE empno=1000 ;

 empno | deptno | ename
-------+--------+--------------
  1000 |     80 | ttakaoka1000

(1 rows)

Tracing session: 58846210-7bc7-11e9-b826-d50d7f728563

 activity                                                                   | timestamp                  | source    | source_elapsed | client
----------------------------------------------------------------------------+----------------------------+-----------+----------------+-----------
                                                         Execute CQL3 query | 2019-05-21 21:52:52.718000 | 127.0.0.1 |              0 | 127.0.0.1
 Parsing SELECT * FROM emp WHERE empno=1000 ; [Native-Transport-Requests-1] | 2019-05-21 21:52:53.069000 | 127.0.0.1 |         480859 | 127.0.0.1
                          Preparing statement [Native-Transport-Requests-1] | 2019-05-21 21:52:53.106000 | 127.0.0.1 |         517759 | 127.0.0.1
                      Executing single-partition query on emp [ReadStage-3] | 2019-05-21 21:52:53.241000 | 127.0.0.1 |         652583 | 127.0.0.1
                                 Acquiring sstable references [ReadStage-3] | 2019-05-21 21:52:53.252000 | 127.0.0.1 |         663364 | 127.0.0.1
                                    Merging memtable contents [ReadStage-3] | 2019-05-21 21:52:53.268000 | 127.0.0.1 |         679132 | 127.0.0.1
                      Bloom filter allows skipping sstable 14 [ReadStage-3] | 2019-05-21 21:52:53.279000 | 127.0.0.1 |         690775 | 127.0.0.1
                                 Key cache hit for sstable 13 [ReadStage-3] | 2019-05-21 21:52:53.317000 | 127.0.0.1 |         728853 | 127.0.0.1
                       Read 1 live rows and 0 tombstone cells [ReadStage-3] | 2019-05-21 21:52:53.891000 | 127.0.0.1 |        1302018 | 127.0.0.1
                                                           Request complete | 2019-05-21 21:52:54.177143 | 127.0.0.1 |        1459143 | 127.0.0.1

1行目から10行目までのtimestampを見ると、2秒位で終わっています。
若干遅いと思いました。

遅いと思った理由は、同じことをPostgreSQLで実行しても1秒未満だったためです。

PostgreSQLでの実行結果

testdb=# \timing on
タイミングは on です。
testdb=# SELECT * FROM emp WHERE empno=1000;
 empno |    ename    | deptno
-------+-------------+--------
  1000 | takaoka1000 |     80
(1 行)

時間: 2.809 ミリ秒

次に、件数をカウントしてみました。

cqlsh:ks1> SELECT count(*) FROM emp ;

 count
---------
 1000000

(1 rows)

Warnings :
Aggregation query used without partition key


Tracing session: d7d99f70-7bc8-11e9-b826-d50d7f728563

Warnings :
Aggregation query used without partition key


Tracing session: d7d99f70-7bc8-11e9-b826-d50d7f728563

 activity                    | timestamp                | source    | source_elapsed | client
---------------------------------------------------------------------------------------------------------
        Execute CQL3 query | 2019-05-21 22:03:35.784000 | 127.0.0.1 |              0 | 127.0.0.1
                       ~中略~
   Submitting ・・・・・・ | 2019-05-21 22:04:04.133000 | 127.0.0.1 |       28432592 | 127.0.0.1

なんと、約30秒もかかってしまうではないですか!
同じことを、PostgreSQLで実行すると、1秒未満で終わったのです。

testdb=# SELECT count(*) FROM emp;
  count
---------
 1000000
(1 行)

時間: 199.518 ミリ秒

Cassandraのパラメータはデフォルトのままです。
ノードも一台だけなので、Cassandraのパワーを活かしきれていないのでしょうか。
どうすれば早くなるのかは、この後で調査・検証したいと思います。

ハマったこと

実は、全件の件数をカウントするSELECT文を実行したところ、最初はエラーが出まくって、なかなか成功しなかったのです。

出力されたエラーメッセージの例


  • ReadTimeout: Error from server: code=1200 [Coordinator node timed out waiting for replica nodes' responses] message="Operation timed out - received only 0 responses." info={'received_responses': 0, 'required_responses': 1, 'consistency': 'ONE'}
  • <stdin>:1:OperationTimedOut: errors={'127.0.0.1': 'Client request timeout. See Session.execute[_async](timeout)'}, last_host=127.0.0.1
  • OperationTimedOut: errors={'127.0.0.1': 'Client request timeout. See Session.execute[_async](timeout)'}, last_host=127.0.0.1
  • のような諸々エラーが出て、なかなか成功しませんでした。

    ググってみると、大量データのカウントで時間がかかるクエリーを実行して同じような目に会っている事例が結構ありました。
    いくつかのパラメータを調整してやっと成功するようになったのですが、調整した内容は次回のブログで整理して皆さんに報告したいと思います。

    それにしても、時間がかかるクエリーを実行するとエラーが出るなんて、ひどいですね。。

    課題

    大量データの件数カウントのようなクエリーが遅かった原因を調査して、解決策が見つかったら検証してみたいと思います。

    ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

    役に立った 役に立たなかった

    0人がこの投稿は役に立ったと言っています。

    コメントを残す

    メールアドレスが公開されることはありません。 が付いている欄は必須項目です