As of Jun 1, 2021.

ZenithDB という

  • Postgres with highly available scale out storage
  • Cloud Native, Open Source alternative to Amazon Aurora

な製品を見つけたので遊んでみた。

前提

  • Ubuntu 20.04 LTS
  • Google Compute Engine E2 インスタンス
    • 8 vCPUs
    • 32 GB DRAM
    • 100 GB SSD

(お金がないので preemptible インスタンスで検証してます……。)

インストール

基本的には 公式ドキュメント のとおり。

# パッケージを最新化
sudo apt update
sudo apt upgrade

# PostgreSQL のビルドに必要なパッケージをインストール
sudo apt install build-essential libtool libreadline-dev zlib1g-dev flex bison libxml2-dev libcurl4-openssl-dev libssl-dev clang

# psql をインストール
sudo apt install postgresql-client
# pip3 をインストール
sudo apt install python3-pip
# psycopg2 の依存パッケージをインストー
sudo apt install libpq-dev
# テスト用のパッケージをインストール
pip install pytest psycopg2

# Rust のインストール
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
echo 'source $HOME/.cargo/env' >> ~/.bashrc
echo ~/.bashrc

# Zenith のビルドに必要なパッケージをインストール
sudo apt install pkg-config
# Zenith のビルド
git clone --recursive https://github.com/libzenith/zenith.git
cd zenith
make -j8

チュートリアル

これも、基本的には 公式ドキュメント のとおり。

ZenithDB は Rust で書かれたページサーバーをストレージレイヤーとして PostgreSQL を動作させる。 ページサーバーの保持するデータはブランチで管理されており、以下では main ブランチの作成と初期化、そして、それに対応するページサーバーの起動とその上の PostgreSQL の起動を行う。

$ ./target/debug/zenith init
created directory structure in .zenith
running initdb... initdb succeeded
created initial timeline 0dcf81f0ff6caaad3209b657f356bf2e
created main branch
updated pg_control
new zenith repository was created in .zenith
$ ./target/debug/zenith start
Starting pageserver at '127.0.0.1:64000' in .zenith
Pageserver started
$ ./target/debug/zenith pg start main
Starting postgres on timeline main...
Extracting base backup to create postgres instance: path=.zenith/pgdatadirs/main port=55432
Starting postgres node at 'host=127.0.0.1 port=55432 user=sira'
waiting for server to start.... done
server started
$ ./target/debug/zenith pg list
BRANCH	ADDRESS		LSN		STATUS
main	127.0.0.1:55432	0/15EAA90	running

main ブランチのページサーバーが 127.0.0.1:64000、PostgreSQL が 127.0.0.1:55432 で起動した。 main ブランチの実データはリポジトリ (.zenith) に格納されるようだ。 これは、PostgreSQL のデータベースクラスターに相当するのだろうか。 中身を覗いてみる。

$ tree -L 2 .zenith/
.zenith/
├── config
├── pageserver.log
├── pageserver.pid
├── pgdatadirs
│   └── main
├── refs
│   ├── branches
│   └── tags
├── rocksdb-storage
│   ├── 000008.sst
│   ├── 000010.log
│   ├── CURRENT
│   ├── IDENTITY
│   ├── LOCK
│   ├── LOG
│   ├── LOG.old.1622476298432492
│   ├── MANIFEST-000009
│   ├── OPTIONS-000006
│   └── OPTIONS-000012
├── timelines
│   └── 0dcf81f0ff6caaad3209b657f356bf2e
└── wal-redo-datadir
    ├── PG_VERSION
    ├── base
    ├── global
    ├── pg_commit_ts
    ├── pg_dynshmem
    ├── pg_hba.conf
    ├── pg_ident.conf
    ├── pg_logical
    ├── pg_multixact
    ├── pg_notify
    ├── pg_replslot
    ├── pg_serial
    ├── pg_snapshots
    ├── pg_stat
    ├── pg_stat_tmp
    ├── pg_subtrans
    ├── pg_tblspc
    ├── pg_twophase
    ├── pg_wal
    ├── pg_xact
    ├── postgresql.auto.conf
    ├── postgresql.conf
    └── postmaster.pid

26 directories, 19 files

RocksDB の構造とタイムラインと呼ばれる構造、redo ログのディレクトリらしきものが確認できる。

.zenith/pgdatadir/main には何が入っているのだろうか。

$ tree -L 1 .zenith/pgdatadirs/main/
.zenith/pgdatadirs/main/
├── PG_VERSION
├── base
├── global
├── log
├── pg_commit_ts
├── pg_dynshmem
├── pg_hba.conf
├── pg_ident.conf
├── pg_logical
├── pg_multixact
├── pg_notify
├── pg_replslot
├── pg_serial
├── pg_snapshots
├── pg_stat
├── pg_stat_tmp
├── pg_subtrans
├── pg_tblspc
├── pg_twophase
├── pg_wal
├── pg_xact
├── postgresql.auto.conf
├── postgresql.conf
├── postmaster.opts
└── postmaster.pid

PostgreSQL のデータベースクラスターのような構造が入っていた。

.zenith/timelines の中身は何が入っているのだろうか。 覗いてみる。

$ tree -L 4 .zenith/timelines/
.zenith/timelines/
└── 0dcf81f0ff6caaad3209b657f356bf2e
    ├── snapshots
    │   └── 00000000015EA930
    │       ├── PG_VERSION
    │       ├── base
    │       ├── global
    │       ├── pg_commit_ts
    │       ├── pg_dynshmem
    │       ├── pg_hba.conf
    │       ├── pg_ident.conf
    │       ├── pg_logical
    │       ├── pg_multixact
    │       ├── pg_notify
    │       ├── pg_replslot
    │       ├── pg_serial
    │       ├── pg_snapshots
    │       ├── pg_stat
    │       ├── pg_stat_tmp
    │       ├── pg_subtrans
    │       ├── pg_tblspc
    │       ├── pg_twophase
    │       ├── pg_xact
    │       ├── postgresql.auto.conf
    │       └── postgresql.conf
    └── wal
        └── 000000010000000000000001.partial

20 directories, 6 files

ここにも PostgreSQL のデータベースクラスターのような構造が入っていた。

チュートリアルに戻る。 psql127.0.0.1:55432 にアクセスしてテーブルの作成とデータの挿入をやってみる。

$ psql -p 55432 -h 127.0.0.1 postgres
psql (12.6 (Ubuntu 12.6-0ubuntu0.20.04.1), server 14devel)
WARNING: psql major version 12, server major version 14.
         Some psql features might not work.
Type "help" for help.

postgres=# CREATE TABLE t(key int primary key, value text);
CREATE TABLE
postgres=# INSERT INTO t VALUES(1,1);
INSERT 0 1
postgres=# SELECT * FROM t;
 key | value 
-----+-------
   1 | 1
(1 row)

postgres=# \q

次に、main ブランチを分岐元として新たなブランチ migration_check を作成し、その上で PostgreSQL を動作させる。

$ ./target/debug/zenith branch migration_check main
Created branch 'migration_check' at 0/1623720
$ ./target/debug/zenith branch
 main
 ┗━ @0/1623720: migration_check
$ ./target/debug/zenith pg start migration_check
Starting postgres on timeline migration_check...
Extracting base backup to create postgres instance: path=.zenith/pgdatadirs/migration_check port=55433
Starting postgres node at 'host=127.0.0.1 port=55433 user=sira'
waiting for server to start.... done
server started
$ ./target/debug/zenith pg list
BRANCH	ADDRESS		LSN		STATUS
main	127.0.0.1:55432	0/1623988	running
migration_check	127.0.0.1:55433	0/1633680	running

ここで新しく作成したブランチは、作成した時点での main ブランチのデータをすべて引き継ぐが、 migration_check 上の変更は main ブランチに影響しない。

例えば、以下の状態のとき

$ psql -p 55432 -h 127.0.0.1 postgres -c 'SELECT * FROM t;'
 key | value 
-----+-------
   1 | 1
(1 row)

$ psql -p 55433 -h 127.0.0.1 postgres -c 'SELECT * FROM t;'
 key | value 
-----+-------
   1 | 1
(1 row)

main ブランチに INSERT INTO t VALUES(2, 2) しても、

$ psql -p 55432 -h 127.0.0.1 postgres -c 'INSERT INTO t VALUES(2, 2)'
INSERT 0 1

main ブランチには反映されるが、migration_check ブランチには反映されない。

$ psql -p 55432 -h 127.0.0.1 postgres -c 'SELECT * FROM t;'
 key | value 
-----+-------
   1 | 1
   2 | 2
(2 rows)

$ psql -p 55433 -h 127.0.0.1 postgres -c 'SELECT * FROM t;'
 key | value 
-----+-------
   1 | 1
(1 row)

同様に migration_check ブランチに INSERT INTO t VALUES(3, 3) しても、

$ psql -p 55433 -h 127.0.0.1 postgres -c 'INSERT INTO t VALUES(3, 3)'
INSERT 0 1

$ psql -p 55432 -h 127.0.0.1 postgres -c 'SELECT * FROM t;'
 key | value 
-----+-------
   1 | 1
   2 | 2
(2 rows)

$ psql -p 55433 -h 127.0.0.1 postgres -c 'SELECT * FROM t;'
 key | value 
-----+-------
   1 | 1
   3 | 3
(2 rows)

migration_check ブランチには反映されるが、main ブランチには反映されない。

zenith remote add

Aurora 的な使い方はできないかとソースコードを読んでいると、zenith/src/main.rszenith remote add <postgres_uri> というコマンドを見つけた。 これが使えそうだが、どうやって使うのだろうか……。

        .subcommand(
            SubCommand::with_name("remote")
                .setting(AppSettings::ArgRequiredElseHelp)
                .about("Manage remote pagerservers")
                .subcommand(
                    SubCommand::with_name("add")
                        .about("Add a new remote pageserver")
                        .arg(Arg::with_name("name").required(true))
                        .arg(
                            Arg::with_name("url")
                                .help("PostgreSQL connection URI")
                                .required(true),
                        ),
                ),
        )

ページサーバーのソースコードを眺めていると pageserver/src/lib.rs にリモートサーバーに関する記述を見つけた。 トランザクション ID の考え方とリモートサーバーの変更の同期方法について書かれている。

/// Zenith Timeline ID is a 128-bit random ID.
///
/// Zenith timeline IDs are different from PostgreSQL timeline
/// IDs. They serve a similar purpose though: they differentiate
/// between different "histories" of the same cluster.  However,
/// PostgreSQL timeline IDs are a bit cumbersome, because they are only
/// 32-bits wide, and they must be in ascending order in any given
/// timeline history.  Those limitations mean that we cannot generate a
/// new PostgreSQL timeline ID by just generating a random number. And
/// that in turn is problematic for the "pull/push" workflow, where you
/// have a local copy of a zenith repository, and you periodically sync
/// the local changes with a remote server. When you work "detached"
/// from the remote server, you cannot create a PostgreSQL timeline ID
/// that's guaranteed to be different from all existing timelines in
/// the remote server. For example, if two people are having a clone of
/// the repository on their laptops, and they both create a new branch
/// with different name. What timeline ID would they assign to their
/// branches? If they pick the same one, and later try to push the
/// branches to the same remote server, they will get mixed up.
///
/// To avoid those issues, Zenith has its own concept of timelines that
/// is separate from PostgreSQL timelines, and doesn't have those
/// limitations. A zenith timeline is identified by a 128-bit ID, which
/// is usually printed out as a hex string.

今日はここまで。