PostgreSQL와 OOM Killer: 엄격한 메모리 오버커밋이 필수적인 이유

PostgreSQL와 OOM Killer: 엄격한 메모리 오버커밋이 필수적인 이유

Linux OOM Killer에 대한 PostgreSQL의 취약성

단 한 번의 OOM (out of memory) 킬은 전체 PostgreSQL 클러스터를 중단시켜, 전체 데이터베이스 재시작과 크래시 복구를 강제할 수 있습니다. 이는 PostgreSQL의 공유 메모리 아키텍처 때문입니다. 메인 감독 프로세스(postmaster)는 각 연결에 대해 백엔드 프로세스를 포크(fork)하며, 이 백엔드들은 shared buffers, WAL buffers, 그리고 lock tables를 위한 핵심 메모리 세그먼트를 공유합니다.

Linux OOM killer가 메모리를 확보하기 위해 백엔드 프로세스를 종료할 때, 이러한 공유 의존성을 이해하지 못한 채 작업을 수행합니다. 만약 백엔드가 공유 메모리 세그먼트를 수정하는 동안 종료된다면, 해당 세그먼트는 불일치 상태로 남을 수 있습니다. 데이터 손상을 방지하기 위해 postmaster는 자식 프로세스의 손실을 감지하고 즉시 다른 모든 남은 백엔드들을 종료시켜 모든 활성 연결을 끊고 진행 중인 모든 트랜잭션을 중단시킵니다. 이후의 크래시 복구 과정은 특히 높은 쓰기 부하가 있을 때 상당한 다운타임을 유발할 수 있습니다.

엄격한 메모리 오버커밋: 재앙을 일상적인 오류로 전환하기

엄격한 메모리 오버커밋(vm.overcommit_memory=2)은 시스템이 메모리를 과도하게 할당하여 나중에 충돌하는 것을 허용하는 대신, 메모리 할당을 조기에 그리고 우아하게 실패하게 함으로써 PostgreSQL를 보호합니다.

Linux는 세 가지 오버커밋 정책을 제공합니다:

  • Mode 0 (Heuristic): 기본값입니다. 커널은 단일 요청이 터무니없이 크지 않는 한 대부분의 할당을 허용합니다.
  • Mode 1 (Always): 커널은 할당을 절대 거부하지 않습니다. 물리적 메모리가 부족해지면 OOM killer가 프로세스를 종료합니다.
  • Mode 2 (Strict): 커널은 총 커밋된 가상 메모리(Committed_AS)를 추적하고 CommitLimit을 강제합니다. 이 한도를 초과하는 모든 할당은 즉시 ENOMEM 오류와 함께 거부됩니다.

PostgreSQL의 경우, ENOMEM은 일상적인 오류입니다. 메모리를 할당할 수 없는 백엔드는 클라이언트에게 오류를 보고하고 트랜잭션을 취소하지만, postmaster와 다른 모든 연결은 영향을 받지 않습니다. 이는 시스템 전체의 장애를 국소적이고 관리 가능한 오류로 전환합니다.

사례 연구: "Phantom Memory" 커널 버그

Linux 6.5 커널의 미묘한 버그로 인해 Committed_AS가 누수되어, 물리적 메모리가 풍부함에도 불구하고 잘못된 OOM 오류가 발생했습니다.

Ubicloud는 일부 8 GB 서버에서 650 GB 이상의 커밋된 메모리가 보고된다는 것을 발견했습니다. 분석 결과, 커널 6.5.0을 실행 중인 서버는 6.8.0을 사용하는 서버보다 이러한 메모리 팽창을 경험할 확률이 52배 더 높았으며, 누수는 매주 약 4.7%의 복리 형태로 증가했습니다.

근본 원인

이 버그는 커밋 408579c에서 도입되었으며, mm/mremap.c 내의 move_vma() 함수 내의 단 한 글자 변경에 집중되어 있었습니다. do_vmi_munmap()에 대한 오류 체크가 < 0 (오류 시 실행)에서 ! (성공 시 실행)로 변경되었습니다.

조건이 반전되었기 때문에, 커널은 실패할 때만 커널의 커밋된 메모리 카운터를 증가시키는 대신, 모든 성공적인 메모리 remap 작업에 대해 커널의 커밋된 메모리 카운터를 재증가시켰습니다. 이로 인해 Committed_AS가 시간이 지남에 따라 단조적으로 증가하게 되었습니다. 이 버그는 기본 휴리스틱 오버커밋 모드에서는 숨겨져 있었습니다. 커널이 Mode 0에서 할당을 제어하기 위해 Committed_AS를 사용하지 않기 때문입니다; 이는 엄격한 오버커밋(Mode 2)을 활성화했을 때만 실패를 유발합니다.

커밋 한도(Commit Limit) 설정을 위한 휴리스틱

엄격한 오버커밋을 안전하게 구현하려면, 커밋 한도는 커널 오버헤드와 사이드카 프로세스를 모두 고려해야 합니다.

Ubicloud는 overcommit_kbytes를 계산하기 위해 다음 공식을 사용합니다:

overcommit_kbytes = (total_memory_kb * 0.75 * 0.8) + 2 * 1048576

계산 방식 상세 설명

  • 80% 규칙: 사용 가능한 메모리의 80%를 유저스페이스에 커밋합니다. 나머지 20%는 커널 데이터 구조(page tables, slab caches, network buffers)를 위해 예약합니다. 이 20%는 낭비되는 것이 아닙니다; 이는 여전히 page cache를 위해 사용되며, page cache는 회수 가능하므로 Committed_AS에 포함되지 않아 PostgreSQL의 읽기 성능을 향상시킵니다.
  • 2 GB 버퍼: 사이드카 프로세스(예: Prometheus, node_exporter, wal-g)를 수용하기 위해 고정된 2 GB 오프셋을 추가합니다. 이들 중 상당수는 Go로 작성되었으며, Go는 사전에 대형 가상 메모리 영역을 예약합니다. Ubicloud의 데이터에 따르면 서버 사이드카들이 커밋된 메모리 1 GB 미만을 소비하는 경우가 96%였으므로, 2 GB는 안전하고 넉넉한 상한선입니다.
  • Hugepage 조정: 제공된 구현에서, total memory는 먼저 0.75를 곱하여 hugepages를 위해 예약된 25%의 메모리를 고려합니다. hugepages는 별도의 계정 방식으로 처리되며 커밋 한도에 포함되지 않습니다.

구현 요약

프로덕션 PostgreSQL 배포 환경에서는 치명적인 OOM 킬을 피하기 위해 vm.overcommit_memory=2를 활성화하는 것이 권재장됩니다. 하지만, 이는 워크로드의 메모리 특성을 모니터링하여 CommitLimit이 너무 낮게 설정되어 빈번하고 불필요한 ENOMEM 오류를를 유발하지 않도록 확인한 후에 수행해야 합니다.

Sources