26Y06a1

Young-Kyoo Kim·2026년 7월 6일

이 조합이면 후보가 꽤 명확해집니다. 특히 Cilium eBPF mapMinIO AIStor의 GPUDirect Storage / 캐시 기능이 지금까지의 어떤 지표에도 안 잡히는 메모리를 만드는 대표적인 원인입니다.

1. Cilium — eBPF map 메모리 (가장 유력한 후보)

Cilium은 conntrack, NAT, LB, ipcache, endpoint map 등을 커널 eBPF map으로 관리하는데, 이건 어떤 프로세스의 RSS에도 안 잡히고, cgroup 계정에도 안 잡히는 전역 커널 메모리입니다. 게다가 기본값이 bpf-map-dynamic-size-ratio총 메모리 비율에 따라 map 크기를 동적으로 정하기 때문에, 1TB짜리 노드라면 map이 예상보다 훨씬 크게 preallocate될 수 있습니다.

# cilium-agent에 실제로 적용된 map dynamic sizing 확인
kubectl -n kube-system exec ds/cilium -- cilium-dbg status --verbose | grep -A20 "BPF Maps"

# 전체 BPF map이 실제로 lock한 메모리 총합 (핵심 지표)
bpftool map list -j | jq '[.[].bytes_memlock] | add'

# map별로 내림차순 정렬해서 큰 것부터 확인
bpftool map list -j | jq -r '.[] | "\(.bytes_memlock // 0) \(.name) \(.type)"' | sort -rn | head -30

# cilium configmap에서 관련 설정값 확인
kubectl -n kube-system get cm cilium-config -o yaml | grep -iE "bpf-map-dynamic-size-ratio|bpf-ct-global|bpf-nat-global|bpf-policy-map|bpf-lb-map"

이 memlock 합계가 수십~수백GB로 나오면 갭의 상당 부분이 설명됩니다. 이 메모리는 /proc/meminfoSlab(bpf map 종류에 따라 kmalloc 기반) 또는 VmallocUsed에 일부 반영되지만, 큰 map은 huge allocation이라 slabtop에서 눈에 잘 안 띄고, dynamic sizing으로 실사용량보다 훨씬 크게 예약(preallocate)되는 게 문제입니다.

# Hubble 활성화 여부와 event buffer 크기도 확인 (이것도 메모리 크게 먹음)
kubectl -n kube-system get cm cilium-config -o yaml | grep -iE "hubble"

2. MinIO AIStor — GPUDirect Storage / 캐시 기능

AIStor는 AI 워크로드 대상으로 GPUDirect Storage(GDS)를 지원하는데, 이건 NVMe ↔ GPU 간 전송을 위해 pinned host memory buffer pool을 잡습니다. 이 pinned 메모리는 nvidia-fs/cufile 드라이버가 관리하는 경우 일반 프로세스 RSS 계정 방식과 다르게 잡힐 수 있습니다.

# nvidia-fs / GDS 관련 모듈, 통계 확인
lsmod | grep -i nvidia_fs
cat /proc/driver/nvidia-fs/stats 2>/dev/null

# GPU 장착 여부와 driver 메모리 사용량
nvidia-smi --query-gpu=memory.used,memory.total --format=csv 2>/dev/null

# MinIO 프로세스가 mlock한 메모리 확인
grep VmLck /proc/$(pgrep -f minio | head -1)/status

또한 AIStor/최신 MinIO는 distributed cache tier(핫 데이터를 메모리/NVMe에 캐싱)를 지원합니다. 이 캐시가 mmap 기반이면 buff/cache로 잡혀야 정상인데, 지금 buff/cache가 171GiB로 상대적으로 작은 걸 보면:

  • 캐시가 anon 메모리로 직접 할당되도록 설정되어 있을 가능성 (이 경우 RSS에 잡혀야 하는데 ps RSS엔 없다 → ps가 minio 프로세스를 못 잡고 있는지 재확인 필요)
  • 또는 O_DIRECT로 캐시를 관리해서 buff/cache를 우회하는 구조일 가능성
# minio 프로세스가 실제로 몇 개고 각각 RSS가 얼마인지 개별 확인 (합산 스크립트 말고 개별로)
ps aux | grep -i minio
kubectl -n <minio-namespace> top pods --containers

# minio 설정에서 cache 관련 env 확인
kubectl -n <minio-namespace> get pod <minio-pod> -o yaml | grep -iE "MINIO_CACHE|MINIO_API_|cache"

3. DirectPV — 디바이스/드라이버 오버헤드

DirectPV 자체는 보통 메모리를 크게 안 먹지만, 확인할 가치는 있습니다:

kubectl -n directpv get pods
kubectl -n directpv top pods

# XFS 등 파일시스템 메타데이터 캐시 (slab의 xfs_inode, xfs_buf 항목)
slabtop -o | grep -iE "xfs|ext4"

XFS를 대량의 디스크에 direct로 붙여 쓰는 경우 inode/buffer cache가 SUnreclaim 쪽에 상당히 쌓일 수 있습니다. slab 70GB를 reclaimable/unreclaimable로 나눠본 결과와 대조해보세요.

종합 확인 순서 추천

  1. bpftool map list -j | jq '[.[].bytes_memlock] | add' — 이 숫자 하나가 지금 가장 궁금한 답일 가능성이 높습니다. Cilium의 동적 map 크기 조정이 대형 노드에서 수백GB급 메모리를 예약하는 사례가 실제로 보고된 바 있습니다.
  2. cilium-config에서 bpf-map-dynamic-size-ratio 값 확인 (기본값에서 변경됐는지)
  3. MinIO/AIStor 파드들의 개별 RSS를 kubectl top으로 직접 확인 (ps 합산 결과와 대조)
  4. GPU/GDS 사용 여부 및 nvidia-fs pinned memory

bpftool map list 결과cilium-config의 bpf 관련 설정값부터 공유해주시면 갭을 거의 다 설명할 수 있을 것 같습니다.

===

Bare metal + kubespray라면 의심 우선순위가 확 달라집니다. VM/하이퍼바이저 오버헤드는 배제하고, K8s 스택 자체(런타임/CNI/cgroup 구조)베어메탈이라 흔히 같이 올라가는 스토리지 솔루션을 봐야 합니다.

1. cgroup 버전부터 확인 (매우 중요)

mount | grep cgroup
stat -fc %T /sys/fs/cgroup

cgroup v1이면 memory accounting이 여러 계층으로 쪼개져 있어서 지금 갭이 생기는 흔한 원인이 됩니다:

# v1인 경우
cat /sys/fs/cgroup/memory/memory.kmem.usage_in_bytes
cat /sys/fs/cgroup/memory/memory.usage_in_bytes
cat /sys/fs/cgroup/memory/memory.stat | grep -E "slab|kernel|sock"

memory.current(410GB)를 어느 cgroup에서 뽑으신 건가요? v1이라면 top-level /sys/fs/cgroup/memory/memory.usage_in_bytes가 진짜 root여야 비교가 의미 있습니다. kubepods.slice 하나만 본 거라면 system.slice(kubelet, containerd, etcd 등)를 더해야 합니다:

for slice in kubepods.slice system.slice user.slice; do
  path="/sys/fs/cgroup/memory/${slice}/memory.usage_in_bytes"
  [ -f "$path" ] && echo "$slice: $(( $(cat $path) / 1024/1024/1024 ))GB"
done

2. 오래된/leak된 pod cgroup (kubespray 환경에서 실제로 자주 발생)

kubelet이 종료된 pod의 cgroup을 제때 GC 못 하는 버그/현상이 있습니다. 프로세스는 죽었는데 cgroup 디렉토리와 그 안의 memory accounting(특히 page cache 잔여분)이 안 지워진 채 남아있는 경우입니다.

# 살아있는 pod 수 대비 cgroup 디렉토리 수 비교
kubectl get pods -A --field-selector=status.phase=Running | wc -l
find /sys/fs/cgroup -maxdepth 4 -type d -path "*kubepods*pod*" | wc -l

# 각 pod cgroup의 memory 사용량을 뽑아서 실제 존재하는 pod와 매칭 안 되는 게 있는지
for d in /sys/fs/cgroup/kubepods.slice/*/*/memory.current; do
  usage=$(cat "$d" 2>/dev/null)
  [ "$usage" -gt 1073741824 ] 2>/dev/null && echo "$usage $d"
done | sort -rn | head -20

이게 나온다면 원인은 명확해지고, 흔한 해결은 kubelet 재시작 또는 garbage-collection 관련 kubelet 파라미터 튜닝입니다.

3. 로컬 스토리지 솔루션 여부 (Ceph/Rook, Longhorn, OpenEBS, MinIO 등)

베어메탈 k8s에서 스토리지를 자체 호스팅하는 경우 정말 흔한데, 이런 워크로드들은 bluestore cache, mmap 기반 버퍼를 대량으로 쓰면서 일반적인 anon RSS 계정 방식과 다르게 잡힐 수 있습니다.

kubectl get pods -A | grep -iE "ceph|rook|longhorn|openebs|minio"

만약 있다면:

# Ceph OSD 예시
ceph daemon osd.<id> dump_mempools

bluestore cache 설정(bluestore_cache_size)이 크게 잡혀 있으면서 실제 RSS엔 일부만 반영되는 경우가 있습니다.

4. Calico/CNI + conntrack + iptables 규모

kubespray 기본 CNI는 Calico인 경우가 많습니다. 노드에 pod/service 수가 많으면:

# conntrack 테이블 크기
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max

# iptables 규칙 수 (kube-proxy iptables 모드일 때 서비스 많으면 폭증)
iptables-save | wc -l

# Calico가 eBPF 모드인지 iptables 모드인지
kubectl get felixconfiguration -o yaml | grep -i bpfEnabled

conntrack이나 iptables 규모 자체가 수백GB를 먹지는 않지만, eBPF map을 쓰는 경우는 다를 수 있습니다:

bpftool map list 2>/dev/null | awk '{print}'

5. containerd/docker 오버헤드 및 컨테이너 개수

crictl ps -a | wc -l    # 전체 컨테이너 수 (죽은 것 포함, GC 안 된 것도)
crictl images | wc -l
systemctl status containerd | grep Memory

컨테이너 수가 비정상적으로 많다면(수천 개 이상) 네트워크 네임스페이스당 커널 오버헤드(veth, 라우팅 캐시)가 누적될 수 있습니다.

6. 종합 진단 명령 (한번에 breakdown)

# node 전체 cgroup v2/v1 top consumer 찾기 (systemd-cgtop이 가장 빠름)
systemd-cgtop -1 -m --order=memory | head -30

이게 아마 지금 상황에서 가장 빨리 "어느 cgroup이 몇 GB 먹는지" 보여줄 겁니다. Used 725GiB를 채우는 상위 소비자가 여기서 드러날 가능성이 높습니다.

정리 — 이 순서로 확인 추천

  1. systemd-cgtop -1 -m --order=memory 로 즉시 상위 소비 cgroup 스캔 (가장 빠른 단서)
  2. cgroup v1/v2 확인 후 kubepods.slice + system.slice 전체 합산이 725GB에 가까운지
  3. 죽은/leak된 pod cgroup 존재 여부 (프로세스 없는데 memory 잡힌 것)
  4. Ceph/Rook/Longhorn 등 스토리지 워크로드 존재 여부 및 자체 캐시 설정

systemd-cgtop 결과부터 공유해주시면 어디를 더 팔지 바로 좁혀드릴 수 있습니다.

==

1,2,3이 모두 "전체(host 전체 기준)"가 맞다면, 결론은 하나입니다: ~450GB가 ps/slab/pagetable/shmem/hugepage 어디에도 안 잡히는, 순수 커널 레벨 또는 가상화 레벨 메모리 소비라는 뜻입니다. 이제 의심 범위를 프로세스 레벨에서 커널/인프라 레벨로 완전히 옮겨야 합니다.

먼저 계산식 자체를 짚고 넘어가기

free의 Used는 실측이 아니라 파생값입니다:

Used = MemTotal - MemFree - Buffers - Cached(-ish)

즉 Used 725GiB 안에는 이미 "RSS + 커널메모리 + Slab-unreclaim + 기타 전부"가 뭉뚱그려 들어있습니다. 그래서 지금 필요한 건 /proc/meminfo 전체를 놓고 등식을 맞춰보는 것입니다.

cat /proc/meminfo

여기서 아래 항목들을 반드시 확인하세요 — 지금까지 체크리스트에 없던 것들입니다:

항목의미왜 의심되는가
VmallocUsedvmalloc으로 할당된 커널 메모리드라이버/모듈이 큰 버퍼를 vmalloc으로 잡으면 여기 쌓임
KernelStack커널 스택스레드 수가 매우 많으면(수십만 개) 누적될 수 있음
Percpuper-CPU 할당대량 네트워크 트래픽/conntrack 등에서 커짐
Mlocked / Unevictable페이지 아웃 안 되는 메모리mlock한 대용량 버퍼(DB, DPDK 등)
Bounce, HardwareCorrupted잘 안 쓰이지만 확인용

이 항목들 다 더해도 수백GB 안 나오면, 커널 계정 시스템 밖에 있는 무언가를 봐야 합니다.

다음으로 확인할 것

1. cgroup의 memory.stat 상세 분해 (memory.current 하나만 보지 말고)

cat /sys/fs/cgroup/memory.stat

anon, file, kernel_stack, slab, sock, shmem, pagetables 등이 세분화되어 나옵니다. memory.current(410GB)가 top-level cgroup이라면 이 stat이 훨씬 신뢰도 높은 breakdown을 줍니다. 특히 sock(소켓 버퍼)과 kernel_stack을 보세요.

2. 가상화/중첩 환경 여부
이 노드가 KVM/QEMU 호스트거나, VM 안이거나, nested container 환경인가요?

  • 호스트라면: qemu 프로세스의 RSS는 보통 ps에 잡히지만, -mem-prealloc 안 쓰고 lazy allocation 중이거나, balloon driver가 개입돼 있으면 실제 소비량과 ps RSS가 어긋날 수 있습니다.
  • VM 안이라면: 하이퍼바이저 쪽 balloon/overcommit 이슈일 수 있어 게스트 내부 지표만으로는 설명 불가능합니다. dmidecode -s system-product-name 등으로 베어메탈인지 먼저 확인하세요.

3. eBPF / conntrack / 네트워크 스택

cat /proc/net/nf_conntrack | wc -l   # conntrack 엔트리 수
sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
bpftool map list 2>/dev/null | grep -i "value_size\|max_entries"

k8s + Cilium/Calico 같은 CNI를 쓴다면 eBPF map, conntrack 테이블이 GB 단위로 커널 메모리를 먹는 경우가 실제로 있습니다.

4. ZFS ARC / 특수 파일시스템 캐시

cat /proc/spl/kstat/zfs/arcstats 2>/dev/null | grep ^size

ZFS ARC는 buff/cache에 안 잡히고 Used로 카운트되는 대표적인 케이스입니다.

5. NUMA 노드별 분포

numastat -m

특정 NUMA 노드에만 메모리가 몰려 있다면 워크로드 배치 문제나 특정 디바이스(GPU/NIC)의 로컬 메모리 소비를 의심할 단서가 됩니다.

6. 커널 메모리 누수 의심 시 — 시계열로 확인
지금 스냅샷 한 번으로는 누수인지 정상 상태인지 구분 안 됩니다.

# 몇 시간 간격으로 반복 수집해서 비교
date >> /tmp/meminfo.log; cat /proc/meminfo >> /tmp/meminfo.log
cat /proc/slabinfo > /tmp/slabinfo_$(date +%s).log

Slab, VmallocUsed, KernelStack이 시간에 따라 계속 우상향하면 드라이버/모듈 누수 가능성이 높고, 안정적이면 그냥 정상적인 대량 사용(위 3, 4번 같은)일 가능성이 높습니다.

7. dmesg에서 부팅 시 예약된 영역 확인

dmesg | grep -i -E "reserved|crashkernel|memmap"

crashkernel 예약, NIC firmware 예약 영역 등은 free 전체 항목에서도 빠질 수 있습니다 (다만 보통 GB 단위가 아니라 수백MB라 지금 갭 크기론 가능성 낮음).

정리

우선순위 추천:
1. /proc/meminfo 풀 덤프 → VmallocUsed/KernelStack/Percpu 합산해서 갭이 얼마나 줄어드는지
2. cgroup memory.stat 상세 breakdown (sock, kernel_stack 특히)
3. 이 노드가 KVM 호스트인지 아닌지 (가장 흔한 "숨은 대량 메모리 소비" 원인)
4. eBPF/conntrack 규모 확인
5. 그래도 안 잡히면 시계열 수집으로 누수 여부 판별

결과 나오면 다시 봐드릴게요.

0개의 댓글