데이터베이스 워크로드와 Linux 커널

데이터베이스 워크로드를 Linux 커널의 I/O, 메모리, 스케줄링 정책과 연결해 심층 분석합니다. fsync·writeback·page cache·direct I/O 선택이 트랜잭션 지연과 내구성에 미치는 영향, io_uring/AIO와 큐 깊이 튜닝, cgroup 기반 리소스 격리, NUMA·THP·swap 정책이 쿼리 처리량에 주는 효과, 파일시스템 및 블록 계층 정렬 이슈, 장애 시 복구 시간 단축을 위한 커널 파라미터 전략, perf/eBPF/ftrace 기반 병목 분석 절차까지 DBA와 커널 엔지니어 협업에 필요한 실전 포인트를 다룹니다.

전제 조건: io_uring메모리 관리 기초 문서를 먼저 읽으세요. DB 워크로드는 메모리·스토리지·스케줄링 경로를 동시에 자극하므로, 병목을 단일 계층이 아니라 종단 경로로 보는 것이 중요합니다.
일상 비유: 이 주제는 대형 매장 계산대/재고 동시 운영과 비슷합니다. 앞단 응답 속도와 뒷단 재고 처리량을 같이 맞춰야 하듯이, 커널 튜닝도 경로 전체를 동시에 봐야 효과가 납니다.

핵심 요약

  • Page Cache — 파일 I/O를 메모리에 완충해 읽기 성능을 높입니다.
  • fsync — 트랜잭션 로그를 영속화해 장애 시 복구 기준점을 만듭니다.
  • O_DIRECT — 페이지 캐시를 우회해 DB 버퍼풀과 이중 캐시를 줄입니다.
  • Writeback — dirty 페이지를 디스크로 밀어내며 지연 분포에 영향을 줍니다.
  • NUMA Locality — CPU와 메모리 노드 배치가 tail latency를 좌우합니다.

단계별 이해

  1. I/O 경로 구분
    DB 파일이 buffered I/O인지 direct I/O인지 먼저 확인합니다.
  2. 영속성 지점 확인
    WAL 로그 flush 경로에서 fsync 빈도와 그룹 커밋을 파악합니다.
  3. 메모리/CPU 배치 점검
    NUMA 바인딩과 cgroup 제한이 캐시 효율을 깨지 않는지 확인합니다.
  4. 관측 후 조정
    perf, iostat, PSI, eBPF로 병목 위치를 확인한 뒤 한 번에 하나씩 조정합니다.

DB와 커널이 만나는 핵심 지점

DB 워크로드에서 커널 영향이 가장 큰 경계면은 다음 여섯 가지입니다.

트랜잭션 I/O 지연 경로

아래 다이어그램은 단순화한 commit 경로입니다. DB 엔진의 WAL 기록이 커널 계층을 통과하면서 어느 지점에서 tail latency가 증가하는지 파악할 수 있습니다.

DB 엔진 WAL / Buffer Pool VFS / Page Cache dirty page 생성 Filesystem ext4 / XFS journal blk-mq queue dispatch NVMe flush/FUA 병목 후보: reclaim / writeback throttle 병목 후보: fsync 배치, flush 순서, 큐 깊이

fsync, 저널링, 장애 복구 관점

관계형 DB는 일반적으로 WAL 선기록 규칙을 사용합니다. 즉, 데이터 페이지보다 로그 페이지가 먼저 영속화되어야 합니다.

/* 단순화한 커밋 경로 예시 */
ssize_t n = pwrite(wal_fd, wal_buf, wal_len, wal_off);
if (n < 0) return -1;

/* 그룹 커밋 구간에서 fsync 호출 */
if (fdatasync(wal_fd) < 0)
    return -1;

/* 체크포인트 시점에 데이터 파일 flush */
if (fsync(data_fd) < 0)
    return -1;

실무 포인트: fdatasync()fsync()의 차이를 정확히 알고, 파일시스템 마운트 옵션과 장치 write cache 정책을 함께 점검해야 합니다.

DB 엔진별 I/O 모델 차이

같은 커널에서도 DB 엔진의 저장 구조가 다르면 병목 지점이 달라집니다. 먼저 엔진의 기본 I/O 모델을 분류해야 튜닝 방향이 정해집니다.

엔진/구성주요 파일 I/O커널 관점 병목우선 점검 항목
PostgreSQL (기본)WAL append + 데이터 파일 random writefsync 묶음 크기, checkpoint spikecheckpoint_timeout, max_wal_size, writeback 패턴
MySQL InnoDBredo log + doublewrite + data fileflush 순서, doublewrite 오버헤드innodb_flush_log_at_trx_commit, innodb_io_capacity
RocksDB/LSM 계열WAL + SSTable compaction백그라운드 compaction이 foreground 지연을 오염compaction 스레드 격리, cgroup I/O 제한
분석계(컬럼 저장)대량 순차 read + 배치 writereadahead 과다/부족, 페이지 캐시 오염readahead 크기, 쿼리/배치 작업 분리

Buffered I/O vs O_DIRECT

DB에서 가장 큰 전략 선택은 페이지 캐시를 사용할지 여부입니다. 정답은 하나가 아니라 워크로드의 갱신 패턴과 복구 정책에 따라 달라집니다.

항목Buffered I/OO_DIRECT
캐시 위치커널 page cache + DB 버퍼풀주로 DB 버퍼풀
장점재읽기 hit 시 낮은 지연, 구현 단순이중 캐시 감소, 메모리 사용 예측 쉬움
단점메모리 압력 시 reclaim 지연 급증정렬 제약, 작은 I/O에서 손해 가능
주요 장애 패턴dirty 폭증 후 writeback stall큐 깊이/정렬 미스에 따른 IOPS 저하
# 프로세스가 O_DIRECT를 실제 사용하는지 간단 점검
strace -f -e openat,pread64,pwrite64 -p <DB_PID>

# 파일 단위 캐시 성향 힌트 (테스트 환경)
vmtouch -v /path/to/dbfile
주의: O_DIRECT를 선택해도 WAL flush의 영속성 비용은 남아 있습니다. O_DIRECT는 주로 캐시/메모리 경합을 줄이는 전략이지, fsync 비용을 제거하는 전략이 아닙니다.

dirty/writeback 제어와 체크포인트 스파이크

DB tail latency 급등은 커널 dirty 페이지 제어와 체크포인트 타이밍이 겹칠 때 자주 발생합니다.

# 현재 dirty/writeback 설정
sysctl vm.dirty_background_ratio vm.dirty_ratio
sysctl vm.dirty_background_bytes vm.dirty_bytes
sysctl vm.dirty_writeback_centisecs vm.dirty_expire_centisecs

# 실무에서는 ratio보다 bytes 기반 고정이 재현성이 높을 때가 많음
sysctl -w vm.dirty_background_bytes=268435456
sysctl -w vm.dirty_bytes=1073741824

튜닝 원칙은 단순합니다. WAL flush 경로는 짧고 자주, 데이터 파일 writeback은 완만하고 예측 가능하게 유지해야 합니다.

WAL flush 빈도 높음 짧은 지연 목표 데이터 파일 dirty 누적 checkpoint 전후 급변 가능 writeback burst p99, p999 급등 완화 전략: 체크포인트 간격/목표 시간 조정 + dirty_bytes 고정 + 백그라운드 I/O 격리 핵심은 \"짧은 fsync\"와 \"완만한 writeback\" 분리

ext4/XFS 선택과 마운트 고려사항

ext4와 XFS 모두 DB 운용이 가능하지만, 파일 할당 정책과 메타데이터 동작 차이로 지연 분포가 달라질 수 있습니다.

항목ext4XFS
강점운영 경험 풍부, 보수적 동작큰 파일/병렬 I/O에서 높은 확장성
주의점마운트 옵션/저널 모드 영향 큼메타데이터 작업 부하에서 특성 차이
DB 관점예측 가능한 안정성 선호 환경고동시성 쓰기/대용량 환경
# 장치 쓰기 캐시 정책과 flush 관련 정보 확인
lsblk -o NAME,ROTA,SCHED,DISC-MAX,DISC-GRAN,WSAME
cat /sys/block/nvme0n1/queue/write_cache
cat /sys/block/nvme0n1/queue/scheduler

메모리/CPU/NUMA 관점의 DB 성능

권장 접근: 먼저 고정된 CPU/NUMA 배치에서 기준 성능을 만들고, 그 다음 I/O 튜닝을 진행하세요. 동시에 여러 계층을 바꾸면 원인 분리가 어렵습니다.
# NUMA 배치 점검
numactl --hardware
numastat -p <DB_PID>

# THP 상태 확인
cat /sys/kernel/mm/transparent_hugepage/enabled
cat /sys/kernel/mm/transparent_hugepage/defrag

관측: 무엇을 먼저 볼 것인가

# 블록 지연과 큐 상태
iostat -x 1

# 메모리 압력과 reclaim 징후
cat /proc/pressure/memory
vmstat 1

# DB 프로세스의 major fault, context switch 추적
perf stat -p <DB_PID> -e page-faults,major-faults,context-switches,cpu-migrations sleep 10

# 파일시스템 sync 관련 이벤트 샘플링
sudo perf trace -e fsync,fdatasync -p <DB_PID>

관측 지표는 반드시 DB 지표(QPS, TPS, p95/p99, checkpoint 시간)와 같은 타임라인으로 비교해야 의미가 있습니다.

cgroup v2 격리 전략

DB 본체와 백업/ETL/분석 배치를 같은 호스트에서 돌릴 때는 cgroup v2로 I/O와 메모리 압력을 분리해야 tail latency를 지킬 수 있습니다.

# 예시: DB 서비스 cgroup에 최소 보호, 배치 cgroup에 상한 적용
mkdir -p /sys/fs/cgroup/db /sys/fs/cgroup/batch

# DB는 메모리 high를 높게, 배치는 낮게
echo 64G > /sys/fs/cgroup/db/memory.high
echo 8G  > /sys/fs/cgroup/batch/memory.high

# 배치 작업 I/O 상한(예: 200MB/s) - DB 마운트 기준 디바이스를 동적으로 계산
DB_MNT=/var/lib/postgresql          # MySQL이면 /var/lib/mysql 등으로 변경
DEV=$(findmnt -n -o SOURCE --target \"$DB_MNT\")
MAJMIN=$(lsblk -no MAJ:MIN \"$DEV\" | head -n 1)
echo \"$MAJMIN rbps=max wbps=209715200 riops=max wiops=max\" > /sys/fs/cgroup/batch/io.max

재현 가능한 벤치마크 절차

  1. 기준선 확정: 커널 버전, DB 버전, 파일시스템, 마운트 옵션, 장치 펌웨어를 기록합니다.
  2. 웜업 구간 분리: 캐시 웜업과 측정 구간을 분리해 초기 편차를 제거합니다.
  3. 단일 변수 변경: 한 번에 한 항목만 변경하고 최소 3회 반복 측정합니다.
  4. 분포 지표 기록: 평균이 아니라 p95/p99/p999와 최대 복구 시간(RTO)을 함께 기록합니다.
  5. 회귀 기준 고정: 동일한 데이터셋, 동일한 쿼리 믹스, 동일한 동시성으로 비교합니다.
# fio로 WAL 유사 패턴(작은 append + fsync) 근사
fio --name=wal --filename=/data/wal.test --rw=write --bs=8k \
    --ioengine=psync --fdatasync=1 --iodepth=1 --numjobs=1 \
    --runtime=60 --time_based=1 --group_reporting=1

# 데이터 파일 유사 패턴(random read/write 혼합)
fio --name=data --filename=/data/data.test --rw=randrw --rwmixread=70 \
    --bs=16k --ioengine=io_uring --iodepth=64 --numjobs=8 \
    --runtime=120 --time_based=1 --group_reporting=1

PostgreSQL/MySQL 실전 설정 플레이북

아래 값은 절대값이 아니라 안전한 시작점입니다. 장치 성능, 동시성, 장애 복구 목표(RPO/RTO)에 맞춰 단계적으로 조정해야 합니다.

PostgreSQL 시작점

# postgresql.conf (예시 시작값)
shared_buffers = 16GB
effective_cache_size = 48GB
work_mem = 32MB
maintenance_work_mem = 2GB

wal_level = replica
wal_compression = on
max_wal_size = 16GB
checkpoint_timeout = 15min
checkpoint_completion_target = 0.9

synchronous_commit = on
random_page_cost = 1.1
effective_io_concurrency = 256
# 커널/OS 측 시작점 예시 (재부팅 후에도 유지하려면 sysctl.d 사용)
sysctl -w vm.swappiness=1
sysctl -w vm.dirty_background_bytes=268435456
sysctl -w vm.dirty_bytes=1073741824
sysctl -w vm.dirty_writeback_centisecs=100

PostgreSQL 스토리지 프로파일

프로파일권장 시작점적용 이유확인 지표
NVMe 단일 디스크 effective_io_concurrency=256
random_page_cost=1.1
checkpoint_completion_target=0.9
낮은 지연 기반으로 랜덤 I/O를 적극 활용 p99 commit, checkpoint sync 시간, WAL flush 지연
SATA SSD effective_io_concurrency=32~64
random_page_cost=1.3~1.8
max_wal_size 보수적 상향
NVMe 대비 병렬성 한계가 낮아 체크포인트 파형을 더 완만하게 유지해야 함 await, 디바이스 util, checkpoint 간 지연 편차
RAID (HW/SW) effective_io_concurrency=64~128
max_wal_size 상향
checkpoint_timeout=10~15min
어레이 내부 캐시/스트라이프 특성으로 writeback 파형 완화 필요 await, aqu-sz, %util, 배열 캐시 flush 주기, 체크포인트 편차
클라우드 블록 스토리지 (EBS) effective_io_concurrency=16~64
checkpoint_completion_target=0.9~0.95
wal_compression=on
네트워크 기반 저장장치에서 지연 변동과 크레딧 소진 영향을 완화 p99/p999 fsync, I/O credit, queue depth, 네트워크 지연
클라우드 블록 스토리지 (Ceph RBD) effective_io_concurrency=16~48
checkpoint_completion_target=0.9~0.95
wal_compression=on
분산 스토리지의 복제/네트워크 지연 변동을 고려해 writeback burst를 억제 p99/p999 fsync, OSD 지연, 네트워크 지연, queue depth

MySQL InnoDB 시작점

# my.cnf (예시 시작값)
innodb_buffer_pool_size = 48G
innodb_buffer_pool_instances = 8
innodb_flush_method = O_DIRECT
innodb_flush_log_at_trx_commit = 1
sync_binlog = 1

innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
innodb_read_io_threads = 8
innodb_write_io_threads = 8
innodb_lru_scan_depth = 2048
# 디바이스 큐 상태와 스케줄러 확인
cat /sys/block/nvme0n1/queue/nr_requests
cat /sys/block/nvme0n1/queue/scheduler
iostat -x 1

MySQL InnoDB 스토리지 프로파일

프로파일권장 시작점적용 이유확인 지표
NVMe 단일 디스크 innodb_flush_method=O_DIRECT
innodb_io_capacity=2000
innodb_io_capacity_max=4000
높은 IOPS를 배경 flush가 따라가게 하되 foreground를 보호 log waits, fsync 횟수, 디바이스 util, p99 trx latency
SATA SSD innodb_io_capacity=800~1500
innodb_io_capacity_max=1600~3000
innodb_lru_scan_depth 보수 설정
과도한 flush 스레드 경쟁을 줄여 foreground 트랜잭션 지연을 안정화 InnoDB log waits, flush list 길이, await, %util
RAID (HW/SW) innodb_io_capacity=1000~2000
innodb_lru_scan_depth 점진 조정
sync_binlog=1 유지
배열 쓰기 증폭과 캐시 flush 주기를 고려해 과한 백그라운드 flush를 방지 redo checkpoint age, flush list 길이, replication 지연
클라우드 블록 스토리지 (EBS) innodb_io_capacity=600~1800
innodb_flush_neighbors=0
sync_binlog=1 유지
크레딧/보장 IOPS 특성에 맞춰 flush 파형을 안정화 p99 trx latency, replication apply delay, I/O credit, queue depth
클라우드 블록 스토리지 (Ceph RBD) innodb_io_capacity=500~1500
innodb_flush_neighbors=0
sync_binlog=1 유지
분산 스토리지의 지연 변동을 고려해 백그라운드 flush를 보수적으로 제어 p99 trx latency, replication apply delay, OSD/네트워크 지연, flush list 길이

MongoDB (WiredTiger) 시작점

# mongod.conf (예시 시작값)
storage:
  engine: wiredTiger
  wiredTiger:
    engineConfig:
      cacheSizeGB: 24
    collectionConfig:
      blockCompressor: snappy
systemLog:
  destination: file
replication:
  replSetName: rs0
setParameter:
  wiredTigerConcurrentReadTransactions: 128
  wiredTigerConcurrentWriteTransactions: 128

MongoDB 스토리지 프로파일

프로파일권장 시작점적용 이유확인 지표
NVMe 단일 디스크 cacheSizeGB를 물리 메모리의 약 40~50%로 시작
wiredTigerConcurrentReadTransactions=128
wiredTigerConcurrentWriteTransactions=128
낮은 지연 특성을 활용하되 과도한 동시성으로 flush 파형이 깨지지 않게 균형 유지 journal commit 지연, checkpoint 시간, p99 read/write latency
SATA SSD cacheSizeGB 보수 설정(메모리의 30~40%)
동시성 파라미터를 NVMe 대비 낮게 시작
I/O 병렬성 한계가 낮아 eviction/checkpoint가 foreground 요청을 방해하기 쉬움 eviction queued 작업, checkpoint 편차, 디바이스 await
클라우드 블록 스토리지 (EBS) cacheSizeGB 보수 설정 + 배치 워크로드 분리
저널/데이터 경로 I/O 급증 시 동시성 단계적 축소
지연 변동과 I/O credit 제약 환경에서 버스트 완화가 우선 p99/p999 write latency, replication lag, I/O credit, queue depth
클라우드 블록 스토리지 (Ceph RBD) cacheSizeGB 보수 설정 + 백그라운드 작업 제한
동시성 파라미터를 낮게 시작 후 점진 상향
분산 스토리지 복제/네트워크 지연 변동으로 checkpoint tail이 길어질 수 있음 checkpoint 지속 시간, OSD/네트워크 지연, replication lag

MongoDB 장애 대응 포인트

  1. journal commit 지연 급등: 배치 I/O를 우선 제한하고, 디스크 지연(iostat)과 MongoDB 지표(opLatencies, WiredTiger log)를 같은 타임라인으로 확인합니다.
  2. checkpoint stall: checkpoint 시간 증가와 eviction pressure가 함께 올라가면 캐시 크기/동시성/백그라운드 작업을 보수적으로 조정합니다.
  3. replication lag 확대: secondary 노드의 스토리지 지연과 CPU 압력을 분리 점검하고, apply 경로를 방해하는 배치 작업을 제한합니다.
  4. 클라우드 지연 변동: 워커 동시성을 단계적으로 낮추고(한 번에 1단계), 각 단계마다 5~10분 관측 후 다음 조치를 결정합니다.

온콜 필수 패키지/권한 체크

런북 명령이 실패하면 대응 속도가 느려집니다. 장애 대응 전에 아래 항목을 먼저 확인하세요.

# 필수 도구 확인 (없으면 패키지 설치)
command -v iostat   # sysstat
command -v pidstat  # sysstat
command -v mpstat   # sysstat
command -v sar      # sysstat
command -v perf     # linux-tools / perf
command -v numactl  # numactl
command -v fio      # fio

# 권한 확인
id
sudo -n true || echo \"sudo 비대화 모드 권한 필요\"

# cgroup v2 마운트 확인
mount | grep cgroup2

장애 상황별 대응 런북

아래 런북은 긴급 상황에서 먼저 서비스 안정화를 목표로 합니다. 성능 최적화는 장애가 진정된 후 재실험으로 진행해야 합니다.

상황 1: fsync 지연 급등 (commit 지연 급등)

# 1) 즉시 관측
iostat -x 1
pidstat -d -p <DB_PID> 1
perf trace -e fsync,fdatasync -p <DB_PID>

# 2) 대기열/압력 확인
cat /proc/pressure/io
cat /proc/pressure/memory
  1. 즉시 조치: 배치 작업 cgroup의 io.max를 낮춰 DB 경로를 우선 보호합니다.
  2. 단기 완화: 체크포인트/플러시 관련 DB 파라미터를 완만한 방향으로 조정합니다.
  3. 사후 보강: WAL/데이터 파일 분리, 스토리지 큐 깊이 재조정, flush 패턴 재측정.

상황 2: writeback stall / dirty 폭증

# dirty/writeback 상태 점검
grep -E \"Dirty|Writeback|MemAvailable\" /proc/meminfo
vmstat 1
cat /proc/vmstat | grep -E \"nr_dirty|nr_writeback|pgscan|pgsteal\"
  1. 즉시 조치: vm.dirty_bytes, vm.dirty_background_bytes를 보수적으로 낮춰 writeback burst를 줄입니다.
  2. 단기 완화: DB 체크포인트 간격과 completion target을 조정해 쓰기 파형을 평탄화합니다.
  3. 사후 보강: O_DIRECT 사용 여부, 버퍼풀 크기, 커널 reclaim 로그를 함께 점검합니다.

상황 3: 복제 지연 확대 (replication lag)

# 스토리지/CPU/메모리 병목 동시 점검
iostat -x 1
mpstat -P ALL 1
cat /proc/pressure/cpu
cat /proc/pressure/memory
  1. 즉시 조치: 복제 적용 워커와 배치 워커를 CPU/cgroup으로 분리합니다.
  2. 단기 완화: 백그라운드 compaction, 대형 쿼리, 인덱스 재빌드 같은 변동 작업을 일시 제한합니다.
  3. 사후 보강: 복제 전용 스토리지 대역폭, apply 스레드 수, 로그 flush 정책을 재설계합니다.

상황 4: 클라우드 스토리지 지연 변동 증가

# 네트워크/스토리지 지연 상관관계 확인
sar -n DEV 1
iostat -x 1
cat /proc/pressure/io
  1. 즉시 조치: 급격한 배치 I/O를 중지하고 DB 트랜잭션 경로를 우선합니다.
  2. 단기 완화: 동시성(워커 수, iodepth)을 줄여 지연 분포의 꼬리를 낮춥니다.
  3. 사후 보강: 볼륨 등급 상향, 대역폭 보장형 구성, 분리 볼륨 설계를 검토합니다.
런북 원칙: 장애 중에는 설정을 한 번에 많이 바꾸지 마세요. 즉시 조치 1개를 적용한 뒤 5~10분 관측해서 개선 여부를 확인하고 다음 조치를 진행해야 역효과를 막을 수 있습니다.

온콜 점검 체크리스트 (10분/30분/1시간)

장애 대응은 시간대별 목표를 분리해야 효과가 높습니다. 초기에는 안정화, 이후에는 원인 고립, 마지막으로 재발 방지까지 연결합니다.

구간목표필수 액션완료 기준
0~10분 서비스 급락 방지 배치/백업 I/O 제한, DB 우선 cgroup 적용, 핵심 지표(p99/fsync/IO wait) 확인 오류율/타임아웃 증가세 정지, p99가 하락 추세로 전환
10~30분 병목 계층 고립 스토리지/메모리/CPU 중 병목 축 1개를 확정하고 설정 1개만 변경 변경 전후 지표 비교가 가능하고, 개선/악화 판단이 명확함
30~60분 안정 상태 유지 임시 조치 고정, 재시작/페일오버 위험도 평가, 복제 지연 회복 확인 TPS/지연/복제 지연이 허용 범위로 복귀
1시간 이후 재발 방지 원인 보고서 작성, 영구 설정 후보 도출, 벤치마크 재현 계획 수립 다음 배포 전 검증 항목과 롤백 기준이 문서화됨
# 온콜 최소 수집 세트 (복붙용)
date
uptime
iostat -x 1 5
vmstat 1 5
cat /proc/pressure/io
cat /proc/pressure/memory
pidstat -d -r -u -p <DB_PID> 1 5
공통 전략: OLTP 트래픽과 배치 트래픽을 분리하고, DB 설정 변경 1개 + 커널 설정 변경 1개만 묶어서 실험하세요. 변경 폭을 작게 가져가야 회귀 원인을 정확히 찾을 수 있습니다.

실전 튜닝 체크리스트

  1. 스토리지 경로 선택: WAL과 데이터 파일의 장치/파일시스템 분리 여부를 검토합니다.
  2. flush 정책 점검: DB의 commit 설정과 커널 dirty writeback 파라미터를 함께 확인합니다.
  3. 큐 깊이 정합: DB 동시성, io_uring depth, NVMe queue depth를 일관되게 맞춥니다.
  4. 격리 정책 적용: cgroup v2로 배치 작업과 OLTP 워크로드를 분리합니다.
  5. 회귀 검증: 변경마다 동일한 재현 절차로 p99와 장애 복구 시간을 재측정합니다.

커널 I/O 스택과 데이터베이스

데이터베이스의 모든 영속 I/O는 커널의 계층 구조를 관통합니다. VFS → 파일시스템 → 블록 계층 → 디바이스 드라이버까지, 각 계층은 고유한 지연시간을 추가하며 데이터베이스 트랜잭션의 응답 시간에 직접 기여합니다. 이 경로를 DB 관점에서 이해하면 병목 구간을 정확히 특정할 수 있습니다.

DB 엔진 (User Space) Buffer Pool / WAL Writer / Checkpoint syscall 경계 (read/write/fsync/io_uring_enter) VFS (Virtual Filesystem Switch) inode lookup, permission check, page cache Filesystem (ext4 / XFS) journal, extent allocation, writeback Block Layer (blk-mq) I/O scheduler, merge, dispatch Device Driver (NVMe / SCSI) submission queue, completion, interrupt Storage Device (SSD / NVMe) syscall overhead: ~100ns VFS: ~200ns (cache hit) FS journal: ~1-10us blk-mq: ~1-5us driver: ~2-10us device: ~10-100us (NVMe) double buffering fsync barrier queue depth interrupt coalescing
계층일반적 지연시간DB 영향 포인트튜닝 가능 파라미터
시스템 콜50~200ns호출 빈도가 높으면 누적 오버헤드 발생io_uring 배치 제출, vDSO
VFS / Page Cache200ns (hit) ~ 수ms (miss)이중 캐시, reclaim stallO_DIRECT, fadvise, vm.vfs_cache_pressure
파일시스템1~10us (journal)fsync barrier, extent allocation마운트 옵션 (nobarrier, journal 모드)
블록 계층 (blk-mq)1~5usI/O 스케줄러 선택, 큐 깊이nr_requests, scheduler, max_sectors_kb
디바이스 드라이버2~10us인터럽트 코얼레싱, 폴링 모드irq affinity, polling, io_poll
스토리지 디바이스10~100us (NVMe)flush/FUA, write cachewrite_cache, FUA 지원 여부

DB에서 fsync()를 호출하면 위 모든 계층을 동기적으로 통과해야 합니다. NVMe 기준 총 지연은 보통 15~200us이지만, writeback throttle이나 journal commit이 겹치면 수ms까지 급등할 수 있습니다.

핵심 원리: DB 지연 분석에서 가장 중요한 것은 "어느 계층에서 시간이 소비되는가"를 분리하는 것입니다. biolatency로 블록 계층 이하를, ext4slower/xfsslower로 파일시스템 계층을, funclatency로 VFS 계층을 각각 측정할 수 있습니다.
# 계층별 지연 분리 측정 예제
# 1) 블록 계층 이하 지연 분포
biolatency-bpfcc -D 10

# 2) 파일시스템 계층에서 느린 작업 추적
ext4slower-bpfcc 1    # 1ms 이상 걸리는 ext4 작업

# 3) VFS 함수별 지연 측정
funclatency-bpfcc vfs_fsync_range -d 10

# 4) 전체 I/O 경로 추적 (요청별)
biosnoop-bpfcc -Q    # 큐 대기 시간 포함

VFS 계층에서의 DB 파일 접근 경로

DB가 파일을 읽고 쓸 때 VFS 계층에서 거치는 주요 함수 경로를 이해하면 어디서 지연이 발생하는지 정확히 추적할 수 있습니다.

DB 작업시스템 콜VFS 경로주요 지연 포인트
데이터 읽기pread64()vfs_read → generic_file_read_iter → page cache lookupcache miss 시 readpage 호출
데이터 쓰기pwrite64()vfs_write → generic_file_write_iter → page dirtydirty throttle 시 balance_dirty_pages
WAL syncfdatasync()vfs_fsync_range → file->f_op->fsync저널 커밋 + 디바이스 flush
파일 확장fallocate()vfs_fallocate → ext4_fallocateextent 할당, 메타데이터 저널링
Direct I/Opread64(O_DIRECT)generic_file_read_iter → direct_IO정렬 검사, DMA 전송
# ftrace로 VFS 함수 경로 추적 (DB 프로세스만)
echo 0 > /sys/kernel/debug/tracing/tracing_on
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo vfs_fsync_range > /sys/kernel/debug/tracing/set_graph_function
echo $(pgrep -x postgres | head -1) > /sys/kernel/debug/tracing/set_ftrace_pid
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 5
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace | head -100

# 결과 예시:
#  1)               |  vfs_fsync_range() {
#  1)               |    ext4_sync_file() {
#  1)               |      jbd2_complete_transaction() {
#  1)   0.854 us    |        jbd2_log_start_commit();
#  1) + 42.621 us   |        jbd2_log_wait_commit();
#  1) + 43.987 us   |      }
#  1) + 45.123 us   |    }
#  1) + 45.456 us   |  }

블록 계층 merge 동작과 DB I/O 패턴

블록 계층의 I/O merge(병합)는 인접한 I/O 요청을 하나로 합쳐 디바이스 효율을 높입니다. 그러나 DB의 랜덤 I/O 패턴에서는 merge가 거의 발생하지 않아, merge 시도 자체가 불필요한 오버헤드가 됩니다.

# merge 통계 확인
cat /sys/block/nvme0n1/stat
# 필드 순서: reads_completed reads_merged sectors_read ms_reading ...
# reads_merged가 0에 가까우면 NVMe + DB 랜덤 I/O 패턴

# nomerges 설정으로 merge 시도 자체를 비활성화
echo 2 > /sys/block/nvme0n1/queue/nomerges
# 0=merge 활성화, 1=일방향 비활성화, 2=완전 비활성화

# max_sectors_kb 조정 (큰 sequential I/O에 유용)
cat /sys/block/nvme0n1/queue/max_sectors_kb
echo 128 > /sys/block/nvme0n1/queue/max_sectors_kb
NVMe에서의 블록 계층 최소화: NVMe 장치는 내부 FTL(Flash Translation Layer)이 자체 스케줄링을 하므로, 커널 블록 계층의 스케줄링은 중복입니다. 스케줄러를 none으로 설정하고, merge도 비활성화하면 블록 계층 통과 시간을 1~2us 줄일 수 있습니다. 이 시간이 중요한지는 워크로드 IOPS에 따라 달라집니다.

페이지 캐시 vs Direct I/O

데이터베이스의 I/O 전략에서 가장 근본적인 선택지는 커널 페이지 캐시를 활용할 것인지(Buffered I/O), 우회할 것인지(Direct I/O)입니다. 이 선택은 메모리 사용 패턴, 지연 분포, 복구 동작에 큰 영향을 줍니다.

Page Cache Hit/Miss 경로와 Direct I/O 경로 DB 프로세스 (read/write) ? Buffered I/O Page Cache Lookup radix tree / xarray Cache Hit ~200ns 즉시 반환 Cache Miss Block I/O 발생 ~10-100us+ 디스크 접근 O_DIRECT Page Cache 우회 DMA 직접 전송 Block Layer 직접 제출 정렬 필수 (512B/4KB) Device (예측 가능한 지연) 이중 캐시 없음, 메모리 절약

페이지 캐시 Hit/Miss 패턴

Buffered I/O에서 읽기 요청이 페이지 캐시에 적중하면 커널은 메모리 복사만으로 데이터를 반환합니다(~200ns). 미스가 발생하면 블록 I/O가 발생해 NVMe 기준 10~100us의 추가 지연이 발생합니다. 문제는 DB가 자체 버퍼 풀을 관리하므로 이중 버퍼링이 발생한다는 점입니다.

이중 버퍼링 문제: PostgreSQL의 shared_buffers가 16GB이고 커널 페이지 캐시에 동일 데이터가 캐시되면, 물리 메모리 32GB를 같은 데이터에 사용합니다. 메모리 압력이 높아지면 reclaim이 DB 성능을 급격히 떨어뜨립니다. 이것이 MySQL/InnoDB가 O_DIRECT를 기본값으로 사용하는 핵심 이유입니다.

O_DIRECT 정렬 요구사항

O_DIRECT를 사용하려면 세 가지 정렬 조건을 모두 충족해야 합니다.

/* O_DIRECT 정렬 요구사항 코드 예제 */
#define BLK_SIZE 4096

/* posix_memalign으로 정렬된 버퍼 할당 */
void *buf;
if (posix_memalign(&buf, BLK_SIZE, BLK_SIZE * 256) != 0)
    return -1;

int fd = open("/data/dbfile", O_RDWR | O_DIRECT);

/* 오프셋도 BLK_SIZE 배수여야 함 */
ssize_t n = pread(fd, buf, BLK_SIZE * 256, BLK_SIZE * 1024);
if (n < 0)
    perror("pread O_DIRECT failed");

PostgreSQL vs MySQL 기본 I/O 전략

항목PostgreSQL (기본)MySQL InnoDB (기본)
데이터 파일 I/OBuffered I/O (페이지 캐시 경유)O_DIRECT (페이지 캐시 우회)
WAL/Redo 로그Buffered + fdatasyncBuffered + fsync (또는 O_DIRECT)
캐시 관리shared_buffers + 페이지 캐시 이중 구조innodb_buffer_pool 단독 관리
이중 캐시 여부발생 (shared_buffers + page cache)최소화 (O_DIRECT 사용 시)
메모리 압력 영향reclaim이 DB 캐시도 영향DB 버퍼 풀은 reclaim 대상 아님
readahead 활용커널 readahead 활용 가능자체 prefetch 로직 사용
O_DIRECT 전환가능하지만 비표준 (효과 제한적)기본값 (innodb_flush_method)
실무 선택 기준: 메모리가 충분하고 working set이 메모리에 들어가면 Buffered I/O가 유리합니다. 메모리 대비 데이터가 크거나, 지연 예측성이 중요한 OLTP에서는 O_DIRECT가 안정적입니다. 혼합 워크로드에서는 WAL은 Buffered + fsync, 데이터 파일은 O_DIRECT를 쓰는 하이브리드 전략도 고려할 수 있습니다.

페이지 캐시 오염(Pollution) 문제

분석 쿼리나 백업이 대량의 순차 읽기를 수행하면 페이지 캐시의 핫 데이터가 밀려나는 캐시 오염이 발생합니다. OLTP 워크로드의 캐시 적중률이 급락하며 지연이 급증합니다.

# 페이지 캐시 오염 방지 기법

# 1) fadvise로 순차 읽기 후 캐시 정리
# 백업 스크립트에서 사용
dd if=/data/dbfile of=/backup/dbfile bs=1M
# 백업 후 캐시에서 제거
python3 -c "import os; fd=os.open('/data/dbfile',os.O_RDONLY); os.posix_fadvise(fd,0,0,os.POSIX_FADV_DONTNEED); os.close(fd)"

# 2) cgroup v2 memory.low로 DB 캐시 보호
# DB cgroup의 memory.low를 설정하면 해당 cgroup의 페이지 캐시가
# 다른 cgroup의 reclaim으로부터 보호됨
echo 48G > /sys/fs/cgroup/db/memory.low

# 3) vmtouch로 핫 데이터 캐시 고정
vmtouch -t /data/pgdata/base/16384/*   # 특정 DB의 파일을 캐시에 로드
vmtouch -l /data/pgdata/base/16384/*   # 캐시에 잠금 (mlock)

# 4) 캐시 사용량 모니터링
vmtouch -v /data/pgdata/ | tail -5
# Files: 1234  Directories: 56  Resident Pages: 2048000/4096000  50%
# 비율이 떨어지면 캐시 오염 의심

Dirty Throttle과 DB 지연 관계

커널은 dirty 페이지 비율이 vm.dirty_background_ratio를 초과하면 백그라운드 writeback을 시작하고, vm.dirty_ratio를 초과하면 쓰기 프로세스를 직접 throttle합니다. DB의 쓰기 경로에서 이 throttle이 발생하면 트랜잭션 commit 지연이 급증합니다.

dirty 수준커널 동작DB 영향모니터링 지표
0% ~ background_ratio정상 (writeback 없음)영향 없음/proc/meminfo Dirty
background_ratio ~ dirty_ratio백그라운드 writeback 시작약간의 I/O 경합 가능/proc/vmstat nr_dirty
dirty_ratio 초과프로세스 직접 throttle (balance_dirty_pages)쓰기 지연 급증, commit stallPSI io, vmstat wa
dirty_ratio 크게 초과강제 writeback + 새 dirty 금지모든 쓰기 작업 정지system hang 수준
ratio vs bytes: vm.dirty_ratio(비율 기반)은 물리 메모리에 비례하므로, 256GB 메모리에서 20% = 51GB의 dirty를 허용합니다. 이 양은 NVMe가 flush하는 데 수십 초가 걸려 장시간 stall을 유발합니다. DB 서버에서는 vm.dirty_bytes/vm.dirty_background_bytes(절대값 기반)를 사용하여 dirty 양을 고정하는 것이 안전합니다.

io_uring 데이터베이스 워크로드

io_uring은 리눅스 5.1에서 도입된 비동기 I/O 인터페이스로, 기존 pread/pwrite 시스템 콜 기반 I/O에 비해 시스템 콜 오버헤드를 크게 줄이고 배치 제출/완료를 지원합니다. 데이터베이스 워크로드에서 특히 높은 IOPS가 필요한 경우 큰 이점을 제공합니다.

io_uring SQE/CQE 링 구조 (DB 관점) User Space (DB 프로세스) SQ (제출 큐) SQE: pread(fd, off, len) SQE: pwrite(fd, off, len) SQE: fsync(fd) ... (배치 제출) CQ (완료 큐) CQE: result=4096 CQE: result=8192 CQE: result=0 (ok) ... (배치 완료) DB WAL Writer / Checkpoint Thread Kernel Space io_uring worker SQE 처리 + I/O 제출 SQPOLL thread (syscall 제거) Block Layer (blk-mq) / NVMe Driver io_poll 지원 시 polling completion 가능 io_uring_enter() CQE 완료 통지 DB 이점: 배치 제출 + 비동기 완료 + 0-copy fixed buffers/files로 추가 최적화 가능

io_uring의 DB 핵심 이점

/* io_uring을 이용한 DB 배치 I/O 예제 (개념 코드) */
#include <liburing.h>

struct io_uring ring;
io_uring_queue_init(256, &ring, 0);  /* SQ/CQ 크기 256 */

/* 여러 읽기 요청을 배치 제출 */
for (int i = 0; i < batch_size; i++) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    io_uring_prep_read(sqe, data_fd, bufs[i], 4096, offsets[i]);
    io_uring_sqe_set_data(sqe, &requests[i]);
}

/* 한 번의 syscall로 모든 요청 제출 */
io_uring_submit(&ring);

/* 완료된 요청 수확 */
struct io_uring_cqe *cqe;
for (int i = 0; i < batch_size; i++) {
    io_uring_wait_cqe(&ring, &cqe);
    struct request *req = io_uring_cqe_get_data(cqe);
    /* cqe->res 에 읽은 바이트 수 */
    process_result(req, cqe->res);
    io_uring_cqe_seen(&ring, cqe);
}

io_uring_queue_exit(&ring);
/* SQPOLL 모드 + Fixed Files 활용 (고성능 DB) */
struct io_uring_params params = { 0 };
params.flags = IORING_SETUP_SQPOLL;
params.sq_thread_idle = 2000;  /* 2초 유휴 후 대기 */

io_uring_queue_init_params(1024, &ring, &params);

/* Fixed Files 등록 (파일 참조 오버헤드 제거) */
int fds[] = { data_fd, wal_fd, idx_fd };
io_uring_register_files(&ring, fds, 3);

/* Fixed Buffers 등록 (DMA 맵핑 재사용) */
struct iovec iovs[64];
for (int i = 0; i < 64; i++) {
    iovs[i].iov_base = aligned_bufs[i];
    iovs[i].iov_len = 4096;
}
io_uring_register_buffers(&ring, iovs, 64);

/* SQPOLL 모드에서는 io_uring_enter() 호출 불필요 */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read_fixed(sqe, 0, aligned_bufs[0], 4096, offset, 0);
sqe->flags |= IOSQE_FIXED_FILE;
/* SQ tail 포인터만 업데이트하면 커널 SQPOLL 스레드가 자동 감지 */

RocksDB / ScyllaDB io_uring 사례

DBio_uring 활용효과비고
RocksDB (MultiGet)비동기 파일 읽기 배치 제출MultiGet 지연 50~80% 감소5.18+ 커널 권장
ScyllaDB모든 디스크 I/O를 io_uring으로 처리기존 AIO 대비 throughput 15~25% 향상Seastar 프레임워크 내장
PostgreSQL (실험)WAL write + data file prefetch높은 동시성에서 fsync 배치 효과16+ 버전에서 진행 중
MariaDB비동기 read-ahead, redo logread-heavy에서 throughput 향상10.6+ 옵션
io_uring DB 적용 시 주의: SQPOLL 모드는 전용 CPU를 소비하므로 코어 수가 적은 환경에서는 오히려 성능이 떨어질 수 있습니다. 먼저 일반 io_uring 모드로 효과를 확인한 뒤, 고 IOPS 환경에서만 SQPOLL을 활성화하세요.

io_uring vs 기존 I/O 인터페이스 성능 비교

인터페이스syscall 수 (배치 N개)커널 진입 비용최대 IOPS (4KB random)DB 적합성
pread/pwrite (동기)N회N * ~100ns~500K (단일 스레드)단순, 낮은 동시성에 적합
Linux AIO (io_submit)1회 (배치)~200ns + context switch~1MO_DIRECT 전용, 제약 있음
io_uring (일반)1회 (배치)~100ns~2M범용, Buffered/Direct 모두
io_uring (SQPOLL)0회0 (커널 폴링)~3M+극한 IOPS, CPU 트레이드오프
io_uring (SQPOLL + IOPOLL)0회0 + 폴링 완료~4M+NVMe 전용, 최저 지연

io_uring 커널 파라미터 확인

# io_uring 관련 커널 설정 확인
# io_uring이 지원되는지 확인 (커널 5.1+)
uname -r

# io_uring 메모리 제한 (unprivileged user)
cat /proc/sys/kernel/io_uring_disabled   # 0=활성화, 1=비활성화, 2=root만
sysctl kernel.io_uring_disabled

# RLIMIT_MEMLOCK 확인 (io_uring 링 메모리용)
ulimit -l   # unlimited 권장 (DB 사용자)

# io_uring 사용 현황 모니터링
cat /proc/$(pgrep -x postgres | head -1)/fdinfo/* 2>/dev/null | grep io_uring

# fio로 io_uring 성능 테스트
fio --name=ioring_test --filename=/data/test \
    --rw=randread --bs=4k --ioengine=io_uring \
    --iodepth=128 --numjobs=4 --hipri=1 \
    --fixedbufs=1 --registerfiles=1 --sqthread_poll=1 \
    --runtime=30 --time_based --group_reporting

THP와 데이터베이스 성능

Transparent Huge Pages(THP)는 커널이 자동으로 2MB 대형 페이지를 할당하여 TLB 미스를 줄이는 기능입니다. 그러나 데이터베이스 워크로드에서는 THP가 심각한 지연 급증을 유발하는 것으로 널리 알려져 있습니다.

THP가 DB에 미치는 부정적 영향

DBTHP 권장 설정이유Explicit Huge Pages 사용
PostgreSQLnever 또는 madvisecheckpoint fork 시 COW 비용, shared_buffers fragmentationhuge_pages=on (권장)
MySQL InnoDBneverbuffer pool 할당 패턴과 충돌, compaction stalllarge-pages=ON (선택)
MongoDBnever공식 권장, WiredTiger 캐시 fragmentation 방지미지원
RedisneverBGSAVE fork COW 증폭, latency spike불필요 (메모리 중심)
Oracle DBneverSGA 할당과 충돌 가능USE_LARGE_PAGES=ONLY
ScyllaDBnever직접 huge page 관리, THP 간섭 회피자체 huge page 매핑
# THP 상태 확인
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never  ← 현재 always가 활성화된 상태

# THP 비활성화 (런타임)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

# khugepaged 비활성화
echo 0 > /sys/kernel/mm/transparent_hugepage/khugepaged/defrag

# 부팅 시 영구 적용 (커널 파라미터)
# GRUB_CMDLINE_LINUX="transparent_hugepage=never"

# systemd 서비스로 영구 적용
cat <<'EOF' > /etc/systemd/system/disable-thp.service
[Unit]
Description=Disable Transparent Huge Pages
DefaultDependencies=no
After=sysinit.target local-fs.target
Before=basic.target

[Service]
Type=oneshot
ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/enabled'
ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/defrag'

[Install]
WantedBy=basic.target
EOF
systemctl enable disable-thp.service
# THP 관련 커널 통계 모니터링
grep -E "thp|huge" /proc/vmstat
# thp_fault_alloc      — THP 할당 시도 횟수
# thp_fault_fallback   — THP 할당 실패 (4KB fallback)
# thp_collapse_alloc   — khugepaged 병합 성공
# thp_split_page       — 대형 페이지 분할 횟수
# compact_stall        — compaction으로 인한 stall 횟수 ← 이 값이 증가하면 위험

# Explicit Huge Pages 설정 (PostgreSQL용)
# shared_buffers = 16GB 기준, 2MB 페이지 8192개 + 여유분
sysctl -w vm.nr_hugepages=8400
grep HugePages /proc/meminfo
경고: 프로덕션 DB 서버에서 THP가 always로 설정되어 있으면 즉시 never로 변경하세요. THP로 인한 지연 급증은 재현이 어렵고, 수분 간격으로 발생하는 p999 스파이크 형태로 나타나 진단이 늦어지는 경우가 많습니다.

THP vs Explicit Huge Pages 비교

THP를 비활성화하는 것이 DB에서 권장되지만, 명시적 Huge Pages(hugetlbfs)는 오히려 성능에 도움이 됩니다. 두 가지를 혼동하지 마세요.

항목THP (Transparent Huge Pages)Explicit Huge Pages (hugetlbfs)
할당 방식커널이 자동으로 2MB 페이지 할당부팅 시 또는 sysctl로 사전 예약
compaction발생 (stall 원인)발생하지 않음 (사전 예약)
splitting발생 (partial access 시)발생하지 않음
khugepaged활성화 (CPU 소모)관여하지 않음
스왑 가능가능 (스왑 시 split)불가 (pinned)
DB 권장비활성화 (never)활성화 (shared_buffers용)
설정 방법/sys/kernel/mm/transparent_hugepage/vm.nr_hugepages
# Explicit Huge Pages 계산 및 설정
# PostgreSQL shared_buffers = 16GB의 경우
# 16GB / 2MB = 8192 페이지 + 여유분 200

# 1) 현재 huge page 크기 확인
grep Hugepagesize /proc/meminfo
# Hugepagesize:     2048 kB

# 2) huge pages 예약
sysctl -w vm.nr_hugepages=8400

# 3) 할당 확인
grep HugePages /proc/meminfo
# HugePages_Total:   8400  ← 요청한 수와 일치해야 함
# 부족하면 메모리 단편화로 인해 할당 실패 → 부팅 시 설정 권장

# 4) PostgreSQL 설정
# postgresql.conf: huge_pages = on

# 5) hugetlbfs 그룹 권한 설정
sysctl -w vm.hugetlb_shm_group=$(id -g postgres)

# 6) 부팅 시 영구 적용
# /etc/sysctl.d/99-hugepages.conf
# vm.nr_hugepages = 8400
# vm.hugetlb_shm_group = 26  (postgres 그룹 GID)
THP madvise 모드: madvise 모드는 madvise(MADV_HUGEPAGE)를 명시적으로 호출한 영역에서만 THP를 활성화합니다. 일부 DB(예: ScyllaDB)는 자체적으로 madvise를 사용하므로 madvise 모드가 적합할 수 있습니다. 대부분의 전통적 RDBMS에서는 never가 안전합니다.

NUMA 메모리 정책과 DB

현대 서버는 대부분 NUMA(Non-Uniform Memory Access) 아키텍처입니다. CPU 소켓마다 로컬 메모리가 있고, 원격 노드의 메모리에 접근하면 40~100ns의 추가 지연이 발생합니다. DB의 버퍼 풀, WAL 버퍼, 워커 스레드가 어느 NUMA 노드에 배치되는지에 따라 tail latency가 크게 달라집니다.

NUMA 토폴로지와 DB 인스턴스 배치 NUMA Node 0 CPU 0-15 L3 Cache 30MB Local Memory 128GB DDR5 (~80ns) OLTP DB 인스턴스 (Primary) Buffer Pool 64GB + WAL Writer 로컬 메모리 접근 → 낮은 지연 NVMe SSD (PCIe Node 0) NUMA Node 1 CPU 16-31 L3 Cache 30MB Remote Memory 128GB DDR5 (~140ns) Batch / Analytics 작업 ETL / Backup / Report OLTP와 NUMA 노드 분리 QPI / UPI Interconnect 원격 접근: +40~100ns 추가 지연

NUMA 메모리 할당 정책

정책설명DB 활용numactl 옵션
Local (기본)현재 CPU의 로컬 노드에서 할당단일 NUMA 노드에 바인딩된 DB에 적합--localalloc
Interleave모든 노드에 라운드로빈 분배대형 공유 버퍼 풀의 균등 분배에 적합--interleave=all
Bind지정 노드에서만 할당특정 NUMA 노드에 DB 인스턴스 고정--membind=0
Preferred지정 노드 우선, 실패 시 다른 노드소프트 바인딩이 필요한 경우--preferred=0
# NUMA 토폴로지 확인
numactl --hardware
# available: 2 nodes (0-1)
# node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# node 0 size: 131072 MB
# node 1 cpus: 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
# node 1 size: 131072 MB
# node distances:
# node   0   1
#   0:  10  21
#   1:  21  10

# PostgreSQL을 NUMA Node 0에 바인딩 실행
numactl --cpunodebind=0 --membind=0 \
    su - postgres -c 'pg_ctl start -D /var/lib/postgresql/data'

# MySQL을 NUMA Node 0에 바인딩, 버퍼 풀은 인터리브
numactl --cpunodebind=0 --interleave=all \
    mysqld --defaults-file=/etc/mysql/my.cnf

# 실행 중인 프로세스의 NUMA 통계 확인
numastat -p $(pgrep -x postgres | head -1)

# NUMA 밸런싱 활성화/비활성화
# 활성화: 커널이 자동으로 메모리를 가까운 노드로 이동
sysctl -w kernel.numa_balancing=1
# 비활성화: 수동 바인딩 유지 (DB에서 권장)
sysctl -w kernel.numa_balancing=0
NUMA 밸런싱 주의: kernel.numa_balancing=1(자동 NUMA 밸런싱)은 DB 워크로드에서 오히려 성능을 떨어뜨릴 수 있습니다. 커널이 페이지를 노드 간 이동시키며 TLB flush와 메모리 복사 비용이 발생하기 때문입니다. numactl로 명시적 바인딩을 한 경우 자동 밸런싱은 비활성화하세요.

NUMA 관련 커널 통계 모니터링

# NUMA 히트/미스 통계
grep -E "numa" /proc/vmstat
# numa_hit      — 로컬 노드 할당 성공 (이 값이 높을수록 좋음)
# numa_miss     — 로컬 노드 할당 실패 → 원격 노드 할당 (이 값이 높으면 위험)
# numa_foreign  — 다른 노드의 요청을 이 노드가 처리
# numa_interleave — interleave 정책 할당
# numa_local    — 로컬 노드에서 할당된 페이지
# numa_other    — 원격 노드에서 할당된 페이지

# DB별 NUMA 상태 상세
numastat -p $(pgrep -x postgres | head -1)
# Per-node process memory usage (in MBs)
#                   Node 0   Node 1    Total
# Huge              8192.00     0.00  8192.00  ← shared_buffers가 Node 0에 집중
# Heap               256.00    12.00   268.00
# Stack                2.00     0.00     2.00

# perf로 NUMA 원격 접근 카운터 측정
perf stat -e node-loads,node-load-misses,node-stores,node-store-misses \
    -p $(pgrep -x postgres | head -1) sleep 10
# node-load-misses 비율이 10% 이상이면 NUMA 배치 재검토 필요

NVMe와 NUMA 정합

NVMe 디바이스도 특정 PCIe 슬롯에 연결되어 NUMA 노드에 귀속됩니다. DB 프로세스가 Node 0에서 실행되는데 NVMe가 Node 1에 연결되어 있으면, 모든 DMA 전송이 인터커넥트를 경유해 추가 지연이 발생합니다.

# NVMe 디바이스의 NUMA 노드 확인
cat /sys/block/nvme0n1/device/numa_node
# 0이면 NUMA Node 0에 연결

# PCIe 디바이스의 NUMA 노드 확인
lspci -vvv -s $(readlink -f /sys/block/nvme0n1/device | grep -oP '[0-9a-f]+:[0-9a-f]+\.[0-9]+') | grep NUMA
# NUMA node: 0

# IRQ의 NUMA 노드 맞춤 (NVMe IRQ를 같은 노드의 CPU에 할당)
# NVMe가 Node 0이면 IRQ도 Node 0 CPU에 배치
for irq in $(grep nvme0 /proc/interrupts | awk -F: '{print $1}' | tr -d ' '); do
    echo "00ff" > /proc/irq/$irq/smp_affinity  # Node 0: CPU 0-7
done
NUMA + NVMe 최적 구성: DB 프로세스, NVMe IRQ, NVMe 디바이스가 모두 같은 NUMA 노드에 있어야 최적 성능을 달성합니다. 2소켓 서버에서 NVMe가 Node 0에 있으면 DB도 numactl --cpunodebind=0 --membind=0으로 실행하세요.
# systemd 서비스에서 NUMA 바인딩 설정
# /etc/systemd/system/postgresql.service.d/numa.conf
[Service]
ExecStart=
ExecStart=/usr/bin/numactl --cpunodebind=0 --membind=0 \
    /usr/lib/postgresql/16/bin/postgres \
    -D /var/lib/postgresql/16/main \
    -c config_file=/etc/postgresql/16/main/postgresql.conf

# 또는 CPUAffinity로 코어 범위 지정
[Service]
CPUAffinity=0-15

스케줄러 튜닝

DB 워크로드에서 CPU 스케줄링은 트랜잭션 응답 지연에 직접 영향을 줍니다. 워커 스레드가 runqueue에서 대기하는 시간, 백그라운드 작업(flush, compaction)과의 경합, 컨텍스트 스위치 비용을 최소화하는 것이 핵심입니다.

DB 워크로드 스케줄링 모델 OLTP 워커 (높은 우선순위) Worker 1 Worker 2 Worker N cpu.weight = 10000 cpuset.cpus = 0-7 (isolcpus) 백그라운드 (낮은 우선순위) Checkpoint Vacuum Compact cpu.weight = 100 cpuset.cpus = 8-11 Batch / ETL Backup ETL Job Exporter cpu.weight = 50, cpu.max = 40% cpuset.cpus = 12-15 CPU 코어 (16코어) CPU 0-7: OLTP 전용 CPU 8-11: BG CPU 12-15: Batch 핵심: OLTP, 백그라운드, 배치를 cgroup + cpuset으로 물리적 분리

CFS 파라미터 튜닝

파라미터기본값DB 튜닝 방향영향
sched_latency_ns6000000 (6ms)3000000~6000000값이 작으면 응답성 향상, 스위치 증가
sched_min_granularity_ns750000 (0.75ms)1000000~3000000값이 크면 DB 워커의 연속 실행 시간 보장
sched_wakeup_granularity_ns1000000 (1ms)1500000~3000000값이 크면 wakeup preemption 감소
sched_migration_cost_ns500000 (0.5ms)5000000 (5ms)스레드 마이그레이션 억제, 캐시 친화성 유지
sched_nr_migrate328~16밸런싱 시 이동 스레드 수 제한
sched_autogroup_enabled10DB 서버에서는 자동 그룹핑 비활성화
# DB 서버용 CFS 파라미터 조정 예시
sysctl -w kernel.sched_min_granularity_ns=2000000   # 2ms 최소 실행 단위
sysctl -w kernel.sched_wakeup_granularity_ns=2000000 # 2ms wakeup 해상도
sysctl -w kernel.sched_migration_cost_ns=5000000    # 마이그레이션 억제
sysctl -w kernel.sched_autogroup_enabled=0           # 자동 그룹핑 비활성화

cgroup CPU 제어

# cgroup v2 CPU 제어 (OLTP vs Batch 분리)

# DB OLTP 그룹: CPU 높은 가중치
mkdir -p /sys/fs/cgroup/db-oltp
echo 10000 > /sys/fs/cgroup/db-oltp/cpu.weight  # 기본 100, 최대 10000

# Batch 그룹: CPU 낮은 가중치 + 상한 설정
mkdir -p /sys/fs/cgroup/db-batch
echo 50 > /sys/fs/cgroup/db-batch/cpu.weight
echo "400000 1000000" > /sys/fs/cgroup/db-batch/cpu.max  # 40% CPU 상한

# 프로세스 할당
echo $OLTP_PID > /sys/fs/cgroup/db-oltp/cgroup.procs
echo $BATCH_PID > /sys/fs/cgroup/db-batch/cgroup.procs

# CPU 사용량 확인
cat /sys/fs/cgroup/db-oltp/cpu.stat
cat /sys/fs/cgroup/db-batch/cpu.stat

백그라운드 작업 격리

# isolcpus로 DB 전용 코어 예약 (부팅 파라미터)
# GRUB_CMDLINE_LINUX="isolcpus=0-7"
# CPU 0-7을 일반 스케줄러에서 제외하고 DB 전용으로 사용

# taskset으로 DB를 격리된 코어에 배치
taskset -c 0-7 /usr/lib/postgresql/16/bin/postgres -D /data

# 백그라운드 flush를 SCHED_IDLE로 설정
# (커널 6.0+에서 cgroup v2 지원)
chrt --idle 0 /path/to/backup-script.sh
스케줄러 튜닝 원칙: DB 워커 스레드에는 긴 실행 단위와 캐시 친화성을 보장하고, 백그라운드 작업(backup, compaction, ETL)에는 낮은 우선순위와 CPU 상한을 적용하세요. 단, 스케줄러 파라미터 변경은 반드시 부하 테스트 후 적용해야 합니다.

컨텍스트 스위치 비용과 DB

컨텍스트 스위치는 레지스터 저장/복원 외에도 TLB flush, L1/L2 캐시 콜드 스타트를 동반합니다. DB의 핫 경로에서 불필요한 컨텍스트 스위치가 발생하면 쿼리 지연이 증가합니다.

스위치 유형비용원인DB에서의 완화
자발적 (voluntary)~1-5usI/O 대기, 락 대기, sleep비동기 I/O, 락 경합 줄이기
비자발적 (involuntary)~5-15us타임슬라이스 만료, 선점sched_min_granularity 증가
CPU 마이그레이션~10-50us로드 밸런싱으로 다른 코어 이동sched_migration_cost 증가, taskset
NUMA 마이그레이션~50-100us+NUMA 밸런싱으로 다른 노드 이동numa_balancing=0, cpuset
# 컨텍스트 스위치 모니터링
pidstat -w -p $(pgrep -x postgres | head -1) 1 10
# cswch/s: 자발적 스위치 (I/O 대기 등)
# nvcswch/s: 비자발적 스위치 (선점) ← 이 값이 높으면 CPU 경합

# CPU 마이그레이션 확인
perf stat -e cpu-migrations -p $(pgrep -x postgres | head -1) sleep 10
# migrations가 초당 100+ 이면 migration_cost 상향 또는 taskset 고려

# 스케줄러 wakeup 지연 분석 (bpftrace)
bpftrace -e '
tracepoint:sched:sched_wakeup
/comm == "postgres"/
{
    @wakeup_lat[tid] = nsecs;
}
tracepoint:sched:sched_switch
/comm == "postgres" && @wakeup_lat[tid]/
{
    @switch_lat = hist((nsecs - @wakeup_lat[tid]) / 1000);
    delete(@wakeup_lat[tid]);
}
interval:s:10 { exit(); }
'

락 경합 분석

데이터베이스 내부 락(row lock, page lock, buffer lock)과 커널 수준 락(futex, rwsem, mutex)은 서로 상호작용하며 경합을 유발합니다. 높은 동시성에서 락 대기 시간은 전체 트랜잭션 지연의 상당 부분을 차지할 수 있습니다.

락 경합 분석 흐름 (DB + Kernel) DB 내부 락 Row Lock Buffer Lock WAL Insert Lock Table Lock 커널 동기화 프리미티브 futex (mutex) rwsem spinlock mmap_lock 분석 도구 perf lock bpftrace lockdep lockstat syscall trace 경합 핫스팟 식별 → 원인 분류 → 완화 전략 적용 DB 락 → 인덱스/스키마 최적화 | 커널 락 → 시스템 파라미터 조정 DB 레벨 완화 인덱스 추가/재설계 파티셔닝, 배치 크기 조정 커널 레벨 완화 NUMA 바인딩 개선 mmap_lock 경합 회피 아키텍처 변경 락 프리 구조 도입 연결 풀 크기 제한

futex 기반 뮤텍스와 DB

사용자 공간 뮤텍스(pthread_mutex)는 경합이 없을 때는 커널 진입 없이 atomic 연산으로 동작합니다. 경합이 발생하면 futex(FUTEX_WAIT) 시스템 콜로 커널에서 대기합니다. 높은 동시성 DB에서 수백 개의 연결이 같은 뮤텍스를 경합하면 futex 대기 체인이 길어지고 응답 지연이 급증합니다.

# bpftrace로 futex 경합 분석
bpftrace -e '
tracepoint:syscalls:sys_enter_futex
/comm == "postgres" && args->op == 0/  /* FUTEX_WAIT */
{
    @futex_wait[tid] = count();
    @stack[kstack] = count();
}

tracepoint:syscalls:sys_exit_futex
/comm == "postgres" && @futex_wait[tid]/
{
    @latency = hist(nsecs - @start[tid]);
    delete(@futex_wait[tid]);
}

interval:s:10 { exit(); }
'

# perf lock으로 커널 락 경합 분석
perf lock record -p $(pgrep -x postgres | head -1) -- sleep 30
perf lock report

# lockstat 활성화 (커널 빌드 시 CONFIG_LOCK_STAT=y 필요)
echo 1 > /proc/sys/kernel/lock_stat
cat /proc/lock_stat | head -50

# bpftrace로 mmap_lock 경합 추적 (PostgreSQL fork 관련)
bpftrace -e '
kprobe:down_write
/comm == "postgres"/
{
    @mmap_write[tid] = nsecs;
}
kretprobe:down_write
/comm == "postgres" && @mmap_write[tid]/
{
    @mmap_lock_wait = hist(nsecs - @mmap_write[tid]);
    delete(@mmap_write[tid]);
}
interval:s:10 { exit(); }
'
DB 락과 커널 락 상호작용: PostgreSQL의 LWLock은 내부적으로 pg_atomic + semaphore(futex)로 구현됩니다. InnoDB의 뮤텍스는 스핀-대기 후 OS mutex로 전환합니다. 따라서 DB 내부 락 경합은 결국 커널의 futex/세마포어 경합으로 전이됩니다. DB 측 락 경합과 커널 측 futex 경합을 함께 분석해야 전체 그림이 보입니다.

mmap_lock 경합과 DB

리눅스 커널의 mmap_lock(이전 mmap_sem)은 프로세스의 가상 메모리 맵을 보호하는 락으로, 높은 동시성 DB에서 의외로 많은 경합을 유발합니다.

mmap_lock 트리거 상황DB 상황영향완화 방법
page fault 처리DB 프로세스 시작, 새 연결 생성짧은 read-lockhuge pages로 fault 횟수 감소
mmap/munmap 호출동적 메모리 할당 (malloc → mmap)write-lock (독점)DB 메모리 사전 할당
fork() 호출PostgreSQL checkpoint forkwrite-lock 장기간huge pages, COW 최소화
/proc/PID/maps 읽기모니터링 에이전트read-lock (빈번 시 경합)모니터링 주기 조정
mremap 호출동적 공유 메모리 확장write-lock사전 할당
# mmap_lock 경합 직접 측정
bpftrace -e '
kprobe:down_read
/comm == "postgres" && arg0 == *kaddr("current->mm->mmap_lock")/
{
    @read_start[tid] = nsecs;
}

kretprobe:down_read
/comm == "postgres" && @read_start[tid]/
{
    @mmap_read_lat = hist((nsecs - @read_start[tid]) / 1000);
    delete(@read_start[tid]);
}

interval:s:10 { exit(); }
'

# 간단한 대안: perf로 lock 관련 함수 프로파일링
perf top -p $(pgrep -x postgres | head -1) -g
# down_read/down_write 관련 함수가 상위에 있으면 mmap_lock 경합

연결 수와 락 경합 관계

DB 연결 수가 증가하면 락 경합이 비선형적으로 증가합니다. 특히 CPU 코어 수를 초과하는 연결은 컨텍스트 스위치와 캐시 무효화를 추가로 유발합니다.

연결 수 / 코어락 경합 수준권장 전략
1~2x낮음기본 설정으로 충분
2~4x중간연결 풀러(pgbouncer/ProxySQL) 도입 검토
4x 이상높음연결 풀러 필수, max_connections 제한
10x 이상극심서비스 분리 또는 읽기 전용 레플리카
PostgreSQL max_connections 함정: PostgreSQL의 max_connections=1000은 1000개의 프로세스를 생성합니다. 16코어 서버에서 1000개의 프로세스가 동시에 활성화되면 futex 경합, 컨텍스트 스위치, 캐시 스래싱으로 성능이 급격히 떨어집니다. max_connections=200 + pgbouncer(pool_mode=transaction)가 거의 항상 더 나은 성능을 보입니다.

NVMe 최적화

NVMe SSD는 멀티큐 아키텍처로 높은 병렬 I/O를 지원하지만, 커널과 DB 설정이 이를 활용하지 못하면 성능을 발휘할 수 없습니다. NVMe의 하드웨어 특성을 이해하고 커널 블록 계층 설정을 최적화해야 합니다.

NVMe 멀티큐 아키텍처 (DB 관점) DB Worker 0 CPU 0 DB Worker 1 CPU 1 DB Worker 2 CPU 2 ... DB Worker N CPU N SW Queue 0 SW Queue 1 SW Queue 2 SW Queue N blk-mq (per-CPU) NVMe HW Queue 0 SQ + CQ pair NVMe HW Queue 1 SQ + CQ pair ... NVMe HW Queue N SQ + CQ pair NVMe Controller 인터럽트 코얼레싱 | 폴링 모드 | FUA/Flush 처리 NAND Flash (TLC/QLC) DB 최적화 포인트 1. 큐 깊이 = DB 동시성 2. IRQ affinity = NUMA 3. scheduler = none 4. io_poll = 저지연

I/O 스케줄러 비교

스케줄러특성NVMe DB 적합성설정 방법
none (noop)스케줄링 없이 바로 dispatchNVMe에서 권장 (내부 FTL 스케줄링)echo none > /sys/block/nvme0n1/queue/scheduler
mq-deadline읽기 우선, deadline 보장읽기/쓰기 혼합 워크로드에서 유용echo mq-deadline > ...
kyber지연 기반 토큰 스로틀링지연 민감 워크로드에서 실험적echo kyber > ...
bfq공정 대역폭 할당DB에는 부적합 (오버헤드 큼)DB 서버에서는 사용 비추천
# NVMe DB 최적화 설정

# 1) I/O 스케줄러: none (NVMe는 내부 FTL이 스케줄링)
echo none > /sys/block/nvme0n1/queue/scheduler

# 2) 큐 깊이 확인 및 조정
cat /sys/block/nvme0n1/queue/nr_requests     # SW 큐 깊이
cat /sys/class/nvme/nvme0/queue_count        # HW 큐 수
nvme id-ctrl /dev/nvme0 -o json | jq '.sqes, .cqes, .mqes'

# 3) 인터럽트 코얼레싱 (NVMe 장치별 지원 여부 확인)
nvme get-feature /dev/nvme0 -f 8   # Interrupt Coalescing
# 높은 IOPS DB에서는 코얼레싱을 낮춰 지연 감소
nvme set-feature /dev/nvme0 -f 8 -v 0x00010001  # 시간=1, 임계값=1

# 4) IRQ affinity를 DB NUMA 노드에 맞춤
# /proc/irq/*/smp_affinity를 DB CPU 코어에 설정
for irq in $(grep nvme /proc/interrupts | awk -F: '{print $1}'); do
    echo "00ff" > /proc/irq/$irq/smp_affinity  # CPU 0-7
done

# 5) 폴링 모드 (io_uring과 함께 사용)
# io_uring IORING_SETUP_IOPOLL 플래그 사용 시
# NVMe 드라이버가 인터럽트 대신 폴링으로 완료 확인
cat /sys/block/nvme0n1/queue/io_poll        # 1이면 활성화
cat /sys/block/nvme0n1/queue/io_poll_delay  # 폴링 시작 지연(ns)

# 6) readahead 조정 (random I/O DB에서는 줄이기)
blockdev --setra 32 /dev/nvme0n1  # 16KB (기본 128 = 64KB)
NVMe over Fabrics: 원격 NVMe 스토리지를 사용하는 경우(NVMe-oF/TCP, NVMe-oF/RDMA), 네트워크 지연이 추가됩니다. NVMe-oF/TCP는 ~50us, NVMe-oF/RDMA는 ~10us 추가 지연이 일반적입니다. DB에서 NVMe-oF를 사용할 때는 큐 깊이를 높이고 배치 제출을 적극 활용해 네트워크 왕복 비용을 상쇄해야 합니다.

NVMe 네임스페이스와 DB 분리

NVMe 네임스페이스(Namespace)는 하나의 NVMe 컨트롤러를 여러 독립적인 블록 디바이스로 분리하는 기능입니다. DB의 WAL, 데이터 파일, 임시 파일을 별도 네임스페이스에 배치하면 I/O 간섭을 줄일 수 있습니다.

# NVMe 네임스페이스 목록 확인
nvme list

# 네임스페이스 생성 (컨트롤러가 지원하는 경우)
# 주의: 모든 NVMe 장치가 네임스페이스 관리를 지원하지는 않음
nvme id-ctrl /dev/nvme0 | grep oacs
# oacs 비트 3이 설정되어 있으면 NS 관리 지원

# NVMe SMART/Health 정보 확인
nvme smart-log /dev/nvme0
# critical_warning, temperature, available_spare, data_units_written 확인
# available_spare가 낮으면 SSD 수명 경고

# NVMe 쓰기 증폭 (WAF) 계산
# data_units_written(NAND) / host_writes(호스트) 비율
# WAF > 3이면 GC/compaction 오버헤드가 큰 것

NVMe 성능 프로파일링

지표확인 방법정상 범위 (NVMe)DB 영향
4KB Random Read 지연fio --rw=randread --bs=4k50~100us (p99)인덱스 lookup 속도
4KB Random Write 지연fio --rw=randwrite --bs=4k20~50us (p99)데이터 페이지 쓰기
8KB Sequential Write + fsyncfio --rw=write --bs=8k --fdatasync=150~200us (p99)WAL commit 지연의 하한선
IOPS (4KB randread)fio --rw=randread --iodepth=128500K~1M+최대 처리량 한계
큐 깊이별 지연fio iodepth 변화시키며 측정iodepth 32까지 선형최적 동시성 지점 결정
Flush/FUA 지연fio --fdatasync=130~150uscommit 지연의 핵심 요소
# NVMe DB 워크로드 벤치마크 세트

# 1) WAL 패턴: 순차 쓰기 + fsync (commit 지연 측정)
fio --name=wal_bench --filename=/data/wal.test \
    --rw=write --bs=8k --ioengine=psync --fdatasync=1 \
    --iodepth=1 --numjobs=1 --runtime=60 --time_based \
    --group_reporting --percentile_list=50:90:95:99:99.9

# 2) 데이터 파일 패턴: 랜덤 읽기/쓰기 혼합
fio --name=data_bench --filename=/data/data.test \
    --rw=randrw --rwmixread=70 --bs=16k --ioengine=io_uring \
    --iodepth=64 --numjobs=8 --runtime=120 --time_based \
    --group_reporting --percentile_list=50:90:95:99:99.9

# 3) Compaction 패턴 (LSM DB): 순차 읽기 + 순차 쓰기
fio --name=compact_bench --filename=/data/compact.test \
    --rw=rw --rwmixread=50 --bs=256k --ioengine=io_uring \
    --iodepth=32 --numjobs=4 --runtime=60 --time_based \
    --group_reporting

# 4) 큐 깊이별 지연 곡선 측정
for qd in 1 2 4 8 16 32 64 128; do
    echo "=== QD=$qd ==="
    fio --name=qd_test --filename=/data/qd.test \
        --rw=randread --bs=4k --ioengine=io_uring \
        --iodepth=$qd --numjobs=1 --runtime=30 --time_based \
        --group_reporting --minimal
done

write cache와 FUA (Force Unit Access)

NVMe 장치의 write cache는 성능을 높이지만, 전원 손실 시 데이터 유실 위험이 있습니다. DB는 이를 flush 명령이나 FUA 비트로 제어합니다.

방식동작지연안전성적합 상황
Write + Flush쓰기 후 별도 flush 명령높음 (flush는 전체 캐시 비움)안전다수 쓰기 후 한 번 flush
Write + FUA쓰기 시 FUA 비트 설정중간 (해당 쓰기만 즉시 영속)안전WAL 커밋 등 개별 영속화
Write (cache only)write cache에만 기록최소위험 (전원 손실 시 유실)PLP(Power Loss Protection) 있는 경우만
# NVMe write cache 상태 확인
cat /sys/block/nvme0n1/queue/write_cache
# write back: 캐시 활성화됨
# write through: 캐시 비활성화 (또는 미지원)

# FUA 지원 여부 확인
cat /sys/block/nvme0n1/queue/fua
# 1: FUA 지원됨

# 커널 로그에서 NVMe 정보 확인
dmesg | grep nvme
# "VWC=1" 이면 Volatile Write Cache 있음 → flush/FUA 필수
PLP(Power Loss Protection) 확인: 엔터프라이즈 NVMe SSD는 대부분 PLP(슈퍼 커패시터)를 탑재하여 전원 손실 시에도 write cache를 NAND에 기록합니다. PLP가 있으면 write cache를 활성화한 상태에서도 안전합니다. 그러나 컨슈머 NVMe는 PLP가 없으므로, DB 서버에서는 반드시 엔터프라이즈급 SSD를 사용하세요.

PostgreSQL 커널 튜닝

PostgreSQL은 Buffered I/O를 기본으로 사용하므로 커널 페이지 캐시와의 상호작용이 성능에 큰 영향을 줍니다. shared_buffers, 체크포인트, WAL 동기화를 커널 파라미터와 함께 최적화해야 합니다.

shared_buffers와 페이지 캐시 관계

PostgreSQL의 shared_buffers는 DB 자체 캐시이지만, Buffered I/O를 사용하므로 커널 페이지 캐시에도 동일 데이터가 캐시됩니다. 이중 캐시 문제를 관리하는 것이 PostgreSQL 메모리 튜닝의 핵심입니다.

# PostgreSQL 전용 sysctl 최적화 설정

# === 메모리 관리 ===
# shared_buffers 16GB 기준
vm.swappiness = 1                    # 스왑 최소화 (0은 OOM 위험)
vm.overcommit_memory = 2             # 오버커밋 금지 (PostgreSQL 권장)
vm.overcommit_ratio = 90             # 물리 메모리의 90%까지 허용

# === Dirty/Writeback 제어 ===
vm.dirty_background_bytes = 268435456  # 256MB (백그라운드 writeback 시작)
vm.dirty_bytes = 1073741824           # 1GB (프로세스 블로킹 시작)
vm.dirty_writeback_centisecs = 100   # 1초 (writeback 주기)
vm.dirty_expire_centisecs = 500      # 5초 (dirty 만료)

# === Huge Pages (shared_buffers용) ===
vm.nr_hugepages = 8400   # shared_buffers 16GB / 2MB + 여유분

# === NUMA ===
kernel.numa_balancing = 0   # 수동 NUMA 바인딩 사용 시

# === 스케줄러 ===
kernel.sched_migration_cost_ns = 5000000   # 마이그레이션 억제
kernel.sched_autogroup_enabled = 0         # 자동 그룹핑 비활성화

# === 네트워크 (원격 접속 최적화) ===
net.core.somaxconn = 4096           # listen 백로그
net.ipv4.tcp_max_syn_backlog = 4096  # SYN 백로그

# === 세마포어/공유 메모리 ===
kernel.shmmax = 17179869184   # 16GB (shared_buffers 이상)
kernel.shmall = 4194304       # shmmax / PAGE_SIZE
kernel.sem = 250 32000 100 128  # PostgreSQL 세마포어 요구

PostgreSQL 핵심 커널 파라미터

파라미터권장값영향PostgreSQL 연관 설정
vm.swappiness1스왑 방지 → shared_buffers 보호shared_buffers 크기
vm.dirty_background_bytes256MB체크포인트 writeback 파형 완화checkpoint_completion_target
vm.dirty_bytes1GBdirty 폭증 시 프로세스 블로킹 임계값max_wal_size, checkpoint_timeout
vm.overcommit_memory2OOM killer 방지shared_buffers + 연결 수
vm.nr_hugepagesshared_buffers/2MB + 여유TLB 미스 감소, 안정적 메모리huge_pages = on
kernel.shmmaxshared_buffers 이상System V 공유 메모리 최대 크기shared_buffers
vm.zone_reclaim_mode0NUMA 영역 회수 비활성화NUMA 환경에서 안정성

WAL 동기화와 Checkpoint 최적화

# WAL fsync 지연 모니터링
# pg_stat_wal (PostgreSQL 14+)
psql -c "SELECT wal_sync, wal_sync_time, wal_write, wal_write_time FROM pg_stat_wal;"

# 체크포인트 통계
psql -c "SELECT * FROM pg_stat_bgwriter;"
# checkpoints_timed: 시간 기반 체크포인트 횟수
# checkpoints_req: 요청 기반 (WAL 크기 초과) ← 이 값이 높으면 max_wal_size 확대
# checkpoint_write_time: 쓰기 시간 (ms)
# checkpoint_sync_time: sync 시간 (ms) ← 이 값이 크면 스토리지 병목

# fio로 WAL 패턴 시뮬레이션
fio --name=pg_wal --filename=/data/pg_wal/test \
    --rw=write --bs=8k --ioengine=psync --fdatasync=1 \
    --iodepth=1 --numjobs=1 --runtime=60 --time_based=1
# fdatasync latency가 p99 commit 지연의 하한선
checkpoint 스파이크 방지: checkpoint_completion_target=0.9로 설정하면 체크포인트 시간의 90%에 걸쳐 쓰기를 분산합니다. 하지만 max_wal_size가 작으면 빈번한 체크포인트로 분산 효과가 줄어듭니다. NVMe 기준 max_wal_size=16GB, checkpoint_timeout=15min을 시작점으로 권장합니다.

PostgreSQL 메모리 맵과 커널 상호작용

PostgreSQL은 System V 공유 메모리 또는 mmap 기반으로 shared_buffers를 할당합니다. 이 영역의 커널 수준 동작을 이해하면 메모리 관련 성능 문제를 빠르게 진단할 수 있습니다.

PostgreSQL 메모리 영역커널 매핑특성커널 튜닝
shared_buffersshmget/mmap (MAP_SHARED)모든 백엔드가 공유, huge pages 대상vm.nr_hugepages, kernel.shmmax
work_mem (쿼리별)malloc → mmap (private)정렬/해시 작업 메모리, 프로세스당 할당vm.overcommit_memory
maintenance_work_memmalloc → mmap (private)VACUUM, CREATE INDEX 등같은 설정
WAL buffers공유 메모리 내WAL 쓰기 버퍼, shared_buffers와 함께 할당shared memory 설정
temp_buffersmalloc (private)임시 테이블 캐시, 세션별기본 메모리 관리
# PostgreSQL 공유 메모리 확인
ipcs -m  # System V 공유 메모리 세그먼트

# 또는 mmap 기반 (PostgreSQL 12+)
ls -la /dev/shm/PostgreSQL.*

# Huge Pages 할당 상태 확인
grep HugePages /proc/meminfo
# HugePages_Total:   8400      ← 설정한 수
# HugePages_Free:     208      ← 미사용
# HugePages_Rsvd:     100      ← 예약됨
# HugePages_Surp:       0      ← 초과 할당

# PostgreSQL이 Huge Pages를 사용하는지 확인
grep -i huge /proc/$(pgrep -x postgres | head -1)/smaps | head
# AnonHugePages: 값이 있으면 THP 사용 중 (비권장)
# 정상은 ShmemHugePages만 있어야 함

PostgreSQL 장애 분석 체크리스트

증상커널 확인 항목PostgreSQL 확인 항목긴급 조치
commit 지연 급등iostat await, biolatency, PSI iopg_stat_wal, checkpoint 통계배치 I/O 제한, dirty_bytes 축소
쿼리 지연 산포 확대runqlat, vmstat cs/si, cachestatpg_stat_activity wait_eventisolcpus, cgroup CPU 분리
OOM kill 발생dmesg, /proc/meminfo, oom_scoreshared_buffers + 연결 수 메모리oom_score_adj=-1000, 연결 제한
Vacuum 지연 누적I/O 경합, dirty throttlepg_stat_user_tables dead_tuplesautovacuum 리소스 상향, cgroup 분리
복제 지연 확대네트워크 지연, CPU 경합, I/O utilpg_stat_replication write_lagapply 워커 CPU 분리, I/O 대역폭 확보

MySQL/InnoDB 커널 튜닝

MySQL InnoDB는 O_DIRECT를 기본으로 사용하여 페이지 캐시 이중 버퍼링을 회피합니다. 이로 인해 커널 메모리 관리보다는 블록 I/O, AIO, doublewrite 성능이 중요해집니다.

innodb_flush_method와 커널 상호작용

flush_method데이터 파일redo 로그커널 경로권장 환경
O_DIRECTO_DIRECT + fsyncfsync페이지 캐시 우회, 직접 블록 I/O대부분의 프로덕션 (기본값)
O_DIRECT_NO_FSYNCO_DIRECT (fsync 생략)fsync데이터 파일 fsync 제거배터리 백업 RAID, NVMe PLP
O_DSYNCO_DSYNCO_DSYNC동기 I/O (write + 즉시 sync)특수 환경
fsyncbuffered + fsyncfsync페이지 캐시 경유테스트/개발 환경
# MySQL InnoDB 전용 sysctl 최적화 설정

# === 메모리 관리 ===
vm.swappiness = 1                    # 스왑 최소화
# O_DIRECT 사용으로 페이지 캐시 의존도 낮음
# innodb_buffer_pool_size가 메모리의 주요 소비자

# === Dirty/Writeback (redo log용) ===
vm.dirty_background_bytes = 134217728  # 128MB
vm.dirty_bytes = 536870912            # 512MB
vm.dirty_writeback_centisecs = 100    # 1초

# === I/O 스케줄러 ===
# NVMe: none, SATA SSD: mq-deadline

# === 파일 디스크립터 ===
fs.file-max = 6553560
fs.aio-max-nr = 1048576   # innodb_use_native_aio=ON용

# === 네트워크 ===
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 4096

# === THP 비활성화 ===
# echo never > /sys/kernel/mm/transparent_hugepage/enabled

MySQL 핵심 커널 파라미터

파라미터권장값영향MySQL 연관 설정
vm.swappiness1buffer pool 스왑 방지innodb_buffer_pool_size
fs.aio-max-nr1048576Native AIO 요청 상한innodb_use_native_aio=ON
vm.dirty_background_bytes128MBredo log writeback 제어innodb_flush_log_at_trx_commit
I/O 스케줄러none (NVMe)불필요한 스케줄링 제거innodb_io_capacity
readahead32 sectors (16KB)random I/O 워크로드에 맞춤innodb_read_ahead_threshold
nr_requests128~256SW 큐 깊이innodb_read_io_threads + write_io_threads
THPnevercompaction stall 방지innodb_buffer_pool_size

Doublewrite와 파일시스템

InnoDB의 doublewrite buffer는 partial write(torn page)를 방지합니다. 파일시스템 선택에 따라 doublewrite의 오버헤드가 달라집니다.

파일시스템Doublewrite 필요성이유권장 마운트 옵션
ext4필요 (기본 ON)data=ordered에서도 partial write 가능noatime,nobarrier (배터리 RAID만)
XFS필요 (기본 ON)메타데이터만 저널링, 데이터 partial write 가능noatime,allocsize=16m
ZFS불필요 (OFF 가능)COW 방식으로 partial write 불가ZFS 자체 설정
Btrfs불필요 (OFF 가능)COW 방식으로 partial write 불가noatime,ssd
# InnoDB doublewrite 상태 확인
mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_dblwr%';"
# Innodb_dblwr_pages_written: 기록된 페이지 수
# Innodb_dblwr_writes: 기록 횟수
# 비율이 높으면 (writes가 많으면) doublewrite가 병목 가능

# MySQL 8.0.20+: 병렬 doublewrite
# innodb_doublewrite_dir, innodb_doublewrite_files, innodb_doublewrite_batch_size

# 파일시스템별 마운트 확인
mount | grep '/var/lib/mysql'
# ext4: data=ordered,noatime
# XFS: noatime,allocsize=16m
Native AIO 확인: InnoDB의 innodb_use_native_aio=ON은 리눅스 네이티브 AIO(io_submit/io_getevents)를 사용합니다. fs.aio-max-nr이 부족하면 AIO 요청이 실패하고 동기 I/O로 폴백합니다. dmesg에서 "aio: nr_events exceeds max" 메시지를 확인하세요.

MySQL InnoDB 스레드 모델과 커널

InnoDB는 멀티스레드 모델을 사용하며, 각 스레드 유형이 커널과 상호작용하는 방식이 다릅니다.

InnoDB 스레드역할커널 상호작용튜닝 포인트
Connection Thread클라이언트 쿼리 처리futex (InnoDB mutex), read/write syscallthread_cache_size, max_connections
IO Read Thread비동기 읽기 (prefetch)io_submit (native AIO) 또는 preadinnodb_read_io_threads
IO Write Thread비동기 쓰기 (flush)io_submit 또는 pwrite + O_DIRECTinnodb_write_io_threads
Log Threadredo log 쓰기pwrite + fsync/fdatasyncinnodb_flush_log_at_trx_commit
Page Cleanerdirty page flushpwrite (O_DIRECT) + fsyncinnodb_page_cleaners, innodb_io_capacity
Purge Threadundo 정리CPU + 읽기 I/Oinnodb_purge_threads
# InnoDB 스레드별 I/O 패턴 확인
pidstat -d -t -p $(pgrep -x mysqld) 1 5
# TID별 kB_rd/s, kB_wr/s를 확인하여 어떤 스레드가 I/O를 소비하는지 식별

# InnoDB 내부 상태 확인
mysql -e "SHOW ENGINE INNODB STATUS\G" | grep -A 20 "FILE I/O"
# Pending normal aio reads: X, aio writes: Y
# 값이 높으면 I/O 병목

# AIO 컨텍스트 사용량 확인
cat /proc/sys/fs/aio-nr    # 현재 사용 중인 AIO 컨텍스트
cat /proc/sys/fs/aio-max-nr # 최대 허용
# aio-nr이 aio-max-nr에 근접하면 fs.aio-max-nr 상향 필요

MySQL 장애 분석 체크리스트

증상커널 확인 항목MySQL 확인 항목긴급 조치
트랜잭션 지연 급등iostat await, biolatency, PSI ioSHOW ENGINE INNODB STATUS (log waits)배치 I/O 제한, io_capacity 확인
Flush stormdirty pages 급증 → writeback burstinnodb_buffer_pool_dirty 비율io_capacity 상향, dirty_bytes 조정
Replication lagCPU/IO util, 네트워크 지연SHOW SLAVE STATUS (Seconds_Behind)apply thread CPU 분리, I/O 대역폭 확보
AIO 실패dmesg "aio" 메시지, fs.aio-max-nrerror log "native aio"fs.aio-max-nr 상향
OOM killdmesg, /proc/meminfobuffer_pool_size + 연결 수oom_score_adj, 연결 제한

컨테이너 환경 DB

컨테이너(Docker, Kubernetes)에서 DB를 운영하면 격리, 배포, 확장성의 이점이 있지만, cgroup 제한, overlay 파일시스템, 네트워크 오버헤드 등 추가적인 커널 계층이 성능에 영향을 줍니다.

컨테이너 환경 DB 아키텍처 Host Kernel cgroup v2 controller namespace isolation blk-mq / NVMe driver overlay2 / devicemapper / bind mount DB Container (Primary) PostgreSQL / MySQL Buffer Pool + WAL memory.max io.max / io.weight bind mount: /data/pgdata overlay 우회 (직접 접근) host network (--net=host) DB Container (Replica) Read Replica WAL Receiver + Apply memory.max io.max / io.weight bind mount: /data/replica overlay 우회 Sidecar / Monitoring Exporter (Prometheus) Log Collector 낮은 리소스 제한

cgroup v2 DB 설정

# Kubernetes Pod에서 cgroup v2 리소스 제한 예시
# (실제로는 Pod spec으로 설정하지만, 커널 관점에서 cgroup으로 매핑됨)

# === 메모리 제어 ===
# memory.max: 하드 리밋 (OOM 발생)
echo 64G > /sys/fs/cgroup/db-container/memory.max
# memory.high: 소프트 리밋 (throttle 시작)
echo 60G > /sys/fs/cgroup/db-container/memory.high
# memory.low: 최소 보장 (reclaim 보호)
echo 48G > /sys/fs/cgroup/db-container/memory.low
# memory.swap.max: 스왑 금지
echo 0 > /sys/fs/cgroup/db-container/memory.swap.max

# === I/O 제어 ===
# I/O weight (100=기본, 10000=최대)
echo "default 1000" > /sys/fs/cgroup/db-container/io.weight
# 배치 컨테이너 I/O 상한
DEV_MAJMIN="259:0"  # NVMe 디바이스
echo "$DEV_MAJMIN rbps=max wbps=209715200 riops=max wiops=max" \
    > /sys/fs/cgroup/batch-container/io.max

# === CPU 제어 ===
echo 10000 > /sys/fs/cgroup/db-container/cpu.weight
# cpuset으로 NUMA 노드 지정
echo "0-15" > /sys/fs/cgroup/db-container/cpuset.cpus
echo "0" > /sys/fs/cgroup/db-container/cpuset.mems

# === OOM 우선순위 ===
# DB는 OOM 대상에서 보호
echo -1000 > /proc/$(pgrep -x postgres | head -1)/oom_score_adj

overlay2 I/O 패널티와 회피

스토리지 방식I/O 패널티DB 적합성권장 사항
overlay2 (컨테이너 기본)높음 (COW, 메타데이터 오버헤드)부적합DB 데이터에 사용 금지
bind mount (호스트 경로)없음 (직접 접근)적합DB 데이터는 반드시 bind mount 사용
local PV (Kubernetes)없음적합NVMe를 local PV로 마운트
네트워크 PV (Ceph, EBS)네트워크 지연 추가조건부지연 허용 범위 내에서 사용
# Docker에서 DB 컨테이너 실행 (최적화 예시)
docker run -d \
  --name postgres-prod \
  --net=host \                    # 네트워크 오버헤드 제거
  --cpuset-cpus="0-15" \          # NUMA Node 0에 고정
  --memory=64g \                  # 메모리 상한
  --memory-swap=64g \             # 스왑 금지 (memory와 동일)
  --oom-kill-disable \            # OOM 킬 방지
  -v /data/pgdata:/var/lib/postgresql/data \  # bind mount
  -v /data/pg_wal:/var/lib/postgresql/wal \   # WAL 별도 마운트
  --ulimit nofile=65536:65536 \   # 파일 디스크립터
  --ulimit memlock=-1:-1 \        # huge pages용
  postgres:16
컨테이너 DB OOM 주의: cgroup memory.max에 도달하면 커널 OOM killer가 컨테이너 내 프로세스를 강제 종료합니다. DB 프로세스가 종료되면 데이터 손실 위험이 있습니다. memory.low로 최소 보장을 설정하고, memory.high로 throttle 구간을 두어 OOM 전에 경고할 수 있도록 하세요. Kubernetes에서는 resources.requestsresources.limits를 적절히 분리하세요.

Kubernetes DB 배포 시 커널 고려사항

항목문제점권장 설정Kubernetes 구성
네트워크CNI 오버레이 네트워크 지연 (~50us 추가)hostNetwork: truePod spec에 hostNetwork 설정
스토리지overlay2 COW 오버헤드local PV + bind mounthostPath 또는 local PV
NUMA자동 스케줄링으로 NUMA 불일치Topology Manager + static policykubelet --topology-manager-policy=best-effort
Huge Pages기본 미지원hugepages 리소스 요청resources: hugepages-2Mi: 16Gi
THP노드 수준 설정 필요DaemonSet으로 THP 비활성화init container 또는 DaemonSet
ulimits기본값 부족nofile, memlock 상향securityContext 또는 init container
# Kubernetes StatefulSet DB 배포 예시 (핵심 부분만)
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres-primary
spec:
  serviceName: postgres
  replicas: 1
  template:
    spec:
      hostNetwork: true          # CNI 오버헤드 제거
      nodeSelector:
        node-role: database      # DB 전용 노드
      tolerations:
      - key: database-only
        operator: Exists
      initContainers:
      - name: disable-thp
        image: busybox
        command: ["sh", "-c", "echo never > /sys/kernel/mm/transparent_hugepage/enabled"]
        securityContext:
          privileged: true
        volumeMounts:
        - name: sys
          mountPath: /sys
      containers:
      - name: postgres
        image: postgres:16
        resources:
          requests:
            memory: 48Gi
            cpu: "8"
            hugepages-2Mi: 16Gi  # shared_buffers용
          limits:
            memory: 64Gi
            cpu: "16"
            hugepages-2Mi: 16Gi
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
        - name: wal
          mountPath: /var/lib/postgresql/wal
        - name: hugepages
          mountPath: /dev/hugepages
      volumes:
      - name: data
        hostPath:
          path: /data/pgdata    # NVMe bind mount
      - name: wal
        hostPath:
          path: /data/pgwal     # WAL 별도 마운트
      - name: hugepages
        emptyDir:
          medium: HugePages
      - name: sys
        hostPath:
          path: /sys

컨테이너 환경 성능 비교

구성I/O 오버헤드네트워크 오버헤드메모리 오버헤드관리 편의성
베어메탈없음없음없음낮음
컨테이너 (최적화)~1% (bind mount)~0% (host network)~1%높음
컨테이너 (기본)~5-15% (overlay2)~3-5% (CNI)~2%높음
VM (KVM)~3-5% (virtio)~2-3% (virtio-net)고정 할당중간
컨테이너 DB 핵심 원칙: 컨테이너의 격리와 배포 편의성을 활용하되, I/O 경로에서는 가능한 한 추상화 계층을 줄이세요. 데이터 볼륨은 반드시 bind mount/local PV로, 네트워크는 host network로, 메모리는 huge pages를 직접 매핑하세요. 이 세 가지를 지키면 베어메탈 대비 1~2% 이내의 오버헤드로 컨테이너 DB를 운영할 수 있습니다.

BPF 관측성

BPF(Berkeley Packet Filter) 기반 도구는 커널 내부를 실시간으로 관측하면서도 오버헤드가 매우 낮아, 프로덕션 DB 서버에서도 안전하게 사용할 수 있습니다. BCC와 bpftrace를 활용한 DB 성능 분석 방법을 정리합니다.

DB 관련 BPF 도구 목록

도구패키지용도DB 활용 시나리오
biolatencyBCC블록 I/O 지연 히스토그램fsync/read 지연 분포 분석
biosnoopBCC개별 블록 I/O 요청 추적느린 I/O 요청 식별
ext4slower / xfsslowerBCC느린 파일시스템 작업 추적fsync, read, write 느린 작업
runqlatBCCrunqueue 대기 시간 히스토그램DB 워커 CPU 스케줄 대기
tcplifeBCCTCP 연결 수명 추적DB 클라이언트 연결 패턴
cachestatBCC페이지 캐시 hit/miss 통계Buffered I/O DB의 캐시 효율
funclatencyBCC특정 함수 지연 히스토그램vfs_fsync_range 등 커널 함수 지연
offcputimeBCCoff-CPU 시간 + 스택 추적DB 스레드 블로킹 원인 분석
syscountBCC시스템 콜 빈도 카운트DB의 주요 시스템 콜 패턴
filetopBCC파일별 I/O 순위어떤 DB 파일이 가장 많은 I/O를 유발하는지
# === 주요 BPF 도구 사용 예제 ===

# 1) 블록 I/O 지연 분포 (10초간)
biolatency-bpfcc -D 10
# usecs      : count    distribution
#   0 -> 1   : 0       |                    |
#   2 -> 3   : 0       |                    |
#   4 -> 7   : 12      |***                 |
#   8 -> 15  : 1847    |********************|  ← NVMe 주 분포
#  16 -> 31  : 523     |*****               |
# 128 -> 255 : 3       |                    |  ← tail latency

# 2) 느린 ext4 작업 추적 (1ms 이상)
ext4slower-bpfcc 1
# TIME     COMM     PID   T BYTES   OFF_KB   LAT(ms) FILENAME
# 14:23:01 postgres 1234  S 0       0        12.34   000000000000001A
# T=S는 fsync, R=read, W=write

# 3) DB 프로세스의 runqueue 대기 시간
runqlat-bpfcc -p $(pgrep -x postgres | head -1) 10
# runqueue 대기가 길면 CPU 경합 → isolcpus/cpuset 고려

# 4) 페이지 캐시 hit/miss 통계 (1초 간격)
cachestat-bpfcc 1
# HITS    MISSES  DIRTIES  HITRATIO  BUFFERS_MB
# 45231   123     89       99.73%    1024
# MISSES가 높으면 working set > 메모리, O_DIRECT 고려

# 5) TCP 연결 수명 추적
tcplife-bpfcc -p $(pgrep -x postgres | head -1)
# 짧은 연결이 많으면 연결 풀 미사용 또는 누수

# 6) DB off-CPU 시간 분석 (어디서 블로킹되는지)
offcputime-bpfcc -p $(pgrep -x postgres | head -1) 10
# futex_wait, io_schedule 등이 보이면 각각 락 경합, I/O 대기

BPF 기반 DB 성능 분석 워크플로우

BPF 도구를 체계적으로 활용하면 DB 성능 문제를 커널 수준에서 빠르게 진단할 수 있습니다. 아래 워크플로우를 따르면 대부분의 병목을 15분 이내에 식별할 수 있습니다.

BPF 기반 DB 성능 분석 워크플로우 1. 증상 확인 PSI, vmstat, iostat CPU/IO/Memory 분류 2. 계층 분리 biolatency, runqlat cachestat, ext4slower 3. 상세 추적 biosnoop, offcputime funclatency, trace 4. 원인 확정 커스텀 bpftrace stackcount, profile 증상별 BPF 도구 선택 I/O 지연 → 블록 분석 biolatency → biosnoop → blkthrot ext4slower/xfsslower → funclatency CPU 경합 → 스케줄링 분석 runqlat → offcputime → profile cpudist → funccount → stackcount 메모리 → 캐시/락 분석 cachestat → memleak → drsnoop oomkill → shmsnoop → futex 분석 결과: 커널 파라미터 조정 또는 DB 설정 변경 변경 후 동일한 BPF 도구로 효과 검증 → 반복

커스텀 bpftrace 프로그램 (DB 전용)

# PostgreSQL fsync 지연 분포 추적
bpftrace -e '
tracepoint:syscalls:sys_enter_fdatasync,
tracepoint:syscalls:sys_enter_fsync
/comm == "postgres"/
{
    @start[tid] = nsecs;
}

tracepoint:syscalls:sys_exit_fdatasync,
tracepoint:syscalls:sys_exit_fsync
/comm == "postgres" && @start[tid]/
{
    $lat = (nsecs - @start[tid]) / 1000;  /* us */
    @fsync_lat = hist($lat);
    if ($lat > 10000) {   /* 10ms 이상이면 경고 */
        printf("SLOW fsync: pid=%d lat=%d us\n", pid, $lat);
    }
    delete(@start[tid]);
}

interval:s:30 { exit(); }
'

# MySQL InnoDB redo log write 패턴 추적
bpftrace -e '
tracepoint:syscalls:sys_enter_pwrite64
/comm == "mysqld"/
{
    @write_size = hist(args->count);
    @write_fd[args->fd] = count();
}

interval:s:10 { exit(); }
'
BPF 도구 설치: Ubuntu/Debian에서는 apt install bpfcc-tools bpftrace, RHEL/CentOS에서는 dnf install bcc-tools bpftrace로 설치합니다. BPF 도구는 커널 4.9+ 필요하며, BTF(BPF Type Format) 지원 커널(5.2+)에서 더 강력하게 동작합니다. 프로덕션에서도 오버헤드 1% 미만으로 안전하게 사용할 수 있습니다.

Prometheus + Grafana BPF 통합

프로덕션 환경에서는 BPF 데이터를 Prometheus로 수집하여 Grafana 대시보드에서 시계열로 분석하는 것이 효과적입니다.

# bpftrace + textfile exporter 연동 예시
# cron으로 주기적 실행, node_exporter textfile 디렉토리에 결과 저장

#!/bin/bash
# /usr/local/bin/db-bpf-metrics.sh
OUTPUT=/var/lib/node_exporter/textfile/db_kernel_metrics.prom

# fsync 지연 p99 측정 (5초)
P99_FSYNC=$(timeout 5 bpftrace -e '
tracepoint:syscalls:sys_enter_fdatasync /comm == "postgres"/ { @s[tid] = nsecs; }
tracepoint:syscalls:sys_exit_fdatasync /comm == "postgres" && @s[tid]/ {
  @lat = hist((nsecs - @s[tid])/1000); delete(@s[tid]);
}
interval:s:5 { exit(); }' 2>/dev/null | grep -oP 'p99=\K[0-9]+' || echo 0)

# dirty 페이지 수
DIRTY_KB=$(grep "^Dirty:" /proc/meminfo | awk '{print $2}')

# Prometheus 포맷으로 출력
cat > $OUTPUT <<EOF
# HELP db_fsync_p99_us PostgreSQL fdatasync p99 latency in microseconds
# TYPE db_fsync_p99_us gauge
db_fsync_p99_us $P99_FSYNC
# HELP node_dirty_kb Dirty pages in KB
# TYPE node_dirty_kb gauge
node_dirty_kb $DIRTY_KB
EOF
Grafana 패널데이터 소스BPF 도구알람 조건
fsync p99 지연bpftrace 커스텀fdatasync tracepointp99 > 10ms 5분 지속
블록 I/O 히스토그램biolatency 출력biolatency1ms 이상 비율 > 5%
runqueue 대기runqlat 출력runqlatp99 > 100us
페이지 캐시 히트율cachestat 출력cachestat히트율 < 95%
Dirty 페이지/proc/meminfo없음 (직접 수집)dirty > dirty_bytes * 80%
NUMA 원격 접근perf / numastat없음miss 비율 > 10%
BPF 프로덕션 안전성: BPF 프로그램은 커널 검증기(verifier)를 통과해야 실행되므로 커널 크래시 위험이 없습니다. 오버헤드는 tracepoint 기반에서 ~1%, kprobe 기반에서 ~2-5%로, 대부분의 프로덕션 DB에서 안전하게 사용할 수 있습니다. 단, stackcountoffcputime 같은 스택 추적 도구는 오버헤드가 다소 높으므로 짧은 시간만 실행하세요.
# DB 장애 분석 원스톱 스크립트 (복사해서 사용)
#!/bin/bash
# db-diagnose.sh - DB 커널 레벨 진단

DB_PID=$(pgrep -x postgres | head -1)  # MySQL이면 pgrep -x mysqld
DURATION=10

echo "=== 1. Block I/O Latency ==="
timeout $DURATION biolatency-bpfcc -D $DURATION 2>/dev/null

echo "=== 2. Slow FS operations (>1ms) ==="
timeout $DURATION ext4slower-bpfcc 1 2>/dev/null

echo "=== 3. Runqueue Latency ==="
timeout $DURATION runqlat-bpfcc -p $DB_PID $DURATION 2>/dev/null

echo "=== 4. Page Cache Stats ==="
timeout $DURATION cachestat-bpfcc 1 $DURATION 2>/dev/null

echo "=== 5. PSI (Pressure Stall Info) ==="
cat /proc/pressure/cpu
cat /proc/pressure/memory
cat /proc/pressure/io

echo "=== 6. Dirty/Writeback ==="
grep -E "Dirty|Writeback|MemAvailable" /proc/meminfo

echo "=== 7. NUMA Stats ==="
numastat -p $DB_PID 2>/dev/null

echo "=== Complete ==="
진단 스크립트 활용 팁: 위 스크립트를 /usr/local/bin/db-diagnose.sh에 저장하고 실행 권한을 부여해두면 장애 시 즉시 사용할 수 있습니다. 출력을 파일로 저장하여 (db-diagnose.sh | tee /tmp/diag-$(date +%Y%m%d-%H%M%S).log) 사후 분석에도 활용하세요. 주기적으로 cron에 등록하면 기준선(baseline) 데이터도 축적할 수 있습니다.

이 문서에서 다룬 커널 서브시스템별 심화 내용은 아래 개별 문서에서 더 깊이 있게 다룹니다.