Linux는 디스크 I/O 비용을 줄이기 위해 읽은 파일 데이터를 RAM에 보관한다.
이것이 페이지 캐시(Page Cache)다.
반대로 RAM이 부족해지면 잘 안 쓰는 메모리를 디스크로 밀어내는데, 이것이 스왑(Swap)이다.
두 메커니즘은 서로 긴밀하게 연결되어 있으며, 동작 원리를 모르면
free -h 출력이 왜 저렇게 나오는지, OOM Killer가 왜 그 프로세스를 죽였는지 설명하기 어렵다.
페이지 캐시 동작 원리
파일을 read()로 읽으면 커널은 해당 데이터를 커널 주소 공간의 페이지 캐시에 올려둔다.
같은 파일을 다시 읽을 때는 디스크에 가지 않고 캐시에서 바로 반환한다.
쓰기(write())도 마찬가지다. 데이터는 먼저 페이지 캐시의 dirty page로 표시되고,
일정 조건이 되면 커널의 writeback 스레드(kworker)가 실제 디스크에 반영한다.
페이지 캐시는 남는 RAM을 전부 사용한다. free -h에서 buff/cache가 크게 잡혀 있어도
새 프로세스가 메모리를 요청하면 커널이 캐시를 자동으로 반환한다.
“메모리가 다 찼다”는 것은 캐시가 줄어들기 시작했다는 뜻이지, 곧 OOM이 온다는 의미가 아니다.
$ free -h
total used free shared buff/cache available
Mem: 15Gi 3.2Gi 512Mi 320Mi 11.5Gi 11.8Gi
Swap: 2Gi 0Bi 2Gi
여기서 available이 실제로 프로세스가 쓸 수 있는 메모리 추정값이다.
free만 보지 말고 available을 본다.
Dirty Page와 Writeback 타이밍
write()로 파일에 쓴 데이터가 실제 디스크에 반영되는 시점은 커널이 결정한다.
관련 파라미터는 /proc/sys/vm/에 있다.
$ cat /proc/sys/vm/dirty_ratio
20
$ cat /proc/sys/vm/dirty_background_ratio
10
$ cat /proc/sys/vm/dirty_expire_centisecs
3000
dirty_background_ratio— 전체 RAM의 이 비율만큼 dirty page가 쌓이면 백그라운드 writeback 시작 (기본 10%)dirty_ratio— 이 비율을 넘으면 새 쓰기 요청이 writeback 완료까지 블로킹됨 (기본 20%)dirty_expire_centisecs— dirty 상태로 이 시간(센티초) 이상 머문 페이지는 강제 writeback (기본 30초)
데이터베이스처럼 직접 fsync를 제어하는 애플리케이션은 이 값을 낮춰 예측 가능한 writeback 패턴을 만들기도 한다.
# dirty 페이지 현황 실시간 확인
watch -n1 'grep -E "Dirty|Writeback" /proc/meminfo'
스왑 동작 원리
스왑은 페이지 캐시가 아니라 익명 메모리(anonymous memory)를 대상으로 한다.
프로세스의 힙, 스택, mmap 영역처럼 파일로 백업되지 않는 메모리다.
커널의 kswapd 데몬이 메모리 압박을 감지하면 LRU(Least Recently Used) 알고리즘으로
오래 안 쓴 익명 페이지를 스왑 장치에 기록하고 해당 물리 메모리를 회수한다.
스왑된 페이지에 다시 접근하면 페이지 폴트(page fault)가 발생하고
커널이 스왑에서 읽어 물리 메모리로 복구한다. 이 과정에서 I/O가 발생하므로 응답 지연이 생긴다.
swappiness
vm.swappiness는 커널이 페이지 캐시를 버리는 것과 익명 메모리를 스왑하는 것 중
어느 쪽을 선호할지를 조정하는 값이다(0~200, 기본 60).
# 현재 값 확인
cat /proc/sys/vm/swappiness
# 즉시 변경 (재부팅 시 초기화)
sysctl -w vm.swappiness=10
# 영구 적용
echo "vm.swappiness=10" >> /etc/sysctl.d/99-memory.conf
sysctl -p /etc/sysctl.d/99-memory.conf
값이 낮을수록 스왑을 덜 쓰고 캐시를 먼저 비운다.
데이터베이스 서버처럼 응답 지연에 민감한 경우 10 이하로 낮추는 경우가 많다.
0으로 설정해도 OOM 상황에서는 스왑이 사용된다.
실전 확인 명령어
/proc/meminfo 주요 항목
$ grep -E "MemTotal|MemFree|MemAvailable|Cached|SwapTotal|SwapFree|Dirty|AnonPages|Slab" /proc/meminfo
MemTotal: 16248784 kB
MemFree: 524288 kB
MemAvailable: 12058624 kB
Cached: 10485760 kB ← 페이지 캐시
AnonPages: 3145728 kB ← 익명 메모리 (스왑 대상)
Slab: 524288 kB ← 커널 슬랩 캐시
Dirty: 16384 kB
SwapTotal: 2097152 kB
SwapFree: 2097152 kB
vmstat으로 실시간 모니터링
# 1초 간격으로 스왑 I/O 확인
vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 524288 98304 10240000 0 0 0 8 120 340 2 1 97 0 0
si(swap in), so(swap out)가 0이 아니면 스왑 I/O가 발생하고 있는 것이다.
지속적으로 값이 올라간다면 메모리 압박 상태로 봐야 한다.
프로세스별 스왑 사용량
# 스왑을 가장 많이 쓰는 프로세스 확인
for pid in /proc/[0-9]*/status; do
awk '/^Pid|^VmSwap/{printf "%s ", $2}' $pid
echo
done | sort -k2 -rn | head -10
페이지 캐시 강제 비우기 (테스트 목적)
# 디스크 캐시만 비우기 (운영 중 사용 주의)
sync && echo 1 > /proc/sys/vm/drop_caches
# 슬랩 캐시까지 포함
sync && echo 2 > /proc/sys/vm/drop_caches
# 페이지 캐시 + 슬랩 캐시 전부
sync && echo 3 > /proc/sys/vm/drop_caches
drop_caches는 I/O 벤치마크 전 워밍업 효과 제거에 유용하지만,
프로덕션에서 불필요하게 실행하면 갑작스러운 I/O 폭증을 유발할 수 있다.
주의사항 및 팁
- 스왑이 없으면 OOM Killer가 빨리 발동한다 — 스왑이 있으면 메모리 압박 시 커널이 시간을 벌 수 있다. 서버에서도 작은 스왑(RAM의 25~50%)은 있는 게 낫다.
- ZRAM을 고려한다 — 라즈베리파이나 RAM이 작은 환경에서는 ZRAM(압축 스왑)이 디스크 스왑보다 훨씬 빠르다.
sudo apt install zram-tools로 쉽게 설정 가능하다. - Transparent HugePage(THP)가 캐시 동작에 영향을 준다 —
/sys/kernel/mm/transparent_hugepage/enabled가always이면 메모리 단편화와 예측 불가한 지연이 생길 수 있다. 데이터베이스 운영 시madvise나never로 설정을 고려한다. - NVMe SSD는 스왑 속도가 빠르다 — HDD 시절 “스왑=느리다”는 공식이 NVMe에서는 많이 완화되었다. 순차 읽기 3GB/s 이상 장치라면 적절한 스왑 설정이 OOM을 막는 데 실용적이다.
- 페이지 캐시 크기를 제한하고 싶다면 cgroups v2 — 특정 컨테이너나 서비스의 캐시 사용량을
memory.high로 제어할 수 있다.
마치며
페이지 캐시와 스왑은 커널이 알아서 관리하도록 설계되어 있지만,
swappiness와 dirty writeback 파라미터를 워크로드에 맞게 조정하면
불필요한 I/O와 응답 지연을 줄일 수 있다.
/proc/meminfo와 vmstat을 주기적으로 들여다보는 습관을 들이면
메모리 관련 장애의 전조를 미리 파악하는 게 가능하다.
참고:
Linux Kernel Memory Management Concepts,
proc(5) man page