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 하드웨어 오프로드, 대역폭 제한·큐 지연·버퍼블로트 문제를 계측하고 조정하는 운영 튜닝 절차까지 상세히 다룹니다.

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

핵심 요약

  • 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, 드라이버 큐 통계를 함께 보고 실제로 지연과 처리량이 원하는 방향으로 갔는지 확인합니다.
관련 표준: RFC 2474 (DiffServ), RFC 2697 (srTCM), RFC 2698 (trTCM), RFC 3168 (ECN), RFC 8290 (FQ-CoDel) — TC는 이 표준들을 기반으로 QoS와 대역폭 제어를 수행합니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

TC 개요

TC(Traffic Control)는 리눅스 커널의 패킷 스케줄링 및 트래픽 제어 프레임워크입니다. 네트워크 인터페이스에서 송수신되는 패킷의 대역폭 제한, 우선순위 지정, 지연 시뮬레이션, 패킷 드롭 정책 등을 제어할 수 있습니다. 커널의 net/sched/ 디렉터리에 구현되어 있으며, 사용자 공간에서는 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대 구성 요소

구성 요소역할커널 구조체
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, 링크 오버헤드 보정을 한 덩어리로 묶어 제공합니다. 즉, HTB + fq_codel + overhead tuning + host fairness를 자주 같이 쓰던 환경에서 설정 실수를 줄이기 좋은 qdisc입니다.

항목fq_codelCAKE
주요 목적 버퍼블로트 완화와 flow fairness 단일 병목 링크 shaping + fairness + DiffServ 통합
대역폭 제한 직접 shaping 기능 없음 bandwidth로 직접 shaping
호스트 공정성 기본 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 조합이 더 직접적입니다. 반대로 홈 라우터, 지점 라우터, 터널 종단처럼 한 곳의 실제 병목를 다듬는 용도라면 CAKE가 더 단순하고 실수가 적습니다.

FQ (Fair Queue)와 TCP pacing

fq qdisc는 지연 제어보다 소켓 pacing흐름별 공정 전송에 초점을 둡니다. 특히 커널 TCP stack이 SO_MAX_PACING_RATE 또는 내부 pacing rate를 계산해 내릴 때, fq가 이를 부드럽게 직렬화합니다. 따라서 로컬 호스트가 직접 송신하는 고속 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는 해시 기반 공정 큐잉입니다. 플로우 해시를 사용하여 패킷을 여러 큐에 분배하고, 라운드 로빈으로 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는 고정 우선순위 스케줄러입니다. 여러 밴드(기본 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로, 패킷 헤더의 임의 오프셋에서 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체크섬 재계산
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 프로그래밍의 표준 진입점입니다.

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 재전송과 지터 증가 가능
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 연쇄는 디버깅과 성능 모두를 빠르게 악화시킵니다.

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 각각이 독립적인 커널 모듈로 구현됩니다.

Qdisc_ops 구조체

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

/* 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'

변경 감시와 트리 시각화

정적인 덤프만 보면 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() 오프로드는 드라이버/펌웨어 버전에 따라 지원 여부가 다릅니다
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

참고자료

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