26Y06n5

Young-Kyoo Kim·2026년 7월 6일
#!/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

0개의 댓글