遅れたスタンバイを測り、握られた WAL を解放する
目標
スタンバイサーバーが「接続している」と「追従している」を区別して測ります。受信と適用の差をバイトで測り、スタンバイサーバーのクエリと適用の競合・フィードバックの代償・放置されたスロットが引き留めるWALを実際に起こしたあと、接続は正常なのにデータが古い3台のスタンバイサーバーを直します。
なぜ重要なのか
レプリケーションのモニタリングは、たいてい接続状態だけを見ます。そのため、スタンバイサーバーが何時間遅れても緑のままで、レポートを取り出す人が先に気づきます。逆に、削除されたスタンバイサーバーのスロット1つが何週間もWALを引き留め、プライマリサーバーのディスクを埋めることもあります。どちらも「どこで止まったか」を位置(LSN)で測ればすぐに見えることです。そして、遅延を減らすつまみはどれも代償を別の場所に押し付けるということ(クエリを切る、スタンバイサーバーが遅れる、プライマリサーバーのクリーンアップが遅れる)を自分で経験しておけば、設定を選ぶときに迷いが減ります。
環境
このPodはpostgresアカウントで動きます(su・sudoなしで、そのまま入力すれば動きます)。プライマリサーバーは5432で動いています。
psql 주 서버(5432, labdb)에 바로 붙습니다
psql -h 127.0.0.1 -p 5433 이 실습에서 세울 대기 서버
スタンバイサーバーには必ず-h 127.0.0.1を付けてください(Unixソケットのディレクトリを移してあります)。このラボのファイルは/root/db/lag/に置きます。
ステップ
- スタンバイサーバーlag1をスロットlag1で5433に
- 適用だけを止め、溜まった量をバイトで →
/root/db/lag/02-paused.txt - 再開して追いついたことを確認 →
/root/db/lag/03-caughtup.txt - スタンバイサーバーのクエリと適用の競合 →
/root/db/lag/04-conflict.txt - フィードバックを有効にすると、プライマリサーバーのクリーンアップが遅れる →
/root/db/lag/05-feedback.txt - 誰も使っていないスロットがWALを引き留める →
/root/db/lag/06-ghost.txt - 上限を設定して、放置されたスロットを切る →
/root/db/lag/07-slots.txt - インシデント: 接続は正常なのに、レポート用の3台のスタンバイサーバーのデータが古い
参考
- スタンバイサーバーのログは
/var/log/labhub/lag1.log、プライマリサーバーのログは/var/log/postgresql.logです。 - スタンバイサーバーでのみ使える関数(pg_wal_replay_pauseなど)と、プライマリサーバーでのみ見えるビュー(pg_stat_replication)を混同しやすいので注意してください。
- インシデントツール:
incident list、incident show <name>。同じインシデントをもう一度出すと、最初の故障状態に戻ります。レポートは/root/incident/<name>/report.txtに、対象ごとに<target>: <code>の1行とevidence-<target>: <observed line>を書きます。 - ラボのPodにはボリュームがないため、セッションが終わるとファイルもサーバーもすべて消えます。
スタンバイサーバーlag1をスロットで接続する
プライマリサーバー(5432)に物理スロットlag1を作り、ベースバックアップを/var/lib/postgresql/lag1に取って、スタンバイサーバーとして5433で起動してください(ログは/var/log/labhub/lag1.log)。スタンバイサーバーは、スロットlag1でプライマリサーバーに追従する必要があります。
方向: 最初のラボでやったとおりです。先にスロットを作り、そのスロットでコピーを取り、コピーをスタンバイサーバーとして起動します。 ツール: pg_create_physical_replication_slot、pg_basebackupの-X stream・-S・-R、postgresql.auto.confのport、pg_ctl start、スタンバイサーバーのpg_stat_wal_receiver。 具体: スロットを作るときに2番目の引数にtrueを渡すと、作った瞬間からWALを引き留めます(スタンバイサーバーが最初に接続する前に、必要なWALが消されないように)。アカウントはlabです(このラボのサーバーはループバックのレプリケーションを許可してあります)。スタンバイサーバーへの接続には-h 127.0.0.1 -p 5433を付けてください。
適用だけを止めて、溜まった量をバイトで測る
スタンバイサーバー(5433)の適用(replay)だけを止めてください。次に、プライマリサーバーにテーブルticksを作り、1000行以上挿入します。スタンバイサーバーでticksがまだ見えないことを確認し、スタンバイサーバーが受け取った位置と適用した位置、その差を、/root/db/lag/02-paused.txtにreceive_lsn: <lsn>・replay_lsn: <lsn>・gap_bytes: <bytes>の3行で書いてください。スタンバイサーバーは止めたままにしておきます(次のステップで再開します)。
方向: スタンバイサーバーの中では、受信と適用が別々に動いています。適用だけを止めると受信は続くので、2つの間が開きます。 ツール: pg_wal_replay_pause()、pg_get_wal_replay_pause_state()、pg_last_wal_receive_lsn()、pg_last_wal_replay_lsn()、pg_wal_lsn_diff()。 具体: 2つの位置は、同じselect 1回で一緒に取ってください。別々に測ると、その間に受信位置が動いて差が合いません。停止はスタンバイサーバーで呼ぶ関数で、プライマリサーバーで呼ぶとエラーになります。
適用を再開して、追いついたことを確認する
スタンバイサーバーの適用を再開し、受け取った位置と適用した位置が同じになるまで(追いつくまで)待ってください。次に、/root/db/lag/03-caughtup.txtにreplay_lsn: <lsn>(追いついた後の適用位置)・ticks_rows: <rows>(スタンバイサーバーで数えたticksの行数)の2行を書いてください。
方向: 止めている間に受け取ったWALはディスクにあるので、再開すると受け取り直さずに、そのまま適用だけを行います。 ツール: pg_wal_replay_resume()、pg_last_wal_receive_lsn()とpg_last_wal_replay_lsn()の比較、count(*)。 具体: 再開した直後は、まだ適用中のことがあります。2つの位置が同じになったのを見てから書いてください。採点ツールは両側のticksを数え直します。
スタンバイサーバーのクエリが適用とぶつかってキャンセルされる
スタンバイサーバーのmax_standby_streaming_delayを5sに変更してください(再起動なしで)。プライマリサーバーに10万行のテーブルbigを作り、スタンバイサーバーで反復読み取り(repeatable read)トランザクションでbigを数えたあと30秒ほど留まるクエリを起動したまま、プライマリサーバーでbigの半分を削除してVACUUMしてください。スタンバイサーバーのクエリが受け取った出力(エラーを含む)を/root/db/lag/04-conflict.txtに保存します。
方向: プライマリサーバーのVACUUMは「もう誰も見ていない」行を消しますが、スタンバイサーバーのクエリは、プライマリサーバーの知らないところでその行を見ています。そのクリーンアップの記録を適用するには、スタンバイサーバーはクエリが終わるのを待つか、クエリを切るしかありません。 ツール: スタンバイサーバーでのalter system set・pg_reload_conf()、psqlの-cを複数回(トランザクションを続ける)、バックグラウンド実行(&)、スタンバイサーバーのpg_stat_database_conflicts。 具体: スタンバイサーバーのクエリはバックグラウンドで起動し、出力を2>&1でファイルに受け取ってください。数秒後にプライマリサーバーでdeleteとvacuumを実行すると、5秒の上限が過ぎてクエリがキャンセルされ、競合の統計が1つ増えます。採点ツールはその統計を見ます。
フィードバックを有効にすると、代償はプライマリサーバーに回る
スタンバイサーバーのhot_standby_feedbackをonに変更してください(再起動なしで)。04と同じようにスタンバイサーバーでbigを読む長いクエリを起動したまま、プライマリサーバーでbigの行をさらに削除してVACUUM (VERBOSE) bigを実行し、その出力を/root/db/lag/05-feedback.txtに保存してください。
方向: フィードバックは、スタンバイサーバーが「自分のクエリがまだこの時点の行を見ている」とプライマリサーバーに知らせる仕組みです。クエリは生き残りますが、その行を消せなくなるのはプライマリサーバーのほうです。 ツール: スタンバイサーバーのalter system set・pg_reload_conf()、VACUUM (VERBOSE)のtuplesの行、プライマリサーバーのpg_replication_slotsのxmin。 具体: VERBOSEの出力はINFOとして標準エラー出力に出ます。2>&1で受け取ってください。「N are dead but not yet removable」のNが0より大きければ、フィードバックがクリーンアップを妨げたということです。
誰も使っていないスロットがWALを引き留める
プライマリサーバーに、誰も使わない物理スロットghostを作ってください(作った瞬間からWALを引き留めるように)。テーブルbulkに大きな書き込みをして、ghostが引き留めたWALが100MB(104857600バイト)を超えて溜まるようにし、CHECKPOINTしてください。/root/db/lag/06-ghost.txtにghost_restart_lsn: <lsn>・current_lsn: <lsn>(プライマリサーバーの現在の位置)・retained_bytes: <bytes>(2つの位置の差)の3行を書きます。
方向: スロットのrestart_lsnは「ここから先は消すな」という約束です。使うスタンバイサーバーがなければ、その位置は永遠に動きません。 ツール: pg_create_physical_replication_slot(name, true)、insert … select generate_series、CHECKPOINT、pg_replication_slotsのrestart_lsn・wal_status、pg_current_wal_lsn()、pg_wal_lsn_diff()、pg_ls_waldir()。 具体: 200バイトの行が数十万個あれば、WALは100MBを超えます。チェックポイントをしてもpg_walディレクトリのサイズが減らないことを、pg_ls_waldir()の合計で確認してみてください。2つの位置は一度に取ってください。
上限を設定して、放置されたスロットを切る
プライマリサーバーのmax_slot_wal_keep_sizeを64MBに設定してください(再起動なしで)。CHECKPOINTでghostが切られる(wal_statusがlostになる)ことを確認し、そのときのpg_replication_slotsの出力を/root/db/lag/07-slots.txtに保存したあと、ghostを削除してください。生きているスタンバイサーバーのスロットlag1は、そのまま追従し続ける必要があります。
方向: 上限は「スロット1つが引き留められるWALの最大量」です。超えたスロットを切ってプライマリサーバーのディスクを守り、切られたスタンバイサーバーは作り直す必要があります。 ツール: alter system set・pg_reload_conf()、CHECKPOINT、pg_replication_slotsのwal_status・safe_wal_size、プライマリサーバーのログ/var/log/postgresql.log、pg_drop_replication_slot。 具体: 上限のチェックは、設定を変えた瞬間ではなく、チェックポイントのときに動きます。切られるとき、プライマリサーバーのログにそのスロット名を含む行が残ります。lostを確認した出力は、削除する前に保存してください。
インシデント: 接続は正常なのに、レポート用の3台のスタンバイサーバーのデータが古い
incident rp-stale-standbyを実行するとインシデントが発生します(先にチケットを読んでください)。症状: プライマリサーバーrp-src(127.0.0.1:5450)に追従するレポート用の3台のスタンバイサーバーrpt1・rpt2・rpt3(5451・5452・5453)は、すべてstreamingで接続しているのに、クエリするとプライマリサーバーに今書いたものが見えません。3台ともプライマリサーバーにリアルタイムで追従するようにし、同じことが再び起きないよう、原因となった設定まで正してください。スタンバイサーバーはスタンバイサーバーのままでなければならず、再起動は、再起動しないと変わらない設定を直すときだけ行います(スタンバイサーバーごとにダッシュボードのセッションが接続しています)。/root/incident/rp-stale-standby/report.txtに、スタンバイサーバーごとにrpt1: <code>の1行とevidence-rpt1: <observed line>の1行を書きます(rpt2・rpt3も同じ形)。コードはスタンバイサーバーごとに次のいずれかです: apply-delay(適用をわざと遅らせる設定)・conflict-wait(スタンバイサーバーのクエリとの競合を際限なく待つ)・recovery-target(リカバリーターゲットの地点に到達して停止)・replay-paused(誰かが適用を手動で停止した)。
方向: 接続が正常なら、受信はできているということです。問題は適用です。まず「停止状態かどうか」で2つずつに分け、それぞれの組の中で何が違うかを見てください。 ツール: プライマリサーバーのpg_stat_replicationのflush_lsn・replay_lsnとpg_wal_lsn_diff、スタンバイサーバーのpg_get_wal_replay_pause_state()、show(またはpg_settings)で適用・リカバリー関連の設定、pg_stat_activity(startupプロセスのwait_event、長く留まっているクエリのbackend_xmin)、スタンバイサーバーのログ(/var/log/labhub/rp-stale/.log)。 具体: 停止状態の2台のうち1台は、resumeするとリカバリーが終わってプロモートしてしまいます。ログのHINTが何をしろと言っているかを最後まで読んでください。replay_lagのような時間の数字は、適用が止まっていると古い値を見せます。ほとんどの適用設定はreloadで変わりますが、recovery_target_*は再起動しないと変わりません。