네트워크 드라이버 디버깅 & 체크리스트
네트워크 드라이버의 장애 대응과 품질 검증 절차를 정리합니다. devlink health 기반 리셋·복구, 회귀 테스트, netpoll/kdump 경로, 실전 트러블슈팅, 운영 지표(Observability) 체크리스트까지 다룹니다.
핵심 요약
- devlink health — 드라이버 장애 상태 보고와 리셋·복구 진입점(Entry Point)입니다.
- 회귀 테스트 — packetdrill/kselftest로 동작 회귀를 자동 검증합니다.
- netpoll/kdump — 커널 패닉(Kernel Panic)(panic) 상황의 네트워크 출력 경로입니다.
- 트러블슈팅 순서 — 증상 → 계측 → 원인 → 조치 단계를 고정합니다.
- 운영 지표 — 드롭(Drop)·지연(Latency)·재시작(Reboot) 횟수를 대시보드로 관측합니다.
단계별 이해
- 장애 분류
하드웨어·드라이버·스택 어느 계층 문제인지 구분합니다. - 상태 확인
devlink health와 커널 로그로 현재 상태를 수집합니다. - 재현·회귀 테스트
결함을 재현하고 실패 케이스를 고정합니다. - 수정 후 검증
리셋/복구 절차와 관측 지표로 안정성을 확인합니다.
리셋/장애 복구: 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();
}
- watchdog:
ndo_tx_timeout()에서 즉시 heavy reset을 수행하지 말고 workqueue로 이관 - devlink health: reporter dump/recover 콜백으로 운영팀의 장애 자동화와 연동
- AER 연계: PCIe fatal/non-fatal 이벤트를 reset state machine과 통합
devlink health reporter 등록과 사용
devlink health 프레임워크는 NIC 드라이버의 장애 감지, 진단, 자동 복구를 체계화하는 인프라입니다. 리포터(Reporter)를 등록하면 devlink health 명령으로 운영팀이 장애 상태를 조회하고, 자동 복구를 트리거할 수 있습니다.
/* 완전한 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_detected → slot_reset → resume 세 단계로 진행됩니다.
/* 완전한 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이 지원하는 주요 테스트 영역은 다음과 같습니다.
- TC flower 오프로드: 하드웨어 TC 규칙 추가/삭제/수정 테스트
- devlink health: reporter 등록, 진단 덤프(Dump), 자동 복구 테스트
- XDP: XDP 프로그램 로드/언로드, 다양한 verdict(PASS, DROP, TX, REDIRECT) 테스트
- devlink trap: 하드웨어 drop 이벤트 시뮬레이션
- devlink params: 드라이버 매개변수 설정/조회 테스트
# 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.sh | GRO(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) 프레임워크를 제공합니다.
- fail_function: 특정 커널 함수가 에러를 반환하도록 설정합니다. DMA 매핑 함수(
dma_map_single)를 실패시켜 드라이버의 DMA 에러 처리를 검증할 수 있습니다. - fail_page_alloc: 페이지(Page) 할당 실패를 시뮬레이션합니다. 링 버퍼(Ring Buffer) 리필(Refill) 실패 시 드라이버가 graceful하게 대응하는지 확인합니다.
- fail_make_request: 블록 I/O 요청 실패를 유발합니다 (네트워크보다는 스토리지 드라이버에 유용).
- KFENCE: 메모리 접근 오류를 샘플링 기반으로 감지합니다.
# 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);
}
netpoll 내부 아키텍처
netpoll은 커널 패닉이나 인터럽트(Interrupt)가 비활성화된 극한 상황에서도 네트워크 패킷을 송신할 수 있는 최소한의 네트워크 경로입니다. struct netpoll은 일반 네트워크 스택(Network Stack)을 우회하여 드라이버의 ndo_start_xmit을 직접 호출하는 폴링(Polling) 기반(Polling-based) 전송 메커니즘을 제공합니다.
netpoll_send_skb_on_dev()의 내부 동작을 단계별로 분석하면 다음과 같습니다.
- trylock 획득: TX 큐의
_xmit_lock을__netif_tx_trylock()으로 시도합니다. 이미 잠겨 있으면(일반 경로가 사용 중) 직접 NAPI poll을 호출하여 TX completion을 처리한 뒤 재시도합니다. - 직접 전송: 잠금(Lock) 획득에 성공하면
netpoll_start_xmit()을 통해 드라이버의ndo_start_xmit을 호출합니다. 일반 경로의 qdisc, TC, BQL 등을 모두 우회합니다. - 재시도 루프: 전송 실패 시
netpoll_poll_dev()로 NAPI poll을 호출하여 링 공간을 확보하고 최대NETPOLL_MAX_RETRIES(기본 20000)번 재시도합니다. - 인터럽트 폴링: 인터럽트가 비활성화된 상태이므로 completion을 받기 위해 드라이버의 poll 함수를 직접 호출합니다.
/* 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)을 부팅하여 크래시 덤프를 저장합니다. 캡처 커널 환경은 일반 부팅과 크게 다르며, 네트워크 드라이버는 여러 제약 조건 하에서 동작해야 합니다.
- 펌웨어 상태 상속: NIC 펌웨어가 크래시 시점의 상태를 그대로 유지합니다. 펌웨어 리셋이 완전하지 않으면 드라이버 초기화가 실패할 수 있습니다.
- 제한된 메모리: 캡처 커널은
crashkernel=파라미터로 예약된 적은 메모리에서 실행됩니다. 큰 링 버퍼(Buffer) 할당이나 대량의 DMA 매핑이 실패할 수 있습니다. - 단일 CPU: 캡처 커널은 일반적으로 단일 CPU에서 실행됩니다. 멀티큐 드라이버는 큐 수를 최소화해야 합니다.
- IOMMU 상태: 크래시 시점의 IOMMU 매핑이 남아 있어 DMA 주소 충돌이 발생할 수 있습니다.
| 제약 사항 | 영향 | 드라이버 대응 전략 | 확인 방법 |
|---|---|---|---|
| 펌웨어 비정상 상태 | 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 카운터 |
| 링크 flap | PHY state machine/interrupt storm | dmesg, 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)이 잘못 설정된 경우 등 다양한 원인이 있습니다. 체계적인 진단 플로우차트를 따라 원인을 빠르게 격리해야 합니다.
각 진단 단계에서 확인해야 할 핵심 사항은 다음과 같습니다.
- ethtool -S 카운터:
tx_timeout,tx_busy,tx_dropped값을 확인합니다. 특정 큐에만 집중되는지 전체 큐에 분산되는지가 원인 격리의 첫 단서입니다. - BQL 상태:
/sys/class/net/eth0/queues/tx-N/byte_queue_limits/아래limit,inflight값을 비교합니다.inflight가limit에 도달한 상태로 해제되지 않으면 completion 경로 문제입니다. - Completion IRQ:
/proc/interrupts에서 해당 큐의 인터럽트 카운터가 증가하는지 확인합니다. MSI-X 벡터와 큐 매핑이 일치하는지도 점검합니다. - Doorbell: MMIO 영역에 대한 쓰기가 PCIe 레벨에서 실패할 수 있습니다.
dmesg에서 AER(Advanced Error Reporting) 메시지를 확인합니다. - 펌웨어 상태:
devlink health show로 firmware reporter 상태를 확인하고, 필요 시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번째 | dropped | netif_rx backlog 초과로 드롭 | input_pkt_queue 크기 부족, CPU 과부하 |
| 3번째 | time_squeeze | softirq 시간 제한(2ms)으로 처리 중단 | 패킷 폭주 또는 NAPI poll 비효율 |
| 9번째 | cpu_collision | TX 경로에서 CPU 경합(Contention) 발생 | 여러 CPU가 동일 TX 큐에 접근 |
| 10번째 | received_rps | RPS로 전달된 프레임 | 정상 지표 (RPS 활성 시) |
| 11번째 | flow_limit_count | flow limit으로 드롭된 프레임 | 단일 플로우 과점유 |
| 12번째 | softnet_backlog_len | 현재 backlog 대기 길이 | 높으면 처리 지연 중 |
time_squeeze와 budget 소진의 차이를 이해하는 것이 중요합니다. 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를 활용하면 원인을 정밀하게 추적할 수 있습니다.
주요 원인 패턴과 진단 방법은 다음과 같습니다.
- PHY 상태 전이(State Transition) 이상: phylink은 내부적으로 상태 머신을 운영합니다.
AN_RESTART -> AN_COMPLETE -> LINK_OK순서로 진행되어야 하며,AN_RESTART가 반복되면 협상 실패입니다. - SFP 모듈 문제: 호환되지 않는 SFP 모듈, 광 출력(TX power) 저하, 수신 감도(RX sensitivity) 미달이 링크 플랩의 흔한 원인입니다.
- 신호 무결성(Signal Integrity): 케이블 길이 초과, 커넥터 접촉 불량, EMI 간섭으로 BER(Bit Error Rate)이 올라가면 PHY가 링크를 재설정합니다.
- 원격 장비 이상: 스위치 포트의 STP(Spanning Tree Protocol) 재계산, LACP 타임아웃, 원격 PHY 리셋 등이 원인일 수 있습니다.
#!/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 error | PCIe 링크 오류 | 슬롯 재장착, BIOS 설정 확인 |
firmware timeout | NIC 펌웨어 응답 없음 | 펌웨어 업데이트, devlink health |
성능 병목 분석 방법론
네트워크 드라이버의 성능 병목(Bottleneck)은 CPU 핫스팟(Hotspot), 잠금 경합(Lock Contention), 캐시(Cache) 미스(Cache Miss), DMA 매핑 오버헤드(Overhead) 등 다양한 원인에서 발생합니다. 체계적인 분석 파이프라인을 구축하면 병목 지점을 정량적으로 식별할 수 있습니다.
- perf top: 실시간(Real-time)으로 CPU를 가장 많이 소비하는 함수를 확인합니다. 네트워크 부하 중
mlx5e_napi_poll,napi_gro_receive,__netif_receive_skb_core등이 상위에 나타나는 것이 정상이며, 특정 잠금 함수가 상위에 나타나면 경합 문제입니다. - ftrace function_graph: 특정 함수의 호출 트리와 소요 시간을 추적합니다. 패킷 처리 경로에서 예상 외로 긴 함수를 식별하는 데 유용합니다.
- bpftrace 히스토그램: 패킷당 처리 시간, NAPI poll 주기, 인터럽트 간격 등을 분포로 시각화합니다.
- 플레임 그래프(Flame Graph): 전체 콜 스택을 한눈에 파악할 수 있는 시각화 도구입니다. CPU 프로파일링(Profiling) 결과를 플레임 그래프로 변환하면 병목 위치가 직관적으로 드러납니다.
# 종합 네트워크 성능 분석 파이프라인
# 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 기반 패킷 카운팅: 드라이버에서 패킷이 커널 스택에 도달하기 전에 통계를 수집합니다. 오버헤드가 극히 낮아 10Gbps 이상에서도 모든 패킷을 관측할 수 있습니다.
- TC BPF: ingress/egress TC 후크에 BPF 프로그램을 부착하여 플로우별 통계, 지연 측정, 조건부 미러링 등을 수행합니다.
- 소켓 레벨 BPF:
SO_ATTACH_BPF로 소켓 단위 모니터링을 수행합니다. - devlink trap: 하드웨어가 드롭한 패킷의 원인을 BPF 프로그램으로 분류하고 통계를 수집합니다.
/* 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_total | rate 5m > 0 | Warning |
| TX 타임아웃 | node_ethtool_tx_timeout | increase 1h > 0 | Critical |
| RX 드롭 | node_network_receive_drop_total | rate 5m > 100 | Warning |
| 링크 상태 변경 | node_network_carrier_changes_total | increase 10m > 2 | Warning |
| 인터페이스 속도 | node_network_speed_bytes | 변경 감지 | Info |
| 큐별 패킷 수 | node_ethtool_rx_queue_N_packets | 큐간 편차 70% 초과 | Warning |
| softnet time_squeeze | node_softnet_times_squeezed_total | rate 5m > 10 | Warning |
# 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 이벤트 모니터링
netlink 소켓은 커널과 사용자 공간(User Space) 간 네트워크 설정 변경 이벤트를 교환하는 주요 인터페이스입니다. RTNETLINK 그룹을 구독하면 링크 상태 변경, 주소 추가/삭제, 라우트 변경 등을 실시간으로 감지할 수 있습니다.
주요 RTNETLINK 멀티캐스트 그룹(Multicast Group)은 다음과 같습니다.
- RTNLGRP_LINK: 인터페이스 생성, 삭제, 상태 변경 (up/down, MTU 변경 등)
- RTNLGRP_IPV4_IFADDR / RTNLGRP_IPV6_IFADDR: IP 주소 추가/삭제
- RTNLGRP_IPV4_ROUTE / RTNLGRP_IPV6_ROUTE: 라우팅 테이블(Routing Table) 변경
- RTNLGRP_NEIGH: ARP/NDP 이웃 항목 변경
- RTNLGRP_TC: TC qdisc, class, filter 변경
/* 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;
}
devlink trap과 drop reason 추적
커널 v5.17부터 도입된 SKB drop reason 인프라(Infrastructure)는 패킷이 드롭되는 정확한 원인을 추적할 수 있게 합니다. 기존에는 kfree_skb tracepoint에서 호출 위치만 확인할 수 있었지만, 이제는 NOT_SPECIFIED, NO_SOCKET, TCP_OLD_DATA 등 구체적인 사유가 함께 기록됩니다.
devlink trap은 하드웨어 레벨에서 드롭된 패킷을 소프트웨어로 전달하여 분석할 수 있게 하는 메커니즘입니다. NIC이 드롭한 패킷의 원인을 파악하는 데 필수적입니다.
- devlink trap: 하드웨어 드롭 이벤트를 소프트웨어로 트랩합니다. 드라이버가
devlink_trap_report()를 호출하여 드롭된 패킷과 원인을 커널에 보고합니다. - SKB drop reason:
kfree_skb_reason()을 통해 소프트웨어 스택에서의 드롭 원인을 기록합니다.perf record -e skb:kfree_skb로 추적 가능합니다. - drop_monitor: netlink 기반 인터페이스로 드롭 이벤트를 실시간으로 수신합니다.
# 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
관련 문서
- 네트워크 디바이스 드라이버 (net_device) — 구조·콜백 레퍼런스
- 네트워크 드라이버 구현 가이드 (net_device) — 구현 실습
- RX/TX 심화 & 오프로드 (net_device) — 데이터 경로 심화
- 디버깅(Debugging) & 트러블슈팅 — 범용 커널 디버깅