Skip to the content.

데이터베이스 시스템에서 메모리는 한정된 고비용 자원이기 때문에 효율적인 관리가 필수적입니다. PostgreSQL에서는 이를 위해 Clock Sweep 알고리즘을 사용하여 공유 버퍼를 관리합니다.

메모리에 올라와 있는 데이터가 자주 사용되지 않는다면(즉, 캐시 히트가 발생하지 않는다면) 해당 데이터는 더 이상 메모리에 상주할 필요가 없습니다. 따라서 메모리 공간 확보를 위해 교체 대상이 되는 버퍼를 Victim(희생양) 버퍼라고 부릅니다.


Clock Sweep 의사결정 흐름

Clock Sweep 알고리즘의 동작 과정을 도식화하면 아래와 같습니다.

[시작: 현재 클록 바늘이 가리키는 버퍼 확인]
  │
  ▼
[의사결정 1: 해당 페이지가 현재 사용 중(Pinned)인가?]
  ├── 예 (refcount > 0) ──────► [바늘을 다음 버퍼로 이동] ──► (시작으로 이동)
  └── 아니오 (refcount == 0)
        │
        ▼
[의사결정 2: 페이지의 사용 빈도(usage_count)가 0보다 큰가?]
  ├── 예 (usage_count > 0) ──► [usage_count를 1 감소시킴] ──► [바늘을 다음 버퍼로 이동] ──► (시작으로 이동)
  └── 아니오 (usage_count == 0)
        │
        ▼
[의사결정 3: 해당 버퍼의 데이터가 변경(Dirty)되었는가?]
  ├── 예 (Dirty Page) ──────► [디스크에 변경사항 기록 (Flush)] ──► [최종 결정: 교체 대상(Victim)으로 선택]
  └── 아니오 (Clean Page) ──► [최종 결정: 교체 대상(Victim)으로 선택]

동작 요약

  1. Pinned 버퍼 패스: 백엔드 프로세스가 해당 데이터를 읽고 있는 중(refcount > 0)이라면, 해당 버퍼는 Victim으로 선정되지 않고 바늘이 다음 버퍼로 넘어갑니다.
  2. 사용 중이지 않은 버퍼 검사: Pinned 상태가 아니라면 usage_count를 확인합니다.
    • usage_count == 0이면 즉시 Victim(교체 대상)으로 선정됩니다.
    • usage_count > 0이면 값을 1 감소(usage_count--)시키고 다음 버퍼로 넘어갑니다.

왜 굳이 순회할 때마다 값을 1씩 감소시킬까?

여기서 “왜 바로 비우지 않고 값을 1씩 깎으면서 다음으로 넘어갈까?” 하는 의문이 생길 수 있습니다.

간단히 말해, 이 감소 장치가 없다면 한 번 메모리에 올라온 버퍼가 오랫동안 쓰이지 않더라도 계속 메모리에 무한 상주할 가능성이 매우 높아지기 때문입니다.

usage_count버퍼의 온도라고 비유한다면, Clock Sweep 바늘은 순회할 때마다 온도를 서서히 낮춰주는 냉각 장치 역할을 하는 셈입니다.


BM_MAX_USAGE_COUNT의 존재 의의

PostgreSQL에서 usage_count가 올라갈 수 있는 최대 임계치는 BM_MAX_USAGE_COUNT로 지정되어 있으며 기본값은 5입니다.

만약 이 한도를 정해두지 않으면, 아주 짧은 시간 동안 특정 버퍼에 수많은 접근이 집중될 경우 usage_count가 수백, 수천까지 올라갈 수 있습니다.

예를 들어 usage_count가 100까지 올라간 버퍼가 있다고 가정해 보겠습니다. 이후 해당 버퍼가 전혀 쓰이지 않더라도, Clock Sweep 바늘이 이 버퍼를 무려 100번 넘게 지나치며 깎아내려야 비로소 메모리에서 해제될 수 있습니다. 이는 메모리 공간의 비효율적인 낭비로 이어지기 때문에 최대 임계값(기본값 5)을 두어 이를 원천 방지하는 것입니다.