Streaming Replication

동기식 스트리밍 리플리케이션(Synchronous streaming replication)은 PostgreSQL 9.1 버전부터 구현되었습니다. 단일 마스터 다중 슬레이브(single-master-multi-slaves) 타입의 리플리케이션이라고도 불리며 여기서 마스터(Master)는 프라이머리(Primary)를 뜻하며, 슬레이브(Slave)는 스탠바이(Standby)를 뜻합니다.
이 리플리케이션 기능은 로그 전달(Log shipping)을 기반으로 합니다. 프라이머리는 WAL 데이터(WAL data)를 지속적으로 스탠바이로 전송하고, 각 스탠바이들은 받은 즉시 WAL 데이터를 리플레이합니다.
이 장에서는 스트리밍 리플리케이션의 동작에 대해 다음과 같은 주제를 다룹니다.
- 스트리밍 리플리케이션을 시작하는 방법
- 프라이머리와 스탠바이 사이에서 데이터가 전송되는 방법
- 프라이머리 서버가 다수의 스탠바이 서버를 관리하는 방법
- 프라이머리 서버가 스탠바이 서버의 실패를 감지하는 방법
PostgreSQL 9.0 버전에서 비동기식 리플리케이션(Asynchronous replication)이 구현되었지만 PostgreSQL 9.1 버전의 동기식 리플리케이션으로 대체되었습니다.
Init Streaming Replication
스트리밍 리플리케이션을 위해서 세 개의 프로세스가 동작합니다. walsender는 프라이머리 서버에서 WAL 데이터를 스탠바이 서버로 보냅니다. 스탠바이 서버의 walreceiver와 startup 프로세스는 WAL 데이터를 전송 받아서 리플레이합니다. walsender와 walreceiver는 단일 TCP 연결로 통신합니다.
이 섹션에서는 위에서 설명한 세 프로세스가 어떻게 시작하고 연결을 맺는지를 확인하기 위해 스트리밍 리플리케이션의 시작 절차를 살펴보겠습니다.
- 프라이머리와 스탠바이를 기동합니다.
- 스탠바이 서버가 startup 프로세스를 기동합니다.
- 스탠바이 서버가 walreceiver 프로세스를 기동합니다.
- walreceiver가 프라이머리 서버로 연결 요청을 전송합니다. 만약 프라이머리 서버가 기동중이지 않다면 walreceiver는 주기적으로 이 요청을 전송합니다.
- 프라이머리 서버가 이 요청을 받으면 walsender 프로세스를 기동하고 walsender와 walreceiver의 TCP 연결이 설정됩니다.
- walreceiver는 스탠바이 데이터베이스 클러스터의 가장 최신 LSN을 walsender로 전송. 일반적으로 이 단계를 Handshaking이라고 합니다.
- 스탠바이의 가장 최신 LSN이 프라이머리의 최신 LSN보다 작다면(스탠바이 LSN < 프라이머리 LSN), walsender는 스탠바이 LSN부터 프라이머리 LSN까지의 WAL 데이터를 전송. 이 WAL 데이터는 프라이머리의 pg_xlog(또는 pg_wal) 서브 디렉토리의 WAL 세그먼트(WAL segment) 파일에서 얻어옴. 스탠바이 서버는 전송 받은 WAL 데이터를 리플레이. 스탠바이가 프라이머리를 따라잡는 이 단계를 Catch-up이라고 합니다.
- Streaming이 시작됩니다.
다음 그림은 위의 과정을 도식화 한 것입니다.

각각의 walsender 프로세스는 연결된 walreceiver 또는 모든 어플리케이션과의 적절한 상태를 유지합니다. 여기서 상태란 walreceiver의 상태나 walsender에 연결된 어플리케이션의 상태가 아닙니다. 가능한 상태는 다음과 같습니다.
- start-up : walsender를 시작한 시점부터 Handshaking이 완료되는 시점까지의 상태 (5), (6)
- catch-up : Standby가 Primary를 따라잡는 단계 (7)
- streaming : Streaming Replication이 수행중인 상태 (8)
- backup : pg_basebackup과 같은 백업 툴들을 위해 데이터베이스 클러스터 전체의 파일을 전송중인 상태
위의 상태는 다음 쿼리로 확인할 수 있습니다.
testdb=# SELECT application_name,state FROM pg_stat_replication;
application_name | state
——————+———–
standby1 | streaming
standby2 | streaming
pg_basebackup | backup
(3 rows)
위에서 언급한대로 두 개의 walsender가 WAL 데이터를 전송하기 위해 실행중에 있고, 다른 하나는 pb_basebackup 유틸리티를 위해 데이터베이스 클러스터의 모든 파일을 전송하기 위해 실행중에 있습니다.
☝ 만약 스탠바이 서버가 오랜 시간동안 정지한 후에 재기동한다면 어떤 일이 벌어질까?
PostgreSQL 9.3 버전까지는 스탠바이에서 요청한 WAL 세그먼트 파일이 이미 프라이머리에서 재사용된 경우는 스탠바이가 프라이머리를 따라잡을 방법이 없었습니다. 그래서 wal_keep_segments 파라미터의 값을 크게 잡는 것 말고는 방법이 없었습니다.
PostgreSQL 9.4 버전부터는 리플리케이션 슬롯(Replication slot)을 사용할 수 이러한 문제를 방지할 수 있습니다. 리플리케이션 슬롯은 WAL 데이터 전송에 대한 유연성을 확장하기 위한 기능입니다. 주로 논리적 리플리케이션(Logical replication)을 위한 기능이지만, 위에서 언급한 문제에 대한 해결책이 되기도 합니다. pg_xlog(또는 pg_wal) 서브 디렉토리에 아직 전송되지 않은 WAL 세그먼트 파일이 있는 경우 재사용 처리를 잠시 멈춤으로 리플리케이션 슬롯에 보관할 수 있습니다. 자세한 설명은 공식 문서를 참조하시기 바랍니다.
https://www.postgresql.org/docs/current/warm-standby.html#STREAMING-REPLICATION-SLOTS
Primary – Standby Communication
스트리밍 리플리케이션에는 로그 전달(Log shipping)과 데이터베이스 동기화(Database synchronization) 두 가지 측면이 있습니다. 프라이머리 서버는 데이터베이스에 WAL 데이터가 써질 때마다 WAL 데이터를 연결된 스탠바이 서버로 전송하기 때문에 로그 전달은 스트리밍 리플리케이션의 기반이 됩니다. 데이터베이스 동기화는 동기식 복제(Synchronous replication)에 필요합니다. 프라이머리 서버는 각각의 스탠바이 서버와 동기화를 이루기 위해 통신을 합니다.
스탠바이 서버가 동기식 복제 모드이며 hot-standby 파라미터는 비활성(Disabled) 되었고, wal_level 파라미터는 archive인 상태를 가정하겠습니다. 프라이머리 서버의 주요 파라미터는 다음과 같습니다.
synchronous_standby_names = ‘standby1’
hot_standby = off
wal_level = archive
백엔드 프로세스(Backend process) 중 하나가 오토커밋(Autocommit) 모드에서 간단한 INSERT를 수행했다고 가정하겠습니다. 백엔드 프로세스는 트랜잭션을 시작하고, INSERT를 수행한 후 트랜잭션을 즉시 커밋합니다. 해당 커밋 작업은 다음 과정을 통해 완료됩니다.
- 백엔드 프로세스가 WAL 데이터를 WAL 세그먼트 파일에 쓰고(Write), 플러시(Flush)합니다.
- walsender 프로세스가 WAL 세그먼트 파일에 쓰여진 WAL 데이터를 walreceiver 프로세스에 전송합니다.
- WAL 데이터를 전송한 후, 백엔드 프로세스는 스탠바이 서버로부터 ACK가 올 때까지 기다립니다.
- walreceiver는 전송받은 WAL 데이터를 스탠바이의 WAL 세그먼트 파일에 쓰고(Write), walsender에 ACK을 응답합니다.
- walreceiver는 WAL 데이터를 WAL 세그먼트 파일에 플러시하고, 이에 대한 ACK을 walsender에 전송함. 그리고 startup 프로세스에게 WAL 데이터가 갱신되었음을 알립니다.
- startup 프로세스는 WAL 세그먼트에 쓰여진 WAL 데이터를 리플레이합니다.
- walsender는 walreceiver로부터 ACK를 받은 후, 백엔드 프로세스의 커밋 또는 취소(Abort)는 완료됩니다.
하단의 그림은 이를 모식도로 나타낸 것입니다.

위에서는 코드 레벨의 설명을 배제하였지만… 필자는 개인적으로 위 과정이 실제 이루어지는 원리를 이해하기 위해서 코드 레벨의 설명이 매우 유용하다고 생각합니다. 따라서, 해당 내용은 하단에 아카이빙하였으니 참고바랍니다.
Code Level 설명
- 백엔드 프로세스는 XLogInsert()와 XLogFlush() 함수를 수행해 WAL 데이터를 WAL 세그먼트 파일에 쓰고(Write), 플러시(Flush)
- walsender 프로세스는 WAL 세그먼트 파일에 쓰여진 WAL 데이터를 walreceiver 프로세스에 전송
- WAL 데이터를 전송한 후, 백엔드 프로세스는 스탠바이 서버로부터 ACK가 올 때까지 기다림. 더 정확히는 SyncRepWaitForLSN() 함수를 수행해서 래치(Latch)를 얻고, 해당 래치가 해제될 때까지 기다림
- walreceiver는 전송받은 WAL 데이터를 스탠바이의 WAL 세그먼트 파일에 write() 시스템 콜(System call)을 이용해 쓰고(Write), walsender에 ACK을 응답
- walreceiver는 fsync() 시스템 콜을 이용해 WAL 데이터를 WAL 세그먼트 파일에 플러시하고, 이에 대한 ACK을 walsender에 전송함. 그리고 startup 프로세스에게 WAL 데이터가 갱신(Update)됐음을 알림
- startup 프로세스는 WAL 세그먼트에 쓰여진 WAL 데이터를 리플레이
- walsender는 walreceiver로부터 ACK를 받은 후 백엔드 프로세스가 잡고 있는 래치를 풀고, 백엔드 프로세스의 커밋 또는 취소(Abort)는 완료됨. 래치가 해지되는 시점은 synchronous_commit 파라미터 값에 따라 다름. 만약 기본값인 ‘on’이라면 5단계인 플러시에 대한 ACK을 받았을 때 해지되고, ‘remote_write’인 경우는 4단계인 쓰기에 대한 ACK을 받았을 때 해지
wal_level 파라미터의 설정값이 ‘hot_standby’ 또는 ‘logical’인 경우, PostgreSQL은 hot standby 기능을 위한 WAL 레코드를 커밋이나 취소에 대한 로그 뒤에 써줍니다. 이 섹션의 예시에서는 wal_level이 ‘archive’이기 때문에 쓰지 않습니다.
각 ACK는 스탠바이 서버 내부의 정보를 프라이머리 서버에게 알려줍니다. ACK가 포함한 네 가지 사항은 다음과 같습니다.
- 가장 최신에 쓰여진(Write) WAL 데이터의 LSN
- 가장 최신에 플러시된 WAL 데이터의 LSN
- 가장 최신에 startup 프로세스에 의해 리플레이된 WAL 데이터의 LSN
- 응답이 전송된 타임스탬프(Timestamp)
walreceiver는 WAL 데이터의 쓰기나 플러시가 발생하지 않더라도 스탠바이 서버의 하트비트(Heartbeat)를 주기적으로 위해 ACK를 보냅니다. 따라서 프라이머리 서버는 연결된 모든 스탠바이 서버의 상태를 항상 파악합니다.
아래의 쿼리(Query)를 수행하면, 연결된 스탠바이 서버의 LSN 관련 정보를 조회할 수 있습니다.
testdb=# SELECT application_name AS host,
write_location AS write_LSN, flush_location AS flush_LSN,
replay_location AS replay_LSN FROM pg_stat_replication;host | write_lsn | flush_lsn | replay_lsn
———-+———–+———–+————
standby1 | 0/5000280 | 0/5000280 | 0/5000280
standby2 | 0/5000280 | 0/5000280 | 0/5000280
(2 rows)
Standby Server Management
프라이머리 서버는 관리하는 모든 스탠바이 서버에 대해 sync_priority와 sync_state 값을 유지하며 해당 값에 따라 스탠바이 서버를 처리합니다.
sync_priority
sync_priority는 동기식 모드의 스탠바이 서버들에 대한 우선순위를 매긴 값으로 고정값입니다. 값이 작을수록 우선순위가 높으며 0은 특별한 값으로 비동기식 모드를 뜻합니다. 스탠바이 서버의 우선순위는 synchronous_standby_names 파라미터에 나열된 순선대로 지정됩니다. 예를 들어 다음 설정에 따르면 standby1과 standby2의 우선순위는 각각 1, 2입니다. synchronous_standby_names에 입력되지 않은 스탠바이 서버는 비동기식 모드로 간주되며 우선순위 값은 0이 됩니다.
sync_state
sync_state는 동기식 스탠바이 서버의 상태를 나타냅니다. 이 값은 모든 스탠바이 서버의 실행 상태와 우선순위에 따라 가변적으로 변합니다. 상태에 대한 값은 다음과 같습니다.
하단의 상태는 Streaming Replication의 중요한 목표 중 하나인 고가용성을 위한 Fail Over를 이해하기 위하여 필수적으로 숙지해야합니다.
- sync : 동작하는 동기식 스탠바이 서버 중 우선순위가 가장 높은 스탠바이 서버의 상태
- potential : sync 상태가 아닌 나머지 동기식 스탠바이 서버인 상태. 만약 sync 상태인 스탠바이 서버에 장애가 발생할 경우 potential 상태 중 가장 우선순위가 높은 스탠바이 서버가 sync 상태로 교체됨
- async : 비동기식 스탠바이의 상태로 동적으로 다른 상태로 변경될 수 없음. 프라이머리 서버는 async 상태의 스탠바이를 potential 상태의 스탠바이와 동일하게 처리하지만 async 상태의 스탠바이는 sync나 potential 상태로 전환될 수는 없음
스탠바이 서버들의 우선순위와 상태는 아래 쿼리를 통해 조회할 수 있습니다
testdb=# SELECT application_name AS host,
sync_priority, sync_state FROM pg_stat_replication;
host | sync_priority | sync_state
————-+——————–+————
standby1 | 1 | sync
standby2 | 2 | potential
(2 rows)
몇몇 개발자들이 다수의 sync 상태의 동기식 스탠바이에 대한 개발을 시도하고 있습니다. 자세한 사항은 아래 링크를 참조하시면 됩니다.
프라이머리 서버는 sync 상태인 스탠바이 서버의 ACK만을 기다립니다. 즉, 프라이머리 서버는 sync 상태인 스탠바이 서버의 쓰기 및 플러시만을 확인합니다. 따라서 스트리밍 레플리케이션은 sync 상태의 스탠바이 서버만 프라이머리 서버와의 동기화와 정합성을 보장합니다.
그림 11.3은 potential 상태인 스탠바이의 ACK가 sync 상태인 스탠바이의 ACK보다 먼저 프라이머리에 도착한 상황을 나타냅니다. 프라이머리 서버는 potential 상태인 스탠바이의 ACK가 도착하더라도 현재 진행중인 트랜잭션의 커밋을 완료하지 않고 여전히 ACK를 기다립니다. 그리고 sync 상태인 스탠바이의 ACK가 도착하면 프라이머리의 백엔드 프로세스는 래치를 해지하고 현재 진행중인 트랜잭션을 완료합니다.
(그림)
반대로 sync 상태인 스탠바이의 ACK가 먼저 도착하면 potential 상태인 스탠바이의 ACK가 도착하지 않았더라도 바로 래치를 해지하고 현재 진행중인 트랜잭션의 커밋을 완료합니다.
Failure Handling
스트리밍 리플리케이션을 수행하는 동안에 장애가 발생할 수 있습니다. 이번 섹션에서는 스트리밍 리플리케이션이 어떻게 장애를 탐지하고 대처하는지에 대하여 설명하겠습니다.
Failure Detection
프라이머리 서버는 스트리밍 리플리케이션을 수행하는 과정에서 스탠바이 서버들의 장애를 탐지하여 적절한 대응을 수행해야합니다. 이번 문단에서는 프라이머리 서버가 스트리밍 리플리케이션 과정에서 스탠바이 서버들의 장애를 탐지하는 방법에 대하여 설명하겠습니다.
프라이머리 서버는 다음 두 가지 방식의 장애 감지 절차를 사용합니다. 해당 과정은 별도의 하드웨어를 필요로 하지 않습니다.
- 스탠바이 서버 프로세스의 장애 감지
- walsender와 walreceiver 사이의 연결이 끊길 경우, 프라이머리 서버는 즉시 스탠바이 서버나 walreceiver에 장애가 있다고 판단합니다. walreceiver의 소켓 인터페이스에 대한 읽기나 쓰기를 수행하는 저수준(Low level) 네트워크 함수가 에러를 리턴하는 경우에도 프라이머리 서버는 즉시 장애로 판단합니다.
- 하드웨어나 네트워크의 장애 감지
- wal_sender_timeout(기본값은 60초) 파라미터에 설정된 시간동안 walreceiver가 응답을 하지 않는다면, 프라이머리 서버는 스탠바이 서버에 장애가 발생했다고 판단합니다. 위에서 설명한 장애 감지와는 다르게 스탠바이 서버가 더 이상 응답을 보낼 수 없는지를 확인하기 위해 최대 wal_sender_timeout 값만큼의 시간이 소요됩니다.
장애의 유형에 따라 장애가 발생한 즉시 감지를 할 수도 있고, 장애가 발생한 시점부터 일정 시간이 지난 후에야 감지를 할 수 있는 경우도 있습니다. 특히 위에서 설명한 후자의 경우, sync 상태의 스탠바이 서버에 장애가 발생했다면 다른 potential 상태의 스탠바이 서버들이 동작을 하고 있더라도 트랜잭션은 일정 시간동안 중지될 수밖에 없습니다.
Fail Over
Crash in non-sync standby
potential 또는 async 상태의 스탠바이 서버에 장애가 발생할 경우, 프라이머리 서버는 장애가 발생한 스탠바이 서버와 연결된 walsender를 종료하고 모든 작업을 계속 수행합니다. 즉, 프라이머리 서버의 트랜잭션 처리는 이 두 가지 유형의 스탠바이 서버의 장애로부터 영향을 받지 않습니다.

Crash in sync standby
동기식 스탠바이 서버에 장애가 발생하여 더 이상 ACK를 받을 수 없는 경우 프라이머리 서버는 계속 응답을 기다립니다. 그렇기 때문에 실행중이던 트랜잭션은 커밋을 할 수 없고, 이후의 쿼리도 시작될 수 없습니다. 즉, 프라이머리 서버의 모든 작업이 사실상 중단됩니다. (Streaming Replication은 자동으로 시간 초과에 의해 동기식 모드에서 비동기식 모드로의 전환하는 기능을 지원하지 않습니다.)
이러한 상황을 해결하기 위한 두 가지 방법이 있습니다. 하나는 다수의 스탠바이 서버를 둬서 시스템 가용성을 높이는 것이고, 다른 하나는 수동으로 동기식(Synchronous)에서 비동기식(Asynchronous)으로 전환하는 것입니다.
Multi-standby server
sync 상태의 스탠바이 서버에 장애가 발생한 경우, 프라이머리 서버는 동일하게 walsender를 종료하고, potential 상태 중 가장 우선순위가 높은 스탠바이 서버를 sync 상태로 교체합니다.

pontential이나 async 상태의 스탠바이 서버의 장애일 때와는 달리 프라이머리 서버에서 진행 중이던 쿼리는 sync 상태의 스탠바이 서버에 장애가 생긴 시점부터 새로운 sync 상태의 스탠바이 서버로 교체될 때까지 중단됩니다.
어떠한 상황에도 하나 이상의 스탠바이 서버가 동기식 모드에서 실행되어야 하는 경우, 프라이머리 서버는 항상 하나의 sync 상태의 스탠바이 서버만 유지합니다. 그리고 sync 상태의 스탠바이 서버는 항상 프라이머리 서버와 동기화 되어 정합성을 유지합니다.
Convert sync to async manually
수동으로 동기식에서 비동기식으로 전환하는 방법은 다음과 같습니다. 아래의 절차를 수행해도 연결된 클라이언트(Client)에 영향을 주지는 않습니다. 프라이머리 서버는 트랜잭션 처리를 계속 하며, 각 백엔드 프로세스와 클라이언트 간의 모든 세션도 유지됩니다.
- postgresql.conf의 synchronous_standby_names 파라미터를 빈 문자열(Empty string)로 수정
synchronous_standby_names = ”
2. reload 옵션을 줘서 pg_ctl을 수행
postgres> pg_ctl -D $PGDATA reload
지금까지 PostgreSQL의 WAL 3 (Streaming Replication)에 관해 알아보았습니다
‘PostgreSQL의 Extension Framework’를 바로 이어서 확인해보세요!