왜 ARC가 문제가 되는가

ZFS의 ARC(Adaptive Replacement Cache)는 기본적으로 시스템 물리 메모리를 적극적으로 점유한다. 리눅스에서 ARC는 페이지 캐시와 별개로 커널 슬랩 영역을 사용하기 때문에, 애플리케이션이 대량 메모리를 요구할 때 회수가 느리거나 회수 압박이 지연되어 OOM Killer가 엉뚱한 프로세스를 죽이는 상황이 발생한다. 특히 PostgreSQL, JVM, Redis처럼 자체적으로 메모리를 크게 잡는 서비스와 같은 호스트에 올리면, ARC와 애플리케이션이 서로 물리 메모리를 놓고 경쟁하면서 스와핑과 지연 스파이크가 생긴다.

현재 상태부터 측정한다

튜닝 전에 ARC가 실제로 얼마를 쓰고 히트율이 어떤지 확인해야 한다. 감으로 값을 바꾸면 캐시 효율만 떨어뜨린다.

# 현재 ARC 크기와 상한 확인 (바이트)
cat /proc/spl/kstat/zfs/arcstats | egrep '^(size|c_max|c_min|hits|misses) '

# 히트율 요약
awk '/^hits /{h=$3} /^misses /{m=$3} END{printf "hit ratio: %.2f%%\n", h/(h+m)*100}' \
  /proc/spl/kstat/zfs/arcstats

# arc_summary가 있으면 한눈에
arc_summary 2>/dev/null | sed -n '1,40p'

히트율이 90% 이상이면서 애플리케이션이 메모리 부족을 겪는다면 상한을 낮출 여지가 있고, 히트율이 낮은데 상한이 크다면 오히려 워크로드 특성(순차 대용량 I/O 등)을 의심해야 한다.

ARC 상한을 명시적으로 고정한다

기본값(리눅스는 물리 메모리의 약 50%)에 의존하지 말고 상한(zfs_arc_max)과 하한(zfs_arc_min)을 명시하는 것이 운영의 시작이다. 애플리케이션이 필요로 하는 메모리를 먼저 확보하고 남는 만큼만 ARC에 준다는 관점으로 계산한다.

# 런타임 즉시 반영 (예: 상한 16GiB, 하한 4GiB)
echo $((16 * 1024**3)) | sudo tee /sys/module/zfs/parameters/zfs_arc_max
echo $((4  * 1024**3)) | sudo tee /sys/module/zfs/parameters/zfs_arc_min

# 재부팅 후에도 유지되도록 modprobe 설정
cat <<'EOF' | sudo tee /etc/modprobe.d/zfs.conf
options zfs zfs_arc_max=17179869184
options zfs zfs_arc_min=4294967296
EOF
sudo update-initramfs -u   # initramfs에 zfs가 포함된 경우 필수

주의할 점은 zfs_arc_max를 런타임에 낮춰도 이미 커진 ARC가 즉시 줄지 않는다는 것이다. 회수는 메모리 압박이 생길 때 점진적으로 일어난다.

메모리 압박 시 동작 이해

ARC는 커널의 슬랩 shrinker를 통해 회수된다. 문제는 회수 지연과 단편화다. 회수가 늦어 스왑이 발생하기 시작하면 지연이 급증하므로, 데이터베이스 호스트에서는 vm.swappiness를 낮게 두어 스왑보다 캐시 회수를 우선하도록 유도한다.

# 캐시 회수를 스왑보다 우선 (DB 호스트 권장 낮은 값)
sudo sysctl -w vm.swappiness=10
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/90-zfs.conf

# ARC가 실제로 줄어드는지 모니터링
watch -n2 "grep -E '^(size|c) ' /proc/spl/kstat/zfs/arcstats"

워크로드별 권장 방향

워크로드ARC 상한추가 고려
PostgreSQL/MySQL 전용물리 메모리의 25~35%DB 버퍼풀에 우선 할당, primarycache=metadata 검토
파일/백업 서버50~70%순차 I/O는 캐시 이득이 적음
가상화/컨테이너 호스트고정 상한 필수게스트 메모리와 명확히 분리

데이터베이스는 자신의 버퍼가 1차 캐시이므로 ARC가 같은 데이터를 이중 캐싱하면 낭비다. 이 경우 데이터셋 단위로 primarycache=metadata를 두어 데이터 블록은 DB에 맡기고 ARC는 메타데이터만 캐시하게 하는 방법이 효과적이다.

데이터셋 단위 정밀 조정

# DB 데이터셋: 데이터 블록은 DB 버퍼풀에 맡기고 ARC는 메타데이터만
sudo zfs set primarycache=metadata tank/pgdata
sudo zfs set recordsize=16k        tank/pgdata   # PG 페이지 8k의 배수, 8k도 검토

# 설정 확인
zfs get primarycache,recordsize,compression tank/pgdata

recordsize는 튜닝 후 새로 기록되는 블록에만 적용되므로, 기존 데이터에 반영하려면 재기록(덤프·복구 또는 복사)이 필요하다.

주의점 정리

  • 상한을 너무 낮추면 히트율이 떨어져 디스크 I/O가 늘고 오히려 지연이 증가한다. 측정값 기반으로 조정한다.
  • zfs_arc_max 축소는 즉시 효과가 아니라 압박 시 점진 회수다. 급하면 재부팅이 확실하다.
  • initramfs에 ZFS가 포함된 시스템은 modprobe.d 변경 후 반드시 initramfs를 갱신해야 부팅 시 반영된다.
  • L2ARC(SSD 캐시)는 ARC 헤더를 위해 오히려 메모리를 더 소비하므로, 메모리 압박 상황에서 무턱대고 추가하면 역효과다.