今回は,筆者が開発しているpgpoolというソフトを紹介する。

 記事のタイトルを見て「おや,pgpoolはコネクションプール・ソフトだったはず?」と首をかしげる読者もいると思う。確かにpgpoolはコネクションプール・ソフトとして誕生した。しかし,進化の結果,ついにレプリケーションの機能も備えるに至ったのである。そこで今回は,レプリケーションを中心に,pgpoolの新機能を説明する。

レプリケーションとは

 本題に入る前に,まずレプリケーションとは何か,なぜレプリケーションが必要かを確認しよう。

 レプリケーション(replication)とは,その名の通り,複製を作ることを指す。データベース・システムの場合は,データベースの一部または全部を複製するのがレプリケーションということになる。もちろん,データベースのバックアップを取得し,別なサーバにリストアすればデータベースの複製ができるが,これはレプリケーションではない。レプリケーションと言うからには,刻々と変化するデータベースの内容を複製先に直ちに反映させることが求められる。

 レプリケーションによる第一のメリットは障害に強くなることである.データベースが破損した場合,通常バックアップからデータベースを復元するが,これには多大な時間がかかるし,バックアップを取得した時点以降に加えられた新しい更新は失われてしまう。レプリケーション機能があれば,複製側のデータベースに切り替えるだけで業務を再開できるし,直近のデータが失われることもない。

 レプリケーションの第二のメリットは負荷分散である.問い合わせの一部を複製先のデータベースに振り向けるようにすれば,マスタ側のデータベースの負荷を軽減することもできる.ただし,すべてのレプリケーションソフトが負荷分散の機能を備えているわけではない。pgpoolも現状では負荷分散には対応していない。

pgpoolのレプリケーション機能

図1●pgpoolのアーキテクチャ
 pgpoolでは,「マスタ」と呼ばれる主データベース・クラスタと「セカンダリ」と呼ばれる複製先データベース・クラスタを用意する.つまり,2つのpostmasterを起動して片方をマスタ,もう一方をセカンダリにする。2つのデータベース・クラスタの内容は最初はまったく同じであるものとする。postmasterとpgpoolは同じホストで動いていても構わないし,別々のホストで動いていても構わない。

 アプリケーションは常にpgpoolを経由して2つのpostmasterにアクセスする。ただし,pgpoolはアプリケーションから見ると普通のPostgreSQLに見えるので,pgpoolを使っていることを意識する必要はない。

 問い合わせはマスタとセカンダリの両方に送られるので,データベースの内容は両者の間で常に一致することになる。データベースの検索結果については常にマスタ側の結果のみが採用される(図1[拡大表示])。

pgpoolのフェイルオーバー機能

図2●pgpoolのフェイルオーバー機能
 pgpoolには,マスタあるいはセカンダリに障害が発生したときに自動的に障害が発生したデータベース・クラスタを切り離して縮退運転に切り替える機能がある。このような機能を一般に「フェイルオーバー」と呼ぶ。フェイルオーバーにより,サーバー障害によって業務が停止する時間を最小限にとどめることができる(図2[拡大表示])。

pgpoolのインストール

 pgpoolのサポートページはhttp://www2b.biglobe.ne.jp/~caco/pgpool/である。ソースコードとVine Linux用のRPM/SRPMがここで入手できる。本稿執筆時点の最新版はpgpool-1.0である。

 RPMのインストール方法については言うまでもないと思うので,ここではソースからインストールする方法をご紹介する。コンパイルのためにはgccやmakeが必要になるが,PostgreSQLのソースツリーやライブラリは不要である。

 pgpoolはconfigureを使用しており,LinuxやFreeBSDならそのままコンパイル,インストールできるはずだ。ここでは,Vine Linux上にインストールする例を示す。ソースコードは /tmp/pgpool-1.0.tar.gzにあらかじめダウンロードしてあるものとする。コンパイルするディレクトリはどこでもよいが,ここでは/usr/local/src/とする。以下,rootで実行する。


# cd /usr/local/src
# tar xfz /tmp/pgpool-1.0.tar.gz
# cd pgpool-1.0
# ./configure
# make
# make install
 これで,プログラム本体が/usr/local/bin/pgpoolに,設定ファイルのテンプレートが/usr/local/etc/pgpool.conf.sampleにインストールされる。

pgpoolの設定

 テンプレートから設定ファイルをコピーする。


# cp /usr/local/etc/pgpool.conf.sample /usr/local/etc/pgpool.conf
 デフォルトの設定では,レプリケーションを行わないことになっている。レプリケーションを実行するための設定をしよう。なお,今回は手軽に実験できると言うことで,1台のマシンでpgpool,2本のpostmasterを動かすものとする。

 /usr/local/etc/pgpool.confの以下をviなどの適当なエディタで編集する。

(1) セカンダリ・バックエンド(2本目のpostmaster)の設定

 まずセカンダリ・バックエンドが動くホストを指定する。


secondary_backend_host_name = ''
 今回は同一ホストでの起動なので,デフォルトのままで良い。他ホストでpostmasterが動いている場合は

secondary_backend_host_name = 'another_host'
 などと,ホスト名を指定する(IPアドレスを書いても良い)。次にそのpostmasterがコネクションを受け付けるポート番号を指定する。

secondary_backend_port = 0
 デフォルトでは0になっているので,これを適当に変える。今回は同一ホストで2つのpostmasterを動かすため,PostgreSQLのデフォルトの5432では都合が悪いので5433とする(もちろん5444など,5433以外の番号でも構わない)。

secondary_backend_port = 5433
 最後にレプリケーション機能を有効にする。

replication_mode = false
 となっているのを

replication_mode = true
 にすればよい。以上でpgpool.confの修正は完了である。

# pgpool -n

 でpgpoolを起動し,何もエラー・メッセージが出力されなければ正常に起動できている。なお,ここではrootでpgpoolを起動しているが,できればroot以外のユーザで起動する方がセキュリティ上の観点からは望ましいと言える。

postmasterの起動

 次にpostmasterを起動する。もしまだPostgreSQLをインストールしていないのであれば,そのインストールが当然必要になるが,ここでは省略する。PostgreSQLのマニュアルや適当な参考書をごらんいただきたい。

 pgpoolはPostgreSQL 7.2.x, 7.3.x,7.4.xで動作実績があるので,このうちのどのバージョンを使用しても良い。以後PostgreSQLはデフォルトの/usr/local/pgsqlにインストール済で,PostgreSQLのスーパユーザはpostgresであるものとする。

 pgpoolでは,初期状態で2つのPostgreSQLのデータベース・クラスタの内容が一致していることが必要である.一番確実な方法は新たにデータベース・クラスタを作成することであり,今回は実験なのでこの方法を採用することにしよう。

 マスタ側のデータベース・クラスタは/usr/local/pgsql/data1,セカンダリ側は/usr/local/pgsql/data2 とする。

 以下,postgresユーザで実行する。もしすでにpostmasterが起動済であれば停止しておく。


$ initdb -E EUC_JP --no-locale -D /usr/local/pgsql/data1
$ initdb -E EUC_JP --no-locale -D /usr/local/pgsql/data2
$ postmaster -D /usr/local/pgsql/data1
$ postmaster -p 5433 -D /usr/local/pgsql/data2
 最初のpostmasterがマスタ側で,ポート5433側がセカンダリになる。念のため,

$ psql -l
$ psql -p 5433 -l

 が正常に実行できることを確認しておく。

pgpoolの利用

 以上で準備ができたので,さっそくpgpoolにアクセスしてみよう。以下,postgresユーザで実行するものとする。まずデータベースを作ってみよう。


$ createdb -p 9999 test
 これでcreatedbがpgpoolにアクセスし,pgpoolは2つのpostmasterに接続してデータベースを作成したはずだ。確認するためには,2つのpostmasterに直接アクセスしてみれば良い。

$ psql test
$ psql -p 5433 test

 の両方でtestデータベースにアクセスできればレプリケーションされていることになる。

 もちろんテーブルを作ってデータを登録し,検索することもできる。


$ psql -p 9999 test
test=# CREATE TABLE t1(i INTEGER, t TEXT);
CREATE TABLE
test=# INSERT INTO t1 VALUES(1, 'foo');
INSERT 1184657 1
test=# SELECT * FROM t1;
 i |  t  
---+-----
 1 | foo
(1 row)
 これも2つのpostmasterに直接アクセスして確認してみよう。

$ psql test
test=# SELECT * FROM t1;
 i |  t  
---+-----
 1 | foo
(1 row)

$ psql -p 5433 test
test=# SELECT * FROM t1;
 i |  t  
---+-----
 1 | foo
(1 row)
 無事にレプリケーションできているようだ。

縮退運転とフェイルオーバー

 前述のように,pgpoolには縮退運転の機能がある。たとえば,セカンダリのpostmaster(今回は5433ポートで動いている方)が停止した場合にはセカンダリを切り離してレプリケーションなしで運用を継続する.実際に試してみよう。

 まずセカンダリ側を停止する。

$ pg_ctl -D /usr/local/pgsql/data2 -m i stop

 ここでpg_ctlの引数に-miを指定しているのは,pgpoolがコネクション・プールを行い,接続を持続しているからである。psqlが終了してもPostgreSQLへのコネクションは切断されず,PostgreSQLから見るとあたかもクライアントが接続中のままに見えるため,強制切断を行う-m iが必要になったわけだ。

 この状態でpgpoolにアクセスすると,


$ psql -p 9999 test
psql: server closed the connection unexpectedly
	This probably means the server terminated abnormally
	before or while processing the request.
となりエラーになる。しかし,心配はいらない。もう一度アクセスすれば今度は接続に成功するはずである。

psql -p 9999 test
Welcome to psql 7.3.6, the PostgreSQL interactive terminal. Type: \copyright for distribution terms \h for help with SQL commands \? for help on internal slash commands \g or terminate with semicolon to execute query \q to quit test=#
 このように,2回目で接続に成功した。このとき,pgpoolを起動した端末には,

ERROR: pid 1559: pool_flush: fflush failed (Broken pipe)
log: pid 1534: starting degenration. shutdown secondary host (5433)
log: pid 1534: degenration done. shutdown secondary host (5433)

 のようなメッセージが表示され,セカンダリを切り離したことが分かる。

 ではマスタ側がダウンした場合にはどうなるだろうか?この場合は2回のエラーの後,セカンダリ側だけの運転にはいる.これは,縮退運転では必ずセカンダリを切り離すからである。そしてもう一度マスタ側にアクセスしたときに今度はフェイルオーバー機能が働き,ここでようやくセカンダリ側がマスタにとって代るのである(ログ参照)。


ERROR: pid 1559: pool_flush: fflush failed (Broken pipe)
log: pid 1534: starting degenration. shutdown secondary host (5433)
log: pid 1534: degenration done. shutdown secondary host (5433)

ERROR: pid 1614: connect_unix_domain_socket: connect() failed: No such file or directory
log: pid 1534: starting failover from (5432) to (5433)
log: pid 1534: failover from (5432) to (5433) done.


 新マスタ(ポート5433)がダウン,そしてたまたま旧マスタ(ポート5432)側が立ち上がっていたらどうなるだろうか?この場合,再度ポート5432側がマスタに取って代わることはない。ダウン後のデータベース・クラスタの状態はシステム的には問題ないかも知れないが,業務として見るとダウン中に失われたデータによって一貫性のない状態になっているかも知れないからである。

 こうした場合,人手を介入せずにデータベースの運用を続けるのは危険であるという考え方から,pgpoolではあえて再フェイルオーバーしないようにしている。

pgpoolの状態確認

 このようにpgpoolが自動的に縮退運転やフェイルオーバーを行うのは便利ではあるが,逆にpgpoolの現在の状態がわかりにくくなる。そこで,pgpoolにはステータスを取得する機能がある。


test=# show pool_status;
            item             | value |               description        
-----------------------------+-------+-----------------------------------------------------------------------
 inetdomain                  | 0     | 1 if accepting TCP/IP connection
 port                        | 9999  | pgpool accepting port number
 socket_dir                  | /tmp  | pgpool socket directory
 backend_host_name           |       | master backend host name
 backend_port                | 5432  | master backend port number
 secondary_backend_host_name |       | secondary backend host name
 secondary_backend_port      | 5433  | secondary backend port number
 num_init_children           | 32    | # of children initially pre-forked
 child_life_time             | 0     | if idle for this seconds, child exits (not implemented yet)
 connection_life_time        | 0     | if idle for this seconds, connection closes
 max_pool                    | 4     | max # of connection pool per child
 logdir                      | /tmp  | logging directory
 backend_socket_dir          | /tmp  | Unix domain socket directory for the PostgreSQL server
 replication_mode            | 1     | non 0 if operating in replication mode
 replication_strict          | 0     | non 0 if operating in strict mode
 replication_timeout         | 5000  | if secondary does not respond in this milli seconds, abort the session
 current_backend_host_name   |       | current master host name
 current_backend_port        | 5432  | current master port #
 replication_enabled         | 1     | non 0 if operating in replication mode
(19 rows)
 このように,SQLコマンドを使ってpgpoolの内部状態を表示できる(もちろんこのSQLコマンドは「本物」ではなく,pgpoolがそのように見せているだけである)。最後の3行以外は設定ファイルの内容である。最後のreplication_enabledが1ならばレプリケーションモードで運転中ということになる。replication_modehが1であるにも関わらず,これが0なら縮退運転中ということになる。そのときに使用中のpostmasterはcurrent_backend_host_name(空白ならUnixドメインソケットでの接続),current_backend_portでわかる。

縮退運転からの復旧

 PostgreSQLサーバーが故障して縮退運転に入り,その後故障したサーバーを修理してレプリケーション運転に復帰するためには,まず2つのデータベース・クラスタの内容を一致させる必要がある。そのための手順は以下のようになる。

(1) pgpoolを停止する

(2) 生きている方のpostmasterを停止する

(3) 生きていた方のデータベース・クラスタの内容を,rsyncなどを使ってファイル・システム・レベルで,止っていた方にコピーする

(4) マスタとセカンダリのpostmasterを起動する

(5) pgpoolを再起動する

デッドロック対策

 pgpoolは,更新系の問い合わせをマスタ,セカンダリの順に発行することによってレプリケーションを行う。その再,マスタでの更新SQLの完了を待たずにセカンダリにリクエストを投げることにより,処理効率を高めることができる。

 しかし,この方式では,単独のPostgreSQLでは発生しないデッドロックが起きることがある。たとえば,pgpoolに2つのセッション(0と1)が接続中のときに,「LOCK t1」コマンドが相次いで2つのセッションから発行されたとしよう。タイミングによっては以下のようになることがある。


           master                   secondary
   session 0    session 1       session 0   session 1
   ------------------------------------------------------
   lock	
                lock
                                            lock
                                lock
   ------------------------------------------------------
 session1のmasterはsession0のmasterを待っている。一方secondaryではsession1が先にロックを獲得したため,session0がsession1を待ち続けている。このようにしてお互いにお互いを待ち続けるデッドロック状態が発生する。

 pgpoolでは,この問題に対処するため,以下の2つの方法を用意している。

(1) pgpool.confのreplication_strictをtrueにする

 こうすると,常にsecondaryはmasterの問い合わせ処理が終わってから問い合わせを処理するのでデッドロックは発生しない。しかし,masterとsecondaryの並列処理ができないペナルティがある(それがどの程度のものであるかは後述のベンチマークを参考にしていただきたい)。

(2) replication_strictをfalseにした上で,デッドロックが発生する可能性のある問い合わせの先頭に特別なキーワード「/*STRICT*/」を入れる


   /*STRICT*/ LOCK TABLE t1;
 このように「/*STRICT*/」をSQLコメントとして挿入すると,この問い合わせだけはmasterとsecondaryが並列処理しなくなるので,デッドロックが起きない。必要な個所だけ/*STRICT*/を記入すればパフォーマンスの低下を最小限に押さえられる。

 なお,以上の対策を忘れて万が一デッドロック状態になっても, タイムアウト処理により,該当セッションが強制切断されるので,pgpoolが永久にフリーズすることはない。

pgpoolの制限事項

 pgpoolは同じ問い合わせを別々のデータベースサーバに発行することによってレプリケーションを行う。そのため,全く同じ問い合わせでも結果が異なるようなある種の問い合わせは正確にレプリケーションされない可能性がある。そのようなものの例としては,乱数,CURRENT_TIMETSTAMPなどの時刻関係の問い合わせがある。

pgpoolの性能

 レプリケーションでは,更新性能が気になるところである。一般にレプリケーションは複数台のデータベースを更新しなければならないため,どうしても単独で動くPostgreSQLよりも性能が低下する。できればこのペナルティは最小限であることが望ましい。そこで,汎用のベンチマーク・ソフトpgbenchを使ってpgpoolの性能を計ってみた。測定環境は以下の通りである。

ハードウェア: Pentium4 2.4GHz x2/メモリ 1GB/ハードディスク IDE 80GB
ソフトウェア: RedHat Linux 9/PostgreSQL 7.3.6

 PostgreSQLの設定は以下のようになっている。


tcpip_socket = true
max_connections = 512
shared_buffers = 2048

 これ以外はデフォルトのままである。

図3●ベンチマーク用システムの構成
 テストは同じハード/ソフトのPCを4台使い,以下のように役割を分担している(図3[拡大表示])。

ホストA: pgbench
ホストB: pgpool
ホストC: PostgreSQL(マスタ側)
ホストD: PostgreSQL(セカンダリ側)

 pgbenchのテスト・データベースはスケール・ファクタ1(10万件)の設定である。

(1) 更新系のテスト

 pgbenchの-Nオプションを利用して,検索,更新(UPDATE,INSERT)を交えたトランザクションを1秒あたり何回実行できるかで性能を評価する。テストはpgbenchの同時接続数を1, 2, 4 , 8, 16, 32, 64, 128に変えながら,各接続あたり100トランザクションを実行する。なお,pgbenchの1ランごとに毎回pgbench -iを発行してテストデータベースを初期化している。

図4●更新系のベンチマーク・テスト
 結果をグラフ1に示す。結果を見ると,pgpoolでレプリケーションを行った場合,pgpoolを使わない場合と比べて最悪の場合(同時接続ユーザ数1)で48.5%の性能,最良の場合(同時接続ユーザ数32)で81%の性能が得られた(図4[拡大表示])。平均すると65.8%の性能で,pgpoolのオーバヘッドやレプリケーションによる影響はほぼ1/3程度と言える。

 参考のためにreplication_strictをfalseにし,UPDATEコマンドに/*STRICT*/コメントを入れたものも測定してみた(「non strict mode」)。同時接続数が32のときにはオーバヘッドが20%以下と効果が見られたが,一方で64以上ではかえって性能が低下した。原因は不明だが,処理効率がよくなったためにかえってデータベース・サーバーに負荷がかかり過ぎているのかも知れない。

(2) 検索系のテスト

 本来レプリケーションの必要のない検索系では,pgpoolによるオーバーヘッドが気になるところである。そこでpgbenchを使って検索のみの性能測定も行ってみた。表に結果を示す。測定条件はすべて同じで,同時接続数10,各接続あたり1000トランザクションを実行している。

表1●検索系のテスト

測定条件 TPS値 pgpoolなしの場合と比較した相対性能
pgpoolなし 4367.12307 100%
pgpoolあり(レプリケーションなし) 4108.920919 94.10%
pgpoolあり(レプリケーションあり) 3823.882671 87.60%
pgpoolあり(レプリケーションあり,strictモード) 3558.758122 81.50%

このように,オーバヘッドは20%以下であることがわかった。

他のレプリケーション・ソフトとの比較

  • Usogres(http://usogres.good-day.net/)
     pgpoolと同様,問い合わせをそのままコピーするタイプのレプリケーション・ソフトであるが,フェイルオーバーやコネクション・プールの機能はない。2002年を最後に開発が止まっている。

  • DBMirror
     PostgreSQLのcontribに付属するレプリケーション・ソフトである。トリガを使って更新データを収集,レプリケーション側で非同期に更新処理を実行する方式である。負荷が高くなるとレプリケーションが追い付かない欠点がある。また,ラージ・オブジェクトのレプリケーションはできない。

  • QueryMaster
     商用のレプリケーション・ソフトである。pgpoolと同じように問い合わせをコピーすることによってレプリケーションを行う。負荷分散や運用中に障害データベースを復旧する機能がある。

  • PGCluster(http://www.csra.co.jp/~mitani/jpug/pgcluster/)
     負荷分散機能や,運用中に障害データベースを復旧できるなど,高度な機能を備えたオープンソースのレプリケーション・ソフトである。また必要ならば商用サポートを受けることもできる(http://powergres.sra.co.jp)。ラージ・オブジェクトのレプリケーションはできない。

     pgpoolが他のソフトに勝る点としては,ラージ・オブジェクトのレプリケーションが可能であること,更新性能が比較的良いことが上げられる。一方,pgpoolは負荷分散ができないので,サーバー・ハードウエアを増やして検索性能を狙うような使い方はできない。

    今後の課題

     pgpoolには縮退運転やフェイルオーバーの機能があるため,1系統のPostgreSQLのダウンには対応できる。しかし,pgpool自体がダウンすると,PostgreSQLが生きていても業務が止ってしまうことになる。もちろんpgpoolを2つ動かし,負荷分散装置を使うことによってこの問題を回避できるが,できればpgpool自体に相互監視機能がついていることが望ましい。

     pgpoolは,シンプルかつ手軽に使え,オーバーヘッドが少ないレプリケーション・ソフトである。pgpoolを活用することによって,データの保全要求が厳しい分野にもPostgreSQLを適用できる可能性が広がると考えている。pgpoolを活用していただければ幸いである。

    石井達夫(Tasuo Ishii)

    ■著者紹介
    石井達夫(いしい・たつお)氏
    1984年,SRA入社。主にUNIX関連 の開発に従事するかたわら,95年からPostgreSQLのメーリング・リストを主宰。現在はオープンソースソリューション部でPostgreSQL関連のビジネス活動を技術支援。著書に『PostgreSQL完全攻略ガイド』(技術評論社),『PHPxPostgreSQLで作る最強Webシステム』(技術評論社),『PostgreSQL構築・運用ガイド』(日経BP,共著)などがある。日本PostgreSQLユーザ会理事 長。