PostgreSQLのレプリケーションのコンフリクトについて言いたい



# 経緯
@soudai1025 さんの以下のエントリを読んでいて、

**[PostgreSQLのレプリケーションのコンフリクトについて](http://soudai.hatenablog.com/entry/2017/12/25/104556)**

補足したくなり、

<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr">細かいけど、実際にコンフリクトするのはスレーブ側でのSELECTとそのSELECTしている行を物理削除するWALの適用（その他にもある）。SELECTのロックとUPDATEのロックは通常のクエリ処理でも競合しないようになっている。<br><br>PostgreSQLのレプリケーションのコンフリクトについて<a href="https://t.co/O4kEcoUVR3">https://t.co/O4kEcoUVR3</a></p>&mdash; Masahiko Sawada (@sawada_masahiko) <a href="https://twitter.com/sawada_masahiko/status/945980328262254592?ref_src=twsrc%5Etfw">December 27, 2017</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
とか
<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr">マスタ側での物理削除を遅らせるために、vacuum_defer_cleanup_ageではマスタ側が、消せるようになった行も少し待って（あとで）物理削除するようなる。レプリケーションスロットとか、hot_standby_feedbackでは、</p>&mdash; Masahiko Sawada (@sawada_masahiko) <a href="https://twitter.com/sawada_masahiko/status/945981224832471042?ref_src=twsrc%5Etfw">December 27, 2017</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
とか色々tweetしていると、
<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr">そういうのはエントリー書いてトラバすると色んな人が喜ぶと思います。<br>またはPostgreSQL公式ドキュメントに書く。</p>&mdash; そーだい@初代ALF (@soudai1025) <a href="https://twitter.com/soudai1025/status/945989800997220352?ref_src=twsrc%5Etfw">December 27, 2017</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

とアドバイスをいただいたので、ドキュメントの[この辺の内容](https://www.postgresql.jp/document/9.6/html/hot-standby.html#hot-standby-conflict)をまとめておきます。

# なにがコンフリクトするか
スタンバイでは、マスタから転送されるWALを適用しながら、クライアントからのSQLを受付けるので(hot_standby = trueの場合)、よくある例として、**スレーブでの参照**と**スレーブが参照しているタプルを物理削除するWAL(VACUUM等で出力される)の適用**がコンフリクトします。

その他にも、以下のようなものがあります。

* マスタでの排他ロック(AccessExclusiveLock)とスタンバイでの参照（AELを取得するとそれに対応するWALがでます）
* マスタでのデータベース削除と、スタンバイでのそのデータベースに接続するセッション 
など

(※)
PostgreSQLはSELECTでもロックを取得します。しかし、SELECTで取得されるロックはAccessShareLockというロックで、UPDATE/DELTE/INSERTで取得されるロックであるRowExclusiveLockとはコンフリクトしません。そのため、レプリケーションでのコンフリクトも起きませんし、通常のクエリ処理でもコンフリクトは起きません。

https://www.postgresql.jp/document/9.6/html/explicit-locking.html#table-lock-compatibility

# コンフリクトを防ぐ
コンフリクトを防ぐために、マスタは**スタンバイを忖度しながら**物理削除するタプルを決定する必要があります。
その方法として、以下の方法があります。
1. vacuum_defer_cleanup_age
2. hot_standby_feedbackやレプリケーションスロットの使用

## 1. vacuum_defer_cleanup_ageパラメータ(トランザクション数を設定する)
これは、マスタが**タプルの物理削除を遅延する**というものです。例えば、物理削除可能になった(DELETE/UPDATE後、そのタプルをみてるトランザクションが居なくなった）タプルに対して、VACUUMは通常すぐに物理削除しますが、vacuum_defer_cleanup_ageが設定されている場合は、**後で**物理削除するようになります。
パラメータには遅延するトランザクション数を指定するので、マスタは「直近削除されたタプルではなく、○○トランザクション前に削除されているタプルを物理削除しよう」と判断します。
なので、vacuum_defer_cleanup_ageはマスタ側で設定します。

## 2. hot_standby_feedback(on/off)やレプリケーションスロットの使用
一方、こちらは、スタンバイが自身の**活動状況をマスタに報告する**ことで、マスタは「このタプルはスタンバイが見ている可能性があるから物理削除しないでおいておこう」と判断することができるようになります。
なので、hot_standby_feedbackはスタンバイ側で設定します。

# コンフリクトが発生したらどっちを待たせるか
コンフリクトが発生した場合、コンフリクトしているクエリをいつキャンセルするか（もしくはキャンセルしないでWAL適用を待たせる）をmax_standby_streaming_delayで設定することができます。

* -1 : コンフリクトが発生してもクエリをキャンセルせず、WAL適用をクエリが完了するまで待たせる。
* 0以上  : コンフリクト後、指定した秒数後にクエリをキャンセル

例えばスタンバイに重い分析系のクエリを投げたり、論理バックアップを取得する時とかで、クエリ処理側を優先したいときには-1にします。

# マスタ側のWALがなくなってしまうのは別の問題
物理削除を遅延させた場合、**マスタでテーブルが肥大化**する可能性があり、スタンバイでのクエリを優先した場合は、**スタンバイでWALが溢れる**可能性があります。
ただし、どちらの場合でも、マスタでのVACUUMやWALの送信が止まったり、スタンバイのWAL受信が止まったりすることはありません。

マスタ側のWALがなくなってしまうのは別の問題で、例えばWALの転送遅延が発生し、スタンバイに送る前にマスタ側でWALを循環(リサイクル)させてしまい、スタンバイが必要としているWALがなくなってしまう、みたいな時に起きます。
これは、当該エントリで説明されているようにレプリケーションスロットを使うことで対処できます。

# おわりに
公式ドキュメントを書くときには、もう少し分かりやすく書くように気をつけよう。