데이터베이스 워크로드와 Linux 커널
데이터베이스 워크로드를 Linux 커널의 I/O, 메모리, 스케줄링 정책과 연결해 심층 분석합니다. fsync·writeback·page cache·direct I/O 선택이 트랜잭션 지연과 내구성에 미치는 영향, io_uring/AIO와 큐 깊이 튜닝, cgroup 기반 리소스 격리, NUMA·THP·swap 정책이 쿼리 처리량에 주는 효과, 파일시스템 및 블록 계층 정렬 이슈, 장애 시 복구 시간 단축을 위한 커널 파라미터 전략, perf/eBPF/ftrace 기반 병목 분석 절차까지 DBA와 커널 엔지니어 협업에 필요한 실전 포인트를 다룹니다.
핵심 요약
- Page Cache — 파일 I/O를 메모리에 완충해 읽기 성능을 높입니다.
- fsync — 트랜잭션 로그를 영속화해 장애 시 복구 기준점을 만듭니다.
- O_DIRECT — 페이지 캐시를 우회해 DB 버퍼풀과 이중 캐시를 줄입니다.
- Writeback — dirty 페이지를 디스크로 밀어내며 지연 분포에 영향을 줍니다.
- NUMA Locality — CPU와 메모리 노드 배치가 tail latency를 좌우합니다.
단계별 이해
- I/O 경로 구분
DB 파일이 buffered I/O인지 direct I/O인지 먼저 확인합니다. - 영속성 지점 확인
WAL 로그 flush 경로에서 fsync 빈도와 그룹 커밋을 파악합니다. - 메모리/CPU 배치 점검
NUMA 바인딩과 cgroup 제한이 캐시 효율을 깨지 않는지 확인합니다. - 관측 후 조정
perf, iostat, PSI, eBPF로 병목 위치를 확인한 뒤 한 번에 하나씩 조정합니다.
DB와 커널이 만나는 핵심 지점
DB 워크로드에서 커널 영향이 가장 큰 경계면은 다음 여섯 가지입니다.
- 파일 I/O 경로:
read()/write()+ page cache 또는O_DIRECT경로 - 블록 계층:
blk-mq큐 깊이, I/O 스케줄러, 멀티큐 매핑 - 영속성 보장:
fsync(), 저널링, 장치 캐시 flush 순서 - 메모리 회수: reclaim, writeback, dirty throttle이 쿼리 지연에 미치는 영향
- CPU/NUMA: 실행 스레드와 메모리 노드 불일치로 인한 원격 접근 지연
- 리소스 격리: cgroup v2의
memory.high,io.max,cpu.max
트랜잭션 I/O 지연 경로
아래 다이어그램은 단순화한 commit 경로입니다. DB 엔진의 WAL 기록이 커널 계층을 통과하면서 어느 지점에서 tail latency가 증가하는지 파악할 수 있습니다.
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 write | fsync 묶음 크기, checkpoint spike | checkpoint_timeout, max_wal_size, writeback 패턴 |
| MySQL InnoDB | redo log + doublewrite + data file | flush 순서, doublewrite 오버헤드 | innodb_flush_log_at_trx_commit, innodb_io_capacity |
| RocksDB/LSM 계열 | WAL + SSTable compaction | 백그라운드 compaction이 foreground 지연을 오염 | compaction 스레드 격리, cgroup I/O 제한 |
| 분석계(컬럼 저장) | 대량 순차 read + 배치 write | readahead 과다/부족, 페이지 캐시 오염 | readahead 크기, 쿼리/배치 작업 분리 |
Buffered I/O vs O_DIRECT
DB에서 가장 큰 전략 선택은 페이지 캐시를 사용할지 여부입니다. 정답은 하나가 아니라 워크로드의 갱신 패턴과 복구 정책에 따라 달라집니다.
| 항목 | Buffered I/O | O_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
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은 완만하고 예측 가능하게 유지해야 합니다.
ext4/XFS 선택과 마운트 고려사항
ext4와 XFS 모두 DB 운용이 가능하지만, 파일 할당 정책과 메타데이터 동작 차이로 지연 분포가 달라질 수 있습니다.
| 항목 | ext4 | XFS |
|---|---|---|
| 강점 | 운영 경험 풍부, 보수적 동작 | 큰 파일/병렬 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 성능
- 이중 캐시 문제: DB 버퍼풀 + page cache가 동시에 커지면 reclaim 압력이 증가합니다.
- THP 정책: 워크로드에 따라 큰 페이지가 도움이 되거나 오히려 지연 변동을 키울 수 있습니다.
- NUMA 불일치: 스레드는 node0, 메모리는 node1에 있으면 원격 접근 비용이 누적됩니다.
- 스케줄링 잡음: 백그라운드 flush 스레드와 DB 워커가 같은 코어를 경합하면 p99가 악화됩니다.
# 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
재현 가능한 벤치마크 절차
- 기준선 확정: 커널 버전, DB 버전, 파일시스템, 마운트 옵션, 장치 펌웨어를 기록합니다.
- 웜업 구간 분리: 캐시 웜업과 측정 구간을 분리해 초기 편차를 제거합니다.
- 단일 변수 변경: 한 번에 한 항목만 변경하고 최소 3회 반복 측정합니다.
- 분포 지표 기록: 평균이 아니라 p95/p99/p999와 최대 복구 시간(RTO)을 함께 기록합니다.
- 회귀 기준 고정: 동일한 데이터셋, 동일한 쿼리 믹스, 동일한 동시성으로 비교합니다.
# 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
- 핵심 의도: checkpoint를 길게 펴서 writeback burst를 줄이고 WAL flush 지연을 짧게 유지
- 관측 지표:
pg_stat_bgwriter, checkpoint write/sync 시간, p99 commit latency - 실패 신호: checkpoint 직후 I/O wait 급증, autovacuum 지연 누적, WAL 디렉터리 포화
PostgreSQL 스토리지 프로파일
| 프로파일 | 권장 시작점 | 적용 이유 | 확인 지표 |
|---|---|---|---|
| NVMe 단일 디스크 | effective_io_concurrency=256random_page_cost=1.1checkpoint_completion_target=0.9 |
낮은 지연 기반으로 랜덤 I/O를 적극 활용 | p99 commit, checkpoint sync 시간, WAL flush 지연 |
| SATA SSD | effective_io_concurrency=32~64random_page_cost=1.3~1.8max_wal_size 보수적 상향 |
NVMe 대비 병렬성 한계가 낮아 체크포인트 파형을 더 완만하게 유지해야 함 | await, 디바이스 util, checkpoint 간 지연 편차 |
| RAID (HW/SW) | effective_io_concurrency=64~128max_wal_size 상향checkpoint_timeout=10~15min |
어레이 내부 캐시/스트라이프 특성으로 writeback 파형 완화 필요 | await, aqu-sz, %util, 배열 캐시 flush 주기, 체크포인트 편차 |
| 클라우드 블록 스토리지 (EBS) | effective_io_concurrency=16~64checkpoint_completion_target=0.9~0.95wal_compression=on |
네트워크 기반 저장장치에서 지연 변동과 크레딧 소진 영향을 완화 | p99/p999 fsync, I/O credit, queue depth, 네트워크 지연 |
| 클라우드 블록 스토리지 (Ceph RBD) | effective_io_concurrency=16~48checkpoint_completion_target=0.9~0.95wal_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
- 핵심 의도: redo/binlog 영속성 보장(1/1)을 유지한 채 백그라운드 플러시를 완만하게 제어
- 관측 지표: InnoDB log waits, buffer pool dirty 비율, flush list 길이, p99 trx latency
- 실패 신호: log write 대기 증가, flush storm, replication apply 지연 확대
MySQL InnoDB 스토리지 프로파일
| 프로파일 | 권장 시작점 | 적용 이유 | 확인 지표 |
|---|---|---|---|
| NVMe 단일 디스크 | innodb_flush_method=O_DIRECTinnodb_io_capacity=2000innodb_io_capacity_max=4000 |
높은 IOPS를 배경 flush가 따라가게 하되 foreground를 보호 | log waits, fsync 횟수, 디바이스 util, p99 trx latency |
| SATA SSD | innodb_io_capacity=800~1500innodb_io_capacity_max=1600~3000innodb_lru_scan_depth 보수 설정 |
과도한 flush 스레드 경쟁을 줄여 foreground 트랜잭션 지연을 안정화 | InnoDB log waits, flush list 길이, await, %util |
| RAID (HW/SW) | innodb_io_capacity=1000~2000innodb_lru_scan_depth 점진 조정sync_binlog=1 유지 |
배열 쓰기 증폭과 캐시 flush 주기를 고려해 과한 백그라운드 flush를 방지 | redo checkpoint age, flush list 길이, replication 지연 |
| 클라우드 블록 스토리지 (EBS) | innodb_io_capacity=600~1800innodb_flush_neighbors=0sync_binlog=1 유지 |
크레딧/보장 IOPS 특성에 맞춰 flush 파형을 안정화 | p99 trx latency, replication apply delay, I/O credit, queue depth |
| 클라우드 블록 스토리지 (Ceph RBD) | innodb_io_capacity=500~1500innodb_flush_neighbors=0sync_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
- 핵심 의도: WiredTiger 캐시와 커널 page cache의 경합을 줄여 지연 분포를 안정화
- 관측 지표: checkpoint 시간, journal commit 지연, cache eviction pressure, replication lag
- 실패 신호: eviction 급증, checkpoint 길어짐, secondary apply 지연 확대
MongoDB 스토리지 프로파일
| 프로파일 | 권장 시작점 | 적용 이유 | 확인 지표 |
|---|---|---|---|
| NVMe 단일 디스크 | cacheSizeGB를 물리 메모리의 약 40~50%로 시작wiredTigerConcurrentReadTransactions=128wiredTigerConcurrentWriteTransactions=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 장애 대응 포인트
- journal commit 지연 급등: 배치 I/O를 우선 제한하고, 디스크 지연(
iostat)과 MongoDB 지표(opLatencies, WiredTiger log)를 같은 타임라인으로 확인합니다. - checkpoint stall: checkpoint 시간 증가와 eviction pressure가 함께 올라가면 캐시 크기/동시성/백그라운드 작업을 보수적으로 조정합니다.
- replication lag 확대: secondary 노드의 스토리지 지연과 CPU 압력을 분리 점검하고, apply 경로를 방해하는 배치 작업을 제한합니다.
- 클라우드 지연 변동: 워커 동시성을 단계적으로 낮추고(한 번에 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
- 즉시 조치: 배치 작업 cgroup의
io.max를 낮춰 DB 경로를 우선 보호합니다. - 단기 완화: 체크포인트/플러시 관련 DB 파라미터를 완만한 방향으로 조정합니다.
- 사후 보강: 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\"
- 즉시 조치:
vm.dirty_bytes,vm.dirty_background_bytes를 보수적으로 낮춰 writeback burst를 줄입니다. - 단기 완화: DB 체크포인트 간격과 completion target을 조정해 쓰기 파형을 평탄화합니다.
- 사후 보강: O_DIRECT 사용 여부, 버퍼풀 크기, 커널 reclaim 로그를 함께 점검합니다.
상황 3: 복제 지연 확대 (replication lag)
# 스토리지/CPU/메모리 병목 동시 점검
iostat -x 1
mpstat -P ALL 1
cat /proc/pressure/cpu
cat /proc/pressure/memory
- 즉시 조치: 복제 적용 워커와 배치 워커를 CPU/cgroup으로 분리합니다.
- 단기 완화: 백그라운드 compaction, 대형 쿼리, 인덱스 재빌드 같은 변동 작업을 일시 제한합니다.
- 사후 보강: 복제 전용 스토리지 대역폭, apply 스레드 수, 로그 flush 정책을 재설계합니다.
상황 4: 클라우드 스토리지 지연 변동 증가
# 네트워크/스토리지 지연 상관관계 확인
sar -n DEV 1
iostat -x 1
cat /proc/pressure/io
- 즉시 조치: 급격한 배치 I/O를 중지하고 DB 트랜잭션 경로를 우선합니다.
- 단기 완화: 동시성(워커 수, iodepth)을 줄여 지연 분포의 꼬리를 낮춥니다.
- 사후 보강: 볼륨 등급 상향, 대역폭 보장형 구성, 분리 볼륨 설계를 검토합니다.
온콜 점검 체크리스트 (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
실전 튜닝 체크리스트
- 스토리지 경로 선택: WAL과 데이터 파일의 장치/파일시스템 분리 여부를 검토합니다.
- flush 정책 점검: DB의 commit 설정과 커널 dirty writeback 파라미터를 함께 확인합니다.
- 큐 깊이 정합: DB 동시성,
io_uringdepth, NVMe queue depth를 일관되게 맞춥니다. - 격리 정책 적용: cgroup v2로 배치 작업과 OLTP 워크로드를 분리합니다.
- 회귀 검증: 변경마다 동일한 재현 절차로 p99와 장애 복구 시간을 재측정합니다.
커널 I/O 스택과 데이터베이스
데이터베이스의 모든 영속 I/O는 커널의 계층 구조를 관통합니다. VFS → 파일시스템 → 블록 계층 → 디바이스 드라이버까지, 각 계층은 고유한 지연시간을 추가하며 데이터베이스 트랜잭션의 응답 시간에 직접 기여합니다. 이 경로를 DB 관점에서 이해하면 병목 구간을 정확히 특정할 수 있습니다.
| 계층 | 일반적 지연시간 | DB 영향 포인트 | 튜닝 가능 파라미터 |
|---|---|---|---|
| 시스템 콜 | 50~200ns | 호출 빈도가 높으면 누적 오버헤드 발생 | io_uring 배치 제출, vDSO |
| VFS / Page Cache | 200ns (hit) ~ 수ms (miss) | 이중 캐시, reclaim stall | O_DIRECT, fadvise, vm.vfs_cache_pressure |
| 파일시스템 | 1~10us (journal) | fsync barrier, extent allocation | 마운트 옵션 (nobarrier, journal 모드) |
| 블록 계층 (blk-mq) | 1~5us | I/O 스케줄러 선택, 큐 깊이 | nr_requests, scheduler, max_sectors_kb |
| 디바이스 드라이버 | 2~10us | 인터럽트 코얼레싱, 폴링 모드 | irq affinity, polling, io_poll |
| 스토리지 디바이스 | 10~100us (NVMe) | flush/FUA, write cache | write_cache, FUA 지원 여부 |
DB에서 fsync()를 호출하면 위 모든 계층을 동기적으로 통과해야 합니다. NVMe 기준 총 지연은 보통 15~200us이지만, writeback throttle이나 journal commit이 겹치면 수ms까지 급등할 수 있습니다.
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 lookup | cache miss 시 readpage 호출 |
| 데이터 쓰기 | pwrite64() | vfs_write → generic_file_write_iter → page dirty | dirty throttle 시 balance_dirty_pages |
| WAL sync | fdatasync() | vfs_fsync_range → file->f_op->fsync | 저널 커밋 + 디바이스 flush |
| 파일 확장 | fallocate() | vfs_fallocate → ext4_fallocate | extent 할당, 메타데이터 저널링 |
| Direct I/O | pread64(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
none으로 설정하고, merge도 비활성화하면 블록 계층 통과 시간을 1~2us 줄일 수 있습니다. 이 시간이 중요한지는 워크로드 IOPS에 따라 달라집니다.
페이지 캐시 vs Direct I/O
데이터베이스의 I/O 전략에서 가장 근본적인 선택지는 커널 페이지 캐시를 활용할 것인지(Buffered I/O), 우회할 것인지(Direct I/O)입니다. 이 선택은 메모리 사용 패턴, 지연 분포, 복구 동작에 큰 영향을 줍니다.
페이지 캐시 Hit/Miss 패턴
Buffered I/O에서 읽기 요청이 페이지 캐시에 적중하면 커널은 메모리 복사만으로 데이터를 반환합니다(~200ns). 미스가 발생하면 블록 I/O가 발생해 NVMe 기준 10~100us의 추가 지연이 발생합니다. 문제는 DB가 자체 버퍼 풀을 관리하므로 이중 버퍼링이 발생한다는 점입니다.
shared_buffers가 16GB이고 커널 페이지 캐시에 동일 데이터가 캐시되면, 물리 메모리 32GB를 같은 데이터에 사용합니다. 메모리 압력이 높아지면 reclaim이 DB 성능을 급격히 떨어뜨립니다. 이것이 MySQL/InnoDB가 O_DIRECT를 기본값으로 사용하는 핵심 이유입니다.
O_DIRECT 정렬 요구사항
O_DIRECT를 사용하려면 세 가지 정렬 조건을 모두 충족해야 합니다.
- 버퍼 주소: 논리 블록 크기(보통 512B 또는 4KB)의 배수로 정렬
- 파일 오프셋: 논리 블록 크기의 배수
- I/O 크기: 논리 블록 크기의 배수
/* 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/O | Buffered I/O (페이지 캐시 경유) | O_DIRECT (페이지 캐시 우회) |
| WAL/Redo 로그 | Buffered + fdatasync | Buffered + 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) |
페이지 캐시 오염(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 stall | PSI io, vmstat wa |
| dirty_ratio 크게 초과 | 강제 writeback + 새 dirty 금지 | 모든 쓰기 작업 정지 | system hang 수준 |
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의 DB 핵심 이점
- 배치 제출 (Batched Submission): 여러 I/O 요청을 하나의
io_uring_enter()호출로 제출해 시스템 콜 오버헤드를 1/N로 줄입니다. - 비동기 완료 (Async Completion): CQE를 폴링하거나 이벤트 통지로 완료를 수신해, 워커 스레드가 I/O 대기 없이 다른 작업을 처리할 수 있습니다.
- SQPOLL 모드: 커널 폴링 스레드가 SQ를 감시해
io_uring_enter()호출 자체를 제거합니다. 높은 IOPS에서 CPU 사용이 증가하지만 지연은 줄어듭니다. - Fixed Buffers/Files:
IORING_REGISTER_BUFFERS/IORING_REGISTER_FILES로 버퍼와 파일 디스크립터를 미리 등록하면 커널의 버퍼 맵핑과 파일 참조 오버헤드를 제거합니다.
/* 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, ¶ms);
/* 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 사례
| DB | io_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 log | read-heavy에서 throughput 향상 | 10.6+ 옵션 |
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 | ~1M | O_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에 미치는 부정적 영향
- 메모리 단편화 해소(compaction): 2MB 연속 페이지를 확보하기 위해 커널이 기존 페이지를 이동시키며, 이 과정에서 수십~수백ms의 stall이 발생합니다.
- 페이지 분할(splitting): 대형 페이지의 일부만 접근하면 페이지를 512개의 4KB 페이지로 분할해야 하며, 이 과정에서 락 경합이 발생합니다.
- khugepaged CPU 소모: 백그라운드 데몬이 주기적으로 연속 페이지를 대형 페이지로 병합하며, DB 프로세스와 CPU를 경쟁합니다.
- COW 증폭:
fork()시 대형 페이지 전체를 복사해야 하므로 COW 비용이 512배 증가합니다. Redis/PostgreSQL의 BGSAVE/checkpoint에 직접 영향을 줍니다. - 메모리 낭비: DB 페이지(보통 8KB/16KB)를 2MB 단위로 할당하면 내부 단편화가 발생합니다.
| DB | THP 권장 설정 | 이유 | Explicit Huge Pages 사용 |
|---|---|---|---|
| PostgreSQL | never 또는 madvise | checkpoint fork 시 COW 비용, shared_buffers fragmentation | huge_pages=on (권장) |
| MySQL InnoDB | never | buffer pool 할당 패턴과 충돌, compaction stall | large-pages=ON (선택) |
| MongoDB | never | 공식 권장, WiredTiger 캐시 fragmentation 방지 | 미지원 |
| Redis | never | BGSAVE fork COW 증폭, latency spike | 불필요 (메모리 중심) |
| Oracle DB | never | SGA 할당과 충돌 가능 | USE_LARGE_PAGES=ONLY |
| ScyllaDB | never | 직접 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
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)
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 활용 | 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
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
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)과의 경합, 컨텍스트 스위치 비용을 최소화하는 것이 핵심입니다.
CFS 파라미터 튜닝
| 파라미터 | 기본값 | DB 튜닝 방향 | 영향 |
|---|---|---|---|
sched_latency_ns | 6000000 (6ms) | 3000000~6000000 | 값이 작으면 응답성 향상, 스위치 증가 |
sched_min_granularity_ns | 750000 (0.75ms) | 1000000~3000000 | 값이 크면 DB 워커의 연속 실행 시간 보장 |
sched_wakeup_granularity_ns | 1000000 (1ms) | 1500000~3000000 | 값이 크면 wakeup preemption 감소 |
sched_migration_cost_ns | 500000 (0.5ms) | 5000000 (5ms) | 스레드 마이그레이션 억제, 캐시 친화성 유지 |
sched_nr_migrate | 32 | 8~16 | 밸런싱 시 이동 스레드 수 제한 |
sched_autogroup_enabled | 1 | 0 | DB 서버에서는 자동 그룹핑 비활성화 |
# 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
컨텍스트 스위치는 레지스터 저장/복원 외에도 TLB flush, L1/L2 캐시 콜드 스타트를 동반합니다. DB의 핫 경로에서 불필요한 컨텍스트 스위치가 발생하면 쿼리 지연이 증가합니다.
| 스위치 유형 | 비용 | 원인 | DB에서의 완화 |
|---|---|---|---|
| 자발적 (voluntary) | ~1-5us | I/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)은 서로 상호작용하며 경합을 유발합니다. 높은 동시성에서 락 대기 시간은 전체 트랜잭션 지연의 상당 부분을 차지할 수 있습니다.
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(); }
'
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-lock | huge pages로 fault 횟수 감소 |
| mmap/munmap 호출 | 동적 메모리 할당 (malloc → mmap) | write-lock (독점) | DB 메모리 사전 할당 |
| fork() 호출 | PostgreSQL checkpoint fork | write-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 이상 | 극심 | 서비스 분리 또는 읽기 전용 레플리카 |
max_connections=1000은 1000개의 프로세스를 생성합니다. 16코어 서버에서 1000개의 프로세스가 동시에 활성화되면 futex 경합, 컨텍스트 스위치, 캐시 스래싱으로 성능이 급격히 떨어집니다. max_connections=200 + pgbouncer(pool_mode=transaction)가 거의 항상 더 나은 성능을 보입니다.
NVMe 최적화
NVMe SSD는 멀티큐 아키텍처로 높은 병렬 I/O를 지원하지만, 커널과 DB 설정이 이를 활용하지 못하면 성능을 발휘할 수 없습니다. NVMe의 하드웨어 특성을 이해하고 커널 블록 계층 설정을 최적화해야 합니다.
I/O 스케줄러 비교
| 스케줄러 | 특성 | NVMe DB 적합성 | 설정 방법 |
|---|---|---|---|
none (noop) | 스케줄링 없이 바로 dispatch | NVMe에서 권장 (내부 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 네임스페이스와 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=4k | 50~100us (p99) | 인덱스 lookup 속도 |
| 4KB Random Write 지연 | fio --rw=randwrite --bs=4k | 20~50us (p99) | 데이터 페이지 쓰기 |
| 8KB Sequential Write + fsync | fio --rw=write --bs=8k --fdatasync=1 | 50~200us (p99) | WAL commit 지연의 하한선 |
| IOPS (4KB randread) | fio --rw=randread --iodepth=128 | 500K~1M+ | 최대 처리량 한계 |
| 큐 깊이별 지연 | fio iodepth 변화시키며 측정 | iodepth 32까지 선형 | 최적 동시성 지점 결정 |
| Flush/FUA 지연 | fio --fdatasync=1 | 30~150us | commit 지연의 핵심 요소 |
# 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 필수
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.swappiness | 1 | 스왑 방지 → shared_buffers 보호 | shared_buffers 크기 |
vm.dirty_background_bytes | 256MB | 체크포인트 writeback 파형 완화 | checkpoint_completion_target |
vm.dirty_bytes | 1GB | dirty 폭증 시 프로세스 블로킹 임계값 | max_wal_size, checkpoint_timeout |
vm.overcommit_memory | 2 | OOM killer 방지 | shared_buffers + 연결 수 |
vm.nr_hugepages | shared_buffers/2MB + 여유 | TLB 미스 감소, 안정적 메모리 | huge_pages = on |
kernel.shmmax | shared_buffers 이상 | System V 공유 메모리 최대 크기 | shared_buffers |
vm.zone_reclaim_mode | 0 | NUMA 영역 회수 비활성화 | 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_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_buffers | shmget/mmap (MAP_SHARED) | 모든 백엔드가 공유, huge pages 대상 | vm.nr_hugepages, kernel.shmmax |
| work_mem (쿼리별) | malloc → mmap (private) | 정렬/해시 작업 메모리, 프로세스당 할당 | vm.overcommit_memory |
| maintenance_work_mem | malloc → mmap (private) | VACUUM, CREATE INDEX 등 | 같은 설정 |
| WAL buffers | 공유 메모리 내 | WAL 쓰기 버퍼, shared_buffers와 함께 할당 | shared memory 설정 |
| temp_buffers | malloc (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 io | pg_stat_wal, checkpoint 통계 | 배치 I/O 제한, dirty_bytes 축소 |
| 쿼리 지연 산포 확대 | runqlat, vmstat cs/si, cachestat | pg_stat_activity wait_event | isolcpus, cgroup CPU 분리 |
| OOM kill 발생 | dmesg, /proc/meminfo, oom_score | shared_buffers + 연결 수 메모리 | oom_score_adj=-1000, 연결 제한 |
| Vacuum 지연 누적 | I/O 경합, dirty throttle | pg_stat_user_tables dead_tuples | autovacuum 리소스 상향, cgroup 분리 |
| 복제 지연 확대 | 네트워크 지연, CPU 경합, I/O util | pg_stat_replication write_lag | apply 워커 CPU 분리, I/O 대역폭 확보 |
MySQL/InnoDB 커널 튜닝
MySQL InnoDB는 O_DIRECT를 기본으로 사용하여 페이지 캐시 이중 버퍼링을 회피합니다. 이로 인해 커널 메모리 관리보다는 블록 I/O, AIO, doublewrite 성능이 중요해집니다.
innodb_flush_method와 커널 상호작용
| flush_method | 데이터 파일 | redo 로그 | 커널 경로 | 권장 환경 |
|---|---|---|---|---|
O_DIRECT | O_DIRECT + fsync | fsync | 페이지 캐시 우회, 직접 블록 I/O | 대부분의 프로덕션 (기본값) |
O_DIRECT_NO_FSYNC | O_DIRECT (fsync 생략) | fsync | 데이터 파일 fsync 제거 | 배터리 백업 RAID, NVMe PLP |
O_DSYNC | O_DSYNC | O_DSYNC | 동기 I/O (write + 즉시 sync) | 특수 환경 |
fsync | buffered + fsync | fsync | 페이지 캐시 경유 | 테스트/개발 환경 |
# 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.swappiness | 1 | buffer pool 스왑 방지 | innodb_buffer_pool_size |
fs.aio-max-nr | 1048576 | Native AIO 요청 상한 | innodb_use_native_aio=ON |
vm.dirty_background_bytes | 128MB | redo log writeback 제어 | innodb_flush_log_at_trx_commit |
I/O 스케줄러 | none (NVMe) | 불필요한 스케줄링 제거 | innodb_io_capacity |
readahead | 32 sectors (16KB) | random I/O 워크로드에 맞춤 | innodb_read_ahead_threshold |
nr_requests | 128~256 | SW 큐 깊이 | innodb_read_io_threads + write_io_threads |
| THP | never | compaction 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
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 syscall | thread_cache_size, max_connections |
| IO Read Thread | 비동기 읽기 (prefetch) | io_submit (native AIO) 또는 pread | innodb_read_io_threads |
| IO Write Thread | 비동기 쓰기 (flush) | io_submit 또는 pwrite + O_DIRECT | innodb_write_io_threads |
| Log Thread | redo log 쓰기 | pwrite + fsync/fdatasync | innodb_flush_log_at_trx_commit |
| Page Cleaner | dirty page flush | pwrite (O_DIRECT) + fsync | innodb_page_cleaners, innodb_io_capacity |
| Purge Thread | undo 정리 | CPU + 읽기 I/O | innodb_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 io | SHOW ENGINE INNODB STATUS (log waits) | 배치 I/O 제한, io_capacity 확인 |
| Flush storm | dirty pages 급증 → writeback burst | innodb_buffer_pool_dirty 비율 | io_capacity 상향, dirty_bytes 조정 |
| Replication lag | CPU/IO util, 네트워크 지연 | SHOW SLAVE STATUS (Seconds_Behind) | apply thread CPU 분리, I/O 대역폭 확보 |
| AIO 실패 | dmesg "aio" 메시지, fs.aio-max-nr | error log "native aio" | fs.aio-max-nr 상향 |
| OOM kill | dmesg, /proc/meminfo | buffer_pool_size + 연결 수 | oom_score_adj, 연결 제한 |
컨테이너 환경 DB
컨테이너(Docker, Kubernetes)에서 DB를 운영하면 격리, 배포, 확장성의 이점이 있지만, cgroup 제한, overlay 파일시스템, 네트워크 오버헤드 등 추가적인 커널 계층이 성능에 영향을 줍니다.
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
memory.max에 도달하면 커널 OOM killer가 컨테이너 내 프로세스를 강제 종료합니다. DB 프로세스가 종료되면 데이터 손실 위험이 있습니다. memory.low로 최소 보장을 설정하고, memory.high로 throttle 구간을 두어 OOM 전에 경고할 수 있도록 하세요. Kubernetes에서는 resources.requests와 resources.limits를 적절히 분리하세요.
Kubernetes DB 배포 시 커널 고려사항
| 항목 | 문제점 | 권장 설정 | Kubernetes 구성 |
|---|---|---|---|
| 네트워크 | CNI 오버레이 네트워크 지연 (~50us 추가) | hostNetwork: true | Pod spec에 hostNetwork 설정 |
| 스토리지 | overlay2 COW 오버헤드 | local PV + bind mount | hostPath 또는 local PV |
| NUMA | 자동 스케줄링으로 NUMA 불일치 | Topology Manager + static policy | kubelet --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) | 고정 할당 | 중간 |
BPF 관측성
BPF(Berkeley Packet Filter) 기반 도구는 커널 내부를 실시간으로 관측하면서도 오버헤드가 매우 낮아, 프로덕션 DB 서버에서도 안전하게 사용할 수 있습니다. BCC와 bpftrace를 활용한 DB 성능 분석 방법을 정리합니다.
DB 관련 BPF 도구 목록
| 도구 | 패키지 | 용도 | DB 활용 시나리오 |
|---|---|---|---|
biolatency | BCC | 블록 I/O 지연 히스토그램 | fsync/read 지연 분포 분석 |
biosnoop | BCC | 개별 블록 I/O 요청 추적 | 느린 I/O 요청 식별 |
ext4slower / xfsslower | BCC | 느린 파일시스템 작업 추적 | fsync, read, write 느린 작업 |
runqlat | BCC | runqueue 대기 시간 히스토그램 | DB 워커 CPU 스케줄 대기 |
tcplife | BCC | TCP 연결 수명 추적 | DB 클라이언트 연결 패턴 |
cachestat | BCC | 페이지 캐시 hit/miss 통계 | Buffered I/O DB의 캐시 효율 |
funclatency | BCC | 특정 함수 지연 히스토그램 | vfs_fsync_range 등 커널 함수 지연 |
offcputime | BCC | off-CPU 시간 + 스택 추적 | DB 스레드 블로킹 원인 분석 |
syscount | BCC | 시스템 콜 빈도 카운트 | DB의 주요 시스템 콜 패턴 |
filetop | BCC | 파일별 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분 이내에 식별할 수 있습니다.
커스텀 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(); }
'
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 tracepoint | p99 > 10ms 5분 지속 |
| 블록 I/O 히스토그램 | biolatency 출력 | biolatency | 1ms 이상 비율 > 5% |
| runqueue 대기 | runqlat 출력 | runqlat | p99 > 100us |
| 페이지 캐시 히트율 | cachestat 출력 | cachestat | 히트율 < 95% |
| Dirty 페이지 | /proc/meminfo | 없음 (직접 수집) | dirty > dirty_bytes * 80% |
| NUMA 원격 접근 | perf / numastat | 없음 | miss 비율 > 10% |
stackcount나 offcputime 같은 스택 추적 도구는 오버헤드가 다소 높으므로 짧은 시간만 실행하세요.
# 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) 데이터도 축적할 수 있습니다.
같이 보면 좋은 문서
이 문서에서 다룬 커널 서브시스템별 심화 내용은 아래 개별 문서에서 더 깊이 있게 다룹니다.
- Page Cache — 파일 캐시 구조, LRU 알고리즘, writeback 메커니즘
- Block I/O — blk-mq 아키텍처, I/O 스케줄러, 큐 깊이 관리
- io_uring — SQE/CQE 구조, 비동기 I/O, SQPOLL, fixed buffers
- NVMe — NVMe 프로토콜, flush/FUA, 멀티큐, 네임스페이스
- NUMA — NUMA 토폴로지, 메모리 정책, 인터커넥트 지연
- cgroups — cgroup v2 컨트롤러, 메모리/IO/CPU 제한
- ext4 — ext4 저널링, extent 기반 할당, 마운트 옵션
- XFS — XFS 할당 그룹, 지연 할당, 병렬 I/O 특성
- 메모리 관리 기초 — 페이지 할당, reclaim, OOM killer
- BPF/eBPF/XDP — BPF 프로그램 구조, 맵, 헬퍼 함수
- 프로세스 스케줄러 — CFS, EEVDF, RT 스케줄링 정책
- Huge Pages — hugetlbfs, THP, 1GB 페이지 관리
관련 문서
- VFS 계층 (Virtual Filesystem Switch) — superblock·inode·dentry·file 객체 모델, namei 경로 탐색, V
- CPUID 명령어 심화 (CPUID) — x86 CPUID 심화: Leaf 구조, 피처 비트 해석, Intel/AMD 차이, boo
- 커널 보안 취약점 사례 — Linux 커널 역사적 보안 취약점 — Spectre, Meltdown, Dirty COW