TC (Traffic Control) 심화
Linux TC(Traffic Control) 심화: qdisc, class, filter, HTB, TBF, fq_codel, ingress, clsact, tc-bpf, 대역폭 제어 분석.
TC 개요
TC(Traffic Control)는 리눅스 커널의 패킷 스케줄링 및 트래픽 제어 프레임워크입니다. 네트워크 인터페이스에서 송수신되는 패킷의 대역폭 제한, 우선순위 지정, 지연 시뮬레이션, 패킷 드롭 정책 등을 제어할 수 있습니다. 커널의 net/sched/ 디렉터리에 구현되어 있으며, 사용자 공간에서는 tc 유틸리티(iproute2 패키지)를 통해 조작합니다.
TC의 핵심 목표는 QoS(Quality of Service)입니다. 제한된 네트워크 대역폭을 효율적으로 분배하고, 중요한 트래픽에 우선순위를 부여하며, 과도한 트래픽을 제어하여 네트워크 전체의 성능을 최적화합니다.
iptables -j CLASSIFY나 tc filter ... action connmark 등으로 연동됩니다.
TC의 4대 구성 요소
| 구성 요소 | 역할 | 커널 구조체 |
|---|---|---|
| qdisc (Queueing Discipline) | 패킷 큐잉/스케줄링 알고리즘 | struct Qdisc |
| class | classful qdisc 내부의 트래픽 분류 단위 | struct Qdisc_class_common |
| filter (classifier) | 패킷을 class로 분류하는 규칙 | struct tcf_proto |
| action | filter 매치 시 수행할 동작 | 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의 경우 계층적 트리 구조로 확장됩니다.
Handle과 ClassID
TC에서 모든 qdisc와 class는 고유한 식별자를 가집니다. 형식은 MAJOR:MINOR이며, 16비트씩 총 32비트입니다.
| 식별자 | 형식 | 설명 |
|---|---|---|
| handle | MAJOR:0 | qdisc 식별자. MINOR는 항상 0 |
| classid | MAJOR:MINOR | class 식별자. MAJOR는 부모 qdisc의 handle |
| root | - | 디바이스의 최상위 egress qdisc 부착점 |
| ingress | ffff:0 | ingress 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를 거치는 과정은 다음과 같습니다.
- enqueue:
dev_queue_xmit()에서 root qdisc의enqueue()호출 - classify: classful qdisc인 경우 filter를 통해 패킷이 속할 class 결정
- queue: 결정된 class(또는 leaf qdisc)의 큐에 패킷 삽입
- dequeue: NET_TX softirq에서
dequeue()를 호출하여 패킷 추출 - 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 Queuing과 CoDel(Controlled Delay) AQM 알고리즘을 결합합니다. 플로우별 공정 큐잉으로 단일 플로우의 독점을 방지하고, CoDel로 버퍼블로트(bufferbloat)를 해결합니다.
# 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 간에 공유됩니다.
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 | 모듈 | 기능 |
|---|---|---|
| gact | act_gact | pass, drop, continue, reclassify, pipe 등 기본 동작 |
| mirred | act_mirred | 패킷 미러링(mirror) 또는 리다이렉트(redirect) |
| police | act_police | 토큰 버킷 기반 속도 제한 (policing) |
| pedit | act_pedit | 패킷 헤더 필드 수정 (edit) |
| connmark | act_connmark | conntrack 마크를 skb 마크로 복사 |
| skbedit | act_skbedit | skb 메타데이터 수정 (priority, mark, queue_mapping) |
| ct | act_ct | conntrack 조회/커밋, zone 지정, NAT |
| csum | act_csum | 체크섬 재계산 |
| tunnel_key | act_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 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_OK | 0 | 패킷을 다음 단계로 전달 (정상 처리) |
TC_ACT_SHOT | 2 | 패킷 드롭 |
TC_ACT_STOLEN | 4 | 패킷을 소비 (BPF가 직접 처리) |
TC_ACT_REDIRECT | 7 | 패킷 리다이렉트 (bpf_redirect()) |
TC_ACT_UNSPEC | -1 | 기본 classful 동작 사용 |
TC_ACT_PIPE | 3 | 다음 action/filter로 전달 |
TC_ACT_RECLASSIFY | 1 | 재분류 |
/* 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 BPF | XDP |
|---|---|---|
| 부착점 | clsact (ingress/egress) | NIC 드라이버 (ingress only) |
| 컨텍스트 | struct __sk_buff | struct xdp_md |
| sk_buff 접근 | 전체 sk_buff 메타데이터 | 없음 (raw 패킷만) |
| egress 지원 | 가능 | 불가 |
| L3/L4 수정 | bpf_skb_store_bytes() | 직접 메모리 수정 |
| 성능 | 높음 (softirq 레벨) | 매우 높음 (드라이버 레벨) |
| 터널링 | bpf_skb_set_tunnel_key() | 불가 |
| conntrack | bpf_ct_lookup() 등 | 불가 |
Policing vs Shaping
트래픽 속도 제어에는 두 가지 접근법이 있습니다. Shaping(정형화)은 패킷을 큐에 보관하며 일정 속도로 내보내고, Policing(경찰)은 초과 트래픽을 즉시 드롭하거나 마킹합니다.
| 특성 | Shaping | Policing |
|---|---|---|
| 동작 | 패킷 지연 (큐잉) | 초과 패킷 드롭/마킹 |
| 적용 경로 | egress only | ingress + egress |
| 메커니즘 | Token Bucket + 큐 | Token Bucket (큐 없음) |
| 구현 | HTB, TBF 등 qdisc | action 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 회선에서 서비스별로 대역폭을 보장하고 제한하는 구조를 설계합니다.
설계 원칙
- 자식 class의
rate합은 부모의rate를 초과하지 않아야 합니다 (대역폭 보장) ceil은 부모의ceil을 초과할 수 없습니다defaultclass를 반드시 지정하여 미분류 트래픽을 처리합니다- 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_SEND | tokens > 0 && ctokens > 0 | 즉시 전송 (rate 이내) |
HTB_MAY_BORROW | tokens <= 0 && ctokens > 0 | 부모에서 토큰 빌려 전송 (rate~ceil 사이) |
HTB_CANT_SEND | ctokens <= 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.c | Qdisc 프레임워크 코어, dev_queue_xmit() 연동 |
sch_api.c | Netlink API: qdisc/class CRUD (tc 명령어 처리) |
cls_api.c | Classifier 프레임워크, filter chain 관리 |
act_api.c | Action 프레임워크 |
sch_htb.c | HTB 구현 |
sch_tbf.c | TBF 구현 |
sch_fq_codel.c | fq_codel 구현 |
sch_netem.c | netem 구현 |
sch_ingress.c | ingress/clsact qdisc |
sch_prio.c | PRIO 구현 |
cls_u32.c | u32 classifier |
cls_flower.c | flower classifier |
cls_bpf.c | BPF classifier (tc-bpf) |
act_mirred.c | mirred action |
act_police.c | police action |
act_gact.c | generic action (drop/pass/...) |
act_ct.c | conntrack 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;
};
Netlink API (tc 명령어 처리)
사용자 공간의 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
성능 튜닝 팁
- 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