Skip to the content.

일시적인 배치 작업으로 대량 데이터들을 처리해야된다면 이전에 배웠던 Shared Buffer 영역을 사용하게 될것이다. 해당 데이터들은 일시적으로 사용되는 데이터들인데 Shared Buffer 영역을 차지하게 되면 정작 hit율이 많은 데이터들은 victim 버퍼 처리되며 일시적으로 Shared Buffer 영역에 올라온 버퍼들도 금방 victim 버퍼가 되어서 결국 0에서 다시 hit된 버퍼들이 올라올것이다. 결국 불필요한 동작들이 생기게 된다.

이렇게 Vacuum 또는 대규모 Seq Scan처럼 많은 데이터 페이지를 일회성으로 읽어서 처리하는 경우 Ring Buffer 라는 영역을 사용하게 된다.

해당 영역은 Shared Buffer 영역 내에 존재하는 임시 버퍼 영역이다. 일정 영역을 이용하여 순환 방식이다.

Ring Buffer 영역은 NORMAL, BULKREAD, BULKWRITE, VACUUM 4가지로 분류

NORMAL 영역 외 나머지 타입들은 ring buffer 영역을 사용하게 되며 할당되는 메모리의 크기 또한 다르다.

  1. BULK READ seq scan이 수행했는데 shared buffer의 1/4 이상인 테이블이라면 큰 테이블이라고 판단, ring buffer 영역을 사용한다. 데이터 변경이 발생하지 않아 디스크에 기록하지 않아도 되기 때문에 더 빠르게 수행된다. 해당 링 버퍼는 256kb(32페이지)의 크기로 할당된다.

  2. BULK WRITER 대량의 쓰기 작업이 발생하면 데이터를 디스크에 계속해서 기록하게 되는데 이때 링 버퍼 영역이 작다면 디스크에 기록하는 횟수가 증가/성능저하가 발생한다. 그렇기 때문에 링 버퍼 영역은 크게 할당 시킨다. 대량 쓰기의 경우 copy table, create table as select, create materialized view 의 명령어 수행시 링 버퍼를 사용한다. 할당되는 메모리의 크기는 16mb(2048 페이지)이다.

  3. VACUUM vacuum 프로세스에 의해 수행되며 테이블에 대한 전체 스캔이 발생한다. BULK READ와 같은 사이즈로 256kb를 할당받는다. 대량의 데이터를 수정해야되는 경우 페이지가 계속 수정되므로 링 버퍼는 쓸모가 없어진다.


pg_buffercache로 VACUUM 링 버퍼 직접 확인하기

이론상 VACUUM은 256KB(32페이지) 크기의 링 버퍼를 순환하며 사용한다고 했다. 이것이 실제로 그렇게 동작하는지 pg_buffercache 확장을 통해 직접 확인해볼 수 있다.

먼저 확장을 설치한다.

CREATE EXTENSION IF NOT EXISTS pg_buffercache;

1. 테이블 조회하기

VACUUM을 수행하기 전, 해당 테이블(r_t)의 페이지가 현재 Shared Buffer 영역에 몇 개나 올라와 있는지 조회한다.

SELECT count(*)
FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode('r_t'::regclass);

 count
-------
     0

아직 아무도 해당 테이블을 조회하지 않았기 때문에 버퍼에는 하나도 올라와 있지 않다. 깨끗한 상태에서 실습을 시작하는 것이다.

2. Vacuum 수행하기

vacuum r_t;
VACUUM

VACUUM이 테이블 전체를 스캔하면서 페이지를 읽어오는데, 이때 앞서 설명한 링 버퍼(32페이지)를 할당받아 그 안에서만 페이지를 순환시키며 처리한다.

3. 버퍼에 존재하는 페이지 수 조회하기

VACUUM이 끝난 후 다시 버퍼에 올라온 페이지 수를 조회한다.

SELECT count(*)
FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode('r_t'::regclass);

 count
-------
    37

0이었던 값이 37로 늘어났다. 여기서 주목할 점은 테이블 전체 크기(수천 페이지)와 무관하게 버퍼에 남은 페이지가 37개뿐이라는 것이다. VACUUM이 테이블을 통째로 Shared Buffer에 올리지 않고 링 버퍼 안에서 순환 처리했기 때문이다.

4. Relforknumber 컬럼을 기준으로 버퍼 개수 조회하기

그렇다면 이 37개는 정확히 무엇인지 fork 종류별로 나누어 조회한다.

SELECT relforknumber, count(1)
FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode('r_t'::regclass)
group by relforknumber;

 relforknumber | count
---------------+-------
             0 |    32
             1 |     4
             2 |     1

하나의 테이블은 본체(main)뿐만 아니라 여러 부속 파일(fork)로 구성되며, relforknumber가 그 종류를 구분한다.

FSM 파일에 대한 4개의 접근과 VM 파일에 대한 1개의 접근, 즉 이 5개를 뺀 값이 정확히 32개가 된다. FSM과 VM은 부속 파일이라 링 버퍼 규칙을 따르지 않고 일반적으로 버퍼에 올라간 것이고, 링 버퍼의 영향을 받는 테이블 본체(main)는 정확히 32페이지에서 멈춘 것이다.

즉 테이블이 아무리 크더라도 VACUUM이 남기는 본체 페이지는 링 버퍼 크기(256KB = 32페이지)를 넘지 않으며, 이를 통해 대량 작업이 Shared Buffer 영역을 오염시키지 않는다는 것을 실제로 확인할 수 있다.

실습 시 주의할 점

이 실습을 그대로 따라 하면 책과 같은 결과가 바로 나오지 않을 수 있다. 실제로 테스트하는 과정에서 몇 가지 함정이 있었다.

결국 32라는 숫자 자체보다 중요한 것은, 큰 테이블을 처리하더라도 본체가 링 버퍼 크기에서 멈춘다는 사실이며 이것이 곧 Shared Buffer 오염을 방지하는 링 버퍼의 핵심 동작이다.