TC (Traffic Control)

Linux TC(Traffic Control) 서브시스템을 실무 중심으로 심층 분석합니다. qdisc/class/filter 계층 구조, HTB/TBF/fq_codel/CAKE/netem/policing 동작 원리, ingress/egress·clsact·IFB 우회 shaping, mqprio/multiq와 queue steering, tc-bpf/flower 하드웨어 오프로드, 대역폭(Bandwidth) 제한·큐 지연(Latency)·버퍼(Buffer)블로트 문제를 계측하고 조정하는 운영 튜닝 절차까지 상세히 다룹니다.

전제 조건: 네트워크 스택(Network Stack), 라우팅(Routing), Network Device 드라이버 문서를 먼저 읽으세요. TC는 "패킷(Packet)을 어디서 어떻게 늦추거나 떨어뜨릴 것인가"를 다루므로, 라우팅 결정 뒤에 어떤 TX 큐와 드라이버 큐가 남는지까지 같이 머릿속에 있어야 합니다.
일상 비유: 이 개념은 톨게이트 진입로와 차선 배정과 비슷합니다. 차를 아예 막아야 하는지, 잠시 세워 속도를 맞춰야 하는지, 어느 차선을 우선으로 보낼지 결정하는 것이 TC입니다. 중요한 것은 어디가 실제 병목(Bottleneck)인지를 먼저 찾는 것입니다.

핵심 요약

  • qdisc — 실제 큐잉과 스케줄링이 일어나는 장소입니다. egress에서는 큐를 만들 수 있지만 ingress는 기본적으로 큐잉 대신 action만 수행합니다.
  • class / classid — HTB, HFSC, PRIO 같은 classful qdisc 안에서 트래픽을 계층적으로 나누는 단위입니다.
  • filter / action — 패킷을 어느 class나 큐로 보낼지 정하고, 필요하면 drop, redirect, mark, pedit 같은 부가 동작을 수행합니다.
  • shaping vs policing — shaping은 늦춰서 맞추고, policing은 넘치면 즉시 버리거나 마킹합니다. 둘은 적용 위치와 부작용이 다릅니다.
  • queue placement — qdisc만 봐서는 부족합니다. BQL, NIC TX queue, GSO/GRO, 드라이버 큐 길이까지 같이 봐야 실제 지연이 줄어듭니다.

단계별 이해

  1. 병목 위치 확정
    병목이 호스트 egress인지, 수신 ingress인지, 아니면 이미 upstream 회선 바깥인지 먼저 구분합니다.
  2. 큐잉 전략 선택
    단순 rate limit이면 TBF, 계층형 분배면 HTB, 지연 제어면 fq_codel/CAKE처럼 목적에 맞는 qdisc를 고릅니다.
  3. 분류 기준 설계
    DSCP, 5-tuple, fwmark, cgroup, BPF 중 운영에 맞는 classifier를 정하고 default class를 남깁니다.
  4. 하드웨어 경계 확인
    mqprio, queue_mapping, hw-tc-offload를 쓸 때는 어느 부분이 소프트웨어이고 어느 부분이 NIC로 내려가는지 확인합니다.
  5. 통계로 검증
    overlimits, backlog, dropped, BQL, 드라이버 큐 통계를 함께 보고 실제로 지연과 처리량(Throughput)이 원하는 방향으로 갔는지 확인합니다.
관련 표준: RFC 2474 (DiffServ), RFC 2697 (srTCM), RFC 2698 (trTCM), RFC 3168 (ECN), RFC 8290 (FQ-CoDel) — TC는 이 표준들을 기반으로 QoS와 대역폭 제어를 수행합니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

TC 개요

TC(Traffic Control)는 리눅스 커널의 패킷 스케줄링 및 트래픽 제어(Traffic Control) 프레임워크입니다. 네트워크 인터페이스에서 송수신되는 패킷의 대역폭 제한, 우선순위(Priority) 지정, 지연 시뮬레이션, 패킷 드롭 정책 등을 제어할 수 있습니다. 커널의 net/sched/ 디렉터리에 구현되어 있으며, 사용자 공간(User Space)에서는 tc 유틸리티(iproute2 패키지)를 통해 조작합니다.

TC의 핵심 목표는 QoS(Quality of Service)입니다. 제한된 네트워크 대역폭을 효율적으로 분배하고, 중요한 트래픽에 우선순위를 부여하며, 과도한 트래픽을 제어하여 네트워크 전체의 성능을 최적화합니다.

TC vs Netfilter: Netfilter(Netfilter)는 패킷 필터링/NAT/맹글링에 초점을 맞추고, TC는 패킷 스케줄링/대역폭 제어/QoS에 초점을 맞춥니다. 둘은 상호 보완적이며, iptables -j CLASSIFYtc filter ... action connmark 등으로 연동됩니다.
중요한 한계: TC는 호스트가 가진 큐를 다룹니다. 이미 ISP/상위 스위치에서 병목이 형성된 뒤라면 호스트 egress에 qdisc를 달아도 지연을 되돌릴 수 없습니다. 이런 경우는 병목 바로 앞에서 shape하거나, ingress를 IFB로 우회해 가상의 egress로 바꾸는 구성이 필요합니다.

TC의 4대 구성 요소

구성 요소역할커널 구조체(Struct)
qdisc (Queueing Discipline)패킷 큐잉/스케줄링 알고리즘struct Qdisc
classclassful qdisc 내부의 트래픽 분류 단위struct Qdisc_class_common
filter (classifier)패킷을 class로 분류하는 규칙struct tcf_proto
actionfilter 매치 시 수행할 동작struct tc_action
# TC 기본 명령어 구조
tc qdisc  add|del|replace|change|show  dev <DEV> [handle HANDLE] [root|parent CLASSID] [QDISC_TYPE] [PARAMS]
tc class  add|del|replace|change|show  dev <DEV> [classid CLASSID] [parent CLASSID] [CLASS_TYPE] [PARAMS]
tc filter add|del|replace|show         dev <DEV> [parent CLASSID] [prio PRIO] protocol <PROTO> [FILTER_TYPE] [PARAMS]
tc action add|del|show                 [ACTION_TYPE] [PARAMS]

# 현재 설정 확인
tc -s qdisc show dev eth0          # qdisc 통계 포함
tc -s class show dev eth0          # class 통계 포함
tc -s filter show dev eth0         # filter 통계 포함
tc -d qdisc show dev eth0          # 상세 출력

TC 아키텍처

TC는 네트워크 디바이스의 egress(송신) 경로와 ingress(수신) 경로에 각각 qdisc를 부착하여 동작합니다. 모든 네트워크 디바이스는 기본적으로 하나의 root qdisc를 가지며, classful qdisc의 경우 계층적 트리 구조로 확장됩니다.

TC 아키텍처: Egress / Ingress 패킷 경로 Application (Socket) IP Stack (Routing) Egress Root Qdisc (egress) enqueue → dequeue → xmit class 1:1 class 1:2 class 1:3 filter: u32 / flower / bpf NIC Driver (xmit) Ingress Ingress Qdisc / clsact filter + action (no queuing) drop mirred police NIC Driver (recv)
TC 아키텍처: Egress 경로에서 classful qdisc + filter로 패킷 스케줄링, Ingress 경로에서 filter + action으로 수신 제어

Handle과 ClassID

TC에서 모든 qdisc와 class는 고유한 식별자를 가집니다. 형식은 MAJOR:MINOR이며, 16비트씩 총 32비트입니다.

식별자형식설명
handleMAJOR:0qdisc 식별자. MINOR는 항상 0
classidMAJOR:MINORclass 식별자. MAJOR는 부모 qdisc의 handle
root-디바이스의 최상위 egress qdisc 부착점
ingressffff:0ingress qdisc의 고정 handle
# Handle/ClassID 예시
tc qdisc add dev eth0 root handle 1: htb default 30
tc class add dev eth0 parent 1: classid 1:1 htb rate 100mbit
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 50mbit ceil 100mbit
tc class add dev eth0 parent 1:1 classid 1:20 htb rate 30mbit ceil 100mbit
tc class add dev eth0 parent 1:1 classid 1:30 htb rate 20mbit ceil 100mbit

패킷 처리 경로

송신 패킷이 TC를 거치는 과정은 다음과 같습니다.

  1. enqueue: dev_queue_xmit()에서 root qdisc의 enqueue() 호출
  2. classify: classful qdisc인 경우 filter를 통해 패킷이 속할 class 결정
  3. queue: 결정된 class(또는 leaf qdisc)의 큐에 패킷 삽입
  4. dequeue: NET_TX softirq에서 dequeue()를 호출하여 패킷 추출
  5. transmit: 드라이버의 ndo_start_xmit()으로 전달
/* net/core/dev.c — 송신 경로에서 TC 진입 */
static int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev)
{
    struct Qdisc *q;
    ...
    q = rcu_dereference_bh(txq->qdisc);
    if (q->enqueue) {
        rc = __dev_xmit_skb(skb, q, dev, txq);   /* qdisc enqueue 경로 */
        goto out;
    }
    ...
}

/* net/sched/sch_generic.c — dequeue 루프 */
static inline struct sk_buff *dequeue_skb(struct Qdisc *q,
                                             int *packets)
{
    struct sk_buff *skb = q->ops->dequeue(q);
    ...
    return skb;
}

Classless Qdisc

Classless qdisc는 class 계층 없이 단일 큐로 동작합니다. 패킷을 분류하지 않고 하나의 알고리즘으로 모든 패킷을 처리합니다.

pfifo_fast (기본 qdisc)

pfifo_fast는 리눅스의 전통적인 기본 qdisc입니다(최신 커널에서는 fq_codel이 기본). 3개의 우선순위 밴드(0, 1, 2)로 구성되며, IP 헤더의 TOS 필드에 따라 패킷을 밴드에 배치합니다.

# pfifo_fast는 일반적으로 직접 설정할 필요 없음 (기본값)
tc qdisc show dev eth0
# 출력: qdisc pfifo_fast 0: root refcnt 2 bands 3 priomap ...

# 밴드 매핑 (TOS → band): 높은 우선순위 트래픽이 band 0으로
# band 0: 최우선  band 1: 일반  band 2: 벌크

fq_codel (Fair Queuing + CoDel)

fq_codel은 현대 리눅스의 기본 qdisc로, Fair QueuingCoDel(Controlled Delay) AQM 알고리즘을 결합합니다. 플로우별 공정 큐잉으로 단일 플로우의 독점을 방지하고, CoDel로 버퍼블로트(bufferbloat)를 해결합니다.

RFC 8290: fq_codel은 RFC 8290 (The FQ-CoDel Packet Scheduler and Active Queue Management Algorithm)에 기술된 알고리즘을 구현합니다.
# fq_codel 파라미터 설정
tc qdisc replace dev eth0 root fq_codel \
    limit 10240 \       # 전체 큐 최대 패킷 수
    flows 1024 \        # 플로우 해시 테이블 크기
    target 5ms \        # CoDel 목표 지연 시간
    interval 100ms \    # CoDel 측정 간격
    quantum 1514 \      # DRR 양자 (바이트)
    ecn                  # ECN 마킹 활성화

# 통계 확인
tc -s qdisc show dev eth0
# Sent 1234567 bytes 8901 pkt (dropped 23, overlimits 0 requeues 1)
#  maxpacket 1514 drop_overlimit 0 new_flow_count 456
#  ecn_mark 12 new_flows_len 0 old_flows_len 2
/* net/sched/sch_fq_codel.c — fq_codel 핵심 구조 */
struct fq_codel_sched_data {
    u32     perturbation;      /* 해시 솔트 */
    u32     limit;             /* 전체 최대 패킷 수 */
    u32     flows_cnt;         /* 플로우 수 */
    u32     quantum;           /* DRR 양자 */
    u32     drop_batch_size;
    u32     memory_limit;
    struct codel_params  cparams;
    struct codel_stats   cstats;
    struct fq_codel_flow *flows;     /* 플로우 배열 */
    struct list_head     new_flows;  /* 새 플로우 리스트 */
    struct list_head     old_flows;  /* 기존 플로우 리스트 */
};

CAKE (Common Applications Kept Enhanced)

CAKE는 단일 병목 링크를 실무적으로 다루기 위해 만들어진 현대적 shaping qdisc입니다. COBALT AQM, Deficit-mode shaper, flow/host isolation, DiffServ tin, 링크 오버헤드(Overhead) 보정을 한 덩어리로 묶어 제공합니다. 즉, HTB + fq_codel + overhead tuning + host fairness를 자주 같이 쓰던 환경에서 설정 실수를 줄이기 좋은 qdisc입니다.

항목fq_codelCAKE
주요 목적 버퍼블로트 완화와 flow fairness 단일 병목 링크 shaping + fairness + DiffServ 통합
대역폭 제한 직접 shaping 기능 없음 bandwidth로 직접 shaping
호스트 공정성(Fairness) 기본 flow 단위 triple-isolate 등 host/flow 공정성 포함
권장 위치 서버 일반 egress, HTB leaf 가정용/엣지 WAN, VPN 종단, IFB downlink shaping
덜 맞는 경우 정확한 계층형 보장 대역폭 복잡한 계층형 서비스 트리, 하드웨어 TC 큐 맵 설계
# 단일 업링크 병목: 100M 회선을 95M로 shape, DiffServ 4단계 사용
tc qdisc replace dev pppoe-wan root cake \
    bandwidth 95mbit \
    diffserv4 nat

# 가변 링크(예: LTE/무선)에서 ingress 추정 shaping
# 보통 IFB에 붙여 사용
tc qdisc replace dev ifb0 root cake \
    bandwidth 200mbit \
    autorate-ingress diffserv3 nat

# GSO를 쪼개 shaping 정확도 우선 (기본 동작)
# 고속 링크에서 CPU 여유가 충분하면 그대로 두는 편이 보통 안전
tc qdisc replace dev eth0 root cake bandwidth 1gbit besteffort

# 처리량 우선으로 GSO 유지
tc qdisc replace dev eth0 root cake bandwidth 1gbit besteffort no-split-gso
선택 기준: CAKE는 "인터넷 회선 하나를 잘 다듬는" 데 매우 강하지만, 서비스별 최소 보장율과 부모/자식 대역폭 빌리기를 정밀하게 설계해야 한다면 여전히 HTB + fq_codel 조합이 더 직접적입니다. 반대로 홈 라우터, 지점 라우터, 터널(Tunnel) 종단처럼 한 곳의 실제 병목를 다듬는 용도라면 CAKE가 더 단순하고 실수가 적습니다.

FQ (Fair Queue)와 TCP pacing

fq qdisc는 지연 제어보다 소켓(Socket) pacing흐름별 공정 전송에 초점을 둡니다. 특히 커널 TCP stack이 SO_MAX_PACING_RATE 또는 내부 pacing rate를 계산해 내릴 때, fq가 이를 부드럽게 직렬화(Serialization)합니다. 따라서 로컬 호스트가 직접 송신하는 고속 TCP에서는 fq가 잘 맞고, 엣지 병목 회선 shaping에는 보통 cakehtb + fq_codel가 더 적합합니다.

# 로컬 호스트 송신 pacing에 맞춘 FQ qdisc
tc qdisc replace dev eth0 root fq \
    limit 10000 \
    flow_limit 100 \
    quantum 1514 \
    initial_quantum 15140 \
    maxrate 10gbit

# TCP가 qdisc 앞에 너무 많이 쌓지 않도록 같이 보는 값
sysctl net.ipv4.tcp_limit_output_bytes
운영 감각: fq"호스트가 스스로 내보내는 속도"를 다듬는 데 강하고, cake/tbf/htb"링크 병목에 맞춰 강제로 늦추는 일"에 더 가깝습니다.

SFQ (Stochastic Fairness Queueing)

SFQ는 해시(Hash) 기반 공정 큐잉입니다. 플로우 해시를 사용하여 패킷을 여러 큐에 분배하고, 라운드 로빈(Round Robin)으로 dequeue합니다. 주기적으로 해시 함수를 변경(perturbation)하여 해시 충돌로 인한 불공정을 완화합니다.

# SFQ 설정
tc qdisc add dev eth0 root sfq perturb 10 quantum 1514
# perturb: 해시 재계산 주기 (초), quantum: 라운드당 전송 바이트

TBF (Token Bucket Filter)

TBF는 토큰 버킷 알고리즘으로 대역폭을 제한합니다. 토큰이 일정 속도로 버킷에 충전되고, 패킷 전송 시 토큰을 소비합니다. 토큰이 부족하면 패킷은 대기하거나 드롭됩니다.

# TBF: 1Mbps 대역폭 제한, 버스트 32KB, 지연 최대 50ms
tc qdisc add dev eth0 root tbf \
    rate 1mbit \        # 평균 전송 속도
    burst 32kbit \      # 버킷 크기 (버스트 허용량)
    latency 50ms        # 최대 큐잉 지연

# 또는 buffer + limit으로 지정
tc qdisc add dev eth0 root tbf \
    rate 10mbit \
    buffer 1600 \       # 버킷 크기 (바이트)
    limit 3000          # 최대 큐 길이 (바이트)
/* net/sched/sch_tbf.c — Token Bucket Filter 구조 */
struct tbf_sched_data {
    u32     limit;             /* 큐 최대 바이트 */
    u32     max_size;          /* 단일 패킷 최대 크기 */
    s64     buffer;            /* 토큰 버킷 깊이 (ns) */
    s64     mtu;               /* peakrate 버킷 깊이 */
    struct psched_ratecfg rate;    /* 평균 전송 속도 */
    struct psched_ratecfg peak;    /* 최대 전송 속도 */
    struct Qdisc  *qdisc;          /* 내부 qdisc */
    s64     tokens;            /* 현재 토큰 수 (ns) */
    s64     ptokens;           /* peak rate 토큰 (ns) */
    s64     t_c;               /* 마지막 토큰 갱신 시각 */
};

netem (Network Emulator)

netem은 WAN 환경을 시뮬레이션하기 위한 qdisc입니다. 지연, 패킷 손실, 패킷 복제, 패킷 순서 변경 등을 에뮬레이션합니다. 네트워크 애플리케이션 테스트에 매우 유용합니다.

# 100ms 지연 추가 (± 10ms 지터, 정규분포)
tc qdisc add dev eth0 root netem delay 100ms 10ms distribution normal

# 1% 패킷 손실
tc qdisc add dev eth0 root netem loss 1%

# 복합: 50ms 지연 + 0.5% 손실 + 0.1% 복제 + 25% 순서 변경
tc qdisc add dev eth0 root netem \
    delay 50ms 5ms \
    loss 0.5% \
    duplicate 0.1% \
    reorder 25% 50%

# 대역폭 제한과 결합 (netem + tbf)
tc qdisc add dev eth0 root handle 1: netem delay 100ms
tc qdisc add dev eth0 parent 1:1 handle 10: tbf rate 1mbit burst 32kbit latency 50ms

Classful Qdisc

Classful qdisc는 내부에 class 계층을 가지며, filter를 통해 패킷을 분류하여 각 class에 할당합니다. class는 자체 qdisc를 가질 수 있어 재귀적 계층 구조를 형성합니다.

HTB (Hierarchical Token Bucket)

HTB는 가장 널리 사용되는 classful qdisc입니다. 계층적 토큰 버킷 구조로, 각 class에 보장 대역폭(rate)최대 대역폭(ceil)을 설정할 수 있습니다. 여유 대역폭은 자식 class 간에 공유됩니다.

HTB 핵심 개념: rate는 해당 class에 보장되는 최소 대역폭, ceil은 빌릴 수 있는 최대 대역폭입니다. rate < ceil이면 부모로부터 여유 대역폭을 빌려 사용할 수 있습니다.
# HTB 기본 구조: 100Mbps 회선을 3개 class로 분배
tc qdisc add dev eth0 root handle 1: htb default 30

# 루트 class: 전체 대역폭
tc class add dev eth0 parent 1: classid 1:1 htb \
    rate 100mbit ceil 100mbit

# 자식 class: 우선 트래픽 (보장 50M, 최대 100M)
tc class add dev eth0 parent 1:1 classid 1:10 htb \
    rate 50mbit ceil 100mbit prio 1

# 자식 class: 일반 트래픽 (보장 30M, 최대 100M)
tc class add dev eth0 parent 1:1 classid 1:20 htb \
    rate 30mbit ceil 100mbit prio 2

# 자식 class: 벌크 트래픽 (보장 20M, 최대 80M) — default
tc class add dev eth0 parent 1:1 classid 1:30 htb \
    rate 20mbit ceil 80mbit prio 3

# 각 leaf class에 fq_codel 부착
tc qdisc add dev eth0 parent 1:10 handle 10: fq_codel
tc qdisc add dev eth0 parent 1:20 handle 20: fq_codel
tc qdisc add dev eth0 parent 1:30 handle 30: fq_codel
/* net/sched/sch_htb.c — HTB class 구조 */
struct htb_class {
    struct Qdisc_class_common common;
    struct psched_ratecfg    rate;    /* 보장 대역폭 */
    struct psched_ratecfg    ceil;    /* 최대 대역폭 */
    s64                      buffer;  /* rate 토큰 버킷 깊이 */
    s64                      cbuffer; /* ceil 토큰 버킷 깊이 */
    s64                      tokens;  /* rate 현재 토큰 */
    s64                      ctokens; /* ceil 현재 토큰 */
    s64                      t_c;     /* 마지막 갱신 시각 */
    int                      prio;    /* 우선순위 (0=최고) */
    int                      quantum; /* DRR 양자 */
    enum htb_cmode           cmode;   /* HTB_CAN_SEND / HTB_MAY_BORROW / HTB_CANT_SEND */
    struct Qdisc             *leaf;   /* leaf qdisc (fq_codel 등) */
    struct htb_class         *parent; /* 부모 class */
    ...
};

PRIO (Priority Scheduler)

PRIO는 고정 우선순위 스케줄러(Scheduler)입니다. 여러 밴드(기본 3개)를 가지며, 낮은 번호의 밴드가 완전히 비어야 다음 밴드의 패킷을 전송합니다. 대역폭 제한 기능은 없으며, 순수한 우선순위 기반 스케줄링을 제공합니다.

# PRIO: 3밴드 우선순위 qdisc
tc qdisc add dev eth0 root handle 1: prio bands 3 \
    priomap 1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1

# 각 밴드에 별도 qdisc 부착 가능
tc qdisc add dev eth0 parent 1:1 handle 10: sfq perturb 10   # 최우선
tc qdisc add dev eth0 parent 1:2 handle 20: tbf rate 20mbit burst 5kb latency 70ms
tc qdisc add dev eth0 parent 1:3 handle 30: sfq perturb 10   # 벌크

CBQ, HFSC, DRR

Qdisc알고리즘특징권장 사용
CBQ Class-Based Queueing 링크 유휴 시간 기반 추정, 복잡한 파라미터 레거시. HTB로 대체 권장
HFSC Hierarchical Fair Service Curve 실시간(rt), 링크 공유(ls), 상한(ul) 서비스 커브 정밀한 지연 보장 필요 시
DRR Deficit Round Robin 간단한 가중치 라운드 로빈, O(1) dequeue 가벼운 공정 스케줄링
# HFSC 예시: 실시간 서비스 커브를 사용한 지연 보장
tc qdisc add dev eth0 root handle 1: hfsc default 30
tc class add dev eth0 parent 1: classid 1:1 hfsc \
    sc rate 100mbit ul rate 100mbit
tc class add dev eth0 parent 1:1 classid 1:10 hfsc \
    rt m1 50mbit d 10ms m2 30mbit \
    ls m1 50mbit d 10ms m2 30mbit

# DRR 예시
tc qdisc add dev eth0 root handle 1: drr
tc class add dev eth0 parent 1: classid 1:1 drr quantum 1500
tc class add dev eth0 parent 1: classid 1:2 drr quantum 3000

멀티큐와 mqprio: TC는 NIC 큐까지 배선할 수 있습니다

현대 NIC는 보통 여러 TX/RX queue를 가지며, 실제 병목은 qdisc 하나가 아니라 qdisc → BQL → 드라이버 TX queue → NIC hardware queue 체인 전체에 걸쳐 생깁니다. multiqmqprio는 이 다중 큐 환경에서 TC가 어디까지 개입할 수 있는지 보여 주는 대표 qdisc입니다.

TX 분류에서 NIC queue까지 이어지는 경로 Socket / App SO_PRIORITY Filter / Action flower / skbedit mqprio / multiq priority → traffic class queue_mapping → 특정 TXQ TC0 TC1 TC2 BQL / netdev TXQ byte_queue_limits NIC Hardware Queue queue 0 / 1 / 2 / 3 RSS / DCB / offload skbedit priority mqprio map으로 traffic class 선택 skbedit queue_mapping 소프트웨어 TX exact queue 지정 flower hw_tc 수신 트래픽을 hardware traffic class로 분배 지연을 줄이려면 qdisc 하나만 볼 것이 아니라 BQL, 실제 TX queue 분산, 드라이버 오프로드 경계까지 같이 봐야 합니다.
mqprio는 우선순위를 hardware traffic class와 queue set으로 연결하고, multiq는 기본적인 다중 큐 head-of-line blocking 완화를 제공합니다.
기법주요 목적강점주의점
multiq 여러 hardware TX queue를 기본적으로 활용 큐 하나가 멈춰도 다른 큐가 막히지 않아 head-of-line blocking 완화 정교한 traffic class 설계는 없음. 기본 큐 분산용
mqprio priority → traffic class → queue set 맵핑 DCB, TSN, NIC 하드웨어 큐와 정합이 좋음 map, queues, hw, shaper 모드를 NIC 지원과 같이 봐야 함
skbedit priority 송신 traffic class 선택 mqprio와 결합 시 filter에서 분류 결과를 queue set까지 전파 가능 실제 queue 선택은 mqprio map 결과에 따름
skbedit queue_mapping 정확한 TX queue 선택 소프트웨어에서 특정 TXQ로 직접 보냄 RX에서는 하드웨어 전용. 소프트웨어 RX queue 선택은 불가
flower hw_tc 수신 traffic class 선택 RSS와 하드웨어 traffic class를 같이 설계할 수 있음 드라이버가 지원하지 않으면 의미가 없고 보통 offload와 같이 봐야 함
# mqprio: 3개 traffic class를 4개 TX queue에 매핑
tc qdisc replace dev eth0 root handle 100: mqprio \
    num_tc 3 \
    map 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 2 \
    queues 1@0 1@1 2@2 \
    hw 0

# flower + skbedit priority: mqprio의 traffic class 3으로 분류
tc filter add dev eth0 parent 100: protocol ip prio 10 \
    flower ip_proto udp dst_port 12345 \
    action skbedit priority 3

# multiq: 소프트웨어에서 특정 TX queue로 직접 배정
tc qdisc replace dev eth0 root handle 200: multiq
tc filter add dev eth0 parent 200: protocol ip prio 20 \
    flower dst_ip 192.0.2.10 \
    action skbedit queue_mapping 3

# RX 쪽은 flower의 hw_tc로 hardware traffic class 지정
tc filter add dev eth0 ingress protocol ip prio 5 \
    flower ip_proto tcp dst_port 443 hw_tc 2
실무 함정: hw 0로 mqprio를 소프트웨어 전용으로 돌릴 수는 있지만, 드라이버가 가진 실제 queue 모델과 어긋나면 기대한 분산이 나오지 않을 수 있습니다. 또 queue_mapping은 송신 쪽 소프트웨어 steering에는 유용하지만, 수신 queue 선택은 보통 NIC 하드웨어 RSS와 hw_tc 같은 기능으로 해결해야 합니다.

Filter / Classifier

Filter는 패킷을 classful qdisc의 특정 class로 분류합니다. 여러 종류의 classifier가 있으며, 우선순위(prio)에 따라 순서대로 평가됩니다.

u32 Filter

u32는 가장 전통적인 classifier로, 패킷 헤더의 임의 오프셋(Offset)에서 32비트 값을 추출하여 매칭합니다. 유연하지만 복잡하고, 최신 환경에서는 flower로 대체되는 추세입니다.

# u32: 목적지 포트 80 (HTTP) → class 1:10
tc filter add dev eth0 parent 1: protocol ip prio 1 u32 \
    match ip dport 80 0xffff flowid 1:10

# u32: 소스 서브넷 10.0.0.0/24 → class 1:20
tc filter add dev eth0 parent 1: protocol ip prio 2 u32 \
    match ip src 10.0.0.0/24 flowid 1:20

# u32: TOS 필드 최소 지연(0x10) 매칭
tc filter add dev eth0 parent 1: protocol ip prio 3 u32 \
    match ip tos 0x10 0xff flowid 1:10

flower Filter

flower는 현대적인 classifier로, 직관적인 키워드 매칭을 제공합니다. L2~L4 헤더 필드를 이름으로 지정할 수 있어 u32보다 사용하기 쉽습니다. 하드웨어 오프로드도 지원합니다.

# flower: 목적지 포트 443 (HTTPS), TCP → class 1:10
tc filter add dev eth0 parent 1: protocol ip prio 1 \
    flower ip_proto tcp dst_port 443 flowid 1:10

# flower: 소스 MAC + VLAN ID 매칭
tc filter add dev eth0 parent 1: protocol 802.1Q prio 1 \
    flower src_mac aa:bb:cc:dd:ee:ff vlan_id 100 \
    action mirred egress redirect dev eth1

# flower: DSCP 매칭 (DiffServ)
tc filter add dev eth0 parent 1: protocol ip prio 1 \
    flower ip_tos 0xb8/0xfc flowid 1:10    # EF (Expedited Forwarding)

matchall, cgroup, BPF Filter

# matchall: 모든 패킷에 action 적용
tc filter add dev eth0 parent 1: protocol all prio 99 \
    matchall action police rate 10mbit burst 64k conform-exceed drop

# cgroup: cgroup 기반 분류 (net_cls)
tc filter add dev eth0 parent 1: protocol ip prio 10 \
    cgroup

# BPF classifier: eBPF 프로그램으로 분류
tc filter add dev eth0 parent 1: protocol all prio 1 \
    bpf obj cls_prog.o sec classifier da
cgroup classifier 주의: tc-cgroup(8)은 이 classifier가 egress path에서, 그것도 주로 로컬에서 생성한 패킷에 유용하다고 설명합니다. 또한 전통적인 net_cls cgroup v1 classid 힌트에 의존하므로, 새 시스템에서는 cgroup v2 + BPF 또는 SO_PRIORITY, fwmark, nft meta priority 같은 방법이 더 흔합니다.

chain과 shared block

규모가 커지면 장치마다 filter를 복사하는 방식은 유지보수가 어렵습니다. block은 여러 장치/포트가 같은 filter 집합을 공유하게 하고, chain은 한 block 안에서 평가 단계를 나누는 개념입니다. 특히 switchdev, representor, HW offload, 다수 포트 공통 정책에서는 shared block이 매우 중요합니다.

# 두 포트가 같은 ingress block을 공유
tc qdisc add dev eth0 clsact ingress_block 22
tc qdisc add dev eth1 clsact ingress_block 22

# block 22에 공통 규칙 추가
tc filter add block 22 protocol ip prio 10 \
    flower ip_proto tcp dst_port 443 \
    action drop

# block 단위로 조회
tc filter show block 22

# 계층적 평가가 필요하면 chain 번호를 나눔
tc filter add block 22 chain 10 protocol ip prio 20 \
    flower src_ip 192.0.2.0/24 action drop
운영 감각: block은 규칙 집합을 재사용하는 도구이고, chain은 한 규칙 집합 안에서 단계적으로 평가하는 도구입니다. 포트가 많아질수록 "장치별 filter"보다 "공유 block + 필요 시 장치별 예외" 구조가 관리 비용이 훨씬 낮습니다.
/* net/sched/cls_api.c — Filter 등록/조회 핵심 구조 */
struct tcf_proto {
    struct tcf_proto   __rcu *next;
    void __rcu         *root;          /* classifier 전용 데이터 */
    int                (*classify)(struct sk_buff *,
                                    const struct tcf_proto *,
                                    struct tcf_result *);
    __be16             protocol;       /* ETH_P_IP 등 */
    u32                prio;           /* 우선순위 */
    struct Qdisc       *q;             /* 부착된 qdisc */
    const struct tcf_proto_ops *ops;   /* classifier 연산 테이블 */
    ...
};

Action

TC action은 filter와 결합하여 패킷에 대한 동작을 수행합니다. 하나의 filter에 여러 action을 체이닝할 수 있습니다.

주요 Action 종류

Action모듈기능
gactact_gactpass, drop, continue, reclassify, pipe 등 기본 동작
mirredact_mirred패킷 미러링(mirror) 또는 리다이렉트(redirect)
policeact_police토큰 버킷 기반 속도 제한 (policing)
peditact_pedit패킷 헤더 필드 수정 (edit)
connmarkact_connmarkconntrack 마크를 skb 마크로 복사
skbeditact_skbeditskb 메타데이터 수정 (priority, mark, queue_mapping)
ctact_ctconntrack 조회/커밋, zone 지정, NAT
csumact_csum체크섬(Checksum) 재계산
tunnel_keyact_tunnel_key터널 메타데이터 설정/해제 (VXLAN, Geneve 등)
action 재사용: tc-actions(8)가 강조하듯 action은 classifier에만 종속된 일회용 객체가 아닙니다. index를 가진 action을 따로 만들고 여러 filter에서 재사용할 수 있으므로, 대규모 정책에서는 "매치 조건"과 "동작"을 분리해 설계하는 편이 좋습니다.
# mirred: eth0 수신 패킷을 eth1으로 미러링
tc filter add dev eth0 ingress protocol all prio 1 \
    matchall action mirred egress mirror dev eth1

# mirred: 리다이렉트 (원본은 드롭)
tc filter add dev eth0 ingress protocol ip prio 1 \
    flower dst_ip 10.0.0.1 \
    action mirred egress redirect dev veth0

# police: 수신 10Mbps 초과 시 드롭
tc filter add dev eth0 ingress protocol ip prio 1 \
    matchall action police rate 10mbit burst 256k \
    conform-exceed drop/continue

# pedit: TTL 값 64로 설정
tc filter add dev eth0 parent 1: protocol ip prio 1 \
    u32 match ip dst 10.0.0.0/8 \
    action pedit ex munge ip ttl set 64

# connmark + skbedit: conntrack 마크 복원 후 skb priority 설정
tc filter add dev eth0 parent 1: protocol ip prio 1 \
    matchall action connmark \
    action skbedit priority 1:10

# ct: conntrack 조회 후 zone 지정
tc filter add dev eth0 ingress protocol ip prio 1 \
    flower ct_state -trk \
    action ct zone 1

# action 체이닝: 여러 action 순서 실행
tc filter add dev eth0 ingress protocol ip prio 1 \
    flower src_ip 192.168.1.0/24 \
    action pedit ex munge ip tos set 0x28 \
    action skbedit priority 1:10 \
    action csum ip4h

ingress qdisc와 clsact

일반적인 qdisc는 egress(송신) 경로에서만 동작합니다. 수신(ingress) 경로에서 트래픽을 제어하려면 특수한 ingress qdisc 또는 clsact qdisc를 사용합니다.

ingress qdisc

ingress qdisc는 실제 큐잉을 수행하지 않습니다. filter와 action을 부착할 수 있는 부착점만 제공하며, 수신 패킷에 대해 policing, 리다이렉트, 드롭 등을 수행합니다.

# ingress qdisc 부착
tc qdisc add dev eth0 ingress

# 수신 트래픽 policing: 100Mbps 초과 드롭
tc filter add dev eth0 ingress protocol ip prio 1 \
    matchall action police rate 100mbit burst 1m \
    conform-exceed drop

# 특정 소스 IP 수신 차단
tc filter add dev eth0 ingress protocol ip prio 2 \
    flower src_ip 10.0.0.100 action drop

# ingress qdisc 제거
tc qdisc del dev eth0 ingress

clsact qdisc

clsact는 ingress의 상위 호환입니다. ingress와 egress 양쪽 모두에 filter/action을 부착할 수 있으며, 특히 BPF 프로그램의 부착점으로 설계되었습니다. 커널 4.5에서 도입되었으며, TC BPF 프로그래밍의 표준 진입점(Entry Point)입니다.

clsact vs ingress: clsact는 ingress를 대체합니다. 하나의 디바이스에 ingress와 clsact를 동시에 부착할 수 없습니다. 새 프로젝트에서는 clsact를 사용하세요.
# clsact qdisc 부착
tc qdisc add dev eth0 clsact

# ingress 방향 filter (수신)
tc filter add dev eth0 ingress protocol ip prio 1 \
    flower dst_port 22 ip_proto tcp action drop

# egress 방향 filter (송신)
tc filter add dev eth0 egress protocol ip prio 1 \
    flower dst_ip 10.0.0.0/8 \
    action police rate 1mbit burst 64k conform-exceed drop

# BPF 프로그램 부착 (clsact의 주요 용도)
tc filter add dev eth0 ingress bpf da obj tc_prog.o sec tc_ingress
tc filter add dev eth0 egress  bpf da obj tc_prog.o sec tc_egress
/* net/sched/sch_ingress.c — clsact qdisc 구현 */
static struct Qdisc_ops clsact_qdisc_ops __read_mostly = {
    .cl_ops     = &clsact_class_ops,
    .id         = "clsact",
    .priv_size  = sizeof(struct clsact_sched_data),
    .enqueue    = clsact_enqueue,       /* 실질적 큐잉 없음 */
    .dequeue    = noop_dequeue,
    .peek       = noop_dequeue,
    .init       = clsact_init,
    .destroy    = clsact_destroy,
    .owner      = THIS_MODULE,
};

/* ingress/egress 구분: TC_H_MIN_INGRESS / TC_H_MIN_EGRESS */
#define TC_H_MIN_INGRESS    0xFFF2U
#define TC_H_MIN_EGRESS     0xFFF3U

IFB로 ingress를 egress처럼 바꿔 shaping 합니다

ingress qdisc와 clsact ingress는 기본적으로 큐를 만들지 않습니다. 그래서 수신 대역폭을 부드럽게 늦추는 shaping은 직접 할 수 없고, 보통은 패킷을 IFB(Intermediate Functional Block) 장치로 mirred redirect한 뒤 IFB의 egress에 qdisc를 달아 처리합니다. 실무에서 "다운로드 shaping"이라고 부르는 구성이 사실은 이 우회 경로입니다.

수신 shaping의 실제 경로: ingress → IFB redirect → IFB egress qdisc WAN NIC RX eth0 ingress clsact / ingress filter + police / mirred IFB redirect mirred egress redirect dev ifb0 IFB root qdisc cake / htb / tbf Local stack IP / socket 직접 ingress police 간단하고 싸지만 초과 패킷을 바로 드롭 IFB + shaping 대기열을 만들고 부드럽게 늦춤 적합한 상황 다운링크 지연 제어, CAKE 적용 핵심은 수신 패킷을 IFB의 송신 패킷처럼 바꿔 큐잉 가능한 위치로 옮기는 것입니다.
ingress는 본래 queueing hook이 아니므로, shaping이 필요하면 IFB로 redirect해서 가상의 egress를 만든 뒤 qdisc를 적용합니다.
방식패킷 초과 시장점단점
ingress police 즉시 drop 또는 mark 단순하고 CPU 비용이 낮음 TCP 재전송(Retransmission)과 지터 증가 가능
IFB + HTB/TBF IFB egress 큐에 대기 정밀한 shaping, 기존 HTB 설계 재사용 가상 장치와 redirect 비용이 추가
IFB + CAKE flow/tin 기준으로 지연 제어 다운링크 bufferbloat 완화에 매우 강함 단일 병목 중심 설계라 복잡한 계층형 서비스 트리에는 덜 적합
# IFB 준비
modprobe ifb numifbs=1
ip link add ifb0 type ifb 2>/dev/null || true
ip link set dev ifb0 up

# 실제 NIC에는 clsact를 달고 ingress 패킷을 IFB로 redirect
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress protocol all prio 10 \
    matchall action mirred egress redirect dev ifb0

# 이제 IFB의 egress에 원하는 shaping qdisc 적용
tc qdisc replace dev ifb0 root cake \
    bandwidth 200mbit ingress diffserv4 nat

# 또는 IFB에 HTB 부착
tc qdisc replace dev ifb0 root handle 1: htb default 20
tc class add dev ifb0 parent 1: classid 1:1 htb rate 200mbit ceil 200mbit
tc class add dev ifb0 parent 1:1 classid 1:20 htb rate 200mbit ceil 200mbit

# 정리
tc qdisc del dev eth0 clsact
tc qdisc del dev ifb0 root
제한: tc-mirred(8)도 경고하듯이 IFB에서 다른 IFB로 다시 redirect하는 구성은 피하세요. 커널은 action nesting을 4단계로 제한하며, IFB 연쇄는 디버깅(Debugging)과 성능 모두를 빠르게 악화시킵니다.

tc-bpf: eBPF 기반 TC 프로그램

TC BPF는 eBPF 프로그램을 TC의 filter/classifier로 부착하여, 커널 내에서 패킷을 프로그래밍 가능한 방식으로 처리합니다. XDP(BPF/XDP)와 달리 전체 sk_buff에 접근할 수 있어 L2~L4 헤더 수정, 터널링, conntrack 연동 등 풍부한 기능을 제공합니다.

direct-action (da) 모드

direct-action 모드에서는 BPF 프로그램이 classifier와 action을 하나로 통합합니다. 프로그램의 반환값이 곧 패킷 처리 결과(TC_ACT_OK, TC_ACT_SHOT 등)가 됩니다. 거의 모든 TC BPF 프로그램이 이 모드를 사용합니다.

반환값상수동작
TC_ACT_OK0패킷을 다음 단계로 전달 (정상 처리)
TC_ACT_SHOT2패킷 드롭
TC_ACT_STOLEN4패킷을 소비 (BPF가 직접 처리)
TC_ACT_REDIRECT7패킷 리다이렉트 (bpf_redirect())
TC_ACT_UNSPEC-1기본 classful 동작 사용
TC_ACT_PIPE3다음 action/filter로 전달
TC_ACT_RECLASSIFY1재분류
/* TC BPF 프로그램 예시: 수신 패킷 rate limiting */
#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <bpf/bpf_helpers.h>

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 1024);
    __type(key, __u32);             /* 소스 IP */
    __type(value, __u64);           /* 마지막 패킷 시간 (ns) */
} rate_map SEC(".maps");

SEC("tc")
int tc_rate_limit(struct __sk_buff *skb)
{
    void *data     = (void *)(long)skb->data;
    void *data_end = (void *)(long)skb->data_end;

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return TC_ACT_OK;

    if (eth->h_proto != bpf_htons(ETH_P_IP))
        return TC_ACT_OK;

    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > data_end)
        return TC_ACT_OK;

    __u32 src = ip->saddr;
    __u64 now = bpf_ktime_get_ns();
    __u64 *last = bpf_map_lookup_elem(&rate_map, &src);

    if (last && (now - *last) < 1000000) {  /* 1ms 이내 재전송 → 드롭 */
        return TC_ACT_SHOT;
    }

    bpf_map_update_elem(&rate_map, &src, &now, BPF_ANY);
    return TC_ACT_OK;
}

char _license[] SEC("license") = "GPL";
# TC BPF 프로그램 컴파일 및 부착
clang -O2 -target bpf -c tc_prog.c -o tc_prog.o

# clsact qdisc 부착 후 BPF 프로그램 로드
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj tc_prog.o sec tc

# 부착된 BPF 프로그램 확인
tc filter show dev eth0 ingress
# filter protocol all pref 49152 bpf chain 0
# filter protocol all pref 49152 bpf chain 0 handle 0x1 tc_prog.o:[tc] direct-action ...

# BPF 프로그램 제거
tc filter del dev eth0 ingress

TC BPF vs XDP 비교

특성TC BPFXDP
부착점clsact (ingress/egress)NIC 드라이버 (ingress only)
컨텍스트struct __sk_buffstruct xdp_md
sk_buff 접근전체 sk_buff 메타데이터없음 (raw 패킷만)
egress 지원가능불가
L3/L4 수정bpf_skb_store_bytes()직접 메모리 수정
성능높음 (softirq 레벨)매우 높음 (드라이버 레벨)
터널링bpf_skb_set_tunnel_key()불가
conntrackbpf_ct_lookup()불가

Policing vs Shaping

트래픽 속도 제어에는 두 가지 접근법이 있습니다. Shaping(정형화)은 패킷을 큐에 보관하며 일정 속도로 내보내고, Policing(경찰)은 초과 트래픽을 즉시 드롭하거나 마킹합니다.

특성ShapingPolicing
동작패킷 지연 (큐잉)초과 패킷 드롭/마킹
적용 경로egress onlyingress + egress
메커니즘Token Bucket + 큐Token Bucket (큐 없음)
구현HTB, TBF 등 qdiscaction police
장점패킷 손실 최소화, 부드러운 전송CPU/메모리 부하 적음, ingress 가능
단점지연 증가, 메모리 사용패킷 손실 → TCP 재전송

Token Bucket 알고리즘

Shaping과 Policing 모두 Token Bucket 알고리즘을 기반으로 합니다. 토큰이 일정 속도(rate)로 버킷에 충전되며, 패킷 전송 시 패킷 크기만큼의 토큰을 소비합니다. 버킷의 최대 용량(burst)은 순간적으로 허용되는 최대 전송량을 결정합니다.

/* Token Bucket 의사 코드 */
void token_bucket_check(struct sk_buff *skb)
{
    s64 now = ktime_get_ns();
    s64 elapsed = now - last_update;
    s64 new_tokens = elapsed * rate;  /* 경과 시간 × 속도 */

    tokens = min(tokens + new_tokens, burst);  /* 버킷 용량 초과 방지 */
    last_update = now;

    if (tokens >= qdisc_pkt_len(skb)) {
        tokens -= qdisc_pkt_len(skb);
        /* 전송 허용 */
    } else {
        /* Shaping: 큐에 보관, 타이머 설정 */
        /* Policing: 드롭 또는 ECN 마킹 */
    }
}
# Shaping (egress): TBF로 10Mbps 제한
tc qdisc add dev eth0 root tbf rate 10mbit burst 32kbit latency 50ms

# Policing (ingress): 10Mbps 초과 드롭
tc qdisc add dev eth0 ingress
tc filter add dev eth0 ingress protocol ip prio 1 \
    matchall action police rate 10mbit burst 256k \
    conform-exceed drop

# Policing 고급: 2-rate 3-color (RFC 2698)
tc filter add dev eth0 ingress protocol ip prio 1 \
    matchall action police \
    rate 10mbit burst 256k \
    peakrate 15mbit mtu 2000 \
    conform-exceed pass/pipe \
    action pedit ex munge ip dscp set 0x0a

HTB 실전 설정

HTB를 사용한 실전 대역폭 관리 설계입니다. 1Gbps 회선에서 서비스별로 대역폭을 보장하고 제한하는 구조를 설계합니다.

설계 원칙

HTB 설계 규칙:
  • 자식 class의 rate 합은 부모의 rate를 초과하지 않아야 합니다 (대역폭 보장)
  • ceil은 부모의 ceil을 초과할 수 없습니다
  • default class를 반드시 지정하여 미분류 트래픽을 처리합니다
  • leaf class에 fq_codel을 부착하여 버퍼블로트를 방지합니다
#!/bin/bash
# HTB 실전 설정 스크립트: 1Gbps 회선 대역폭 관리

DEV=eth0

# 기존 설정 초기화
tc qdisc del dev $DEV root 2>/dev/null

# ── Root qdisc ──
tc qdisc add dev $DEV root handle 1: htb default 40 r2q 10

# ── Level 1: 전체 대역폭 ──
tc class add dev $DEV parent 1: classid 1:1 htb \
    rate 1000mbit ceil 1000mbit

# ── Level 2: 서비스별 클래스 ──

# VoIP/실시간 (보장 100M, 최대 200M, 최우선)
tc class add dev $DEV parent 1:1 classid 1:10 htb \
    rate 100mbit ceil 200mbit prio 0

# 웹 서비스 (보장 400M, 최대 900M)
tc class add dev $DEV parent 1:1 classid 1:20 htb \
    rate 400mbit ceil 900mbit prio 1

# 데이터베이스 복제 (보장 300M, 최대 800M)
tc class add dev $DEV parent 1:1 classid 1:30 htb \
    rate 300mbit ceil 800mbit prio 2

# 기본/벌크 (보장 200M, 최대 500M) — default
tc class add dev $DEV parent 1:1 classid 1:40 htb \
    rate 200mbit ceil 500mbit prio 3

# ── Leaf qdisc: 각 class에 fq_codel 부착 ──
tc qdisc add dev $DEV parent 1:10 handle 10: fq_codel
tc qdisc add dev $DEV parent 1:20 handle 20: fq_codel
tc qdisc add dev $DEV parent 1:30 handle 30: fq_codel
tc qdisc add dev $DEV parent 1:40 handle 40: fq_codel

# ── Filter: 트래픽 분류 ──

# VoIP: DSCP EF (0xb8) → class 1:10
tc filter add dev $DEV parent 1: protocol ip prio 1 \
    flower ip_tos 0xb8/0xfc flowid 1:10

# VoIP: UDP 5060-5080 (SIP) → class 1:10
tc filter add dev $DEV parent 1: protocol ip prio 2 \
    flower ip_proto udp dst_port 5060-5080 flowid 1:10

# 웹: TCP 80/443 → class 1:20
tc filter add dev $DEV parent 1: protocol ip prio 3 \
    flower ip_proto tcp dst_port 80 flowid 1:20
tc filter add dev $DEV parent 1: protocol ip prio 3 \
    flower ip_proto tcp dst_port 443 flowid 1:20

# DB 복제: TCP 3306 (MySQL), 5432 (PostgreSQL) → class 1:30
tc filter add dev $DEV parent 1: protocol ip prio 4 \
    flower ip_proto tcp dst_port 3306 flowid 1:30
tc filter add dev $DEV parent 1: protocol ip prio 4 \
    flower ip_proto tcp dst_port 5432 flowid 1:30

# 나머지 → default class 1:40 (htb default 40)

# 설정 확인
tc -s -d qdisc show dev $DEV
tc -s -d class show dev $DEV
tc -s filter show dev $DEV

HTB 대역폭 빌리기 메커니즘

HTB의 핵심은 대역폭 빌리기(borrowing)입니다. class의 트래픽이 rate 미만이면 여유 토큰이 부모를 통해 형제 class에게 빌려집니다. 이 메커니즘은 3가지 모드로 표현됩니다.

모드조건동작
HTB_CAN_SENDtokens > 0 && ctokens > 0즉시 전송 (rate 이내)
HTB_MAY_BORROWtokens <= 0 && ctokens > 0부모에서 토큰 빌려 전송 (rate~ceil 사이)
HTB_CANT_SENDctokens <= 0전송 불가 (ceil 초과), 대기

커널 내부 구현

TC의 커널 구현은 net/sched/ 디렉터리에 위치합니다. 모듈화된 구조로, qdisc/classifier/action 각각이 독립적인 커널 모듈(Kernel Module)로 구현됩니다.

Qdisc_ops 구조체

모든 qdisc는 struct Qdisc_ops를 구현하여 등록합니다. 이 구조체는 qdisc의 enqueue, dequeue, init, destroy 등의 콜백(Callback)을 정의합니다.

/* include/net/sch_generic.h — Qdisc 연산 테이블 */
struct Qdisc_ops {
    struct Qdisc_ops       *next;
    const struct Qdisc_class_ops *cl_ops;
    char                    id[IFNAMSIZ];
    int                     priv_size;
    unsigned int            static_flags;

    int   (*enqueue)(struct sk_buff *skb,
                      struct Qdisc *sch,
                      struct sk_buff **to_free);
    struct sk_buff *  (*dequeue)(struct Qdisc *);
    struct sk_buff *  (*peek)(struct Qdisc *);

    int   (*init)(struct Qdisc *sch, struct nlattr *arg,
                   struct netlink_ext_ack *extack);
    void  (*reset)(struct Qdisc *);
    void  (*destroy)(struct Qdisc *);
    int   (*change)(struct Qdisc *sch, struct nlattr *arg,
                     struct netlink_ext_ack *extack);
    int   (*dump)(struct Qdisc *, struct sk_buff *);
    int   (*dump_stats)(struct Qdisc *, struct gnet_dump *);

    struct module           *owner;
};

tcf_proto_ops (Classifier 연산)

/* include/net/sch_generic.h — Classifier 연산 테이블 */
struct tcf_proto_ops {
    struct list_head        head;
    char                    kind[IFNAMSIZ];

    int   (*classify)(struct sk_buff *,
                       const struct tcf_proto *,
                       struct tcf_result *);
    int   (*init)(struct tcf_proto *);
    void  (*destroy)(struct tcf_proto *, bool,
                      struct netlink_ext_ack *);

    void *(*get)(struct tcf_proto *, u32 handle);
    int   (*change)(struct net *, struct sk_buff *,
                     struct tcf_proto *, unsigned long,
                     u32 handle, struct nlattr **,
                     void **, bool,
                     struct netlink_ext_ack *);
    int   (*delete)(struct tcf_proto *, void *, bool *,
                     bool, struct netlink_ext_ack *);
    int   (*dump)(struct net *, struct tcf_proto *,
                   void *, struct sk_buff *,
                   struct tcmsg *, bool);

    struct module           *owner;
    ...
};

net/sched/ 주요 소스 파일

파일내용
sch_generic.cQdisc 프레임워크 코어, dev_queue_xmit() 연동
sch_api.cNetlink API: qdisc/class CRUD (tc 명령어 처리)
cls_api.cClassifier 프레임워크, filter chain 관리
act_api.cAction 프레임워크
sch_htb.cHTB 구현
sch_tbf.cTBF 구현
sch_fq_codel.cfq_codel 구현
sch_netem.cnetem 구현
sch_ingress.cingress/clsact qdisc
sch_prio.cPRIO 구현
cls_u32.cu32 classifier
cls_flower.cflower classifier
cls_bpf.cBPF classifier (tc-bpf)
act_mirred.cmirred action
act_police.cpolice action
act_gact.cgeneric action (drop/pass/...)
act_ct.cconntrack action

struct Qdisc

/* include/net/sch_generic.h — Qdisc 핵심 구조체 */
struct Qdisc {
    int   (*enqueue)(struct sk_buff *skb,
                      struct Qdisc *sch,
                      struct sk_buff **to_free);
    struct sk_buff *  (*dequeue)(struct Qdisc *sch);
    unsigned int     flags;
    u32              limit;
    const struct Qdisc_ops  *ops;     /* 연산 테이블 */
    struct qdisc_size_table __rcu *stab;
    struct hlist_node       hash;
    u32              handle;          /* MAJOR:0 */
    u32              parent;          /* 부모 class ID */
    struct netdev_queue     *dev_queue;
    struct net_rate_estimator __rcu *rate_est;
    struct gnet_stats_basic_sync  bstats;   /* 기본 통계 */
    struct gnet_stats_queue       qstats;   /* 큐 통계 */
    unsigned long    state;
    struct Qdisc     *next_sched;
    struct sk_buff_head gso_skb;
    struct sk_buff_head skb_bad_txq;
    long unsigned int state2;           /* 추가 상태 플래그 */
    struct rcu_head  rcu;
};

사용자 공간의 tc 명령어는 Netlink 소켓을 통해 커널과 통신합니다. RTM_NEWQDISC, RTM_DELQDISC, RTM_NEWTFILTER 등의 메시지 타입을 사용합니다.

/* net/sched/sch_api.c — Netlink 메시지 핸들러 등록 */
static int __init pktsched_init(void)
{
    register_qdisc(&pfifo_fast_ops);
    register_qdisc(&noqueue_qdisc_ops);
    register_qdisc(&mq_qdisc_ops);

    rtnl_register(PF_UNSPEC, RTM_NEWQDISC, tc_modify_qdisc, NULL, 0);
    rtnl_register(PF_UNSPEC, RTM_DELQDISC, tc_get_qdisc, NULL, 0);
    rtnl_register(PF_UNSPEC, RTM_GETQDISC, tc_get_qdisc,
                  tc_dump_qdisc, 0);
    rtnl_register(PF_UNSPEC, RTM_NEWTCLASS, tc_ctl_tclass, NULL, 0);
    rtnl_register(PF_UNSPEC, RTM_DELTCLASS, tc_ctl_tclass, NULL, 0);
    rtnl_register(PF_UNSPEC, RTM_GETTCLASS, tc_ctl_tclass,
                  tc_dump_tclass, 0);
    return 0;
}

성능 튜닝과 모니터링

tc 통계 확인

# 상세 통계 (-s: statistics, -d: details)
tc -s -d qdisc show dev eth0
# qdisc htb 1: root refcnt 2 r2q 10 default 0x40 ...
#  Sent 12345678 bytes 9012 pkt (dropped 34, overlimits 567 requeues 0)
#  backlog 0b 0p requeues 0

tc -s -d class show dev eth0
# class htb 1:10 parent 1:1 prio 0 rate 100Mbit ceil 200Mbit ...
#  Sent 5678901 bytes 4567 pkt (dropped 0, overlimits 123 requeues 0)
#  lended: 234 borrowed: 56 giants: 0
#  tokens: -1234 ctokens: 5678

# JSON 출력 (스크립트 파싱용)
tc -j -s qdisc show dev eth0 | python3 -m json.tool

# 실시간 모니터링 (변화 감시)
watch -n 1 'tc -s class show dev eth0'

변경 감시와 트리 시각화

정적인 덤프(Dump)만 보면 qdisc/class 트리 전체가 한눈에 안 들어오는 경우가 많습니다. tc(8)-g 옵션은 트리 구조를 보여 주고, tc monitor는 런타임 변경을 추적합니다. 여러 자동화 도구가 규칙을 덮어쓰는 환경에서는 이 두 명령이 특히 유용합니다.

# qdisc / class 트리를 그래프 형태로 보기
tc -g qdisc show dev eth0
tc -g class show dev eth0

# RTNETLINK 기반 런타임 변경 감시
tc monitor

# 특정 공유 block 정책 확인
tc filter show block 22

# JSON 덤프와 조합하면 자동 점검에 편함
tc -j -s filter show dev eth0 ingress | python3 -m json.tool

통계 필드 해석

필드의미
Sent전송된 총 바이트/패킷 수
dropped드롭된 패킷 수 (큐 가득, AQM 등)
overlimits속도 제한 초과 횟수 (dequeue 시도 실패)
requeues드라이버 거부로 재큐잉된 횟수
backlog현재 큐에 대기 중인 바이트/패킷
lended자체 토큰으로 전송한 횟수 (HTB)
borrowed부모에서 빌려 전송한 횟수 (HTB)
tokens현재 rate 토큰 수 (음수=부족, HTB)
ctokens현재 ceil 토큰 수 (HTB)

psched 시간 해상도

# TC 내부 시간 해상도 확인
cat /proc/net/psched
# 출력: 00000001 0000003e9 000f4240 3b9aca00
# [1] tick_in_usec_num  [2] tick_in_usec_den
# [3] us2tick(1000000)  [4] clock_res (ns)

# 실질적으로 1 tick = 1 ns (PSCHED_TICKS_PER_SEC = 10^9)

지연이 어디서 생기는지 계층별로 봅니다

TC를 붙였는데도 지연이 안 줄어드는 가장 흔한 이유는 병목이 qdisc가 아니라 TCP 송신 버퍼, BQL, 드라이버 TX ring, NIC hardware queue 쪽에 있기 때문입니다. 따라서 backlog만 보지 말고 아래 지점을 같이 봐야 합니다.

계층무엇을 보나대표 명령의미
qdisc backlog, overlimits, dropped tc -s qdisc show dev eth0 qdisc가 실제로 병목인지 확인
class borrowed, tokens, ctokens tc -s class show dev eth0 HTB/HFSC가 ceil/rate 때문에 막히는지 확인
BQL limit, inflight /sys/class/net/.../byte_queue_limits/* 드라이버 queue가 qdisc 뒤에서 다시 길어지는지 확인
드라이버 / NIC queue stop, restart, timeout, xmit busy ethtool -S eth0 실제 hardware queue 편중과 드라이버 병목 확인
링크 계층 TX drop, carrier, qlen ip -s link show dev eth0 qdisc 바깥의 인터페이스 레벨 상태 확인
TCP 출력 제한 tcp_limit_output_bytes sysctl net.ipv4.tcp_limit_output_bytes TCP가 qdisc 앞에 과도하게 적재하는지 판단
# 1. qdisc / class에서 backlog와 overlimits 확인
tc -s qdisc show dev eth0
tc -s class show dev eth0

# 2. BQL: qdisc 뒤 드라이버 큐가 얼마나 차는지 확인
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/inflight

# 3. 드라이버 / NIC 통계
ethtool -S eth0
ip -s link show dev eth0

# 4. TCP가 qdisc 앞에 너무 많이 쌓는지 확인
sysctl net.ipv4.tcp_limit_output_bytes

하드웨어 오프로드

일부 NIC는 TC 규칙의 하드웨어 오프로드를 지원합니다. flower filter와 matchall filter가 주요 오프로드 대상이며, 하드웨어에서 직접 패킷 분류와 action을 수행하여 CPU 부하를 줄입니다.

# 하드웨어 오프로드 지원 확인
ethtool -k eth0 | grep tc-offload
# hw-tc-offload: on

# 하드웨어 오프로드 활성화
ethtool -K eth0 hw-tc-offload on

# flower filter에 skip_sw 플래그: 하드웨어 전용 실행
tc filter add dev eth0 ingress protocol ip prio 1 \
    flower skip_sw dst_port 80 ip_proto tcp \
    action mirred egress redirect dev eth1

# skip_hw: 소프트웨어 전용 실행 (오프로드 비활성화)
tc filter add dev eth0 ingress protocol ip prio 2 \
    flower skip_hw src_ip 10.0.0.0/8 action drop

# 오프로드 상태 확인 (in_hw 플래그)
tc -s filter show dev eth0 ingress
# ... in_hw in_hw_count 1

eSwitch와 TC Flower Offload

eSwitch (Embedded Switch)를 지원하는 SmartNIC/DPU(Mellanox/NVIDIA mlx5, Intel ice 등)에서는 TC flower 규칙이 NIC의 하드웨어 스위치로 오프로드됩니다. 이는 소프트웨어 브릿지나 OVS의 CPU 오버헤드를 획기적으로 줄여줍니다.

# ━━━ eSwitch switchdev 모드 전환 ━━━

# 1. eSwitch를 switchdev 모드로 설정
devlink dev eswitch set pci/0000:03:00.0 mode switchdev

# 2. Representor 포트 확인 (VF에 대한 가상의 제어 인터페이스)
ip link show
# enp3s0f0np0      <-- PF (uplink)
# enp3s0f0np0_0    <-- VF0 representor
# enp3s0f0np0_1    <-- VF1 representor

# 3. Representor에 TC flower 규칙 설치 (VF 간 포워딩)
tc qdisc add dev enp3s0f0np0_0 ingress
tc filter add dev enp3s0f0np0_0 ingress \
    protocol 802.1Q \
    flower vlan_id 100 \
    action vlan pop \
    action mirred egress redirect dev enp3s0f0np0_1

# 4. conntrack 상태 기반 오프로드
tc filter add dev enp3s0f0np0_0 ingress \
    protocol ip \
    flower ct_state +trk+new \
    action ct commit \
    action mirred egress redirect dev enp3s0f0np0_1

# 5. HW 오프로드 확인
tc -s filter show dev enp3s0f0np0_0 ingress | grep in_hw
#   in_hw in_hw_count 1    <-- HW에서 처리됨
eSwitch TC Flower 매칭 키

eSwitch HW로 오프로드 가능한 TC flower 매칭 키와 액션입니다.

카테고리매칭 키설명
L2eth_type이더타입 (0x0800=IPv4, 0x86DD=IPv6)
vlan_id, vlan_ethtypeVLAN ID 및 태그 해제
dst_mac, src_macMAC 주소 매칭
L3ip_protoIP 프로토콜 (TCP=6, UDP=17, ICMP=1)
src_ip, dst_ipIP 주소 (IPv4/IPv6)
tos, dscpToS/DSCP 값
ttlTTL/Hop Limit
L4src_port, dst_portTCP/UDP 포트
tcp_flagsTCP 플래그 (SYN, ACK, FIN 등)
ct_stateconntrack 상태 (NEW, ESTABLISHED, RELATED)
ct_zoneconntrack zone
액션mirred패킷 리다이렉트 ( egress redirect)
vlanVLAN 태그 추가/변경/삭제
TC Flower Offload 제한:
  • 모든 매칭 키와 액션이 HW 오프로드 가능한 것은 아닙니다. 드라이버가 지원하지 않으면 skip_sw 지정 시 오류 발생
  • HW 플로우 테이블 크기가 제한적입니다 (수백만 엔트리). 초과 시 자동 소프트웨어 폴백
  • ct() 오프로드는 드라이버/펌웨어(Firmware) 버전에 따라 지원 여부가 다릅니다
OVS Hardware Offload

OVS(Open vSwitch)와 eSwitch를 결합하면 가상 스위칭을 HW로 오프로드할 수 있습니다.

# ━━━ OVS + eSwitch HW Offload 설정 ━━━

# 1. eSwitch를 switchdev 모드로 전환
devlink dev eswitch set pci/0000:03:00.0 mode switchdev

# 2. OVS에서 HW offload 활성화
ovs-vsctl set Open_vSwitch . other_config:hw-offload=true
ovs-vsctl set Open_vSwitch . other_config:tc-policy=skip_sw

# 3. PF와 VF representor를 브릿지에 연결
ovs-vsctl add-br br-int
ovs-vsctl add-port br-int enp3s0f0np0       # PF (uplink)
ovs-vsctl add-port br-int enp3s0f0np0_0     # VF0 representor
ovs-vsctl add-port br-int enp3s0f0np0_1     # VF1 representor

# 4. 오프로드된 플로우 확인
ovs-appctl dpctl/dump-flows type=offloaded
# recirc_id(0),in_port(2),eth(...),ipv4(src=10.0.0.5,dst=10.0.0.10,...)
# packets:1523400, bytes:97497600, used:0.001s, flags:SFPR

성능 튜닝 팁

TC 성능 최적화 지침:
  • leaf qdisc 선택: HTB leaf에 fq_codel을 사용하여 버퍼블로트 방지
  • 단일 병목 링크: WAN uplink/downlink 한 군데를 다듬는 용도라면 CAKE가 HTB보다 단순하고 안전한 경우가 많음
  • r2q 파라미터: HTB의 r2q (rate-to-quantum)은 자동 quantum 계산에 사용. 저대역폭 class에서 "quantum too small" 경고 시 r2q 조정 또는 quantum 직접 지정
  • burst 크기: TBF/police의 burst가 너무 작으면 처리량 저하. 최소 MTU 이상, 권장: rate * HZ / 8
  • ingress shaping: 수신 트래픽을 늦춰야 하면 직접 police하지 말고 IFB redirect 후 egress qdisc로 shaping
  • filter 순서: 자주 매칭되는 filter를 높은 prio(낮은 숫자)로 배치
  • flower vs u32: 신규 설정에서는 flower 권장 (HW offload 지원, 직관적)
  • clsact + BPF: 복잡한 분류 로직은 BPF 프로그램으로 구현 (성능 + 유연성)
  • mqprio / queue steering: 고속 NIC에서는 qdisc뿐 아니라 실제 queue set과 BQL까지 같이 설계
  • txqueuelen: ip link set dev eth0 txqueuelen 1000 — qdisc 큐와 별도의 드라이버 큐 길이
# txqueuelen 확인/설정
ip link show dev eth0 | grep qlen
ip link set dev eth0 txqueuelen 1000

# BQL (Byte Queue Limits) 확인 — 드라이버 레벨 큐 제한
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit_max
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit_min

# 전체 TC 설정 백업/복원
tc -d qdisc show dev eth0 > tc_backup.txt
tc -d class show dev eth0 >> tc_backup.txt
tc -d filter show dev eth0 >> tc_backup.txt

# 모든 TC 설정 초기화
tc qdisc del dev eth0 root 2>/dev/null
tc qdisc del dev eth0 ingress 2>/dev/null

Qdisc 내부 구현

Qdisc는 단순한 큐가 아니라 enqueue → classify → queue → dequeue → xmit 파이프라인(Pipeline)을 관리하는 커널 객체입니다. 이 절에서는 Qdisc_ops의 콜백 체인, bulk dequeue 최적화, Qdisc 트리의 재귀적 구조를 상세히 분석합니다.

enqueue/dequeue/peek 콜백 체인

enqueue()는 패킷을 qdisc 내부 자료구조에 삽입하고 NET_XMIT_SUCCESS 또는 NET_XMIT_DROP을 반환합니다. classful qdisc에서는 enqueue 내부에서 tc_classify()를 호출하여 패킷이 속할 class를 결정한 뒤, 해당 class의 leaf qdisc로 재귀적으로 enqueue합니다.

/* net/sched/sch_htb.c — HTB enqueue 경로 (간략화) */
static int htb_enqueue(struct sk_buff *skb,
                        struct Qdisc *sch,
                        struct sk_buff **to_free)
{
    struct htb_sched *q = qdisc_priv(sch);
    struct htb_class *cl;
    int ret;

    /* 1. classify: filter chain을 통해 class 결정 */
    cl = htb_classify(skb, sch, &ret);
    if (cl == NULL) {
        if (ret & __NET_XMIT_BYPASS)
            qdisc_qstats_drop(sch);
        __qdisc_drop(skb, to_free);
        return ret;
    }

    /* 2. leaf qdisc로 재귀적 enqueue */
    ret = qdisc_enqueue(skb, cl->leaf.q, to_free);
    if (unlikely(ret != NET_XMIT_SUCCESS)) {
        if (net_xmit_drop_count(ret)) {
            qdisc_qstats_drop(sch);
            cl->drops++;
        }
        return ret;
    }

    /* 3. class를 활성 트리에 추가 (dequeue 대상) */
    htb_activate(q, cl);
    sch->q.qlen++;
    return NET_XMIT_SUCCESS;
}

dequeue()NET_TX softirq__qdisc_run()에서 호출됩니다. classful qdisc에서는 우선순위/토큰 상태에 따라 적절한 class를 선택하고, 해당 class의 leaf qdisc에서 패킷을 꺼냅니다.

/* net/sched/sch_generic.c — dequeue 루프 핵심 */
void __qdisc_run(struct Qdisc *q)
{
    int quota = dev_tx_weight;
    int packets;

    while (qdisc_restart(q, &packets)) {
        quota -= packets;
        if (quota <= 0) {
            /* 할당량 소진 → softirq 양보, 나중에 재스케줄 */
            __netif_schedule(q);
            break;
        }
    }
}

/* qdisc_restart는 dequeue → validate → xmit을 한 번 수행 */
static inline bool qdisc_restart(struct Qdisc *q, int *packets)
{
    struct sk_buff *skb = dequeue_skb(q, packets);
    if (unlikely(!skb))
        return false;
    return sch_direct_xmit(skb, q, ...);
}

Bulk Dequeue와 GSO 세그먼트

커널 4.16+에서 도입된 bulk dequeue는 한 번의 dequeue 호출에서 여러 패킷을 꺼내 sk_buff 리스트로 반환합니다. 이는 per-packet lock overhead를 줄이고 드라이버의 ndo_start_xmit() 배치 호출을 가능하게 합니다. 특히 GSO 세그먼트가 qdisc에서 분리될 때, 개별 세그먼트를 하나씩 처리하는 대신 리스트 단위로 전달합니다.

/* GSO skb가 qdisc에 들어올 때의 처리 */
/* fq_codel 등 일부 qdisc는 GSO skb를 세그먼트 단위로 분리하여 */
/* 개별 플로우 큐에 넣는다 → 공정성 향상, 하지만 CPU 비용 증가 */

/* CAKE는 split-gso / no-split-gso 옵션으로 이를 제어 */
/* 저속 링크: split-gso (정확한 shaping)  */
/* 고속 링크: no-split-gso (CPU 절약)     */

Qdisc 트리 구조와 탐색

Qdisc 계층 구조: root → class → leaf qdisc 재귀 트리 Root Qdisc (HTB 1:) Filter Chain flower / u32 / bpf Class 1:1 (rate 50M) Class 1:2 (rate 30M) Class 1:3 (rate 20M) Leaf: fq_codel (10:) Class 1:21 (15M) Class 1:22 (15M) Leaf: fq_codel (21:) Leaf: sfq (22:) Leaf: pfifo (30:) Qdisc 트리 규칙 1. root qdisc는 디바이스당 하나. handle MAJOR:0 형태. 2. classful qdisc 내부에 class가 있고, 각 class는 자체 leaf qdisc를 가질 수 있습니다. 3. leaf qdisc가 없는 class에는 커널이 기본 qdisc(pfifo_fast/fq_codel)를 자동 부착합니다.
Qdisc 트리는 재귀적 구조: root qdisc → class → leaf qdisc, leaf가 다시 classful이면 하위 class를 가질 수 있습니다

커널은 enqueue 시 트리를 top-down으로 탐색(classify → leaf 도달)하고, dequeue 시에는 bottom-up으로 leaf에서 패킷을 꺼내 root까지 올립니다. 이 비대칭 구조가 TC의 핵심입니다.

HTB: 토큰 버킷과 클래스 계층

HTB(Hierarchical Token Bucket)는 리눅스 TC에서 가장 많이 사용되는 classful qdisc입니다. 이 절에서는 이중 토큰 버킷(rate/ceil), quantum과 DRR 기반 형제 스케줄링, burst/cburst 계산, HTB 내부 레벨과 Red-Black 트리를 상세히 분석합니다.

이중 토큰 버킷: rate와 ceil

HTB class는 두 개의 독립적인 토큰 버킷을 유지합니다. tokens는 rate 버킷, ctokens는 ceil 버킷입니다. 이 두 값의 부호 조합이 class의 전송 모드를 결정합니다.

HTB 이중 토큰 버킷 알고리즘 Rate Token Bucket 충전 속도: rate (보장 대역폭) 버킷 깊이: buffer (burst) tokens ← rate 충전 ← pkt_len 소비 Ceil Token Bucket 충전 속도: ceil (최대 대역폭) 버킷 깊이: cbuffer (cburst) ctokens ← ceil 충전 ← pkt_len 소비 전송 모드 결정 (tokens, ctokens 부호 조합) HTB_CAN_SEND tokens > 0 && ctokens > 0 → 즉시 전송 (자체 rate) HTB_MAY_BORROW tokens ≤ 0 && ctokens > 0 → 부모에서 빌려 전송 HTB_CANT_SEND ctokens ≤ 0 → 전송 불가 (대기) rate < ceil이면 rate 소진 후에도 ceil 범위까지 부모 토큰을 빌려 전송할 수 있다 rate = ceil이면 HTB_MAY_BORROW 상태가 발생하지 않는다 (빌리기 불가)
HTB의 이중 토큰 버킷: rate 버킷(보장)과 ceil 버킷(최대)의 조합으로 3가지 전송 모드를 결정

quantum과 형제 클래스 스케줄링

같은 부모 아래에서 여러 class가 HTB_MAY_BORROW 상태이면, 어떤 class에게 먼저 보낼 기회를 줄 것인가? HTB는 Deficit Round Robin(DRR) 알고리즘을 사용합니다. 각 class의 quantum(한 라운드에서 보낼 수 있는 최대 바이트)이 이 결정의 핵심입니다.

# quantum 자동 계산: rate / r2q
# 기본 r2q=10, rate=10mbit → quantum = 10000000/8/10 = 125000 bytes
# rate=100kbit → quantum = 100000/8/10 = 1250 bytes (MTU 이하!)

# quantum이 MTU 이하면 경고 발생, 직접 지정 필요
tc class add dev eth0 parent 1:1 classid 1:10 htb \
    rate 100kbit ceil 1mbit quantum 1514

# 또는 r2q를 낮춰 전체 자동 quantum을 키움
tc qdisc add dev eth0 root handle 1: htb default 30 r2q 1
quantum too small 경고: quantum < MTU이면 한 라운드에 패킷 하나도 못 보내는 상황이 생겨 스케줄링이 비효율적입니다. 저속 class가 많은 환경에서는 r2q를 낮추거나 class별 quantum을 직접 지정하세요.

burst/cburst 계산

burst(또는 buffer)는 rate 토큰 버킷의 깊이이고, cburst(또는 cbuffer)는 ceil 토큰 버킷의 깊이입니다. 이 값이 클수록 순간 버스(Bus)트를 많이 허용하지만, 지연도 증가합니다.

# burst 계산 공식 (기본값)
# burst = rate / HZ (커널 타이머 주파수)
# HZ=250일 때: rate=100mbit → burst = 100000000/8/250 = 50000 bytes
# HZ=1000일 때: rate=100mbit → burst = 100000000/8/1000 = 12500 bytes

# burst 직접 지정 (latency-sensitive 환경)
tc class add dev eth0 parent 1:1 classid 1:10 htb \
    rate 100mbit ceil 200mbit \
    burst 15k cburst 15k

# burst가 너무 작으면 처리량이 rate에 못 미침
# burst가 너무 크면 순간적으로 과도한 트래픽 전송
# 일반적으로 MTU의 10~30배가 적절

HTB 내부: 레벨별 Red-Black 트리

HTB는 내부적으로 8개 레벨(0~7)의 Red-Black 트리를 유지합니다. 각 레벨에는 해당 깊이의 class 중 활성 상태인 것들이 들어있습니다. dequeue 시 가장 낮은 레벨(leaf)부터 탐색하여 전송 가능한 class를 찾습니다.

/* net/sched/sch_htb.c — HTB 스케줄러 구조 (간략화) */
struct htb_sched {
    struct Qdisc_class_hash clhash;
    int                     defcls;     /* default class ID */
    int                     rate2quantum; /* r2q */

    /* 각 TC 우선순위(0~7)에 대해 레벨별 활성 class 트리 */
    struct rb_root          hlevel[TC_HTB_MAXDEPTH];
    struct rb_root          wait_pq[TC_HTB_MAXDEPTH];

    /* 가장 가까운 이벤트 시각 (토큰 충전 완료 예정) */
    s64                     near_ev_cache[TC_HTB_MAXDEPTH];
    ...
};

/* dequeue 경로: 우선순위별로 CAN_SEND 또는 MAY_BORROW class를 찾는다 */
/* CAN_SEND class가 있으면 즉시 전송 */
/* 없으면 MAY_BORROW를 찾아 부모 토큰을 빌려 전송 */
/* 둘 다 없으면 가장 가까운 토큰 충전 시각에 타이머 설정 */

fq_codel: 공정 큐잉과 CoDel AQM

fq_codelStochastic Fair Queuing(SFQ) 계열의 플로우 해싱과 CoDel(Controlled Delay) AQM을 결합한 qdisc입니다. 현대 리눅스의 기본 qdisc로, 버퍼블로트 문제를 효과적으로 해결합니다.

플로우 해시와 DRR 스케줄링

패킷이 도착하면 5-tuple(src IP, dst IP, src port, dst port, protocol) 해시를 계산하여 flows_cnt(기본 1024)개의 플로우 큐 중 하나에 배치합니다. 처음 보는 플로우는 new_flows 리스트에, 이미 패킷이 있는 플로우는 old_flows 리스트에 들어갑니다.

fq_codel: 플로우 해시 + DRR 스케줄링 + CoDel AQM 수신 패킷 Flow Hash 5-tuple + perturbation Flow Queues (1024) flow[0]: TCP 10.0.0.1:80 flow[1]: UDP 10.0.0.2:53 flow[2]: TCP 10.0.0.3:443 ... flow[1023] new_flows 리스트 (우선 dequeue) 처음 도착한 플로우 → 여기에 추가 quantum 소진 시 old_flows로 이동 짧은 플로우에 우선권 → 인터랙티브 유리 old_flows 리스트 new_flows에서 quantum 소진된 플로우 DRR로 공정하게 라운드 로빈 대용량 전송(bulk) 플로우는 여기서 처리 CoDel AQM (플로우별 적용) interval(100ms) 동안 최소 sojourn time이 target(5ms) 초과하면 드롭 시작 드롭 간격: 1/√count (연속 드롭 시 간격이 좁아짐) ECN 활성 시 드롭 대신 CE 마킹 (ecn 옵션, 기본 켜짐) dequeue → xmit
fq_codel의 핵심: 플로우 해시로 큐 분리 → new/old 리스트로 짧은 플로우 우대 → 각 플로우에 CoDel AQM 적용

CoDel 알고리즘 상세

CoDel은 패킷 체류 시간(sojourn time)을 측정하여 큐가 과도하게 차있는지 판단합니다. 단순히 큐 길이를 보는 것이 아니라, 패킷이 큐에서 보낸 시간이 target(기본 5ms)을 초과하는 상태가 interval(기본 100ms) 동안 지속되면 드롭을 시작합니다.

/* include/net/codel.h — CoDel 핵심 파라미터 */
struct codel_params {
    codel_time_t  target;     /* 목표 sojourn time (기본 5ms) */
    codel_time_t  interval;   /* 측정 간격 (기본 100ms) */
    codel_time_t  mtu_time;   /* MTU를 전송하는 데 걸리는 시간 */
    u32           ce_threshold; /* CE 마킹 임계값 */
    bool          ecn;        /* ECN 마킹 활성화 */
};

/* CoDel dequeue 판정 의사 코드 */
/*  1. dequeue하면서 sojourn_time = now - enqueue_time 계산    */
/*  2. sojourn_time < target이면 → dropping = false, 통과      */
/*  3. sojourn_time >= target이고 interval 동안 계속이면 →      */
/*     drop 시작, count++                                       */
/*  4. 다음 드롭 시각 = first_above_time + interval/√count      */
/*  5. 큐가 비거나 sojourn < target이면 dropping 해제           */
fq_codel의 강점: 순수 CoDel은 단일 큐에서 동작하므로 한 플로우가 큐를 독점하면 다른 플로우도 함께 느려집니다. fq_codel은 플로우별 큐를 만들어 이 문제를 해결하고, 추가로 new flow 우선순위로 짧은 인터랙티브 플로우(DNS, SSH)가 즉시 서비스되도록 합니다.

CAKE: 올인원 AQM/Shaping

CAKE(Common Applications Kept Enhanced)는 HTB + fq_codel 조합을 단일 qdisc로 통합한 현대적 솔루션입니다. 이 절에서는 DiffServ tin, NAT-aware 플로우 격리(Isolation), ACK 필터, 오버헤드 보정을 상세히 분석합니다.

DiffServ 모드와 tin 구조

CAKE는 패킷을 DSCP 값에 따라 tin(우선순위 버킷)으로 분류합니다. 각 tin은 독립적인 AQM과 shaping을 수행합니다.

모드tin 수분류 기준용도
besteffort1없음 (전부 동일)단순한 shaping + AQM
diffserv33Bulk / Best Effort / Voice일반적인 가정용
diffserv44Bulk / BE / Video / Voice권장 기본값
diffserv88RFC 4594 표준 매핑(Mapping)엔터프라이즈
precedence8IP Precedence 비트레거시 호환

NAT-aware 플로우 격리

CAKE의 nat 옵션은 conntrack에서 NAT 전 원본 주소를 추출하여 플로우 해시에 사용합니다. 이를 통해 NAT 뒤의 개별 호스트를 구분하여 공정한 대역폭 분배가 가능합니다.

# 플로우 격리 모드
# flowblind  — 플로우 구분 없음 (FIFO에 가까움)
# srchost    — 소스 IP 기준 공정성
# dsthost    — 목적지 IP 기준 공정성
# hosts      — src+dst 양방향 공정성
# flows      — 5-tuple 플로우별 공정성
# dual-srchost / dual-dsthost — 호스트 + 플로우 2단계
# triple-isolate — 호스트 + 플로우 + 내부 우선순위 (기본값)

tc qdisc replace dev eth0 root cake \
    bandwidth 100mbit diffserv4 nat triple-isolate

# NAT-aware: NAT 뒤 10대의 PC가 각각 ~10Mbps씩 공정하게 분배
# nat 없이는 NAT 후 IP가 같아서 모두 하나의 호스트로 취급

ACK 필터링

비대칭 링크(다운 100M / 업 10M)에서 TCP ACK가 uplink를 채우면 다운링크도 느려집니다. CAKE의 ack-filter는 동일 플로우의 중복 ACK를 감지하여 이전 것을 드롭하고 최신 ACK만 유지합니다.

# ACK 필터 활성화 (기본: ack-filter-aggressive 아닌 ack-filter)
tc qdisc replace dev pppoe-wan root cake \
    bandwidth 10mbit diffserv4 nat ack-filter

# 공격적 모드: 더 많은 ACK를 제거 (약간의 재전송 위험)
tc qdisc replace dev pppoe-wan root cake \
    bandwidth 10mbit diffserv4 nat ack-filter-aggressive

# 비활성화
tc qdisc replace dev pppoe-wan root cake \
    bandwidth 10mbit diffserv4 nat no-ack-filter

링크 오버헤드 보정

shaping 정확도를 위해 CAKE는 실제 링크 계층 오버헤드를 보정합니다. 이더넷, PPPoE, ATM, DOCSIS 등 링크 타입에 따라 패킷 크기 계산을 조정합니다.

# PPPoE + VLAN (일반적인 한국 가정용 인터넷)
tc qdisc replace dev pppoe-wan root cake \
    bandwidth 95mbit overhead 18 mpu 64 diffserv4 nat

# ATM 셀 기반 (구형 ADSL)
tc qdisc replace dev ppp0 root cake \
    bandwidth 20mbit atm overhead 44

# 기본 이더넷 (오버헤드 보정 불필요)
tc qdisc replace dev eth0 root cake \
    bandwidth 1gbit noatm raw

EDT (Earliest Departure Time)

EDT(Earliest Departure Time)는 패킷별로 "이 시각 이후에 전송하라"는 타임스탬프를 설정하는 방식입니다. Google이 제안하고 커널 4.20에 도입된 이 모델은 전통적인 큐 기반 shaping과 근본적으로 다릅니다.

fq qdisc와 EDT 연동

fq qdisc는 소켓이 설정한 pacing rate에 따라 패킷의 departure time을 계산합니다. TCP BBR, TCP CUBIC(fq 연동 시), BPF 프로그램 등이 EDT를 활용합니다.

/* include/linux/skbuff.h — EDT 타임스탬프 */
struct sk_buff {
    ...
    union {
        ktime_t  tstamp;       /* 수신 시각 또는 EDT */
    };
    ...
};

/* net/sched/sch_fq.c — fq dequeue에서 EDT 확인 */
/* 패킷의 tstamp가 현재 시각보다 미래이면 대기 */
/* 타이머로 해당 시각에 재스케줄 */

/* BPF에서 EDT 설정 */
/* skb->tstamp = departure_time; */
/* bpf_skb_set_tstamp(skb, time, BPF_SKB_TSTAMP_DELIVERY_MONO) */
# fq qdisc + TCP BBR: EDT 기반 pacing
tc qdisc replace dev eth0 root fq
sysctl -w net.ipv4.tcp_congestion_control=bbr

# SO_TXTIME 소켓 옵션으로 사용자 공간에서 직접 EDT 지정
# 이 경우 fq가 아닌 ETF(Earliest TxTime First) qdisc 사용
tc qdisc replace dev eth0 root etf \
    clockid CLOCK_TAI delta 200000 offload

# ETF + hardware timestamping: NIC 하드웨어가 정확한 시각에 전송
# TSN(Time-Sensitive Networking) 환경에서 사용
EDT vs 큐 기반: 전통적 shaping(HTB, TBF)은 "큐에 넣고 토큰이 차면 꺼낸다"는 모델이고, EDT는 "패킷에 시각을 찍고 그 시각에 보낸다"는 모델입니다. EDT는 큐 깊이를 최소화하여 지연을 줄이고, BPF와 결합하면 프로그래밍 가능한 pacing을 구현할 수 있습니다.

tc-bpf / tc-ebpf

TC BPF는 단순한 classifier를 넘어 프로그래밍 가능한 데이터플레인입니다. 이 절에서는 cls_bpf 내부 구현, act_bpf와의 차이, direct-action 모드의 최적화, XDP와의 처리 위치 차이를 상세히 분석합니다.

cls_bpf 내부 구현

/* net/sched/cls_bpf.c — cls_bpf classify 콜백 */
static int cls_bpf_classify(struct sk_buff *skb,
                             const struct tcf_proto *tp,
                             struct tcf_result *res)
{
    struct cls_bpf_head *head = rcu_dereference_bh(tp->root);
    struct cls_bpf_prog *prog;
    int ret = -1;

    list_for_each_entry_rcu(prog, &head->plist, link) {
        /* BPF 프로그램 실행 */
        int filter_res = bpf_prog_run(prog->filter, skb);

        if (prog->exts_integrated) {
            /* direct-action 모드: 반환값이 곧 TC_ACT_* */
            res->class = 0;
            res->classid = TC_H_MAJ(prog->res.classid) |
                           clamp(filter_res, 0, TC_ACT_VALUE_MAX);

            if (filter_res == TC_ACT_UNSPEC && prog->res.classid)
                res->classid = prog->res.classid;

            ret = tcf_exts_exec_ex(skb, &prog->exts,
                                    filter_res, res);
        } else {
            /* 전통 모드: 반환값이 classid */
            if (filter_res != 0) {
                res->classid = prog->res.classid;
                ret = tcf_exts_exec(skb, &prog->exts, res);
            }
            continue;
        }
        break;
    }
    return ret;
}

TC BPF vs XDP 처리 위치

TC BPF와 XDP 처리 위치 비교 NIC DMA ring XDP Hook driver NAPI poll struct xdp_md skb 할당 napi_gro_receive TC Ingress Hook clsact ingress struct __sk_buff IP Stack IP Stack TC Egress Hook clsact egress struct __sk_buff Qdisc enqueue/dequeue ndo_start_xmit XDP 특징 skb 생성 전, 최고 성능 ingress only, L2/L3 수정 conntrack/tunnel 불가 TC BPF 특징 sk_buff 접근, 풍부한 헬퍼 ingress + egress 모두 지원 conntrack, tunnel, csum 지원
XDP는 드라이버 레벨에서 sk_buff 생성 전에 동작하여 최고 성능, TC BPF는 sk_buff 위에서 동작하여 풍부한 기능 제공

TC BPF 주요 헬퍼 함수

헬퍼기능용도
bpf_skb_store_bytes()패킷 데이터 수정헤더 필드 변경, 캡슐화(Encapsulation)
bpf_skb_load_bytes()패킷 데이터 읽기deep packet inspection
bpf_redirect()패킷 리다이렉트다른 인터페이스로 전달
bpf_redirect_neigh()이웃 lookup 포함 리다이렉트L3 포워딩
bpf_skb_set_tunnel_key()터널 키 설정VXLAN/Geneve 캡슐화
bpf_skb_get_tunnel_key()터널 키 조회터널 디캡슐화 후 메타
bpf_ct_lookup()conntrack 조회상태 기반 필터링
bpf_skb_cgroup_id()cgroup ID 조회cgroup 기반 정책
bpf_skb_change_head()headroom 확장헤더 추가 (캡슐화)
bpf_skb_change_tail()tailroom 변경패킷 크기 조정
bpf_skb_set_tstamp()EDT 타임스탬프 설정BPF 기반 pacing
bpf_fib_lookup()FIB 테이블 조회라우팅 결정
/* TC BPF: VXLAN 캡슐화 + 리다이렉트 예시 */
SEC("tc")
int tc_vxlan_encap(struct __sk_buff *skb)
{
    struct bpf_tunnel_key key = {};
    key.remote_ipv4 = bpf_htonl(0x0a000001);  /* 10.0.0.1 */
    key.tunnel_id = 100;                       /* VNI 100 */
    key.tunnel_ttl = 64;

    bpf_skb_set_tunnel_key(skb, &key, sizeof(key), 0);
    return bpf_redirect(ifindex_vxlan, 0);
}

하드웨어 오프로드

TC flower offload는 소프트웨어 TC 규칙을 NIC 하드웨어로 내려보내 CPU 없이 패킷을 처리합니다. 이 절에서는 드라이버 콜백 체인, switchdev 아키텍처, CT(conntrack) offload를 상세히 분석합니다.

오프로드 콜백 경로

TC Flower 하드웨어 오프로드 콜백 경로 tc filter add flower skip_sw ... Netlink API RTM_NEWTFILTER cls_flower fl_change() fl_hw_replace_filter() TC_SETUP_CLSFLOWER ndo_setup_tc() 드라이버 콜백 (mlx5, ice, bnxt ...) FW/HW 규칙 설치 eSwitch flow table 실패 시 skip_sw면 에러 반환 오프로드 제어 플래그 skip_sw: HW 전용 (SW 실행 안 함) skip_hw: SW 전용 (HW 오프로드 안 함) (둘 다 없으면: SW + HW 모두 실행) 확인 방법 in_hw: HW에 설치됨 in_hw_count: HW 수 not_in_hw: SW만
TC flower 규칙이 HW에 설치되는 경로: cls_flower → fl_hw_replace_filter() → ndo_setup_tc() → 드라이버/FW

CT(conntrack) Offload

커널 5.8+에서 도입된 CT offload는 act_ct의 conntrack 상태 추적을 하드웨어로 내립니다. 첫 패킷(NEW)은 소프트웨어에서 처리하고, connection이 ESTABLISHED되면 이후 패킷은 NIC 하드웨어가 직접 포워딩합니다.

# CT offload 설정 예시: VF 간 conntrack 기반 포워딩

# 1. NEW 상태: conntrack 조회 → commit → redirect
tc filter add dev rep0 ingress protocol ip flower \
    ct_state -trk \
    action ct zone 1 \
    action goto chain 1

# 2. ESTABLISHED: HW에서 직접 포워딩
tc filter add dev rep0 ingress chain 1 protocol ip flower \
    ct_state +trk+est \
    action ct zone 1 \
    action mirred egress redirect dev rep1

# 3. 확인: HW offload 상태
tc -s filter show dev rep0 ingress
# ... in_hw in_hw_count 1 ...

# conntrack 테이블에서 offload 상태 확인
conntrack -L | grep OFFLOAD
CT offload 제한: 모든 conntrack 기능이 오프로드 가능한 것은 아닙니다. NAT, zone, mark 등은 드라이버/펌웨어 지원에 따라 다릅니다. ct_state +rel(RELATED)은 대부분 HW에서 지원하지 않으므로 소프트웨어 폴백이 필요합니다.

mqprio/taprio: TSN과 시간 게이트

TSN(Time-Sensitive Networking)은 이더넷에서 실시간 트래픽 보장을 위한 IEEE 802.1 표준 집합입니다. taprio qdisc는 802.1Qbv 시간 게이트(Time-Aware Shaper)를 구현하여, 특정 시간 슬롯에만 특정 traffic class의 전송을 허용합니다.

taprio 시간 게이트 스케줄링

taprio: 802.1Qbv 시간 게이트 스케줄링 시간 → 0 200us 300us 1000us (cycle) TC0 (RT) OPEN (200us) CLOSED TC1 (Video) CLOSED OPEN (100us) CLOSED TC2 (BE) CLOSED OPEN (700us) 802.1Qbv Gate Control List (GCL) entry 0: gate_mask=0b001(TC0), duration=200us → 실시간 전용 전송 구간 entry 1: gate_mask=0b010(TC1), duration=100us → 비디오 전용 전송 구간 entry 2: gate_mask=0b100(TC2), duration=700us → Best Effort 전송 구간 cycle-time=1000us → 1ms 주기로 3개 TC가 시분할 전송 (실시간 트래픽 간섭 차단)
taprio의 시간 게이트: 1ms 주기 안에서 TC0(RT)→TC1(Video)→TC2(BE) 순으로 전용 전송 구간을 할당
# taprio 설정: 802.1Qbv 시간 게이트
tc qdisc replace dev eth0 parent root handle 100 taprio \
    num_tc 3 \
    map 2 2 1 0 2 2 2 2 2 2 2 2 2 2 2 2 \
    queues 1@0 1@1 2@2 \
    base-time 0 \
    sched-entry S 01 200000 \
    sched-entry S 02 100000 \
    sched-entry S 04 700000 \
    clockid CLOCK_TAI

# sched-entry 형식: S <gate_mask_hex> <duration_ns>
# gate_mask: bit 0=TC0, bit 1=TC1, bit 2=TC2
# S 01 = TC0만 open, S 02 = TC1만 open, S 04 = TC2만 open

# HW offload 활성화 (NIC 지원 시)
tc qdisc replace dev eth0 parent root handle 100 taprio \
    num_tc 3 \
    map 2 2 1 0 2 2 2 2 2 2 2 2 2 2 2 2 \
    queues 1@0 1@1 2@2 \
    base-time 0 \
    sched-entry S 01 200000 \
    sched-entry S 02 100000 \
    sched-entry S 04 700000 \
    flags 0x2 \
    clockid CLOCK_TAI

# flags 0x2 = TAPRIO_FLAGS_FULL_OFFLOAD (완전 HW 오프로드)
# flags 0x1 = TAPRIO_FLAGS_TXTIME_ASSIST (SW 시간 계산, HW 전송)

ETF(Earliest TxTime First)와 LaunchTime

ETF qdisc는 taprio의 leaf로 부착되어, 각 패킷에 정확한 전송 시각을 지정합니다. 하드웨어 LaunchTime을 지원하는 NIC에서는 패킷이 NIC 내부 큐에서 정확한 시각에 전송됩니다.

# taprio + ETF 조합: 실시간 TC0에 정밀 시각 전송
tc qdisc replace dev eth0 parent 100:1 etf \
    clockid CLOCK_TAI delta 200000 offload

# delta: 스케줄링에서 실제 전송까지의 여유 시간 (ns)
# offload: NIC 하드웨어 LaunchTime 사용

# 사용자 공간에서 SO_TXTIME으로 전송 시각 지정
# struct sock_txtime st;
# st.clockid = CLOCK_TAI;
# st.flags = SOF_TXTIME_DEADLINE_MODE;
# setsockopt(fd, SOL_SOCKET, SO_TXTIME, &st, sizeof(st));

ingress/clsact: TC_ACT 반환값과 패킷 흐름

clsact의 ingress/egress hook은 커널의 sch_handle_ingress()sch_handle_egress()에서 호출됩니다. 이 절에서는 TC_ACT_* 반환값의 정확한 의미, 리다이렉트/미러링 내부, policing과의 상호작용을 상세히 분석합니다.

clsact ingress 패킷 처리 흐름과 TC_ACT 반환값 NIC RX sch_handle_ingress() filter chain 순회 TC_ACT_OK (0) → IP Stack TC_ACT_SHOT (2) → kfree_skb TC_ACT_REDIRECT (7) 다른 NIC egress IFB 디바이스 veth peer TC_ACT_STOLEN (4) BPF가 소유권 가져감 TC_ACT_PIPE (3) 다음 action으로 전달 mirred action의 두 가지 모드 mirror: 원본 패킷은 그대로 진행하고, 복사본을 대상 디바이스로 전송 (패킷 분석/모니터링) redirect: 원본 패킷 자체를 대상 디바이스로 전달하고, 원래 경로 진행을 중단 (IFB, 포워딩) egress hook (sch_handle_egress)도 동일한 TC_ACT_* 반환값을 사용하며, qdisc enqueue 전에 실행됩니다
clsact ingress에서 filter chain 실행 후 TC_ACT_* 반환값에 따라 패킷의 운명이 결정됩니다

Ingress Policing 상세

ingress에서의 policing은 토큰 버킷으로 구현됩니다. conform-exceed 파라미터로 적합/초과 패킷의 처리를 세밀하게 제어할 수 있습니다.

conform-exceed적합 패킷초과 패킷용도
droppassdrop단순 rate limit
pass/droppassdrop명시적 2-color
pass/pipepass다음 action으로초과 시 DSCP 마킹
drop/continuedropcontinue역방향 (초과만 통과)
ok/reclassifypass재분류초과 시 다른 class로

IFB (Intermediate Functional Block)

IFB는 가상 네트워크 디바이스로, ingress 패킷을 IFB의 egress로 리다이렉트하여 큐잉 가능한 위치에 놓습니다. 이 절에서는 IFB의 내부 동작과 실전 구성 패턴을 상세히 분석합니다.

IFB 패킷 리다이렉트 경로: ingress → IFB egress → netif_rx 복귀 1. WAN NIC (eth0) clsact ingress hook matchall → mirred redirect 2. act_mirred skb→dev = ifb0 dev_queue_xmit(skb) 3. IFB egress qdisc CAKE / HTB / TBF 여기서 실제 shaping! enqueue → delay → dequeue 4. ifb_xmit() skb→dev = eth0 netif_rx(skb) 5. IP Stack (정상 수신) IFB 내부 동작 핵심 1. mirred가 skb->dev를 ifb0으로 변경하고 dev_queue_xmit() 호출 2. ifb0의 root qdisc(CAKE/HTB)가 패킷을 enqueue → 토큰/AQM에 따라 지연 3. dequeue 시 ifb_xmit()가 호출되어 skb->dev를 원래 디바이스(eth0)로 복원 4. netif_rx()로 패킷을 다시 수신 경로에 주입 → 정상 IP 스택 처리 진행 핵심: ingress 패킷이 IFB egress에서 '큐잉 가능한 위치'를 경유한 뒤 원래 수신 경로로 복귀
IFB는 ingress 패킷을 가상 egress로 우회시켜 shaping 후 원래 수신 경로로 복귀시키는 가상 디바이스

IFB 실전 구성 패턴

# ━━━ 패턴 1: 다운링크 CAKE shaping (가장 흔한 용도) ━━━
modprobe ifb
ip link add ifb-down type ifb
ip link set ifb-down up

tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress matchall \
    action mirred egress redirect dev ifb-down

tc qdisc replace dev ifb-down root cake \
    bandwidth 200mbit ingress diffserv4 nat wash

# ━━━ 패턴 2: 선택적 리다이렉트 (특정 트래픽만 IFB) ━━━
tc filter add dev eth0 ingress protocol ip prio 10 \
    flower ip_proto tcp dst_port 80 \
    action mirred egress redirect dev ifb-down

tc filter add dev eth0 ingress protocol ip prio 20 \
    flower ip_proto tcp dst_port 443 \
    action mirred egress redirect dev ifb-down

# 나머지 트래픽은 IFB 없이 바로 IP 스택으로

# ━━━ 패턴 3: 양방향 shaping (WAN uplink + downlink) ━━━
# uplink: eth0 egress에 직접 cake
tc qdisc replace dev eth0 root cake \
    bandwidth 50mbit diffserv4 nat

# downlink: eth0 ingress → IFB → cake
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress matchall \
    action mirred egress redirect dev ifb-down
tc qdisc replace dev ifb-down root cake \
    bandwidth 200mbit ingress diffserv4 nat

디버깅 & 모니터링

TC 설정이 기대대로 동작하지 않을 때, 체계적인 디버깅 방법이 필요합니다. 이 절에서는 tc monitor, ftrace, BPF 트레이싱, perf를 활용한 심층 디버깅을 다룹니다.

tc monitor를 이용한 실시간 추적

tc monitor는 RTNETLINK를 감시하여 qdisc/class/filter/action의 추가/삭제/변경을 실시간으로 보여줍니다. 여러 자동화 도구(CNI, cloud-init, NetworkManager)가 TC 규칙을 동적으로 조작하는 환경에서 필수입니다.

# 모든 TC 이벤트 실시간 감시
tc monitor

# 특정 이벤트만 필터
tc monitor qdisc     # qdisc 변경만
tc monitor filter    # filter 변경만
tc monitor action    # action 변경만

# 다른 터미널에서 규칙 추가하면 monitor에 즉시 표시
# added qdisc htb 1: dev eth0 root ...
# added class htb 1:10 dev eth0 parent 1: ...

ftrace로 Qdisc 내부 추적

# TC 관련 커널 함수 추적
echo 1 > /sys/kernel/debug/tracing/events/qdisc/enable
cat /sys/kernel/debug/tracing/trace_pipe

# 특정 함수 추적 (function_graph)
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo htb_enqueue > /sys/kernel/debug/tracing/set_graph_function
echo htb_dequeue >> /sys/kernel/debug/tracing/set_graph_function
cat /sys/kernel/debug/tracing/trace_pipe

# net:net_dev_queue tracepoint — qdisc enqueue 시점
echo 1 > /sys/kernel/debug/tracing/events/net/net_dev_queue/enable

# net:net_dev_xmit tracepoint — 실제 드라이버 xmit 시점
echo 1 > /sys/kernel/debug/tracing/events/net/net_dev_xmit/enable

# 두 시점의 시간 차이 = qdisc에서 보낸 시간

BPF 트레이싱으로 TC 분석

# bpftrace: qdisc enqueue/dequeue 지연 측정
bpftrace -e '
kprobe:htb_enqueue {
    @start[arg0] = nsecs;
}
kprobe:htb_dequeue /@ start[arg0]/ {
    @latency = hist(nsecs - @start[arg0]);
    delete(@start[arg0]);
}'

# bpftrace: 드롭된 패킷 추적
bpftrace -e '
tracepoint:skb:kfree_skb {
    @drops[kstack] = count();
}'

# tc-bpf 프로그램 자체의 실행 시간 측정
bpftrace -e '
kprobe:cls_bpf_classify {
    @start[tid] = nsecs;
}
kretprobe:cls_bpf_classify /@start[tid]/ {
    @classify_ns = hist(nsecs - @start[tid]);
    delete(@start[tid]);
}'

# perf로 TC 관련 CPU 프로파일링
perf record -g -a -e net:net_dev_xmit -- sleep 10
perf report

TC 트러블슈팅 체크리스트

증상확인 사항명령
트래픽이 분류되지 않음 filter prio 순서, protocol 지정, default class tc -s filter show dev eth0
shaping이 안 되는 것 같음 backlog=0이면 qdisc가 병목이 아님, BQL/드라이버 확인 tc -s qdisc show, ethtool -S
지연이 줄지 않음 BQL limit, 드라이버 TX ring, TCP 출력 버퍼 cat /sys/class/net/*/queues/tx-*/byte_queue_limits/*
HW offload가 안 됨 hw-tc-offload 활성화, skip_sw/in_hw 확인 ethtool -k eth0, tc -s filter show
IFB 패킷이 사라짐 ifb 디바이스 UP 상태, mirred redirect 확인 ip link show ifb0, tc -s filter show ingress
"quantum too small" 경고 r2q 조정 또는 quantum 직접 지정 dmesg | grep quantum
taprio 시간이 안 맞음 PTP 동기화, clockid, base-time phc2sys -s eth0 -c CLOCK_REALTIME
BPF 프로그램이 안 붙음 clsact qdisc 존재, 프로그램 검증 에러 tc qdisc show dev eth0, bpftool prog

TC 관련 커널 설정 (Kconfig)

옵션설명필수 여부
CONFIG_NET_SCHEDTC 프레임워크 전체 활성화필수
CONFIG_NET_SCH_HTBHTB qdisc일반적으로 필요
CONFIG_NET_SCH_FQ_CODELfq_codel qdisc (기본 qdisc)거의 필수
CONFIG_NET_SCH_CAKECAKE qdiscshaping 시
CONFIG_NET_SCH_TAPRIOtaprio qdisc (TSN)TSN 환경
CONFIG_NET_SCH_ETFETF qdisc (LaunchTime)TSN 환경
CONFIG_NET_SCH_INGRESSingress/clsact qdiscingress 제어 시
CONFIG_NET_SCH_MQPRIOmqprio qdisc멀티큐 환경
CONFIG_IFBIFB 가상 디바이스ingress shaping 시
CONFIG_NET_CLS_FLOWERflower classifier현대 설정에 필수
CONFIG_NET_CLS_BPFBPF classifier (tc-bpf)BPF 사용 시
CONFIG_NET_CLS_U32u32 classifier레거시 호환
CONFIG_NET_ACT_MIRREDmirred actionredirect/mirror 시
CONFIG_NET_ACT_CTconntrack actionCT offload 시
CONFIG_NET_ACT_POLICEpolice actionpolicing 시
CONFIG_NET_ACT_BPFBPF actionact_bpf 사용 시

참고자료

이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.