본문으로 건너뛰기
피드

PostgreSQL에서 OOM 킬러가 위험한 이유와 리눅스 커널의 한 글자 버그

backend 약 9분
vote
0
댓글
북마크

Ubicloud가 PostgreSQL에 엄격한 메모리 오버커밋을 쓰는 이유를 설명하다가, 리눅스 6.5 커널의 Committed_AS 누수 버그를 추적한 사례를 공개했음. 8GB 서버에서 651GB가 커밋된 것처럼 보이는 현상이 있었고, 원인은 mremap 경로의 조건문 하나가 잘못 바뀐 커널 버그였음.

  • 1

    PostgreSQL은 백엔드 프로세스 하나가 OOM으로 죽어도 공유 메모리 손상 가능성 때문에 전체 연결을 끊고 복구에 들어감

  • 2

    Ubicloud는 엄격한 오버커밋을 쓰다 8GB 서버에서 651GB Committed_AS가 찍히는 이상 현상을 발견함

  • 3

    리눅스 6.5의 mremap 관련 조건문이 < 0에서 !로 잘못 바뀌며 성공 경로에서도 메모리 회계가 되돌려지는 버그가 생겼음

PostgreSQL은 OOM 킬러를 그냥 맞고 재시작하면 되는 프로그램이 아님

  • 리눅스는 기본적으로 실제 물리 메모리보다 더 많은 가상 메모리 할당을 허용함

    • malloc() 같은 호출이 들어오면 일단 가상 주소 공간을 예약하고, 프로세스가 실제로 그 메모리를 만질 때 물리 페이지를 붙임
    • “모든 프로세스가 할당한 메모리를 동시에 다 쓰진 않을 것”이라는 가정이 깨지면 OOM killer가 프로세스를 하나 죽임
  • 일반 앱은 OOM으로 죽어도 재시작하면 되는 경우가 많지만, PostgreSQL은 다름

    • Postgres의 postmaster는 연결마다 백엔드 프로세스를 fork하고, 이 백엔드들이 shared buffers, WAL buffers, lock table 같은 공유 메모리를 같이 씀
    • OOM killer는 이런 구조를 이해하지 못하고 휴리스틱으로 프로세스 하나를 골라 죽임
    • 만약 죽은 백엔드가 공유 메모리를 수정 중이었다면 OS 레벨에서는 그 상태를 트랜잭션처럼 되돌려주지 않음
  • 그래서 Postgres는 자식 프로세스 하나가 비정상 종료돼도 최악을 가정함

    • 공유 메모리가 깨졌을 수 있으니 남은 백엔드를 모두 종료하고, 모든 연결을 끊고, 진행 중이던 트랜잭션을 abort함
    • 다음 시작 때 crash recovery를 돌면서 WAL을 재생함
    • 쓰기량이 많았으면 WAL replay가 길어져서 단순 OOM 한 번이 긴 장애로 이어질 수 있음

⚠️주의

> PostgreSQL에서 OOM kill은 “커넥션 하나 죽음”이 아니라 “서버 전체 연결 끊김 + crash recovery”로 번질 수 있음. 데이터 보호를 위한 올바른 동작이지만, 운영 영향은 꽤 큼.

strict overcommit을 켰더니 8GB 서버에 651GB 유령 메모리가 찍힘

  • Ubicloud는 PostgreSQL에 strict memory overcommit을 선호해왔음

    • 이유는 나중에 OOM killer가 랜덤하게 백엔드를 죽이는 것보다, 애초에 위험한 메모리 할당을 거절하는 편이 데이터베이스에는 낫기 때문임
    • 그런데 이번에 strict overcommit을 켠 뒤, 물리 메모리가 충분한 서버에서도 out of memory 오류가 나기 시작함
  • 단서가 된 값은 /proc/meminfo의 Committed_AS였음

    • 8GB 메모리 서버에서 Committed_AS가 683,547,672KB, 즉 약 651GB로 찍힘
    • 같은 크기의 정상 서버는 2,703,940KB, 약 2.6GB 수준이었음
    • 실제 accountable memory를 /proc/*/smaps에서 합산해보니 2.43GB뿐이라, 약 648GB가 말 그대로 유령 커밋 메모리였음
  • 처음 의심한 건 PostgreSQL의 shared_buffers와 huge pages였음

    • 각 백엔드 프로세스의 VSZ가 약 2GB로 보였고, 이는 8GB VM에서 설정한 2GB shared_buffers와 거의 같았음
    • 하지만 각 백엔드는 같은 2GB 공유 메모리 세그먼트를 자기 주소 공간에 매핑할 뿐이고, 물리적으로는 한 번만 존재함
    • 게다가 huge_pages=on이라 hugetlb 매핑은 Committed_AS에 잡히면 안 됨
  • VMA 플래그 확인으로 huge pages 가설은 탈락함

    • /proc//smaps에서 hugepage 매핑의 VmFlags를 봤더니 VM_ACCOUNT를 뜻하는 ac 플래그가 없었음
    • 즉 huge pages는 정상적으로 committed memory accounting에서 빠지고 있었음
    • 남은 가설은 커널의 vm_committed_as 카운터가 allocation 때 증가하고 해제 때 제대로 줄지 않는다는 쪽이었음

범인은 리눅스 6.5의 한 글자 조건문 버그였음

  • fleet 전체를 보니 커널 버전 차이가 확실하게 드러남

    • 커널 6.5.0 서버는 Committed_AS / MemTotal 비율 평균이 24.97이었고, 최대는 3,405까지 튐
    • 커널 6.8.0 서버는 평균 0.32, 최대 1.86 수준이었음
    • 비율이 1.0을 넘는 서버도 6.5에서는 23%, 6.8에서는 1% 미만이었음
    • 통계적으로 6.5 서버가 committed memory inflation을 겪을 가능성이 52배 높았다고 함
  • 버그는 리눅스 6.5의 commit 408579c에서 들어갔음

    • do_vmi_align_munmap()의 반환 규칙이 “0은 성공, 1은 lock downgraded 성공, 음수는 에러”에서 “성공은 항상 0, 음수는 에러”로 바뀜
    • 호출부들을 고치는 과정에서 mm/mremap.c의 move_vma() 조건문이 잘못 바뀜
    • 원래는 do_vmi_munmap() 결과가 < 0일 때만 에러 처리로 vm_acct_memory()를 되돌려야 했음
  • 문제의 핵심은 < 0이 !로 바뀐 한 글자급 실수였음

    • 올바른 조건은 “unmap 실패 시에만 메모리 회계를 복구”하는 것임
    • 깨진 조건은 “unmap 성공 시에도 메모리 회계를 복구”해버림
    • 결과적으로 mremap이 성공할 때마다 Committed_AS가 단조 증가했고, strict overcommit에서는 시간이 지나 CommitLimit을 넘으며 정상 할당까지 실패하게 됨

중요

> 이 버그는 기본 overcommit 모드에서는 Committed_AS가 주로 참고용이라 잘 안 보였음. strict overcommit처럼 커널이 이 값을 실제 할당 판단에 쓰는 환경에서만 몇 주 뒤 장애로 터지는, 딱 운영자 괴롭히는 타입의 버그임.

  • Linus Torvalds가 원인 분석 후 조건문을 다시 < 0으로 고치면서 해결됐음
    • 실제 VM 동작은 거의 안 바뀌고, VM_ACCOUNT가 붙은 VMA의 메모리 회계만 틀어지는 미묘한 버그였다고 설명함
    • 시스템은 계속 “동작하는 것처럼” 보이지만 Committed_AS가 점점 이상해지고, 나중에 VM 휴리스틱이나 strict overcommit에서 문제가 터지는 구조였음

기술 맥락

  • PostgreSQL에서 strict overcommit을 쓰는 이유는 OOM killer에게 결정을 맡기면 데이터베이스 입장에서 너무 위험하기 때문이에요. 백엔드 하나만 죽어도 공유 메모리 일관성을 보장할 수 없어서 전체 연결을 끊고 복구해야 하거든요.

  • Committed_AS는 단순 모니터링 숫자처럼 보이지만, strict overcommit에서는 커널이 새 메모리 할당을 허용할지 판단하는 기준이 돼요. 그래서 8GB 서버에 651GB가 커밋됐다고 잘못 찍히면, 실제 메모리가 남아 있어도 할당이 실패할 수 있어요.

  • Ubicloud가 huge pages를 먼저 의심한 건 합리적인 접근이에요. PostgreSQL 백엔드마다 2GB shared_buffers 매핑이 VSZ에 반복해서 보였고, hugetlb는 커널 메모리 회계에서 특수한 경로를 타기 때문이에요.

  • 결정적인 포인트는 /proc//smaps의 VMA 플래그였어요. hugepage 매핑에 ac 플래그가 없다는 건 해당 영역이 committed memory accounting 대상이 아니라는 뜻이라, 원인을 PostgreSQL 설정이 아니라 커널 카운터 누수 쪽으로 좁힐 수 있었어요.

  • 이 사례가 무서운 건 실제 메모리 관리 동작은 정상인데 회계 숫자만 틀어진다는 점이에요. 기본 설정에서는 조용히 숨어 있다가, strict overcommit처럼 더 안전한 운영 정책을 쓰는 곳에서만 장애로 드러났거든요.

데이터베이스 운영에서 “리눅스 기본값이면 되겠지”가 얼마나 위험할 수 있는지 보여주는 글임. 특히 Postgres, OOM killer, overcommit, huge pages가 한 번에 얽히는 케이스라 운영자 입장에서는 꽤 저장해둘 만함.

댓글

댓글

댓글을 불러오는 중...

backend

유니스와프 ‘듀얼풀 훅’, 비활성 자산에서도 수익을 만들 수 있을까

유니스와프가 듀얼풀 훅 감사를 진행 중이며 완료 후 오픈소스로 공개할 계획이라고 전했다. 이 기능은 활성 자산과 비활성 자산 모두에서 수익 전략을 만들 수 있다는 점에서 DeFi 유동성 구조 변화로 언급되고 있다.

backend

EDB, 포스트그레스 AI에 자율 운영 DB와 실시간 분석 기능 추가

EDB가 포스트그레스 기반 AI 데이터 플랫폼에 에이전틱 데이터베이스와 통합 분석 기능을 추가했다. 데이터 이동 없이 관계형·분석·벡터·에이전트 워크로드를 한 플랫폼에서 처리하고, 운영 지표 200개 이상을 모니터링해 튜닝과 문제 대응을 자동화하는 방향이다.

backend

EDB 포스트그레스 AI, 데이터베이스가 튜닝하고 에이전트 권한까지 잡는 쪽으로 간다

EDB가 포스트그레스 기반 AI 데이터 플랫폼에 에이전틱 데이터베이스, 통합 분석, 거버넌스 기능을 추가했다. 200개 이상 운영 지표를 모니터링해 튜닝과 문제 대응을 자동화하고, 관계형·분석·벡터 데이터를 단일 SQL 레이어에서 다루겠다는 전략이다.

backend

Databricks가 Postgres 데이터를 S3의 Parquet로 저장해 트랜잭션과 분석을 합치려는 방식

Databricks가 Lakebase와 LTAP 아키텍처를 설명하며, Postgres의 WAL과 데이터 파일을 외부 서비스로 분리하고 운영 데이터는 Parquet 기반 오픈 포맷으로 저장하는 접근을 소개했음. 목표는 CDC나 미러링 없이 하나의 데이터 사본으로 트랜잭션과 분석을 동시에 처리하는 것임.

backend

ORM이 결국 알려준 것: 그냥 SQL을 배우는 게 낫다

ORM은 단순한 CRUD에서는 편하지만, 테이블이 넓어지고 조인이 많아지는 순간 추상화 비용이 확 튄다는 글이다. 저자는 Hibernate 같은 ORM을 쓰면서 SELECT *급 쿼리, 과한 객체 변환, 트랜잭션 경계, 마이그레이션 문제를 겪고 결국 쿼리를 데이터베이스 API처럼 보는 쪽으로 생각이 바뀌었다고 말한다.