PostgreSQL 커뮤니티 패치 리뷰 — XLOG_SWITCH 반환값 일관성 (#6680)
DBA로서 PostgreSQL 오픈소스 커뮤니티에 처음 기여하며 마주한 #6680 패치 — WAL 구조의 기본부터 XLOG_SWITCH 반환값 일관성 문제, v1→v2 진화 과정과 메일 토론을 단계별로 따라가며 정리합니다.
이 글에 등장하는 V1/V2 패치 비교와 메일 인용은 PostgreSQL 커뮤니티 메일링 리스트 아카이브(pgsql-hackers Commitfest PG20-1, thread #6680)에서 확인할 수 있습니다.
DBA로서 PostgreSQL 오픈소스 커뮤니티에 처음 기여하며 배운 것들. WAL 구조의 기본부터 v1 → v2 패치 진화 과정까지 전 과정을 기록합니다.
들어가며
Oracle이나 MySQL과 달리 PostgreSQL은 완전히 오픈된 커뮤니티가 개발합니다. 특정 회사 소유가 아니고, 전 세계 개발자들이 메일링 리스트에서 토론하며 코드를 만들어갑니다.
이 글은 제가 그 문을 두드려본 경험을 정리한 것입니다. PostgreSQL master 브랜치에 제안된 패치(#6680) 하나를 리뷰하면서 WAL의 내부 구조부터 커뮤니티 협업 프로세스까지 꽤 많은 것을 배웠고, DBA 관점에서의 경험과 생각들을 정리해두면 개인적으로 도움이 될 것 같았습니다.
다룰 내용
- WAL 구조의 기본 (페이지, 세그먼트, LSN)
- XLOG_SWITCH가 무엇이고 왜 특별한가
- 패치 #6680이 다루는 실제 문제
- V1 → V2, 패치의 진화 과정
- 비교 실험에서 얻은 통찰
- DBA 관점에서 무엇을 배웠나
1. WAL 구조의 기본
본론에 들어가기 전에 WAL이 디스크에 어떻게 저장되는지 짚고 넘어가야 합니다.
계층 구조
PostgreSQL의 WAL은 세 단계로 쌓여 있습니다 — 세그먼트가 페이지를 담고, 페이지가 헤더 + 레코드 영역을 담는 중첩 구조입니다.
| 단위 | 크기 | 설명 |
|---|---|---|
| 세그먼트 | 16 MB | 파일 하나. pg_wal/ 디렉토리에 쌓임 |
| 페이지 | 8 KB | 세그먼트 하나에 2048개 |
| 레코드 | 가변 | 실제 변경 정보. 여러 페이지에 걸쳐 쓸 수도 있음 |
페이지 헤더 두 종류
페이지마다 시작에 헤더가 있는데, 크기가 두 종류입니다 — 세그먼트 첫 페이지에만 큰 헤더가 들어가고, 나머지는 작은 헤더가 들어갑니다.
| 헤더 타입 | 크기 | 사용처 |
|---|---|---|
SizeOfXLogLongPHD | 40 바이트 | 세그먼트의 첫 페이지만 |
SizeOfXLogShortPHD | 24 바이트 | 나머지 모든 페이지 |
긴 헤더에는 시스템 식별자, 세그먼트 크기 같은 메타 정보가 추가로 들어갑니다.
LSN (Log Sequence Number)
WAL의 모든 위치는 LSN으로 표현됩니다.
1
2
3
4
LSN: 0/03000060
│ │ └── 세그먼트 내 오프셋 (16진수)
│ └──────── 세그먼트 번호 (16진수)
└─────────── 상위 비트
0/03000060은 3번 세그먼트의 0x60(=96) 바이트 위치입니다0/04000000은 4번 세그먼트의 시작입니다- 두 LSN의 차이 = WAL 양 (바이트)
DBA로서 LSN이 중요한 이유
LSN은 PostgreSQL 내부의 모든 동기화 메커니즘의 기준점입니다.
1
2
3
4
5
6
7
8
9
10
11
-- 현재 WAL 쓰기 위치
SELECT pg_current_wal_lsn();
-- 결과: 0/03000060
-- 복제 지연 (바이트)
SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_bytes
FROM pg_stat_replication;
-- 이 LSN이 있는 WAL 파일 이름
SELECT pg_walfile_name(pg_current_wal_lsn());
-- 결과: 000000010000000000000003
LSN 계산이 틀어지면 백업·복제·아카이브가 모두 꼬입니다. 그래서 WAL 관련 버그는 DBA에게 직접 영향이 있습니다.
2. XLOG_SWITCH — 특별한 레코드
일반 레코드 vs XLOG_SWITCH
WAL 레코드에는 여러 종류가 있습니다. INSERT, UPDATE, DELETE, CHECKPOINT… 이들은 모두 일반 레코드입니다.
일반 레코드는 WAL 버퍼의 빈 공간에 차곡차곡 쌓입니다.
페이지 내부 — 헤더 다음에 레코드들이 적재되고 끝쪽에 빈 공간이 남는다
반면 XLOG_SWITCH 는 완전히 다릅니다.
크기는 일반 레코드의 헤더와 같지만, 행동이 완전히 다릅니다.
XLOG_SWITCH 가 필요한 운영 시나리오
XLOG_SWITCH는 이런 명령입니다.
“현재 세그먼트를 여기서 끝내고, 다음 세그먼트로 넘어가라.”
pg_switch_wal() 함수가 이걸 트리거합니다. 실행하면 세그먼트의 남은 공간이 전부 버려집니다. XLOG_SWITCH는 그저 “여기서 끊는다”는 마커일 뿐입니다.
BEFORE — 세그먼트 N (16 MB)
AFTER pg_switch_wal() — 세그먼트 N (16 MB)
SW = XLOG_SWITCH 레코드 (24 B). 그 뒤 13 MB 공간은 통째로 버려진다.
세그먼트 N+1 (16 MB) — 새로 시작
실제로 XLOG_SWITCH 가 쓰이는 상황은 셋입니다.
1) 백업 경계 맞출 때
1
2
3
SELECT pg_backup_start('daily_backup');
-- 백업 실행 …
SELECT pg_backup_stop(); -- 내부적으로 pg_switch_wal() 호출
백업이 끝나면 새 세그먼트에서 시작하도록 강제해서, 백업 아카이브의 경계를 명확하게 만들기 위해 XLOG_SWITCH를 호출합니다.
2) archive_timeout 도달할 때
1
2
# postgresql.conf
archive_timeout = 60min
트래픽이 적어도 60분마다 세그먼트를 강제 전환 → 아카이브가 정기적으로 흐르게 함. 내부적으로 XLOG_SWITCH 를 호출합니다.
3) 복제 / DR 상황일 때
1
2
-- Primary가 죽기 직전, Replica에 최신 상태 강제로 보내기
SELECT pg_switch_wal();
“레코드의 끝” 이 두 가지 의미를 가진다
XLOG_SWITCH 의 미묘한 점은 끝 위치가 두 가지라는 것입니다.
세그먼트 N — 두 개의 "끝"
레코드 자체 끝
세그먼트 끝
| 개념 | 위치 |
|---|---|
| 논리적 끝 | XLOG_SWITCH 의 24 B 뒤 |
| 물리적 끝 | 세그먼트의 마지막 바이트 |
일반 레코드는 논리적 끝과 물리적 끝이 같습니다. XLOG_SWITCH 만 다릅니다.
XLogInsertRecord()가 반환해야 할 값은 “논리적 끝” — 호출자가 “이 레코드 다음에 올 레코드의 시작 위치”를 올바르게 계산하려면 이 값이 정확해야 합니다. 패치 #6680이 손대는 것이 바로 이 계산 로직입니다.
3. 패치 #6680이 다루는 문제
원본 코드 구조
src/backend/access/transam/xlog.c 의 XLogInsertRecord() 함수는 모든 WAL 레코드를 쓰는 공통 관문입니다. 일반 레코드든 XLOG_SWITCH 든 이 함수를 거칩니다.
하지만 내부에서 두 경로가 다르게 처리됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 일반 레코드 경로 (단순) */
EndPos = XLogBytePosToEndRecPtr(...); // 헬퍼 함수 하나로 끝
/* XLOG_SWITCH 경로 (복잡, 12줄) */
if (inserted)
{
EndPos = StartPos + SizeOfXLogRecord;
if (StartPos / XLOG_BLCKSZ != EndPos / XLOG_BLCKSZ)
{
uint64 offset = XLogSegmentOffset(EndPos, wal_segment_size);
if (offset == EndPos % XLOG_BLCKSZ)
EndPos += SizeOfXLogLongPHD; // 세그먼트 첫 페이지면 긴 헤더
else
EndPos += SizeOfXLogShortPHD; // 아니면 짧은 헤더
}
}
저자의 문제 제기
패치 저자([email protected])는 이 불일치를 지적했습니다.
“같은 함수 안에서 두 경로가 이렇게 다를 필요가 있나?”
합리적인 문제 제기입니다. 일관성은 유지보수성과 직접 연관됩니다.
왜 XLOG_SWITCH 만 복잡한가
24 B 레코드가 페이지 경계를 넘어갈 수 있기 때문입니다. 넘어가면 다음 페이지의 헤더 크기(24 또는 40 B)를 EndPos 에 더해야 합니다.
Case 1 — 같은 페이지에 완전히 들어감
EndPos = SW 끝 (페이지 경계 보정 없음)
Case 2 — 페이지 경계를 넘어감
EndPos 에 다음 페이지의 헤더 크기(24 또는 40 B)를 더해야 한다
이 “페이지 경계 보정” 로직이 일반 레코드와 XLOG_SWITCH 가 다른 이유입니다.
4. V1 → V2, 패치의 진화
V1: “헬퍼 함수를 재사용하자”
저자의 첫 제안(V1)은 심플했습니다. 12줄을 2줄로 줄이고 일반 레코드와 같은 헬퍼 함수를 공유하는 접근.
장점은 명확했습니다 — 코드 간결, 패턴 통일. 다만 한 가지가 걸렸습니다. 원본에는 없던 MAXALIGN() 이 추가되었는데 저자가 따로 설명을 달지 않았습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (inserted)
{
- EndPos = StartPos + SizeOfXLogRecord;
- if (StartPos / XLOG_BLCKSZ != EndPos / XLOG_BLCKSZ)
- {
- uint64 offset = XLogSegmentOffset(EndPos, wal_segment_size);
- if (offset == EndPos % XLOG_BLCKSZ)
- EndPos += SizeOfXLogLongPHD;
- else
- EndPos += SizeOfXLogShortPHD;
- }
+ EndPos = XLogBytePosToEndRecPtr(XLogRecPtrToBytePos(StartPos) +
+ MAXALIGN(SizeOfXLogRecord));
}
제가 던진 질문
DBA로서 코드를 짤 수는 없지만, “운영에서 문제가 될 수 있는 지점” 은 짚을 수 있습니다. 질문은 단순했습니다.
“이
MAXALIGN()은 실제 버그 수정인가요, 방어적 no-op 인가요, 아니면 동작 변화라 commit 메시지에 별도 언급이 필요한가요?”
패치의 의도를 저자가 명확히 밝혀줘야 나중에 누가 비슷한 코드를 고칠 때 혼란이 없습니다. 이게 리뷰어가 할 수 있는 “작지만 중요한 일” 입니다.
V2: 저자가 접근 자체를 재고
그날 저녁 저자의 답장이 왔습니다. “MAXALIGN 은 ReserveXLogSwitch() 와 일관성 때문이고 실제 값은 안 변한다. 더 효율적인 V2 를 첨부한다.”
놀랍게도 V2 는 V1 의 2줄 헬퍼 방식을 버리고 원본 구조로 회귀했습니다. 대신 두 가지가 추가되었습니다.
- 페이지 경계 엣지 케이스 명시 처리 — EndPos 가 정확히 페이지 경계에 떨어지는 상황 (다음 페이지를 아직 안 건드렸으니 헤더 크기를 더하면 안 됨)
- 주석으로 의도 설명 — 왜 이 조건이 필요한지
XLogBytePosToEndRecPtr()와의 연관성 명시
V1 이 simplify 였다면 V2 는 “원본 유지 + 방어적 보완” — 접근 방향이 완전히 달라졌습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (inserted)
{
- EndPos = StartPos + SizeOfXLogRecord;
- if (StartPos / XLOG_BLCKSZ != EndPos / XLOG_BLCKSZ)
+ EndPos = StartPos + MAXALIGN(SizeOfXLogRecord);
+
+ /*
+ * If the XLOG_SWITCH record crosses a page boundary and actually has
+ * a part on the next page, we need to consider the page header. This
+ * is consistent with XLogBytePosToEndRecPtr().
+ */
+ if (StartPos / XLOG_BLCKSZ != EndPos / XLOG_BLCKSZ &&
+ EndPos % XLOG_BLCKSZ != 0)
{
uint64 offset = XLogSegmentOffset(EndPos, wal_segment_size);
...
세 버전 비교
| 항목 | 원본 master | V1 | V2 |
|---|---|---|---|
| 코드 스타일 | 수동 로직 | 2줄 헬퍼 | 수동 로직 |
| 페이지 경계 엣지 케이스 | ❌ 미처리 | ❌ (헬퍼가 암묵 처리) | ✅ 명시 처리 |
| 주석으로 의도 설명 | ❌ | ❌ | ✅ |
| 함수 호출 오버헤드 | 없음 | 있음 | 없음 |
5. 비교 실험에서 얻은 통찰
실제 버그일까, 방어적 개선일까
V2 테스트를 돌리며 궁금해졌습니다. “이 수정이 진짜 버그를 고치는 건가?”
확인 방법은 단순합니다. V2 적용본과 원본 master 에서 같은 시나리오(pg_switch_wal() 반복 호출)를 돌려 LSN 동작을 비교하는 거죠.
결과는 의외였습니다
두 버전이 완전히 동일하게 동작했습니다. pg_switch_wal() 을 세그먼트 경계에서 반복 호출해도 멱등(idempotent)했고, 관찰 가능한 버그 증상이 없었습니다.
왜 원본도 멀쩡한가? 호출 체인을 따라가 보았습니다.
1
2
3
4
5
pg_switch_wal()
↓
ReserveXLogSwitch() ← 여기서 이미 경계 체크
↓
경계에 있으면 → inserted = false → 문제 경로 진입 안 됨
상위 호출자가 이미 가드하고 있어서, XLogInsertRecord() 의 잠재적 버그 경로가 실제로는 도달 불가능했던 겁니다.
그럼 V2 는 의미가 없는가?
그렇지 않습니다. “지금 잘 동작하는 것”과 “앞으로도 안전한 것”은 다릅니다.
V2 는 관찰 가능한 버그를 수정하진 않지만:
- 상위 호출자의 계약에 의존하지 않고 함수 내부에서 스스로 불변 조건을 보장
- 주석으로 의도를 명시해서 코드 가치를 높임
- 나중에
XLogInsertRecord()를 다른 경로에서 직접 호출해도 안전
이게 방어적 프로그래밍(defensive-in-depth) 의 전형입니다.
Follow-up 리뷰 메일
이 통찰을 바탕으로 저자와 커미터에게 follow-up 을 보냈습니다.
“V2 의 새 가드는 현재 master 의 관찰 가능한 버그를 수정하진 않습니다.
ReserveXLogSwitch()가 상위에서 이미 막고 있기 때문입니다. 하지만 이 가드는 invariant 를XLogInsertRecord()자체에 국한시키고, 주석으로 의도를 명시함으로써 방어적 개선으로서 가치가 있습니다.”
6. DBA 관점에서 배운 것들
1) “잘 동작하는 것” 과 “안전한 것” 은 다르다
원본 코드는 잘 동작합니다. 하지만 안전하지는 않습니다. 상위 호출자가 가드를 풀거나, 다른 경로에서 XLogInsertRecord() 를 직접 부르는 순간 잠재 결함이 드러납니다.
2) WAL 관련 버그가 DBA 에게 미치는 영향
WAL 의 EndPos 계산이 단 1 바이트라도 어긋나면, archive·backup·replication 의 동기화 기준점이 동시에 비뚤어집니다. 한 곳을 잘못 짚는 게 아니라 운영 면 전체가 무너지는 종류의 버그입니다.
XLOG_SWITCH 의 EndPos 계산이 틀어지면 이론적으로 어떤 일이 벌어질까요?
| 서비스 | 영향 |
|---|---|
archive_command | 잘못된 LSN 을 받아 아카이브 타이밍 꼬임 |
pg_basebackup | 백업 시작/끝 LSN 불일치 가능 |
| Streaming replication | Replica 가 Primary 의 WAL 위치를 오해 |
pg_waldump | WAL 분석 도구가 잘못된 위치를 파싱 |
| Logical replication | 슬롯이 잘못된 LSN 을 따라감 |
지금 이 패치는 관찰 가능한 버그를 수정하지 않지만, 이런 엣지 케이스들을 방어적으로 처리하는 것 자체가 운영 안정성에 큰 역할을 한다고 생각합니다.
3) 질문이 패치를 바꾼다
리뷰어가 던진 한 마디 질문이 패치 접근 자체를 바꿀 수 있습니다. V1 에서 제가 던진 단순한 의문(“MAXALIGN 이 뭐 하는 거죠?”)이 저자로 하여금 재고하게 만들었고, 그 결과가 V2 의 방향 전환이었습니다.
코드를 고친 건 아니지만, 질문 하나가 패치 품질에 영향을 줬습니다. DBA 로서 C 코드를 못 써도 기여할 수 있는 지점이 분명히 있다는 걸 체감했어요.
7. 마치며
패치 하나를 리뷰하면서 WAL 구조, XLOG_SWITCH 의 특수성, 저자와의 기술 대화, 비교 실험까지 — 이 모든 게 하루이틀 사이에 일어난 일입니다.
이 경험이 특별했던 건 “내가 운영하는 소프트웨어를 만들어가는 과정에 실제로 참여했다” 는 경험입니다. 사용자에서 기여자로 한 발 넘어간 느낌.
잘 동작한다고 안전한 것은 아니다 — 상위 호출자의 가드에 의존하지 않고 함수 내부에서 불변 조건을 보장하는 게 방어적 프로그래밍의 본질이고, DBA 가 코드 리뷰에서 가장 잘 짚을 수 있는 지점이기도 하다고 생각합니다.