앞서 PostgreSQL의 Shared Buffer와 WAL Buffer를 중심으로 메모리 영역에서 데이터가 어떻게 처리되고 어떤 Wait Event가 발생되는지 알아보았습니다.
이제 이러한 메모리 상의 변경이 어떠한 방식으로 디스크로 적용되는지 살펴보겠습니다.
PostgreSQL에서 디스크 I/O는 단순한 저장 동작이 아니라 데이터 무결성, 복구, 트랜잭션 처리 방식과 밀접하게 연결된 핵심 동작입니다.
이러한 이유로, I/O와 관련된 다양한 타입의 File(WAL File, Data File, Checkpoint, Temp File)의 목적과 Wait Event에 대해 차례대로 살펴보도록 하며, I/O의 첫 번째 글로 WAL File 및 Wait Event에 대해서 알아보겠습니다.
WAL File
WAL File은 데이터 변경 작업에 대한 로그가 저장되는 파일로, 장애 발생 시 데이터를 복구하는데 사용됩니다.
WAL File에 대한 I/O 작업은 WAL Buffer에 쌓인 WAL Record를 디스크에 저장하거나, 필요 시 디스크에서 읽어오는 형태로 동작합니다.
Write 작업은 WAL File에 WAL Record를 저장하는 과정으로 데이터 손실을 방지하기 위해 주기적으로 수행됩니다. 반면 Read 작업은 Crash Recovery나 Replication과 같은 특수한 상황에서 수행됩니다.
본 주제에서는 일반적인 운영 환경에서 발생하는 WAL Record 저장 과정을 중심으로 한 WAL Write에 대해서 살펴보겠습니다.
WAL Read 작업과 관련된 WAL File I/O는 이후 이어지는 “PWI - Replication” 주제에서 다루도록 하겠습니다.
WAL File과 프로세스
WAL File에 WAL Record를 저장하는 작업은 WAL Writer에 의해 주기적으로 수행되며, 필요에 따라 Backend Process에 의해서도 수행됩니다.

WAL Writer Process
WAL Writer는 주기적으로 WAL Buffer에 저장되어 있는 WAL Record를 WAL File로 Write하는 Background 프로세스입니다.
해당 시점까지 WAL Buffer에 누적된 WAL Record를 한번에 WAL File로 Write하여 WAL Buffer 공간을 재사용 가능하도록 확보하며, 이로 인해 Backend Process가 Commit 시 직접 WAL File에 Write해야 하는 WAL Record 양을 줄여 I/O 부하를 분산시키는 역할을 합니다.
Checkpoint 발생 시에는 Redo 위치를 명확히 확인하기 위해 Checkpoint WAL Record가 생성됩니다. 이 과정에서 Checkpointer는 WAL Writer가 WAL Record를 WAL File에 기록하도록 Write 작업을 요청(트리거)합니다.
Backend Process
다음과 같은 상황에서는 Backend Process가 WAL Record를 직접 WAL File에 WAL Record를 저장합니다.
- 트랜잭션 Commit
synchronous_commit=
on으로 설정된 환경에서는 Commit 시점에 WAL Record의 Flush(fsync)가 반드시 보장되어야 합니다. 따라서 Backend Process는 Commit 완료 응답을 반환하기 전에, 해당 트랜잭션의 WAL Record를 WAL File에 직접 Write 및 Flush합니다.- WAL Buffer 공간 부족
WAL Record 생성 속도가 빠르고, 재사용 가능한 WAL Buffer 공간이 부족한 경우 Backend Process는 WAL Record를 직접 WAL File에 Write하여 WAL Buffer 공간을 확보합니다.
- CHECKPOINT Command 수행
CHECKPOINT Command를 실행하면 Backend Process가 Checkpointer에 Checkpoint 수행을 요청하여 즉시 Checkpoint가 수행되도록 합니다. Checkpoint 시점에는 Checkpoint WAL Record까지 WAL File에 저장되어 있어야 하므로, 이를 만족하기 위해 WAL Write 및 Flush가 수행됩니다.
WAL Record가 WAL File에 저장되는 과정
WAL Buffer에 저장되어 있는 WAL Record가 WAL File에 저장되는 과정을 살펴보겠습니다. 관련 파라미터 설정에 따라 동작에 차이가 존재하므로, 다음과 같이 값이 설정되어 있다는 가정 하에 설명하도록 하겠습니다.
full_page_writes=on, synchronous_commit=on
[1] CHECKPOINT Command 수행

① CHECKPOINT Command 수행 즉시 CHECKPOINT에 대한 정보가 WAL Record로 생성되는데 이를 Checkpoint WAL Record라고 합니다.
② CHECKPOINT 수행으로 REDO POINT 이전의 변경 사항은 모두 Data File에 반영되어야 합니다. WAL 규칙에 따라 CHECKPOINT WAL Record를 포함한 WAL Record까지 WAL File에 저장됩니다.
[2] INSERT Query 수행

③ 트랜잭션 시작 후 INSERT Query를 수행하면, 대상 Data Page를 Data File로부터 로드하여 Shared Buffer에 적재합니다.
④ 로드된 Data Page에 ‘a’에 대한 Tuple을 생성합니다. 그리고 작업 내용에 대한 WAL Record가 WAL Buffer에 생성됩니다. (이때, WAL Record가 저장된 위치는 LSN_1입니다.)
⑤ Data Page에 마지막으로 적용된 WAL Record의 위치를 가리키는 LSN 정보를 LSN_1로 갱신합니다. (LSN_0 → LSN_1)
[3] COMMIT 수행

⑥ synchronous_commit=
on로 설정된 환경에서 COMMIT을 수행했으므로, 이 작업에 대한 WAL Record인 Commit WAL Record를 생성합니다.⑦ Commit 시점에 WAL Record의 Flush(fsync)가 반드시 보장되어야 하므로 트랜잭션 시작 후 생성된 모든 WAL Record가 WAL File에 저장됩니다. 이때 WAL File 내 WAL Record는 위치는 WAL Buffer와 동일하게 LSN_1입니다.
[4] INSERT Query 수행

⑧ 다시 한번 INSERT Query를 수행합니다. 필요한 Data Page가 Shared Buffer에 존재하므로 바로 해당 Data Page에 ‘b’에 대한 Tuple을 생성하고, 이 작업에 대한 WAL Record를 생성합니다.
⑨ Data Page에 마지막으로 적용된 WAL Record가 변경되었으므로, WAL Record의 위치를 다시 확인하여 Data Page의 LSN 정보를 갱신합니다. (LSN_1 → LSN_3)
[5] COMMIT 수행

⑩ COMMIT을 수행하면, “[3] COMMIT 수행” 과정과 동일하게 Commit WAL Record를 생성하고, 지금까지 트랜잭션에서 생성된 모든 WAL Record를 WAL File에 저장합니다.
Wait Event
WAL File에 WAL Record를 저장하는 과정에서 I/O가 증가하면 IO:WALWrite와 IO:WALSync 두 Wait Event가 발생할 수 있습니다.
WAL Record 저장 과정은 크게 두 단계로 나뉩니다.
먼저 WAL Record를 WAL File에 Write하고, 이후 필요에 따라 Flush 작업을 수행하여 디스크에 영구적으로 반영합니다.
- Write WAL Record를 WAL File에 저장하되, 실제로는 OS Cache 영역에만 저장됩니다. 이 시점의 데이터는 아직 OS Cache에만 존재하며 디스크에 반영되지 않았습니다.
- Flush fsync() 호출을 통해 OS Cache 영역에 있는 WAL 데이터를 디스크로 동기화합니다. Flush가 완료되면 WAL Record는 디스크에 안전하게 저장되며, 장애 복구 등에 사용 가능합니다.
WAL Record를 저장할 때 Write와 Flush를 매번 연속으로 수행하면 fsync() 호출이 빈번해져 디스크 I/O 부하가 급격히 증가할 수 있습니다. 특히 Flush는 디스크 동기화 작업까지를 동반하므로 Write 작업에 비해 그 비용이 큽니다.
이러한 이유로 Write와 Flush 작업을 분리하여 처리하며, 상대적으로 비용이 낮은 Write 작업은 자주 수행하고 Flush 작업은 지연하거나 여러 WAL Record를 묶어 배치 방식으로 처리함으로써 디스크 I/O 발생을 분산시킵니다.
IO:WALWrite
IO:WALWrite는 WAL File에 Write하는 작업이 완료되기를 대기할 때 발생하는 Wait Event입니다.
WAL Record가 많이 쌓일수록 IO:WALWrite 발생 빈도가 증가합니다.
IO:WALSync
IO:WALSync는 WAL File에 Write된 WAL Record가 영구적으로 디스크에 반영되기를(디스크 동기화) 대기할 때 발생하는 Wait Event입니다. Write 작업 이후 수행되는 Flush 작업이 집중될 때 주로 발생합니다.
두 Wait Event는 동일하게 WAL File에 WAL Record 저장하는 과정에서 발생하지만, Write 단계의 Wait인지 Flush 단계의 Wait인지를 구분해줍니다.
주요 발생 원인
- WAL Buffer 크기
WAL Buffer 공간이 충분하지 않은 경우, WAL Writer가 주기적으로 잘 동작하더라도 재사용 가능한 WAL Buffer 공간을 확보하기 위해 WAL File로의 Write 작업이 높은 빈도로 발생할 수 있습니다.
이로 인해 WAL Write 작업 횟수가 증가하고, 그 결과 IO:WALWrite Wait Event 발생 빈도 역시 함께 증가합니다.
- DML 작업 부하
대량의 DML 작업이 발생하면 생성되는 WAL Record 양이 급격히 증가합니다.
WAL Record가 늘어날수록 WAL File에 기록해야 할 데이터도 많아지며, 이에 따라 WAL File I/O 부하가 증가합니다. 그 결과, WAL Write 과정에서 대기 시간이 발생하며 IO:WALWrite 발생 가능성도 높아집니다.
- 빈번한 Checkpoint
Checkpoint가 수행되면 Shared Buffer에 존재하는 Dirty Page를 Data File에 저장하고, 동시에 해당 시점을 설명하는 WAL Record를 WAL File에 기록합니다. 이 과정은 데이터 무결성과 복구 시점을 보장하는데 필수적이지만, Data File과 WAL File에 모두 I/O를 유발합니다.
특히 full_page_writes=on으로 설정된 환경에서는 Dirty Page의 전체 이미지를 WAL Record에 저장해야 하므로, WAL Record 크기가 커지고 더 빠른 속도로 WAL Buffer를 채우게 됩니다.
그 결과 WAL Buffer 공간을 확보하기 위한 WAL Write 작업이 증가하며, IO:WALWrite 발생 빈도도 함께 증가하게 됩니다.
- 잦은 Commit 실행
트랜잭션이 Commit될 때마다 해당 트랜잭션의 WAL Record는 WAL File로 Write됩니다. 따라서 Commit 수행 빈도가 높을수록 WAL Write 작업 역시 증가하며, 이는 IO:WALWrite 발생을 증가시키는 주요 원인입니다.
특히 synchronous_commit=
on으로 설정된 경우, Commit 시점에 WAL Record가 디스크에 안전하게 저장되었음을 보장하기 위해 Flush 작업까지 연쇄적으로 수행하므로 IO:WALSync Wait Event의 주된 원인이 됩니다.해결 방안
- 대량의 DML 작업이 빈번하게 발생하는 환경에서는 생성되는 WAL Record 양이 많아질 수 밖에 없습니다. 이 경우 WAL Buffer 크기를 확장하면 WAL Buffer 공간이 빠르게 소진되는 상황을 완화할 수 있으며, 그 결과 WAL File에 대한 Write 작업 빈도를 줄여 IO:WALWrite 발생을 줄일 수 있습니다.
- WAL Record는 설정된 주기에 따라 WAL Writer 프로세스에 의해 WAL File에 저장됩니다.
WAL Writer 동작 주기가 길수록 더 많은 WAL Record를 누적한 뒤 한번에 처리하게 되며, 반대로 주기가 짧을수록 WAL Write가 더 빈번하게 수행됩니다.
따라서 WAL Record 생성량과 시스템 I/O 특성을 고려하여, I/O 작업이 과도하게 집중되지 않도록 WAL Writer 동작 주기를 적절하게 조정하는 것이 필요합니다. 이를 통해 WAL File I/O를 분산시킴으로써 IO:WALWrite 및 IO:WALSync 발생 빈도도 줄일 수 있습니다.
📢 wal_writer_delay(Default, 200)
WAL Writer가 WAL을 Flush하는 주기를 시간 단위로 지정하는 파라미터입니다.
마지막 WAL Flush 이후 생성된 WAL의 양이 wal_writer_flush_after보다 적은 경우, OS Cache에만 기록되고(Write), 디스크에는 즉시 반영되지 않습니다.
WAL Writer가 한번의 주기 동안 wal_writer_flush_after보다 많은 WAL Record를 처리한 경우,
fsync()를 호출하여 Flush 작업을 진행합니다. 따라서 wal_writer_flush_after 값을 조정하면 WAL Write와 Flush 작업 간의 간격을 제어할 수 있으며, Flush로 인한 디스크 I/O 부하를 개선하는데 도움이 됩니다.- Checkpoint가 수행되면 WAL Record를 WAL File에 기록해야 합니다. 따라서 Checkpoint 수행 시점을 최적화를 통해 IO:WALWrite 발생을 줄일 수 있습니다.
Checkpoint는 “PWI - I/O > Checkpoint”에서 설명합니다.
wal_writer_delay에 따른 WAL File I/O 발생 비교
테스트를 통해 WAL Writer 동작 주기(wal_writer_delay)의 변화에 따라 WAL File I/O 발생량이 어떻게 달라지는지 확인해보겠습니다.
WAL Buffer 공간 부족을 원인으로 발생하는 WAL Write 및 Sync의 영향을 최대한 배제하기 위해 WAL Buffer는 충분하게 할당하고 Dirty Data Page의 전체 이미지가 저장되어 WAL Record 크기가 커지는 것을 방지하기 위해 Full Page Writes는 비활성화하였습니다.
wal_buffers=1MB, full_page_writes=off
wal_writer_delay를 10, 200, 4000으로 각각 설정한 후, WAL Record 생성을 위한 DML(
INSERT/UPDATE/DELETE)을 순차적으로 수행하면서 테스트를 진행하였습니다.[CASE1] wal_writer_delay= 10
- 총 47개의 WAL Record가 생성되었으며, WAL Buffer에서 OS Cache로 Write된 횟수는
44786이며, OS Cache에서 디스크로 Flush된 횟수는44758입니다.
wal_writer_delay=
10으로 설정된 환경에서는 WAL Writer가 매우 짧은 주기로 동작하므로, 누적되는 WAL Record 양이 많지 않은 상태에서도 Write와 Flush가 거의 매 주기마다 수행됩니다. 그 결과 wal_write와 wal_sync가 거의 동일한 수준으로 나타납니다.- WAL Write 작업에 소요된 시간은
2808.199ms, Flush 작업에 소요된 시간은36024.807ms입니다.
WAL 작업은 주로 OS Cache를 대상으로 수행되기 때문에 상대적으로 빠르게 처리되었으나, Flush 작업은
fsync() 호출을 통해 실제 디스크 동기화를 수행하므로 Write 대비 훨씬 많은 시간이 소요되었습니다. 이는 Write와 Flush 작업 빈도는 유사하지만, 시스템 성능에 미치는 영향은 Flush 작업에서 대부분 발생한다는 것을 알 수 있습니다.[CASE2] wal_writer_delay= 200
- 동일한 DML Query를 수행하여 테스트를 진행하였으므로, 생성된 WAL Record 수는 CASE1과 유사한 수준으로 나타납니다.
- 반면 WAL Writer 동작 주기를 CASE1보다 길게 설정한 결과, WAL Record가 더 많이 누적된 후 한번에 처리되면서 WAL Write 및 Flush 작업 횟수가 크게 감소한 것을 확인할 수 있습니다. (wal_write: 44786→
2532, wal_sync: 44758→2501)
- 작업 횟수 감소에 따라, 작업 소요시간 역시 현저하게 줄어들었습니다. (wal_write_time: 2808.199→
929.038, wal_sync_time: 36024.807→8830.022)
[CASE3] wal_writer_delay= 4000
- wal_writer_delay 값을 CASE2 대비 20배 증가시킨
4000으로 설정한 후 동일한 DML 쿼리를 수행한 결과, 이번 케이스에서도 생성된 WAL Record 수는 이전 케이스들과 유사한 수준으로 유지되었습니다.
- 반면, 유사한 양의 WAL Record를 WAL File에 저장하는 과정에서 발생한 WAL Write 및 Flush 작업 횟수는 더욱 크게 줄어든 것을 확인할 수 있습니다. (wal_write: 44786→2532→
175, wal_sync: 44758→2501→155) 이는 WAL Writer 동작 주기가 길어지면서, 여러 개의 WAL Record를 한번에 묶어 배치 방식으로 처리했기 때문으로 해석할 수 있습니다.
- WAL Write와 Flush 작업 횟수가 감소함에 따라, 각 작업에 소요된 누적 시간 역시 CASE1부터 CASE3로 갈수록 지속적으로 감소했습니다. (wal_write_time: 2808.199→929.038→
56.18, wal_sync_time: 36024.807→8830.022→1873.296)
그 결과 CASE3는 WAL Write 및 Flush 작업 횟수와 소요 시간 모두에서 가장 낮은 수치를 기록했습니다.
Wait Event 발생 비교
이번에는 wal_writer_delay 값을 10ms, 200ms, 4000ms으로 설정했을 때, WAL File I/O와 관련된 Wait Event 발생 이력을 pg_wait_sampling_profile을 통해 비교 분석해보겠습니다.
- wal_writer_delay=
10인 경우, WAL Writer가 매우 짧은 주기로 동작하면서 Write 및 Flush 작업을 아주 빠르게, 짧은 시간 단위로 반복 수행합니다. 그 결과 IO:WALWrite 및 IO:WALSync 대기 시간이 매우 짧아 샘플링 시점에 포착될 확률이 낮아집니다.
실제 두 Wait Event 모두 기록되지 않은 것으로 나타나지만, 이는 I/O가 없었다는 의미가 아닌 대기가 충분히 길지 않았다는 의미에 가깝습니다.
- wal_writer_delay를 큰 값으로 설정할수록 IO:WALWrite와 IO:WALSync의 count가 증가한 것을 볼 수 있습니다.
이는 한 주기 동안 누적되는 WAL Record의 양이 증가하여, WAL Writer가 한번에 처리해야 하는 WAL Write 및 Flush의 작업 단위가 커졌기 때문으로 보입니다.
이로 인해, WAL Write와 Flush 작업에 대한 대기 시간이 길어지고 샘플링 시점에 포착될 가능성이 높아져 Wait Event count가 증가하였음을 알 수 있습니다.
- 정리하면 wal_writer_delay가 길어질수록 I/O 작업 빈도는 감소하지만, 한번의 I/O 작업에서 발생하는 대기 시간이 길어져 Wait Event가 확인될 가능성은 증가한다는 점을 확인할 수 있습니다.
마무리
- WAL File은 데이터 변경 작업에 대한 로그가 저장되는 파일로, 장애 발생 시 데이터 복구를 위한 핵심 구성요소이다.
- WAL File에 대한 I/O 작업은 WAL Buffer에 쌓인 WAL Record를 디스크에 저장(Write/Flush)하거나, 필요 시 디스크에서 WAL 데이터를 읽어오는(Read) 형태로 이루어진다.
- WAL File에 WAL Record를 저장하는 작업은 WAL Writer에 의해 주기적으로 수행되며, 상황에 따라 Backend Process가 직접 수행하기도 한다.
WAL Writer는 주기적으로 WAL Buffer에 누적된 WAL Record를 WAL File에 저장함으로써, WAL Buffer 공간을 사전에 재사용 가능하도록 확보하고 I/O 부하를 분산시키는 역할을 한다.
- WAL File에 WAL Record를 저장하는 과정에서 I/O 부하가 증가하면, IO:WALWrite와 IO:WALSync 두 Wait Event가 발생할 가능성이 높아진다.
- WAL Writer의 동작 주기는 wal_writer_delay 파라미터를 통해 설정한다.
동작 주기가 짧을수록, 누적되는 WAL Record 양이 많지 않은 상태에서도 Write 및 Flush 작업이 수행되며, 짧은 시간 단위로 빠르게 반복되기 때문에 Wait Event가 발생할 가능성은 낮아진다.
반면, 동작 주기가 길어질수록 한 주기 동안 누적되는 WAL Record 양이 증가하고, WAL Writer가 한번에 처리해야 하는 WAL Write 및 Flush 작업 단위가 커지면서, 작업 시 Wait Event 발생 가능성은 높아진다.
다만, 동작 주기가 지나치게 짧은 경우 전체 처리량이 떨어지고, CPU 오버헤드가 증가할 수 있으므로 워크로드 특성에 맞는 설정이 필요하다.
함께 보면 좋은 아티클
