#!/usr/bin/env bash
#
# mem_gap_diagnose.sh
#
# 목적:
# free -h 의 Used 값과, 실제로 확인 가능한 메모리 사용처
# (프로세스 RSS/PSS, page cache, slab, hugepages, vmalloc/bpf map, shmem 등)
# 합계 사이의 "gap"을 찾아내기 위한 진단 스크립트.
#
# DirectPV 기반 MinIO AIStor + Cilium 환경에서
# "free 상 Used는 큰데 RSS/slab/cache 합쳐도 절반밖에 안 된다"는
# 상황을 좁혀나가기 위해 작성됨.
#
# 주의:
# - 완벽한 등식(Used = A + B + C + ...)을 보장하지 않습니다.
# 커널 메모리 계정 방식이 겹치는 부분(예: Shmem이 RSS/PSS에도 잡히고
# Cached 계산에도 관여)이 있어 "근사치" breakdown 입니다.
# - bpftool, vmallocinfo 접근 등 일부 항목은 root 권한이 필요합니다.
# - 죽은(dying) cgroup이 물고 있는 메모리는 sysfs 만으로는 정확히
# 드러나지 않아 이 스크립트는 "의심 신호"만 보여주고, 정확한 확인은
# 커널 디버깅 도구(drgn 등)가 필요하다는 점을 안내만 합니다.
#
# 사용법:
# sudo ./mem_gap_diagnose.sh # 전체 진단
# sudo ./mem_gap_diagnose.sh --no-bpf # bpftool 관련 항목 스킵
#
set -uo pipefail
SKIP_BPF=0
for arg in "$@"; do
case "$arg" in
--no-bpf) SKIP_BPF=1 ;;
esac
done
IS_ROOT=0
[[ $EUID -eq 0 ]] && IS_ROOT=1
hr() { printf '%s\n' "--------------------------------------------------------------------"; }
section() { echo; hr; printf '## %s\n' "$1"; hr; }
# kB -> human readable (MiB/GiB)
kb_h() {
local kb=$1
awk -v kb="$kb" 'BEGIN {
if (kb >= 1048576) printf "%.2f GiB", kb/1048576;
else printf "%.2f MiB", kb/1024;
}'
}
meminfo_val() {
# /proc/meminfo 에서 특정 필드의 kB 값 추출 (없으면 0)
awk -v k="$1" '$1==k":" {print $2; found=1} END{if(!found) print 0}' /proc/meminfo
}
if [[ ! -r /proc/meminfo ]]; then
echo "ERROR: /proc/meminfo 를 읽을 수 없습니다." >&2
exit 1
fi
echo "=============================================================="
echo " Memory Gap Diagnose ($(date '+%Y-%m-%d %H:%M:%S'))"
echo " host: $(hostname) kernel: $(uname -r)"
[[ $IS_ROOT -eq 0 ]] && echo " (주의: root 권한 없이 실행 중 - 일부 항목은 스킵되거나 부정확할 수 있음)"
echo "=============================================================="
###############################################################################
section "1. free -h / 기본 값"
###############################################################################
free -h
echo
free -b > /tmp/.mem_gap_free_b.$$ 2>/dev/null
USED_B=$(awk '/^Mem:/ {print $3}' /tmp/.mem_gap_free_b.$$)
TOTAL_B=$(awk '/^Mem:/ {print $2}' /tmp/.mem_gap_free_b.$$)
rm -f /tmp/.mem_gap_free_b.$$
USED_KB=$(( USED_B / 1024 ))
TOTAL_KB=$(( TOTAL_B / 1024 ))
echo "free(1) 기준 Used = ${USED_KB} kB ($(kb_h "$USED_KB"))"
echo "free(1) 기준 Total = ${TOTAL_KB} kB ($(kb_h "$TOTAL_KB"))"
###############################################################################
section "2. /proc/meminfo 핵심 필드"
###############################################################################
MEMTOTAL=$(meminfo_val MemTotal)
MEMFREE=$(meminfo_val MemFree)
BUFFERS=$(meminfo_val Buffers)
CACHED=$(meminfo_val Cached)
SRECLAIM=$(meminfo_val SReclaimable)
SUNRECLAIM=$(meminfo_val SUnreclaim)
SHMEM=$(meminfo_val Shmem)
PAGETABLES=$(meminfo_val PageTables)
KERNELSTACK=$(meminfo_val KernelStack)
VMALLOCUSED=$(meminfo_val VmallocUsed)
PERCPU=$(meminfo_val Percpu)
MLOCKED=$(meminfo_val Mlocked)
UNEVICTABLE=$(meminfo_val Unevictable)
HUGE_TOTAL_CNT=$(meminfo_val HugePages_Total)
HUGE_FREE_CNT=$(meminfo_val HugePages_Free)
HUGE_RSVD_CNT=$(meminfo_val HugePages_Rsvd)
HUGEPAGESIZE_KB=$(meminfo_val Hugepagesize)
ANONHUGEPAGES=$(meminfo_val AnonHugePages)
printf "%-16s %12s kB (%s)\n" "Buffers" "$BUFFERS" "$(kb_h "$BUFFERS")"
printf "%-16s %12s kB (%s)\n" "Cached" "$CACHED" "$(kb_h "$CACHED")"
printf "%-16s %12s kB (%s)\n" "SReclaimable" "$SRECLAIM" "$(kb_h "$SRECLAIM")"
printf "%-16s %12s kB (%s)\n" "SUnreclaim" "$SUNRECLAIM" "$(kb_h "$SUNRECLAIM")"
printf "%-16s %12s kB (%s)\n" "Shmem" "$SHMEM" "$(kb_h "$SHMEM")"
printf "%-16s %12s kB (%s)\n" "PageTables" "$PAGETABLES" "$(kb_h "$PAGETABLES")"
printf "%-16s %12s kB (%s)\n" "KernelStack" "$KERNELSTACK" "$(kb_h "$KERNELSTACK")"
printf "%-16s %12s kB (%s)\n" "VmallocUsed" "$VMALLOCUSED" "$(kb_h "$VMALLOCUSED")"
printf "%-16s %12s kB (%s)\n" "Percpu" "$PERCPU" "$(kb_h "$PERCPU")"
printf "%-16s %12s kB (%s)\n" "Mlocked" "$MLOCKED" "$(kb_h "$MLOCKED")"
printf "%-16s %12s kB (%s)\n" "Unevictable" "$UNEVICTABLE" "$(kb_h "$UNEVICTABLE")"
printf "%-16s %12s kB (%s)\n" "AnonHugePages" "$ANONHUGEPAGES" "$(kb_h "$ANONHUGEPAGES")"
echo
echo "HugePages: Total=${HUGE_TOTAL_CNT} Free=${HUGE_FREE_CNT} Rsvd=${HUGE_RSVD_CNT} size=${HUGEPAGESIZE_KB}kB/page"
HUGE_UNUSED_CNT=$(( HUGE_TOTAL_CNT - (HUGE_TOTAL_CNT - HUGE_FREE_CNT) ))
HUGE_RESERVED_UNUSED_KB=$(( HUGE_FREE_CNT * HUGEPAGESIZE_KB ))
if [[ $HUGE_TOTAL_CNT -gt 0 ]]; then
echo " -> HugePages가 설정되어 있습니다. 실제 사용 중(Total-Free)=$(( (HUGE_TOTAL_CNT-HUGE_FREE_CNT)*HUGEPAGESIZE_KB )) kB, 예약만 되고 미사용=${HUGE_RESERVED_UNUSED_KB} kB"
if [[ $HUGE_RESERVED_UNUSED_KB -gt 0 ]]; then
echo " [의심] 미사용 예약 hugepage가 존재합니다. 이 영역은 RSS/slab 어디에도 안 잡히고 Used만 깎아먹습니다."
fi
else
echo " -> HugePages 미설정. (용의선상에서 제외)"
fi
###############################################################################
section "3. 프로세스 RSS / PSS 합계 (실제 사용량, 공유 메모리 중복 제거)"
###############################################################################
echo "PSS(Proportional Set Size) 합계를 우선 계산합니다 (공유 라이브러리 등 중복 카운트 방지)."
PSS_TOTAL_KB=0
RSS_TOTAL_KB=0
PSS_OK=1
for smaps in /proc/[0-9]*/smaps_rollup; do
[[ -r "$smaps" ]] || continue
v=$(awk '/^Pss:/ {print $2}' "$smaps" 2>/dev/null)
[[ -n "${v:-}" ]] && PSS_TOTAL_KB=$(( PSS_TOTAL_KB + v ))
done
if [[ $PSS_TOTAL_KB -eq 0 ]]; then
PSS_OK=0
echo " (smaps_rollup 접근 불가 - 권한 부족 또는 커널 미지원. RSS 합계로 대체)"
fi
for status in /proc/[0-9]*/status; do
[[ -r "$status" ]] || continue
v=$(awk '/^VmRSS:/ {print $2}' "$status" 2>/dev/null)
[[ -n "${v:-}" ]] && RSS_TOTAL_KB=$(( RSS_TOTAL_KB + v ))
done
echo "RSS 단순 합계(공유 메모리 중복 포함 가능) = ${RSS_TOTAL_KB} kB ($(kb_h "$RSS_TOTAL_KB"))"
if [[ $PSS_OK -eq 1 ]]; then
echo "PSS 합계(중복 제거, 더 정확) = ${PSS_TOTAL_KB} kB ($(kb_h "$PSS_TOTAL_KB"))"
PROC_REAL_KB=$PSS_TOTAL_KB
else
PROC_REAL_KB=$RSS_TOTAL_KB
fi
echo
echo "메모리 상위 프로세스 Top 10 (RSS 기준):"
ps -eo pid,ppid,comm,rss --sort=-rss --no-headers | head -10 | \
awk '{printf " pid=%-8s ppid=%-8s comm=%-20s rss=%s\n",$1,$2,$3,$4"kB"}'
###############################################################################
section "4. Slab (SReclaimable + SUnreclaim) 상세 - slabtop 합계 vs meminfo 합계 교차검증"
###############################################################################
SLAB_TOTAL_KB=$(( SRECLAIM + SUNRECLAIM ))
echo "meminfo 기준 Slab 총합 = ${SLAB_TOTAL_KB} kB ($(kb_h "$SLAB_TOTAL_KB"))"
if [[ -r /proc/slabinfo ]]; then
SLABINFO_TOTAL_KB=$(awk 'NR>2 {sum += ($3*$4)/1024} END{printf "%.0f", sum}' /proc/slabinfo 2>/dev/null)
echo "/proc/slabinfo 실측 합계 = ${SLABINFO_TOTAL_KB:-N/A} kB"
echo
echo "상위 15개 slab (크기 기준):"
awk 'NR>2 {printf "%-28s objs=%-8s objsize=%-6s total_kB=%.0f\n",$1,$3,$4,($3*$4)/1024}' /proc/slabinfo 2>/dev/null \
| sort -t= -k4 -n -r | head -15
echo
echo "XFS 관련 slab만 필터:"
grep -i xfs /proc/slabinfo || echo " (없음)"
else
echo " /proc/slabinfo 접근 불가 (root 필요)."
fi
###############################################################################
section "5. Shmem / tmpfs 사용 현황"
###############################################################################
echo "meminfo Shmem = ${SHMEM} kB ($(kb_h "$SHMEM"))"
echo "tmpfs 마운트별 사용량:"
df -h -t tmpfs 2>/dev/null | awk 'NR==1 || $3!="0"'
echo
echo "IPC 공유 메모리 세그먼트 (연결된 프로세스 없이 떠 있는 것 주의):"
ipcs -m 2>/dev/null || echo " (ipcs 명령 없음)"
###############################################################################
section "6. Vmalloc 사용량 및 BPF 연관 여부"
###############################################################################
echo "meminfo VmallocUsed = ${VMALLOCUSED} kB ($(kb_h "$VMALLOCUSED"))"
if [[ $IS_ROOT -eq 1 && -r /proc/vmallocinfo ]]; then
BPF_VMALLOC_KB=$(grep -i bpf /proc/vmallocinfo 2>/dev/null \
| grep -oE '[0-9]+/[0-9]+ pages' \
| awk -F'/' '{print $1}' \
| awk '{sum+=$1} END{printf "%.0f", (sum*4)}') # 4kB/page 가정
echo "vmallocinfo 중 'bpf' 키워드 포함 영역 합계(추정) = ${BPF_VMALLOC_KB:-0} kB"
echo
echo "vmallocinfo 상위 20개 항목 (크기순):"
awk '{print $2, $0}' /proc/vmallocinfo 2>/dev/null | sort -n -r | head -20 | cut -d' ' -f2-
else
echo " (root 아니거나 /proc/vmallocinfo 미지원 - 커널에서 CONFIG_MMU / debugfs 옵션 확인 필요)"
fi
###############################################################################
section "7. Cilium / BPF Map 크기 추정"
###############################################################################
if [[ $SKIP_BPF -eq 1 ]]; then
echo " (--no-bpf 옵션으로 스킵됨)"
elif ! command -v bpftool >/dev/null 2>&1; then
echo " bpftool 명령을 찾을 수 없습니다. (apt/yum install linux-tools-common 또는 cilium 이미지 내 bpftool 사용)"
elif [[ $IS_ROOT -eq 0 ]]; then
echo " bpftool은 root 권한이 필요합니다. sudo로 재실행하세요."
else
if command -v jq >/dev/null 2>&1; then
echo "현재 로드된 BPF map 목록 및 max 용량 기준 예상 크기:"
printf "%-30s %12s %10s %10s %14s\n" "NAME" "MAX_ENTRIES" "KEY_SZ" "VAL_SZ" "EST_MAX_KB"
bpftool map show -j 2>/dev/null | jq -r '.[] | [.name, .max_entries, .key_size, .value_size] | @tsv' | \
while IFS=$'\t' read -r name maxe ksz vsz; do
[[ -z "$maxe" ]] && continue
est_kb=$(( maxe * (ksz + vsz) / 1024 ))
printf "%-30s %12s %10s %10s %14s\n" "${name:-<unnamed>}" "$maxe" "$ksz" "$vsz" "$est_kb"
done
TOTAL_BPF_KB=$(bpftool map show -j 2>/dev/null | jq -r '[.[] | (.max_entries * (.key_size + .value_size))] | add // 0')
TOTAL_BPF_KB=$(( TOTAL_BPF_KB / 1024 ))
echo
echo "전체 BPF map 예상 최대 사용량 합계(누적 max_entries 기준) = ${TOTAL_BPF_KB} kB ($(kb_h "$TOTAL_BPF_KB"))"
echo " (주의: 실제 커널 할당은 map 타입/prealloc 여부에 따라 다르며, 이는 '이론상 상한'입니다.)"
else
echo "jq가 없어 표 형태 대신 원본 출력만 표시합니다:"
bpftool map show 2>/dev/null
echo " (jq 설치 시 map별 예상 크기 합산 가능: apt/yum install jq)"
fi
echo
echo "pin된 BPF object 개수 (/sys/fs/bpf 하위):"
if [[ -d /sys/fs/bpf ]]; then
PIN_COUNT=$(find /sys/fs/bpf -type f 2>/dev/null | wc -l)
echo " 총 ${PIN_COUNT} 개"
echo " cilium 관련:"
find /sys/fs/bpf -path '*cilium*' -type f 2>/dev/null | wc -l | xargs echo " ->"
else
echo " /sys/fs/bpf 마운트 안 됨"
fi
if command -v cilium-dbg >/dev/null 2>&1 || command -v cilium >/dev/null 2>&1; then
CBIN=$(command -v cilium-dbg || command -v cilium)
echo
echo "cilium 엔드포인트 수 (pin된 policy map 수와 비교용):"
$CBIN endpoint list 2>/dev/null | grep -c '^[0-9]' || echo " (조회 실패, 권한/네임스페이스 확인 필요)"
echo
echo "cilium BPF map pressure (가득 찬 정도, 100%에 가까우면 GC 실패로 leak처럼 보일 수 있음):"
$CBIN status --verbose 2>/dev/null | grep -A30 -i 'BPF Maps' || echo " (cilium status 접근 불가)"
else
echo " cilium / cilium-dbg CLI 를 찾을 수 없습니다 (cilium-agent 파드 내부에서 실행 필요할 수 있음)."
fi
fi
###############################################################################
section "8. Dead/Orphan cgroup 의심 신호 (정확한 확인은 커널 디버깅 도구 필요)"
###############################################################################
if [[ -d /sys/fs/cgroup ]]; then
if [[ -f /sys/fs/cgroup/cgroup.controllers ]]; then
echo "cgroup v2 감지됨."
LIVE_CGROUPS=$(find /sys/fs/cgroup -maxdepth 6 -name cgroup.procs 2>/dev/null | wc -l)
echo "현재 살아있는 cgroup 디렉터리 수(대략): ${LIVE_CGROUPS}"
echo "루트 memory.stat 중 slab 관련 값:"
grep -E '^slab' /sys/fs/cgroup/memory.stat 2>/dev/null || echo " (memory.stat 없음/미지원)"
else
echo "cgroup v1 감지됨."
LIVE_CGROUPS=$(find /sys/fs/cgroup/memory -maxdepth 6 -name cgroup.procs 2>/dev/null | wc -l)
echo "현재 살아있는 memory cgroup 디렉터리 수(대략): ${LIVE_CGROUPS}"
fi
echo
echo "커널 로그 중 memcg/oom 관련 경고 (최근 200줄 내):"
dmesg 2>/dev/null | grep -i -E 'memcg|cgroup' | tail -20 || echo " (dmesg 접근 불가 또는 관련 로그 없음)"
echo
echo " [참고] 죽은(dying) memcg가 dentry/inode 캐시를 물고 있는 leak은"
echo " sysfs 만으로는 직접 보이지 않습니다. 아래로 추가 확인 권장:"
echo " - 커널 버전이 오래된 경우 관련 fix 백포트 여부 확인"
echo " - crash/drgn 스크립트로 'dying' memcg 리스트 및 점유 slab 확인"
echo " - 파드/컨테이너 churn 빈도(재시작/삭제 횟수)와 Used 증가 추세의 상관관계 확인"
else
echo " /sys/fs/cgroup 없음 (cgroup 미사용 환경?)"
fi
###############################################################################
section "9. 종합 Gap 계산 (근사치)"
###############################################################################
ACCOUNTED_KB=$(( PROC_REAL_KB + SLAB_TOTAL_KB + KERNELSTACK + PAGETABLES + VMALLOCUSED + PERCPU + HUGE_RESERVED_UNUSED_KB ))
GAP_KB=$(( USED_KB - ACCOUNTED_KB ))
echo "free 기준 Used : ${USED_KB} kB ($(kb_h "$USED_KB"))"
echo "----------------------------------------"
if [[ $PSS_OK -eq 1 ]]; then
echo " (+) 프로세스 PSS 합계 : ${PROC_REAL_KB} kB"
else
echo " (+) 프로세스 RSS 합계(중복 가능) : ${PROC_REAL_KB} kB"
fi
echo " (+) Slab (SReclaim+SUnreclaim) : ${SLAB_TOTAL_KB} kB"
echo " (+) KernelStack : ${KERNELSTACK} kB"
echo " (+) PageTables : ${PAGETABLES} kB"
echo " (+) VmallocUsed : ${VMALLOCUSED} kB"
echo " (+) Percpu : ${PERCPU} kB"
echo " (+) HugePages 예약-미사용 : ${HUGE_RESERVED_UNUSED_KB} kB"
echo "----------------------------------------"
echo "설명 가능한 합계(근사) : ${ACCOUNTED_KB} kB ($(kb_h "$ACCOUNTED_KB"))"
echo "========================================"
echo "GAP (Used - 설명가능 합계) : ${GAP_KB} kB ($(kb_h "$GAP_KB"))"
echo "========================================"
if [[ $GAP_KB -gt 0 ]]; then
GAP_PCT=$(awk -v g="$GAP_KB" -v u="$USED_KB" 'BEGIN{ if(u>0) printf "%.1f", (g/u)*100; else print "N/A"}')
echo
echo "-> Used의 약 ${GAP_PCT}%가 여전히 설명되지 않습니다."
echo " 위 6~8번 섹션(vmalloc/BPF map, shmem, dead cgroup)을 우선 의심하세요."
echo " 특히 BPF map 예상 최대 크기(섹션 7)가 gap과 비슷한 규모라면 유력한 원인입니다."
else
echo
echo "-> Gap이 거의 없거나 음수입니다. 위 항목들로 대부분 설명 가능한 상태로 보입니다."
echo " (음수라면 PSS/RSS 계산의 중복 카운트 가능성 - PSS 값을 우선 신뢰하세요)"
fi
echo
hr
echo "완료. 이 스크립트는 참고용 근사 계산이며, 정확한 원인 확정에는"
echo "bpftool/drgn/perf 등 추가 커널 레벨 분석이 필요할 수 있습니다."
hr