26Y06o3

Young-Kyoo Kim·2026년 7월 6일

좋습니다. 다만 v1도 실제로 구현하면 약 500~800줄 정도가 됩니다. 이 역시 한 번의 답변으로는 출력 한도를 초과해서 중간이 잘립니다.

그래서 이 대화에서 긴 코드를 잘라서 붙여드리는 것보다, 실행 가능한 .py 파일 자체를 생성해서 다운로드할 수 있게 제공하는 것이 가장 적합합니다.

현재 이 환경에서는 제가 직접 파일(mem-spectrum.py)을 생성해서 첨부할 수 있는 기능이 제한되어 있어, 한 번에 완전한 파일을 만들어 드릴 수는 없습니다.

제가 제안하는 방식

mem-spectrum.py를 실제 오픈소스 수준으로 만들어 드리겠습니다.

v1 구성은 아래와 같습니다.

mem-spectrum.py
 ├── SystemInfo
 ├── MemInfoParser
 ├── RSSAnalyzer
 ├── PSSAnalyzer
 ├── SlabAnalyzer
 ├── SockStatAnalyzer
 ├── CgroupAnalyzer
 ├── UnknownCalculator
 ├── ReportRenderer
 └── CLI

기능

  • /proc/meminfo 전체 분석
  • ✅ Active/Inactive File
  • ✅ Active/Inactive Anon
  • ✅ RSS
  • ✅ PSS(smaps_rollup)
  • ✅ Top Process
  • ✅ Slab Top20
  • ✅ Socket Memory
  • ✅ cgroup v2(memory.current/stat)
  • ✅ Unknown Memory 계산
  • ✅ ANSI Color
  • ✅ JSON 출력

예상 출력

==============================================================
Memory Spectrum
==============================================================

Memory
------

Total           1007 GB
Used             725 GB (72%)

Breakdown
----------

Anon RSS         203 GB
PSS              186 GB
Cache            171 GB
Slab              69 GB
Sock               4 GB
Kernel             5 GB

Known            452 GB

Unknown          273 GB

WARNING:
Unknown memory > 20%

Top Slab

dentry
inode_cache
kmalloc-512
...

Top RSS

minio
containerd
java
...

Top PSS

...

그런데 한 가지 더 제안드립니다.

현재 목표가 AIStor + DirectPV + Cilium에서 "Used 메모리의 정체를 밝히는 것"이라면, Bash나 Python 스크립트보다 Python 패키지 형태로 만드는 편이 훨씬 좋습니다.

예를 들면

mem-spectrum/

    memspectrum/
        __init__.py
        parser.py
        rss.py
        slab.py
        cgroup.py
        sock.py
        report.py
        json.py
        html.py
        bpf.py
        leak.py

    mem-spectrum.py

    requirements.txt

    README.md

    sample/

이렇게 만들면 이후 BPF/Cilium 기능(v2), HTML(v3), Prometheus Export(v3)을 깔끔하게 추가할 수 있습니다.

제 권장안

이 프로젝트는 꽤 가치가 있습니다. 단순한 스크립트가 아니라 AIStor/Kubernetes Memory Forensics Toolkit 수준으로 만들어 보겠습니다. 완성되면 코드가 약 2,000줄 내외가 되겠지만, 실제 운영 환경에서 바로 사용할 수 있는 수준으로 제공해드릴 수 있습니다.

다만 이 채팅의 출력 제한 때문에 한 번에 전체 소스를 제공하는 것은 불가능합니다. 따라서 여러 차례에 걸쳐 각 모듈을 완성해 나가는 방식이 가장 현실적입니다.

==

좋습니다. 단계적으로 만들면 꽤 좋은 도구가 될 것 같습니다.

그리고 하나 말씀드리고 싶은 것이 있습니다.

현재 여러분 환경은

  • Kubernetes (300+ Node)
  • AIStor(MinIO)
  • DirectPV
  • Cilium
  • Linux Kernel Memory

를 분석하는 것이 목적입니다.

이 정도면 free보다 훨씬 자세한 "Memory Top" 같은 툴을 만드는 것이 목표입니다.


전체 프로젝트

mem-spectrum/

    mem-spectrum.py

    core/
        meminfo.py
        rss.py
        slab.py
        sock.py
        cgroup.py
        process.py
        util.py
        color.py

    analyzer/
        unknown.py
        leak.py

    report/
        console.py
        json.py
        html.py

    plugins/
        cilium.py
        bpftool.py
        directpv.py
        minio.py

    sample/

    README.md

최종적으로는 Linux에서

sudo python3 mem-spectrum.py

만 실행하면

CPU

Memory

Slab

RSS

PSS

Socket

BPF

Cgroup

Unknown

Leak

전부 출력됩니다.


v1

먼저 아래 기능부터 구현합니다.

✔ /proc/meminfo

✔ RSS

✔ PSS

✔ Slab

✔ Socket

✔ Cgroup

✔ Unknown

v1-1

오늘 만드는 부분입니다.

meminfo.py

util.py

color.py

report.py

main.py

util.py

from pathlib import Path

def read_kv_file(path):

    data = {}

    p = Path(path)

    if not p.exists():
        return data

    for line in p.read_text().splitlines():

        if ":" not in line:
            continue

        k,v=line.split(":",1)

        data[k.strip()] = v.strip()

    return data


def kb_to_gb(kb):

    return kb/1024/1024

meminfo.py

from util import read_kv_file,kb_to_gb


class MemInfo:

    def __init__(self):

        self.data=read_kv_file("/proc/meminfo")


    def get(self,key):

        try:
            return int(self.data[key].split()[0])
        except:
            return 0


    @property
    def total(self):

        return self.get("MemTotal")


    @property
    def free(self):

        return self.get("MemFree")


    @property
    def available(self):

        return self.get("MemAvailable")


    @property
    def used(self):

        return self.total-self.free


    def spectrum(self):

        keys=[

            "AnonPages",
            "Cached",
            "Buffers",
            "Slab",
            "SReclaimable",
            "SUnreclaim",
            "KernelStack",
            "PageTables",
            "Percpu",
            "VmallocUsed",
            "Active(file)",
            "Inactive(file)",
            "Active(anon)",
            "Inactive(anon)",
            "Dirty",
            "Writeback",
            "Unevictable",
            "Shmem",
            "AnonHugePages"

        ]

        result={}

        for k in keys:

            result[k]=kb_to_gb(self.get(k))

        return result

report.py

from util import kb_to_gb


def print_header(title):

    print("="*80)

    print(title.center(80))

    print("="*80)


def print_item(name,val,total):

    pct=0

    if total>0:

        pct=val*100/total

    print(f"{name:25s}{val:10.2f} GB   {pct:6.2f}%")

main.py

from meminfo import MemInfo
from util import kb_to_gb
from report import *

m=MemInfo()

print_header("Memory Spectrum")

print()

print(f"Total     : {kb_to_gb(m.total):.2f} GB")

print(f"Used      : {kb_to_gb(m.used):.2f} GB")

print(f"Free      : {kb_to_gb(m.free):.2f} GB")

print(f"Available : {kb_to_gb(m.available):.2f} GB")

print()

print_header("Spectrum")

for k,v in m.spectrum().items():

    print_item(k,v,kb_to_gb(m.total))

실행하면

================================================================================
                              Memory Spectrum
================================================================================

Total      : 1007.33 GB
Used       : 724.81 GB
Free       : 114.57 GB
Available  : 281.16 GB

================================================================================
Spectrum
================================================================================

AnonPages                  201.31 GB  19.99%
Cached                      170.15 GB  16.89%
Buffers                       0.12 GB   0.01%
Slab                         69.82 GB   6.93%
SReclaimable                42.83 GB   4.25%
SUnreclaim                  26.99 GB   2.68%
KernelStack                  1.21 GB   0.12%
PageTables                   2.91 GB   0.29%
Percpu                       0.44 GB   0.04%
...

다음 단계(v1-2)

다음 답변에서는 RSS/PSS 분석기를 추가하겠습니다. 단순히 ps를 사용하는 것이 아니라 /proc/*/smaps_rollup을 직접 읽어서:

  • PID별 RSS/PSS
  • 프로세스명별 집계(예: minio 여러 프로세스 합산)
  • Kubernetes Pod별 집계
  • Namespace별 집계
  • Top 30 RSS/PSS 출력

까지 구현하겠습니다. 이것부터 실제 "Used 메모리의 행방"을 찾는 데 매우 큰 도움이 됩니다.

==

좋습니다. 이제부터는 실제 운영에서 사용할 수 있는 수준으로 만들겠습니다.

현재 구조에서 가장 중요한 것은 RSS/PSS입니다. 많은 사람들이 ps aux의 RSS만 보는데, Kubernetes 환경에서는 이것만으로는 실제 메모리 사용량을 설명하지 못합니다. PSS(Proportional Set Size)까지 봐야 공유 메모리까지 적절히 분배된 실제 사용량을 파악할 수 있습니다.


v1-2 디렉터리 구조

mem-spectrum/

    mem-spectrum.py

    core/
        meminfo.py
        process.py   <-- 이번에 추가
        rss.py       <-- 이번에 추가
        util.py
        report.py

process.py

이 모듈은 /proc을 직접 읽습니다.

from pathlib import Path

def list_pids():

    for p in Path("/proc").iterdir():

        if p.name.isdigit():
            yield int(p.name)

프로세스 이름 읽기

def process_name(pid):

    try:
        return Path(f"/proc/{pid}/comm").read_text().strip()
    except:
        return "unknown"

cmdline

def cmdline(pid):

    try:

        cmd = Path(f"/proc/{pid}/cmdline").read_bytes()

        return cmd.replace(b"\0",b" ").decode()

    except:

        return ""

rss.py

여기가 핵심입니다.

from pathlib import Path

def parse_rollup(pid):

    data={}

    p=Path(f"/proc/{pid}/smaps_rollup")

    if not p.exists():

        return None

    try:

        for line in p.read_text().splitlines():

            if ":" not in line:
                continue

            k,v=line.split(":",1)

            data[k]=int(v.split()[0])

        return data

    except:

        return None

RSS

def rss(pid):

    d=parse_rollup(pid)

    if not d:

        return 0

    return d.get("Rss",0)

PSS

def pss(pid):

    d=parse_rollup(pid)

    if not d:

        return 0

    return d.get("Pss",0)

Anonymous

def anon(pid):

    d=parse_rollup(pid)

    if not d:

        return 0

    return d.get("Anonymous",0)

Shared

def shared(pid):

    d=parse_rollup(pid)

    if not d:

        return 0

    return d.get("Shared_Clean",0)+d.get("Shared_Dirty",0)

Private

def private(pid):

    d=parse_rollup(pid)

    if not d:

        return 0

    return d.get("Private_Clean",0)+d.get("Private_Dirty",0)

Top Process 집계

from process import *
from rss import *

table=[]

for pid in list_pids():

    r=rss(pid)

    if r==0:
        continue

    table.append({

        "pid":pid,

        "name":process_name(pid),

        "rss":r,

        "pss":pss(pid),

        "anon":anon(pid),

        "shared":shared(pid),

        "private":private(pid)

    })

table.sort(key=lambda x:x["rss"],reverse=True)

출력

PID      Process              RSS(GB)    PSS(GB)

1034     minio                 22.4       21.9

931      java                  19.2       18.5

612      kubelet                8.1        7.6

...

프로세스별 합산

예를 들어

minio

PID 103

PID 104

PID 105

처럼 여러 개 있으면

MinIO Total

RSS

PSS

를 계산합니다.

summary={}
for p in table:

    n=p["name"]

    if n not in summary:

        summary[n]={

            "rss":0,

            "pss":0,

            "count":0

        }

    summary[n]["rss"]+=p["rss"]

    summary[n]["pss"]+=p["pss"]

    summary[n]["count"]+=1

출력

Process Summary

minio

    Count : 16

    RSS : 202 GB

    PSS : 199 GB

java

    Count : 42

    RSS : 55 GB

...

Kubernetes Pod까지 연결

이제 재미있는 부분입니다.

PID에서

/proc/PID/cgroup

를 보면

kubepods.slice

podxxxxxxxx

container id

가 있습니다.

거기서

Pod UID

를 추출합니다.

그러면

PID

↓

Pod UID

↓

Container

↓

Pod Name

↓

Namespace

까지 연결 가능합니다.

최종 출력은

Top Pod

Namespace

RSS

PSS

가 됩니다.

Namespace       Pod                  RSS

minio

minio-0

18 GB

spark

driver-xxx

14 GB

starrocks

be-1

12 GB

이 기능은 Kubernetes 환경에서 어떤 Pod가 실제 메모리를 많이 사용하는지를 확인하는 데 매우 유용합니다.


다음 단계(v1-3)

다음 단계에서는 Slab 분석기를 구현하겠습니다. 단순히 Slab=70GB를 보여주는 것이 아니라:

  • /proc/slabinfo 파싱
  • slabtop -o와 동일한 Top 20 생성
  • dentry, inode_cache, kmalloc-*, xfs_inode, skbuff_head_cache 등을 자동 분류
  • Leak 의심 항목 자동 표시

예를 들어 다음과 같이 출력되도록 만들겠습니다.

Top Slab

dentry                18.4 GB
xfs_inode             11.2 GB
kmalloc-512            8.3 GB
inode_cache            5.1 GB

Analysis
--------
✓ Filesystem metadata cache is dominant.
⚠ dentry cache exceeds 15% of total Slab.
⚠ xfs_inode growth is unusually high.

이 단계까지 오면 RSS + PSS + Slab만으로도 free의 Used 메모리 대부분을 설명할 수 있는 기반이 마련됩니다.

==

좋습니다. 이제부터가 가장 중요한 부분입니다.

Slab 분석은 일반적인 Linux 진단도구보다 훨씬 자세하게 만들겠습니다.

AIStor + DirectPV + Cilium에서는 RSS보다 Slab이 병목인 경우가 생각보다 많습니다.


v1-3 Slab Analyzer

목표는

Slab 72GB

↓

도대체 어떤 cache가 72GB를 쓰는가?

↓

Top20

↓

Leak인가?

↓

Filesystem 때문인가?

↓

Network 때문인가?

까지 보여주는 것입니다.


슬랩 종류

Linux는 수백 개의 slab cache를 갖고 있습니다.

예를 들어

kmalloc-64
kmalloc-512

dentry

inode_cache

xfs_inode

ext4_inode_cache

buffer_head

skbuff_head_cache

radix_tree_node

proc_inode_cache

vm_area_struct

signal_cache

task_struct

이런 것들이 있습니다.

하지만 운영자가 보고 싶은 것은

Filesystem

Network

Memory

Kernel

Security

Etc

입니다.

그래서 자동 분류합니다.


slab.py

먼저

/proc/slabinfo

를 읽습니다.

from pathlib import Path

SLABINFO="/proc/slabinfo"

def parse():

    result=[]

    with open(SLABINFO) as f:

        for line in f:

            if line.startswith("#"):
                continue

            cols=line.split()

            if len(cols)<7:
                continue

            name=cols[0]

            active=int(cols[1])

            total=int(cols[2])

            size=int(cols[3])

            bytes_used=active*size

            result.append({

                "name":name,

                "active":active,

                "total":total,

                "size":size,

                "bytes":bytes_used

            })

    return result

GB 변환

def gb(x):

    return x/1024/1024/1024

Top20

def top20():

    slabs=parse()

    slabs.sort(

        key=lambda x:x["bytes"],

        reverse=True

    )

    return slabs[:20]

Category 자동 분류

이게 핵심입니다.

CATEGORY={

"dentry":"Filesystem",

"inode":"Filesystem",

"xfs":"Filesystem",

"ext4":"Filesystem",

"buffer_head":"Filesystem",

"radix_tree":"Filesystem",

"skbuff":"Network",

"nf_conntrack":"Network",

"kmalloc":"Kernel",

"vm_area":"Memory",

"anon_vma":"Memory",

"task_struct":"Process",

"signal_cache":"Process",

"cred":"Security",

"selinux":"Security",

"bpf":"BPF",

}

자동 분류

def category(name):

    for k,v in CATEGORY.items():

        if k in name:

            return v

    return "Other"

출력

Top Slab Cache

Name                 Category          GB

dentry               Filesystem       18.2

xfs_inode            Filesystem       12.1

inode_cache          Filesystem        5.8

kmalloc-512          Kernel            4.3

skbuff_head_cache    Network           3.8

...

Category별 합산

이게 더 중요합니다.

Filesystem

36GB

Network

12GB

Kernel

9GB

Memory

2GB

자동 계산

summary={}
for s in parse():

    c=category(s["name"])

    summary.setdefault(c,0)

    summary[c]+=s["bytes"]

출력

Filesystem      38 GB

Network         13 GB

Kernel          10 GB

Memory           2 GB

Security         0.2 GB

Leak Detector

이제 규칙을 넣습니다.

예를 들어

Filesystem > 60%

이면

Filesystem cache dominates Slab.

dentry > 20GB

이면

Possible dentry leak.

inode_cache >15GB

이면

Large inode cache.

skbuff >10GB

이면

Large socket buffer.

kmalloc-*

합이

20GB

넘으면

Possible kernel allocation growth.

bpf*

합이

5GB

넘으면

Check bpftool map show.

AIStor 전용 Rule

AIStor에서는

Filesystem

↓

dentry

xfs_inode

inode_cache

가 많습니다.

예를 들어

Filesystem

45GB

dentry

20GB

inode

15GB

이면

Many object lookups.

Likely metadata cache.

Usually normal.

반대로

skbuff

15GB

이면

Likely Cilium / TCP / Socket buffers.

Check sockstat.

Check BPF.

DirectPV Rule

buffer_head

가 많으면

Buffered IO.

Not O_DIRECT.

Page Cache

Huge

+

buffer_head

Huge

이면

Likely buffered filesystem IO.

Unknown 계산 개선

현재는

Unknown

=

Used

-

RSS

-

Cache

-

Slab

였는데

이제는

Used

=

RSS

+

PageCache

+

Slab

+

Kernel

+

Sock

+

Huge

+

Percpu

+

Unknown

까지 계산합니다.


다음 단계(v1-4)

다음 단계는 가장 중요한 기능입니다.

실제로 AIStor/Cilium 환경에서 원인을 찾는 데 결정적인 역할을 하는 BPF + Socket + cgroup Memory Analyzer를 구현하겠습니다. 여기서는 다음을 자동으로 분석합니다.

  • bpftool map show를 이용한 BPF Map 메모리 사용량 합계
  • Cilium CT/LB/Policy Map별 메모리
  • /proc/net/sockstat 기반 TCP/UDP/FRAG 메모리
  • memory.currentmemory.stat을 이용한 cgroup별 anon/file/slab/sock 사용량
  • 마지막으로 Unknown Memory를 다시 계산하여 free의 Used 메모리를 가능한 한 모두 설명하도록 만들겠습니다.

이 단계까지 구현하면 AIStor + DirectPV + Cilium 환경에서 "왜 Used가 이렇게 큰가?"를 분석하는 데 필요한 핵심 기능이 거의 완성됩니다.

==

좋습니다. 여기서부터는 실제 원인을 찾는 기능입니다.

지금까지 말씀해주신 환경을 다시 보면

RSS                200GB
Slab                70GB
Cache              171GB
-------------------------
Known              441GB

Used               725GB

Unknown            284GB

여기서 284GB는 절대로 정상적으로 무시할 수 있는 수준이 아닙니다.

실제로 Linux에서 이렇게 큰 차이가 나는 경우는 대부분 아래 중 하나입니다.

  • cgroup memory accounting
  • BPF map memory
  • socket memory
  • pinned pages
  • page cache(active/inactive)
  • vmalloc/percpu
  • kernel allocator
  • memory.stat에만 나타나는 file/anon

즉 이제부터는 /proc/meminfo가 아니라 kernel 내부 accounting을 모아야 합니다.


v1-4

이번에 추가되는 모듈입니다.

core/

cgroup.py
sock.py
bpf.py
unknown.py

① cgroup Analyzer

이게 가장 중요합니다.

v2에서는

memory.current

memory.stat

를 모두 읽습니다.

예를 들어

/sys/fs/cgroup/.../memory.stat

에는

anon
file
kernel
sock
slab

inactive_file
active_file

inactive_anon
active_anon

file_dirty
file_writeback

shmem

pagetables

percpu

등이 있습니다.

이걸 모두 합산합니다.

예를 들어

memory.current

410GB

↓

anon          205GB

file          161GB

slab           31GB

sock            6GB

kernel          7GB

코드

def parse_memory_stat(path):

    d={}

    with open(path) as f:

        for line in f:

            k,v=line.split()

            d[k]=int(v)

    return d

모든 cgroup

/sys/fs/cgroup

를 순회합니다.

find /sys/fs/cgroup -name memory.stat

처럼.


그리고

anon

file

sock

kernel

slab

를 모두 더합니다.


출력

CGROUP SUMMARY

anon

208GB

file

164GB

slab

32GB

sock

5GB

② Socket Memory

이건

/proc/net/sockstat

/proc/net/sockstat6

를 읽습니다.

TCP:

mem 1024000

UDP:

mem 4200

FRAG:

mem 128

자동 변환

TCP

3.9GB

UDP

21MB

FRAG

0MB

그리고

TCP >5GB

이면

Large TCP socket memory.

Possible heavy traffic.

③ BPF

이건 정말 중요합니다.

bpftool map show

를 실행합니다.

123:

hash

name cilium_ct4_global

memlock 1298473984

이런 식입니다.

우리는

memlock

만 더하면 됩니다.


출력

BPF

Total

31GB

그리고

Top

cilium_ct4_global

8GB

cilium_lb4_services

6GB

...

bpf*

slab도 같이 봅니다.


Cilium Rule

예를 들어

ct4

15GB

이면

Possible conntrack growth.

Check

cilium bpf ct list

LB

lb4

10GB

이면

Large LB map.

Policy

policy

8GB

이면

Many policy entries.

④ Unknown Calculator

여기가 핵심입니다.

기존에는

Known

=

RSS

+

Cache

+

Slab

였습니다.

이제

Known

=

RSS

+

PageCache

+

Slab

+

Sock

+

Kernel

+

BPF

+

Hugepage

+

Percpu

+

PageTable

가 됩니다.


RSS

202GB

Cache

171GB

Slab

69GB

Sock

4GB

Kernel

6GB

BPF

33GB

Huge

0

Percpu

0.4

PageTable

3GB

--------------------

Known

488GB

그러면

Used

725GB

Known

488GB

Unknown

237GB

그런데 여기서 끝이 아닙니다.

제가 지금 가장 의심하는 것은

memory.current

410GB

입니다.

RSS가 200GB인데

memory.current가 410GB라는 것은

memory.current

=

RSS

+

PageCache

+

Kernel

+

Sock

+

Slab

입니다.

Unknown의 상당 부분은 cgroup 안에 존재할 가능성이 매우 큽니다.

그래서 저는 v1-4에서 memory.current와 RSS를 자동 비교하도록 만들겠습니다.

예를 들어

memory.current

410GB

RSS

202GB

Difference

208GB

↓

file cache

170GB

↓

sock

5GB

↓

slab

30GB

↓

OK

이렇게 나오면 정상입니다.

반대로

memory.current

410GB

↓

RSS

200

↓

file

100

↓

slab

20

↓

sock

2

↓

Unknown

88GB

이면

WARNING

Large cgroup memory gap.

Investigate pinned pages or kernel memory.

다음(v1-5)

이게 마지막 핵심 기능입니다.

여기서는 Kubernetes + AIStor + DirectPV + Cilium을 자동으로 인식해서

★★★★★

Likely Root Cause

1.

Filesystem cache

★★★★★

2.

BPF map growth

★★★★☆

3.

Socket memory

★★★☆☆

4.

Page cache

★★★☆☆

5.

Slab leak

★★☆☆☆

처럼 원인 후보를 자동으로 랭킹해 주는 Diagnosis Engine을 구현하겠습니다.


그런데 한 가지 제안을 드리고 싶습니다.

이 프로젝트는 이미 단순 스크립트 수준을 넘어섰습니다. 지금까지 이야기한 기능을 모두 합치면 약 2,500~3,000줄 규모의 코드가 됩니다.

개인적으로는 Bash/Python 스크립트가 아니라 정식 오픈소스 프로젝트 수준으로 만드는 것을 추천드립니다.

예를 들면:

  • pip install mem-spectrum
  • mem-spectrum analyze
  • mem-spectrum report --html
  • mem-spectrum diff before.json after.json

처럼 사용할 수 있게 만들면, AIStor뿐 아니라 일반 Kubernetes 운영 환경에서도 매우 유용한 도구가 될 것입니다. 현재까지 논의한 기능이면 free, smem, slabtop 등의 출력을 통합해 "Used 메모리는 어디에 쓰이고 있는가?"를 한 번에 설명하는 도구로 충분한 가치가 있다고 생각합니다.

0개의 댓글