同期レプリケーション?
PostgreSQL9.1以降でストリームレプリケーションを設定するとき、postgresql.confでsynchronous_commitをon(デフォルト)にしておくと、同期レプリケーションで動作します。
「同期」と聞くと、以下のような動作じゃないかと思いますよね。例としてinsertを考えます。また、単純化するためにスレーブも1台とします。
- アプリケーションからマスターDBにinsertを発行
- マスターDBがinsertを実行。自身に反映される
- insertの内容がスレーブDBに送られる
- スレーブDBにinsertの内容が反映される
- マスターはアプリに「insert完了」と通知する
insertが全てのノードに適用されて初めて、アプリは「insert成功」と認識するので「同期レプリケーション」だ!
・・はい、残念ながら違います。
もし上記のような動作であれば、アプリケーションからinsertを発行し、完了直後に「スレーブDBに」selectを投げたらinsertしたデータが見えるはずですよね?
しかし、Let's Postgresql!では以下のように書いてあります。
同期モードであっても、更新直後にスレーブで参照クエリ実行すると、まだ更新が反映されていない場合があります。
(http://lets.postgresql.jp/documents/technical/9.1/1)
えー?
実際、私もこの現象に遭遇しました。
ログシッピングであるということ
PostgreSQLのストリーミングレプリケーションはWALの転送によるレプリケーションです。
ですので、上記のinsertの動作はもう少し細かく書くと以下のようになります(タイミングや用語がちょっと違うかもしれませんが・・)
- アプリケーションからマスターDBにinsertを発行
- マスターDBがinsertの内容をWALに書き込む
- マスターDBが自身にinsertの内容を反映
- WALがスレーブDBに転送される
- スレーブDBは受け取ったWALをディスクに書き込む
- スレーブDBはWALの書き込み完了時点でマスターDBに「受信した」と返答
- マスターDBはスレーブDBからの受信完了通知を受けて、アプリに「insert完了」と通知
- スレーブは受信したWALを自身に反映する(この時点でinsertの内容が反映される)
つまり、アプリがinsert完了したと思った時点ではスレーブDBがそのinsertに関するWALは受信したことは保証されている(同期レプリケーション)が、WALの内容が自身に反映されているかは保証されないのです。
同時に転送されているWALの量やスレーブDBの負荷状況によってはこの「受信から適用」までの時間は延びる可能性があることは容易に想像できます。
結論と回避方法
ほとんどのケースでは上記の時間差は問題にならず、「insert直後にスレーブでselect」をしてもちゃんとデータは見えると思います。
ただ、上記のような事がタイミング次第ではあり得る、ということはアプリケーション設計時に憶えておいた方がいいでしょう。「insertしたデータをすぐにselectしてserialを取得しておき、そのserialに基づいてデータをいじる」というような処理は結構ありがちですので気をつけましょう。
じゃあどうやって回避するのか?
一番楽ちんなのはpgpool使うことでしょうか。pgpoolだと、「更新系SQLはマスターに送り、参照系SQLはスレーブに送る」ということを自動でやってくれますが、その中で、絶対にスレーブに飛んで欲しくない参照系SQLの前に
/* NO LOAD BALANCE */
というコメントを入れてやればOKです。そうするとこのSQLは絶対にマスターに飛びます。
もしくは、明示的なトランザクションで囲ってもOKです。insertの失敗に備えるのであればトランザクション使う方がいいでしょうね。