좋습니다. 다만 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 전체 분석예상 출력
==============================================================
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줄 내외가 되겠지만, 실제 운영 환경에서 바로 사용할 수 있는 수준으로 제공해드릴 수 있습니다.
다만 이 채팅의 출력 제한 때문에 한 번에 전체 소스를 제공하는 것은 불가능합니다. 따라서 여러 차례에 걸쳐 각 모듈을 완성해 나가는 방식이 가장 현실적입니다.
==
좋습니다. 단계적으로 만들면 꽤 좋은 도구가 될 것 같습니다.
그리고 하나 말씀드리고 싶은 것이 있습니다.
현재 여러분 환경은
를 분석하는 것이 목적입니다.
이 정도면 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
전부 출력됩니다.
먼저 아래 기능부터 구현합니다.
✔ /proc/meminfo
✔ RSS
✔ PSS
✔ Slab
✔ Socket
✔ Cgroup
✔ Unknown
오늘 만드는 부분입니다.
meminfo.py
util.py
color.py
report.py
main.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
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
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}%")
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%
...
다음 답변에서는 RSS/PSS 분석기를 추가하겠습니다. 단순히 ps를 사용하는 것이 아니라 /proc/*/smaps_rollup을 직접 읽어서:
까지 구현하겠습니다. 이것부터 실제 "Used 메모리의 행방"을 찾는 데 매우 큰 도움이 됩니다.
==
좋습니다. 이제부터는 실제 운영에서 사용할 수 있는 수준으로 만들겠습니다.
현재 구조에서 가장 중요한 것은 RSS/PSS입니다. 많은 사람들이 ps aux의 RSS만 보는데, Kubernetes 환경에서는 이것만으로는 실제 메모리 사용량을 설명하지 못합니다. PSS(Proportional Set Size)까지 봐야 공유 메모리까지 적절히 분배된 실제 사용량을 파악할 수 있습니다.
mem-spectrum/
mem-spectrum.py
core/
meminfo.py
process.py <-- 이번에 추가
rss.py <-- 이번에 추가
util.py
report.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"
def cmdline(pid):
try:
cmd = Path(f"/proc/{pid}/cmdline").read_bytes()
return cmd.replace(b"\0",b" ").decode()
except:
return ""
여기가 핵심입니다.
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)
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
...
이제 재미있는 부분입니다.
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가 실제 메모리를 많이 사용하는지를 확인하는 데 매우 유용합니다.
다음 단계에서는 Slab 분석기를 구현하겠습니다. 단순히 Slab=70GB를 보여주는 것이 아니라:
/proc/slabinfo 파싱slabtop -o와 동일한 Top 20 생성dentry, inode_cache, kmalloc-*, xfs_inode, skbuff_head_cache 등을 자동 분류예를 들어 다음과 같이 출력되도록 만들겠습니다.
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이 병목인 경우가 생각보다 많습니다.
목표는
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
입니다.
그래서 자동 분류합니다.
먼저
/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
def gb(x):
return x/1024/1024/1024
def top20():
slabs=parse()
slabs.sort(
key=lambda x:x["bytes"],
reverse=True
)
return slabs[:20]
이게 핵심입니다.
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
...
이게 더 중요합니다.
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
이제 규칙을 넣습니다.
예를 들어
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에서는
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.
buffer_head
가 많으면
Buffered IO.
Not O_DIRECT.
Page Cache
Huge
+
buffer_head
Huge
이면
Likely buffered filesystem IO.
현재는
Unknown
=
Used
-
RSS
-
Cache
-
Slab
였는데
이제는
Used
=
RSS
+
PageCache
+
Slab
+
Kernel
+
Sock
+
Huge
+
Percpu
+
Unknown
까지 계산합니다.
다음 단계는 가장 중요한 기능입니다.
실제로 AIStor/Cilium 환경에서 원인을 찾는 데 결정적인 역할을 하는 BPF + Socket + cgroup Memory Analyzer를 구현하겠습니다. 여기서는 다음을 자동으로 분석합니다.
bpftool map show를 이용한 BPF Map 메모리 사용량 합계/proc/net/sockstat 기반 TCP/UDP/FRAG 메모리memory.current와 memory.stat을 이용한 cgroup별 anon/file/slab/sock 사용량free의 Used 메모리를 가능한 한 모두 설명하도록 만들겠습니다.이 단계까지 구현하면 AIStor + DirectPV + Cilium 환경에서 "왜 Used가 이렇게 큰가?"를 분석하는 데 필요한 핵심 기능이 거의 완성됩니다.
==
좋습니다. 여기서부터는 실제 원인을 찾는 기능입니다.
지금까지 말씀해주신 환경을 다시 보면
RSS 200GB
Slab 70GB
Cache 171GB
-------------------------
Known 441GB
Used 725GB
Unknown 284GB
여기서 284GB는 절대로 정상적으로 무시할 수 있는 수준이 아닙니다.
실제로 Linux에서 이렇게 큰 차이가 나는 경우는 대부분 아래 중 하나입니다.
즉 이제부터는 /proc/meminfo가 아니라 kernel 내부 accounting을 모아야 합니다.
이번에 추가되는 모듈입니다.
core/
cgroup.py
sock.py
bpf.py
unknown.py
이게 가장 중요합니다.
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
이건
/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.
이건 정말 중요합니다.
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도 같이 봅니다.
예를 들어
ct4
15GB
이면
Possible conntrack growth.
Check
cilium bpf ct list
LB
lb4
10GB
이면
Large LB map.
Policy
policy
8GB
이면
Many policy entries.
여기가 핵심입니다.
기존에는
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.
이게 마지막 핵심 기능입니다.
여기서는 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-spectrummem-spectrum analyzemem-spectrum report --htmlmem-spectrum diff before.json after.json처럼 사용할 수 있게 만들면, AIStor뿐 아니라 일반 Kubernetes 운영 환경에서도 매우 유용한 도구가 될 것입니다. 현재까지 논의한 기능이면 free, smem, slabtop 등의 출력을 통합해 "Used 메모리는 어디에 쓰이고 있는가?"를 한 번에 설명하는 도구로 충분한 가치가 있다고 생각합니다.