스트리밍 복제를 쓰는 구성에서 primary 와 standby 에 같은 postgresql.conf 를 내려도 되는가 하는 물음에서 출발한 정리다. ConfigMap 하나를 모든 파드에 마운트하는 Kubernetes 구성이나, 형상 관리로 설정 파일을 한 벌만 유지하고 싶은 온프레미스 구성에서 실제로 부딪히는 문제다. PostgreSQL 16 기준이다.
wal_level 이나 hot_standby 는 "복제가 가능한 상태로 열어 두는" 스위치일 뿐이고, 어느 인스턴스가 primary 인지 standby 인지는 정하지 않는다. 역할을 정하는 것은 데이터 디렉터리 안의 신호 파일이다.
$PGDATA/standby.signal |
기동 결과 |
|---|---|
| 없음 | primary. 읽기·쓰기 가능 |
| 있음 | standby. primary_conninfo 로 접속해 WAL 을 받아 재생 |
그래서 설정 파일 한 벌을 모든 인스턴스에 뿌려도 문제가 없다. 역할은 pg_basebackup -R 이 남긴 standby.signal 과 postgresql.auto.conf 의 primary_conninfo 가 결정한다. 재시작해도 신호 파일이 그대로 있으므로 역할이 유지되고, pg_promote() 로 승격하면 PostgreSQL 이 standby.signal 을 스스로 지우므로 그 뒤로는 primary 로 뜬다.
SELECT pg_is_in_recovery();
true 면 standby, false 면 primary 다. 스크립트에서 역할을 판정할 때는 이 함수를 쓴다.
주의할 점은 승격된 구 standby 와 원래 primary 가 동시에 primary 가 되는 상황이다. 설정 파일이 같다는 사실 자체가 이를 막아 주지는 않는다. 옛 primary 를 되살릴 때는 pg_rewind 로 새 primary 의 타임라인에 맞춘 뒤 standby 로 붙인다.
| 값 | 기록 내용 | 쓰이는 곳 |
|---|---|---|
minimal |
크래시 복구에 필요한 최소한 | 복제·아카이브 없음 |
replica |
물리적 페이지 변경. 스트리밍 복제와 PITR 에 충분 | HA standby, 백업 |
logical |
위에 더해 행 단위 변경 정보 | 논리 복제, CDC(Debezium 등) |
logical 은 replica 의 상위 집합이므로 논리 복제를 켜도 물리 복제는 그대로 된다. 다만 WAL 양이 늘고 그만큼 디스크와 네트워크, 아카이브 비용이 커진다. HA 만 필요하면 replica 로 둔다. 나중에 CDC 가 필요해지면 그때 올리면 되고, 이 변경은 재시작이 필요하다.
logical 로 올릴 때는 max_replication_slots 와 max_wal_senders 도 함께 늘린다. 논리 복제 슬롯이 소비되지 않으면 WAL 이 무한히 쌓여 디스크를 채운다 — 이것이 논리 복제를 켠 뒤 가장 흔하게 터지는 사고다.
listen_addresses = '*'
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
hot_standby = on
hot_standby_feedback = on
wal_keep_size = 1GB
hot_standby_feedback = on 은 standby 에서 도는 긴 조회가 primary 의 VACUUM 때문에 취소되는 것을 줄여 준다. 대신 primary 쪽 정리가 지연되어 bloat 이 늘 수 있으므로, 조회 취소가 실제로 문제가 될 때만 켠다.
"기본적으로 모든 접속은 읽기 전용, 쓰기는 수동으로" 라는 요구는 두 층에서 갈린다.
standby 는 물리적으로 쓰기가 불가능하다. 설정과 무관하게 쓰기 문이 거부된다.
ERROR: cannot execute INSERT in a read-only transaction
primary 에서 기본을 읽기 전용으로 만들려면 default_transaction_read_only 를 쓴다.
default_transaction_read_only = on
이 값은 기본값일 뿐 강제가 아니다. 세션이 스스로 해제할 수 있다.
SET default_transaction_read_only = off;
BEGIN;
UPDATE ...;
즉 실수로 인한 쓰기는 막아 주지만 권한 통제는 아니다. 진짜로 막으려면 계정 권한으로 처리한다.
ALTER ROLE app_ro SET default_transaction_read_only = on;
REVOKE INSERT, UPDATE, DELETE, TRUNCATE ON ALL TABLES IN SCHEMA public FROM app_ro;
Kubernetes 에서는 쓰기용 Service 와 읽기용 Service 를 나누고 애플리케이션이 읽기 Service 만 보게 하는 방식도 함께 쓴다. 그러나 이것도 경로를 나눌 뿐 권한이 아니므로, 위 권한 설정과 병행해야 한다.
복제 계정을 무인증(trust)으로 두는 구성이 간편해 보이지만 권하지 않는다. WAL 스트림은 데이터 그 자체이므로 복제 접속을 얻는 것은 사실상 덤프를 얻는 것과 같다. 같은 클러스터 안의 아무 파드나 접속할 수 있는 상태가 된다.
# 권장
host replication replicator 10.244.0.0/16 scram-sha-256
# 비권장 — 내부망이라도 무인증은 두지 않는다
host replication replicator 0.0.0.0/0 trust
pg_hba.conf 는 IP 와 CIDR 만 본다. "특정 네임스페이스만 허용" 같은 조건은 표현할 수 없다. Kubernetes 에서 파드 IP 는 노드마다 임의로 배정되므로 네임스페이스와 IP 대역이 대응하지도 않는다. 이 통제는 NetworkPolicy 로 한다.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-replication-from-app-ns
namespace: pg-ha
spec:
podSelector:
matchLabels:
app: postgres
policyTypes: ["Ingress"]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: app-ns
ports:
- protocol: TCP
port: 5432
kubernetes.io/metadata.name 은 Kubernetes 가 모든 네임스페이스에 자동으로 붙이는 레이블이라 별도 라벨링 없이 쓸 수 있다. 임의 레이블을 셀렉터로 쓰려면 kubectl label namespace <NS> <KEY>=<VALUE> 로 먼저 붙여야 한다.
두 층은 대체 관계가 아니다. pg_hba.conf 는 대역과 인증 방식을, NetworkPolicy 는 어느 워크로드가 5432 에 닿을 수 있는지를 각각 담당한다.
파드 CIDR 은 다음으로 확인한다.
kubectl cluster-info dump | grep -m1 cluster-cidr
kubectl get node -o jsonpath='{range .items[*]}{.spec.podCIDR}{"\n"}{end}'
수동 VACUUM 을 주기 배치로 도는 운영은 권하지 않는다. autovacuum 을 쓰고 임계값만 조정하는 것이 표준이다. 발동 조건은 다음과 같다.
dead_tuples > autovacuum_vacuum_threshold
+ autovacuum_vacuum_scale_factor * 테이블 행 수
기본값은 threshold = 50, scale_factor = 0.2 다. 즉 테이블 행의 20% 가 죽은 튜플이 되어야 발동하므로, 큰 테이블일수록 늦게 돈다. 갱신이 잦은 테이블은 이 비율을 낮춘다.
autovacuum_vacuum_scale_factor = 0.05
autovacuum_analyze_scale_factor = 0.02
autovacuum_naptime = 30s
전역으로 낮추면 조용한 테이블에도 부담이 가므로, 문제가 되는 테이블만 따로 조정하는 편이 낫다.
ALTER TABLE events SET (autovacuum_vacuum_scale_factor = 0.01);
수동 VACUUM FULL 은 테이블 전체에 ACCESS EXCLUSIVE 잠금을 건다. 운영 중에는 쓰지 않는다. 물리적 축소가 필요하면 pg_repack 같은 도구를 쓰거나 점검 시간에 수행한다.
bloat 과 마지막 vacuum 시각은 통계 뷰로 본다.
SELECT relname, n_live_tup, n_dead_tup, last_autovacuum, last_autoanalyze
FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 20;