こたつ&&みかん&&でーたべーす

DB 関連の話を中心に技術っぽい記事を書きます。

Scalar DB on CockroachDB を試してみた

分散 Tx マネージャーである Scalar DBStorage として複数の 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 形式で用意します。

{
  "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;
        }
    }
}
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 ExceptionsTrouble-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 の作成で) しこたま躓いたのは内緒です。

Java なんもわからん... (´・ω・`)