1P by GN⁺ 2시간전 | ★ favorite | 댓글과 토론
  • Linux 7.0에서 기존 서버 기본값이던 PREEMPT_NONE 선점 모드가 제거되면서, 동일 하드웨어에서 PostgreSQL 처리량이 절반으로 급감하는 심각한 성능 회귀가 발생
  • AWS 엔지니어가 96-vCPU Graviton4 머신에서 pgbench를 실행한 결과, Linux 6.x 대비 Linux 7.0에서 초당 트랜잭션이 98,565건에서 50,751건으로 떨어지며 CPU의 55%가 단일 스핀락 함수에서 소모
  • PostgreSQL의 공유 버퍼 풀(shared buffer pool) 접근을 보호하는 스핀락이 4KB 메모리 페이지의 마이너 페이지 폴트와 결합되어, 락 보유 중 스케줄러에 의한 선점이 발생하면 대기 중인 모든 백엔드가 CPU를 낭비하며 회전
  • Huge Pages(2MB 또는 1GB)를 활성화하면 잠재적 페이지 폴트 수가 3,100만 건에서 수만~수백 건으로 감소하여 회귀 현상 해소
  • 커널 측에서는 Restartable Sequences(rseq) 채택을 제안했으나, PostgreSQL 커뮤니티는 커널 업그레이드로 인한 성능 저하 자체가 "유저스페이스를 깨뜨리지 않는다"는 원칙에 위배된다는 입장

문제 현상

  • AWS 엔지니어 Salvatore Dipietro가 96-vCPU Graviton4 프로세서에서 pgbench를 실행, scale factor 8,470(약 8억 4,700만 행 테이블), 1,024 클라이언트, 96 스레드 구성의 고병렬 부하 테스트 수행
  • Linux 6.x에서 98,565 TPS, Linux 7.0에서 50,751 TPS로 처리량이 거의 절반으로 하락
  • perf 프로파일링 결과, CPU 시간의 55.60%가 s_lock 함수 내부에서 소모
    • 호출 경로: StartReadBufferGetVictimBufferStrategyGetBuffers_lock

선점(Preemption)이란

  • OS 스케줄러가 실행 중인 스레드를 중단하고 다른 스레드에 CPU를 넘기는 결정이 선점
  • Linux 7.0 이전에는 세 가지 옵션 존재
    • PREEMPT_NONE: 스레드가 자발적으로 CPU를 양보할 때까지(syscall, I/O 블록, sleep) 거의 중단하지 않음. 전통적인 서버 기본값으로 컨텍스트 스위치가 적고 처리량이 높음
    • PREEMPT_FULL: 안전한 거의 모든 지점에서 실행 중인 스레드를 중단 가능. 응답 시간은 줄지만 컨텍스트 스위치 오버헤드 증가. 전통적인 데스크톱 기본값
    • PREEMPT_LAZY: Linux 6.12에서 도입된 절충안으로, 자연스러운 경계를 기다리면서도 필요시 선점 허용. PREEMPT_NONE의 처리량 특성을 근사하도록 설계
  • Linux 7.0에서 PREEMPT_NONE이 최신 CPU 아키텍처에서 제거되어 PREEMPT_FULL과 PREEMPT_LAZY만 남음
    • PREEMPT_LAZY가 대부분의 서버 소프트웨어에서는 대체재로 작동하지만, PostgreSQL에서는 치명적 차이 발생

PostgreSQL 메모리 관리

  • PostgreSQL은 고정 크기의 데이터 페이지(기본 8KB)를 기본 저장 단위로 사용하며, 테이블 행, B-tree 인덱스 노드, 메타데이터 등 모두 이 페이지에 저장
  • 디스크 읽기를 줄이기 위해 공유 버퍼 풀(shared buffer pool)이라는 대규모 공유 메모리 영역에 최근 읽은 데이터 페이지를 캐싱
  • 클라이언트가 접속하면 전용 백엔드 프로세스가 생성되며, 버퍼 풀에 없는 페이지는 디스크에서 읽은 뒤 빈 버퍼 또는 축출 가능한 버퍼를 찾아야 함
    • 이 버퍼 선택 작업을 담당하는 함수가 StrategyGetBuffer

PostgreSQL의 스핀락

  • 스핀락은 잠금을 기다리며 잠들지 않고 루프를 돌며 계속 확인하는 잠금 메커니즘
    • 매우 짧은 임계 구간에서는 스레드를 재우고 깨우는 비용보다 회전이 더 효율적
  • 핵심 가정: 락을 보유한 스레드가 아주 빠르게 해제할 것
  • StrategyGetBuffer는 버퍼 선택을 보호하기 위해 단일 전역 스핀락 사용
    • 96-vCPU, 1,024 클라이언트 환경에서 모든 백엔드가 동일 락을 놓고 경쟁

가상 메모리와 TLB

  • 모든 프로세스는 가상 메모리 주소를 사용하며, 하드웨어가 페이지 테이블(다단계 트리 구조)을 통해 물리 주소로 변환
  • 매번 페이지 테이블을 순회하면 느리므로, CPU는 최근 변환 결과를 캐싱하는 TLB(Translation Lookaside Buffer) 보유
    • TLB 히트 시 빠른 접근, TLB 미스 시 페이지 테이블 워크가 필요하여 시간 소요
  • Linux는 지연 할당(lazy allocation) 원칙을 사용하여, 가상 메모리 할당 시 실제 물리 페이지는 첫 접근 시점에 매핑
    • 첫 접근 시 마이너 페이지 폴트 발생: 커널이 물리 페이지를 할당하고 매핑을 저장하며, 일반 읽기/쓰기보다 수 마이크로초 단위로 느림

4KB 페이지의 문제

  • 벤치마크에서 shared_buffers를 120GB로 설정, 4KB 메모리 페이지 기준 약 3,100만 개의 메모리 페이지, 즉 3,100만 건의 잠재적 첫 접근 페이지 폴트
  • 120GB 공유 버퍼 풀을 사용하는 장시간 벤치마크에서는 새로운 메모리 영역이 계속 워킹셋에 진입하므로 페이지 폴트가 시작 시에만이 아니라 지속적으로 발생
  • StrategyGetBuffer 내에서 스핀락을 보유한 상태로 공유 메모리에 접근할 때 해당 영역이 아직 매핑되지 않았으면 마이너 페이지 폴트 발생
  • PREEMPT_NONE(Linux 7.0 이전): 백엔드 A가 페이지 폴트 핸들러에 진입해도 자발적 재스케줄링 포인트를 회피하므로, 폴트 해결 전에 스케줄 아웃될 가능성이 낮음. 대기 시간이 예상보다 길어지지만 피해 제한적
  • PREEMPT_LAZY(Linux 7.0 이후): 스케줄러가 페이지 폴트 핸들러 내부에서 백엔드 A를 선점하여 다른 프로세스를 스케줄링할 수 있음. 폴트 처리가 완료되어도 스케줄러가 제어권을 돌려줄 때까지 추가 대기 시간 t 발생
    • 이 추가 대기 시간은 단순 t가 아니라 현재 회전 중인 모든 백엔드 수 × t의 CPU 낭비로 증폭
    • 96-vCPU에 수백 개 백엔드 환경에서 이 승수 효과가 치명적이며, 결과적으로 CPU의 56%가 s_lock에서 소모

Huge Pages를 통한 해결

  • shared_buffers 120GB 기준, 메모리 페이지 크기를 변경하면 잠재적 페이지 폴트 수가 극적으로 감소
    • 4KB 페이지: ~31,000,000건의 잠재적 페이지 폴트
    • 2MB Huge Pages: ~61,440건
    • 1GB Huge Pages: ~120건
  • 페이지 크기 증가는 페이지 폴트 수 감소뿐 아니라 TLB 압력도 완화: 훨씬 적은 TLB 엔트리로 동일 메모리를 커버하므로 TLB 미스와 페이지 테이블 워크 감소
  • StrategyGetBuffer가 락 보유 중 폴트를 발생시키지 않게 되어 락 보유자가 빠르게 완료, 다른 백엔드는 밀리초 대신 마이크로초만 대기. 회귀 현상 해소
  • PostgreSQL에서 huge pages 설정은 huge_pages 파라미터로 제어
    • off, on, try(기본값) 세 가지 값 지원
    • try는 huge pages가 가능하면 사용하고 불가능하면 4KB로 조용히 폴백하므로, 잘못된 구성을 인지하지 못할 위험
    • on으로 설정하면 huge pages를 사용할 수 없을 때 PostgreSQL이 시작에 실패하여 문제를 즉시 인지 가능
  • 트레이드오프: huge pages는 사전 할당·예약 방식이므로 PostgreSQL이 전부 사용하지 않더라도 해당 메모리를 시스템 나머지 부분에서 사용 불가. 페이지 일부만 사용될 경우 나머지가 낭비. 대규모 shared_buffers를 사용하는 프로덕션 환경에서는 대체로 트레이드오프 감수 가치 있음

향후 전개

  • 선점 변경을 설계한 Intel 커널 엔지니어 Peter Zijlstra는 PostgreSQL이 Restartable Sequences(rseq) 를 채택할 것을 제안
    • rseq는 유저스페이스 코드가 임계 구간 중 선점 또는 마이그레이션 여부를 감지하고 해당 구간을 재시작할 수 있게 하는 Linux 커널 기능
    • PostgreSQL의 스핀락 경로에 rseq를 적용하면 선점된 락 보유자가 모든 대기 백엔드를 지연시키는 시나리오 회피 가능
  • PostgreSQL 커뮤니티의 반응은 부정적
    • Linux 7.0 이전에는 무료로 확보되던 성능을 되찾기 위해 별도 커널 기능을 채택해야 한다는 점이 수용하기 어려움
    • 커널의 오랜 원칙인 "유저스페이스를 깨뜨리지 않는다"(커널 업그레이드 전 정상 작동하던 소프트웨어는 업그레이드 후에도 정상 작동해야 함)에 위배된다는 입장