2015年2月17日

HAProxyでPostgreSQLを負荷分散する(デモ動画あり)

PostgreSQLにレプリケーション機能が標準機能として入ってしばらく経ちました。

PostgreSQLでもレプリケーション機能がいろいろなところで使われるようになってきた最近ですが、以前から気になっていたこととして、「L4のロードバランサでPostgreSQLの負荷分散ができないのだろうか?」という素朴な疑問がありました。

ずっと試してみたいと思っていたのですが、ようやく今回 HAProxy を用いることで動作させることができましたので、その方法を簡単にご紹介します。

■「L4のロードバランサ」とは

「L4(Layer4)ロードバランス」とはTCP層におけるロードバランスのことです。L4のロードバランサは、アプリケーションごとのセッションの内容には関係なく、TCP層の情報だけを使ってロードバランスを行います。

ネットワークのロードバランスには、L4以外にもL7のロードロードバランサもあり、L7のロードバランサの場合には、例えばHTTP/HTTPSのロードバランサであれば、HTTPのセッションの内容まで見て、Cookieを挿入したり参照したりして、ロードバランスの挙動を制御したりします。

L4とL7のロードバランサ違いについては、以下のページの「Layer-4 と Layer-7 のロードバランサの違い」の項目を参照してください。

一般論として、L4の場合は機能がシンプルで高性能、多くのアプリケーションで使える一方、L7は多機能である代わりにアプリケーションごとの処理の実装が必要で処理負荷が高くなる、と言えるでしょう。

例えば、PostgreSQLで有名な pgpool は、PostgreSQLのセッションの中身まで把握した上でロードバランスの挙動を決めますので、L7のロードバランサであると言えます。

HAProxy は、HTTPについてはL7として動作しますが、それ以外のアプリケーションについてはL4のロードバランサとして使用することができます。今回のテストはL4のロードバランサとしてのテストになります。

■システム構成

今回の構成は以下の通りです。
  • CentOS 6.5
  • PostgreSQL 9.4.0
  • HAProxy 1.5.11
Vagrant上で動かしているCentOS 6.5で、PostgreSQLをポート番号とデータベースクラスタのディレクトリを別々に設定することで、マスターのインスタンスとレプリカを3インスタンス稼働させることにします。
  • マスター
    • データベースクラスタ /tmp/pgdata/master1
    • ポート 5432
  • レプリカ1
    • データベースクラスタ /tmp/pgdata/replica1
    • ポート 5433
  • レプリカ2
    • データベースクラスタ /tmp/pgdata/replica2
    • ポート 5434
  • レプリカ3
    • データベースクラスタ /tmp/pgdata/replica3
    • ポート 5435
図にすると以下のような構成になります。


なお、レプリケーション関連の作業は、手順が非常に煩雑になるため、PostgreSQLのコマンドを直接実行するのではなく、いくつかスクリプトを作成して利用しています。実際のコマンド実行やオプションの詳細を確認したい場合は、スクリプトを参照してください。

今回使ったスクリプトや設定ファイル、HAProxyの(S)RPMファイルは、以下のGithubレポジトリから取得することができます。

■テストのシナリオ

今回のテストでは、以下のシナリオを試してみました。
  1. まず最初にPostgreSQLのマスターとなるインスタンスを初期化、起動する。
  2. 次にレプリカを3インスタンス分、非同期レプリケーション構成で起動させる。
  3. 3台のレプリカで負荷分散させるためのHAProxyを起動させる。
  4. マスター上で pgbench で必要なテーブル、データを作成する。
  5. HAProxyを通して、pgbenchのSELECT Onlyを実行する。
  6. pgbenchを実行中に、レプリカを1台ダウンさせる(デモ中では2番目のレプリカ)。
  7. HAProxyが障害を検知してロードバランスの対象から外していることを確認。
  8. ダウンさせたレプリカを再セットアップして、HAProxyに復帰させる。
  9. 復帰したレプリカをHAProxyが検知して再びトランザクションを流し始める。
なお、レプリケーションの状態やセッション数、トランザクション数などを確認するため、専用のスクリプトを使っています。これらについても上記のGithubレポジトリから取得できます。

■マスターの初期化と起動

まずは、マスターの初期化と起動を行います。以下は実行コマンドとその出力です。(出力がbusyになるため抜粋しています)

repli-config-start-master.shというスクリプトを実行していますが、内部で行っているのはinitdbの実行、設定ファイルのコピーとpg_ctlコマンドによるインスタンスの起動です。
[snaga@devvm04 haproxy]$ ./repli-config-start-master.sh
The files belonging to this database system will be owned by user "snaga".
This user must also own the server process.
(...snip...)

creating directory /tmp/pgdata/master1 ... ok
creating subdirectories ... ok
selecting default max_connections ... 100
(...snip...)
`postgresql.conf' -> `/tmp/pgdata/master1/postgresql.conf'
`pg_hba.conf' -> `/tmp/pgdata/master1/pg_hba.conf'
waiting for server to start.... done
server started
[snaga@devvm04 haproxy]$ ps auxx | grep pgdata
snaga    17396  0.5  2.4 230200 15056 pts/0    S    15:06   0:00 /usr/pgsql-9.4/bin/postgres -D /tmp/pgdata/master1 -p 5432
snaga    17406  0.0  0.1   8388   852 pts/0    S+   15:06   0:00 grep pgdata
[snaga@devvm04 haproxy]$ 
これでマスターの起動が完了しました。

■レプリカの設定と起動

次にレプリカの設定と起動を行います。レプリカの設定と起動は repli-config-add-replica.sh というスクリプトを使って行います。

スクリプトの中で実施していることは、pg_basebackupコマンドを使ったマスターのデータベースクラスタのコピーの作成、recovery.confファイルを作成してホットスタンバイの設定、pg_ctlコマンドでインスタンスの起動です。

repli-config-add-replica.sh には、引数として、ノードの名前、データベースクラスタのPATH、およびインスタンスを稼働させるポート番号を与えます。

以下の例では、1台目のレプリカとして "replica1" という名前で "/tmp/pgdata/replica1" をデータベースクラスタとして、ポート "5433" で起動するように設定しています。
[snaga@devvm04 haproxy]$ ./repli-config-add-replica.sh replica1 /tmp/pgdata/replica1 5433
rm -rf /tmp/pgdata/replica1
CHECKPOINT
pg_basebackup -h 127.0.0.1 -U snaga -D /tmp/pgdata/replica1 --xlog --progress --verbose
transaction log start point: 0/2000060 on timeline 1
36644/36644 kB (100%), 1/1 tablespace
transaction log end point: 0/2000128
pg_basebackup: base backup completed
standby_mode = 'on'
primary_conninfo = 'host=127.0.0.1 port=5432 user=snaga application_name=replica1'
`recovery.conf' -> `/tmp/pgdata/replica1/recovery.conf'
waiting for server to start.... done
server started
[snaga@devvm04 haproxy]$ 
同様に2台目、3台目のレプリカも設定します。(実行結果、出力は割愛)
[snaga@devvm04 haproxy]$ ./repli-config-add-replica.sh replica2 /tmp/pgdata/replica2 5434
[snaga@devvm04 haproxy]$ ./repli-config-add-replica.sh replica3 /tmp/pgdata/replica3 5435
レプリカの設定が終わったら、ut-stat-replicationスクリプトを使ってレプリケーションの状態を確認し、1台のマスターと3台のレプリカが動作していることを確認します。以下の例では、replica1, replica2, replica3 に対して、それぞれ非同期 async のストリーミング streaming でレプリケーションできていることが分かります。

(ut-stat-replicationスクリプトは、マスターのpg_stat_replicationビューを整形して表示するスクリプトです。)
[snaga@devvm04 haproxy]$ ./ut-stat-replication
****** Sun Feb 15 15:10:58 JST 2015 ******
  pid  |   name   |   addr    | port  |   state   |   sent    |   write   |   flush   |  replay   | pri |  mode
-------+----------+-----------+-------+-----------+-----------+-----------+-----------+-----------+-----+--------
       |          |           |       | local     | 0/EAFA2C0 | 0/EAFA2C0 |           |           |     | master
 17485 | replica1 | 127.0.0.1 | 35744 | streaming | 0/EAFA2C0 | 0/EAFA2C0 | 0/EAFA2C0 | 0/EAFA2C0 |   0 | async
 17539 | replica2 | 127.0.0.1 | 35752 | streaming | 0/EAFA2C0 | 0/EAFA2C0 | 0/EAFA2C0 | 0/EAFA2C0 |   0 | async
 17594 | replica3 | 127.0.0.1 | 35759 | streaming | 0/EAFA2C0 | 0/EAFA2C0 | 0/EAFA2C0 | 0/EAFA2C0 |   0 | async

■HAProxyの設定と起動

PostgreSQLのレプリケーションの設定ができたら、レプリカの間でロードバランスするためのHAProxyの設定、起動を行います。

HAProxyはPostgreSQLのヘルスチェックの機能を組み込みで持っていますので、それを利用してHAProxyの配下にぶら下げるレプリカの設定を行います。今回は以下のような設定にしています。

HAProxyが問い合わせを受け付けるポートは15432です。このポートに接続すると、PostgreSQLのコネクションがロードバランスされます。

設定ができたら repli-config-start-haproxy.sh スクリプトでHAProxyを起動します。 repli-config-start-haproxy.sh スクリプトは設定ファイルのコピーとHAProxyサービスの起動を内部で行っています。
[snaga@devvm04 haproxy]$ grep -A 4 pgsql haproxy.cfg
listen pgsql 0.0.0.0:15432
    mode tcp
    balance roundrobin

    # Add PostgreSQL replica here.
    option pgsql-check user snaga
    server replica1 127.0.0.1:5433 check inter 10000
    server replica2 127.0.0.1:5434 check inter 10000
    server replica3 127.0.0.1:5435 check inter 10000

[snaga@devvm04 haproxy]$ ./repli-config-start-haproxy.sh
Stopping haproxy:                                          [  OK  ]
Starting haproxy:                                          [  OK  ]
[snaga@devvm04 haproxy]$ 

■マスターに対するデータベース更新処理

それでは、まずはマスターに対してデータベースの更新処理を行ってみます。ここでは、ベンチマークツール pgbench のテーブルやデータの作成を行っています。

この更新処理はマスターに対して実行されていますので(ポート番号が5432になっていることに注意してください)、当然ながらその結果はレプリカにもレプリケーションされます。
[snaga@devvm04 haproxy]$ pgbench -h 127.0.0.1 -p 5432 -i -s 10 postgres
NOTICE:  table "pgbench_history" does not exist, skipping
NOTICE:  table "pgbench_tellers" does not exist, skipping
(...snip...)
900000 of 1000000 tuples (90%) done (elapsed 14.13 s, remaining 1.57 s).
1000000 of 1000000 tuples (100%) done (elapsed 15.54 s, remaining 0.00 s).
vacuum...
set primary keys...
done.
[snaga@devvm04 haproxy]$ 

■レプリカに対する参照処理

それでは、次にHAProxyを通してレプリカに対する参照処理を行ってみます。

ここでは、pgbench の SELECT Only で実行しています。(pgbenchの実行時に指定しているポート番号が、HAProxy が listen しているポート 15432 になっていることに注意してください。)
[snaga@devvm04 haproxy]$ nohup pgbench -h 127.0.0.1 -p 15432 -S -T 180 -c 16 -C -n postgres > log 2>&1 &
[1] 18123
[snaga@devvm04 haproxy]$
この時、ut-stat-xactスクリプトで各インスタンストランザクションを確認してみると、レプリカへのコネクションが同程度に増えて、それと同時に、コミットされたトランザクションが増加していることが確認できます。但し、マスター(ポート番号5432)への接続やコミットは増えていません。

(ut-stat-xactスクリプトは、複数のインスタンスの pg_stat_database ビューを検索して、並べて表示するスクリプトです)
[snaga@devvm04 haproxy]$ ./ut-stat-xact
****** Sun Feb 15 15:10:33 JST 2015 ******
 port | datname  | numbackends | xact_commit | xact_rollback
------+----------+-------------+-------------+---------------
 5432 | postgres |           1 |         208 |             0
 5433 | postgres |           1 |          30 |             0
 5434 | postgres |           1 |          30 |             0
 5435 | postgres |           1 |          30 |             0

****** Sun Feb 15 15:10:36 JST 2015 ******
 port | datname  | numbackends | xact_commit | xact_rollback
------+----------+-------------+-------------+---------------
 5432 | postgres |           1 |         212 |             0
 5433 | postgres |           5 |         234 |             0
 5434 | postgres |           7 |         234 |             0
 5435 | postgres |           6 |         237 |             0

****** Sun Feb 15 15:10:39 JST 2015 ******
 port | datname  | numbackends | xact_commit | xact_rollback
------+----------+-------------+-------------+---------------
 5432 | postgres |           1 |         218 |             0
 5433 | postgres |           6 |         459 |             0
 5434 | postgres |           7 |         460 |             0
 5435 | postgres |           5 |         462 |             0

■レプリカの障害

それでは、レプリカの間でロードバランスしている間に、レプリカを1台落としてみます。ここでは2台目のレプリカ replica2 のプロセスを kill しています。
[snaga@devvm04 haproxy]$ ps auxx | grep replica2
snaga    17527  0.2  0.6 230200  3736 pts/0    S    15:07   0:00 /usr/pgsql-9.4/bin/postgres -D /tmp/pgdata/replica2 -p 5434
snaga    20262  0.0  0.1   8392   868 pts/0    S+   15:10   0:00 grep replica2
[snaga@devvm04 haproxy]$ kill -9 17527
レプリカを落とすと当該インスタンスの情報が表示されなくなり、当該インスタンスへのレプリケーションが停止したことが分かります。
****** Sun Feb 15 15:10:58 JST 2015 ******
  pid  |   name   |   addr    | port  |   state   |   sent    |   write   |   flush   |  replay   | pri |  mode
-------+----------+-----------+-------+-----------+-----------+-----------+-----------+-----------+-----+--------
       |          |           |       | local     | 0/EAFA2C0 | 0/EAFA2C0 |           |           |     | master
 17485 | replica1 | 127.0.0.1 | 35744 | streaming | 0/EAFA2C0 | 0/EAFA2C0 | 0/EAFA2C0 | 0/EAFA2C0 |   0 | async
 17539 | replica2 | 127.0.0.1 | 35752 | streaming | 0/EAFA2C0 | 0/EAFA2C0 | 0/EAFA2C0 | 0/EAFA2C0 |   0 | async
 17594 | replica3 | 127.0.0.1 | 35759 | streaming | 0/EAFA2C0 | 0/EAFA2C0 | 0/EAFA2C0 | 0/EAFA2C0 |   0 | async

****** Sun Feb 15 15:11:01 JST 2015 ******
  pid  |   name   |   addr    | port  |   state   |   sent    |   write   |   flush   |  replay   | pri |  mode
-------+----------+-----------+-------+-----------+-----------+-----------+-----------+-----------+-----+--------
       |          |           |       | local     | 0/EAFA2C0 | 0/EAFA2C0 |           |           |     | master
 17485 | replica1 | 127.0.0.1 | 35744 | streaming | 0/EAFA2C0 | 0/EAFA2C0 | 0/EAFA2C0 | 0/EAFA2C0 |   0 | async
 17594 | replica3 | 127.0.0.1 | 35759 | streaming | 0/EAFA2C0 | 0/EAFA2C0 | 0/EAFA2C0 | 0/EAFA2C0 |   0 | async

このような状態でも、その他のノードはサービスを継続しています。
****** Sun Feb 15 15:10:59 JST 2015 ******
 port | datname  | numbackends | xact_commit | xact_rollback
------+----------+-------------+-------------+---------------
 5432 | postgres |           1 |         244 |             0
 5433 | postgres |           6 |        2195 |             0
 5434 | postgres |           7 |        2195 |             0
 5435 | postgres |           6 |        2199 |             0

****** Sun Feb 15 15:11:02 JST 2015 ******
 port | datname  | numbackends | xact_commit | xact_rollback
------+----------+-------------+-------------+---------------
 5432 | postgres |           1 |         248 |             0
 5433 | postgres |           7 |        2266 |             0
psql: could not connect to server: Connection refused
        Is the server running locally and accepting
        connections on Unix domain socket "/tmp/.s.PGSQL.5434"?
 5435 | postgres |           6 |        2265 |             0

(...snip...)

****** Sun Feb 15 15:11:22 JST 2015 ******
 port | datname  | numbackends | xact_commit | xact_rollback
------+----------+-------------+-------------+---------------
 5432 | postgres |           1 |         272 |             0
 5433 | postgres |           5 |        2324 |             0
psql: could not connect to server: Connection refused
        Is the server running locally and accepting
        connections on Unix domain socket "/tmp/.s.PGSQL.5434"?
 5435 | postgres |           5 |        2328 |             0

****** Sun Feb 15 15:11:25 JST 2015 ******
 port | datname  | numbackends | xact_commit | xact_rollback
------+----------+-------------+-------------+---------------
 5432 | postgres |           1 |         276 |             0
 5433 | postgres |           6 |        2596 |             0
psql: could not connect to server: Connection refused
        Is the server running locally and accepting
        connections on Unix domain socket "/tmp/.s.PGSQL.5434"?
 5435 | postgres |           4 |        2601 |             0

■レプリカの復旧

最後に、障害の発生したレプリカを復旧してみます。

レプリカを復旧させる方法にはいくつか選択肢がありますが、ここではシンプルに最初にレプリカを設定した時と同様に repli-config-add-replica.sh スクリプトを使って再度クラスタに参加させます。
[snaga@devvm04 haproxy]$ ./repli-config-add-replica.sh replica2 /tmp/pgdata/replica2 5434
rm -rf /tmp/pgdata/replica2
CHECKPOINT
pg_basebackup -h 127.0.0.1 -U snaga -D /tmp/pgdata/replica2 --xlog --progress --verbose
transaction log start point: 0/F000028 on timeline 1
189938/189938 kB (100%), 1/1 tablespace
transaction log end point: 0/F0000F0
pg_basebackup: base backup completed
standby_mode = 'on'
primary_conninfo = 'host=127.0.0.1 port=5432 user=snaga application_name=replica2'
`recovery.conf' -> `/tmp/pgdata/replica2/recovery.conf'
waiting for server to start.... done
server started
[snaga@devvm04 haproxy]$ 
この時、ut-stat-replicationの出力を見ると、ベースバックアップを取得した後、再度、非同期レプリケーションでレプリカとして登録されたことが分かります。
[snaga@devvm04 haproxy]$ ./ut-stat-replication
****** Sun Feb 15 15:11:53 JST 2015 ******
  pid  |     name      |   addr    | port  |   state   |    sent    |   write   |   flush   |  replay   | pri |  mode 
-------+---------------+-----------+-------+-----------+------------+-----------+-----------+-----------+-----+--------
       |               |           |       | local     | 0/10000028 | 0/FC00000 |           |           |     | master
 17485 | replica1      | 127.0.0.1 | 35744 | streaming | 0/F0000C8  | 0/F0000C8 | 0/F0000C8 | 0/F000000 |   0 | async
 24628 | pg_basebackup | 127.0.0.1 | 47933 | backup    | 0/0        | 0/EAFA2C0 | 0/EAFA2C0 | 0/EAFA2C0 |   0 | async
 17594 | replica3      | 127.0.0.1 | 35759 | streaming | 0/F0000C8  | 0/F0000C8 | 0/F0000C8 | 0/F000000 |   0 | async

****** Sun Feb 15 15:11:56 JST 2015 ******
  pid  |   name   |   addr    | port  |   state   |    sent    |   write    |   flush    |   replay   | pri |  mode
-------+----------+-----------+-------+-----------+------------+------------+------------+------------+-----+--------
       |          |           |       | local     | 0/10000060 | 0/10000060 |            |            |     | master
 17485 | replica1 | 127.0.0.1 | 35744 | streaming | 0/10000060 | 0/10000000 | 0/10000000 | 0/10000000 |   0 | async
 24920 | replica2 | 127.0.0.1 | 48445 | streaming | 0/10000060 | 0/10000000 | 0/10000000 | 0/10000000 |   0 | async
 17594 | replica3 | 127.0.0.1 | 35759 | streaming | 0/10000060 | 0/10000000 | 0/10000000 | 0/F0000C8  |   0 | async

■デモビデオ

ここまで紹介してきたシナリオで実際に動作させているデモビデオは以下です。6分ほどの動画です。

Loadbalancing PostgreSQL with HAProxy from uptimejp on Vimeo.

HDの動画になっていますので、全画面表示などで見ることをお勧めします。この動画を見ると、実際にどのように動作しているのかが分かると思います。

■まとめ

HAProxyをL4のロードバランサとして使うことで、PostgreSQLのロードバランサとして機能させることができることが分かりました。

また、障害が発生した場合であっても、クラスタ全体としてはサービスを継続しつつ、復旧後に再度クラスタに参加させることが可能であることも確認できました。

実際に使う場合にはもう少し細かな検証が必要になるかと思いますが、PostgreSQLにおける負荷分散の実現方法の一つとしてHAProxyという選択肢があるのではないか、ということがお分かりいただけたのではないかと思います。

HAProxyについては、今後ももう少し研究を続けてみたいと思います。

では、また。

1 件のコメント: