대형 페이지(huge pages)는 Postgres의 공유 버퍼를 관리하는 데 필요한 페이지 테이블 메모리를 연결 수에 따라 크게 늘리지 않도록 돕는다. ClickHouse Managed Postgres는 서버의 버퍼 캐시를 대형 페이지에서 실행하며, 이를 안정적으로 적용하려면 메모리 예약과 시작 시 확인, 공유 메모리 크기 조정이 필요하다.

운영체제(OS)는 메모리를 4KB 단위 페이지로 제공하고, CPU는 가상 주소와 물리 주소의 변환 정보를 변환 색인 버퍼(TLB)에 저장한다. 대형 페이지는 2MB 또는 1GB 단위로 메모리를 관리하므로 페이지 테이블 항목 하나가 최대 262,144배 넓은 영역을 다룬다.

Postgres는 페이지 크기에 특히 민감하다. 캐시 역할을 하는 shared_buffers는 하나의 큰 공유 메모리 세그먼트이며, 각 백엔드 프로세스는 각자의 페이지 테이블을 통해 이 메모리를 매핑한다. 4KB 페이지를 쓸 때 캐시가 100GB라면 백엔드 하나마다 페이지 테이블 항목에 약 200MB가 들고, 연결 100개에서는 페이지 테이블만으로 20GB를 사용한다. 2MB 대형 페이지를 쓰면 필요한 메모리는 수십 MB 수준이다.

TLB에는 변환 정보가 수천 개 저장되므로 4KB 페이지에서는 캐시 중 몇 MB만 다룰 수 있다. 2MB 페이지 하나는 TLB 항목 하나가 담당하는 영역을 512배 넓힌다. 따라서 자주 쓰는 데이터가 TLB에 머물고, 페이지 테이블을 따라가느라 읽기가 지연되는 일을 줄일 수 있다.

대형 페이지를 안정적으로 적용하는 방법

ClickHouse Managed Postgres는 컴퓨터 메모리의 4분의 1을 대형 페이지용으로 예약한다. 기본 설정에서 shared_buffers를 RAM의 25%로 두는 규칙에 맞춘 것이다. Postgres를 시작하기 전에 페이지를 예약하며, 커널이 연속된 2MB 블록을 찾을 수 있도록 먼저 캐시를 비우고 메모리를 압축한다.

1echo 'vm.nr_hugepages = ' > /etc/sysctl.d/10-hugepages.conf 2sync 3echo 3 > /proc/sys/vm/drop_caches 4echo 1 > /proc/sys/vm/compact_memory 5sysctl --systemCopy command

예약 결과는 /proc/meminfo의 HugePages_Total 값으로 확인하고, 커널이 필요한 수량을 확보하지 못하면 프로비저닝을 중단한다. 메모리가 조각나지 않은 상태에서만 대형 페이지를 안정적으로 예약할 수 있어, 부하를 받은 컴퓨터에서는 같은 요청이 자주 실패한다.

Postgres 설정은 huge_pages = 'on'으로 고정한다. 페이지를 확보하지 못하면 시작을 거부하기 때문에 문제를 확인할 수 있다. 기본값인 'try'는 대형 페이지를 쓰지 못해도 4KB 페이지로 조용히 전환하며 로그에도 남기지 않는다. 이 경우 대형 페이지의 이점이 사라지고, 문제가 몇 달 뒤 프로파일러를 통해 발견될 수 있다.

실행 중에도 확인할 수 있다. postmaster의 /proc/<pid>/status에는 주소 공간 중 hugetlb 페이지를 사용하는 크기가 기록된다. 예시 서버의 출력은 다음과 같다.

1$ grep HugetlbPages /proc//status 2HugetlbPages: 700416 kBCopy command

대형 페이지를 사용해야 하는 서버에서 HugetlbPages: 0이 나오면 4KB 페이지로 전환된 것이다.

공유 메모리 세그먼트는 shared_buffers보다 크다. 버퍼 설명자와 WAL 버퍼, 잠금 테이블도 포함하기 때문이다. 예시 서버에서 shared_buffers = 32GB를 설정하면 세그먼트 크기는 32.75GB가 되며, 약 750MB가 추가로 필요하다. shared_buffers만 예약 풀을 가득 채우도록 설정하면 전체 세그먼트가 들어가지 않아 huge_pages = 'on' 상태에서 서버가 시작되지 않는다.

크기는 두 단계로 맞춘다. 먼저 shared_buffers를 예약 풀 전체로 설정한 뒤, 서버를 시작하지 않고 Postgres 실행 파일에 전체 세그먼트의 실제 크기를 조회한다.

1postgres -D -C shared_memory_sizeCopy command

그다음 전체 크기와 예약 풀의 차이만큼 shared_buffers를 줄이고, 8KB 블록 단위로 내림해 결과를 기록한다.

1 2huge_pages = 'on' 3huge_page_size = 0 4shared_buffers = kBCopy command

오버헤드는 버전과 설정에 따라 달라지므로 추정하지 않고 측정한다. 더 큰 크기에서 측정하면 예약량보다 몇 페이지 적게 맞춰진다. 대형 페이지는 커널의 커밋 한도에 포함되지 않으므로, 예약 풀과 나머지 메모리도 따로 계산한다. 메모리 오버커밋은 RAM의 나머지 75%를 기준으로 조정한다.

실제 하드웨어에서 측정한 결과

페이지 테이블에 관한 주장을 확인하기 위해 r7i.4xlarge(16 vCPU, 128GB) 한 대에서 Postgres 16과 shared_buffers = 32GB를 사용했다. pgbench 데이터셋의 15.6GB 계정 테이블을 pg_prewarm으로 캐시에 전부 적재했다. 각 연결은 해당 테이블 전체를 순차적으로 한 번 훑어 캐시된 모든 페이지를 백엔드가 매핑하게 한 뒤 열린 상태를 유지했다. 4KB 페이지와 2MB 대형 페이지를 각각 사용해 연결 수를 25, 50, 100, 200개로 바꾸며 /proc/meminfo의 PageTables 값을 측정했다. 컴퓨터와 데이터, 접근 방식은 같고 페이지 크기만 달랐다.

4KB 페이지에서는 연결 하나가 늘 때마다 페이지 테이블이 31.1MB씩 증가했다. 이는 계산값인 31.2MB(캐시에서 접근한 15.6GB / 4KB 페이지 × 항목당 8바이트)에 부합한다. 연결 200개에서는 페이지 테이블이 6.1GB에 달했다. 2MB 대형 페이지에서는 같은 연결 수에서 111MB, 연결 하나당 약 0.5MB를 사용했다.

연결 수4KB 페이지2MB 대형 페이지
048 MB5 MB
25789 MB18 MB
501.53 GB31 MB
1003.06 GB58 MB
2006.12 GB111 MB

huge_pages = 'on' 설정은 예약 풀이 없을 때도 동작이 분명하다. Postgres는 시작을 거부하고 이유를 출력한다.

1FATAL: could not map anonymous shared memory: Cannot allocate memory 2HINT: This error usually means that PostgreSQL's request for a shared 3memory segment exceeded available memory, swap space, or huge pages.Copy command

처리량도 달라졌지만 메모리 차이만큼 크지는 않았다. 읽기 전용 pgbench에서 클라이언트 100개를 사용했을 때 4KB 페이지는 초당 373,083건, 2MB 대형 페이지는 초당 418,087건을 처리했다. 같은 컴퓨터에서 차이는 12%였다. 구조적으로 큰 차이는 연결 수에 따라 늘어나는 페이지 테이블과 거의 일정하게 유지되는 페이지 테이블 사이에서 나타났다.

대형 페이지를 사용하면 shared_buffers를 연결마다 드는 페이지 테이블 비용에서 분리해, 주소 변환에 유리한 캐시로 운용할 수 있다. 운영 환경에서도 설정이 유지되려면 메모리가 조각나기 전에 페이지를 예약하고, huge_pages = 'on'으로 설정해 잘못된 구성이 조용히 성능을 떨어뜨리는 대신 시작 단계에서 실패하도록 해야 한다. 또 Postgres 실행 파일이 보고하는 실제 크기를 기준으로 공유 메모리 세그먼트를 예약량에 맞춰야 한다.