26M29a

Young-Kyoo Kim·2026년 5월 29일

/proc/softirqsCPU별 누적 카운터라서 그대로 보면 비교가 어렵습니다.
아래처럼 interval 동안 얼마나 증가했는지를 계산해서 per second로 바꿔 보는 게 좋습니다.


1. 가장 간단한 방법: NET_RX만 core별 비교

장애 분석에서는 우선 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가 몰리는지 바로 보입니다.


2. NET_RX, NET_TX, TASKLET까지 같이 보기

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_RXRX packet 처리 softirq
NET_TXTX packet 처리 softirq
TASKLET일부 NIC/driver 후처리와 연관 가능
SCHEDscheduler softirq
RCURCU callback 처리
TIMERtimer softirq

MinIO/K8s 네트워크 병목을 볼 때는 NET_RX가 특정 core에 몰리는지가 가장 중요합니다.


3. 5초 평균으로 보기

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

4. 특정 core 쏠림을 판단하는 기준

예를 들어 결과가 이렇게 나오면:

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

이건 비교적 잘 분산된 상태입니다.


5. 상위 N개 core만 보기

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

6. 연속 모니터링용 스크립트

장애 시간대에 계속 보고 싶으면 아래 스크립트를 저장해서 쓰면 됩니다.

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

7. 같이 보면 좋은 값: /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 > 0backlog overflow성 packet drop 가능성
squeeze/s > 0만 증가packet loss보다는 softirq 처리 지연 가능성

8. 이 결과로 무엇을 판단하면 되나?

현재처럼 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_stattime_squeeze/s가 같이 증가하면 네트워크 RX 처리 지연을 의심할 수 있습니다.

0개의 댓글