페이지 회수(page reclaim)와 kswapd 동작 원리

메모리 사용량이 갑자기 늘면 시스템이 느려지는데 free -h로 봐도 buff/cache가 넉넉해 보여 원인을 못 짚을 때가 있다. 그 여유분을 실제로 회수해 할당 가능하게 만드는 과정이 kswapd의 페이지 회수(reclaim)인데, 회수 속도가 할당 속도를 못 따라가면 애플리케이션 자신이 직접 회수(direct reclaim)에 끌려 들어가면서 지연이 발생한다. 이 글에서는 워터마크 기준으로 kswapd가 언제 깨어나는지 확인하고, 의도적으로 메모리를 압박해 백그라운드 회수와 direct reclaim의 차이를 /proc/vmstat으로 구분해본다.

워터마크와 kswapd 동작 순서

$ awk '/zone   Normal/{f=1} f{print} f&&/high/{exit}' /proc/zoneinfo
Node 0, zone   Normal
  pages free     337104
        min      9296
        low      11620
        high     13944
워터마크의미
highfree가 이 이상이면 여유 있는 상태. kswapd는 회수를 여기까지 하고 다시 잠든다
lowfree가 이 아래로 떨어지면 kswapd가 깨어나 백그라운드로 회수 시작
minfree가 이 아래로 떨어지면 할당 요청 자체가 direct reclaim(동기 회수)에 끌려 들어간다

회수 카운터 베이스라인

$ grep -E 'pgscan_kswapd|pgsteal_kswapd|pgscan_direct|pgsteal_direct|allocstall' /proc/vmstat
pgsteal_kswapd 309607
pgscan_kswapd 310538
pgsteal_direct 3124
pgscan_direct 3127
allocstall_normal 0
allocstall_movable 3
카운터의미
pgscan_kswapd / pgsteal_kswapdkswapd가 훑어본 페이지 수 / 실제로 회수(steal)한 페이지 수. 비율(steal/scan)이 회수 효율
pgscan_direct / pgsteal_direct할당을 요청한 프로세스 자신이 동기적으로 회수한 페이지 수
allocstall_*direct reclaim 때문에 할당이 멈춘(stall) 횟수. 이 값이 늘면 애플리케이션이 체감하는 지연으로 직결된다

메모리 압박 유발

7.8GB RAM에서 100MB씩 익명 메모리를 fault-in하며 총 6.8GB까지 채웠다. buff/cache가 2.4GB 있던 상태라 free 워터마크를 확실히 건드릴 만한 크기다.

import mmap, time
chunks = []
for i in range(68):
    m = mmap.mmap(-1, 100*1024*1024)
    for off in range(0, len(m), 4096):
        m[off] = 1
    chunks.append(m)
time.sleep(30)
카운터이전이후증가분
pgsteal_kswapd309,607794,186484,579 페이지(약 1.85GB)
pgscan_kswapd310,538840,687530,149
pgsteal_direct3,12490,48087,356
allocstall_normal0870870회 스톨

kswapd가 1.85GB를 백그라운드로 회수했지만, 할당 속도가 워낙 빨라 그것만으로는 부족해 우리 프로세스 자신도 87,356페이지(약 341MB)를 직접 회수하며 870번 멈췄다. 회수 대상은 대부분 buff/cache였고, 테스트가 끝난 뒤 swap_used는 13MB에 불과했다 — 캐시를 먼저 비우고, 그래도 부족할 때만 스왑을 쓰는 순서가 그대로 드러난다.

kswapd 자체 오버헤드

$ ps -o pid,pri,ni,stat,time,comm -p 81
    PID PRI  NI STAT     TIME COMMAND
     81  19   0 S    00:00:04 kswapd0

530,149페이지를 스캔하고 484,579페이지를 회수하는 동안 kswapd0가 쓴 누적 CPU 시간은 4초에 불과했다. nice 값도 0으로 특별히 우선순위가 낮지 않지만, 스캔·회수 자체가 가벼운 연산이라 시스템 전체에 주는 부담은 크지 않다.

vm.swappiness — 캐시 vs 스왑 우선순위

kswapd가 회수할 때 파일 캐시와 익명(anonymous) 페이지 중 어느 쪽을 먼저 건드릴지는 vm.swappiness(0~200)가 정한다. 같은 6.8GB 압박 테스트를 swappiness=0swappiness=200에서 각각 돌려 pswpout(스왑아웃된 페이지 수)와 pgsteal_anon(익명 페이지 회수 수)을 비교했다.

# swappiness=0
pswpout      전후 변화 없음 (0)
pgsteal_anon 전후 변화 없음 (0)   ← 익명 페이지는 아예 건드리지 않음

# swappiness=200
pswpout      124214 → 126552  (+2338)
pgsteal_anon 124098 → 126637  (+2539)  ← 익명 페이지 회수·스왑 발생

swappiness=0은 파일 캐시만으로 압박을 버틸 수 있는 한 익명 페이지는 절대 스왑하지 않는다. 200(최댓값)은 반대로 캐시와 익명 페이지를 거의 동등하게 취급해 스왑을 훨씬 적극적으로 쓴다. DB처럼 스왑 지연에 민감한 워크로드는 낮은 값을, 배치성 워크로드는 기본값(60)이나 높은 값을 고려할 수 있다.

주의사항

항목내용
allocstall이 늘면 할당 자체가 병목이 값이 계속 증가한다면 kswapd 튜닝보다 워크로드가 필요로 하는 메모리 총량 자체를 줄이거나 늘리는 게 근본 해법이다
swappiness 재부팅 시 초기화런타임 sysctl이라 영구 적용은 /etc/sysctl.conf에 기록해야 한다
min_free_kbytes로 워터마크 조정 가능이 값을 올리면 kswapd가 더 일찍(더 여유 있을 때) 깨어나 direct reclaim으로 밀리는 상황을 줄일 수 있지만, 그만큼 상시 예비 메모리가 늘어 가용 메모리는 줄어든다
mlock된 메모리는 회수 대상에서 제외DB 버퍼풀처럼 mlock된 영역은 kswapd/direct reclaim 모두 건드리지 못해, 이 메모리를 과하게 잡으면 나머지 시스템의 회수 여력만 줄어든다

마무리

pgsteal_kswapd는 정상적인 백그라운드 회수, pgsteal_direct·allocstall은 그 회수가 할당 속도를 못 따라가 애플리케이션이 직접 떠안은 몫이다. 시스템이 느려지는데 free 메모리가 애매하게 남아 있다면 이 두 카운터의 증가 속도부터 비교해볼 것 — direct reclaim 비중이 크다면 캐시 튜닝보다 워크로드의 메모리 요구량 자체를 줄이는 방향이 맞다.

참고

Kernel.org – Memory Management Concepts

답글 남기기