체크포인트 조정의 기본 원리

Tomas Vondra
2024년 7월 11일

체크포인트를 적절히 조정하는 것은 시스템에서 많은 쓰기 작업이 발생할 때 성능을 최적화하는 데 필수적입니다. 그러나 체크포인트는 커뮤니티 메일링 리스트나 고객 지원 및 컨설팅 과정에서 자주 혼란과 설정 문제로 지적되는 영역 중 하나입니다.

이 글에서는 체크포인트의 개념, 목적, 데이터베이스에서의 구현 방식, 그리고 이를 효과적으로 조정하는 방법에 대해 자세히 다룹니다.

체크포인트의 목적은 무엇인가요?

PostgreSQL은 **WAL(Write-Ahead Log)**을 사용하는 데이터베이스 중 하나입니다. 데이터 파일에 변경 작업이 수행되기 전에, 변경 사항은 별도의 로그(변경 스트림)에 기록됩니다. 이 방식은 내구성을 제공하는데, 시스템 충돌 시 데이터베이스가 WAL을 사용해 복구를 수행하고, WAL에서 변경 사항을 읽어 데이터 파일에 다시 적용할 수 있기 때문입니다.

처음에는 이러한 방식이 비효율적으로 보일 수 있습니다. 데이터 파일을 변경하면서 동시에 로그에 변경 사항을 기록하므로 쓰기 작업이 두 배로 늘어나기 때문입니다. 하지만 몇 가지 이유로 이 방식은 성능을 실제로 향상시킬 수 있습니다.

  • COMMIT은 WAL이 디스크에 기록되고 플러시되어 내구성을 확보할 때까지만 기다리면 됩니다.
  • 데이터 파일은 메모리(공유 버퍼)에서만 수정되며, 디스크에 쓰이거나 플러시되는 시점은 나중으로 미뤄질 수 있습니다.
  • WAL은 순차적으로 기록되지만, 데이터 파일 쓰기는 종종 랜덤하게 이루어지므로 WAL 플러시가 훨씬 저렴합니다.
  • 공유 버퍼에 있는 데이터 페이지는 여러 번 수정될 수 있으며, 단일 물리적 I/O 쓰기로 저장되므로 성능이 크게 향상됩니다.

복구와 체크포인트

시스템이 충돌해 데이터베이스 복구가 필요하다고 가정해봅시다. 가장 단순한 방법은 처음부터 시작해 모든 WAL을 적용하는 것입니다. 결국 완전하고 올바른 데이터베이스를 얻을 수 있습니다. 하지만 이 방법에는 큰 단점이 있습니다. 데이터베이스 생성 이후의 모든 WAL을 유지하고 재생해야 하기 때문입니다.

많은 양의 쓰기 작업을 처리하는 데이터베이스는 하루에 수 TB의 WAL을 생성할 수 있습니다. 이러한 데이터베이스를 1년 동안 실행한다고 가정하면, 모든 WAL을 유지하는 데 필요한 디스크 공간과 복구에 걸리는 시간을 상상해보세요. 이런 접근 방식은 현실적이지 않습니다.

그렇다면 데이터베이스가 특정 WAL 위치(로그 오프셋, “로그 시퀀스 번호(LSN)”)까지의 모든 데이터 파일 변경 사항이 디스크에 플러시되었음을 보장할 수 있다면 어떻게 될까요? 복구 시점에 해당 위치를 기준으로 나머지 WAL만 재생하면 됩니다. 이렇게 하면 복구 시간이 크게 단축되며, 해당 위치 이전의 WAL은 복구에 필요 없으므로 삭제할 수 있습니다.

이것이 바로 체크포인트의 역할입니다. 특정 LSN 이전의 WAL이 복구에 필요하지 않도록 보장함으로써 디스크 공간 요구 사항과 복구 시간을 줄이는 것입니다. 데이터베이스는 현재 WAL 위치를 확인하고, 플러시되지 않은 변경 사항을 디스크에 기록하며, 복구 시 필요한 LSN을 기록합니다.

체크포인트는 데이터베이스에서 주기적으로 생성되며, 이는 시간 간격 또는 이전 체크포인트 이후 생성된 WAL의 양에 따라 결정됩니다.

체크포인트 구성의 극단적인 두 가지 사례

  1. 체크포인트를 거의 생성하지 않는 경우
    체크포인트를 전혀 생성하지 않거나 매우 드물게 생성하면, 데이터 파일 변경 사항을 비동기적으로 결합해 쓰기 작업을 최소화할 수 있습니다. 하지만 모든 WAL을 유지해야 하며, 충돌이나 비정상 종료 후 복구 시간이 길어지는 단점이 있습니다.
  2. 체크포인트를 너무 자주 생성하는 경우
    체크포인트를 매우 자주 생성(예: 1초마다)하면 WAL의 양이 줄고 복구 시간도 매우 짧아질 수 있습니다. 하지만 데이터 파일 쓰기가 비동기적에서 동기적으로 전환되고, 여러 변경 사항이 하나의 물리적 쓰기로 결합될 가능성이 줄어들어 성능에 부정적인 영향을 미칩니다(예: COMMIT 지연 증가, 처리량 감소).

결론적으로, 체크포인트는 사용자에게 영향을 주지 않을 정도로 충분히 드물게 발생해야 하지만, 복구 시간과 디스크 공간 요구 사항을 제한할 정도로 충분히 자주 발생해야 합니다.

체크포인트 트리거 방식

체크포인트를 트리거하는 방법은 약 3~4가지가 있습니다:

첫 번째와 두 번째 방법은 이 글에서 다루지 않습니다. 이 두 가지는 주로 유지보수 작업과 관련된 수동 이벤트로, 발생 빈도가 낮기 때문입니다. 이 글은 정기적인 주기적 체크포인트에 영향을 미치는 나머지 두 가지 설정(시간 및 크기 제한)에 대해 다룹니다.

관련 설정 옵션

이러한 시간/크기 제한은 다음 두 가지 구성 옵션으로 설정됩니다:

위 기본값을 기준으로 PostgreSQL은 마지막 체크포인트 이후 5분이 경과하거나 약 1/2 GB의 WAL이 작성되었을 때, 둘 중 먼저 도달하는 조건에 따라 체크포인트를 트리거합니다.

참고 사항:


max_wal_size는 WAL 총 용량에 대한 소프트 제한으로, 두 가지 주요 결과를 가집니다.

  1. 데이터베이스는 이 한도를 초과하지 않으려고 시도하지만, 필요에 따라 초과할 수 있으므로 파티션에 충분한 여유 공간을 확보하고 모니터링해야 합니다.
  2. 이 값은 체크포인트당 WAL 양이 아닌 WAL의 총량에 대한 제한입니다. 체크포인트 분산(spread checkpoints, 이후 설명)으로 인해 이 할당량은 1~2개의 체크포인트(또는 PostgreSQL 11 이전에는 2~3개)를 포함합니다.

따라서, max_wal_size = 1GB로 설정된 경우 데이터베이스는 500~1000MB의 WAL을 작성한 후 checkpoint_completion_target에 따라 체크포인트를 시작합니다.

기본값 및 최적화

기본값은 보수적인 경향(즉, 낮은 설정치)을 가지며, 샘플 설정 파일의 다른 기본값들처럼 Raspberry Pi 같은 소형 시스템에서도 작동하도록 설계되었습니다. 하지만 더 강력한 하드웨어에서 최상의 성능을 얻으려면 이러한 한도를 상당히 높여야 할 가능성이 큽니다.

그렇다면 시스템이나 애플리케이션에 적합한 값을 어떻게 결정할 수 있을까요? 앞서 언급했듯이, 체크포인트는 너무 자주도, 너무 드물게도 발생하지 않아야 합니다. 이를 위한 튜닝 베스트 프랙티스는 두 가지 단계로 구성됩니다:

  1. 합리적인 checkpoint_timeout 값을 선택합니다.
  2. max_wal_size를 설정하여 거의 한계에 도달하지 않도록 충분히 높은 값을 지정합니다.

합리적인 checkpoint_timeout 값은?

checkpoint_timeout 값이 클수록 마지막 체크포인트 이후 더 많은 작업이 축적됩니다. 따라서 복구 시 더 많은 작업을 처리해야 하며, 이는 복구 시간 목표(RTO, Recovery Time Objective)와 관련됩니다.


즉, 충돌 후 복구를 얼마나 빠르게 완료하고 싶은지에 따라 적절한 값을 설정해야 합니다. RTO가 짧을수록 checkpoint_timeout 값을 작게 설정하고, 더 긴 복구 시간을 허용할 수 있다면 더 큰 값을 선택할 수 있습니다.

참고:


충돌 후 복구 시간이 길어질까 걱정된다면, 특히 시스템이 고가용성(High Availability)이나 SLA 요구사항을 충족해야 하는 경우, 단순히 체크포인트에 의존하기보다는 복제본(Replica) 설정을 고려하는 것이 좋습니다.

시스템은 긴 재부팅, 하드웨어 부품 교체 등의 작업이 필요할 수 있습니다. 짧은 간격의 체크포인트 설정만으로는 이러한 문제를 해결할 수 없습니다. 복제본으로의 **장애 조치(Failover)**는 이미 검증된 효과적인 솔루션입니다.

이 부분은 다소 까다로운데, checkpoint_timeout은 WAL 생성 시간의 제한일 뿐 복구 시간 제한은 아닙니다. WAL은 여러 프로세스(DML을 실행하는 백엔드)에서 생성되지만, 복구는 단일 프로세스에서 수행되므로 CPU 제한과 I/O 병목이 발생할 수 있습니다. 이는 로컬 복구뿐만 아니라 스트리밍 복제에서도 영향을 미쳐, 복제본이 기본 서버를 따라가지 못할 수 있습니다. 또한, 재부팅 후처럼 캐시가 비어 있는 상태(차가운 캐시)에서는 복구가 느려질 수 있습니다.


참고: PostgreSQL 15에서는 recovery_prefetch 옵션이 도입되어, 복구 중 데이터를 비동기로 미리 가져올 수 있습니다. 이를 통해 “단일 프로세스 복구”의 한계를 보완하며, 경우에 따라 복구 속도가 데이터를 생성했던 원래 작업 속도보다 더 빨라질 수도 있습니다.

워크로드와 체크포인트 설정

인스턴스는 워크로드에 따라 매우 다른 WAL을 생성할 수 있습니다. 예를 들어, 쓰기 중심의 워크로드를 처리하는 인스턴스는 동일한 시간 동안 읽기 전용 인스턴스보다 훨씬 더 많은 WAL을 생성합니다.

checkpoint_timeout 값 설정

checkpoint_timeout 값이 길수록 더 많은 WAL이 생성되고 복구 시간도 길어지지만, “적절한” 값이 무엇인지 명확히 정의하기는 어렵습니다. 불행히도 최적의 값을 알려주는 간단한 공식은 없습니다.

기본값(5분)은 너무 낮은 편이며, 프로덕션 시스템에서는 일반적으로 30분에서 1시간 사이의 값을 사용합니다. PostgreSQL 9.6에서는 이 한도를 1일로 늘렸는데, 이는 일부 시스템에서 유효할 수 있다고 생각한 전문가들이 있었기 때문입니다.

주의: 낮은 값은 전체 페이지 쓰기로 인해 쓰기 증폭을 일으킬 수 있으며, 이는 심각한 문제입니다. 하지만 이 글에서는 간결함을 위해 이 주제는 다루지 않습니다.

체크포인트와 수익 체감의 법칙

checkpoint_timeout는 **수익 체감의 법칙(diminishing returns)**을 잘 보여주는 예입니다. 값을 늘릴수록 얻는 이점은 점차 감소합니다.

  • 5분에서 10분으로 늘리면 명확한 개선이 나타날 수 있습니다.
  • 10분에서 20분으로 늘리면 또다시 개선되겠지만, 그 효과는 훨씬 작아집니다.
  • 20분에서 40분으로 늘리면, 비용(WAL 생성량 및 복구 시간)은 두 배가 되지만, 이점은 더욱 미미해집니다.

최적의 균형은 시스템 요구사항에 따라 결정해야 합니다.

실용적인 예시: checkpoint_timeout = 30분

30분은 너무 낮지도, 너무 높지도 않은 합리적인 값으로 경험상 적합합니다.

arduino코드 복사checkpoint_timeout = 30min  

이제 우리의 목표는 평균적으로 30분마다 체크포인트가 발생하도록 설정하는 것입니다. 그러나 이 값만 설정하면 체크포인트가 WAL 용량 제한(max_wal_size)에 의해 더 자주 발생할 수 있습니다.

체크포인트가 오직 checkpoint_timeout에 의해 트리거되도록 하기 위해, 30분 동안 생성되는 WAL의 양을 추정해야 합니다. 이 값을 max_wal_size로 설정하면 됩니다.

checkpoint_timeout을 30분으로 설정한 경우, 30분 동안 데이터베이스가 생성하는 WAL 양을 추정하여 그 값을 **max_wal_size**로 설정해야 합니다. 이를 추정하는 방법은 여러 가지가 있습니다:

  • pg_current_wal_lsn()을 사용하여 WAL 위치 계산
    PostgreSQL의 함수 pg_current_wal_lsn()을 사용해 특정 시점의 WAL 위치를 확인한 후, 30분 후에 다시 측정해 두 위치의 차이를 계산합니다.
    (PG 9.6 이전에는 이 함수가 pg_current_xlog_location()으로 불렸습니다.)
  • log_checkpoints 활성화 후 서버 로그 확인
    log_checkpoints=on을 설정하면 각 완료된 체크포인트에 대한 상세 통계(생성된 WAL 양 포함)가 서버 로그에 기록됩니다.
  • pg_stat_bgwriter를 사용해 체크포인트 데이터 추출
    이 뷰는 체크포인트 수와 관련된 정보를 제공합니다. 현재 설정된 max_wal_size 값을 바탕으로 데이터를 결합해 추정할 수 있습니다.

예제: 첫 번째 방법 사용

아래는 테스트 머신에서 pgbench를 실행하며 첫 번째 방법을 사용한 예입니다:

test=# select pg_current_wal_lsn();
 pg_current_wal_lsn
--------------------
 C7/72C140D8
(1 row)

... after 5 minutes ...

test=# select pg_current_wal_lsn();
 pg_current_wal_lsn
--------------------
 C7/E8A494CF
(1 row)

test=# SELECT pg_wal_lsn_diff('C7/E8A494CF', 'C7/72C140D8');
 pg_wal_lsn_diff
-----------------
    1977832439
(1 row)

위 결과를 통해 데이터베이스가 5분 동안 약 1.8GB의 WAL을 생성했음을 확인할 수 있습니다.

  • 30분 동안의 WAL 양 = 6 * 1.8GB = 11GB
  • 하지만 max_wal_size1~2개의 체크포인트를 포함하는 할당량이므로, 22GB로 설정해야 합니다.

참고 사항
  • max_wal_size가 포함하는 체크포인트 수는 checkpoint_completion_target 값에 따라 달라집니다(이 내용은 이후 섹션에서 논의).
  • PostgreSQL 11 이전에는 2~3개의 체크포인트가 포함되었습니다.
  • 다른 방법으로 데이터를 수집해도 개념은 동일하며 결과도 비교 가능할 것입니다.

단순 추정값의 한계

체크포인트 빈도를 조정하면 WAL 쓰기 증폭에 영향을 미치기 때문에, 측정값은 본질적으로 정확하지 않을 수 있습니다. 이는 체크포인트 빈도가 **풀 페이지 쓰기(full-page write)**에 영향을 주기 때문입니다.


데이터 파일의 각 데이터 페이지(8kB)가 체크포인트 이후 처음 변경되면, 해당 페이지 전체를 WAL에 기록합니다.

  • 동일 페이지를 두 번 변경(예: 두 트랜잭션에서 UPDATE)하면, 중간에 체크포인트가 발생했는지에 따라 하나 또는 두 개의 풀 페이지 쓰기가 발생할 수 있습니다.
  • 따라서 체크포인트를 덜 자주 발생시키면 일부 풀 페이지 쓰기를 제거하여 생성되는 WAL의 양을 줄일 수 있습니다.

예를 들어, 체크포인트가 10분마다 발생하며, 체크포인트당 10GB의 WAL을 생성한다고 가정해봅시다. 만약 체크포인트를 30분마다 발생시키고 싶다면, 다음과 같이 설정할 수 있습니다:

max_wal_size = 60GB

이는 2개의 체크포인트(각각 30GB)를 커버할 수 있으며, 원하는 결과를 얻을 수 있을 것입니다. 그러나 실제로 체크포인트당 생성된 WAL의 양을 측정해보면, 예상했던 30GB 대신 평균 약 15GB 정도만 생성되는 것을 발견할 수 있습니다. 이는 **풀 페이지 쓰기(full-page write)**가 줄어들었기 때문이며, 이는 궁극적으로 긍정적인 결과입니다.

  • 풀 페이지 쓰기가 줄어들면 다음과 같은 이점이 있습니다:
    • WAL 증폭 감소
    • 보관 및 복제해야 할 WAL 양 감소
    • 시스템 효율성 향상

이처럼 체크포인트 빈도를 줄이면 백업 정책에 따라 보관해야 할 WAL 양을 크게 줄일 수 있습니다.

  • 일반적으로 **PITR(Point-In-Time Recovery)**을 위해 최근 30일간의 WAL과 백업을 유지합니다.
  • 체크포인트 빈도를 낮추면 풀 페이지 쓰기가 감소하여 WAL 크기가 줄어들고, 이에 따라 WAL 보관 공간도 절감됩니다.

분산 체크포인트(Spread Checkpoints)

체크포인트를 튜닝할 때 **checkpoint_timeout**과 **max_wal_size**만 조정하면 된다고 했지만, 사실 완전히 정확한 얘기는 아닙니다. 이 두 가지가 가장 중요한 매개변수이긴 하지만, **checkpoint_completion_target**이라는 세 번째 매개변수도 존재합니다.

기본값은 꽤 합리적이기 때문에 대부분의 경우 별도로 조정할 필요는 없습니다. 하지만 분산 체크포인트가 무엇인지 이해하면 필요한 경우 조정할 수 있습니다.

체크포인트의 기본 작업 단계

체크포인트 동안 데이터베이스는 다음 세 가지 주요 단계를 수행합니다:

  1. 공유 버퍼(shared buffers)에서 수정된(더티) 블록 식별
  2. 이 블록들을 디스크(또는 파일 시스템 캐시)에 기록
  3. 수정된 파일들을 디스크에 fsync() 호출로 동기화

이 모든 작업이 완료되어야 체크포인트가 끝난 것으로 간주됩니다. 특히 마지막 fsync() 단계는 데이터를 내구성 있게 만드는 데 핵심입니다.

빠른 체크포인트의 문제

PostgreSQL 8.2 이전에는 이 작업들을 가능한 빨리 수행했으며, 이는 다음과 같은 문제를 초래했습니다. 더티 데이터를 한꺼번에 디스크로 기록 → I/O 병목 발생, 파일 시스템 캐시를 가득 채움 → 사용자 세션에 큰 영향을 미침.

예를 들어, 공유 버퍼에 8GB의 수정된 데이터가 있는 경우, 이를 한꺼번에 기록하고 fsync()를 호출하면 커널이 이를 최대한 빨리 처리하려고 하지만, 그 결과 다른 프로세스의 I/O 대기가 길어짐

PostgreSQL 8.3: 분산 체크포인트 도입

이 문제를 해결하기 위해 PostgreSQL 8.3에서는 분산 체크포인트(spread checkpoints) 개념을 도입했습니다.

데이터를 한 번에 기록하는 대신, 시간에 걸쳐 데이터를 분산 기록합니다. 그래서 OS가 백그라운드에서 더티 데이터를 플러시할 시간을 제공하여 최종 fsync() 비용을 낮추고, 사용자 세션에 미치는 영향을 줄입니다.

checkpoint_completion_target의 역할

checkpoint_completion_target = 기본값: 0.9 (PostgreSQL 14 이전은 0.5) 매개변수는 다음 체크포인트까지 얼마나 진행되었을 때 모든 쓰기 작업이 완료되어야 하는지를 결정합니다.

예를 들면, 체크포인트가 30분마다 트리거된다면, 데이터베이스는 마지막 쓰기 작업을 30 * 0.9 = 27분 후에 완료합니다.

OS는 이후 3분 동안 데이터를 디스크로 플러시할 시간을 확보합니다.

결과적으로 최종 fsync()는 더 적은 데이터만 처리하므로 더 빠르고 저렴하게 완료됩니다.

PostgreSQL 9.6: checkpoint_flush_after 도입

PostgreSQL 9.6 이전에는 커널이 페이지 캐시에서 더티 데이터를 얼마나 빠르게 제거하는지에 대해 신경 써야 했습니다. 이는 기본값으로 30초로 설정된 vm.dirty_expire_centisecs(더티 데이터 만료 시간)와 vm.dirty_background_bytes(데이터 쓰기를 시작하는 더티 데이터 양)에 의해 결정됩니다. 또한 체크포인트 종료 시 OS가 데이터를 쓰는 데 필요한 시간을 계산할 때 이를 고려해야 했습니다.

그러나 PostgreSQL 9.6에서는 checkpoint_flush_after 옵션이 도입되어 이러한 커널 매개변수의 튜닝이 대부분 불필요해졌습니다. 이 옵션은 CHECKPOINT 동안 데이터베이스가 작은 데이터(기본값: 256kB)를 쓴 후 정기적으로 **fsync**를 수행하도록 지시합니다. 이를 통해 다른 프로세스에 대한 fsync 호출의 영향을 크게 줄일 수 있으며(주요 I/O 병목이 발생하지 않음), 미처리된 데이터의 양이 적어지므로 체크포인트 종료 시점의 시간에 대해 크게 신경 쓸 필요가 없어졌습니다.

마치며…

이제 체크포인트의 목적과 기본적인 튜닝 방법에 대해 이해했을 것입니다. 이를 요약하면 다음과 같습니다:

  • 대부분의 체크포인트는 시간(checkpoint_timeout)에 의해 트리거되어야 합니다.
  • 체크포인트 간격은 **처리량(드문 체크포인트)**과 복구 시간(잦은 체크포인트) 사이의 절충입니다.
  • 대부분의 프로덕션 시스템에서는 30~60분 사이의 checkpoint_timeout 값을 사용합니다. 특별한 데이터가 없는 경우 이 범위 내에서 값을 선택하세요.
  • checkpoint_timeout을 결정한 후, 30분 동안 생성되는 WAL 양을 추정하여 max_wal_size 값을 설정하세요.
  • checkpoint_completion_target을 0.9로 설정하세요(구버전에서는 기본값이 0.5였습니다).
  • PostgreSQL 9.6 이전 버전에서는 **checkpoint_flush_after**가 없으므로, **커널 매개변수(vm.dirty_expire_centisecs, vm.dirty_background_bytes)**를 조정해야 할 수도 있습니다.

이러한 설정은 효율적인 체크포인트 관리를 통해 시스템 성능과 복구 시간을 최적화하는 데 도움을 줄 것입니다.

본문 : Basics of Tuning Checkpoints

이메일: salesinquiry@enterprisedb.com