Scalar DB on CockroachDB を試してみた
分散 Tx マネージャーである Scalar DB は Storage として複数の DB をサポート していますが、その中に PostgreSQL も含まれています。
PostgreSQL がサポートされているということは、Scalar DB と PostgreSQL が「PostgreSQL のプロトコルを使って通信している」ということです (具体的には JDBC ドライバを使っています)。
そして、PostgreSQL のプロトコルで通信しているということは、PostgreSQL とプロトコル互換のある CockroachDB でもいける可能性があるということです!!!
ということで、Scalar DB on CockroachDB を試してみました。
注意
前述した通り、Scalar DB にて PostgreSQL はサポート されていますが、CockroachDB はサポートされていない DB です。今回の内容はあくまで趣味の範囲で検証したものなので、あしからず。
環境
今回は以下の環境で検証しています。
バージョン
Ubuntu : 20.04
OpenJDK : 11
Docker : 20.10.12
Scalar DB : 3.5.0
CockroachDB : 21.2.5
Container Image (CockroachDB) : cockroachdb/cockroach
Scalar DB の使い方
Scalar DB には「Java のライブラリとして使う方法」と「gRPC サーバーとして使う方法」の 2種類があります。
- Pattern 1 (As a Java Library)
+----------------------+ | Java App | | +------------------+ | +----------------------+ | | Scalar DB |--------------------------------->| Backend DB | | +------------------+ | | (PostgreSQL etc...) | +----------------------+ +----------------------+
- Pattern 2 (As a gRPC Server)
+----------------------+ | App | | +------------------+ | +----------------------+ +----------------------+ | | Scalar DB Client |----->| Scalar DB Server |--->| Backend DB | | +------------------+ | | (gRPC Server) | | (PostgreSQL etc...) | +----------------------+ +----------------------+ +----------------------+
今回は「Java のライブラリとして使う」方で検証します。
CockroachDB の準備
最初に、Scalar DB の Storage として利用する CockroachDB を準備します。
クラスタ構築 (3匹構成)
本題からそれてしまうので構築方法の詳細は割愛しますが、今回は以下のような 3匹構成の CockroachDB クラスタ (Docker を利用したローカルの Secure クラスタ) を使います。cockroach-1 が 127.0.0.1:26257 でクライアントからの接続を LISTEN しているので、Scalar DB からはこの 127.0.0.1:26257 に接続します。
- コンテナ
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES f1726fd20c6d cockroachdb/cockroach:v21.2.5 "/cockroach/cockroac…" 46 minutes ago Up 46 minutes 26257/tcp, 127.0.0.1:8083->8080/tcp cockroach-3 be0584fde8e6 cockroachdb/cockroach:v21.2.5 "/cockroach/cockroac…" 46 minutes ago Up 46 minutes 26257/tcp, 127.0.0.1:8082->8080/tcp cockroach-2 37d8c9c42790 cockroachdb/cockroach:v21.2.5 "/cockroach/cockroac…" 46 minutes ago Up 46 minutes 127.0.0.1:26257->26257/tcp, 127.0.0.1:8081->8080/tcp cockroach-1 7e918c6d4e29 cockroachdb/cockroach:v21.2.5 "tail -f /dev/null" 46 minutes ago Up 46 minutes 8080/tcp, 26257/tcp cockroach-client
※cockroach-client は sql shell を実行するためのクライアント用コンテナ。
- クラスタ情報
id | address | sql_address | build | started_at | updated_at | locality | is_available | is_live -----+-------------------+-------------------+---------+----------------------------+----------------------------+-----------------------------+--------------+---------- 1 | cockroach-1:26257 | cockroach-1:26257 | v21.2.5 | 2022-02-23 00:53:26.41591 | 2022-02-23 00:53:26.483088 | region=region-1,zone=zone-a | true | true 2 | cockroach-2:26257 | cockroach-2:26257 | v21.2.5 | 2022-02-23 00:53:27.073393 | 2022-02-23 00:53:27.149113 | region=region-1,zone=zone-b | true | true 3 | cockroach-3:26257 | cockroach-3:26257 | v21.2.5 | 2022-02-23 00:53:27.527108 | 2022-02-23 00:53:27.602613 | region=region-1,zone=zone-c | true | true
DB 作成
CockroachDB 側で Scalar DB が利用する DB (db0) を作成します。
- DB 作成
$ docker exec -it cockroach-client ./cockroach sql --user=root --certs-dir=certs --host=cockroach-1:26257 # # Welcome to the CockroachDB SQL shell. # All statements must be terminated by a semicolon. # To exit, type: \q. # # Server version: CockroachDB CCL v21.2.5 (x86_64-unknown-linux-gnu, built 2022/02/07 21:01:07, go1.16.6) (same version as client) # Cluster ID: e3e484e2-8026-4bc5-8a6f-15ff7a764e9f # # Enter \? for a brief introduction. # root@cockroach-1:26257/defaultdb> root@cockroach-1:26257/defaultdb> CREATE DATABASE db0; CREATE DATABASE Time: 37ms total (execution 37ms / network 0ms) root@cockroach-1:26257/defaultdb> root@cockroach-1:26257/defaultdb> SHOW DATABASES; database_name | owner | primary_region | regions | survival_goal ----------------+-------+----------------+---------+---------------- db0 | root | NULL | {} | NULL defaultdb | root | NULL | {} | NULL intro | root | NULL | {} | NULL postgres | root | NULL | {} | NULL system | node | NULL | {} | NULL (5 rows) Time: 4ms total (execution 4ms / network 0ms) root@cockroach-1:26257/defaultdb>
ユーザー作成
Scalar DB が CockroachDB にアクセスする際のユーザー (scalar) を作成します。今回はパスワード認証でアクセスするため、パスワードも併せて設定しておきます。また、今回は scalar ユーザーに db0 に対するフルアクセスの権限をざっくりと付与しておきます。
- ユーザー作成
root@cockroach-1:26257/defaultdb> CREATE USER scalar WITH PASSWORD 'xxxxxxxx'; CREATE ROLE Time: 250ms total (execution 249ms / network 0ms) root@cockroach-1:26257/defaultdb> root@cockroach-1:26257/defaultdb> GRANT ALL ON DATABASE db0 TO scalar; GRANT Time: 105ms total (execution 104ms / network 0ms) root@cockroach-1:26257/defaultdb> root@cockroach-1:26257/defaultdb> SHOW USERS; username | options | member_of ------------+---------+------------ admin | | {} cockroach | | {} root | | {admin} scalar | | {} (4 rows) Time: 9ms total (execution 8ms / network 0ms) root@cockroach-1:26257/defaultdb>
これで、CockroachDB 側での準備は完了です。実際は TABLE 作成も必要ですが、Scalar DB で使う TABLE は Scalar DB 側のツールを使って作成します。
Scalar DB の準備
次に、Scalar DB 側の準備を行います。
設定ファイル作成
まずは、Scalar DB や Schema Loader から CockroachDB に接続するための設定ファイル (database.properties) を準備します。今回は、必要最低限の設定として「JDBC での接続情報」「ユーザー/パスワード」のみ設定します。
この設定ファイル内で、CockroachDB コンテナが LISTEN している 127.0.0.1:26257 と、CockroachDB 側で作成した DB (db0) を指定しています。
- database.properties
# Use PostgreSQL (JDBC Database) scalar.db.storage=jdbc # JDBC URL scalar.db.contact_points=jdbc:postgresql://127.0.0.1:26257/db0 # User and Password scalar.db.username=scalar scalar.db.password=xxxxxxxx
TABLE 作成
次に、テスト用の TABLE を作成します。(分散 Tx マネージャーである) Scalar DB では、クライアント側 (Scalar DB 側 ) で分散 Tx を実現するために Distributed WAL と呼ばれるメタデータをレコード (TABLE) 内に保持する仕組みになっています。
そのため、TABLE を作成する際は Distributed WAL を格納するためのカラムも一緒に定義する必要がありますが、Schema Loader というツールを使うといい感じに TABLE を作成してくれます。
まずは、GitHub Releases から Schema Loader をダウンロードします。
- Schema Loader をダウンロード
$ curl -OL https://github.com/scalar-labs/scalardb/releases/download/v3.5.0/scalardb-schema-loader-3.5.0.jar $ ls -l scalardb-schema-loader-3.5.0.jar -rw-rw-r-- 1 ubuntu ubuntu 60464284 Feb 23 11:33 scalardb-schema-loader-3.5.0.jar
そして、作成する TABLE の情報を JSON 形式で用意します。
- test-schema.json
{ "s0.t0": { "transaction": true, "partition-key": [ "id" ], "clustering-key": [], "columns": { "id": "INT", "c0": "INT" } } }
今回は「"s0" スキーマ」内に「"t0" テーブル」を作成し、「id (int/primary key)」「c0 (int)」という 2つのカラムを定義します。要するに、通常の DDL だと以下のような感じの操作をしているイメージです。
- DDL のイメージ
CREATE SCHEMA s0; CREATE TABLE s0.t0 (id INT PRIMARY KEY, c0 INT);
用意した JSON ファイル (test-schema.json) を指定して Schema Loader を実行します。この時、事前に用意した database.properties ファイル (CockroachDB に JDBC で接続するための情報を記載した設定ファイル) も指定します。
- Schema Loader 実行
$ java -jar ./scalardb-schema-loader-3.5.0.jar --config ./database.properties -f ./test-schema.json --coordinator [main] INFO com.scalar.db.schemaloader.command.SchemaLoaderCommand - Config path: ./database.properties [main] INFO com.scalar.db.schemaloader.command.SchemaLoaderCommand - Schema path: ./test-schema.json [main] INFO com.scalar.db.schemaloader.SchemaOperator - Creating the table t0 in the namespace s0 succeeded. [main] INFO com.scalar.db.schemaloader.SchemaOperator - Creating the coordinator table succeeded.
これで TABLE 作成は完了です。試しに、直接 CockroachDB に接続して確認してみると、以下のような TABLE が作成されていることが確認できます。
- TABLE 定義
root@cockroach-1:26257/db0> SHOW COLUMNS FROM s0.t0; column_name | data_type | is_nullable | column_default | generation_expression | indices | is_hidden -------------------------+-----------+-------------+----------------+-----------------------+-----------+------------ id | INT8 | false | NULL | | {primary} | false c0 | INT8 | true | NULL | | {primary} | false tx_id | STRING | true | NULL | | {primary} | false tx_state | INT8 | true | NULL | | {primary} | false tx_version | INT8 | true | NULL | | {primary} | false tx_prepared_at | INT8 | true | NULL | | {primary} | false tx_committed_at | INT8 | true | NULL | | {primary} | false before_tx_id | STRING | true | NULL | | {primary} | false before_tx_state | INT8 | true | NULL | | {primary} | false before_tx_version | INT8 | true | NULL | | {primary} | false before_tx_prepared_at | INT8 | true | NULL | | {primary} | false before_tx_committed_at | INT8 | true | NULL | | {primary} | false before_c0 | INT8 | true | NULL | | {primary} | false (13 rows)
また、実際には Scalar DB 側で分散 Tx 等を管理するために必要なメタデータを格納する TABLE (scalardb.metadata / coordinator.state) も作成されていることが確認できます。
- 作成された TABLE 一覧
root@cockroach-1:26257/db0> SHOW TABLES; schema_name | table_name | type | owner | estimated_row_count | locality --------------+------------+-------+--------+---------------------+----------- coordinator | state | table | scalar | 0 | NULL s0 | t0 | table | scalar | 0 | NULL scalardb | metadata | table | scalar | 18 | NULL (3 rows)
これで、Scalar DB 側の事前準備も完了です。
Scalar DB を使った CRUD
ということで、Scalar DB を使った CRUD 操作を実施してみようと思います。
が、現状 Scalar DB に SQL クライアントのようなツールはなく、今回は前述した通り Scalar DB を Java のライブラリとして利用するので、まずは CRUD 操作を実行するための Java App を用意します。
Scalar DB のダウンロード
GitHub から clone してきて build する方法もあるのですが、Scalar DB は Maven Central で公開されているので、今回はそこからダウンロードしてきます。
Java App の作成
今回は以下の 3 のプログラムで CRUD 操作を実行します。各コード内で先ほど準備した database.properties を読み込んで CockroachDB に JDBC で接続しています。
- Write.java (Create / Update)
package com.example; import java.io.File; import java.io.IOException; import com.scalar.db.api.DistributedTransaction; import com.scalar.db.api.DistributedTransactionManager; import com.scalar.db.api.Get; import com.scalar.db.api.Put; import com.scalar.db.config.DatabaseConfig; import com.scalar.db.exception.transaction.TransactionException; import com.scalar.db.io.Key; import com.scalar.db.service.TransactionFactory; public class Write { public static void main(String[] args) throws TransactionException, IOException { TransactionFactory factory = new TransactionFactory(new DatabaseConfig(new File("./database.properties"))); DistributedTransactionManager manager = factory.getTransactionManager(); manager.with("s0", "t0"); DistributedTransaction tx = manager.start(); int id = Integer.parseInt(args[0]); int value = Integer.parseInt(args[1]); try { Get get = new Get(new Key("id", id)); tx.get(get); Put put = new Put(new Key("id", id)).withValue("c0", value); tx.put(put); tx.commit(); } catch (Exception e) { tx.abort(); throw e; } } }
- Read.java (Read)
package com.example; import java.io.File; import java.io.IOException; import java.util.Optional; import com.scalar.db.api.DistributedTransaction; import com.scalar.db.api.DistributedTransactionManager; import com.scalar.db.api.Get; import com.scalar.db.api.Result; import com.scalar.db.config.DatabaseConfig; import com.scalar.db.exception.transaction.TransactionException; import com.scalar.db.io.Key; import com.scalar.db.service.TransactionFactory; public class Read { public static void main(String[] args) throws TransactionException, IOException { TransactionFactory factory = new TransactionFactory(new DatabaseConfig(new File("./database.properties"))); DistributedTransactionManager manager = factory.getTransactionManager(); manager.with("s0", "t0"); DistributedTransaction tx = manager.start(); int id = Integer.parseInt(args[0]); try { Get get = new Get(new Key("id", id)); Optional<Result> result = tx.get(get); tx.commit(); if (result.isPresent()) { int value = result.get().getValue("c0").get().getAsInt(); System.out.println("id: " + id); System.out.println("c0: " + value); } else { System.out.println("\"id = " + id + "\" does not exist"); } } catch (Exception e) { tx.abort(); throw e; } } }
- Remove.java (Delete)
package com.example; import java.io.File; import java.io.IOException; import com.scalar.db.api.Delete; import com.scalar.db.api.DistributedTransaction; import com.scalar.db.api.DistributedTransactionManager; import com.scalar.db.api.Get; import com.scalar.db.config.DatabaseConfig; import com.scalar.db.exception.transaction.TransactionException; import com.scalar.db.io.Key; import com.scalar.db.service.TransactionFactory; public class Remove { public static void main(String[] args) throws TransactionException, IOException{ TransactionFactory factory = new TransactionFactory(new DatabaseConfig((new File("./database.properties")))); DistributedTransactionManager manager = factory.getTransactionManager(); manager.with("s0", "t0"); DistributedTransaction tx = manager.start(); int id = Integer.parseInt(args[0]); try { Get get = new Get(new Key("id", id)); tx.get(get); Delete del = new Delete(new Key("id", id)); tx.delete(del); tx.commit(); } catch (Exception e) { tx.abort(); throw e; } } }
ちなみに、今回は例外処理をサボっていますが、「分散 Tx を実行する」だけあって、実際はいろんなパターンのエラーを想定した複数の例外処理を実施する必要があります。(検証用のコードでそこまでガッツリ例外処理する元気はありませんでした...)
興味がある方は A Guide on How to Handle Exceptions や Trouble-shooting Guilde を参照してみてください。
App を実行
では、用意した各 App を実行してみます。
Create
まずは Create です。t0 TABLE に対して id = 1 / c0 = 100 のレコードを INSERT します。
- Create
$ java -cp ./lib/*: com.example.Write 1 100
App 実行後、CockroachDB 側で直接確認すると、以下のようにレコードが INSERT されていることが確認できます。
- CockroachDB 側で確認
root@cockroach-1:26257/db0> SELECT id, c0 FROM s0.t0; id | c0 -----+------ 1 | 100 (1 row)
Read
次に Read です。先ほど書き込んだ id = 1 / c0 = 100 のレコードを Scalar DB 経由で SELECT してみます。
- Read
$ java -cp ./lib/*: com.example.Read 1 id: 1 c0: 100
Update
次は Update です。先ほど書き込んだ id = 1 / c0 = 100 のレコードの c0 の値を 200 に UPDATE してみます。(※Create/INSERT と同じものを利用します。)
- Update
$ java -cp ./lib/*: com.example.Write 1 200
App 実行後、Scalar DB 経由で SELECT してみると、値が更新されていることが確認できます。
- Read (Update 後の確認)
$ java -cp ./lib/*: com.example.Read 1 id: 1 c0: 200
また、CockroachDB 側で直接 SELECT すると、値が UPDATE されていることが確認できます。
- CockroachDB 側で UPDATE された値を確認
root@cockroach-1:26257/db0> SELECT id, c0 FROM s0.t0; id | c0 -----+------ 1 | 200 (1 row)
Delete
最後に Delete です。先ほど書き込んだ id = 1 のレコードを DELETE してみます。
- Delete
$ java -cp ./lib/*: com.example.Remove 1
App 実行後、Scalar DB 経由で SELECT してみると、id = 1 のレコードが削除されていることが確認できます。
- Read (Delete 後の確認)
$ java -cp ./lib/*: com.example.Read 1 "id = 1" does not exist
また、CockroachDB 側で直接 SELECT すると、レコードが DELETE されていることが確認できます。
- CockroachDB 側で DELETE されたことを確認
root@cockroach-1:26257/db0> SELECT id, c0 FROM s0.t0; id | c0 -----+----- (0 rows)
まとめ
ということで、とりあえず Scalar DB on CockroachDB でシンプルな CRUD 操作は実行できました。(繰り返しになりますが、公式にサポートされている訳ではないのであしからず...)
なお、CockroachDB にアクセスする云々の前に Java 周りで (サンプル App の作成で) しこたま躓いたのは内緒です。