TC (Traffic Control) 심화

Linux TC(Traffic Control) 심화: qdisc, class, filter, HTB, TBF, fq_codel, ingress, clsact, tc-bpf, 대역폭 제어 분석.

관련 표준: 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의 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;  /* 기존 플로우 리스트 */
};

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

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
/* 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 등)
# 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

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'

통계 필드 해석

필드의미
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)

하드웨어 오프로드

일부 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

성능 튜닝 팁

TC 성능 최적화 지침:
  • leaf qdisc 선택: HTB leaf에 fq_codel을 사용하여 버퍼블로트 방지
  • r2q 파라미터: HTB의 r2q (rate-to-quantum)은 자동 quantum 계산에 사용. 저대역폭 class에서 "quantum too small" 경고 시 r2q 조정 또는 quantum 직접 지정
  • burst 크기: TBF/police의 burst가 너무 작으면 처리량 저하. 최소 MTU 이상, 권장: rate * HZ / 8
  • filter 순서: 자주 매칭되는 filter를 높은 prio(낮은 숫자)로 배치
  • flower vs u32: 신규 설정에서는 flower 권장 (HW offload 지원, 직관적)
  • clsact + BPF: 복잡한 분류 로직은 BPF 프로그램으로 구현 (성능 + 유연성)
  • 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