728x90
※ PostgreSQL: Disk I/O and WAL Flow.
안녕하세요. 듀스트림입니다.
기능적인 요소도 중요하지만 엔진 파인 튜닝을 하기 위해서는 내부에서 어떻게 흘러가는지 아는 것도 중요합니다.
오늘의 포스팅은 PostgreSQL에서 데이터를 어떻게 처리하는지에 대한 내용입니다.
PostgreSQL의 디스크 I/O 및 트랜잭션 로그 처리 구조는 shared_buffers, WAL buffers, OS page cache 그리고 디스크 계층 간의 정확한 흐름과 역할 분담을 기반으로 구성됩니다.
1. PostgreSQL의 트랜잭션 처리 및 WAL 쓰기 흐름
1. 클라이언트 SQL 요청 처리
- 클라이언트로부터 SQL 쿼리가 PostgreSQL 백엔드 프로세스로 전달됩니다.
2. 데이터 페이지 접근 (shared_buffers)
- 백엔드는 쿼리 실행을 위해 테이블/인덱스 블록을 shared_buffers에 로딩하고, 필요한 경우 이를 수정합니다.
3. WAL 레코드 생성 및 WAL Buffer 기록
- 변경된 트랜잭션 내용을 Write-Ahead Logging(WAL) 포맷으로 변환해 WAL Buffer에 기록합니다.
- 관련 소스 코드: XLogInsert()
4. WAL Buffer → 파일 시스템 기록 (write)
- WAL Writer 또는 백엔드 프로세스는 write() 시스템 콜을 통해 WAL 데이터를 pg_wal/ 디렉토리의 WAL 파일로 씁니다. 이때는 실제 디스크가 아니라 OS 페이지 캐시에 저장됩니다.
- 관련 소스 코드: XLogWrite()
- 공식 문서: WAL Internals
- 관련 소스 코드: XLogWrite()
5. 디스크 동기화 (flush)
- XLogFlush()가 fsync() 또는 fdatasync()를 호출해 WAL이 디스크에 안전하게 저장(동기화)됩니다. (durability 보장을 위한 단계)
- 관련 소스 코드: XLogFlush()
- 조건: synchronous_commit = on 또는 remote_write일 경우 WAL이 디스크에 flush(동기화)됩니다.
(remote_write는 streaming replication 환경에서만 의미 있으며, standalone 환경에서도 기본값인 on 상태에서는 WAL flush가 항상 발생)
- 관련 소스 코드: XLogFlush()
6. 커밋 확정 및 응답
- WAL flush 완료 후 클라이언트에 커밋 완료 응답이 전송됩니다.
2. 복구 및 스트리밍 레플리케이션 시의 WAL Replay 흐름
PostgreSQL은 시스템 재시작(Crash Recovery), PITR 또는 Streaming-Replication 중 Standby에서 다음과 같은 방식으로 WAL을 처리합니다.
1.WAL 파일 읽기
- PostgreSQL은 Crash Recovery, PITR, Streaming-Replication 시 pg_wal/(pg_xlog/) 또는 아카이브에서 WAL 파일을 읽어와 복구를 시작합니다.
- 관련 소스 코드: XLogReadRecord()
2. WAL record 분석 및 적용 대상 확인
- WAL 레코드는 각 타입(XLOG_HEAP_INSERT, XLOG_BTREE_SPLIT 등)에 따라 데이터 파일, FSM, VM, 인덱스 등에 적용됩니다.
- StartupXLOG() → xlog_redo() → 각 작업별 Redo 루틴 호출
| WAL 레코드 타입 | 적용 대상 |
| XLOG_HEAP_INSERT | Relation 파일 |
| XLOG_FSM_CLEANUP | FSM(Free Space Map) |
| XLOG_BTREE_SPLIT | 인덱스(B-Tree) |
| XLOG_VACUUM | VM(Visibility Map) |
3. WAL Redo (Apply)
- 복구 모드(Crash Recovery, PITR, Standby)에서는 WAL 레코드가 데이터 파일에 직접 적용되며, shared_buffers를 거치지 않습니다. (이는 복구 성능과 일관성 보장을 위한 설계입니다.)
- WAL Redo 동작 특성
- 직접 디스크 쓰기: 버퍼 캐시를 우회하여 데이터 파일(물리적 저장소)에 직접 적용됩니다.
- 원자성 보장: 각 WAL 레코드는 해당 데이터 블록의 LSN과 비교하여 필요할 때만 적용되고, 적용 후 LSN이 갱신되어 중복 적용을 방지합니다.
- synchronous_commit 설정에 따라 커밋된 트랜잭션의 WAL이 디스크에 안전하게 기록된 후 복구가 진행되므로, 복구 시점까지의 데이터 일관성이 보장됩니다.
4. 디스크 동기화 및 checkpoint 갱신
- 복구가 완료되면 PostgreSQL은 새로운 체크포인트를 생성하고, pg_control 파일을 갱신하여 복구가 끝났음을 표시합니다.
- 관련 소스 코드: CreateCheckPoint()
※ synchronous_commit = remote_apply일 경우 Standby에서 Redo(apply)가 완료되어야 primary에서 커밋이 완료됩니다.
+ Flush 메커니즘
- Normal 모드
| 상황 | 처리 방식 | 관련 함수 | 설명 |
| 버퍼를 "Dirty" 상태로 표시 | shared_buffers 내 마킹 | MarkBufferDirty() | 실제 쓰기 트리거 X LRU 관리 대상 |
| 주기적 플러시 | shared_buffers → OS 캐시 | FlushBuffer() | 백그라운드 writer/checkpointer 주기적 실행 |
| Checkpoint | 버퍼 → OS 캐시 (비동기) | BufferSync() → FlushBuffer() | 모든 더티 버퍼 일괄 처리 |
| OS 캐시 → 디스크 (동기) | smgrimmedsync() | 파일 단위 fsync() 수행 |
- Recovery 모드
| 상황 | 처리 방식 | 관련 함수 | 설명 |
| WAL Redo | 직접 디스크 쓰기 | smgrwrite() → mdwritev() | 버퍼 풀 무시 벡터 I/O로 다중 블록 처리 |
| 체크포인트 | 물리적 동기화 생략 | - | 복구 완료 시 최종 sync만 수행 |
++호출 계층
- 일반 쓰기 경로
MarkBufferDirty()
→ (체크포인트 발생 시)
BufferSync()
→ FlushBuffer()
→ smgrwrite()
→ mdwrite() # 단일 블록
→ pg_fsync() # 필요 시
- Recovery 모드 경로
StartupXLOG()
→ XLogReadRecord()
→ heap_redo()/btree_redo()
→ smgrwrite()
→ mdwritev() # 다중 블록
→ pg_fsync() # 최종 sync
+++ Normal vs Recovery 모드 차이
| 항목 | 일반 모드 | 복구 모드 |
| 버퍼 풀 사용 | ✅ | ❌ |
| I/O 단위 | 페이지(8KB) | 블록 그룹(벡터 I/O) |
| 동기화 주체 | checkpointer | Startup 프로세스 |
3. OS Page Cache와 PostgreSQL의 역할 구분
| 계층 | 내용 |
| Shared Buffers | PostgreSQL 내부의 블록 캐시(8KB 페이지 단위), 데이터 페이지 I/O의 첫 진입점, LRU 알고리즘으로 페이지 관리 |
| WAL Buffers | 트랜잭션 변경을 로그 형식으로 기록하는 내부 버퍼 (Default: 16MB), 주기적으로 OS Page Cache에 기록 |
| OS Page Cache | PostgreSQL이 write() 호출로 암묵적 사용(직접 제어 x), fsync() 없이도 커널이 주기적으로 디스크에 동기화 |
| Disk | fsync() 호출 시 데이터가 최종 반영되는 영구 저장소 |
- PostgreSQL은 OS page cache를 직접 제어하진 않으며, write()를 통해 암묵적으로 커널 캐시에 기록하고 fsync()로 이를 디스크에 반영합니다.
4. 디스크 I/O 및 WAL 관련 튜닝 가이드
| 파라미터 | 설명 | 권장값/튜닝 포인트 |
| shared_buffers | PostgreSQL이 자체적으로 관리하는 메모리 캐시 | 전체 RAM의 25~40%. 너무 크면 OS 캐시 영역 부족 위험 |
| effective_cache_size | Planner에게 OS 캐시 추정치 제공 (쿼리 비용 계산에 영향) | 전체 RAM의 70~80% 수준으로 설정 |
| wal_writer_delay | WAL Writer가 flush 주기를 얼마나 지연할지 | WAL 발생량 많다면 낮추는 것도 고려 (Default: 200ms) |
| synchronous_commit | 커밋이 flush를 기다릴지 여부 (on, remote_write, remote_apply) | 장애 내구성 vs 성능 trade-off |
| checkpoint_timeout | checkpoint 간격 제어 | 너무 짧으면 디스크 쓰기 증가, 너무 길면 crash recovery 시간 증가 (Default: 5min) |
| checkpoint_completion_target | checkpoint 분산률 제어 (0~1) | 0.7~0.9 설정 시 I/O spike 완화 가능 (Default: 0.5) |
| bgwriter_lru_maxpages | bgwriter가 dirty page를 얼마나 flush할지 | WAL pressure가 높을 경우 증가 고려 |
| wal_compression | WAL 크기 줄이기 위한 압축 | on 설정 시 I/O 및 archive 저장 공간 절감 가능 |
| wal_buffers | WAL 버퍼 크기 | 특별한 튜닝은 대량 트랜잭션 발생 시 고려 |
오늘은 여기까지~
728x90
'PostgreSQL' 카테고리의 다른 글
| PostgreSQL: Lock (0) | 2025.06.24 |
|---|---|
| PostgreSQL: credcheck (0) | 2025.06.17 |
| PostgreSQL: Streaming Replication (0) | 2025.06.10 |
| PostgreSQL: ROLE (4) | 2025.06.09 |
| PostgreSQL: 병렬 INSERT가 되지 않는 이유 (1) | 2025.06.08 |