/proc/softirqs는 CPU별 누적 카운터라서 그대로 보면 비교가 어렵습니다.
아래처럼 interval 동안 얼마나 증가했는지를 계산해서 per second로 바꿔 보는 게 좋습니다.
장애 분석에서는 우선 NET_RX부터 보면 됩니다.
python3 - <<'PY'
import time
interval = 1
def read_softirqs():
data = {}
with open("/proc/softirqs") as f:
header = f.readline().split()
cpus = header
for line in f:
parts = line.replace(":", "").split()
if not parts:
continue
name = parts[0]
values = list(map(int, parts[1:]))
data[name] = values
return cpus, data
cpus, a = read_softirqs()
time.sleep(interval)
_, b = read_softirqs()
rows = []
for i, cpu in enumerate(cpus):
net_rx = b.get("NET_RX", [0]*len(cpus))[i] - a.get("NET_RX", [0]*len(cpus))[i]
rows.append((cpu, net_rx / interval))
rows.sort(key=lambda x: x[1], reverse=True)
print(f"{'CPU':<8} {'NET_RX/s':>15}")
print("-" * 25)
for cpu, rate in rows:
print(f"{cpu:<8} {rate:15,.0f}")
PY
출력 예시는 이런 식입니다.
CPU NET_RX/s
-------------------------
CPU12 245,112
CPU13 238,901
CPU14 12,301
CPU15 10,882
CPU0 1,022
이렇게 보면 특정 CPU에 RX softirq가 몰리는지 바로 보입니다.
NIC/driver 쪽이면 NET_RX만 보지 말고 NET_TX, TASKLET도 같이 보는 게 좋습니다.
python3 - <<'PY'
import time
interval = 1
keys = ["NET_RX", "NET_TX", "TASKLET", "TIMER", "SCHED", "RCU"]
def read_softirqs():
data = {}
with open("/proc/softirqs") as f:
cpus = f.readline().split()
for line in f:
parts = line.replace(":", "").split()
if not parts:
continue
data[parts[0]] = list(map(int, parts[1:]))
return cpus, data
cpus, a = read_softirqs()
time.sleep(interval)
_, b = read_softirqs()
rows = []
for i, cpu in enumerate(cpus):
row = {"CPU": cpu}
total = 0
for k in keys:
vals_a = a.get(k, [0] * len(cpus))
vals_b = b.get(k, [0] * len(cpus))
delta = vals_b[i] - vals_a[i]
rate = delta / interval
row[k] = rate
total += rate
row["TOTAL"] = total
rows.append(row)
rows.sort(key=lambda r: r["NET_RX"], reverse=True)
header = ["CPU"] + keys + ["TOTAL"]
print(
f"{'CPU':<8}"
f"{'NET_RX/s':>12}"
f"{'NET_TX/s':>12}"
f"{'TASKLET/s':>12}"
f"{'TIMER/s':>12}"
f"{'SCHED/s':>12}"
f"{'RCU/s':>12}"
f"{'TOTAL/s':>12}"
)
print("-" * 92)
for r in rows:
print(
f"{r['CPU']:<8}"
f"{r['NET_RX']:12,.0f}"
f"{r['NET_TX']:12,.0f}"
f"{r['TASKLET']:12,.0f}"
f"{r['TIMER']:12,.0f}"
f"{r['SCHED']:12,.0f}"
f"{r['RCU']:12,.0f}"
f"{r['TOTAL']:12,.0f}"
)
PY
여기서 중요한 건 보통 이 순서입니다.
| 항목 | 의미 |
|---|---|
NET_RX | RX packet 처리 softirq |
NET_TX | TX packet 처리 softirq |
TASKLET | 일부 NIC/driver 후처리와 연관 가능 |
SCHED | scheduler softirq |
RCU | RCU callback 처리 |
TIMER | timer softirq |
MinIO/K8s 네트워크 병목을 볼 때는 NET_RX가 특정 core에 몰리는지가 가장 중요합니다.
1초는 순간 변동이 커서 보기 어렵다면 interval = 5로 바꾸면 됩니다.
python3 - <<'PY'
import time
interval = 5
keys = ["NET_RX", "NET_TX", "TASKLET"]
def read_softirqs():
data = {}
with open("/proc/softirqs") as f:
cpus = f.readline().split()
for line in f:
parts = line.replace(":", "").split()
if parts:
data[parts[0]] = list(map(int, parts[1:]))
return cpus, data
cpus, a = read_softirqs()
time.sleep(interval)
_, b = read_softirqs()
rows = []
for i, cpu in enumerate(cpus):
net_rx = (b["NET_RX"][i] - a["NET_RX"][i]) / interval
net_tx = (b["NET_TX"][i] - a["NET_TX"][i]) / interval
tasklet = (b.get("TASKLET", [0]*len(cpus))[i] - a.get("TASKLET", [0]*len(cpus))[i]) / interval
rows.append((cpu, net_rx, net_tx, tasklet))
rows.sort(key=lambda x: x[1], reverse=True)
print(f"{'CPU':<8} {'NET_RX/s':>15} {'NET_TX/s':>15} {'TASKLET/s':>15}")
print("-" * 58)
for cpu, rx, tx, tasklet in rows:
print(f"{cpu:<8} {rx:15,.0f} {tx:15,.0f} {tasklet:15,.0f}")
PY
예를 들어 결과가 이렇게 나오면:
CPU NET_RX/s NET_TX/s TASKLET/s
----------------------------------------------------------
CPU4 380,000 15,000 2,000
CPU5 365,000 14,000 2,100
CPU6 12,000 3,000 300
CPU7 11,000 2,900 280
이건 CPU4, CPU5 쪽으로 RX 처리가 몰림입니다.
반대로 이렇게 나오면:
CPU NET_RX/s
-------------------------
CPU4 80,000
CPU5 78,000
CPU6 76,000
CPU7 75,000
CPU8 73,000
CPU9 72,000
이건 비교적 잘 분산된 상태입니다.
core가 많으면 상위 20개만 보는 게 편합니다.
python3 - <<'PY'
import time
interval = 1
topn = 20
def read_softirqs():
data = {}
with open("/proc/softirqs") as f:
cpus = f.readline().split()
for line in f:
parts = line.replace(":", "").split()
if parts:
data[parts[0]] = list(map(int, parts[1:]))
return cpus, data
cpus, a = read_softirqs()
time.sleep(interval)
_, b = read_softirqs()
rows = []
for i, cpu in enumerate(cpus):
rx = (b["NET_RX"][i] - a["NET_RX"][i]) / interval
tx = (b["NET_TX"][i] - a["NET_TX"][i]) / interval
rows.append((cpu, rx, tx))
rows.sort(key=lambda x: x[1], reverse=True)
print(f"Top {topn} CPUs by NET_RX/s")
print(f"{'CPU':<8} {'NET_RX/s':>15} {'NET_TX/s':>15}")
print("-" * 42)
for cpu, rx, tx in rows[:topn]:
print(f"{cpu:<8} {rx:15,.0f} {tx:15,.0f}")
PY
장애 시간대에 계속 보고 싶으면 아래 스크립트를 저장해서 쓰면 됩니다.
cat > /tmp/softirq-watch.py <<'PY'
#!/usr/bin/env python3
import time
import os
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--interval", type=float, default=1.0)
parser.add_argument("-n", "--top", type=int, default=20)
args = parser.parse_args()
keys = ["NET_RX", "NET_TX", "TASKLET"]
def read_softirqs():
data = {}
with open("/proc/softirqs") as f:
cpus = f.readline().split()
for line in f:
parts = line.replace(":", "").split()
if parts:
data[parts[0]] = list(map(int, parts[1:]))
return cpus, data
cpus, prev = read_softirqs()
while True:
time.sleep(args.interval)
cpus, cur = read_softirqs()
rows = []
for i, cpu in enumerate(cpus):
rx = (cur.get("NET_RX", [0]*len(cpus))[i] - prev.get("NET_RX", [0]*len(cpus))[i]) / args.interval
tx = (cur.get("NET_TX", [0]*len(cpus))[i] - prev.get("NET_TX", [0]*len(cpus))[i]) / args.interval
tasklet = (cur.get("TASKLET", [0]*len(cpus))[i] - prev.get("TASKLET", [0]*len(cpus))[i]) / args.interval
total = rx + tx + tasklet
rows.append((cpu, rx, tx, tasklet, total))
rows.sort(key=lambda x: x[1], reverse=True)
os.system("clear")
print(time.strftime("%F %T"))
print(f"interval={args.interval}s, top={args.top}, sorted by NET_RX/s")
print(f"{'CPU':<8} {'NET_RX/s':>15} {'NET_TX/s':>15} {'TASKLET/s':>15} {'RX%ofTop':>10}")
print("-" * 68)
total_rx_top = sum(r[1] for r in rows[:args.top]) or 1
for cpu, rx, tx, tasklet, total in rows[:args.top]:
rxpct = rx / total_rx_top * 100
print(f"{cpu:<8} {rx:15,.0f} {tx:15,.0f} {tasklet:15,.0f} {rxpct:9.1f}%")
prev = cur
PY
chmod +x /tmp/softirq-watch.py
실행:
/tmp/softirq-watch.py -i 1 -n 20
5초 평균:
/tmp/softirq-watch.py -i 5 -n 20
/proc/net/softnet_stat delta/proc/softirqs는 “softirq 발생량”이고, /proc/net/softnet_stat는 “처리 지연/드롭” 쪽입니다. 둘을 같이 봐야 합니다.
python3 - <<'PY'
import time
interval = 1
def read_softnet():
rows = []
with open("/proc/net/softnet_stat") as f:
for cpu, line in enumerate(f):
cols = line.split()
processed = int(cols[0], 16)
dropped = int(cols[1], 16)
time_squeeze = int(cols[2], 16)
rows.append((cpu, processed, dropped, time_squeeze))
return rows
a = read_softnet()
time.sleep(interval)
b = read_softnet()
rows = []
for before, after in zip(a, b):
cpu = before[0]
processed = (after[1] - before[1]) / interval
dropped = (after[2] - before[2]) / interval
squeeze = (after[3] - before[3]) / interval
rows.append((cpu, processed, dropped, squeeze))
rows.sort(key=lambda x: x[3], reverse=True)
print(f"{'CPU':<8} {'processed/s':>15} {'dropped/s':>15} {'squeeze/s':>15}")
print("-" * 58)
for cpu, processed, dropped, squeeze in rows[:20]:
print(f"CPU{cpu:<5} {processed:15,.0f} {dropped:15,.0f} {squeeze:15,.3f}")
PY
이렇게 같이 보면 좋습니다.
| 관찰 | 해석 |
|---|---|
NET_RX/s 특정 CPU 몰림 + squeeze/s 증가 | RX softirq budget 부족/분산 불균형 가능성 |
NET_RX/s 높지만 squeeze/s=0, dropped/s=0 | 현재는 잘 처리 중 |
dropped/s > 0 | backlog overflow성 packet drop 가능성 |
squeeze/s > 0만 증가 | packet loss보다는 softirq 처리 지연 가능성 |
현재처럼 bond1 쪽 K8s 트래픽이 많다면, 아래를 확인하면 됩니다.
NET_RX/s가 여러 CPU에 고르게 분산
softnet dropped/s = 0
softnet squeeze/s = 0 또는 매우 낮음
→ softirq/RX 처리 병목 가능성 낮음.
NET_RX/s가 특정 1~2개 CPU에 집중
softnet squeeze/s가 해당 CPU에서 증가
→ IRQ/RSS/RPS 분산, NIC queue 설정, irqbalance, netdev_budget 확인 필요.
NET_RX/s 특정 CPU 집중
softnet dropped/s 증가
ethtool RX no_buffer / missed / queue drop 증가
TCP retransmission 증가
→ application timeout과 직접 연관 가능성이 큼.
한 줄로 정리하면, /proc/softirqs는 누적값을 보지 말고 interval delta로 변환해서 NET_RX/s 기준으로 CPU별 정렬해 보세요. 그 결과에서 특정 CPU에 RX가 몰리고, 같은 CPU에서 /proc/net/softnet_stat의 time_squeeze/s가 같이 증가하면 네트워크 RX 처리 지연을 의심할 수 있습니다.