「つながっている」と「追いついている」は違う
一行まとめ
レプリケーション遅延は受信と適用の間で起きることが多く、その間はpg_stat_replicationの時間の数字よりもLSNのバイト差のほうが正直に見せてくれます。そして、WALを引き留める仕組み(スロット・フィードバック)は、どれもプライマリサーバーにコストを押し付けます。
なぜこれが必要だったのか
スタンバイサーバーが「streaming」で動いていれば、誰もがレプリケーションは正常だと言います。ところが、レポートをスタンバイサーバーから取り出しているチームが、ある日「明け方以降のデータがない」と言い出します。接続は正常で、プライマリサーバーが送ったWALもスタンバイサーバーのディスクにすべて届いています。**適用だけがされていないのです。**逆向きの事故もあります。スタンバイサーバーを削除するときにスロットを残しておくと、プライマリサーバーはそのスタンバイサーバーがいつか戻ってくると思ってWALを消しません。数週間後、プライマリサーバーのディスクがいっぱいになり、書き込みが止まります。
どう動くのか
スタンバイサーバーの中には働き手が2つあります。WALレシーバー(walreceiver)がプライマリサーバーから受け取ってディスクに書き(pg_last_wal_receive_lsn())、startupプロセスがそれを一列に適用します(pg_last_wal_replay_lsn())。この2つは別々に止まることがあります。
| 適用が止まる理由 | どう見えるか |
|---|---|
誰かがpg_wal_replay_pause()を呼んだ |
pg_get_wal_replay_pause_state()がpaused、ログにrecovery has paused |
リカバリーターゲット(recovery_target_*)に到達した |
同じくpaused。ログはrecovery stopping at restore point。ここでresumeするとリカバリーが終了してプロモートする |
recovery_min_apply_delay |
停止ではない。コミット記録をその時間だけ寝かせてから適用する(災害復旧用の「遅延スタンバイサーバー」の設定) |
| スタンバイサーバーのクエリとの競合 | 停止ではない。プライマリサーバーのVACUUMが消した行をスタンバイサーバーのクエリがまだ見ていると、適用はmax_standby_streaming_delayだけ待ってからクエリをキャンセルする。-1なら際限なく待つ |
バイトで測ってください。pg_wal_lsn_diff(flush_lsn, replay_lsn)は、受け取ったものの適用できていない量です。replay_lagのような時間の数字は、適用がある地点を通過するときに計算されるので、適用が完全に止まっていると、最後に計算された小さな値がそのまま残っていることもあります。
**競合を避ける2つの道には、それぞれ代償があります。**待つ時間を延ばせばスタンバイサーバーがその分遅れ、hot_standby_feedback = onで「自分のクエリがまだこの行を見ている」とプライマリサーバーに知らせれば、競合はなくなる代わりにプライマリサーバーのVACUUMがその行を消せなくなります(スロットのxminとして残ります)。タダの設定はありません。
**スロットは約束です。**スロットのrestart_lsn以降のWALは消されません。使っているスタンバイサーバーが消えても、約束は残ります。max_slot_wal_keep_sizeを設定すると、それ以上を引き留めたスロットはチェックポイントのときに切られ(wal_status = lost)、プライマリサーバーは助かります。切られたスタンバイサーバーは、ベースバックアップから作り直す必要があります。
**測る順序を決めておくと迷いが減ります。**第一に、プライマリサーバーでスタンバイサーバーごとにsent_lsn・flush_lsn・replay_lsnを並べて、どこで開いているかを見ます。送信と受信の間ならネットワーク、受信と確定の間ならスタンバイサーバーのディスク、確定と適用の間なら適用です。第二に、適用側であれば、スタンバイサーバーで停止状態を問い合わせ、停止でなければstartupプロセスが何を待っているか(pg_stat_activityのwait_event)と、適用を遅らせる設定を見ます。第三に、スタンバイサーバーのログを読みます。停止・リカバリーターゲット・競合待ちは、どれもログに1行ずつ痕跡を残し、その行には何をすべきか(あるいはすべきでないか)まで書かれています。
現場で出会う姿
レポート用のスタンバイサーバーに「絶対にクエリをキャンセルするな」とmax_standby_streaming_delay = -1を設定しておいたところ、夜通し動く分析クエリ1つのせいでスタンバイサーバーが6時間遅れた、ということはよくあります。モニタリングは接続状態だけを見ていたので緑のままでした。また、PITRの練習で使ったrecovery_target_nameが構成管理ツールを通じてスタンバイサーバーに広がり、ある日その名前の復元ポイントに出会った途端、スタンバイサーバーが一斉に止まったこともあります。誰かが「止まったからresume」を押し、そのスタンバイサーバーは静かにプロモートして書き込みを受け付け始めました。スロット側の事故はもっと静かです。読み取り負荷用のスタンバイサーバーを減らすときにサーバーだけを消してスロットを残したところ、プライマリサーバーのpg_walが数週間かけて数百GBに膨らみ、ある明け方にディスクがいっぱいになりました。その間、何のアラートも鳴りませんでした。pg_replication_slotsでactiveがfのスロットと、そのrestart_lsnがどれだけ遅れているかを測るアラートがなかったからです。
次のラボでやること
スタンバイサーバーを1台立てて適用だけを止め、受信と適用の差をバイトで測ります。スタンバイサーバーのクエリが適用と競合してキャンセルされること、フィードバックを有効にするとその代償がプライマリサーバーのクリーンアップに移ることを実際に起こします。誰も使っていないスロットにWALを100MB以上引き留めさせてから、上限で切ります。最後はインシデントです。接続は正常なのにデータが古い3台のスタンバイサーバーを、原因を見分けて、再起動なしで(どうしても必要なところを除いて)直します。