# Linux 7.0이 PostgreSQL을 망가뜨린 방법

> Clean Markdown view of GeekNews topic #29142. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=29142](https://news.hada.io/topic?id=29142)
- GeekNews Markdown: [https://news.hada.io/topic/29142.md](https://news.hada.io/topic/29142.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-05-04T10:25:02+09:00
- Updated: 2026-05-04T10:25:02+09:00
- Original source: [read.thecoder.cafe](https://read.thecoder.cafe/p/linux-broke-postgresql)
- Points: 2
- Comments: 1

## Topic Body

- 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` 함수** 내부에서 소모  
  - 호출 경로: `StartReadBuffer` → `GetVictimBuffer` → `StrategyGetBuffer` → `s_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 이전에는 무료로 확보되던 성능을 되찾기 위해 별도 커널 기능을 채택해야 한다는 점이 수용하기 어려움  
  - 커널의 오랜 원칙인 **"유저스페이스를 깨뜨리지 않는다"**(커널 업그레이드 전 정상 작동하던 소프트웨어는 업그레이드 후에도 정상 작동해야 함)에 위배된다는 입장

## Comments



### Comment 56804

- Author: cafedead
- Created: 2026-05-04T13:31:03+09:00
- Points: 1

"유저스페이스를 깨뜨리지 마라" vs "유저스페이스에서 스핀락 하지마라"
