PostgreSQL Replication and Promotion
Replication Is the Act of Streaming WAL
In one line
PostgreSQL replication is sending the WAL (write-ahead log) to a standby server and replaying it as is. That makes the standby a byte-for-byte copy of the primary, and in exchange you cannot mix different versions.
Why this was needed
If the database dies, the service dies. Even with a backup, recovery takes hours. Replication keeps a copy made in advance and cuts that time down to minutes.
What PostgreSQL does is simple. Every change is written to the WAL first (that is what makes crash recovery possible). Replication streams that WAL over the network, and the standby replays it onto its own data exactly as it arrives.
Two things follow from this.
- The standby is read-only. It cannot produce WAL on its own, so it cannot accept writes.
- The primary and the standby must run the same major version. The WAL format differs from version to version.
How it works
There are four steps to set it up.
1) 주 서버 설정 wal_level=replica, max_wal_senders, 복제 계정
2) 복제 슬롯 생성 pg_create_physical_replication_slot('s1')
3) 베이스백업 pg_basebackup -X stream -S s1 -R
4) 대기 서버 기동 standby.signal 이 있으면 자동으로 recovery 모드
The -R option of pg_basebackup automatically creates the standby.signal file and the primary_conninfo setting. That one file is the marker that says "you are a standby."
What a replication slot does
Without a slot, the primary does not know how far the standby has received. It recycles WAL by its own rules, so if the standby disconnects briefly and comes back, the WAL it needs is already gone. Replication then breaks and you have to start over from the base backup.
A slot is a marker that says "do not delete WAL until this standby has picked it up." The price is that if the standby never comes back, WAL piles up without limit and the disk fills. That is why you put a cap on it with max_slot_wal_keep_size — once the cap is exceeded, the primary gives up the slot to protect the disk. It is a setting that decides in advance which side you are willing to lose.
Synchronous or asynchronous
The default is asynchronous. The primary sends the WAL and returns the commit immediately. It is fast, but if the primary dies suddenly, transactions that have not been sent yet are lost.
Setting synchronous_commit = on and synchronous_standby_names makes it synchronous. The commit waits until the standby replies that it has received the WAL. You lose nothing, but commit latency grows by one network round trip, and if the standby dies, writes on the primary stop.
That is why synchronous replication is normally used with two or more standbys and a rule like ANY 1 (s1, s2), meaning "a reply from either one is enough." If you configure synchronous replication with only one standby, availability actually goes down.
How to measure lag, and what to watch out for
Replication lag is not a single number. You have to split "how far it has gone" into four points to know where it is stuck.
| Point | Meaning | If it is stuck here |
|---|---|---|
sent_lsn |
Up to where the primary has sent | Network bandwidth |
write_lsn |
Up to where the standby has received and written | The standby's disk |
flush_lsn |
Up to where it is confirmed on disk | fsync performance |
replay_lsn |
Up to where it has actually been applied and is visible to queries | Apply conflicts |
select application_name,
pg_wal_lsn_diff(sent_lsn, replay_lsn) as 적용_잔량,
write_lag, flush_lag, replay_lag
from pg_stat_replication;
Report it in time, not in bytes. "It is 8MB behind" is serious at a quiet hour of the night and means nothing while a batch job is running. If you measure now() - pg_last_xact_replay_timestamp() on the standby, you get how many seconds in the past it is looking, and that is closer to what users actually experience. But when the primary has no writes at all, this value keeps growing, so alert on it together with the remaining bytes.
Apply runs in a single line. What the primary wrote in parallel over many connections, the standby applies one at a time in WAL order. That is why a single bulk update or index build can widen the lag a lot. If the standby's CPU is idle but the lag is growing, this is usually the cause.
A query can block apply. If a long query is running on the standby and the primary has deleted rows that the query reads, the apply conflicts with that query. It waits for the time set by max_standby_streaming_delay and then cancels the query. This is where you meet ERROR: canceling statement due to conflict with recovery after sending an analytics query to the standby. Setting hot_standby_feedback = on lets the standby tell the primary about its own queries, which reduces conflicts, but this time vacuuming on the primary is delayed and the tables bloat. Whichever you choose, there is a cost, and no setting is free.
Common misconceptions
"Replication is a backup" — No. If you accidentally run DROP TABLE, that command is replicated, and the table disappears on the standby too. Replication protects against hardware failure, and backups protect against human error. You need both.
"Using the standby for read load balancing is free" — When a long query runs on the standby, WAL replay conflicts with it and is delayed, or the query is canceled (hot_standby_feedback eases this, but then vacuuming on the primary is delayed). Nothing is free.
What really matters in practice
You need to know how to read the lag.
-- 주 서버에서
select client_addr, state, sent_lsn, replay_lsn,
write_lag, flush_lag, replay_lag
from pg_stat_replication;
If state is not streaming, the standby is not connected. If replay_lag keeps growing, the standby is not keeping up — either its disk is slow or a long query is running on it.