본문으로 건너뛰기
6.6 pg_rewind와 timeline

6.6 pg_rewind와 timeline

failover가 끝나도 클러스터의 일이 끝난 것은 아니다. 죽었던 구 primary는 언젠가 복구되어 돌아오는데, 그 노드를 새 primary의 replica로 되돌리는 과정에는 timeline 분기라는 걸림돌이 있다. Patroni는 이 재합류를 pg_rewind로 자동화하는 옵션을 제공하며, rewind가 불가능한 상황을 위한 처리 옵션도 별도로 둔다.

timeline은 왜 갈라지는가

PostgreSQL은 recovery를 마치고 promote되는 순간 새 timeline을 연다. failover에서 replica가 승격되면 클러스터의 WAL 이력은 그 시점에서 새 timeline으로 갈라진다. 문제는 구 primary 쪽이다. asynchronous replication에서는 primary가 커밋을 반환하고도 아직 replica로 전송하지 못한 WAL이 있을 수 있다. 이 구간은 승격된 새 primary에는 없고 구 primary의 옛 timeline에만 존재한다.

    flowchart TD
    A["timeline 1<br/>primary A 운영"] --> B["A 장애<br/>분기점 LSN X"]
    B --> C["replica B promote<br/>timeline 2 시작"]
    B --> D["A에만 있는 WAL<br/>LSN X 이후 구간"]
    C --> E["클러스터는<br/>timeline 2로 진행"]
    D --> F["forked timeline<br/>미복제 트랜잭션"]
    E --> G["A 재합류하려면<br/>LSN X 이전으로 복귀"]
    F --> G
  

공식 문서는 이 forked timeline에 남은 미복제 트랜잭션을 “effectively unrecoverable"이라고 표현한다. 승격 이후 클러스터는 이미 timeline 2에서 새 WAL을 쌓고 있으므로, 갈라진 가지의 트랜잭션을 본류로 합칠 방법이 없다. 유실 자체를 막으려면 사후 처리가 아니라 6.4 synchronous mode처럼 커밋 시점의 복제 보장이 필요하다.

구 primary 입장에서는 자신의 WAL이 분기점 LSN X를 지나 있으므로, 그대로는 새 primary의 streaming replica가 되지 못한다. 재합류하려면 데이터 디렉토리를 분기점 이전 상태로 되돌리거나, 처음부터 다시 복제해야 한다.

use_pg_rewind가 하는 일

postgresql.use_pg_rewind(기본 false)를 켜면, 구 leader가 replica로 클러스터에 재합류할 때 Patroni가 pg_rewind 실행을 시도한다. pg_rewind는 데이터 디렉토리 전체를 다시 복사하는 대신 새 primary와 달라진 블록만 되감으므로, 전체 재복제 대비 재합류 비용이 크게 줄어든다.

전제 조건이 있다. 공식 문서 원문 그대로, “Either the cluster must be initialized with data page checksums (--data-checksums option for initdb) and/or wal_log_hints must be set to on, or pg_rewind will not work.” 즉 클러스터를 data checksums로 초기화했거나 wal_log_hintson이어야 한다. 이미 운영 중인 클러스터는 checksums를 소급 적용하기 어려우므로, 부트스트랩 시점에 결정해 두는 편이 좋다.

# patroni.yml
bootstrap:
  initdb:
    - data-checksums        # pg_rewind 전제 조건 확보

postgresql:
  use_pg_rewind: true
  authentication:
    rewind:
      username: rewind_user
      password: rewind_password

postgresql.authentication.rewind는 rewind 전용 계정이다. PostgreSQL 11 이상에서 postgres 초기화 시 이 사용자가 생성되고 pg_rewind에 필요한 권한이 자동으로 부여되므로, superuser 계정 없이 rewind를 실행한다. 지정하지 않으면 superuser 자격이 사용된다.

rewind가 실패하거나 불가능할 때

rewind가 항상 성공하는 것은 아니다. Patroni는 실패 상황을 위한 처리 옵션 두 가지를 둔다. 둘 다 기본값은 false다.

옵션발동 조건동작
remove_data_directory_on_rewind_failurepg_rewind 실행이 실패데이터 디렉토리를 제거하고 replica를 재생성
remove_data_directory_on_diverged_timelinestimeline이 분기했는데 구 primary가 새 primary에서 streaming을 시작하지 못함데이터 디렉토리를 제거하고 replica를 재생성

remove_data_directory_on_rewind_failure가 꺼져 있으면 rewind 실패 후에도 새 leader를 follow하려고 시도한다. remove_data_directory_on_diverged_timelines는 rewind를 쓰지 않거나 쓸 수 없는 클러스터에서 분기를 감지했을 때의 마지막 수단이다. 재생성은 Part V의 replica 생성 경로를 그대로 따른다.

두 옵션 모두 데이터 디렉토리 삭제와 전체 재복제를 수반한다. 수백 GB 이상의 데이터베이스라면 재생성에 걸리는 시간 동안 replica 수가 하나 줄어든 채 운영된다는 뜻이므로, 자동 삭제를 켤지는 데이터 크기와 replica 생성 소요 시간을 재 보고 판단할 문제다. 켜지 않은 경우 rewind 실패 노드는 DBA가 직접 조치할 때까지 정상 합류하지 못한 상태로 남는다.

timeline history 조회

failover와 switchover의 이력은 DCS의 history key에 누적되며, 두 가지 방법으로 조회한다. REST API의 GET /historypg_wal 디렉토리의 history 파일과 비슷한 형식으로 반환한다. 각 항목은 timeline 번호, 전환 지점 LSN, 사유, 시각의 4원소 배열이다.

curl -s http://localhost:8008/history | jq .
[
  [1, 25623960, "no recovery target specified", "2026-07-23T16:57:57+09:00"],
  [2, 85662632, "no recovery target specified", "2026-07-24T09:12:04+09:00"]
]

같은 정보를 CLI에서는 patronictl history로 본다. -f json처럼 출력 형식 지정도 지원한다.

patronictl history my-cluster

DCS에 유지할 이력 개수는 max_timelines_history(기본 0)로 정하며, 0이면 전체 이력을 DCS에 보관한다.

timeline이 예상 밖에 올라가 있다면 그 사이 failover가 있었다는 뜻이다. patronictl history의 사유와 시각을 보면 언제 무엇 때문에 전환됐는지 역추적할 수 있다. 이전 primary와 timeline이 다른 노드의 승격을 막는 check_timeline 옵션은 6.2 후보 선정에서 다뤘다.

정리

failover가 남기는 숙제는 구 primary의 재합류다. forked timeline의 미복제 트랜잭션은 되살릴 수 없으므로, 사후 처리의 목표는 유실 복구가 아니라 재합류 비용 절감이다. use_pg_rewind와 data checksums(또는 wal_log_hints)를 부트스트랩 시점에 준비해 두면 전체 재복제 없이 되감기로 합류하고, rewind가 실패하는 경우의 자동 재생성 여부는 두 remove_data_directory_* 옵션으로 정한다.