26M29b

Young-Kyoo Kim·2026년 5월 29일

대략적인 기준은 이렇게 잡으면 됩니다.

96 core 전체를 기준으로 균등해야 한다고 보면 안 되고, 실제 RX queue/IRQ가 배정된 “active core 수”를 기준으로 봐야 합니다. RSS/RPS는 receive 처리를 여러 CPU로 분산하기 위한 구조지만, flow hash 기반이라 단일/소수 elephant flow는 특정 queue/core에 몰릴 수 있습니다. Linux kernel 문서도 RSS/RPS/RFS/XPS를 “멀티프로세서에서 네트워크 처리 병렬성을 높이는 기법”으로 설명합니다. (커널 도메인)

1. 먼저 기준 active core 수를 잡아야 함

96 core 서버라도 NIC RX queue가 16개면, 정상적으로는 16개 안팎 core에 NET_RX가 몰릴 수 있습니다. 그래서 먼저 봐야 할 것은 이것입니다.

ethtool -l <bond1-slave1>
ethtool -l <bond1-slave2>

cat /proc/interrupts | egrep '<bond1-slave1>|<bond1-slave2>|ice|i40e|ixgbe'

Intel NIC 드라이버가 ice, i40e, ixgbe 중 무엇인지에 따라 이름은 다를 수 있습니다.

판단 기준은:

N = 실제 RX queue/IRQ가 분산된 core 수

입니다.
96개 전체 core가 아니라 N개 active core 안에서 균형을 봐야 합니다.


2. 10초 샘플 기준 실무 판단 기준

아래는 NET_RX/s 기준입니다.

상태판단 기준
정상 또는 허용top core가 active core median의 2배 이하
약한 biastop core가 median의 2~3배
의심top core가 median의 3~5배
강한 biastop core가 median의 5배 이상
매우 강한 biastop core가 median의 10배 이상

다만 median이 너무 작으면 왜곡되므로, active core는 보통 이렇게 정의하세요.

active core = NET_RX/s가 top core의 5% 이상인 core

예를 들어 10초 평균 결과가 이렇다면:

CPU12  400,000 NET_RX/s
CPU13  370,000
CPU14   80,000
CPU15   70,000
나머지  10,000 이하

이건 top core가 중간 core 대비 5배 이상이라 강한 bias로 봐도 됩니다.

반대로:

CPU12  120,000
CPU13  115,000
CPU14  108,000
CPU15  102,000
CPU16   95,000

이 정도면 정상 분산에 가깝습니다.


3. active core 수 대비 top share 기준

더 직관적인 기준은 top core가 전체 NET_RX 중 몇 %를 처리하느냐입니다.

이론적으로 N개 active core가 균등하면:

ideal share per core = 100 / N %

실무 기준은 이렇게 보면 됩니다.

top core share > ideal × 3  → bias 의심
top core share > ideal × 5  → 강한 bias

예를 들어 active core 수가 32개라면:

ideal = 100 / 32 = 3.1%
의심 = 9~10% 이상
강한 bias = 15~16% 이상

active core 수가 48개라면:

ideal = 100 / 48 = 2.1%
의심 = 6% 이상
강한 bias = 10% 이상

active core 수가 96개라면:

ideal = 100 / 96 = 1.0%
의심 = 3% 이상
강한 bias = 5% 이상

하지만 다시 강조하면, 96개 core 전체가 RX 처리에 쓰이고 있다는 전제가 없으면 96 기준으로 판단하면 안 됩니다.


4. 10초 샘플에서 제가 쓰는 판단표

10초 평균 기준으로는 이렇게 보겠습니다.

지표괜찮음의심강한 의심
max / median_active≤ 23~5≥ 5
top 1 core shareideal × 2 이하ideal × 3 이상ideal × 5 이상
top 4 core share20~30% 이하40~50% 이상60% 이상
active core 수RX queue 수와 유사RX queue보다 훨씬 적음1~4개 core에 집중
해당 core softnet time_squeeze/s0간헐 증가지속 증가
NIC drop/retrans 동시 증가없음일부명확

중요한 건 NET_RX 편중만으로 장애라고 보지는 않는다는 점입니다. 편중 + time_squeeze 증가 + TCP retransmission 또는 NIC RX drop 증가가 같이 있어야 application timeout과 강하게 연결됩니다.

Red Hat 문서도 softirqd가 한 번의 NAPI polling cycle에서 모든 packet을 가져오지 못하면 SoftIRQ CPU time이 부족하다는 지표이고, 이때 net.core.netdev_budget, net.core.netdev_budget_usecs를 통해 packet 처리량과 시간을 조정할 수 있다고 설명합니다. (Red Hat Documentation) Kernel 문서상으로도 netdev_budget은 한 polling cycle에서 처리할 packet 수이고, netdev_budget_usecs는 한 cycle에서 사용할 수 있는 최대 시간입니다. (커널 문서)


5. 바로 계산하는 스크립트

아래는 10초 동안 /proc/softirqs를 수집해서 NET_RX 편중도를 계산합니다.

cat > /tmp/netrx-bias.py <<'PY'
#!/usr/bin/env python3
import time
import statistics

interval = 10
active_threshold_ratio = 0.05  # top core의 5% 이상이면 active core로 봄

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()

rx_a = a.get("NET_RX", [0] * len(cpus))
rx_b = b.get("NET_RX", [0] * len(cpus))

rows = []
for i, cpu in enumerate(cpus):
    delta = rx_b[i] - rx_a[i]
    rate = delta / interval
    rows.append((cpu, rate))

rows.sort(key=lambda x: x[1], reverse=True)

total = sum(rate for _, rate in rows)
top_rate = rows[0][1] if rows else 0

active_rows = [(cpu, rate) for cpu, rate in rows if top_rate > 0 and rate >= top_rate * active_threshold_ratio]
active_rates = [rate for _, rate in active_rows]

if active_rates:
    median_active = statistics.median(active_rates)
    avg_active = statistics.mean(active_rates)
    max_active = max(active_rates)
    max_to_median = max_active / median_active if median_active else float("inf")
    active_count = len(active_rates)
    ideal_share = 100 / active_count
    top_share = top_rate / total * 100 if total else 0
    top4_share = sum(rate for _, rate in rows[:4]) / total * 100 if total else 0
    top8_share = sum(rate for _, rate in rows[:8]) / total * 100 if total else 0

    if max_to_median >= 5 or top_share >= ideal_share * 5 or top4_share >= 60:
        verdict = "STRONG_BIAS"
    elif max_to_median >= 3 or top_share >= ideal_share * 3 or top4_share >= 40:
        verdict = "SUSPECT_BIAS"
    elif max_to_median >= 2 or top_share >= ideal_share * 2:
        verdict = "MILD_BIAS"
    else:
        verdict = "OK_OR_ACCEPTABLE"
else:
    median_active = avg_active = max_active = max_to_median = active_count = ideal_share = top_share = top4_share = top8_share = 0
    verdict = "NO_ACTIVE_RX"

print(f"interval_sec={interval}")
print(f"total_NET_RX_per_sec={total:,.0f}")
print(f"active_core_threshold=top_core * {active_threshold_ratio:.2f}")
print(f"active_core_count={active_count}")
print(f"ideal_share_per_active_core={ideal_share:.2f}%")
print(f"top_core_share={top_share:.2f}%")
print(f"top4_share={top4_share:.2f}%")
print(f"top8_share={top8_share:.2f}%")
print(f"max_to_median_active={max_to_median:.2f}")
print(f"verdict={verdict}")
print()
print(f"{'CPU':<8} {'NET_RX/s':>15} {'share%':>10} {'active':>8}")
print("-" * 46)

for cpu, rate in rows[:30]:
    share = rate / total * 100 if total else 0
    active = "yes" if top_rate > 0 and rate >= top_rate * active_threshold_ratio else "no"
    print(f"{cpu:<8} {rate:15,.0f} {share:9.2f}% {active:>8}")
PY

chmod +x /tmp/netrx-bias.py
/tmp/netrx-bias.py

출력 예:

interval_sec=10
total_NET_RX_per_sec=4,800,000
active_core_count=32
ideal_share_per_active_core=3.12%
top_core_share=14.80%
top4_share=47.50%
top8_share=68.20%
max_to_median_active=4.70
verdict=SUSPECT_BIAS

이 경우는 상당히 의심입니다.
특히 같은 시각에 time_squeeze/s가 증가하면 더 의미가 큽니다.


6. softnet의 time_squeeze까지 같이 붙이는 버전

NET_RX가 많은 core에서 time_squeeze도 같이 증가하는지가 중요하므로, 아래 버전이 더 좋습니다.

cat > /tmp/netrx-softnet-bias.py <<'PY'
#!/usr/bin/env python3
import time
import statistics

interval = 10
active_threshold_ratio = 0.05

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

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)
            squeeze = int(cols[2], 16)
            rows.append((processed, dropped, squeeze))
    return rows

cpus, si_a = read_softirqs()
sn_a = read_softnet()

time.sleep(interval)

_, si_b = read_softirqs()
sn_b = read_softnet()

rows = []
for i, cpu in enumerate(cpus):
    rx = (si_b.get("NET_RX", [0]*len(cpus))[i] - si_a.get("NET_RX", [0]*len(cpus))[i]) / interval
    tx = (si_b.get("NET_TX", [0]*len(cpus))[i] - si_a.get("NET_TX", [0]*len(cpus))[i]) / interval

    processed = dropped = squeeze = 0
    if i < len(sn_a) and i < len(sn_b):
        processed = (sn_b[i][0] - sn_a[i][0]) / interval
        dropped = (sn_b[i][1] - sn_a[i][1]) / interval
        squeeze = (sn_b[i][2] - sn_a[i][2]) / interval

    rows.append((cpu, rx, tx, processed, dropped, squeeze))

rows.sort(key=lambda x: x[1], reverse=True)

total_rx = sum(r[1] for r in rows)
top_rx = rows[0][1] if rows else 0

active = [r for r in rows if top_rx > 0 and r[1] >= top_rx * active_threshold_ratio]
active_rx = [r[1] for r in active]

if active_rx:
    active_count = len(active_rx)
    ideal_share = 100 / active_count
    median_rx = statistics.median(active_rx)
    max_to_median = top_rx / median_rx if median_rx else float("inf")
    top_share = top_rx / total_rx * 100 if total_rx else 0
    top4_share = sum(r[1] for r in rows[:4]) / total_rx * 100 if total_rx else 0
else:
    active_count = 0
    ideal_share = 0
    max_to_median = 0
    top_share = 0
    top4_share = 0

total_dropped = sum(r[4] for r in rows)
total_squeeze = sum(r[5] for r in rows)

if max_to_median >= 5 or (ideal_share and top_share >= ideal_share * 5) or top4_share >= 60:
    verdict = "STRONG_BIAS"
elif max_to_median >= 3 or (ideal_share and top_share >= ideal_share * 3) or top4_share >= 40:
    verdict = "SUSPECT_BIAS"
elif max_to_median >= 2 or (ideal_share and top_share >= ideal_share * 2):
    verdict = "MILD_BIAS"
else:
    verdict = "OK_OR_ACCEPTABLE"

print(f"interval_sec={interval}")
print(f"total_NET_RX_per_sec={total_rx:,.0f}")
print(f"active_core_count={active_count}")
print(f"ideal_share_per_active_core={ideal_share:.2f}%")
print(f"top_core_share={top_share:.2f}%")
print(f"top4_share={top4_share:.2f}%")
print(f"max_to_median_active={max_to_median:.2f}")
print(f"softnet_dropped_per_sec_total={total_dropped:.3f}")
print(f"softnet_time_squeeze_per_sec_total={total_squeeze:.3f}")
print(f"verdict={verdict}")
print()
print(f"{'CPU':<8} {'NET_RX/s':>13} {'share%':>8} {'NET_TX/s':>13} {'softnet_proc/s':>15} {'drop/s':>9} {'squeeze/s':>10}")
print("-" * 86)

for cpu, rx, tx, processed, dropped, squeeze in rows[:30]:
    share = rx / total_rx * 100 if total_rx else 0
    print(f"{cpu:<8} {rx:13,.0f} {share:7.2f}% {tx:13,.0f} {processed:15,.0f} {dropped:9.3f} {squeeze:10.3f}")
PY

chmod +x /tmp/netrx-softnet-bias.py
/tmp/netrx-softnet-bias.py

7. 결과 해석 예시

예시 1: 문제 가능성 낮음

active_core_count=32
ideal_share_per_active_core=3.12%
top_core_share=5.20%
top4_share=18.50%
max_to_median_active=1.70
softnet_dropped_per_sec_total=0.000
softnet_time_squeeze_per_sec_total=0.000
verdict=OK_OR_ACCEPTABLE

이 정도면 NET_RX 분산은 양호합니다.


예시 2: 편중은 있지만 아직 장애 근거 약함

active_core_count=32
ideal_share_per_active_core=3.12%
top_core_share=11.00%
top4_share=38.00%
max_to_median_active=3.40
softnet_dropped_per_sec_total=0.000
softnet_time_squeeze_per_sec_total=0.000
verdict=SUSPECT_BIAS

편중은 있지만 dropped/s=0, squeeze/s=0이면 아직 application timeout과 직접 연결하기는 약합니다. 다만 IRQ/RSS 분산 상태는 확인할 만합니다.


예시 3: timeout과 관련 가능성 높음

active_core_count=16
ideal_share_per_active_core=6.25%
top_core_share=32.00%
top4_share=72.00%
max_to_median_active=7.50
softnet_dropped_per_sec_total=0.000
softnet_time_squeeze_per_sec_total=25.000
verdict=STRONG_BIAS

이 경우는 RX 처리 편중 + softirq budget 부족 신호가 같이 있으므로 application tail latency와 연결될 가능성이 큽니다.


예시 4: 직접적인 packet loss 가능성

softnet_dropped_per_sec_total > 0

이게 나오면 더 심각합니다. /proc/net/softnet_stat의 두 번째 컬럼은 CPU별 backlog queue가 full이어서 drop된 frame 수로 해석됩니다. (Insights-Core) 이 경우는 net.core.netdev_max_backlog, RX ring, IRQ 분산, softirq budget을 같이 봐야 합니다.


8. 현재 환경에 맞춘 제 기준

지금처럼 96 core, 25G × 2 bond, K8s east-west traffic이 bond1에 집중된 환경이면 저는 10초 샘플에서 이렇게 판단하겠습니다.

max/median_active <= 2
top4_share <= 30%
time_squeeze/s = 0
dropped/s = 0

이면 정상 범위.

max/median_active >= 3
또는 top4_share >= 40%

이면 bias 의심.

max/median_active >= 5
또는 top4_share >= 60%
또는 active core 수가 RX queue 수보다 훨씬 적음

이면 강한 bias.

그리고 아래가 같이 있으면 application timeout과 연결 가능성이 높습니다.

time_squeeze/s 증가
NIC ethtool rx_no_buffer/rx_missed/rx_queue_drop 증가
TCP retransmission 증가
MinIO 5xx/quorum 저하 시각과 일치

반대로 편중은 있어도 time_squeeze/s=0, dropped/s=0, retransmission 증가 없음이면, 그 편중은 LACP/RSS hash 특성 또는 elephant flow 때문일 수 있고 바로 장애 원인으로 보긴 어렵습니다.

0개의 댓글