PostgreSQL BDR (Bi-Directional Replication) を使ってみた

[PostgreSQL Advent Calendar 2015](http://qiita.com/advent-calendar/2015/postgresql) 23日目の記事です！
かねてより[2ndQuadrant](http://2ndquadrant.com/en/)が開発している [PostgreSQL BDR](http://bdr-project.org/docs/next/index.html) に興味があったので簡単に触ってみました。

# マルチマスタ＆結果整合性の PostgreSQL
PostgreSQL には 9.0 から [Streaming Replication](https://www.postgresql.jp/document/9.4/html/warm-standby.html#STREAMING-REPLICATION) が実装されています。マスタサーバから複数のスレーブサーバに対して [WAL](https://www.postgresql.jp/document/9.4/html/wal-intro.html) を送信し、その WAL をスレーブサーバに反映させることで、マスターサーバと同じ状態にします。データはマスタとスレーブにレプリケーションされ、スレーブでは参照クエリを受け付けることができるので、可用性の確保と負荷分散(参照)を実現することができます。

![postgresql-9-638.jpg](https://qiita-image-store.s3.amazonaws.com/0/74588/e01db26d-6c10-a38f-7f7e-172129484d26.jpeg)

<iframe src="//www.slideshare.net/slideshow/embed_code/key/zv777Pmfr2rza2" width="595" height="485" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC; border-width:1px; margin-bottom:5px; max-width: 100%;" allowfullscreen> </iframe> <div style="margin-bottom:5px"> <strong> <a href="//www.slideshare.net/hadoopxnttdata/postgresql-39739357" title="PostgreSQLレプリケーション徹底紹介" target="_blank">PostgreSQLレプリケーション徹底紹介</a> </strong> from <strong><a href="//www.slideshare.net/hadoopxnttdata" target="_blank">NTT DATA OSS Professional Services</a></strong> </div>

9.4 からは [logical replication](https://www.postgresql.jp/document/9.4/html/logicaldecoding.html) が追加されました。BDRはこの logical replication を使うことで、1マスタNスレーブ構成だった Streaming Replication を Nマスタに拡張することができる外部ツールです。BDR のNマスタ(node)はそれぞれ、参照に加えて更新も可能です。このような構成は、複数のマスタで同時に更新が発生した場合に、競合することが容易に想定されますが、BDRはこれを optimistic concurrency control 的に eventually consistent (結果整合性)で解決します。

面白そうですよね！というわけでインストールしてみましょう！

# インストール
まずは下記マニュアル通りに淡々とやってみます。
http://bdr-project.org/docs/next/installation-source.html
今回は自宅PCの CentOS6.6 でやってみました。BDR の各 node を１台のサーバ上で動かします。

* インストール準備

```
wget http://yum.postgresql.org/9.4/redhat/rhel-6-x86_64/pgdg-centos94-9.4-2.noarch.rpm
rpm -ihv pgdg-centos94-9.4-2.noarch.rpm
yum check-update
yum groupinstall "Development Tools"
yum install yum-utils openjade docbook-dtds docbook-style-dsssl docbook-style-xsl
yum-builddep postgresql94
```
####### yum-builddepなんてあるんですね。知らなかった。

* PostgreSQL94 をインストール。

```
git clone -b bdr-pg/REL9_4_STABLE git://git.postgresql.org/git/2ndquadrant_bdr.git postgresql-bdr
cd postgresql-bdr/
./configure --prefix=/path/to/install --enable-debug --with-openssl
make -j4 -s install-world
```

* BDR をインストール

```
git clone -b bdr-plugin/REL0_9_STABLE git://git.postgresql.org/git/2ndquadrant_bdr.git bdr-plugin
cd ../bdr-plugin/
PATH=/path/to/install/bin:"$PATH" ./configure
make -j4 -s all
make -s install
```

* インストールできたみたいです

```
ll /path/to/install/bin/ | grep bdr
-rwxr-xr-x 1 root root   929388 Dec 17 08:03 bdr_dump
-rwxr-xr-x 1 root root   169384 Dec 17 08:03 bdr_init_copy
-rwxr-xr-x 1 root root     2399 Dec 17 08:03 bdr_initial_load
-rwxr-xr-x 1 root root    89464 Dec 17 08:03 bdr_resetxlog
```

* PostgreSQLを起動させる準備をします

```
PATH=/path/to/install/bin:"$PATH"
mkdir $HOME/2ndquadrant_bdr/
initdb -D $HOME/2ndquadrant_bdr/bdr5598 -A trust -U postgres
initdb -D $HOME/2ndquadrant_bdr/bdr5599 -A trust -U postgres
```

* マニュアルに記載の通り設定

```
vim bdr5598/postgresql.conf
    shared_preload_libraries = 'bdr'
    wal_level = 'logical'
    track_commit_timestamp = on
    max_connections = 100
    max_wal_senders = 10
    max_replication_slots = 10
    # Make sure there are enough background worker slots for BDR to run
    max_worker_processes = 10

    # These aren't required, but are useful for diagnosing problems
    log_error_verbosity = verbose
    log_min_messages = debug1
    log_line_prefix = 'd=%d p=%p a=%a%q '

    # Useful options for playing with conflicts
    bdr.default_apply_delay=2000   # milliseconds
    bdr.log_conflicts_to_table=on

vim bdr5598/pg_hba.conf
    local   replication   postgres                  trust
    host    replication   postgres     127.0.0.1/32 trust
    host    replication   postgres     ::1/128      trust
```

* マニュアル通り起動させます。とりあえず、node1(ポート番号5598), node2(ポート番号5599)の２台を起動しました。

```
pg_ctl -l $HOME/2ndquadrant_bdr/bdr5598.log -D $HOME/2ndquadrant_bdr/bdr5598 -o "-p 5598" -w start
pg_ctl -l $HOME/2ndquadrant_bdr/bdr5599.log -D $HOME/2ndquadrant_bdr/bdr5599 -o "-p 5599" -w start
```

* マニュアル通りDBを作成します

```
createdb -p 5598 -U postgres bdrdemo
createdb -p 5599 -U postgres bdrdemo
```

* PostgreSQLにログインして CREATE EXTENSION を実行

```
psql -p 5598 -U postgres bdrdemo
psql (9.4.5)
Type "help" for help.

CREATE EXTENSION btree_gist;
CREATE EXTENSION
CREATE EXTENSION bdr;
CREATE EXTENSION
```

* BDRグループを作成します。マニュアル通り bdrdemo データベースをレプリケーション対象としました。BDR はデータベース単位でレプリケーションの設定が可能です。

```
SELECT bdr.bdr_group_create(
      local_node_name := 'node1',
      node_external_dsn := 'port=5598 dbname=bdrdemo'
);
 bdr_group_create 
------------------
 
(1 row)
```

* BDR 開始。他の node からの接続を待機します。

```
SELECT bdr.bdr_node_join_wait_for_ready();
 bdr_node_join_wait_for_ready 
------------------------------
 
(1 row)
```

* node2 でも同様に設定しました。
* 動作テスト。まずは node1 でテーブルを作成してインサートしてみます。

```
psql -p 5598 -U postgres bdrdemo
psql (9.4.5)
Type "help" for help.

CREATE TABLE t1bdr (c1 INT, PRIMARY KEY (c1));
CREATE TABLE
INSERT INTO t1bdr VALUES (1);
INSERT 0 1
INSERT INTO t1bdr VALUES (2);
INSERT 0 1
SELECT * FROM t1bdr;
 c1 
----
  1
  2
(2 rows)
```

* node2 に反映されているか確認。反映されている模様。

```
psql -p 5599 -U postgres bdrdemo
psql (9.4.5)
Type "help" for help.

SELECT * FROM t1bdr;
 c1 
----
  1
  2
(2 rows)
```

* node2 でデリートしてみます。

```
DELETE FROM t1bdr WHERE c1 = 2;
DELETE 1
SELECT * FROM t1bdr;
 c1 
----
  1
(1 row)
```

* node1 で確認。

```
psql -p 5598 -U postgres bdrdemo
psql (9.4.5)
Type "help" for help.

SELECT * FROM t1bdr;
 c1 
----
  1
(1 row)
```
２つの node それぞれで更新処理ができることを確認！
意外とあっさりできてしまったので、もう少し node を追加してみます。

* というわけで 5node 作りました。

```
SELECT * FROM bdr.bdr_nodes;
     node_sysid      | node_timeline | node_dboid | node_status | node_name |      node_local_dsn      |    node_init_from_dsn    
---------------------+---------------+------------+-------------+-----------+--------------------------+--------------------------
 6229625696698174877 |             1 |      16385 | r           | node1     | port=5598 dbname=bdrdemo | 
 6229625885773213129 |             1 |      16385 | r           | node2     | port=5599 dbname=bdrdemo | port=5598 dbname=bdrdemo
 6229638671824904727 |             1 |      16385 | r           | node3     | port=5560 dbname=bdrdemo | port=5598 dbname=bdrdemo
 6229644293364933753 |             1 |      16385 | r           | node4     | port=5561 dbname=bdrdemo | port=5598 dbname=bdrdemo
 6229644586486318278 |             1 |      16385 | r           | node5     | port=5562 dbname=bdrdemo | port=5598 dbname=bdrdemo
(5 rows)
```

5node それぞれで参照更新ができることを確認しました。

# [更新の競合](http://bdr-project.org/docs/next/conflicts.html)
さて、インストールがあっさり終わってしまったので、更新が競合する状況を発生させて挙動を確認したいと思います。今、Postgresql.conf には下記の GUC が設定されています。

<dl>
  <dt>bdr.log_conflicts_to_table=on</dt>
  <dd>コンフリクト(競合)発生時、bdr.bdr_conflict_history テーブルに詳細を記録する。</dd>
  <dt>bdr.default_apply_delay=2000</dt>
  <dd>レプリケーションの反映を設定した時間(ミリ秒)遅らせる。ネットワーク遅延のシュミレートに有効。</dd>
</dl>

これらの設定を施すことで、容易に競合状態を発生させ、それがどのように処理されたのかを追うことが可能です。さっそくやってみましょう。

まず、下記テーブルを作成します。

```
CREATE TABLE t3bdr (c1 INT, c2 text, PRIMARY KEY (c1));
```

t3bdr テーブルの c1 カラムに対して、node1 と node5 で同時に、同じ値をインサートしてみます。c1 は PRIMARY KEY なので、どちらかのインサートはエラーになるはずです。今回の場合は bdr.default_apply_delay=2000 を設定しているので、レプリケーションの反映が２秒間遅延します。よって、node1 と node5 のインサート実行時点では問題なく処理が通りますが、レプリケーションを反映する段階で競合が発生するはずです。

ではやってみましょう。node1, node5 で同時に下記SQLを実行します。

* node1

```
INSERT INTO t3bdr VALUES (1, 'node1');
INSERT 0 1
```
* node5

```
INSERT INTO t3bdr VALUES (1, 'node5');
INSERT 0 1
```

どちらもエラーが発生することなく処理されました。値を確認してみます。node1 で下記を実行してみました。

```
SELECT * FROM t3bdr;
 c1 |  c2   
----+-------
  1 | node5
(1 row)
```

どうやら node5 のインサートが勝ったようです。
node5 で bdr.bdr_conflict_history テーブルを確認してみます。

```
SELECT * FROM bdr.bdr_conflict_history;
-[ RECORD 11 ]-----------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------conflict_id              | 11
local_node_sysid         | 6229644586486318278
local_conflict_xid       | 8351
local_conflict_lsn       | 0/223DD70
local_conflict_time      | 2015-12-23 04:24:39.967961-08
object_schema            | public
object_name              | t3bdr
remote_node_sysid        | 6229625696698174877
remote_txid              | 8326
remote_commit_time       | 2015-12-23 04:24:37.965434-08
remote_commit_lsn        | 0/21C5AF0
conflict_type            | insert_insert
conflict_resolution      | last_update_wins_keep_local
local_tuple              | {"c1":1,"c2":"node5"}
remote_tuple             | {"c1":1,"c2":"node1"}
local_tuple_xmin         | 8350
local_tuple_origin_sysid | 
error_message            | 
error_sqlstate           | 
error_querystring        | 
error_cursorpos          | 
error_detail             | 
error_hint               | 
error_context            | 
error_columnname         | 
error_typename           | 
error_constraintname     | 
error_filename           | 
error_lineno             | 
error_funcname           | 
```
コンフリクトが発生したことがちゃんと記録されています。コンフリクトが発生した node、発生時間、コンフリクトのタイプ、解決方法などが記載されています。今回の場合は、node1 で 2015-12-23 04:24:37.965434-08 にコミットされた {"c1":1,"c2":"node1"} が node5 に届き、そこで {"c1":1,"c2":"node5"} のデータと 2015-12-23 04:24:39.967961-08 にコンフリクトした。コンフリクトの種類は insert_insert だったので、last_update_wins_keep_local で解決した、つまり、node1 より後の時刻にインサートされた node5 の値を優先させた、ということが読み取れます。たぶん。ちなみに、node1 で bdr.bdr_conflict_history を参照しても、上記は記録されていません。bdr.bdr_conflict_history は各nodeで独立のようです。

以上、簡単に更新の競合を発生させて挙動を確認してみました。BDR のデフォルトの競合解決は last_update_wins ですが、マニュアルには競合の解決方法をユーザが独自に conflict handler として定義できると[書いてあります](http://bdr-project.org/docs/next/functions-conflict-handlers.html)。

この conflict handler や下記で紹介する Global Sequences を使えば、BDR はより便利になりそうです。

# [Global Sequences](http://bdr-project.org/docs/next/global-sequences.html)
BDR は複数の node で重複しない値を生成する Global Sequences を提供しています。BDR は1000個ずつの値を chunk という単位で管理し、各 node に割り振ります。各 node は常に複数の chunk を持っていて、ある程度 chunk を消費すると、BDRクラスタ全体で voting という処理を行い、各 node で重複が発生しないよう新たな chunk を割り振るようです。つまり、各 node で生成される値は１から始まるわけではありません。ある node では 5000 から、ある node では 10000 から値が生成されたりします。

早速試してみましょう。

* node1 で下記テーブルを作成します。

```
BEGIN;
SET LOCAL default_sequenceam = 'bdr';
CREATE TABLE gstest (
       id serial primary key,
       parrot text
);
COMMIT;
```

* node1 でインサートしてみます。65001 が生成されました。

```
INSERT INTO gstest (parrot) VALUES ('AAA');
select * from gstest;
  id   | parrot 
-------+--------
 65001 | AAA
(1 row)
```

* node2~5 でもそれぞれ一回ずつインサートしてみます。重複が発生しないよう値が生成されていることがわかります。

```
SELECT * FROM gstest;
   id   | parrot 
--------+--------
  65001 | AAA
  80001 | AAA
 130001 | AAA
 120001 | AAA
 105001 | AAA
(5 rows)
```

* node1でもう一回インサートしてみます。node1 でインサートしたときに生成された値から連番が振られました。

```
SELECT * FROM gstest;
   id   | parrot 
--------+--------
  65001 | AAA
  80001 | AAA
 130001 | AAA
 120001 | AAA
 105001 | AAA
  65002 | AAA
(6 rows)
```

* 挿入を連続して実行してみます。chunk の値を使い切り、voting が行われて新しい chunk が割り当てられたようです。

```
SELECT * FROM gstest;
   id   | parrot 
--------+--------
  65001 | AAA
  80001 | AAA
 130001 | AAA
 120001 | AAA
 105001 | AAA
  65002 | AAA
  65003 | AAA
...
  69996 | AAA
  69997 | AAA
  69998 | AAA
  69999 | AAA
  70000 | AAA
 170001 | AAA
 170002 | AAA
 170003 | AAA
 170004 | AAA
...
 170298 | AAA
 170299 | AAA
 170300 | AAA
 170301 | AAA
(5305 rows)
```

# 最後に
マルチマスタ PostgreSQL の PostgreSQL BDR を紹介しました。FDW、Postgres-XC/XL/X2、Greenplum Database などと違って、マスタ/スレーブの関係がない分散PostgreSQLを構築可能です。要件によってはかなり使えるのではないでしょうか。FDWと組み合わせることができるようになるともっと面白くなりそうですね！

明日の担当は glory_of さんです！よろしくお願いします！