WAL 구성 — 체크포인트와 성능 파라미터로 복구 속도를 조율하기

WAL 구성 — 체크포인트와 성능 파라미터로 복구 속도를 조율하기

WAL 관련 파라미터는 데이터베이스 성능에 깊게 관여해요. 특히 체크포인트(checkpoint)는 "데이터 파일이 어디까지 확실히 반영됐는지"를 정하는 기준점이라, 이를 어떻게 설정하느냐에 따라 복구 시간과 디스크 I/O 부하가 크게 달라집니다. 지금부터 가장 중요한 파라미터들을 실제 운영 관점에서 하나씩 살펴볼게요.

출처: 공식문서

체크포인트란 무엇인가요

체크포인트는 트랜잭션 흐름 속에서 "이 시점 이전에 써진 모든 정보가 힙·인덱스 데이터 파일에 반영됐다"고 보장되는 지점이에요. 체크포인트 시점에 모든 더티 데이터 페이지가 디스크로 flush 되고, 특별한 체크포인트 레코드가 WAL 파일에 기록되죠(변경 레코드 자체는 그 전에 이미 WAL 파일로 flush 되어 있어요).

크래시가 나면 복구 절차는 가장 최근 체크포인트 레코드를 찾아, WAL 중 어느 지점부터 REDO를 시작할지(이를 redo record라고 해요) 결정합니다. 그 지점 이전의 데이터 파일 변경은 이미 디스크에 있다는 게 보장되니까요. 그래서 체크포인트 이후에는 redo record가 들어있는 WAL 세그먼트 이전 것들은 더 이상 필요 없어서 재활용하거나 제거할 수 있어요(WAL 아카이빙 중이라면 재활용·제거 전에 반드시 아카이브부터 해야 해요).

체크포인트는 어떻게 스케줄 되나요

모든 더티 페이지를 디스크로 flush 해야 하니 체크포인트는 꽤 큰 I/O 부하를 만들어요. 그래서 체크포인트 활동은 조절(throttle)되어, I/O가 체크포인트 시작과 함께 시작해 다음 체크포인트가 시작되기 전에 끝나도록 설계됐어요.

체크포인트는 checkpoint_timeout 초마다, 혹은 max_wal_size를 넘기려 할 때(둘 중 먼저 오는 쪽) 시작되는데 기본값은 각각 5분과 1GB예요. 이전 체크포인트 이후 WAL이 하나도 쓰이지 않았다면 시간이 지나도 새 체크포인트는 건너뜁니다. 아카이빙 중이고 파일이 아카이브되는 빈도에 하한을 두어 데이터 손실을 제한하고 싶다면 체크포인트 파라미터보다 archive_timeout을 조정하는 게 맞아요. SQL 명령 CHECKPOINT로 직접 체크포인트를 강제할 수도 있어요.

간격을 줄이면 좋을까요

checkpoint_timeout이나 max_wal_size를 줄이면 체크포인트가 더 자주 일어나는데, 그러면 복구 시 다시 해야 할(redo) 작업이 줄어 복구가 빨라져요. 대신 더티 페이지를 더 자주 flush 하는 비용이 늘어난다는 걸 저울질해야 해요. 게다가 기본값으로 켜져 있는 full_page_writes 때문에 또 다른 요소가 생깁니다. 각 체크포인트 이후 어떤 데이터 페이지가 처음 수정되면 페이지 전체 내용을 WAL에 기록해야 하는데, 체크포인트 간격이 짧아지면 이 WAL 출력량이 늘어나 원래 의도(간격 줄이기)를 부분적으로 상쇄하고 디스크 I/O도 늘어나요.

파라미터가 너무 빡빡한지 확인하는 법

체크포인트는 현재 더티 버퍼를 전부 써내야 하고, 그 후속 WAL 트래픽도 늘어나 꽤 비싼 연산이에요. 그래서 체크포인트가 너무 자주 생기지 않도록 파라미터를 충분히 높게 잡는 게 현명합니다. checkpoint_warning 파라미터로 간단히 점검할 수 있는데, 체크포인트 간격이 checkpoint_warning 초보다 가까워지면 서버 로그에 "max_wal_size를 늘리세요"라는 메시지가 출력돼요. 가끔 나오는 건 놀랄 일이 아니지만, 자주 나온다면 체크포인트 제어 파라미터를 올려야 해요. 대량 COPY 같은 작업을 max_wal_size를 충분히 안 올린 채로 하면 이런 경고가 여러 번 나올 수 있죠.

I/O를 분산시키는 checkpoint_completion_target

체크포인트 시점에 페이지 쓰기 버스트로 I/O 시스템이 넘치지 않도록, 더티 버퍼 쓰기는 일정 기간에 걸쳐 퍼뜨립니다. 이 기간은 checkpoint_completion_target으로 조절하는데, 체크포인트 간격(checkpoint_timeout으로 설정)의 분율로 주어져요. 기본값 0.9에서는 각 체크포인트가 다음 예정 체크포인트보다 조금 앞선 시점(직전 체크포인트 지속 시간의 약 90%)에 끝나도록 I/O 속도가 맞춰집니다. 이렇게 하면 체크포인트 I/O 부하가 간격 전체에 걸쳐 고르게 퍼져요.

단점도 있는데, 체크포인트를 늘리면 복구에 사용될 WAL 세그먼트를 더 오래 보관해야 해서 복구 시간에 영향을 줘요. 복구 시간이 걱정된다면 checkpoint_completion_target을 낮추기보다 checkpoint_timeout을 줄여 체크포인트를 더 자주 하되 I/O는 간격에 걸쳐 퍼뜨리는 걸 권장해요. checkpoint_completion_target은 최대 1.0까지 설정 가능하지만, 체크포인트에는 더티 버퍼 쓰기 외 활동도 포함되므로 보통 0.9(기본값)를 넘지 말라고 권합니다. 1.0이면 체크포인트가 제때 끝나지 않을 가능성이 커져, 필요한 WAL 세그먼트 수가 예상 밖으로 변동하면서 성능 저하를 부를 수 있어요.

checkpoint_flush_after

Linux·POSIX 플랫폼에서 checkpoint_flush_after는 체크포인트가 쓴 OS 페이지를 설정된 바이트 수만큼 쌓인 뒤 디스크로 flush 하도록 강제할 수 있어요. 그렇지 않으면 이 페이지들이 OS 페이지 캐시에 남아 있어서 체크포인트 끝에 fsync를 할 때 멈춤(stall)이 생길 수 있거든요. 트랜잭션 지연을 줄여주는 경우가 많지만, 특히 shared_buffers보다 크고 OS 페이지 캐시보다는 작은 워크로드에서는 성능에 역효과를 줄 수도 있어요.

pg_wal 디렉토리의 파일 수 조절

pg_wal 디렉토리의 WAL 세그먼트 파일 수는 min_wal_size, max_wal_size, 그리고 이전 체크포인트 주기 동안 생성된 WAL 양에 따라 달라져요. 더 이상 필요 없는 오래된 WAL 세그먼트는 제거되거나 재활용(번호 매겨진 시퀀스에서 미래 세그먼트로 이름이 바뀜)됩니다. WAL 출력률의 단기 피크 때문에 max_wal_size를 넘으면, 시스템이 그 한도 아래로 돌아올 때까지 불필요한 세그먼트가 제거돼요. 한도 아래에서는 다음 체크포인트까지 필요할 것으로 추정되는 만큼만 WAL 파일을 재활용하고 나머지는 제거합니다. 이 추정치는 이전 체크포인트 주기에서 사용한 WAL 파일 수의 이동 평균 기반인데, 실제 사용이 추정치를 넘으면 즉시 올라가므로 평균보다는 피크 사용량을 어느 정도 수용하죠. min_wal_size는 미래 사용을 위해 재활용할 WAL 파일 양의 최소치를 정하는데, 시스템이 놀고 추정치가 작아도 그만큼은 항상 재활용해 둡니다.

어떤 경우에 파일이 쌓일까요

max_wal_size와 별개로, 가장 최근 wal_keep_size MB의 WAL 파일 + 추가 1개 파일은 항상 유지돼요. 그리고 WAL 아카이빙을 쓰면 오래된 세그먼트는 아카이브되기 전까지 제거·재활용될 수 없습니다. 아카이빙이 WAL 생성 속도를 못 따라가거나 archive_command·archive_library가 반복 실패하면, 상황이 해결될 때까지 오래된 WAL 파일이 pg_wal에 쌓여요. 복제 슬롯을 쓰는 Standby가 느리거나 실패해도 같은 효과가 나고, WAL 요약(summarization)이 켜져 있으면 오래된 세그먼트를 요약될 때까지 보관합니다.

복구 중의 restartpoint

아카이브 복구나 Standby 모드에서 서버는 주기적으로 restartpoint를 수행해요. 일반 운영의 체크포인트와 비슷하게, 모든 상태를 디스크로 강제하고, 이미 처리된 WAL 데이터를 다시 스캔할 필요가 없다고 pg_control 파일을 갱신한 뒤 pg_wal의 오래된 세그먼트를 재활용합니다. restartpoint는 체크포인트 레코드에서만 수행 가능해서 프라이머리의 체크포인트보다 자주는 못 해요. pg_stat_checkpointer 뷰의 restartpoints_timed 카운터는 스케줄에 의한 것, restartpoints_req는 외부 요청에 의한 것을 셉니다. 요청 기반 restartpoint도 체크포인트와 비슷한 이유로 생기지만 대부분 WAL 크기가 max_wal_size를 넘기려 할 때죠. 다만 수행 가능 시점 제약 때문에 복구 중에는 max_wal_size가 한 체크포인트 주기만큼의 WAL까지 초과되는 일도 흔해요(max_wal_size는 어디까지나 하드 한도가 아니므로, 디스크가 차지 않도록 여유를 남겨두는 게 좋아요). 스탠바이에서 프라이머리 WAL이 대량 INSERT 등으로 급증하면 restartpoints_req가 치솟는 것처럼 보일 수 있는데, 증가한 WAL 소비로 새 restartpoint를 요청해도 마지막 restartpoint 이후 안전한 체크포인트 레코드가 아직 스탠바이에서 재생되지 않아 수행할 수 없기 때문이에요. 이는 정상 동작이고 시스템 자원을 추가로 소비하지 않아요.

WAL 버퍼: XLogInsertRecord와 XLogFlush

WAL 관련 내부 함수로는 XLogInsertRecordXLogFlush가 자주 쓰여요. XLogInsertRecord는 새 레코드를 공유 메모리의 WAL 버퍼에 넣는데, 공간이 없으면 채워진 WAL 버퍼 몇 개를 직접 쓰게(kernel 캐시로 옮김) 됩니다. 문제는 XLogInsertRecord가 모든 저수준 DB 변경(예: 행 삽입)에서, 그 영향 받는 데이터 페이지에 배타적 잠금이 걸린 채 호출되기 때문에 최대한 빨라야 한다는 점이에요. 게다가 WAL 버퍼 쓰기가 새 WAL 세그먼트 생성을 강제할 수도 있어 더 느려져요.

보통은 XLogFlush 요청이 WAL 버퍼를 쓰고 flush 하며, 이 요청은 대부분 트랜잭션 커밋 시점에 일어나 트랜잭션 레코드가 영구 저장소에 반영되게 합니다. WAL 출력이 많은 시스템에서는 XLogFlush 요청만으로는 XLogInsertRecord가 직접 쓰지 않게 하기 부족할 수 있는데, 그럴 땐 wal_buffers로 WAL 버퍼 수를 늘려주는 게 좋아요. full_page_writes가 켜져 있고 시스템이 매우 바쁠 때 wal_buffers를 높이면 각 체크포인트 직후의 응답 시간이 부드러워지는 데 도움이 돼요.

commit_delay와 그룹 커밋

commit_delay는 그룹 커밋 리더 프로세스가 XLogFlush 안에서 잠금을 획득한 뒤 몇 마이크로초 동안 잠들지, 즉 그룹 커밋 팔로워들이 리더 뒤에 줄을 서는 동안의 대기 시간을 정의해요. 이 지연 덕분에 다른 서버 프로세스들이 커밋 레코드를 WAL 버퍼에 추가하고, 결국 리더의 sync 연산이 그것들을 모두 flush 하죠. fsync가 꺼져 있거나, 현재 활성 트랜잭션인 다른 세션이 commit_siblings보다 적으면 잠들지 않습니다. 일부 플랫폼에서는 sleep 해상도가 10ms여서 1~10000 마이크로초 사이의 비영(非零) 설정이 같은 효과를 낼 수 있어요.

commit_delay의 목적은 flush 한 번의 비용을 동시 커밋 트랜잭션들에 나눠 부담시키는(잠재적으로 트랜잭션 지연을 희생) 것이니, 그 비용을 정량화해야 적절히 고를 수 있어요. pg_test_fsync로 WAL flush 한 번의 평균 시간(마이크로초)을 측정할 수 있는데, "단일 8kB 쓰기 후 flush"에 걸리는 평균 시간의 절반 정도가 가장 효과적인 commit_delay로 자주 언급됩니다. 높은 회전 지연의 회전식 디스크에서 특히 유용하지만, SSD나 배터리 백업 쓰기 캐시가 있는 RAID처럼 sync가 매우 빠른 매체에서도 이점이 클 수 있어요(다만 대표 워크로드로 반드시 테스트해야 해요). 너무 높은 commit_delay는 트랜잭션 지연을 크게 늘려 총 처리량을 오히려 떨어뜨릴 수 있어요.

commit_delay가 0(기본값)이어도 일종의 그룹 커밋은 일어납니다. 다만 각 그룹이 이전 flush가 진행 중인 창 안에 커밋 레코드 flush 지점에 도달한 세션들로만 구성되어요. 클라이언트 수가 많아지면 이른바 "갱웨이 효과"가 나타나 0이어도 그룹 커밋 효과가 커져서, commit_delay를 명시적으로 설정하는 이득은 줄어들죠.

wal_sync_method

wal_sync_method는 PostgreSQL이 커널에 WAL 갱신을 디스크로 밀어내라고 요청하는 방식을 결정해요. 신뢰성 면에서 모든 옵션이 같다고 보면 되고, 예외는 fsync_writethrough로 다른 옵션이 안 하는 디스크 캐시 flush를 때로 강제할 수 있어요. 어느 쪽이 가장 빠른지는 플랫폼마다 크게 달라서 pg_test_fsync 프로그램으로 속도를 비교해볼 수 있습니다. fsync를 껐다면 이 파라미터는 무의미해요.

그 밖의 파라미터

  • wal_debug: PostgreSQL이 지원 컴파일된 경우 켜면 모든 XLogInsertRecord·XLogFlush WAL 호출이 서버 로그에 기록돼요(미래에 더 일반적인 메커니즘으로 대체될 수 있어요).
  • track_wal_io_timing + pg_stat_io: XLogWrite가 쓴 시간과 issue_xlog_fsync가 sync한 시간이 각각 object = walwrite_time, fsync_time으로 집계되고, 호출 횟수는 writes, fsyncs로 항상 집계돼요. wal_sync_methodopen_datasyncopen_sync면 쓰기 연산이 sync까지 보장해 issue_xlog_fsync는 아무것도 안 하고, fdatasync·fsync·fsync_writethrough면 버퍼를 kernel 캐시로 옮기고 issue_xlog_fsync가 디스크로 sync해요.
  • recovery_prefetch: 복구 중 곧 필요해질 디스크 블록 읽기를 커널에 미리 지시해 I/O 대기를 줄여요. maintenance_io_concurrency가 prefetch 동시성을, wal_decode_buffer_size가 거리를 제한해요. 기본값 try는 read-ahead 조언을 지원하는 시스템에서 이 기능을 켭니다.

더 알아보기 (Learn more)