네트워크 드라이버 디버깅 & 체크리스트

네트워크 드라이버의 장애 대응과 품질 검증 절차를 정리합니다. devlink health 기반 리셋·복구, 회귀 테스트, netpoll/kdump 경로, 실전 트러블슈팅, 운영 지표(Observability) 체크리스트까지 다룹니다.

전제 조건: 네트워크 디바이스 드라이버 (net_device) 문서를 먼저 읽으세요. 장애 대응은 드라이버 동작 원리와 데이터 경로를 알아야 진단 순서가 설계되므로, 구조·콜백(Callback) 이해를 먼저 권장합니다.
일상 비유: 이 주제는 병원 응급실 프로토콜과 비슷합니다. 환자 분류(Triage)처럼 증상을 먼저 판별하고, 리셋(재생), 회귀 테스트(재발 방지), 관측 지표(모니터링)를 순서대로 진행합니다.

핵심 요약

  • devlink health — 드라이버 장애 상태 보고와 리셋·복구 진입점(Entry Point)입니다.
  • 회귀 테스트 — packetdrill/kselftest로 동작 회귀를 자동 검증합니다.
  • netpoll/kdump — 커널 패닉(Kernel Panic)(panic) 상황의 네트워크 출력 경로입니다.
  • 트러블슈팅 순서 — 증상 → 계측 → 원인 → 조치 단계를 고정합니다.
  • 운영 지표 — 드롭(Drop)·지연(Latency)·재시작(Reboot) 횟수를 대시보드로 관측합니다.

단계별 이해

  1. 장애 분류
    하드웨어·드라이버·스택 어느 계층 문제인지 구분합니다.
  2. 상태 확인
    devlink health와 커널 로그로 현재 상태를 수집합니다.
  3. 재현·회귀 테스트
    결함을 재현하고 실패 케이스를 고정합니다.
  4. 수정 후 검증
    리셋/복구 절차와 관측 지표로 안정성을 확인합니다.
대상 독자: 드라이버 장애를 진단하고 회귀를 방지하려는 개발자를 대상으로 합니다. 동작 원리는 네트워크 디바이스 드라이버 (net_device)를 먼저 읽을 것을 권장합니다.

리셋/장애 복구: devlink health와 watchdog

실서비스에서는 드라이버의 평균 성능보다 복구 전략이 더 중요합니다. TX timeout, 펌웨어(Firmware) hang, PCI AER 오류에서 자동 복구가 되지 않으면 장기 장애로 이어집니다.

/* 개념 예시: tx_timeout 복구를 workqueue로 분리 */
static void my_tx_timeout(struct net_device *ndev, unsigned int txqueue)
{
    struct my_priv *priv = netdev_priv(ndev);

    netdev_warn(ndev, "tx timeout on queue %u\\n", txqueue);
    schedule_work(&priv->reset_work);
}

static void my_reset_work(struct work_struct *work)
{
    struct my_priv *priv = container_of(work, struct my_priv, reset_work);

    rtnl_lock();
    my_ndo_stop(priv->ndev);
    my_hw_function_reset(priv);
    my_ndo_open(priv->ndev);
    rtnl_unlock();
}

devlink health 프레임워크는 NIC 드라이버의 장애 감지, 진단, 자동 복구를 체계화하는 인프라입니다. 리포터(Reporter)를 등록하면 devlink health 명령으로 운영팀이 장애 상태를 조회하고, 자동 복구를 트리거할 수 있습니다.

devlink health 복구 흐름 오류 감지 TX timeout / FW hang devlink_health_report() 리포터에 오류 보고 자동 복구 판단 grace period 확인 recover() 콜백 HW 리셋 수행 dump() 콜백 FW 상태/레지스터 덤프 diagnose() 콜백 현재 상태 진단 자동 Health 상태 전이 HEALTHY ERROR RECOVERING 오류 발생 복구 시작 복구 성공 복구 실패
/* 완전한 devlink health reporter 구현 */
static int my_health_recover(struct devlink_health_reporter *reporter,
                             void *priv_ctx,
                             struct netlink_ext_ack *extack)
{
    struct my_priv *priv = devlink_health_reporter_priv(reporter);
    int err;

    netdev_info(priv->ndev, "health recover: performing FLR\n");

    rtnl_lock();
    if (netif_running(priv->ndev))
        my_ndo_stop(priv->ndev);

    err = my_hw_function_reset(priv);
    if (err) {
        NL_SET_ERR_MSG_MOD(extack, "FLR failed");
        rtnl_unlock();
        return err;
    }

    err = my_ndo_open(priv->ndev);
    rtnl_unlock();
    return err;
}

static int my_health_dump(struct devlink_health_reporter *reporter,
                          struct devlink_fmsg *fmsg, void *priv_ctx,
                          struct netlink_ext_ack *extack)
{
    struct my_priv *priv = devlink_health_reporter_priv(reporter);
    struct my_err_ctx *ctx = priv_ctx;
    int err;

    err = devlink_fmsg_obj_nest_start(fmsg);
    if (err)
        return err;

    if (ctx) {
        devlink_fmsg_put(fmsg, "error_type", ctx->err_type);
        devlink_fmsg_put(fmsg, "queue_id", ctx->queue_id);
        devlink_fmsg_put(fmsg, "timestamp", ctx->timestamp);
    }

    devlink_fmsg_put(fmsg, "fw_state", my_hw_read_fw_state(priv));
    devlink_fmsg_put(fmsg, "hw_status", my_hw_read_status(priv));

    devlink_fmsg_pair_nest_start(fmsg, "queues");
    devlink_fmsg_arr_pair_nest_start(fmsg, "tx_queues");
    for (int i = 0; i < priv->num_tx_queues; i++) {
        devlink_fmsg_obj_nest_start(fmsg);
        devlink_fmsg_put(fmsg, "idx", i);
        devlink_fmsg_put(fmsg, "head", priv->tx_ring[i].head);
        devlink_fmsg_put(fmsg, "tail", priv->tx_ring[i].tail);
        devlink_fmsg_obj_nest_end(fmsg);
    }
    devlink_fmsg_arr_pair_nest_end(fmsg);
    devlink_fmsg_pair_nest_end(fmsg);

    return devlink_fmsg_obj_nest_end(fmsg);
}

static int my_health_diagnose(struct devlink_health_reporter *reporter,
                              struct devlink_fmsg *fmsg,
                              struct netlink_ext_ack *extack)
{
    struct my_priv *priv = devlink_health_reporter_priv(reporter);

    devlink_fmsg_obj_nest_start(fmsg);
    devlink_fmsg_put(fmsg, "link_up", netif_carrier_ok(priv->ndev));
    devlink_fmsg_put(fmsg, "fw_heartbeat", my_hw_check_heartbeat(priv));
    devlink_fmsg_put(fmsg, "pci_status", my_check_pci_status(priv));
    return devlink_fmsg_obj_nest_end(fmsg);
}

static const struct devlink_health_reporter_ops my_health_ops = {
    .name     = "tx",
    .recover  = my_health_recover,
    .dump     = my_health_dump,
    .diagnose = my_health_diagnose,
};

/* 프로브 시 리포터 등록 */
static int my_probe_health(struct my_priv *priv)
{
    priv->health_reporter = devlink_health_reporter_create(
        priv->devlink, &my_health_ops, 0, priv);
    return IS_ERR(priv->health_reporter) ?
           PTR_ERR(priv->health_reporter) : 0;
}

/* tx_timeout에서 health report 트리거 */
static void my_tx_timeout_health(struct net_device *ndev,
                                 unsigned int txqueue)
{
    struct my_priv *priv = netdev_priv(ndev);
    struct my_err_ctx ctx = {
        .err_type = "tx_timeout",
        .queue_id = txqueue,
        .timestamp = ktime_get_real_ns(),
    };
    devlink_health_report(priv->health_reporter,
                          "TX timeout detected", &ctx);
}

리셋 수준 세분화

NIC 리셋은 영향 범위에 따라 여러 수준으로 나뉩니다. 가능한 한 가장 좁은 범위의 리셋을 먼저 시도하고, 실패 시 더 넓은 범위로 에스컬레이션(Escalation)하는 것이 서비스 영향을 최소화하는 핵심입니다.

리셋 수준영향 범위서비스 중단사용 시점
큐 리셋(Queue Reset)단일 TX/RX 큐해당 큐만 일시 중단 (<1ms)단일 큐 TX timeout
FLR(Function-Level Reset)단일 PF/VF해당 함수의 모든 큐 중단 (~100ms)펌웨어 응답 없음
PF 리셋(PF Reset)PF + 소속 VF 전체모든 VF 포함 중단 (~1s)PF 펌웨어 hang
글로벌 리셋(Global Reset)NIC 전체카드 모든 트래픽 중단 (~5s)ASIC 오류
PCIe 버스(Bus) 리셋PCIe 슬롯 전체슬롯 내 모든 디바이스AER fatal 오류
/* 다단계 리셋 구현 */
static int my_escalated_reset(struct my_priv *priv,
                              enum my_reset_level level)
{
    int err;
    switch (level) {
    case MY_RESET_QUEUE:
        netdev_info(priv->ndev, "attempting queue-level reset\n");
        err = my_hw_queue_reset(priv, priv->err_queue);
        if (!err) return 0;
        /* fall through */
    case MY_RESET_FLR:
        my_ndo_stop(priv->ndev);
        err = pcie_flr(priv->pdev);
        if (!err && !my_hw_reinit(priv)) {
            my_ndo_open(priv->ndev);
            return 0;
        }
        /* fall through */
    case MY_RESET_PF:
        err = my_hw_pf_reset(priv);
        if (!err) { my_ndo_open(priv->ndev); return 0; }
        netdev_err(priv->ndev, "PF reset failed\n");
        return err;
    default: return -EINVAL;
    }
}

/* devlink reload 지원 */
static int my_devlink_reload_down(struct devlink *devlink, bool netns_change,
    enum devlink_reload_action action, enum devlink_reload_limit limit,
    struct netlink_ext_ack *extack)
{
    struct my_priv *priv = devlink_priv(devlink);
    if (action == DEVLINK_RELOAD_ACTION_DRIVER_REINIT) {
        rtnl_lock();
        if (netif_running(priv->ndev)) my_ndo_stop(priv->ndev);
        rtnl_unlock();
    }
    return 0;
}

static int my_devlink_reload_up(struct devlink *devlink,
    enum devlink_reload_action action, enum devlink_reload_limit limit,
    u32 *actions_performed, struct netlink_ext_ack *extack)
{
    struct my_priv *priv = devlink_priv(devlink);
    *actions_performed = BIT(action);
    my_hw_reinit(priv);
    rtnl_lock(); my_ndo_open(priv->ndev); rtnl_unlock();
    return 0;
}

PCIe AER 복구 통합

PCIe AER(Advanced Error Reporting)은 하드웨어 오류를 감지하고 복구하는 PCIe 표준 메커니즘입니다. NIC 드라이버는 pci_error_handlers를 등록하여 커널의 AER 복구 흐름에 참여합니다. 복구는 error_detectedslot_resetresume 세 단계로 진행됩니다.

/* 완전한 pci_error_handlers 구현 */
static pci_ers_result_t my_pci_error_detected(struct pci_dev *pdev,
                                                pci_channel_state_t state)
{
    struct my_priv *priv = pci_get_drvdata(pdev);
    netdev_info(priv->ndev, "PCI error detected, state=%d\n", state);
    if (state == pci_channel_io_perm_failure)
        return PCI_ERS_RESULT_DISCONNECT;
    rtnl_lock();
    netif_device_detach(priv->ndev);
    if (netif_running(priv->ndev)) my_ndo_stop(priv->ndev);
    rtnl_unlock();
    set_bit(MY_FLAG_PCI_ERR, &priv->flags);
    pci_disable_device(pdev);
    return PCI_ERS_RESULT_NEED_RESET;
}

static pci_ers_result_t my_pci_slot_reset(struct pci_dev *pdev)
{
    struct my_priv *priv = pci_get_drvdata(pdev);
    int err;
    err = pci_enable_device(pdev);
    if (err) return PCI_ERS_RESULT_DISCONNECT;
    pci_set_master(pdev);
    pci_restore_state(pdev);
    pci_save_state(pdev);
    err = my_hw_reinit(priv);
    if (err) return PCI_ERS_RESULT_DISCONNECT;
    return PCI_ERS_RESULT_RECOVERED;
}

static void my_pci_resume(struct pci_dev *pdev)
{
    struct my_priv *priv = pci_get_drvdata(pdev);
    clear_bit(MY_FLAG_PCI_ERR, &priv->flags);
    rtnl_lock();
    my_ndo_open(priv->ndev);
    netif_device_attach(priv->ndev);
    rtnl_unlock();
    devlink_health_reporter_state_update(priv->health_reporter,
        DEVLINK_HEALTH_REPORTER_STATE_HEALTHY);
}

static const struct pci_error_handlers my_pci_err_handlers = {
    .error_detected = my_pci_error_detected,
    .slot_reset     = my_pci_slot_reset,
    .resume         = my_pci_resume,
};
중요: error_detected 콜백에서는 절대로 PCIe MMIO에 접근하면 안 됩니다. 이 시점에서 PCI 버스가 이미 오류 상태이므로 레지스터(Register) 읽기/쓰기가 hang을 유발합니다. set_bit(MY_FLAG_PCI_ERR, ...)로 플래그를 설정하고, 데이터 경로의 모든 MMIO 접근에서 이 플래그를 확인해야 합니다.

펌웨어 리셋과 라이브 패치(Livepatch)

devlink dev flash 명령은 NIC 펌웨어를 라이브 업데이트합니다. 드라이버는 devlink_flash_update_params를 통해 업데이트 요청을 받고, 펌웨어를 하드웨어에 기록한 후 활성화(Activation) 방식을 결정합니다.

/* 펌웨어 업데이트와 알림 구현 */
static int my_devlink_flash_update(struct devlink *devlink,
    struct devlink_flash_update_params *params,
    struct netlink_ext_ack *extack)
{
    struct my_priv *priv = devlink_priv(devlink);
    const struct firmware *fw = params->fw;
    u32 offset = 0;
    int err;

    err = my_fw_validate_header(priv, fw->data, fw->size);
    if (err) {
        NL_SET_ERR_MSG_MOD(extack, "invalid firmware header");
        return err;
    }

    if (!(params->overwrite_mask & DEVLINK_FLASH_OVERWRITE_SETTINGS)) {
        if (my_fw_is_downgrade(priv, fw)) {
            NL_SET_ERR_MSG_MOD(extack, "firmware downgrade not allowed");
            return -EPERM;
        }
    }

    devlink_flash_update_status_notify(devlink, "Preparing", NULL, 0, 0);

    while (offset < fw->size) {
        u32 chunk = min_t(u32, fw->size - offset, MY_FW_CHUNK_SIZE);
        err = my_hw_write_fw_chunk(priv, fw->data + offset, chunk, offset);
        if (err) return err;
        offset += chunk;
        devlink_flash_update_status_notify(devlink,
            "Flashing", NULL, offset, fw->size);
    }

    if (priv->caps & MY_CAP_FW_LIVE_PATCH) {
        err = my_hw_fw_activate(priv, MY_FW_ACTIVATE_IMMEDIATE);
        devlink_flash_update_status_notify(devlink,
            "Activated (live)", NULL, 0, 0);
    } else {
        err = my_hw_fw_activate(priv, MY_FW_ACTIVATE_PENDING);
        devlink_flash_update_status_notify(devlink,
            "Pending reset", NULL, 0, 0);
    }

    devlink_flash_update_timeout_notify(devlink,
        "Flash complete", NULL, 120);
    return err;
}

static const struct devlink_ops my_devlink_ops = {
    .reload_actions = BIT(DEVLINK_RELOAD_ACTION_DRIVER_REINIT) |
                      BIT(DEVLINK_RELOAD_ACTION_FW_ACTIVATE),
    .reload_down    = my_devlink_reload_down,
    .reload_up      = my_devlink_reload_up,
    .flash_update   = my_devlink_flash_update,
};
펌웨어 활성화 방식 비교:
  • 즉시 활성화(Immediate): 서비스 중단 없이 새 펌웨어를 적용합니다. NIC가 라이브 패치(Live Patch)를 지원해야 하며, mlx5 ConnectX-6 이상에서 지원합니다.
  • 지연 활성화(Pending): 새 펌웨어는 다음 devlink dev reload 또는 시스템 재부팅 시 적용됩니다. 대부분의 NIC가 이 방식을 사용합니다.
  • DEVLINK_FLASH_OVERWRITE_SETTINGS: 이 플래그가 설정되면 NIC 설정(MAC 주소, boot 옵션 등)도 함께 덮어씁니다. 다운그레이드 보호를 우회할 수 있으므로 주의가 필요합니다.

회귀 테스트: packetdrill, kselftest, fault injection

netdev 드라이버는 환경 의존성이 커서 재현 테스트가 어렵습니다. 릴리스 전에 최소 회귀 시나리오를 자동화하면 “간헐적 링크 다운” 같은 문제를 조기에 차단할 수 있습니다.

도구테스트 대상예시
kselftest (net)기능 회귀MTU/VLAN/GRO/GSO 기본 동작
packetdrill프로토콜 타이밍/에러 경로재전송(Retransmission), out-of-order, checksum 에러
tc + iperf3성능/큐 안정성장시간 부하 중 drop/timeout 감시
fault injection복구 경로DMA map 실패, TX timeout, reset 반복

netdevsim을 활용한 드라이버 테스트

netdevsim은 실제 하드웨어 없이 네트워크 드라이버 기능을 테스트할 수 있는 가상 장치(Virtual Device)입니다. TC 오프로드, devlink health, XDP, flow offload 등 다양한 드라이버 인터페이스를 시뮬레이션하므로 CI/CD 파이프라인(Pipeline)에 통합하기 적합합니다.

netdevsim이 지원하는 주요 테스트 영역은 다음과 같습니다.

# netdevsim 테스트 스크립트
#!/bin/bash

set -e

# 1. netdevsim 장치 생성
modprobe netdevsim
echo "1 1" > /sys/bus/netdevsim/new_device
DEV_NAME=$(ls /sys/bus/netdevsim/devices/netdevsim1/net/)
echo "netdevsim 인터페이스: $DEV_NAME"

# 인터페이스 활성화
ip link set dev $DEV_NAME up

# 2. TC flower 오프로드 테스트
echo "--- TC flower 오프로드 테스트 ---"
tc qdisc add dev $DEV_NAME ingress
tc filter add dev $DEV_NAME ingress protocol ip \
    flower src_ip 192.168.1.0/24 dst_ip 10.0.0.0/8 \
    action drop skip_sw

# 오프로드된 규칙 확인
tc -s filter show dev $DEV_NAME ingress
echo "TC 오프로드 규칙 수:"
tc -s filter show dev $DEV_NAME ingress | grep -c "in_hw"

# 3. devlink health reporter 테스트
echo "--- devlink health 테스트 ---"
DEVLINK_DEV=$(devlink dev show | head -1 | awk '{print $1}')
devlink health show $DEVLINK_DEV

# 4. XDP 프로그램 로드 테스트
echo "--- XDP 로드 테스트 ---"
# 간단한 XDP_PASS 프로그램 (사전 컴파일 필요)
if [ -f /tmp/xdp_pass.o ]; then
    ip link set dev $DEV_NAME xdp obj /tmp/xdp_pass.o sec xdp
    ip link show dev $DEV_NAME | grep xdp
    ip link set dev $DEV_NAME xdp off
fi

# 5. devlink trap 테스트
echo "--- devlink trap 테스트 ---"
devlink trap show $DEVLINK_DEV 2>/dev/null | head -10

# 6. 정리
echo "--- 정리 ---"
tc qdisc del dev $DEV_NAME ingress 2>/dev/null
echo 1 > /sys/bus/netdevsim/del_device
echo "netdevsim 테스트 완료"

kselftest 네트워크 테스트 실행

커널 소스 트리의 tools/testing/selftests/net/ 디렉터리에는 네트워크 기능을 검증하는 자동화 테스트가 포함되어 있습니다. 드라이버 개발자는 변경 사항이 기존 기능을 깨뜨리지 않는지 kselftest를 통해 검증해야 합니다.

주요 테스트 카테고리는 다음과 같습니다.

디렉터리/파일테스트 대상네트워크 구성 필요
net/forwarding/L2/L3 포워딩, VLAN, 브릿지veth pair + 네임스페이스(Namespace)
net/bonding/본딩(Bonding) 모드, failover더미 인터페이스
net/tc-testing/TC qdisc, filter, action가상 인터페이스
net/mptcp/Multipath TCP네임스페이스
net/fib_tests.sh라우팅(Routing) 테이블(Routing Table) (FIB)네임스페이스
net/gro.shGRO(Generic Receive Offload)veth pair
# kselftest 네트워크 테스트 실행 가이드

# 1. 커널 소스에서 전체 네트워크 셀프테스트 빌드 및 실행
cd /path/to/linux
make -C tools/testing/selftests TARGETS=net run_tests

# 2. 특정 테스트만 실행
# 포워딩 테스트
make -C tools/testing/selftests TARGETS=net/forwarding run_tests

# 3. 개별 테스트 스크립트 직접 실행
cd tools/testing/selftests/net
./fib_tests.sh
./gro.sh

# 4. 커스텀 kselftest 예시: 드라이버 기능 검증
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# 드라이버 MTU 변경 테스트

source lib.sh

# 네임스페이스 생성
setup_ns NS1 NS2
trap cleanup_ns EXIT

# veth 쌍 생성
ip link add veth0 netns $NS1 type veth peer name veth1 netns $NS2
ip -n $NS1 link set veth0 up
ip -n $NS2 link set veth1 up
ip -n $NS1 addr add 10.0.0.1/24 dev veth0
ip -n $NS2 addr add 10.0.0.2/24 dev veth1

# MTU 변경 테스트
for mtu in 576 1500 9000; do
    ip -n $NS1 link set veth0 mtu $mtu
    ip -n $NS2 link set veth1 mtu $mtu

    # ping으로 연결 확인 (MTU - IP/ICMP 헤더)
    payload_size=$((mtu - 28))
    if ip netns exec $NS1 ping -c 3 -s $payload_size -M do 10.0.0.2 >/dev/null 2>&1; then
        echo "PASS: MTU $mtu - ping 성공"
    else
        echo "FAIL: MTU $mtu - ping 실패"
        exit 1
    fi
done

echo "모든 MTU 테스트 통과"
exit 0

Fault Injection을 통한 복구 경로 검증

정상 경로(Happy Path)만 테스트해서는 드라이버의 안정성을 보장할 수 없습니다. DMA 매핑(Mapping) 실패, 메모리 할당 실패, 함수 에러 반환 등 오류 경로를 의도적으로 유발하여 드라이버의 복구 로직이 올바르게 동작하는지 검증해야 합니다.

커널은 다양한 오류 주입(Fault Injection) 프레임워크를 제공합니다.

# Fault Injection 테스트 스크립트
#!/bin/bash

set -e

DEV="eth0"
FAULT_DIR="/sys/kernel/debug/fail_function"

echo "=== Fault Injection 복구 경로 테스트 ==="

# 필수 커널 설정 확인
# CONFIG_FAULT_INJECTION=y
# CONFIG_FAIL_FUNCTION=y
# CONFIG_FAULT_INJECTION_DEBUG_FS=y

if [ ! -d "$FAULT_DIR" ]; then
    echo "ERROR: fail_function not available"
    echo "커널 CONFIG_FAIL_FUNCTION=y 필요"
    exit 1
fi

# 테스트 1: DMA 매핑 실패 시 복구
echo "--- 테스트 1: dma_map_single 실패 ---"
echo dma_map_single > $FAULT_DIR/inject

# 확률 설정: 10% 확률로 실패
echo 10 > /sys/kernel/debug/fail_function/probability
echo 1 > /sys/kernel/debug/fail_function/times
echo 0 > /sys/kernel/debug/fail_function/space
echo 1 > /sys/kernel/debug/fail_function/verbose

# 부하 생성하여 오류 경로 유발
ping -c 100 -f 10.0.0.2 2>/dev/null || true

# 드라이버 상태 확인 (크래시/행 여부)
if ip link show $DEV >/dev/null 2>&1; then
    echo "PASS: 인터페이스 정상 유지"
else
    echo "FAIL: 인터페이스 비정상"
fi

# 오류 주입 해제
echo > $FAULT_DIR/inject

# 테스트 2: 메모리 할당 실패 (fail_page_alloc)
echo "--- 테스트 2: 페이지 할당 실패 ---"
echo 1 > /proc/sys/vm/fault_injection/fail_page_alloc/probability
echo 5 > /proc/sys/vm/fault_injection/fail_page_alloc/times

# RX 링 리필 경로 유발 (대량 패킷 수신)
ip netns exec ns_remote iperf3 -c 10.0.0.1 -t 5 -b 1G 2>/dev/null || true

# 오류 주입 해제
echo 0 > /proc/sys/vm/fault_injection/fail_page_alloc/probability

# 통계 확인
echo "--- 드라이버 에러 카운터 ---"
ethtool -S $DEV | grep -iE "alloc.*fail|dma.*err|drop"

# dmesg에서 드라이버 경고/에러 확인
echo "--- 관련 dmesg ---"
dmesg | tail -20 | grep -iE "$DEV|dma|alloc|fail"

echo "Fault injection 테스트 완료"

네트워크 네임스페이스 기반 격리 테스트

네트워크 네임스페이스(Network Namespace)를 사용하면 호스트 네트워크에 영향을 주지 않고 드라이버 기능을 안전하게 테스트할 수 있습니다. veth 쌍으로 격리(Isolation)된 환경을 만들고, iperf3/netperf로 부하를 생성하며, tcpdump/tshark로 패킷(Packet)을 검증하는 테스트 하네스(Test Harness)를 구축합니다.

# 네임스페이스 기반 종합 테스트 하네스
#!/bin/bash

set -e

# 네임스페이스 이름
NS1="ns_sender"
NS2="ns_receiver"
RESULTS="/tmp/netns-test-$(date +%s)"
mkdir -p $RESULTS

cleanup() {
    ip netns del $NS1 2>/dev/null || true
    ip netns del $NS2 2>/dev/null || true
    echo "정리 완료"
}
trap cleanup EXIT

# 1. 네임스페이스 및 veth 쌍 생성
echo "=== 네임스페이스 환경 구성 ==="
ip netns add $NS1
ip netns add $NS2

ip link add veth-s type veth peer name veth-r
ip link set veth-s netns $NS1
ip link set veth-r netns $NS2

# IP 설정
ip netns exec $NS1 ip addr add 10.0.0.1/24 dev veth-s
ip netns exec $NS2 ip addr add 10.0.0.2/24 dev veth-r
ip netns exec $NS1 ip link set veth-s up
ip netns exec $NS2 ip link set veth-r up
ip netns exec $NS1 ip link set lo up
ip netns exec $NS2 ip link set lo up

# 2. 기본 연결 테스트
echo "--- 기본 연결 ---"
ip netns exec $NS1 ping -c 3 10.0.0.2 | tail -1

# 3. MTU 변경 테스트
echo "--- MTU 테스트 ---"
for mtu in 68 576 1500 9000; do
    ip netns exec $NS1 ip link set veth-s mtu $mtu
    ip netns exec $NS2 ip link set veth-r mtu $mtu
    if ip netns exec $NS1 ping -c 1 -s $((mtu - 28)) -M do 10.0.0.2 >/dev/null 2>&1; then
        echo "  MTU $mtu: PASS"
    else
        echo "  MTU $mtu: FAIL"
    fi
done

# MTU 복원
ip netns exec $NS1 ip link set veth-s mtu 1500
ip netns exec $NS2 ip link set veth-r mtu 1500

# 4. 처리량 테스트 (iperf3)
echo "--- 처리량 테스트 ---"
ip netns exec $NS2 iperf3 -s -D -p 5201
sleep 1
ip netns exec $NS1 iperf3 -c 10.0.0.2 -t 10 -p 5201 \
    --json > $RESULTS/iperf3.json
# 결과 추출
python3 -c "
import json, sys
data = json.load(open('$RESULTS/iperf3.json'))
bps = data['end']['sum_sent']['bits_per_second']
print(f'  처리량: {bps/1e9:.2f} Gbps')
"

# 5. 패킷 캡처 검증
echo "--- 패킷 캡처 테스트 ---"
ip netns exec $NS2 tcpdump -i veth-r -c 10 -w $RESULTS/capture.pcap &
TCPDUMP_PID=$!
sleep 1
ip netns exec $NS1 ping -c 5 10.0.0.2 >/dev/null
sleep 2
kill $TCPDUMP_PID 2>/dev/null || true
echo "  캡처 파일: $RESULTS/capture.pcap"
tcpdump -r $RESULTS/capture.pcap -q 2>/dev/null | wc -l | \
    xargs -I{} echo "  캡처된 패킷: {} 개"

# 6. GRO/GSO 테스트
echo "--- GRO/GSO 오프로드 테스트 ---"
for feature in gro gso tso; do
    ip netns exec $NS1 ethtool -K veth-s $feature off 2>/dev/null
    ip netns exec $NS1 ping -c 1 10.0.0.2 >/dev/null 2>&1 && \
        echo "  $feature off: PASS" || echo "  $feature off: FAIL"
    ip netns exec $NS1 ethtool -K veth-s $feature on 2>/dev/null
done

# iperf3 서버 정리
pkill -f "iperf3 -s" 2>/dev/null || true

echo "=== 테스트 완료. 결과: $RESULTS ==="

Netpoll/kdump 경로 지원

패닉 상황에서 네트워크 로그 덤프가 필요하면 netpoll/netconsole 경로가 사용됩니다. 일반 데이터 경로와 독립된 최소 송신 경로를 유지해야 crash dump 신뢰성이 올라갑니다.

/* 개념 예시: netpoll 경로의 재진입/잠금 제약 */
static void my_netpoll_send_skb(struct net_device *ndev, struct sk_buff *skb)
{
    struct my_priv *priv = netdev_priv(ndev);

    /* 최소 TX 경로: sleep 금지, 동적 메모리 할당 최소화 */
    if (!my_tx_ring_has_space(priv)) {
        dev_kfree_skb_any(skb);
        return;
    }
    my_map_skb_to_tx_desc(priv, skb);
    my_ring_doorbell(priv);
}
운영 주의: kdump 캡처 커널에서 동일 NIC 드라이버가 정상 동작하는지 별도 검증이 필요합니다. 본 커널에서 정상이어도 캡처 커널 initramfs/펌웨어 누락으로 전송 실패가 발생할 수 있습니다.

netpoll 내부 아키텍처

netpoll은 커널 패닉이나 인터럽트(Interrupt)가 비활성화된 극한 상황에서도 네트워크 패킷을 송신할 수 있는 최소한의 네트워크 경로입니다. struct netpoll은 일반 네트워크 스택(Network Stack)을 우회하여 드라이버의 ndo_start_xmit을 직접 호출하는 폴링(Polling) 기반(Polling-based) 전송 메커니즘을 제공합니다.

netpoll 전송 아키텍처 커널 패닉 / oops netconsole / netdump netpoll_send_udp() netpoll_send_skb() trylock(txq->_xmit_lock) 실패 시 poll + 재시도 ndo_start_xmit() 직접 호출 (일반 스택 우회) HW TX ring → doorbell 네트워크로 패킷 전송 인터럽트 컨텍스트 제약 sleep 금지 / GFP_ATOMIC만 허용 / trylock만 사용 napi_poll() 직접 호출로 TX completion 처리

netpoll_send_skb_on_dev()의 내부 동작을 단계별로 분석하면 다음과 같습니다.

/* netpoll 드라이버 통합 예시
 * 드라이버가 netpoll을 지원하려면 몇 가지 조건을 충족해야 합니다 */

/* 1. ndo_poll_controller 구현 (NAPI 기반) */
static void my_poll_controller(struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);
    int i;

    /* 모든 큐의 인터럽트를 비활성화하고 poll 수행 */
    for (i = 0; i < priv->num_queues; i++) {
        disable_irq(priv->queues[i].irq);
        napi_schedule(&priv->queues[i].napi);
        enable_irq(priv->queues[i].irq);
    }
}

/* 2. xmit 경로에서 netpoll 안전성 확보 */
static netdev_tx_t my_start_xmit(struct sk_buff *skb,
                                  struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);
    struct my_tx_ring *tx;
    int qidx;

    /* netpoll 경로에서는 큐 선택이 제한적 */
    qidx = skb_get_queue_mapping(skb);
    if (qidx >= priv->num_tx_queues)
        qidx = 0;

    tx = &priv->tx_ring[qidx];

    /* sleep 가능한 함수 호출 금지: mutex, kmalloc(GFP_KERNEL) 등
     * netpoll 경로는 인터럽트 컨텍스트에서 실행될 수 있음 */
    if (unlikely(!my_tx_has_space(tx))) {
        /* netpoll에서는 queue stop 대신 바로 drop */
        if (unlikely(skb->dev->priv_flags & IFF_IN_NETPOLL)) {
            dev_kfree_skb_any(skb);
            return NETDEV_TX_OK;
        }
        netif_stop_subqueue(ndev, qidx);
        return NETDEV_TX_BUSY;
    }

    my_fill_and_submit(tx, skb);
    return NETDEV_TX_OK;
}

/* 3. net_device_ops에 등록 */
static const struct net_device_ops my_netdev_ops = {
    .ndo_open           = my_open,
    .ndo_stop           = my_stop,
    .ndo_start_xmit     = my_start_xmit,
#ifdef CONFIG_NET_POLL_CONTROLLER
    .ndo_poll_controller = my_poll_controller,
#endif
};

netconsole 설정과 운영

netconsole은 netpoll 위에 구축된 커널 로그 전송 도구입니다. 시리얼 콘솔이 없는 환경에서 커널 메시지를 원격 서버로 전송할 수 있으며, 확장 netconsole(Extended Netconsole)은 사용자 정의 메타데이터까지 포함할 수 있습니다.

# netconsole 설정 스크립트
#!/bin/bash

# 기본 netconsole 설정
# 형식: netconsole=[+][src-port]@[src-ip]/[dev],[tgt-port]@/[tgt-macaddr]

# 모듈로 로드 (동적 설정 가능)
modprobe netconsole \
    netconsole=@10.0.0.1/eth0,6666@10.0.0.2/aa:bb:cc:dd:ee:ff

# 또는 configfs를 통한 동적 설정 (확장 netconsole)
mkdir -p /sys/kernel/config/netconsole/target1
cd /sys/kernel/config/netconsole/target1

echo 10.0.0.1 > local_ip
echo 6665 > local_port
echo eth0 > dev_name
echo 10.0.0.2 > remote_ip
echo 6666 > remote_port
echo aa:bb:cc:dd:ee:ff > remote_mac

# 확장 netconsole: 사용자 정의 데이터 추가 (v5.18+)
echo 1 > extended
mkdir userdata/hostname
echo "$(hostname)" > userdata/hostname/value
mkdir userdata/kernel_version
echo "$(uname -r)" > userdata/kernel_version/value

# 활성화
echo 1 > enabled

# 상태 확인
echo "=== netconsole 대상 목록 ==="
ls /sys/kernel/config/netconsole/

# 수신 측 (원격 서버)에서 netconsole 메시지 수신
# 간단한 수신기: nc -u -l 6666
# 또는 syslog 데몬으로 UDP 수신 설정

# 테스트: 커널 로그 강제 출력
echo "netconsole test $(date)" > /dev/kmsg

kdump에서의 네트워크 드라이버 제약

kdump는 커널 패닉 시 캡처 커널(Capture Kernel)을 부팅하여 크래시 덤프를 저장합니다. 캡처 커널 환경은 일반 부팅과 크게 다르며, 네트워크 드라이버는 여러 제약 조건 하에서 동작해야 합니다.

제약 사항영향드라이버 대응 전략확인 방법
펌웨어 비정상 상태probe 실패, 행(hang)FW 강제 리셋 (FLR/PCIe reset)캡처 커널에서 수동 테스트
메모리 부족ring alloc 실패링 크기 축소 매개변수 지원crashkernel= 크기 조정
단일 CPU멀티큐 초기화 실패큐 수 자동 감소 로직num_online_cpus() 확인
MSI-X 벡터 제한인터럽트 할당 실패INTx 폴백(Fallback) 지원/proc/interrupts 확인
IOMMU 비활성DMA 주소 매핑 차이swiotlb 대응dmesg | grep SWIOTLB
initramfs 불일치펌웨어 파일 누락dracut에 FW 파일 포함lsinitrd로 확인
# kdump 환경에서 네트워크 드라이버 호환성 테스트
#!/bin/bash

echo "=== kdump 네트워크 드라이버 호환성 점검 ==="

# 1. kdump 서비스 상태 확인
systemctl status kdump

# 2. crashkernel 예약 메모리 확인
echo "--- crashkernel 설정 ---"
cat /proc/cmdline | grep -o 'crashkernel=[^ ]*'
echo "예약된 메모리:"
cat /proc/iomem | grep "Crash kernel"

# 3. 캡처 커널 initramfs에 NIC 펌웨어 포함 여부 확인
NIC_DRIVER=$(ethtool -i eth0 | grep driver | awk '{print $2}')
echo "--- NIC 드라이버: $NIC_DRIVER ---"

# 펌웨어 파일 경로 확인
modinfo $NIC_DRIVER | grep firmware
echo "--- initramfs 내 펌웨어 파일 ---"
KDUMP_INITRD=$(grep -r initrd /etc/kdump.conf 2>/dev/null | awk '{print $2}')
if [ -n "$KDUMP_INITRD" ]; then
    lsinitrd $KDUMP_INITRD | grep -i firmware | grep -i $NIC_DRIVER
else
    echo "기본 initramfs 사용 중"
    lsinitrd /boot/initramfs-$(uname -r)kdump.img | grep -i firmware 2>/dev/null
fi

# 4. 드라이버의 netpoll 지원 여부
echo "--- netpoll 지원 여부 ---"
grep -c poll_controller /sys/class/net/eth0/device/driver/module/holders/ 2>/dev/null
# 또는 커널 소스에서 ndo_poll_controller 구현 확인

# 5. NIC reset 테스트 (주의: 일시적 연결 끊김)
echo "--- FLR(Function Level Reset) 지원 ---"
PCI_ADDR=$(ethtool -i eth0 | grep bus-info | awk '{print $2}')
lspci -vvs $PCI_ADDR | grep -i "FLR"

디버깅 체크리스트와 실전 트러블슈팅

증상의심 지점확인 방법
TX 멈춤queue wake 누락, completion path 손상ethtool -S, netif_tx_queue_stopped() 추적
RX drop 급증NAPI budget 과소, ring refill 지연/proc/net/softnet_stat, RX no-buffer 카운터
링크 flapPHY state machine/interrupt stormdmesg, phylink tracepoint
고부하에서 패킷 손실IRQ affinity/NUMA 불일치/proc/interrupts, ethtool -x/-X
XDP 적용 후 비정상page_pool recycle/XDP verdict 처리 오류bpftool prog, drop reason trace
# 실습 예제: 큐/오프로드/통계 빠른 점검
# 큐/오프로드/통계 빠른 점검
ethtool -i eth0
ethtool -k eth0
ethtool -l eth0
ethtool -S eth0 | grep -E "drop|error|timeout|busy"

# 소프트넷 병목 확인
cat /proc/net/softnet_stat

# netdev 관련 tracepoint 예시
trace-cmd record -e net -e napi -e skb

TX Hang 심층 진단 플로우차트

TX 타임아웃(TX Timeout)은 네트워크 드라이버에서 가장 치명적인 장애 유형 중 하나입니다. TX 완료 인터럽트(Completion Interrupt)가 오지 않거나, 도어벨(Doorbell) 쓰기가 실패하거나, BQL(Byte Queue Limits)이 잘못 설정된 경우 등 다양한 원인이 있습니다. 체계적인 진단 플로우차트를 따라 원인을 빠르게 격리해야 합니다.

TX Timeout 발생 ethtool -S: tx_timeout 카운터 확인 TX ring에 미완료 descriptor 존재? No BQL 설정 확인 Yes Completion IRQ 수신 중? Yes Queue wake 누락 No Doorbell write 정상? No MMIO 접근 실패 Yes FW 응답 정상 (health reporter)? No 펌웨어 행/크래시 Yes DMA mapping / IOMMU 확인 devlink health dump 수집 후 벤더 리포트

각 진단 단계에서 확인해야 할 핵심 사항은 다음과 같습니다.

# bpftrace를 사용한 TX timeout 경로 추적

# ndo_tx_timeout 호출 추적 - 어떤 큐에서 발생하는지 확인
bpftrace -e 'kprobe:dev_watchdog {
    printf("TX watchdog fired on CPU %d\n", cpu);
}'

# TX completion latency 히스토그램 (마이크로초 단위)
bpftrace -e 'kprobe:napi_complete_done {
    @start[tid] = nsecs;
}
kretprobe:napi_complete_done /@start[tid]/ {
    @latency_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}'

# BQL inflight 모니터링
for q in /sys/class/net/eth0/queues/tx-*/byte_queue_limits; do
    echo "$(basename $(dirname $q)): limit=$(cat $q/limit) inflight=$(cat $q/inflight)"
done

RX Drop 원인 분석

RX 경로에서의 패킷 손실은 여러 계층에서 발생할 수 있습니다. 하드웨어 링 버퍼 오버플로(Buffer Overflow)우, NAPI 예산(Budget) 소진, softirq 처리 시간 초과, 소켓(Socket) 버퍼(Buffer) 가득 참 등 원인별로 관측 포인트가 다릅니다.

/proc/net/softnet_stat의 각 컬럼은 CPU별 softnet 처리 통계를 나타냅니다.

컬럼필드명의미급증 시 원인
1번째processed처리된 총 프레임 수정상 지표 (증가는 문제 아님)
2번째droppednetif_rx backlog 초과로 드롭input_pkt_queue 크기 부족, CPU 과부하
3번째time_squeezesoftirq 시간 제한(2ms)으로 처리 중단패킷 폭주 또는 NAPI poll 비효율
9번째cpu_collisionTX 경로에서 CPU 경합(Contention) 발생여러 CPU가 동일 TX 큐에 접근
10번째received_rpsRPS로 전달된 프레임정상 지표 (RPS 활성 시)
11번째flow_limit_countflow limit으로 드롭된 프레임단일 플로우 과점유
12번째softnet_backlog_len현재 backlog 대기 길이높으면 처리 지연 중

time_squeezebudget 소진의 차이를 이해하는 것이 중요합니다. time_squeeze는 2ms의 softirq 시간 제한에 의해 처리가 중단된 횟수이며, 이는 NAPI poll 루프에서 budget(기본 300)을 다 쓰기 전에 시간이 먼저 초과된 경우입니다. budget 소진은 300개 패킷을 모두 처리했지만 아직 더 처리할 패킷이 남은 상태를 의미합니다.

# 종합 RX drop 분석 스크립트
#!/bin/bash

DEV="eth0"
echo "=== RX Drop 분석: $DEV ==="

# 1. 하드웨어 레벨 drop 확인
echo "\n--- 하드웨어 카운터 ---"
ethtool -S $DEV | grep -iE "rx.*drop|rx.*miss|rx.*error|rx.*discard|no.buffer|no_buffer"

# 2. 커널 레벨 drop 확인
echo "\n--- 커널 네트워크 카운터 ---"
cat /proc/net/dev | grep $DEV | awk '{printf "RX packets:%s errors:%s drop:%s fifo:%s\n", $2,$4,$5,$6}'

# 3. softnet_stat 분석 (CPU별)
echo "\n--- softnet_stat (CPU별) ---"
echo "CPU   processed   dropped   time_squeeze"
cpu=0
while IFS= read -r line; do
    processed=$((16#$(echo $line | awk '{print $1}')))
    dropped=$((16#$(echo $line | awk '{print $2}')))
    squeeze=$((16#$(echo $line | awk '{print $3}')))
    printf "CPU%-3d %10d %8d %13d\n" $cpu $processed $dropped $squeeze
    cpu=$((cpu+1))
done < /proc/net/softnet_stat

# 4. 링 버퍼 사용률 확인
echo "\n--- 링 버퍼 설정 ---"
ethtool -g $DEV

# 5. SKB drop reason 추적 (perf 기반)
echo "\n--- SKB Drop Reason 추적 (5초) ---"
perf record -e skb:kfree_skb -a -- sleep 5
perf script | awk '{print $NF}' | sort | uniq -c | sort -rn | head -20

# 6. drop_monitor 사용
echo "\n--- drop_monitor (5초 샘플) ---"
dropwatch -l kas <<CMDS
start
sleep 5
stop
exit
CMDS

링크 Flap 디버깅

링크 플랩(Link Flap)은 네트워크 인터페이스가 반복적으로 up/down을 오가는 현상입니다. 물리 계층(PHY), 광모듈(SFP/QSFP), 자동 협상(Autonegotiation), 케이블 문제 등 다양한 원인이 있으며, phylink/PHY 상태 머신(State Machine)의 tracepoint를 활용하면 원인을 정밀하게 추적할 수 있습니다.

주요 원인 패턴과 진단 방법은 다음과 같습니다.

#!/bin/bash
# 링크 플랩 모니터링 및 진단 스크립트

DEV="eth0"
LOG="/tmp/link-flap-$DEV.log"
INTERVAL=1
FLAP_COUNT=0
PREV_STATE=""

echo "=== 링크 플랩 모니터 시작: $DEV ==="
echo "로그: $LOG"

# phylink tracepoint 활성화
echo 1 > /sys/kernel/debug/tracing/events/phylink/enable 2>/dev/null

# SFP 모듈 정보 수집
echo "--- SFP 모듈 정보 ---" | tee $LOG
ethtool -m $DEV 2>/dev/null | tee -a $LOG

# 현재 PHY 상태 확인
echo "--- PHY 상태 ---" | tee -a $LOG
ethtool $DEV | grep -E "Speed|Duplex|Link|Auto" | tee -a $LOG

# 링크 상태 폴링 루프
while true; do
    STATE=$(cat /sys/class/net/$DEV/operstate)
    TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S.%N")

    if [ "$STATE" != "$PREV_STATE" ] && [ -n "$PREV_STATE" ]; then
        FLAP_COUNT=$((FLAP_COUNT+1))
        echo "[$TIMESTAMP] FLAP #$FLAP_COUNT: $PREV_STATE -> $STATE" | tee -a $LOG

        # 플랩 시점에 추가 정보 수집
        echo "  ethtool:" | tee -a $LOG
        ethtool $DEV 2>/dev/null | grep -E "Speed|Duplex|Link" | tee -a $LOG

        # dmesg에서 관련 메시지 추출
        dmesg --time-format iso | tail -5 | grep -iE "$DEV|link|phy|sfp" | tee -a $LOG
    fi

    PREV_STATE=$STATE
    sleep $INTERVAL
done

dmesg에서 자주 보이는 링크 관련 패턴과 의미는 다음과 같습니다.

dmesg 패턴의미조치
Link is Down / Link is Up 반복물리 계층 불안정케이블/SFP 교체, 속도 고정 시도
PHY: autonegotiation failed자동 협상 실패양쪽 속도/듀플렉스 고정
sfp: module ... not supported미지원 SFP 모듈호환 모듈로 교체
PCIe: AER correctable errorPCIe 링크 오류슬롯 재장착, BIOS 설정 확인
firmware timeoutNIC 펌웨어 응답 없음펌웨어 업데이트, devlink health

성능 병목 분석 방법론

네트워크 드라이버의 성능 병목(Bottleneck)은 CPU 핫스팟(Hotspot), 잠금 경합(Lock Contention), 캐시(Cache) 미스(Cache Miss), DMA 매핑 오버헤드(Overhead) 등 다양한 원인에서 발생합니다. 체계적인 분석 파이프라인을 구축하면 병목 지점을 정량적으로 식별할 수 있습니다.

# 종합 네트워크 성능 분석 파이프라인

# 1. perf top으로 실시간 CPU 핫스팟 확인
perf top -g --no-children -p $(pgrep -d, ksoftirqd)

# 2. ftrace function_graph로 NAPI poll 레이턴시 추적
echo napi_poll > /sys/kernel/debug/tracing/set_graph_function
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 3
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace | head -100

# 3. bpftrace: NAPI poll당 처리 패킷 수 히스토그램
bpftrace -e 'kretprobe:napi_poll {
    @pkts = hist(retval);
}'

# 4. bpftrace: 패킷당 처리 지연 (나노초)
bpftrace -e 'kprobe:netif_receive_skb {
    @start[arg0] = nsecs;
}
kprobe:__netif_receive_skb_core /@start[arg0]/ {
    @latency_ns = hist(nsecs - @start[arg0]);
    delete(@start[arg0]);
}'

# 5. 플레임 그래프 생성 파이프라인
# perf로 10초간 콜 스택 수집
perf record -a -g -F 99 -- sleep 10

# 플레임 그래프 변환 (Brendan Gregg의 FlameGraph 도구)
perf script | stackcollapse-perf.pl | flamegraph.pl \
    --title "Network Stack CPU Flame Graph" \
    --subtitle "$(hostname) - $(date)" \
    --width 1200 > net-flame.svg

# 6. 락 경합 분석
perf lock record -a -- sleep 5
perf lock report --sort contended,avg_wait

운영 관측: 필수 대시보드 지표

드라이버 품질은 장애가 났을 때 “원인을 빠르게 찾을 수 있는가”로 평가됩니다. 아래 지표를 대시보드로 상시 수집하면 회귀를 조기에 감지할 수 있습니다.

지표 그룹필수 항목경보 조건 예시
링크 상태link up/down flap 횟수, 속도/duplex 변경10분 내 flap 3회 이상
RX/TX 에러crc/frame/rx_missed/tx_timeout분당 에러 증가율 급등
큐 불균형queue별 packet/byte 편차, backlog상위 큐 편중 70% 초과
복구 이벤트reset 횟수, devlink health recover 횟수하루 1회 이상 자동 리셋
지연 품질p99/p999 RTT, drop reason 통계tail latency 임계 초과

eBPF 기반 네트워크 모니터링

eBPF(extended Berkeley Packet Filter)는 커널 수정 없이 네트워크 스택의 다양한 지점에 프로그램을 삽입하여 실시간 모니터링을 수행할 수 있는 기술입니다. XDP, TC BPF, 소켓 레벨 BPF, kprobe/tracepoint 기반 BPF 등 다양한 후크(Hook) 포인트를 활용하여 드라이버 수준의 정밀한 관측이 가능합니다.

/* XDP 기반 패킷 카운팅 프로그램 */

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>

/* per-CPU 맵: 프로토콜별 패킷/바이트 카운터 */
struct stats_key {
    __u16 proto;     /* EtherType */
};

struct stats_val {
    __u64 packets;
    __u64 bytes;
};

struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_HASH);
    __uint(max_entries, 256);
    __type(key, struct stats_key);
    __type(value, struct stats_val);
} proto_stats SEC(".maps");

/* 드롭 원인별 카운터 */
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, 4);
    __type(key, __u32);
    __type(value, __u64);
} drop_cnt SEC(".maps");

SEC("xdp")
int xdp_monitor(struct xdp_md *ctx)
{
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    struct ethhdr *eth = data;
    struct stats_key key;
    struct stats_val *val, init_val = {};

    /* 경계 검사: 이더넷 헤더 접근 가능 여부 */
    if (data + sizeof(*eth) > data_end)
        return XDP_PASS;

    key.proto = eth->h_proto;

    /* 프로토콜별 통계 업데이트 */
    val = bpf_map_lookup_elem(&proto_stats, &key);
    if (!val) {
        bpf_map_update_elem(&proto_stats, &key,
                            &init_val, BPF_ANY);
        val = bpf_map_lookup_elem(&proto_stats, &key);
        if (!val)
            return XDP_PASS;
    }

    val->packets++;
    val->bytes += (data_end - data);

    return XDP_PASS;  /* 패킷을 정상 처리 경로로 전달 */
}
# BPF 모니터링 프로그램 로드 및 사용

# 1. XDP 모니터 프로그램 컴파일 및 로드
clang -O2 -target bpf -c xdp_monitor.c -o xdp_monitor.o
ip link set dev eth0 xdpgeneric obj xdp_monitor.o sec xdp

# 2. 통계 조회 (bpftool)
bpftool map dump name proto_stats

# 3. TC BPF를 사용한 플로우 모니터링
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj tc_monitor.o sec classifier

# 4. bpftrace를 사용한 실시간 드라이버 관측
# NAPI poll 빈도와 처리량 관측
bpftrace -e 'tracepoint:napi:napi_poll {
    @poll_work = hist(args->work);
    @poll_budget = hist(args->budget);
}'

# 드라이버별 패킷 수신 추적
bpftrace -e 'kprobe:netif_receive_skb {
    $skb = (struct sk_buff *)arg0;
    @rx_by_dev[$skb->dev->name] = count();
}'

# 5. devlink trap 모니터링
devlink trap show pci/0000:03:00.0
devlink trap set pci/0000:03:00.0 trap source_mac_is_multicast \
    action trap
# trap된 패킷 확인
devlink trap group show pci/0000:03:00.0

Prometheus/Grafana 연동 설계

네트워크 드라이버 메트릭을 Prometheus로 수집하고 Grafana 대시보드로 시각화하면 장기적인 추세 분석과 이상 탐지가 가능합니다. node_exporter의 기본 네트워크 메트릭과 ethtool 통계를 결합하여 종합적인 모니터링 체계를 구축합니다.

node_exporter는 기본적으로 /proc/net/dev, /sys/class/net/ 정보를 수집합니다. 드라이버별 ethtool 통계는 --collector.ethtool 플래그를 활성화하거나 커스텀 익스포터(Custom Exporter)를 사용합니다.

메트릭Prometheus 이름임계값 예시경보 심각도
RX 에러 증가율node_network_receive_errs_totalrate 5m > 0Warning
TX 타임아웃node_ethtool_tx_timeoutincrease 1h > 0Critical
RX 드롭node_network_receive_drop_totalrate 5m > 100Warning
링크 상태 변경node_network_carrier_changes_totalincrease 10m > 2Warning
인터페이스 속도node_network_speed_bytes변경 감지Info
큐별 패킷 수node_ethtool_rx_queue_N_packets큐간 편차 70% 초과Warning
softnet time_squeezenode_softnet_times_squeezed_totalrate 5m > 10Warning
# Prometheus 경보 규칙 예시 (alerting_rules.yml)
cat <<'EOF'
groups:
  - name: network_driver
    rules:
      - alert: NICTxTimeout
        expr: increase(node_ethtool_tx_timeout[1h]) > 0
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "NIC TX timeout 발생 ({{ $labels.device }})"
          description: "{{ $labels.instance }}의 {{ $labels.device }}에서
                       TX timeout이 감지되었습니다."

      - alert: HighRxDropRate
        expr: rate(node_network_receive_drop_total[5m]) > 100
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "높은 RX drop rate ({{ $labels.device }})"

      - alert: LinkFlapping
        expr: increase(node_network_carrier_changes_total[10m]) > 4
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "링크 플랩 감지 ({{ $labels.device }})"

      - alert: SoftnetSqueeze
        expr: rate(node_softnet_times_squeezed_total[5m]) > 10
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: "softnet time_squeeze 빈발 (CPU {{ $labels.cpu }})"
EOF

netlink 소켓은 커널과 사용자 공간(User Space) 간 네트워크 설정 변경 이벤트를 교환하는 주요 인터페이스입니다. RTNETLINK 그룹을 구독하면 링크 상태 변경, 주소 추가/삭제, 라우트 변경 등을 실시간으로 감지할 수 있습니다.

주요 RTNETLINK 멀티캐스트 그룹(Multicast Group)은 다음과 같습니다.

/* netlink 이벤트 모니터 - C 구현 예시 */

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <linux/if.h>
#include <net/if.h>
#include <time.h>

static void handle_link_msg(struct nlmsghdr *nlh)
{
    struct ifinfomsg *ifi = NLMSG_DATA(nlh);
    struct rtattr *rta;
    int len = nlh->nlmsg_len - NLMSG_LENGTH(sizeof(*ifi));
    char ifname[IF_NAMESIZE] = "unknown";
    char timebuf[64];
    time_t now = time(NULL);

    strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S",
             localtime(&now));

    /* 인터페이스 이름 추출 */
    for (rta = IFLA_RTA(ifi); RTA_OK(rta, len);
         rta = RTA_NEXT(rta, len)) {
        if (rta->rta_type == IFLA_IFNAME) {
            strncpy(ifname, RTA_DATA(rta),
                    IF_NAMESIZE - 1);
            break;
        }
    }

    const char *action;
    switch (nlh->nlmsg_type) {
    case RTM_NEWLINK:
        action = (ifi->ifi_flags & IFF_UP) ?
                 "UP" : "DOWN";
        break;
    case RTM_DELLINK:
        action = "DELETED";
        break;
    default:
        action = "UNKNOWN";
    }

    printf("[%s] LINK %s: %s (index=%d, flags=0x%x)\n",
           timebuf, action, ifname,
           ifi->ifi_index, ifi->ifi_flags);
}

int main(void)
{
    int fd;
    struct sockaddr_nl sa;
    char buf[8192];

    fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
    if (fd < 0) {
        perror("socket");
        return 1;
    }

    memset(&sa, 0, sizeof(sa));
    sa.nl_family = AF_NETLINK;
    /* 구독할 멀티캐스트 그룹 설정 */
    sa.nl_groups = RTMGRP_LINK |        /* 링크 상태 */
                   RTMGRP_IPV4_IFADDR | /* IPv4 주소 */
                   RTMGRP_IPV6_IFADDR | /* IPv6 주소 */
                   RTMGRP_IPV4_ROUTE |  /* IPv4 라우트 */
                   RTMGRP_NEIGH;        /* ARP/NDP 이웃 */

    if (bind(fd, (struct sockaddr *)&sa,
             sizeof(sa)) < 0) {
        perror("bind");
        close(fd);
        return 1;
    }

    printf("netlink 이벤트 모니터 시작...\n");

    while (1) {
        ssize_t len = recv(fd, buf, sizeof(buf), 0);
        if (len <= 0)
            break;

        struct nlmsghdr *nlh;
        for (nlh = (struct nlmsghdr *)buf;
             NLMSG_OK(nlh, len);
             nlh = NLMSG_NEXT(nlh, len)) {

            switch (nlh->nlmsg_type) {
            case RTM_NEWLINK:
            case RTM_DELLINK:
                handle_link_msg(nlh);
                break;
            case RTM_NEWADDR:
                printf("[ADDR] 새 주소 추가\n");
                break;
            case RTM_DELADDR:
                printf("[ADDR] 주소 삭제\n");
                break;
            case RTM_NEWROUTE:
                printf("[ROUTE] 새 라우트 추가\n");
                break;
            case RTM_DELROUTE:
                printf("[ROUTE] 라우트 삭제\n");
                break;
            }
        }
    }

    close(fd);
    return 0;
}

커널 v5.17부터 도입된 SKB drop reason 인프라(Infrastructure)는 패킷이 드롭되는 정확한 원인을 추적할 수 있게 합니다. 기존에는 kfree_skb tracepoint에서 호출 위치만 확인할 수 있었지만, 이제는 NOT_SPECIFIED, NO_SOCKET, TCP_OLD_DATA 등 구체적인 사유가 함께 기록됩니다.

devlink trap은 하드웨어 레벨에서 드롭된 패킷을 소프트웨어로 전달하여 분석할 수 있게 하는 메커니즘입니다. NIC이 드롭한 패킷의 원인을 파악하는 데 필수적입니다.

# SKB drop reason 및 devlink trap 종합 분석

# 1. perf를 사용한 SKB drop reason 추적
# kfree_skb tracepoint에서 reason 필드 확인
perf record -e skb:kfree_skb -a -- sleep 10
perf script --fields=comm,pid,time,event,sym,trace | \
    grep -oP 'reason: \K\S+' | sort | uniq -c | sort -rn

# 2. bpftrace로 실시간 drop reason 관측
bpftrace -e 'tracepoint:skb:kfree_skb {
    @drop_reason[args->reason] = count();
}
interval:s:5 {
    print(@drop_reason);
    clear(@drop_reason);
}'

# 3. devlink trap 관리
# 사용 가능한 trap 목록 확인
devlink trap show pci/0000:03:00.0

# 특정 trap을 활성화하여 해당 패킷을 소프트웨어로 전달
devlink trap set pci/0000:03:00.0 \
    trap source_mac_is_multicast action trap

# trap 그룹별 통계
devlink trap group show pci/0000:03:00.0

# trap 통계 초기화
devlink trap group set pci/0000:03:00.0 \
    group l2_drops action drop

# 4. drop_monitor netlink 인터페이스
# dropwatch 도구 사용
dropwatch -l kas
# 대화형 세션에서:
# > start
# > set alertmode packet  (패킷 단위 알림)
# > set trunc 100         (패킷 첫 100바이트 캡처)

# 5. /sys/kernel/debug/tracing 기반 drop reason 추적
echo 1 > /sys/kernel/debug/tracing/events/skb/kfree_skb/enable
cat /sys/kernel/debug/tracing/trace_pipe | \
    grep -v "reason: NOT_SPECIFIED" | head -50
echo 0 > /sys/kernel/debug/tracing/events/skb/kfree_skb/enable