PostgreSQL 고가용성: 서버 세 대로 하는 active-standby
서버가 둘이 되면 복제가 묻는 것
한 줄 요약
대기 서버가 다른 서버로 가는 순간 복제는 세 가지를 새로 묻습니다. 어느 주소로 듣는가, 누구에게 문을 여는가, 주 서버는 이 대기 서버를 무엇이라고 부르는가. 그리고 동기 복제는 그 이름에 커밋을 겁니다.
왜 이게 필요했나
한 서버 안에서 5432 와 5433 을 띄워 복제를 붙여 보면 모든 것이 쉽게 됩니다. 둘 다 localhost 에서 듣고, pg_hba 의 기본 줄이 로컬 연결을 이미 허락하고, 대기 서버가 하나뿐이라 이름을 부를 일도 없습니다. 그런데 운영의 대기 서버는 다른 랙, 다른 가용 영역에 있습니다. 같은 절차를 서버 두 대로 옮기면 처음 만나는 오류는 대개 복제가 아니라 접속입니다. could not connect to server, no pg_hba.conf entry for replication connection 같은 말입니다.
동기 복제도 마찬가지입니다. 이름 하나를 잘못 적으면 설정은 동기인데 실제로는 비동기로 돌고, 아무 경고도 나오지 않습니다. 반대로 제대로 걸면 대기 서버가 멈추는 순간 주 서버의 커밋이 멈춥니다. 이 실습은 그 두 장면을 진짜 서버 세 대에서 봅니다.
어떻게 동작하나
듣는 주소. listen_addresses 의 기본값은 localhost 입니다. 다른 서버가 닿으려면 그 서버 쪽 NIC 의 주소에서도 들어야 하고, 이 값은 재시작해야 바뀝니다. 이 실습의 VM 은 첫 NIC 의 주소가 셋 모두 10.0.2.2 이고 서로를 부르는 망은 두 번째 NIC(eth1)입니다. 관리망과 복제망을 나눈 실제 서버와 같은 모양입니다.
문을 여는 규칙. pg_hba.conf 는 위에서부터 읽어 처음 맞는 줄 하나로 판정합니다. 복제 연결은 database 칸에 replication 이라는 특별한 이름으로 적어야 하고, all 은 복제 연결에 맞지 않습니다. 대역은 좁게, 인증은 scram-sha-256 으로 둡니다. trust 를 쓰면 그 대역의 누구나 비밀번호 없이 데이터 전체를 받아 갈 수 있습니다. 복제 계정은 모든 테이블을 읽는 것과 같기 때문입니다.
같은 서버의 사본인가. pg_basebackup 으로 받은 데이터 디렉터리는 원본과 시스템 식별자가 같습니다(pg_controldata 의 Database system identifier). 패키지가 깔면서 만든 빈 클러스터는 식별자가 달라서 WAL 을 받을 수 없습니다. 대기 서버를 세우는 첫걸음은 그 빈 클러스터를 버리는 일입니다.
이름. 대기 서버가 붙을 때 보내는 application_name 이 주 서버의 pg_stat_replication 에 찍히고, synchronous_standby_names 는 이 이름으로 동기 대상을 고릅니다. 데비안·우분투 패키지는 cluster_name 을 16/main 으로 두기 때문에, 이름을 따로 주지 않으면 모든 대기 서버가 같은 이름으로 보입니다.
슬롯. 물리 복제 슬롯은 주 서버가 "이 대기 서버가 받아 갈 때까지 WAL 을 지우지 않는다" 고 약속하는 자리입니다. 슬롯 없이 붙은 대기 서버도 잘 돌지만, 잠깐 끊겼다 돌아왔을 때 필요한 WAL 이 이미 재활용됐으면 베이스백업부터 다시 해야 합니다.
동기 커밋이 사는 것과 파는 것
| synchronous_commit | 커밋이 기다리는 것 | 주 서버가 죽으면 |
|---|---|---|
| off | 아무것도(로컬 디스크도 나중에) | 최근 커밋이 사라질 수 있다 |
| local | 로컬 디스크 기록 | 대기 서버에 안 간 것이 사라질 수 있다 |
| on(동기 대상이 있을 때) | 대기 서버의 디스크 기록 | 사라지지 않는다 |
| remote_apply | 대기 서버의 적용(조회에 보임) | 사라지지 않고, 대기 서버에서 바로 읽힌다 |
동기로 걸면 유실이 없는 대신 커밋마다 왕복 시간이 붙고, 동기 대상이 응답하지 않으면 커밋이 멈춥니다. 멈춘 커밋을 Ctrl+C 로 취소하면 PostgreSQL 은 경고와 함께 이렇게 말합니다. 트랜잭션은 이미 로컬에 커밋됐지만 대기 서버로 갔는지는 모른다. 클라이언트는 실패로 봤는데 데이터는 남아 있는 상태입니다. 재시도하는 앱이라면 같은 주문이 두 번 들어갑니다.
synchronous_commit 은 계정이나 트랜잭션마다 바꿀 수 있습니다. 대량 적재는 local 로 빠르게, 쓰자마자 대기 서버에서 읽는 보고서 계정은 remote_apply 로 두는 식으로, 하나의 클러스터 안에서 내구성과 지연을 업무마다 다르게 고릅니다.
현장에서 만나는 모습
새 대기 서버를 붙였는데 pg_stat_replication 이 비어 있다면 순서대로 봅니다. 주 서버가 그 주소에서 듣는가(ss -lnt), pg_hba 의 처음 맞는 줄이 무엇인가(pg_hba_file_rules), 대기 서버 로그에 무엇이 찍혔는가. 동기 복제를 걸었는데 sync_state 가 async 라면 이름부터 봅니다. 그리고 대기 서버 하나에 동기를 건 클러스터는 그 대기 서버의 정비 시간이 곧 쓰기 장애 시간이라는 것을 운영 문서 첫 줄에 적어 둡니다.
다음 실습에서 할 것
db1 을 세션망에서 듣게 하고 복제 계정과 슬롯을 만든 뒤, db2 의 빈 클러스터를 db1 의 베이스백업으로 갈아 끼워 대기 서버로 띄웁니다. 동기 복제를 걸고 db2 를 멈춰 커밋이 멈추는 것을 보고, 취소한 커밋이 남는 것을 확인합니다. 마지막으로 보고서 계정 하나만 remote_apply 로 둡니다.