[Rust] CockroachDBを使ってみる(diesel)

[2020年現在のNewSQLについて - Qiita](https://qiita.com/tzkoba/items/5316c6eac66510233115)
こちらの記事を読んで、Spannerクローンってこんなにあるのか、と思ったので、試してみたくなったので、Postgres互換っぽい[CockroachDB](https://www.cockroachlabs.com/product/)を使ってみたくなって、やってみたメモ。
Spannerは高いしローカルで開発時に使いづらいので今まで敬遠していた部分があったけど、CockroachDBならローカルでも使える。

# 環境

- OS: Ubuntu 18.04
- Docker: 19.03.6
- DockerCompose: 1.25.0
- CockroachDB: v19.2.4
- Rust: 1.41.0

# CockroachDBをdocker-composeで起動する

[Start a Cluster in Docker (Insecure) | CockroachDB Docs](https://www.cockroachlabs.com/docs/stable/start-a-local-cluster-in-docker-linux.html)
これを参考に進めます。
公式では`docker run -d`で起動してるけど、docker-composeで楽したいので、docker-compose.ymlを書きます。

```yaml:docker-compose.yml
version: "3.7"
services:
  cockroachdb_1:
    image: cockroachdb/cockroach:v19.2.4
    container_name: roach1
    ports:
      - 26257:26257
      - 8080:8080
    networks:
      - roachnet
    restart: always
    command:
      - start
      - "--insecure"
    volumes:
      - "./tmp/cockroach-data/roach1:/cockroach/cockroach-data"
networks:
  roachnet:
    driver: bridge
```

※とりあえずクラスタリングなしで1台起動するだけにしたい為 `join` オプションは外しました

```shell
$ docker-compose up
Starting roach1 ... done
Attaching to roach1
roach1           | *
roach1           | * WARNING: RUNNING IN INSECURE MODE!
roach1           | *
roach1           | * - Your cluster is open for any client that can access <all your IP addresses>.
roach1           | * - Any user, even root, can log in without providing a password.
roach1           | * - Any user, connecting as root, can read or write any data in your cluster.
roach1           | * - There is no network encryption nor authentication, and thus no confidentiality.
roach1           | *
roach1           | * Check out how to secure your cluster: https://www.cockroachlabs.com/docs/v19.2/secure-a-cluster.html
roach1           | *
roach1           | *
roach1           | * WARNING: running 'cockroach start' without --join is deprecated.
roach1           | * Consider using 'cockroach start-single-node' or 'cockroach init' instead.
roach1           | *
roach1           | *
roach1           | * WARNING: neither --listen-addr nor --advertise-addr was specified.
roach1           | * The server will advertise "547bc63e7314" to other nodes, is this routable?
roach1           | *
roach1           | * Consider using:
roach1           | * - for local-only servers:  --listen-addr=localhost
roach1           | * - for multi-node clusters: --advertise-addr=<host/IP addr>
roach1           | *
roach1           | *
roach1           | CockroachDB node starting at 2020-02-29 14:30:12.917285211 +0000 UTC (took 0.8s)
roach1           | build:               CCL v19.2.4 @ 2020/02/06 21:55:19 (go1.12.12)
roach1           | webui:               http://547bc63e7314:8080
roach1           | sql:                 postgresql://root@547bc63e7314:26257?sslmode=disable
roach1           | RPC client flags:    /cockroach/cockroach <client cmd> --host=547bc63e7314:26257 --insecure
roach1           | logs:                /cockroach/cockroach-data/logs
roach1           | temp dir:            /cockroach/cockroach-data/cockroach-temp785838781
roach1           | external I/O path:   /cockroach/cockroach-data/extern
roach1           | store[0]:            path=/cockroach/cockroach-data
roach1           | status:              restarted pre-existing node
roach1           | clusterID:           49e704f9-fafa-453c-8232-2c46af5932f4
roach1           | nodeID:              1
```

起動したっぽいので、ブラウザから管理画面を見てみます。
`http://localhost:8080`

![Screen Shot 2020-02-29 at 23.35.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9575/72cac546-1ee4-9504-d1b6-eb9b2a4e62e8.png)


モダンな画面が出てきましたね

## Postgresとしてアクセスしてみる

postgresのcliは[pgcli](https://www.pgcli.com/)を愛用しているので、こちらからアクセスしてみます。
管理画面から見ると、 `system` というdatabaseにいくつかテーブルがありそうなので、見てみます

```shell
$ pgcli -h localhost -p 26257 -u root -d system
Server: PostgreSQL CCL
Version: 2.1.1
Chat: https://gitter.im/dbcli/pgcli
Mail: https://groups.google.com/forum/#!forum/pgcli
Home: http://pgcli.com
root@localhost:system> \d comments
at or near "pg_catalog": syntax error: type does not exist
DETAIL:  source SQL:
SELECT c.relchecks, c.relkind, c.relhasindex,
                    c.relhasrules, c.relhastriggers, c.relhasoids,
                    '',
                    c.reltablespace,
                    CASE WHEN c.reloftype = 0 THEN ''
                        ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text
                                          ^

Time: 0.006s
```

入ることは入れましたが `\d` はエラーになりました。
さすがにpg_xx系は使えないのか。

```shell
root@localhost:system> select * from comments;
+--------+-------------+----------+-----------+
| type   | object_id   | sub_id   | comment   |
|--------+-------------+----------+-----------|
+--------+-------------+----------+-----------+
SELECT 0
Time: 0.013s
```

select は普通にできますね。
（これだけで13msかかってる？？）

# dieselからアクセスしてみる
一応、Rustのpostgresドライバを使ったサンプルは公式にもあります。
https://www.cockroachlabs.com/docs/stable/build-a-rust-app-with-cockroachdb.html

Rustのdieselからアクセスしてみたいので、cargo initして、Dockerfileも書いて、dieselを使ったコードも書いてみます。

## docker

```yaml:docker-compose.yml
version: "3.7"
services:
  rust: # 追加
    container_name: rust
    build:
      context: .
    volumes:
      - .:/app
    depends_on:
      - cockroachdb_1
    command: cargo watch -x "run" -w src
    networks:
      - roachnet
    environment:
      DATABASE_URL: "postgresql://root@roach1:26257?sslmode=disable"
  cockroachdb_1:
    image: cockroachdb/cockroach:v19.2.4
    container_name: roach1
    ports:
      - 26257:26257
      - 8080:8080
    networks:
      - roachnet
    restart: always
    command:
      - start
      - "--insecure"
    volumes:
      - "./tmp/cockroach-data/roach1:/cockroach/cockroach-data"
networks:
  roachnet:
    driver: bridge
```

```Dockerfile
FROM rust:1.41.0-slim-buster

# ローカルのtargetディレクトリにビルドするとマウントしている時に遅くなるのでビルドディレクトリを変える
ENV CARGO_TARGET_DIR=/tmp/target \
    DEBIAN_FRONTEND=noninteractive \
    LC_CTYPE=ja_JP.utf8 \
    LANG=ja_JP.utf8

RUN apt-get update \
  && apt-get install -y -q \
     ca-certificates \
     locales \
     libpq-dev \
     gnupg \
     apt-transport-https\
     libssl-dev \
     pkg-config \
     curl \
     build-essential \
     git \
     wget \
  && echo "ja_JP UTF-8" > /etc/locale.gen \
  && locale-gen \
  && echo "install rust tools" \
  && cargo install cargo-watch cargo-make diesel_cli \
  && cargo install diesel_cli --no-default-features --features postgres

RUN USER=root cargo new --bin app
WORKDIR /app
COPY ./Cargo.* ./
RUN cargo build --color never \
  && rm src/*.rs

COPY . /app
RUN cargo build

CMD ["cargo", "run"]
```

## database & table 作成

diesel-cliを入れたので、docker-compose buildして、cliからdatabaseとテーブルを作っていきます。

```shell
$ docker-compose run --rm rust bash
root@d232f7a72958:/app# diesel setup
Creating migrations directory at: /app/migrations
database "bench" does not exist

root@d232f7a72958:/app# diesel migration generate create_users
Creating migrations/2020-02-29-150223_create_users/up.sql
Creating migrations/2020-02-29-150223_create_users/down.sql
```

ん、何故かdatabaseがcreateされないみたいですね。。。今回は一旦手動で作っておきます。

```shell
root@f78956c522e6:/app# diesel database setup
Executing migration script /app/migrations/00000000000000_diesel_initial_setup/up.sql
Failed with: at or near "diesel_manage_updated_at": syntax error: unimplemented: this syntax
```

```sql
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
    EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
                    FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
```
これが失敗したみたいですが、今回は一旦このあたりは不要なので、消してしまいます。

とりあえずテーブルはなんでもいいので、usersテーブルを以下のように作ってみます。
https://www.cockroachlabs.com/docs/stable/performance-best-practices-overview.html#unique-id-best-practices
ここに書かれているようにシーケンシャルなIDを使うとデータが分散されずにパフォーマンスが劣化するので、複合主キーを使うとかUUID使うと良いらしい。
https://www.cockroachlabs.com/docs/stable/performance-best-practices-overview.html#use-uuid-to-generate-unique-ids
↑UUIDを使うサンプルがあったので、これで行ってみます。

```sql:migrations/2020-02-29-150223_create_users/up.sql
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name STRING NOT NULL
);
```

```shell
root@f78956c522e6:/app# diesel migration run
Running migration 2020-02-29-150223_create_users
```

テーブルが作成できました。

## diesel

とりあえず、適当なデータを入れるコードを書いてみます

```toml:Cargo.toml
[package]
name = "cockroachdb_bench"
version = "0.1.0"
authors = ["yagince <straitwalk@gmail.com>"]
edition = "2018"
publish = false

[dependencies]
diesel = { version = "1.4.3", features = ["postgres", "uuidv07"] }
uuid = { version = "0.7", features = ["serde", "v4"] }
```

※dieselがuuidの0.7までしか対応してない(2020/02/29現在)ので、`uuidv07`をfeaturesに指定して、uuidの0.7を使うようにしています。
https://github.com/diesel-rs/diesel/blob/119d27e2a1/diesel/src/pg/types/mod.rs#L171-L172

```rust:schema.rs
table! {
    users (id) {
        id -> Uuid,
        name -> Text,
    }
}
```

```rust:models.rs
use diesel::{pg::PgConnection, prelude::*, Queryable};
use std::env;
use crate::schema::users;

pub fn establish_connection() -> PgConnection {
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    PgConnection::establish(&database_url).expect(&format!("Error connecting to {}", database_url))
}

#[derive(Debug, Clone, Queryable)]
pub struct User {
    pub id: uuid::Uuid,
    pub name: String,
}

#[derive(Debug, Clone, PartialEq, Insertable)]
#[table_name="users"]
pub struct NewUser {
    pub name: String,
}
```

```rust:lib.rs
#[macro_use]
extern crate diesel;

pub mod models;
pub mod schema;
```

```rust:main.rs
use cockroachdb_bench::{
    models::{self, NewUser, User},
    schema,
};
use diesel::prelude::*;

fn main() {
    let user = NewUser {
        name: "hoge".to_owned(),
    };
    println!("{:?}", user);

    let conn = models::establish_connection();

    let user = diesel::insert_into(schema::users::table)
        .values(&user)
        .get_result::<User>(&conn);

   println!("{:?}", user);
}
```

```shell
$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `/tmp/target/debug/cockroachdb_bench`
NewUser { name: "hoge" }
Ok(User { id: f8d016fd-ec77-4717-b669-67134282d2d0, name: "hoge" })
```

うん、データはinsertできました。
idもauto generateされていますね。
意外と行けそう。

# PostgresとCockroachDBでベンチマークとってみる

## 負荷をかけるコードに変える

- 100スレッドで並列に書き込み
  - rayonで並列化する
- 毎回establish_connectionしたくないので、ConnectionPoolで一気にコネクションをプーリングする
  - r2d2

```Cargo.toml
[package]
name = "cockroachdb_bench"
version = "0.1.0"
authors = ["yagince <straitwalk@gmail.com>"]
edition = "2018"
publish = false

[dependencies]
diesel = { version = "1.4.3", features = ["postgres", "uuidv07", "r2d2"] }
uuid = { version = "0.7", features = ["serde", "v4"] }
r2d2 = "0.8.8"
rayon = "1.3.0"
```

- r2d2とrayonを追加

```rust:models.rs
use crate::schema::users;
use diesel::{
    pg::PgConnection,
    prelude::*,
    r2d2::{self, ConnectionManager},
    Queryable,
};
use std::env;

pub type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>;
pub type DbCon = r2d2::PooledConnection<ConnectionManager<PgConnection>>;

fn database_url() -> String {
    env::var("DATABASE_URL").expect("DATABASE_URL must be set")
}

pub fn create_db_pool(size: u32) -> DbPool {
    r2d2::Pool::builder()
        .max_size(size)
        .build(ConnectionManager::<PgConnection>::new(database_url()))
        .expect("failed to create db connection pool")
}

#[derive(Debug, Clone, Queryable)]
pub struct User {
    pub id: uuid::Uuid,
    pub name: String,
}

#[derive(Debug, Clone, PartialEq, Insertable)]
#[table_name = "users"]
pub struct NewUser {
    pub name: String,
}
```

```rust:main.rs
use cockroachdb_bench::{
    models::{self, NewUser, User},
    schema,
};
use diesel::prelude::*;
use rayon::prelude::*;

fn main() {
    let concurrency = 100;
    let n = 100000;

    rayon::ThreadPoolBuilder::new().num_threads(concurrency as usize).build_global().unwrap();

    let pool = models::create_db_pool(concurrency);

    (1..=n).into_par_iter().for_each(|_|{
        let conn = pool.get().unwrap();

        let user = conn.transaction(|| {
            diesel::insert_into(schema::users::table)
                .values(&NewUser {
                    name: "hoge".to_owned(),
                })
                .get_result::<User>(&conn)
        });
        // println!("{:?}", user);
    });
}
```

100並列で10万回書き込む

## Postgresを準備

docker-compose.ymlにpostgresを追加します

```yaml:docker-compose.yml
version: "3.7"
services:
  rust:
    container_name: rust
    build:
      context: .
    volumes:
      - .:/app
    depends_on:
      - cockroachdb_1
    command: cargo watch -x "run" -w src
    networks:
      - roachnet
    environment:
      # DATABASE_URL: "postgresql://root@roach1:26257/bench?sslmode=disable"
      DATABASE_URL: "postgresql://root@postgres:5432/bench"
  postgres: # 追加！
    image: postgres:11.1
    container_name: postgres
    ports:
      - 5432:5432
    networks:
      - roachnet
    volumes:
      - "./tmp/pgdata:/var/lib/postgresql/data"
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: ""
      POSTGRES_INITDB_ARGS: --encoding=UTF-8
      POSTGRES_DB: bench
    restart: always
  cockroachdb_1:
    image: cockroachdb/cockroach:v19.2.4
    container_name: roach1
    ports:
      - 26257:26257
      - 8080:8080
    networks:
      - roachnet
    restart: always
    command:
      - start
      - "--insecure"
    volumes:
      - "./tmp/cockroach-data/roach1:/cockroach/cockroach-data"
networks:
  roachnet:
    driver: bridge
```

migrationしておきます

```shell
root@8ce080efff79:/app# diesel setup
Running migration 2020-02-29-150223_create_users
Executing migration script /app/migrations/2020-02-29-150223_create_users/up.sql
Failed with: type "string" does not exist
```

`STRING`型が無いと怒られました。そりゃ無いですね。
`VARCHAR` に変えておきます。
ちなみに、CockroachDBでVARCHARはSTRINGのaliasっぽいですね。
https://www.cockroachlabs.com/docs/stable/string.html#aliases

```sql:up.sql
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name VARCHAR NOT NULL
);
```

```shell
root@8ce080efff79:/app# diesel migration run
Running migration 2020-02-29-150223_create_users
Executing migration script /app/migrations/2020-02-29-150223_create_users/up.sql
Failed with: function gen_random_uuid() does not exist
```

おっと、今度はそっちが無いですか。
[PostgresでUUIDを生成する - blog.beaglesoft.net](https://blog.beaglesoft.net/entry/2019/01/26/102031)
※参考にさせて頂きました。

```sql
root@localhost:bench> SELECT * FROM pg_available_extensions WHERE name = 'pgcrypto';
+----------+-------------------+---------------------+-------------------------+
| name     | default_version   | installed_version   | comment                 |
|----------+-------------------+---------------------+-------------------------|
| pgcrypto | 1.3               | <null>              | cryptographic functions |
+----------+-------------------+---------------------+-------------------------+
SELECT 1
Time: 0.025s
```

入ってないですね。

```sql
root@localhost:bench> CREATE EXTENSION if not exists pgcrypto;
CREATE EXTENSION
Time: 0.009s

root@localhost:bench> SELECT * FROM pg_available_extensions WHERE name = 'pgcrypto';
+----------+-------------------+---------------------+-------------------------+
| name     | default_version   | installed_version   | comment                 |
|----------+-------------------+---------------------+-------------------------|
| pgcrypto | 1.3               | 1.3                 | cryptographic functions |
+----------+-------------------+---------------------+-------------------------+
SELECT 1
Time: 0.011s

root@localhost:bench> select gen_random_uuid();
+--------------------------------------+
| gen_random_uuid                      |
|--------------------------------------|
| d08dbbba-b5b8-47a2-8fbc-30140ca3dc95 |
+--------------------------------------+
SELECT 1
Time: 0.016s
```

入りました。
もう一度migrationします。

```shell
root@8ce080efff79:/app# diesel migration run
Running migration 2020-02-29-150223_create_users
```

成功しましたね。

じゃあ、さっきのコードを実行してみます。

```shell
root@8ce080efff79:/app# cargo run
   Compiling cockroachdb_bench v0.1.0 (/app)
    Finished dev [unoptimized + debuginfo] target(s) in 0.73s
     Running `/tmp/target/debug/cockroachdb_bench`
thread 'main' panicked at 'failed to create db connection pool: Error(Some("FATAL:  sorry, too many clients already\n"))', src/libcore/result.rs:1188:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
```

Connection数が多すぎてPoolが作れないと。
postgresのmax_connectionsを上げましょう

```yaml:docker-compose.yml
...
  postgres:
    image: postgres:11.1
    container_name: postgres
    ports:
      - 5432:5432
    networks:
      - roachnet
    volumes:
      - "./tmp/pgdata:/var/lib/postgresql/data"
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: ""
      POSTGRES_INITDB_ARGS: --encoding=UTF-8
      POSTGRES_DB: bench
    restart: always
    command: ["-c", "max_connections=200"]
...
```

`command` でオプションを渡します。

```shell
$ docker-compose up -d postgres
```

起動しなおしました。
再実行

```shell
$ cargo run
root@8ce080efff79:/app# cargo run
   Compiling cockroachdb_bench v0.1.0 (/app)
    Finished dev [unoptimized + debuginfo] target(s) in 0.75s
     Running `/tmp/target/debug/cockroachdb_bench`
```

無事動きましたね。
今回はPostgres依存なクエリを使ってないので、コードは書き換えることなく接続先を切り替えられましたが、どこまで互換性があるのかはいつかやってみたいかも。

## CockroachDB x 1 VS Postgres x 1

両方一台の構成でベンチマークをとってみます。
先に、releaseビルドを作っておきます。

```shell
$ cargo build --release
```

### Postgres

```shell
root@8ce080efff79:/app# time DATABASE_URL=postgresql://root@postgres:5432/bench /tmp/target/release/cockroachdb_bench

real    0m2.058s
user    0m2.835s
sys     0m9.661s
```

### CockroachDB

```shell
root@8ce080efff79:/app# time DATABASE_URL=postgresql://root@roach1:26257/bench?sslmode=disable /tmp/target/release/cockroachdb_bench

real    0m10.870s
user    0m4.429s
sys     0m11.361s
```

### 結果

- Postgres
  - 約2s
- CockroachDB
  - 約10s

Postgres圧勝ですね。

## CockroachDB x 3 VS Postgres x 1

### CockroachDBのNodeを増やす

公式documentも3台でクラスタ組んでるのでCockroachDBを3台に増やしてみます。

```yaml:docker-compose.yml
...
  cockroachdb_1:
    image: cockroachdb/cockroach:v19.2.4
    container_name: roach1
    ports:
      - 26257:26257
      - 8080:8080
    networks:
      - roachnet
    restart: always
    command:
      - start
      - "--insecure"
      - "--join=roach1,roach2,roach3"
    volumes:
      - "./tmp/cockroach-data/roach1:/cockroach/cockroach-data"
  cockroachdb_2:
    image: cockroachdb/cockroach:v19.2.4
    container_name: roach2
    networks:
      - roachnet
    restart: always
    command:
      - start
      - "--insecure"
      - "--join=roach1,roach2,roach3"
    volumes:
      - "./tmp/cockroach-data/roach2:/cockroach/cockroach-data"
  cockroachdb_3:
    image: cockroachdb/cockroach:v19.2.4
    container_name: roach3
    networks:
      - roachnet
    restart: always
    command:
      - start
      - "--insecure"
      - "--join=roach1,roach2,roach3"
    volumes:
      - "./tmp/cockroach-data/roach3:/cockroach/cockroach-data"
...
```

- joinオプションを追加
- cockroachdb_2, cockroachdb_3 を追加

コンテナを起動します。

```shell
➜ docker-compose up -d cockroachdb_1 cockroachdb_2 cockroachdb_3
Recreating roach1 ... done
Creating roach2   ... done
Creating roach3   ... done
```

管理画面から見てみます。

![Screen Shot 2020-03-01 at 22.59.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9575/8ba5f7b6-93c3-b048-a3b4-58d290ad3826.png)

3台に増えましたね。

### ベンチマーク

```shell
root@39989b3eaf62:/app# time /tmp/target/release/cockroachdb_bench

real    0m16.872s
user    0m4.649s
sys     0m11.553s
```

### 結果

`約17s`
遅くなりましたね。
Node増やすと速くなるかと思ったんですが、予想に反してかなり遅くなりました。

# まとめ

- 既存のPostgresDriverでアクセスできた
  - Postgres用のcliやGUIのViwerなど既存のツールが流用できそうなのは良い
- 単純に１台構成で比較するとPostgresのほうが速そう
- **未解決の問題**
  - diesel setupでcreate databaseできない
  - Node増やしたらパフォーマンスが劣化した
      - 同じNodeにアクセスして書き込んでいるからだろうか？
      - Patition等でシャーディングっぽくする必要があるんだろうか？
      - **要調査**