(KO) PostgreSQL 동기 복제 — synchronous_commit과 대기 큐
목차
이론적 배경
섹션 제목: “이론적 배경”postgres-wal-sender-receiver.md에서 설명한 비동기 스트리밍 복제(asynchronous streaming replication)는 WAL이 스탠바이에 어떻게 도달하는지 다룬다. 그러나 그 설계는 의도적으로 주 서버의 COMMIT을 스탠바이의 진행 상황과 연결하지 않는다. 주 서버는 자신의 WAL을 플러시한 뒤 클라이언트에 성공을 반환하고, 스탠바이는 네트워크와 I/O가 허용하는 속도로 따라온다. 이 분리가 비동기 복제를 빠르게 만드는 요인이고, 동시에 데이터를 잃을 수 있게 만드는 요인이다. 주 서버의 디스크와 스탠바이가 로컬 플러시와 원격 수신 사이의 시간 창에서 모두 사라진다면, 이미 확인된 커밋이 소실된다.
동기 복제는 그 시간 창을 커밋을 붙잡아 두는 방식으로 닫는다. 주 서버가 커밋 WAL 레코드를 로컬에 플러시한 뒤 클라이언트에 “커밋됨”을 즉시 알리지 않는다. 대신 설정된 스탠바이 집합이 같은 LSN을 보유하고 있음(또는 플러시했음, 또는 적용했음)을 확인할 때까지 기다린다. 그 후에야 백엔드가 반환한다. 대가는 지연(latency)이다. 동기 커밋은 로컬 fsync에 더해 주 서버→스탠바이 네트워크 왕복을 최소 한 번 지불한다.
이것이 모든 복제된 로그 시스템이 직면하는 고전적인 내구성 대 지연(durability-vs-latency) 트레이드오프다. Kleppmann의 Designing Data-Intensive Applications(ch. 5, “Replication”)는 이를 동기 팔로워(synchronous follower) 대 비동기 팔로워로 정의한다. 동기 팔로워는 최신 복사본을 보장하지만 팔로워가 느리거나 다운되면 리더를 블로킹한다. 비동기 팔로워는 리더를 절대 블로킹하지 않지만, 페일오버 시 최근 확인된 쓰기를 잃을 수 있다. Kleppmann이 제시하는 실용적인 중간 지점인 세미 동기(semi-synchronous) — 팔로워 하나는 동기로, 나머지는 비동기로 — 는 FIRST 1 (...)로 구성된 PostgreSQL synchronous_standby_names의 형태와 정확히 일치한다.
동기 복제 설계가 고정해야 할 세 가지 독립 축이 있으며, PostgreSQL은 이 셋을 모두 설정으로 노출한다.
-
어떤 이벤트가 “확인”으로 간주되는가? 스탠바이에는 단조롭게 강해지는 세 시점이 있다. WAL 바이트가 OS에 기록(write) 됐는가(페이지 캐시에만 있을 수 있음), 영속 스토리지에 플러시(flush) 됐는가(fsync), 스탠바이의 데이터 페이지에 적용(apply) 됐는가. 플러시까지 기다리면 스탠바이 크래시에서 데이터 손실이 없음을 보장한다. 적용까지 기다리면 스탠바이에서의 읽기가 커밋과 인과적으로 일치함도 보장한다.
-
스탠바이는 몇 개, 어떤 것을 기준으로 하는가? 스탠바이가 여럿일 때 주 서버는 전부, k개 중 아무거나(정족수), 또는 우선순위 상위 k개를 요구할 수 있다. 정족수는 구성원 하나의 손실을 허용하고, 우선순위는 결정적인 페일오버 순서를 제공한다.
-
정책이 어디에 있는가? 주 서버가 내구성 요구 사항을 스탠바이에 위임하거나(각 스탠바이가 자신이 확인해야 함을 앎), 모든 정책을 주 서버에 둘 수 있다(스탠바이는 그냥 스트림하고 응답하며, 주 서버의 트랜잭션 내구성 요구 사항을 모름). PostgreSQL은 두 번째를 택했다.
syncrep.c파일 헤더는 이 설계를 명시한다. “대기/해제에 관한 모든 로직을 주 서버에 격리한다. … 스탠바이는 주 서버 트랜잭션의 내구성 요구 사항을 완전히 알지 못한다.”
이론적 토대는 postgres-xlog-wal.md에서 설명하는 LSN-바이트-오프셋 모델과 같다. 커밋은 XactLastRecEnd에서 끝나는 WAL 레코드를 생성하고, “스탠바이가 커밋을 확인했다”는 것은 “스탠바이의 보고된 write/flush/apply LSN이 해당 값에 도달하거나 초과했다”는 뜻으로 축약된다. 따라서 동기 복제는 LSN에서 대기하는 문제이며, 이 모듈 전체는 LSN 순서 대기 큐와 큐 헤드의 LSN이 충족됐는지 결정하는 정책으로 구성된다.
DBMS 공통 설계
섹션 제목: “DBMS 공통 설계”동기 모드를 제공하는 복제 데이터베이스는 소수의 구조적 선택으로 수렴한다. 이를 먼저 정리해 두면, PostgreSQL의 구체적 기호들이 공유된 설계 공간 안의 한 지점으로 읽힌다.
로컬 내구성 이후의 커밋 경로 삽입
섹션 제목: “로컬 내구성 이후의 커밋 경로 삽입”대기는 항상 로컬 WAL 플러시 이후에 삽입된다. 커밋 중인 트랜잭션은 이미 주 서버에서 내구성이 있다. 남은 것은 원격 내구성을 확인하는 일이다. 이 순서는 크래시 의미론에 중요하다. 주 서버가 대기 중에 크래시하더라도, 클라이언트가 확인을 받지 못했더라도 트랜잭션은 로컬에서 복구 가능하다. PostgreSQL은 이 호출을 정확히 여기에 배치한다. RecordTransactionCommit이 XLOG를 플러시하고 CLOG를 표시한 뒤 SyncRepWaitForLSN을 호출한다.
LSN을 키로 하는 공유 메모리 대기 구조
섹션 제목: “LSN을 키로 하는 공유 메모리 대기 구조”커밋 중인 백엔드는 스탠바이의 진행 상황을 바쁜 폴링(busy-polling)으로 확인할 수 없다. 잠들어서 깨어나야 한다. 보편적인 패턴은 “프로세스 P가 LSN L을 기다린다”를 기록하는 공유 메모리 구조와, 각 진행 보고 시 LSN에 도달한 모든 대기자를 해제하는 깨우는 쪽(복제 수신자/송신자)이다. 구조를 LSN 순서로 유지하면 “조건이 충족된 모두 깨우기”가 처음으로 충족되지 않은 대기자에서 멈추는 단일 헤드 탐색으로 바뀐다. 응답당 O(전체-대기자)가 아닌 O(깨어난 수)다.
여러 확인 수준
섹션 제목: “여러 확인 수준”“확인됨”이 여러 의미를 가지므로(write / flush / apply) 성숙한 설계는 수준당 별도의 큐를 유지한다. 적용을 기다리는 백엔드는 단순한 플러시 보고로 깨어나서는 안 된다. PostgreSQL은 NUM_SYNC_REP_WAIT_MODE == 3개의 독립적인 큐와 세 개의 별도 “여기까지 해제됨” 커서를 유지한다.
”충분한” 스탠바이를 위한 정책
섹션 제목: “”충분한” 스탠바이를 위한 정책”N개의 후보 스탠바이와 k 요건이 있을 때, 설계는 동기화된 위치 — 적어도 k개 스탠바이에 있음이 보장된 LSN — 를 정의해야 한다. 정족수(any k)의 경우 k번째로 큰 보고 LSN이다. 우선순위(first k)의 경우 k개 모두 해당 위치에 있어야 하므로 상위 k개 중 가장 작은(최솟값) LSN이다. PostgreSQL은 둘 다 구현한다. 정족수에는 SyncRepGetNthLatestSyncRecPtr, 우선순위에는 SyncRepGetOldestSyncRecPtr이다.
클라이언트에 거짓말할 수 없는 취소
섹션 제목: “클라이언트에 거짓말할 수 없는 취소”대기 중에 블로킹된 백엔드는 민감한 위치에 있다. 트랜잭션은 이미 로컬에 커밋됐다. 대기가 중단되더라도(쿼리 취소, 백엔드 종료, 포스트마스터 사망) 시스템은 클라이언트에 “중단됨”을 알려서는 안 된다. 그것은 거짓이다. 표준 해결책은 “로컬에 커밋됐지만 복제되지 않았을 수 있다”고 명시하는 경고를 내보내고 진행하는 것이다. 상황을 트랜잭션 중단으로 전환하지 않는다.
이론과 PostgreSQL 매핑
섹션 제목: “이론과 PostgreSQL 매핑”| 개념 | PostgreSQL 이름 |
|---|---|
| 커밋 경로 대기 진입점 | SyncRepWaitForLSN(XactLastRecEnd, true) (RecordTransactionCommit에서) |
| 내구성 수준 GUC | synchronous_commit (SyncCommitLevel 열거형) |
| 내부 대기 모드 | SyncRepWaitMode (SYNC_REP_WAIT_WRITE/FLUSH/APPLY) |
| 스탠바이 선택 GUC | synchronous_standby_names |
| 파싱된 선택 정책 | SyncRepConfigData (syncrep_method, num_sync, member_names) |
| 우선순위 방식 | FIRST k (...) → SYNC_REP_PRIORITY |
| 정족수 방식 | ANY k (...) → SYNC_REP_QUORUM |
| LSN 순서 대기 큐 | WalSndCtl->SyncRepQueue[NUM_SYNC_REP_WAIT_MODE] |
| 백엔드별 대기 레코드 | MyProc->waitLSN, ->syncRepState, ->syncRepLinks |
| ”여기까지 해제됨” 커서 | WalSndCtl->lsn[mode] |
| 대기자 깨우기 (walsender 측) | SyncRepReleaseWaiters → SyncRepWakeQueue |
| 동기화된 위치 계산 | SyncRepGetSyncRecPtr |
| 후보 스탠바이 스냅샷 | SyncRepGetCandidateStandbys |
| walsender별 동기 우선순위 | WalSnd->sync_standby_priority |
| 설정 초기화 플래그 | WalSndCtl->sync_standbys_status (SYNC_STANDBY_INIT/DEFINED) |
PostgreSQL의 접근법
섹션 제목: “PostgreSQL의 접근법”두 GUC와 각각의 제어 대상
섹션 제목: “두 GUC와 각각의 제어 대상”동기 복제는 두 개의 사용자 노출 파라미터로 설정되며, 각각 서로 다른 질문에 답한다.
synchronous_commit은 *“얼마나 강한 확인을 원하는가?”*에 답한다. xact.h의 SyncCommitLevel 열거형이다.
// SyncCommitLevel — src/include/access/xact.htypedef enum{ SYNCHRONOUS_COMMIT_OFF, /* asynchronous commit */ SYNCHRONOUS_COMMIT_LOCAL_FLUSH, /* wait for local flush only */ SYNCHRONOUS_COMMIT_REMOTE_WRITE,/* wait for local flush and remote write */ SYNCHRONOUS_COMMIT_REMOTE_FLUSH,/* wait for local and remote flush */ SYNCHRONOUS_COMMIT_REMOTE_APPLY,/* wait for local and remote flush and remote apply */} SyncCommitLevel;
/* Define the default setting for synchronous_commit */#define SYNCHRONOUS_COMMIT_ON SYNCHRONOUS_COMMIT_REMOTE_FLUSH친숙한 설정 이름들은 각각 off → OFF(로컬 플러시도 기다리지 않음 — 비동기 커밋), local → LOCAL_FLUSH(로컬 fsync만, 원격 대기 없음), remote_write → REMOTE_WRITE, on → REMOTE_FLUSH(on의 기본값), remote_apply → REMOTE_APPLY에 대응한다. 마지막 세 개만 원격 대기를 수반한다. off와 local은 동기 복제 큐에 전혀 진입하지 않는다.
GUC 할당 훅이 사용자 가시적 수준을 큐를 인덱싱하는 내부 대기 모드로 변환한다.
// assign_synchronous_commit — src/backend/replication/syncrep.cvoidassign_synchronous_commit(int newval, void *extra){ switch (newval) { case SYNCHRONOUS_COMMIT_REMOTE_WRITE: SyncRepWaitMode = SYNC_REP_WAIT_WRITE; /* 0 */ break; case SYNCHRONOUS_COMMIT_REMOTE_FLUSH: SyncRepWaitMode = SYNC_REP_WAIT_FLUSH; /* 1 */ break; case SYNCHRONOUS_COMMIT_REMOTE_APPLY: SyncRepWaitMode = SYNC_REP_WAIT_APPLY; /* 2 */ break; default: SyncRepWaitMode = SYNC_REP_NO_WAIT; /* -1: off / local */ break; }}synchronous_standby_names는 “어떤 스탠바이의, 몇 개의 확인인가?”라는 직교 질문에 답한다. 값은 소형 문법(syncrep_gram.y)이다.
// productions — src/backend/replication/syncrep_gram.ystandby_list -> create_syncrep_config("1", list, SYNC_REP_PRIORITY)NUM '(' standby_list ')' -> create_syncrep_config(NUM, list, SYNC_REP_PRIORITY)ANY NUM '(' standby_list ')' -> create_syncrep_config(NUM, list, SYNC_REP_QUORUM)FIRST NUM '(' standby_list ')'-> create_syncrep_config(NUM, list, SYNC_REP_PRIORITY)s1, s2, s3은 FIRST 1 (s1, s2, s3)(9.6 시대 단일 스탠바이 동작과의 하위 호환)을 의미하고, ANY 2 (s1, s2, s3)은 셋 중 임의의 둘의 정족수이며, FIRST 2 (s1, s2, s3)는 연결된 가장 높은 우선순위 두 스탠바이를 기다린다. 파싱 결과는 플랫(flat)하고 malloc 가능한 SyncRepConfigData다.
// SyncRepConfigData — src/include/replication/syncrep.htypedef struct SyncRepConfigData{ int config_size; /* total size of this struct, in bytes */ int num_sync; /* number of sync standbys that we need to wait for */ uint8 syncrep_method; /* SYNC_REP_PRIORITY or SYNC_REP_QUORUM */ int nmembers; /* number of members in the following list */ char member_names[FLEXIBLE_ARRAY_MEMBER]; /* nmembers NUL-terminated names */} SyncRepConfigData;check_synchronous_standby_names가 GUC 검사 훅에서 파서를 실행해 파싱된 구조체를 GUC의 “extra”로 보관하고, assign_synchronous_standby_names가 이를 전역 SyncRepConfig에 게시한다. 결과가 플랫 malloc 메모리이므로 메모리 컨텍스트 없이 GUC extra 데이터로 살 수 있다.
커밋 백엔드의 관점: SyncRepWaitForLSN
섹션 제목: “커밋 백엔드의 관점: SyncRepWaitForLSN”진입점은 커밋당 한 번 호출된다. xact.c의 RecordTransactionCommit에서, 트랜잭션이 실제로 WAL을 기록하고 XID를 할당받은 경우에만 호출된다.
// RecordTransactionCommit (excerpt) — src/backend/access/transam/xact.cif (wrote_xlog && markXidCommitted) SyncRepWaitForLSN(XactLastRecEnd, true);XactLastRecEnd는 이 트랜잭션의 커밋 레코드 끝 LSN — 스탠바이들이 도달해야 하는 정확한 위치다. commit = true 인수는 이 LSN이 커밋 레코드임을 대기에 알린다. remote_apply의 경우에 의미가 있다(커밋 레코드만 적용 피드백을 생성하므로, 비커밋 LSN은 플러시 수준으로 캡핑된다).
SyncRepWaitForLSN은 모든 커밋에서 실행되므로 동기 복제가 꺼져 있을 때 빠른 경로를 제공하도록 설계됐다. 빠른 경로는 잠금 없이 SyncRepRequested()와 sync_standbys_status 플래그를 확인한다.
// SyncRepWaitForLSN fast path — src/backend/replication/syncrep.cif (!SyncRepRequested() || ((((volatile WalSndCtlData *) WalSndCtl)->sync_standbys_status) & (SYNC_STANDBY_INIT | SYNC_STANDBY_DEFINED)) == SYNC_STANDBY_INIT) return;
/* Cap the level for anything other than commit to remote flush only. */if (commit) mode = SyncRepWaitMode;else mode = Min(SyncRepWaitMode, SYNC_REP_WAIT_FLUSH);SyncRepRequested()는 (max_wal_senders > 0 && synchronous_commit > SYNCHRONOUS_COMMIT_LOCAL_FLUSH) 매크로다. 수준이 off 또는 local이면 할 일이 없다. 두 번째 절은 “체크포인터가 상태 데이터를 초기화했지만 동기 스탠바이가 정의되지 않음” 경우다. SYNC_STANDBY_INIT는 설정됐지만 SYNC_STANDBY_DEFINED는 해제된 상태이면 잠금 없이 건너뛸 수 있다.
빠른 경로가 종료하지 않으면, 백엔드는 SyncRepLock을 독점적으로 취득하고 잠금 하에 재확인(SyncRepUpdateSyncStandbysDefined의 경쟁 조건 해소)한 뒤 자신을 큐에 삽입한다.
// SyncRepWaitForLSN enqueue — src/backend/replication/syncrep.cMyProc->waitLSN = lsn;MyProc->syncRepState = SYNC_REP_WAITING;SyncRepQueueInsert(mode);Assert(SyncRepQueueIsOrderedByLSN(mode));LWLockRelease(SyncRepLock);잠금을 해제한 뒤 래치 루프에서 잠든다. 자신의 procLatch에서만 깨어난다. 백엔드 하나의 상태 기계는 NOT_WAITING → WAITING → WAIT_COMPLETE → NOT_WAITING이다.
// SyncRepWaitForLSN wait loop (condensed) — src/backend/replication/syncrep.cfor (;;){ ResetLatch(MyLatch); if (MyProc->syncRepState == SYNC_REP_WAIT_COMPLETE) break; if (ProcDiePending) { /* WARNING: committed locally, maybe not replicated */ whereToSendOutput = DestNone; SyncRepCancelWait(); break; } if (QueryCancelPending) { QueryCancelPending = false; /* WARNING ... */ SyncRepCancelWait(); break; } rc = WaitLatch(MyLatch, WL_LATCH_SET | WL_POSTMASTER_DEATH, -1, WAIT_EVENT_SYNC_REP); if (rc & WL_POSTMASTER_DEATH) { ProcDiePending = true; whereToSendOutput = DestNone; SyncRepCancelWait(); break; }}취소 시 코드가 트랜잭션을 절대 중단하지 않는다는 점이 핵심이다. 트랜잭션은 이미 로컬에 커밋됐다. 코드가 하는 최대한은 WARNING — “The transaction has already committed locally, but might not have been replicated to the standby.” — 을 내보내고 출력 전송을 중단하는 것이다. 정상 완료(SYNC_REP_WAIT_COMPLETE, walsender가 설정) 시 루프를 빠져나와 pg_read_barrier()를 발행하고 syncRepState/waitLSN을 재설정한다.
flowchart TB
A["RecordTransactionCommit<br/>XLogFlush 로컬 WAL"] --> B["SyncRepWaitForLSN(XactLastRecEnd, true)"]
B --> C{SyncRepRequested<br/>and standbys defined?}
C -- no --> R["즉시 반환<br/>(비동기 / 동기 스탠바이 없음)"]
C -- yes --> D["SyncRepLock 획득<br/>waitLSN 설정, syncRepState=WAITING"]
D --> E["SyncRepQueueInsert(mode)<br/>LSN 오름차순 삽입"]
E --> F["SyncRepLock 해제"]
F --> G["래치 대기 루프"]
G --> H{syncRepState ==<br/>WAIT_COMPLETE?}
H -- yes --> I["read barrier<br/>상태 재설정, 클라이언트 반환"]
H -- no --> J{ProcDie / QueryCancel /<br/>PostmasterDeath?}
J -- yes --> K["WARNING: 로컬 커밋됨<br/>SyncRepCancelWait, break"]
J -- no --> L["WaitLatch on procLatch"]
L --> G
Figure 1 — SyncRepWaitForLSN을 통한 커밋 백엔드 경로. 대기가 시작되기 전에 트랜잭션은 이미 로컬에서 내구성이 있다. 완료(walsender가 깨움)와 로컬 커밋을 보존하는 중단, 두 가지 종료만 있다. (syncrep.c의 SyncRepWaitForLSN과 RecordTransactionCommit의 호출 지점에서.)
대기 큐: 공유 메모리의 세 LSN 순서 리스트
섹션 제목: “대기 큐: 공유 메모리의 세 LSN 순서 리스트”대기자들은 walsender들이 사용하는 공유 메모리 제어 블록인 WalSndCtl에 있다. 동기 복제 부분은 세 개의 이중 연결 리스트 헤드(대기 모드당 하나)와 세 개의 “여기까지 해제됨” 커서다.
// WalSndCtlData (sync-rep fields) — src/include/replication/walsender_private.htypedef struct{ dlist_head SyncRepQueue[NUM_SYNC_REP_WAIT_MODE]; /* one queue per request type */ XLogRecPtr lsn[NUM_SYNC_REP_WAIT_MODE]; /* head-of-queue release cursor */ bits8 sync_standbys_status; /* SYNC_STANDBY_INIT | _DEFINED */ /* ... condition variables, walsnds[] flexible array ... */} WalSndCtlData;각 백엔드의 링크 노드와 대기 LSN은 자신의 PGPROC에 있다.
// PGPROC (sync-rep fields) — src/include/storage/proc.hXLogRecPtr waitLSN; /* waiting for this LSN or higher */int syncRepState; /* wait state for sync rep */dlist_node syncRepLinks; /* list link if process is in syncrep queue */SyncRepQueueInsert는 각 큐를 waitLSN 오름차순으로 정렬 유지한다. 대부분의 커밋은 LSN 순서로 도착하므로(나중 커밋은 더 큰 XactLastRecEnd를 가짐) 일반적인 경우 꼬리에 추가된다. 따라서 함수는 꼬리에서 역방향으로 스캔해 삽입 지점을 찾는다.
// SyncRepQueueInsert — src/backend/replication/syncrep.cqueue = &WalSndCtl->SyncRepQueue[mode];dlist_reverse_foreach(iter, queue){ PGPROC *proc = dlist_container(PGPROC, syncRepLinks, iter.cur); /* Stop at the element we should insert after, to keep queue LSN-ordered. */ if (proc->waitLSN < MyProc->waitLSN) { dlist_insert_after(&proc->syncRepLinks, &MyProc->syncRepLinks); return; }}/* list was empty, or we belong at the head */dlist_push_head(queue, &MyProc->syncRepLinks);정렬 불변식이 깨우기를 O(깨어난 수)로 만든다. 깨우는 쪽은 헤드에서 탐색을 시작해 waitLSN이 해제된 위치를 초과하는 첫 번째 대기자에서 멈춘다. 단언(assertion) 하에서 SyncRepQueueIsOrderedByLSN은 모든 삽입과 깨우기에서 불변식을(그리고 두 대기자가 동일 LSN을 공유하지 않음을) 검증한다.
walsender의 관점: 대기자 해제
섹션 제목: “walsender의 관점: 대기자 해제”깨우는 쪽은 walsender들이다. walsender의 ProcessStandbyReplyMessage(postgres-wal-sender-receiver.md 참조)가 스탠바이의 보고된 write/flush/apply 위치를 갱신하면 비캐스케이딩 스탠바이 경로에서 SyncRepReleaseWaiters를 호출한다. 이 함수는 이 walsender의 스탠바이가 동기 스탠바이 중 하나인지 결정하고, 모든 동기 스탠바이에 걸쳐 동기화된 위치를 재계산하고, 해제 커서를 전진시키고, 충족된 대기자들을 깨운다.
이 walsender가 잠재적 동기 스탠바이가 아니거나, 아직 스트리밍 중이 아니거나, 유효한 플러시 위치가 없으면 즉시 종료한다.
// SyncRepReleaseWaiters guard — src/backend/replication/syncrep.cif (MyWalSnd->sync_standby_priority == 0 || (MyWalSnd->state != WALSNDSTATE_STREAMING && MyWalSnd->state != WALSNDSTATE_STOPPING) || XLogRecPtrIsInvalid(MyWalSnd->flush)){ announce_next_takeover = true; return;}그렇지 않으면 SyncRepLock을 독점적으로 취득하고, 동기화된 위치를 계산하고, 이 walsender가 실제로 동기 스탠바이이고 충분한 수가 있는 경우에만 각 모드별 커서를 전진시키고 해당 모드 큐를 깨운다.
// SyncRepReleaseWaiters release loop — src/backend/replication/syncrep.cgot_recptr = SyncRepGetSyncRecPtr(&writePtr, &flushPtr, &applyPtr, &am_sync);/* ... if (!got_recptr || !am_sync) release lock and leave ... */if (walsndctl->lsn[SYNC_REP_WAIT_WRITE] < writePtr){ walsndctl->lsn[SYNC_REP_WAIT_WRITE] = writePtr; numwrite = SyncRepWakeQueue(false, SYNC_REP_WAIT_WRITE);}if (walsndctl->lsn[SYNC_REP_WAIT_FLUSH] < flushPtr){ walsndctl->lsn[SYNC_REP_WAIT_FLUSH] = flushPtr; numflush = SyncRepWakeQueue(false, SYNC_REP_WAIT_FLUSH);}if (walsndctl->lsn[SYNC_REP_WAIT_APPLY] < applyPtr){ walsndctl->lsn[SYNC_REP_WAIT_APPLY] = applyPtr; numapply = SyncRepWakeQueue(false, SYNC_REP_WAIT_APPLY);}LWLockRelease(SyncRepLock);커서는 단조롭게 전진한다. < 가드가 늦거나 순서가 뒤바뀐 응답이 해제 지점을 되감는 것을 막는다. 세 모드는 각자의 큐에서 독립적으로 해제된다.
SyncRepWakeQueue가 실제 깨우기다. 모드의 큐를 헤드에서 탐색해 waitLSN에 도달한 각 대기자를 큐에서 분리하고, 상태를 SYNC_REP_WAIT_COMPLETE로 설정하고, 래치를 설정한다.
// SyncRepWakeQueue — src/backend/replication/syncrep.cdlist_foreach_modify(iter, &WalSndCtl->SyncRepQueue[mode]){ PGPROC *proc = dlist_container(PGPROC, syncRepLinks, iter.cur); /* Queue is ordered by LSN: stop at first unsatisfied waiter. */ if (!all && walsndctl->lsn[mode] < proc->waitLSN) return numprocs; dlist_delete_thoroughly(&proc->syncRepLinks); pg_write_barrier(); /* publish detach before state */ proc->syncRepState = SYNC_REP_WAIT_COMPLETE; SetLatch(&(proc->procLatch)); numprocs++;}pg_write_barrier()는 SyncRepWaitForLSN의 pg_read_barrier()와 쌍을 이룬다. 대기자는 잠금 없이 syncRepState를 읽으므로, 깨우는 쪽은 상태가 WAIT_COMPLETE로 바뀌기 전에 큐 분리를 가시화해야 한다. 이렇게 해야 깨어난 백엔드가 자신이 큐에서 떠났음을 볼 수 있다.
동기화된 위치 선택: 우선순위 대 정족수
섹션 제목: “동기화된 위치 선택: 우선순위 대 정족수”SyncRepGetSyncRecPtr에서 FIRST/ANY 정책이 구체화된다. 먼저 후보 스탠바이를 스냅샷하고, 이 walsender가 그 중에 있는지 확인하고, 최소 num_sync개의 후보가 있는지 검증한다.
// SyncRepGetSyncRecPtr (excerpt) — src/backend/replication/syncrep.cnum_standbys = SyncRepGetCandidateStandbys(&sync_standbys);for (i = 0; i < num_standbys; i++) if (sync_standbys[i].is_me) { *am_sync = true; break; }if (!(*am_sync) || num_standbys < SyncRepConfig->num_sync){ pfree(sync_standbys); return false; /* not enough sync standbys yet — don't release */}if (SyncRepConfig->syncrep_method == SYNC_REP_PRIORITY) SyncRepGetOldestSyncRecPtr(writePtr, flushPtr, applyPtr, sync_standbys, num_standbys);else SyncRepGetNthLatestSyncRecPtr(writePtr, flushPtr, applyPtr, sync_standbys, num_standbys, SyncRepConfig->num_sync);우선순위(FIRST k)의 경우, SyncRepGetCandidateStandbys가 이미 후보 목록을 우선순위 상위 num_sync개로 잘랐다. 보장된 위치는 그들 중 가장 작은(최솟값) LSN이다. k개 모두 적어도 그 지점에 있기 때문이다.
// SyncRepGetOldestSyncRecPtr (excerpt) — src/backend/replication/syncrep.cfor (i = 0; i < num_standbys; i++){ if (XLogRecPtrIsInvalid(*flushPtr) || *flushPtr > sync_standbys[i].flush) *flushPtr = sync_standbys[i].flush; /* ... same for write and apply ... */}정족수(ANY k)의 경우, 후보 중 k개면 충분하므로 보장된 위치는 k번째로 큰 LSN이다. 내림차순 정렬 후 인덱스 nth - 1을 취한다.
// SyncRepGetNthLatestSyncRecPtr (excerpt) — src/backend/replication/syncrep.cfor (i = 0; i < num_standbys; i++) { write_array[i] = sync_standbys[i].write; /* ... */ }qsort(write_array, num_standbys, sizeof(XLogRecPtr), cmp_lsn); /* descending */qsort(flush_array, num_standbys, sizeof(XLogRecPtr), cmp_lsn);qsort(apply_array, num_standbys, sizeof(XLogRecPtr), cmp_lsn);*writePtr = write_array[nth - 1];*flushPtr = flush_array[nth - 1];*applyPtr = apply_array[nth - 1];cmp_lsn은 pg_cmp_u64(lsn2, lsn1)로 내림차순 정렬한다. 따라서 [nth-1]이 k번째로 높은 값이 되며, 이는 적어도 k개 스탠바이가 도달한 LSN이다.
flowchart TB
A["walsender: 스탠바이 응답<br/>ProcessStandbyReplyMessage"] --> B["SyncRepReleaseWaiters"]
B --> C{이 walsender가 동기<br/>후보이고, 스트리밍 중,<br/>유효한 flush?}
C -- no --> Z["반환 (announce_next_takeover)"]
C -- yes --> D["SyncRepGetCandidateStandbys<br/>스핀락 하에 WalSnd[] 스냅샷"]
D --> E{am_sync이고<br/>num_standbys >= num_sync?}
E -- no --> Z
E -- yes --> F{syncrep_method?}
F -- PRIORITY (FIRST) --> G["SyncRepGetOldestSyncRecPtr<br/>우선순위 상위-k 중 최솟값 LSN"]
F -- QUORUM (ANY) --> H["SyncRepGetNthLatestSyncRecPtr<br/>k번째 최댓값 LSN (qsort 내림차순)"]
G --> I["WalSndCtl->lsn[mode] 전진<br/>(더 크면)"]
H --> I
I --> J["SyncRepWakeQueue(false, mode)<br/>lsn[mode]까지 대기자 깨움"]
J --> K["syncRepState=WAIT_COMPLETE 설정<br/>각 깨어난 proc에 SetLatch"]
Figure 2 — 스탠바이 응답이 대기자를 해제하는 방식. walsender는 synchronous_standby_names의 정책(FIRST: 상위-k의 최솟값, ANY: k번째 최댓값)으로 동기화된 위치를 계산하고, 모드별 해제 커서를 전진시키고, waitLSN이 충족된 모든 큐 백엔드를 깨운다. (syncrep.c의 SyncRepReleaseWaiters / SyncRepGetSyncRecPtr / SyncRepWakeQueue에서.)
후보 스냅샷과 walsender별 우선순위
섹션 제목: “후보 스냅샷과 walsender별 우선순위”SyncRepGetCandidateStandbys는 WalSndCtl->walsnds[]를 탐색하면서, 각 활성 walsender의 보고 위치와 sync_standby_priority를 해당 walsender의 스핀락 하에 사설 SyncRepStandbyData 배열로 복사한다. walsender가 후보가 되려면 PID가 있고, STREAMING 또는 STOPPING 상태이고, 우선순위가 0이 아니고, 유효한 플러시 위치를 가져야 한다. 우선순위 모드에서 num_sync보다 많은 후보가 있으면 배열을 우선순위 순으로 정렬하고 num_sync로 자른다.
// SyncRepGetCandidateStandbys (excerpt) — src/backend/replication/syncrep.cif (SyncRepConfig->syncrep_method == SYNC_REP_PRIORITY && n > SyncRepConfig->num_sync){ qsort(*standbys, n, sizeof(SyncRepStandbyData), standby_priority_comparator); n = SyncRepConfig->num_sync; /* keep only the highest-priority ones */}각 walsender는 시작 시와 SIGHUP 이후 SyncRepInitConfig → SyncRepGetStandbyPriority 호출로 자신의 우선순위를 한 번 계산한다. 스탠바이의 application_name을 파싱된 member_names 목록(또는 * 와일드카드)과 대조한다. FIRST에서는 목록의 위치가 우선순위이고, ANY에서는 모든 구성원의 우선순위가 1이다. 캐스케이딩 walsender는 항상 우선순위 0을 받는다(동기 캐스케이드 복제는 지원되지 않음).
// SyncRepGetStandbyPriority (excerpt) — src/backend/replication/syncrep.cif (am_cascading_walsender) return 0;if (!SyncStandbysDefined() || SyncRepConfig == NULL) return 0;standby_name = SyncRepConfig->member_names;for (priority = 1; priority <= SyncRepConfig->nmembers; priority++){ if (pg_strcasecmp(standby_name, application_name) == 0 || strcmp(standby_name, "*") == 0) { found = true; break; } standby_name += strlen(standby_name) + 1;}if (!found) return 0;return (SyncRepConfig->syncrep_method == SYNC_REP_PRIORITY) ? priority : 1;상태 플래그가 닫는 경쟁 조건
섹션 제목: “상태 플래그가 닫는 경쟁 조건”synchronous_standby_names는 런타임에 빈 값으로 설정될 수 있다(SIGHUP). 단순한 구현이라면 아무도 깨워주지 않아 백엔드가 큐에서 영원히 막혀 있을 것이다. 체크포인터가 sync_standbys_status 플래그를 소유하고 SyncRepUpdateSyncStandbysDefined에서 이를 조정한다. GUC가 빈 값으로 전환되면 모든 모드에서 모든 큐를 깨우고(SyncRepWakeQueue(true, i) 반복 호출) SYNC_STANDBY_DEFINED를 해제한다. 플래그는 큐 진입도 막으므로, 설정을 아직 다시 로드하지 않은 백엔드는 플러시 후 큐에 진입하지 못한다.
// SyncRepUpdateSyncStandbysDefined (excerpt) — src/backend/replication/syncrep.cif (!sync_standbys_defined){ for (i = 0; i < NUM_SYNC_REP_WAIT_MODE; i++) SyncRepWakeQueue(true, i); /* all=true: drain the queue unconditionally */}WalSndCtl->sync_standbys_status = SYNC_STANDBY_INIT | (sync_standbys_defined ? SYNC_STANDBY_DEFINED : 0);SyncRepWaitForLSN이 큐에 진입하기 전에 잠금 하에서 SYNC_STANDBY_DEFINED를 재확인하는 이유가 여기에 있다. 플래그와 큐는 SyncRepLock 하에 원자적으로 조작되므로, 방금 비워진 큐에 백엔드가 진입하는 시간 창이 없다.
소스 탐방
섹션 제목: “소스 탐방”기호를 역할별로 그룹화했다. 파일은 /data/hgryoo/references/postgres/ 아래에 있다.
설정: 두 GUC (xact.h, syncrep.h, syncrep_gram.y, syncrep.c)
섹션 제목: “설정: 두 GUC (xact.h, syncrep.h, syncrep_gram.y, syncrep.c)”SyncCommitLevel(열거형) —SYNCHRONOUS_COMMIT_OFF…_REMOTE_APPLY;SYNCHRONOUS_COMMIT_ON은_REMOTE_FLUSH의 별칭.synchronous_commit(GUC int) — 현재 수준.SyncRepWaitMode(파일-정적 int) — 수준에서 파생된 내부 모드:SYNC_REP_NO_WAIT(-1),SYNC_REP_WAIT_WRITE/FLUSH/APPLY(0/1/2).NUM_SYNC_REP_WAIT_MODE— 상수 3; 큐와 커서의 차원.SyncRepRequested()(매크로) —max_wal_senders > 0 && synchronous_commit > SYNCHRONOUS_COMMIT_LOCAL_FLUSH.assign_synchronous_commit— GUC 할당 훅; 수준 →SyncRepWaitMode매핑.SyncRepConfigData(구조체) — 파싱된synchronous_standby_names:num_sync,syncrep_method,nmembers,member_names[].SYNC_REP_PRIORITY/SYNC_REP_QUORUM—syncrep_method값.SyncStandbysDefined()(매크로) —SyncRepStandbyNames가 비어 있지 않음.check_synchronous_standby_names/assign_synchronous_standby_names— GUC 검사/할당 훅;syncrep_yyparse로 파싱,SyncRepConfig게시.
커밋 백엔드 (syncrep.c, xact.c, proc.h)
섹션 제목: “커밋 백엔드 (syncrep.c, xact.c, proc.h)”SyncRepWaitForLSN— 커밋 경로 진입; 빠른 경로 종료, 큐 삽입, 래치 대기 루프, 취소 처리.RecordTransactionCommit— 호출 지점: 로컬XLogFlush이후SyncRepWaitForLSN(XactLastRecEnd, true).MyProc->waitLSN/->syncRepState/->syncRepLinks—PGPROC의 백엔드별 대기 레코드.SYNC_REP_NOT_WAITING/SYNC_REP_WAITING/SYNC_REP_WAIT_COMPLETE—syncRepState값.SyncRepQueueInsert—MyProc를 모드 큐에 삽입,waitLSN으로 정렬(꼬리에서 스캔).SyncRepCancelWait— 중단 시 잠금 하에 큐에서 분리.SyncRepCleanupAtProcExit— 백엔드 종료 시 분리(잠금-없는 사전 확인).
공유 메모리 대기 구조 (walsender_private.h)
섹션 제목: “공유 메모리 대기 구조 (walsender_private.h)”WalSndCtlData.SyncRepQueue[NUM_SYNC_REP_WAIT_MODE]— 세 개의 LSN 순서 큐.WalSndCtlData.lsn[NUM_SYNC_REP_WAIT_MODE]— 모드별 “여기까지 해제됨” 커서.WalSndCtlData.sync_standbys_status—SYNC_STANDBY_INIT/SYNC_STANDBY_DEFINED비트.SyncRepQueueIsOrderedByLSN(단언 전용) — 정렬 불변식 검증.
walsender 해제 측 (syncrep.c)
섹션 제목: “walsender 해제 측 (syncrep.c)”SyncRepReleaseWaiters—ProcessStandbyReplyMessage에서 호출; 커서를 전진시키고 큐를 깨움.SyncRepGetSyncRecPtr— 동기화된 write/flush/apply 계산;syncrep_method로 분기;am_sync설정.SyncRepGetOldestSyncRecPtr— 우선순위: 후보들의 최솟값 LSN.SyncRepGetNthLatestSyncRecPtr— 정족수: k번째 최댓값 LSN (qsort 내림차순).cmp_lsn— 내림차순XLogRecPtr비교자.SyncRepGetCandidateStandbys—WalSnd[]스냅샷; 활성/스트리밍/우선순위/유효-flush 필터; 우선순위 모드에서num_sync로 자름.standby_priority_comparator— 우선순위로 정렬,walsnd_index로 동점 처리.SyncRepWakeQueue— 큐를 헤드에서 탐색, 충족된 대기자 깨움,WAIT_COMPLETE설정.SyncRepInitConfig/SyncRepGetStandbyPriority— 시작 / SIGHUP 시 walsender별 우선순위 계산.WalSnd->sync_standby_priority— 이 walsender의 우선순위 (0 = 동기 후보 아님).SyncRepStandbyData(구조체) — 후보의write/flush/apply/priority/is_me사설 복사본.
체크포인터 조정 (syncrep.c)
섹션 제목: “체크포인터 조정 (syncrep.c)”SyncRepUpdateSyncStandbysDefined—sync_standbys_status유지; GUC가 비어지면 큐 비움.
위치 힌트 (2026-06-05 기준, REL_18 273fe94)
섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”| 기호 | 파일 | 행 |
|---|---|---|
SyncCommitLevel (열거형) | src/include/access/xact.h | 68 |
SYNCHRONOUS_COMMIT_ON (매크로) | src/include/access/xact.h | 80 |
SyncRepRequested (매크로) | src/include/replication/syncrep.h | 18 |
SYNC_REP_WAIT_WRITE (매크로) | src/include/replication/syncrep.h | 23 |
NUM_SYNC_REP_WAIT_MODE (매크로) | src/include/replication/syncrep.h | 27 |
SYNC_REP_NOT_WAITING (매크로) | src/include/replication/syncrep.h | 30 |
SYNC_REP_PRIORITY (매크로) | src/include/replication/syncrep.h | 35 |
SyncRepStandbyData (구조체) | src/include/replication/syncrep.h | 42 |
SyncRepConfigData (구조체) | src/include/replication/syncrep.h | 63 |
WalSndCtlData.SyncRepQueue | src/include/replication/walsender_private.h | 90 |
WalSndCtlData.lsn | src/include/replication/walsender_private.h | 96 |
WalSndCtlData.sync_standbys_status | src/include/replication/walsender_private.h | 103 |
SYNC_STANDBY_INIT (매크로) | src/include/replication/walsender_private.h | 125 |
SYNC_STANDBY_DEFINED (매크로) | src/include/replication/walsender_private.h | 132 |
PGPROC.waitLSN | src/include/storage/proc.h | 267 |
PGPROC.syncRepState | src/include/storage/proc.h | 268 |
PGPROC.syncRepLinks | src/include/storage/proc.h | 269 |
SyncRepWaitForLSN | src/backend/replication/syncrep.c | 148 |
SyncRepQueueInsert | src/backend/replication/syncrep.c | 372 |
SyncRepCancelWait | src/backend/replication/syncrep.c | 406 |
SyncRepCleanupAtProcExit | src/backend/replication/syncrep.c | 416 |
SyncRepInitConfig | src/backend/replication/syncrep.c | 445 |
SyncRepReleaseWaiters | src/backend/replication/syncrep.c | 474 |
SyncRepGetSyncRecPtr | src/backend/replication/syncrep.c | 586 |
SyncRepGetOldestSyncRecPtr | src/backend/replication/syncrep.c | 660 |
SyncRepGetNthLatestSyncRecPtr | src/backend/replication/syncrep.c | 693 |
cmp_lsn | src/backend/replication/syncrep.c | 738 |
SyncRepGetCandidateStandbys | src/backend/replication/syncrep.c | 754 |
standby_priority_comparator | src/backend/replication/syncrep.c | 833 |
SyncRepGetStandbyPriority | src/backend/replication/syncrep.c | 860 |
SyncRepWakeQueue | src/backend/replication/syncrep.c | 907 |
SyncRepUpdateSyncStandbysDefined | src/backend/replication/syncrep.c | 964 |
SyncRepQueueIsOrderedByLSN | src/backend/replication/syncrep.c | 1024 |
check_synchronous_standby_names | src/backend/replication/syncrep.c | 1058 |
assign_synchronous_standby_names | src/backend/replication/syncrep.c | 1118 |
assign_synchronous_commit | src/backend/replication/syncrep.c | 1124 |
SyncRepWaitForLSN 호출 지점 | src/backend/access/transam/xact.c | 1557 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”아래 모든 주장은 2026-06-05에 커밋 273fe94의 REL_18_STABLE에서 재확인했다. 기호 행 번호는 위의 위치 힌트 표에 있다.
검증된 사실
섹션 제목: “검증된 사실”-
모든 동기 복제 로직은 주 서버에서 실행된다. 스탠바이는 자신이 동기 복제 대상임을 알지 못한다.
syncrep.c파일 헤더에서 확인. “이 모듈의 모든 코드는 주 서버에서 실행된다. … 대기/해제에 관한 모든 로직을 주 서버에 격리한다. 스탠바이는 주 서버 트랜잭션의 내구성 요구 사항을 완전히 알지 못한다.” 스탠바이의walreceiver는 어느 주 서버의synchronous_standby_names에 이름이 있는지와 관계없이 동일한 write/flush/apply 피드백을 보낸다. -
대기는 로컬 WAL 플러시 이후에 삽입된다.
RecordTransactionCommit의 호출 지점(xact.c)에서 확인.XactLastRecEnd의 로컬XLogFlush와 CLOG 상태 갱신이SyncRepWaitForLSN(XactLastRecEnd, true)호출보다 앞에 온다. 주 서버가 대기 중에 크래시하더라도 트랜잭션은 클라이언트가 확인을 받지 못했더라도 로컬에서 복구 가능하다. -
synchronous_commit은 다섯 수준을 갖는다.on은remote_flush다.SyncCommitLevel(xact.h)에서 확인.OFF,LOCAL_FLUSH,REMOTE_WRITE,REMOTE_FLUSH,REMOTE_APPLY.#define SYNCHRONOUS_COMMIT_ON SYNCHRONOUS_COMMIT_REMOTE_FLUSH.assign_synchronous_commit은 세REMOTE_*수준만 실제 대기 모드에 매핑한다.off와local은SYNC_REP_NO_WAIT(-1)을 산출한다. -
대기 모드와 큐는 정확히 세 개다.
NUM_SYNC_REP_WAIT_MODE == 3(syncrep.h) 확인.WalSndCtlData는dlist_head SyncRepQueue[NUM_SYNC_REP_WAIT_MODE]와XLogRecPtr lsn[NUM_SYNC_REP_WAIT_MODE]를 가진다(walsender_private.h). 세 모드는SyncRepReleaseWaiters에서 독립적으로 해제된다. -
SyncRepWaitForLSN은SyncRepRequested()와sync_standbys_status를 기반으로 잠금-없는 빠른 경로를 가진다.!SyncRepRequested()이거나 상태 플래그가 정확히SYNC_STANDBY_INIT(초기화됐지만 동기 스탠바이 미정의)이면 즉시 반환한다. 두 단락-회로 모두 발동하지 않으면SyncRepLock을 독점적으로 취득하고SYNC_STANDBY_DEFINED를 재확인한다. -
대기 큐는 오름차순
waitLSN으로 엄격히 정렬된다.SyncRepQueueInsert(꼬리에서 역방향 스캔, 더 작은waitLSN을 가진 첫 번째 요소 뒤에 삽입)에서 확인되며,SyncRepQueueIsOrderedByLSN으로 모든 삽입/깨우기에서 단언된다. 이 불변식이SyncRepWakeQueue를 첫 번째 충족되지 않은 대기자에서 멈추게 한다. -
FIRST k(우선순위)는 상위-k 후보의 최솟값 LSN을 사용한다.ANY k(정족수)는 k번째 최댓값 LSN을 사용한다.SyncRepGetSyncRecPtr이SyncRepConfig->syncrep_method로 분기해SYNC_REP_PRIORITY에는SyncRepGetOldestSyncRecPtr(최솟값),SYNC_REP_QUORUM에는SyncRepGetNthLatestSyncRecPtr(cmp_lsn으로 내림차순 qsort 후 인덱스nth - 1)을 호출한다. 우선순위 모드에서SyncRepGetCandidateStandbys는 이미 후보 집합을num_sync개의 최고 우선순위 구성원으로 잘랐다. -
후보 walsender는 활성, 스트리밍/정지 중, 0이 아닌 동기 우선순위, 유효한 flush LSN을 가져야 한다.
SyncRepGetCandidateStandbys에서 확인.pid == 0,WALSNDSTATE_STREAMING/WALSNDSTATE_STOPPING이외의 상태,sync_standby_priority == 0,XLogRecPtrIsInvalid(flush)walsender는 건너뛴다. 캐스케이딩 walsender는SyncRepGetStandbyPriority에서 우선순위 0을 받는다. -
해제 커서는 단조롭게 전진한다.
SyncRepReleaseWaiters에서 확인. 각walsndctl->lsn[mode]는 새로 계산된 위치가 엄격히 더 클 때만(<가드) 갱신된다. -
취소는 트랜잭션을 절대 중단하지 않는다. 경고를 내보내고 진행한다.
SyncRepWaitForLSN대기 루프에서 확인.ProcDiePending시 “canceling the wait for synchronous replication and terminating connection due to administrator command” 경고를 올리고SyncRepCancelWait를 호출한다. 어떤 경로도 롤백하지 않는다. -
synchronous_standby_names가 런타임에 지워지면 모든 큐가 무조건 비워진다.SyncRepUpdateSyncStandbysDefined에서 확인.!sync_standbys_defined이면 모든 모드에SyncRepWakeQueue(true, i)를 호출한다(all = true인수가 LSN 검사를 우회해 모든 대기자를 깨움). 그런 다음SYNC_STANDBY_DEFINED를sync_standbys_status에서 해제한다.
추론 (행 앵커 없음)
섹션 제목: “추론 (행 앵커 없음)”-
SyncRepWakeQueue의pg_write_barrier()(dlist_delete_thoroughly이후,SYNC_REP_WAIT_COMPLETE설정 전)와SyncRepWaitForLSN의pg_read_barrier()(SYNC_REP_WAIT_COMPLETE관찰 이후)의 쌍은 깨어난 백엔드가SyncRepLock없이syncRepState를 읽으면서도 자신이 큐에서 떠난 것을 볼 수 있게 하는 잠금-없는 핸드오프다. -
SyncRepQueueInsert를 꼬리에서 역방향으로 스캔하는 “대부분의 커밋이 꼬리에 추가된다” 근거는 역방향 스캔 코드와 일치하는 성능 관찰이며, 별도로 측정된 주장이 아니다.
PostgreSQL 너머 — 비교 설계와 연구 프런티어
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프런티어”-
세미 동기 복제 (DDIA ch. 5). PostgreSQL의
FIRST 1 (...)은 Kleppmann의 세미 동기 구성과 정확히 일치한다. 팔로워 하나는 동기로, 나머지는 비동기로, 동기 팔로워가 정지하면 다른 것이 그 자리를 차지한다(SyncRepReleaseWaiters의announce_next_takeover가 더 높은 우선순위 스탠바이가 동기 슬롯을 재탈환하는 접합부다). DDIA의 “단일 동기 팔로워” 장애 모드를 PostgreSQL의 우선순위-인계 로직에 매핑하는 정리된 노트가 가용성 이야기를 선명히 할 것이다. -
정족수 복제 대 Dynamo 방식
R + W > N.ANY k (s1..sN)은 N개 후보 스탠바이에 대한 크기 k의 쓰기 정족수지만, PostgreSQL은 읽기 측을 “주 서버에서 읽기”로 고정해 조정 가능한 읽기 정족수를 제공하지 않는다.SyncRepGetNthLatestSyncRecPtr의 “k번째 최댓값 LSN” 규칙을 Dynamo의 느슨한-정족수 + 힌트된-핸드오프 모델과 비교하면 PostgreSQL이 단일 권위적 주 서버를 위해 무엇을 포기하는지(비지도자 쓰기 없음, 읽기-복구 없음)가 드러난다. -
합의 없음: PostgreSQL 동기 복제는 Raft/Paxos가 아니다. 커밋 백엔드가 k개의 확인을 기다리지만, 리더 선출도, 로그-매칭 안전 속성도, 코어의 자동 페일오버도 없다. 주 서버는 단순히 블로킹한다. 주 서버 크래시 시 클러스터는 스탠바이를 승격할 외부 중재자(Patroni, repmgr, pg_auto_failover)가 필요하다. Raft(커밋 인덱스가 복제된 합의 자체이고, 리더십이 프로토콜의 일부인)와의 대조가 “synchronous_commit은 내구성을 보장하지 가용성을 보장하지 않는다”를 설명하는 가장 깔끔한 방법이다.
-
CAP 포지셔닝. 동기 복제는 파티션 하에서 A를 C와 교환한다. 요구된 스탠바이에 연결할 수 없으면 커밋 백엔드는 무한정 블로킹된다(쿼리-취소 / 종료 / 포스트마스터-사망 종료만 있음). 이것이 CAP 삼각형의 CP 모서리다. “왜
synchronous_replication_timeout이 없는가”에 대한 노트 — 그리고 타임아웃이 내구성을 조용히 다운그레이드할 것이라는 명시적 설계 결정 — 가 여기에 속한다. -
ARIES와 커밋-레코드 LSN. 전체 메커니즘은 ARIES 방식 WAL 불변식(
postgres-xlog-wal.md)에 기반한다. 커밋은XactLastRecEnd에서 끝나는 단일 WAL 레코드를 생성하고, “복제됨”은 “스탠바이의 보고된 LSN ≥XactLastRecEnd”로 축약된다. 동기 복제는 새로운 로그 의미론을 추가하지 않는다. 기존 리두 스트림이 원격-내구성을 가진다고 알려질 때까지 클라이언트 확인을 지연시킬 뿐이다. -
적용 수준 대기와 스탠바이의 읽기-자신-쓰기.
remote_apply는 스탠바이로 라우팅된 쿼리가 커밋과 인과적으로 일치하도록 만드는 유일한 수준이다. 대가는 스탠바이 재실행(세 피드백 지점 중 가장 느림)을 기다리는 것이다. 명시적 세션 수준 “읽기-자신-쓰기” 토큰(LSN 핸드오프를 애플리케이션에)을 노출하는 시스템과 비교하면 PostgreSQL이 멈추는 지점이 드러난다. 클러스터 전체 보장을 제공하지만 코어에 세션별 인과 토큰은 없다.
인-트리 소스 파일 (REL_18_STABLE, 커밋 273fe94)
섹션 제목: “인-트리 소스 파일 (REL_18_STABLE, 커밋 273fe94)”src/backend/replication/syncrep.c— 전체 모듈:SyncRepWaitForLSN(커밋 경로 대기, 빠른 경로, 래치 루프, 취소),SyncRepQueueInsert/SyncRepWakeQueue/SyncRepQueueIsOrderedByLSN(LSN 순서 큐),SyncRepReleaseWaiters/SyncRepGetSyncRecPtr/SyncRepGetOldestSyncRecPtr/SyncRepGetNthLatestSyncRecPtr/SyncRepGetCandidateStandbys(해제 측과 FIRST/ANY 정책),SyncRepGetStandbyPriority/SyncRepInitConfig(walsender별 우선순위),SyncRepUpdateSyncStandbysDefined(체크포인터 조정),assign_synchronous_commit/check_synchronous_standby_names/assign_synchronous_standby_names(GUC 훅).src/backend/replication/syncrep_gram.y와syncrep_scanner.l—synchronous_standby_names문법.SYNC_REP_PRIORITY(FIRST/나열) 또는SYNC_REP_QUORUM(ANY)를 가진SyncRepConfigData생성.src/include/replication/syncrep.h—SyncRepRequested/SyncStandbysDefined매크로,SYNC_REP_WAIT_*와NUM_SYNC_REP_WAIT_MODE상수,SYNC_REP_NOT_WAITING/WAITING/WAIT_COMPLETE상태,SyncRepStandbyData,SyncRepConfigData.src/include/replication/walsender_private.h—WalSndCtlData(SyncRepQueue[],lsn[],sync_standbys_status필드)와SYNC_STANDBY_INIT/SYNC_STANDBY_DEFINED플래그 비트.src/include/storage/proc.h—PGPROC.waitLSN/syncRepState/syncRepLinks, 백엔드별 대기 레코드.src/include/access/xact.h—SyncCommitLevel열거형과SYNCHRONOUS_COMMIT_ON별칭.src/backend/access/transam/xact.c—RecordTransactionCommit,SyncRepWaitForLSN의 유일한 인-코어 호출 지점.
논문 및 교과서 장
섹션 제목: “논문 및 교과서 장”- Designing Data-Intensive Applications (Kleppmann 2017), ch. 5 “Replication” — 동기 대 비동기 팔로워,
FIRST 1 (...)이 구현하는 세미 동기 중간 지점. - DeCandia et al. (2007), “Dynamo.”
dbms-papers/dynamo.md—ANY k의 비교 지점으로서 정족수 복제(R + W > N). - Ongaro & Ousterhout (2014), “Raft”; Lamport, “Paxos.”
dbms-papers/raft.md,dbms-papers/paxos.md— 합의 복제, 동기 복제가 내구성이지 가용성이 아님을 설명하는 대조점. - Gilbert & Lynch (2002), CAP.
dbms-papers/cap.md— 타임아웃 없는 동기 대기의 CP 포지셔닝. - Mohan et al. (1992), “ARIES.”
dbms-papers/aries.md— 대기가 기반하는 WAL / 커밋-레코드-LSN 토대. - Database Internals (Petrov 2019), 복제와 합의 장 — 리더 기반 로그 복제의 일반적 프레이밍.
형제 문서 (교차 참조 — 메커니즘은 거기에 있으며 여기서 중복되지 않음)
섹션 제목: “형제 문서 (교차 참조 — 메커니즘은 거기에 있으며 여기서 중복되지 않음)”postgres-wal-sender-receiver.md— 스트리밍 전송과ProcessStandbyReplyMessage.SyncRepReleaseWaiters를 호출한다. write/flush/apply 피드백 위치는 거기서 유래한다. 이 문서는 그것을 소비할 뿐이다.postgres-xact.md—RecordTransactionCommit과 로컬 플러시 이후SyncRepWaitForLSN을 호출하는 커밋 경로.postgres-xlog-wal.md—XactLastRecEnd,XLogFlush, 전체 대기가 기반하는 LSN-바이트-오프셋 모델.postgres-replication-slots.md— 슬롯 기반 스탠바이 추적. 동기 스탠바이와 직교하지만 자주 함께 배포된다.postgres-overview-replication-ha.md— 복제/HA 서브시스템 중 동기 복제가 위치하는 축 수준 개요.