レプリケーションとはWALを流す仕事だ
한국어 원문으로 표시합니다.
한 줄 요약
PostgreSQL 복제는 WAL(선행 기록 로그)을 대기 서버로 보내고 그대로 재생하는 것이다. 그래서 대기 서버는 주 서버의 바이트 단위 복사본이 되고, 그 대신 서로 다른 버전을 섞을 수 없다.
왜 이게 필요했나
데이터베이스가 죽으면 서비스가 죽는다. 백업이 있어도 복구에 몇 시간이 걸린다. 복제는 미리 만들어 둔 복사본을 두어 그 시간을 분 단위로 줄인다.
PostgreSQL 이 하는 일은 단순하다. 모든 변경은 먼저 WAL 에 적힌다(그래야 크래시에서 복구할 수 있다). 복제는 그 WAL 을 네트워크로 흘려보내고, 대기 서버가 자기 데이터에 그대로 재생한다.
여기서 두 가지가 따라 나온다.
- 대기 서버는 읽기 전용이다. 스스로 WAL 을 만들 수 없으므로 쓰기를 받을 수 없다.
- 주 서버와 대기 서버는 같은 메이저 버전이어야 한다. WAL 형식이 버전마다 다르다.
어떻게 동작하나
세우는 순서는 네 단계다.
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 모드
pg_basebackup 의 -R 이 standby.signal 파일과 primary_conninfo 설정을 자동으로 만들어 준다. 그 파일 하나가 "너는 대기 서버다" 라는 표시다.
복제 슬롯이 하는 일
슬롯이 없으면 주 서버는 대기 서버가 어디까지 받았는지 모른다. 그래서 WAL 을 자기 기준으로 재활용하고, 대기 서버가 잠깐 끊겼다 돌아오면 필요한 WAL 이 이미 사라져 있다. 그러면 복제가 깨지고 베이스백업부터 다시 해야 한다.
슬롯은 "이 대기 서버가 받아 갈 때까지 WAL 을 지우지 마라" 는 표식이다. 대신 대기 서버가 영영 안 돌아오면 WAL 이 무한히 쌓여 디스크가 찬다. 그래서 max_slot_wal_keep_size 로 상한을 둔다 — 상한을 넘으면 슬롯을 포기하고 디스크를 지킨다. 어느 쪽을 잃을지 미리 정하는 설정이다.
동기냐 비동기냐
기본은 비동기다. 주 서버는 WAL 을 보내 놓고 커밋을 즉시 반환한다. 빠르지만, 주 서버가 갑자기 죽으면 아직 안 간 트랜잭션이 사라진다.
synchronous_commit = on 과 synchronous_standby_names 를 설정하면 동기가 된다. 대기 서버가 WAL 을 받았다고 답할 때까지 커밋이 기다린다. 유실이 없는 대신 커밋 지연이 네트워크 왕복만큼 늘고, 대기 서버가 죽으면 주 서버의 쓰기가 멈춘다.
그래서 동기 복제는 대기 서버를 둘 이상 두고 ANY 1 (s1, s2) 처럼 "둘 중 하나만 답하면 된다" 로 쓰는 것이 정석이다. 하나만 두고 동기로 걸면 가용성이 오히려 내려간다.
지연을 어떻게 재고, 무엇을 보고 놀랄 것인가
복제 지연은 하나의 숫자가 아니다. 어디까지 갔는지를 네 지점으로 나눠 보아야 어디가 막혔는지 안다.
| 지점 | 뜻 | 여기서 막히면 |
|---|---|---|
sent_lsn |
주 서버가 보낸 데까지 | 네트워크 대역폭 |
write_lsn |
대기 서버가 받아서 쓴 데까지 | 대기 서버의 디스크 |
flush_lsn |
디스크에 확정한 데까지 | fsync 성능 |
replay_lsn |
실제로 적용해 조회에 보이는 데까지 | 적용 충돌 |
select application_name,
pg_wal_lsn_diff(sent_lsn, replay_lsn) as 적용_잔량,
write_lag, flush_lag, replay_lag
from pg_stat_replication;
바이트가 아니라 시간으로 보고해야 한다. "8MB 밀렸다" 는 말은 쓰기가 적은
새벽에는 심각하고 배치가 도는 시간에는 아무것도 아니다. 대기 서버에서
now() - pg_last_xact_replay_timestamp() 를 재면 몇 초 전 세상을 보고
있는지가 나오고, 이것이 사용자가 겪는 값에 가깝다. 다만 주 서버에 쓰기가
전혀 없으면 이 값은 계속 커지므로, 경보에는 잔량 바이트와 함께 건다.
적용은 한 줄로 돈다. 주 서버가 여러 연결로 병렬로 쓴 것을 대기 서버는 WAL 순서대로 하나씩 적용한다. 그래서 대량 갱신이나 인덱스 생성 한 번이 지연을 크게 벌린다. 대기 서버의 CPU 는 한가한데 지연이 늘어난다면 대개 이것이다.
조회가 적용을 막을 수 있다. 대기 서버에서 긴 조회가 돌고 있고 그 조회가
읽는 행을 주 서버가 지웠다면, 적용이 그 조회와 충돌한다. max_standby_streaming_delay
가 정한 시간까지 기다렸다가 조회를 취소한다. 분석 조회를 대기 서버로 보냈다가
ERROR: canceling statement due to conflict with recovery 를 만나는 자리다.
hot_standby_feedback = on 으로 대기 서버가 자기 조회를 주 서버에 알리게 하면
충돌은 줄지만, 이번에는 주 서버의 청소가 밀려 부풀어 오른다. 어느 쪽을
택하든 대가가 있고, 공짜인 설정은 없다.
흔한 착각
"복제가 백업이다" — 아니다. DROP TABLE 을 실수로 치면 그 명령이 복제되어 대기 서버에서도 테이블이 사라진다. 복제는 하드웨어 장애를 막고, 백업은 사람의 실수를 막는다. 둘 다 필요하다.
"대기 서버를 읽기 부하 분산에 쓰면 공짜" — 대기 서버에서 긴 쿼리가 돌면 WAL 재생이 그 쿼리와 충돌해 지연되거나 쿼리가 취소된다(hot_standby_feedback 로 완화하지만 이번엔 주 서버의 청소가 밀린다). 공짜는 없다.
실무에서 진짜 중요한 것
지연을 보는 법을 알아야 한다.
-- 주 서버에서
select client_addr, state, sent_lsn, replay_lsn,
write_lag, flush_lag, replay_lag
from pg_stat_replication;
state 가 streaming 이 아니면 붙지 않은 것이다. replay_lag 이 계속 커지면 대기 서버가 따라잡지 못하고 있다 — 디스크가 느리거나, 대기 서버에서 긴 쿼리가 돌고 있다.