TC (Traffic Control)
Linux TC(Traffic Control) 서브시스템을 실무 중심으로 심층 분석합니다. qdisc/class/filter 계층 구조, HTB/TBF/fq_codel/CAKE/netem/policing 동작 원리, ingress/egress·clsact·IFB 우회 shaping, mqprio/multiq와 queue steering, tc-bpf/flower 하드웨어 오프로드, 대역폭(Bandwidth) 제한·큐 지연(Latency)·버퍼(Buffer)블로트 문제를 계측하고 조정하는 운영 튜닝 절차까지 상세히 다룹니다.
핵심 요약
- 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, 드라이버 큐 길이까지 같이 봐야 실제 지연이 줄어듭니다.
단계별 이해
- 병목 위치 확정
병목이 호스트 egress인지, 수신 ingress인지, 아니면 이미 upstream 회선 바깥인지 먼저 구분합니다. - 큐잉 전략 선택
단순 rate limit이면 TBF, 계층형 분배면 HTB, 지연 제어면 fq_codel/CAKE처럼 목적에 맞는 qdisc를 고릅니다. - 분류 기준 설계
DSCP, 5-tuple, fwmark, cgroup, BPF 중 운영에 맞는 classifier를 정하고 default class를 남깁니다. - 하드웨어 경계 확인
mqprio, queue_mapping, hw-tc-offload를 쓸 때는 어느 부분이 소프트웨어이고 어느 부분이 NIC로 내려가는지 확인합니다. - 통계로 검증
overlimits,backlog,dropped, BQL, 드라이버 큐 통계를 함께 보고 실제로 지연과 처리량(Throughput)이 원하는 방향으로 갔는지 확인합니다.
TC 개요
TC(Traffic Control)는 리눅스 커널의 패킷 스케줄링 및 트래픽 제어(Traffic Control) 프레임워크입니다. 네트워크 인터페이스에서 송수신되는 패킷의 대역폭 제한, 우선순위(Priority) 지정, 지연 시뮬레이션, 패킷 드롭 정책 등을 제어할 수 있습니다. 커널의 net/sched/ 디렉터리에 구현되어 있으며, 사용자 공간(User Space)에서는 tc 유틸리티(iproute2 패키지)를 통해 조작합니다.
TC의 핵심 목표는 QoS(Quality of Service)입니다. 제한된 네트워크 대역폭을 효율적으로 분배하고, 중요한 트래픽에 우선순위를 부여하며, 과도한 트래픽을 제어하여 네트워크 전체의 성능을 최적화합니다.
iptables -j CLASSIFY나 tc filter ... action connmark 등으로 연동됩니다.
TC의 4대 구성 요소
| 구성 요소 | 역할 | 커널 구조체(Struct) |
|---|---|---|
| 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; /* 기존 플로우 리스트 */
};
CAKE (Common Applications Kept Enhanced)
CAKE는 단일 병목 링크를 실무적으로 다루기 위해 만들어진 현대적 shaping qdisc입니다. COBALT AQM, Deficit-mode shaper, flow/host isolation, DiffServ tin, 링크 오버헤드(Overhead) 보정을 한 덩어리로 묶어 제공합니다. 즉, HTB + fq_codel + overhead tuning + host fairness를 자주 같이 쓰던 환경에서 설정 실수를 줄이기 좋은 qdisc입니다.
| 항목 | fq_codel | CAKE |
|---|---|---|
| 주요 목적 | 버퍼블로트 완화와 flow fairness | 단일 병목 링크 shaping + fairness + DiffServ 통합 |
| 대역폭 제한 | 직접 shaping 기능 없음 | bandwidth로 직접 shaping |
| 호스트 공정성(Fairness) | 기본 flow 단위 | triple-isolate 등 host/flow 공정성 포함 |
| 권장 위치 | 서버 일반 egress, HTB leaf | 가정용/엣지 WAN, VPN 종단, IFB downlink shaping |
| 덜 맞는 경우 | 정확한 계층형 보장 대역폭 | 복잡한 계층형 서비스 트리, 하드웨어 TC 큐 맵 설계 |
# 단일 업링크 병목: 100M 회선을 95M로 shape, DiffServ 4단계 사용
tc qdisc replace dev pppoe-wan root cake \
bandwidth 95mbit \
diffserv4 nat
# 가변 링크(예: LTE/무선)에서 ingress 추정 shaping
# 보통 IFB에 붙여 사용
tc qdisc replace dev ifb0 root cake \
bandwidth 200mbit \
autorate-ingress diffserv3 nat
# GSO를 쪼개 shaping 정확도 우선 (기본 동작)
# 고속 링크에서 CPU 여유가 충분하면 그대로 두는 편이 보통 안전
tc qdisc replace dev eth0 root cake bandwidth 1gbit besteffort
# 처리량 우선으로 GSO 유지
tc qdisc replace dev eth0 root cake bandwidth 1gbit besteffort no-split-gso
CAKE는 "인터넷 회선 하나를 잘 다듬는" 데 매우 강하지만, 서비스별 최소 보장율과 부모/자식 대역폭 빌리기를 정밀하게 설계해야 한다면 여전히 HTB + fq_codel 조합이 더 직접적입니다.
반대로 홈 라우터, 지점 라우터, 터널(Tunnel) 종단처럼 한 곳의 실제 병목를 다듬는 용도라면 CAKE가 더 단순하고 실수가 적습니다.
FQ (Fair Queue)와 TCP pacing
fq qdisc는 지연 제어보다 소켓(Socket) pacing과 흐름별 공정 전송에 초점을 둡니다. 특히 커널 TCP stack이 SO_MAX_PACING_RATE 또는 내부 pacing rate를 계산해 내릴 때, fq가 이를 부드럽게 직렬화(Serialization)합니다. 따라서 로컬 호스트가 직접 송신하는 고속 TCP에서는 fq가 잘 맞고, 엣지 병목 회선 shaping에는 보통 cake나 htb + fq_codel가 더 적합합니다.
# 로컬 호스트 송신 pacing에 맞춘 FQ qdisc
tc qdisc replace dev eth0 root fq \
limit 10000 \
flow_limit 100 \
quantum 1514 \
initial_quantum 15140 \
maxrate 10gbit
# TCP가 qdisc 앞에 너무 많이 쌓지 않도록 같이 보는 값
sysctl net.ipv4.tcp_limit_output_bytes
fq는 "호스트가 스스로 내보내는 속도"를 다듬는 데 강하고, cake/tbf/htb는 "링크 병목에 맞춰 강제로 늦추는 일"에 더 가깝습니다.
SFQ (Stochastic Fairness Queueing)
SFQ는 해시(Hash) 기반 공정 큐잉입니다. 플로우 해시를 사용하여 패킷을 여러 큐에 분배하고, 라운드 로빈(Round Robin)으로 dequeue합니다. 주기적으로 해시 함수를 변경(perturbation)하여 해시 충돌로 인한 불공정을 완화합니다.
# SFQ 설정
tc qdisc add dev eth0 root sfq perturb 10 quantum 1514
# perturb: 해시 재계산 주기 (초), quantum: 라운드당 전송 바이트
TBF (Token Bucket Filter)
TBF는 토큰 버킷 알고리즘으로 대역폭을 제한합니다. 토큰이 일정 속도로 버킷에 충전되고, 패킷 전송 시 토큰을 소비합니다. 토큰이 부족하면 패킷은 대기하거나 드롭됩니다.
# TBF: 1Mbps 대역폭 제한, 버스트 32KB, 지연 최대 50ms
tc qdisc add dev eth0 root tbf \
rate 1mbit \ # 평균 전송 속도
burst 32kbit \ # 버킷 크기 (버스트 허용량)
latency 50ms # 최대 큐잉 지연
# 또는 buffer + limit으로 지정
tc qdisc add dev eth0 root tbf \
rate 10mbit \
buffer 1600 \ # 버킷 크기 (바이트)
limit 3000 # 최대 큐 길이 (바이트)
/* net/sched/sch_tbf.c — Token Bucket Filter 구조 */
struct tbf_sched_data {
u32 limit; /* 큐 최대 바이트 */
u32 max_size; /* 단일 패킷 최대 크기 */
s64 buffer; /* 토큰 버킷 깊이 (ns) */
s64 mtu; /* peakrate 버킷 깊이 */
struct psched_ratecfg rate; /* 평균 전송 속도 */
struct psched_ratecfg peak; /* 최대 전송 속도 */
struct Qdisc *qdisc; /* 내부 qdisc */
s64 tokens; /* 현재 토큰 수 (ns) */
s64 ptokens; /* peak rate 토큰 (ns) */
s64 t_c; /* 마지막 토큰 갱신 시각 */
};
netem (Network Emulator)
netem은 WAN 환경을 시뮬레이션하기 위한 qdisc입니다. 지연, 패킷 손실, 패킷 복제, 패킷 순서 변경 등을 에뮬레이션합니다. 네트워크 애플리케이션 테스트에 매우 유용합니다.
# 100ms 지연 추가 (± 10ms 지터, 정규분포)
tc qdisc add dev eth0 root netem delay 100ms 10ms distribution normal
# 1% 패킷 손실
tc qdisc add dev eth0 root netem loss 1%
# 복합: 50ms 지연 + 0.5% 손실 + 0.1% 복제 + 25% 순서 변경
tc qdisc add dev eth0 root netem \
delay 50ms 5ms \
loss 0.5% \
duplicate 0.1% \
reorder 25% 50%
# 대역폭 제한과 결합 (netem + tbf)
tc qdisc add dev eth0 root handle 1: netem delay 100ms
tc qdisc add dev eth0 parent 1:1 handle 10: tbf rate 1mbit burst 32kbit latency 50ms
Classful Qdisc
Classful qdisc는 내부에 class 계층을 가지며, filter를 통해 패킷을 분류하여 각 class에 할당합니다. class는 자체 qdisc를 가질 수 있어 재귀적 계층 구조를 형성합니다.
HTB (Hierarchical Token Bucket)
HTB는 가장 널리 사용되는 classful qdisc입니다. 계층적 토큰 버킷 구조로, 각 class에 보장 대역폭(rate)과 최대 대역폭(ceil)을 설정할 수 있습니다. 여유 대역폭은 자식 class 간에 공유됩니다.
rate는 해당 class에 보장되는 최소 대역폭, ceil은 빌릴 수 있는 최대 대역폭입니다. rate < ceil이면 부모로부터 여유 대역폭을 빌려 사용할 수 있습니다.
# HTB 기본 구조: 100Mbps 회선을 3개 class로 분배
tc qdisc add dev eth0 root handle 1: htb default 30
# 루트 class: 전체 대역폭
tc class add dev eth0 parent 1: classid 1:1 htb \
rate 100mbit ceil 100mbit
# 자식 class: 우선 트래픽 (보장 50M, 최대 100M)
tc class add dev eth0 parent 1:1 classid 1:10 htb \
rate 50mbit ceil 100mbit prio 1
# 자식 class: 일반 트래픽 (보장 30M, 최대 100M)
tc class add dev eth0 parent 1:1 classid 1:20 htb \
rate 30mbit ceil 100mbit prio 2
# 자식 class: 벌크 트래픽 (보장 20M, 최대 80M) — default
tc class add dev eth0 parent 1:1 classid 1:30 htb \
rate 20mbit ceil 80mbit prio 3
# 각 leaf class에 fq_codel 부착
tc qdisc add dev eth0 parent 1:10 handle 10: fq_codel
tc qdisc add dev eth0 parent 1:20 handle 20: fq_codel
tc qdisc add dev eth0 parent 1:30 handle 30: fq_codel
/* net/sched/sch_htb.c — HTB class 구조 */
struct htb_class {
struct Qdisc_class_common common;
struct psched_ratecfg rate; /* 보장 대역폭 */
struct psched_ratecfg ceil; /* 최대 대역폭 */
s64 buffer; /* rate 토큰 버킷 깊이 */
s64 cbuffer; /* ceil 토큰 버킷 깊이 */
s64 tokens; /* rate 현재 토큰 */
s64 ctokens; /* ceil 현재 토큰 */
s64 t_c; /* 마지막 갱신 시각 */
int prio; /* 우선순위 (0=최고) */
int quantum; /* DRR 양자 */
enum htb_cmode cmode; /* HTB_CAN_SEND / HTB_MAY_BORROW / HTB_CANT_SEND */
struct Qdisc *leaf; /* leaf qdisc (fq_codel 등) */
struct htb_class *parent; /* 부모 class */
...
};
PRIO (Priority Scheduler)
PRIO는 고정 우선순위 스케줄러(Scheduler)입니다. 여러 밴드(기본 3개)를 가지며, 낮은 번호의 밴드가 완전히 비어야 다음 밴드의 패킷을 전송합니다. 대역폭 제한 기능은 없으며, 순수한 우선순위 기반 스케줄링을 제공합니다.
# PRIO: 3밴드 우선순위 qdisc
tc qdisc add dev eth0 root handle 1: prio bands 3 \
priomap 1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
# 각 밴드에 별도 qdisc 부착 가능
tc qdisc add dev eth0 parent 1:1 handle 10: sfq perturb 10 # 최우선
tc qdisc add dev eth0 parent 1:2 handle 20: tbf rate 20mbit burst 5kb latency 70ms
tc qdisc add dev eth0 parent 1:3 handle 30: sfq perturb 10 # 벌크
CBQ, HFSC, DRR
| Qdisc | 알고리즘 | 특징 | 권장 사용 |
|---|---|---|---|
| CBQ | Class-Based Queueing | 링크 유휴 시간 기반 추정, 복잡한 파라미터 | 레거시. HTB로 대체 권장 |
| HFSC | Hierarchical Fair Service Curve | 실시간(rt), 링크 공유(ls), 상한(ul) 서비스 커브 | 정밀한 지연 보장 필요 시 |
| DRR | Deficit Round Robin | 간단한 가중치 라운드 로빈, O(1) dequeue | 가벼운 공정 스케줄링 |
# HFSC 예시: 실시간 서비스 커브를 사용한 지연 보장
tc qdisc add dev eth0 root handle 1: hfsc default 30
tc class add dev eth0 parent 1: classid 1:1 hfsc \
sc rate 100mbit ul rate 100mbit
tc class add dev eth0 parent 1:1 classid 1:10 hfsc \
rt m1 50mbit d 10ms m2 30mbit \
ls m1 50mbit d 10ms m2 30mbit
# DRR 예시
tc qdisc add dev eth0 root handle 1: drr
tc class add dev eth0 parent 1: classid 1:1 drr quantum 1500
tc class add dev eth0 parent 1: classid 1:2 drr quantum 3000
멀티큐와 mqprio: TC는 NIC 큐까지 배선할 수 있습니다
현대 NIC는 보통 여러 TX/RX queue를 가지며, 실제 병목은 qdisc 하나가 아니라 qdisc → BQL → 드라이버 TX queue → NIC hardware queue 체인 전체에 걸쳐 생깁니다. multiq와 mqprio는 이 다중 큐 환경에서 TC가 어디까지 개입할 수 있는지 보여 주는 대표 qdisc입니다.
| 기법 | 주요 목적 | 강점 | 주의점 |
|---|---|---|---|
| multiq | 여러 hardware TX queue를 기본적으로 활용 | 큐 하나가 멈춰도 다른 큐가 막히지 않아 head-of-line blocking 완화 | 정교한 traffic class 설계는 없음. 기본 큐 분산용 |
| mqprio | priority → traffic class → queue set 맵핑 | DCB, TSN, NIC 하드웨어 큐와 정합이 좋음 | map, queues, hw, shaper 모드를 NIC 지원과 같이 봐야 함 |
| skbedit priority | 송신 traffic class 선택 | mqprio와 결합 시 filter에서 분류 결과를 queue set까지 전파 가능 | 실제 queue 선택은 mqprio map 결과에 따름 |
| skbedit queue_mapping | 정확한 TX queue 선택 | 소프트웨어에서 특정 TXQ로 직접 보냄 | RX에서는 하드웨어 전용. 소프트웨어 RX queue 선택은 불가 |
| flower hw_tc | 수신 traffic class 선택 | RSS와 하드웨어 traffic class를 같이 설계할 수 있음 | 드라이버가 지원하지 않으면 의미가 없고 보통 offload와 같이 봐야 함 |
# mqprio: 3개 traffic class를 4개 TX queue에 매핑
tc qdisc replace dev eth0 root handle 100: mqprio \
num_tc 3 \
map 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 2 \
queues 1@0 1@1 2@2 \
hw 0
# flower + skbedit priority: mqprio의 traffic class 3으로 분류
tc filter add dev eth0 parent 100: protocol ip prio 10 \
flower ip_proto udp dst_port 12345 \
action skbedit priority 3
# multiq: 소프트웨어에서 특정 TX queue로 직접 배정
tc qdisc replace dev eth0 root handle 200: multiq
tc filter add dev eth0 parent 200: protocol ip prio 20 \
flower dst_ip 192.0.2.10 \
action skbedit queue_mapping 3
# RX 쪽은 flower의 hw_tc로 hardware traffic class 지정
tc filter add dev eth0 ingress protocol ip prio 5 \
flower ip_proto tcp dst_port 443 hw_tc 2
hw 0로 mqprio를 소프트웨어 전용으로 돌릴 수는 있지만, 드라이버가 가진 실제 queue 모델과 어긋나면 기대한 분산이 나오지 않을 수 있습니다.
또 queue_mapping은 송신 쪽 소프트웨어 steering에는 유용하지만, 수신 queue 선택은 보통 NIC 하드웨어 RSS와 hw_tc 같은 기능으로 해결해야 합니다.
Filter / Classifier
Filter는 패킷을 classful qdisc의 특정 class로 분류합니다. 여러 종류의 classifier가 있으며, 우선순위(prio)에 따라 순서대로 평가됩니다.
u32 Filter
u32는 가장 전통적인 classifier로, 패킷 헤더의 임의 오프셋(Offset)에서 32비트 값을 추출하여 매칭합니다. 유연하지만 복잡하고, 최신 환경에서는 flower로 대체되는 추세입니다.
# u32: 목적지 포트 80 (HTTP) → class 1:10
tc filter add dev eth0 parent 1: protocol ip prio 1 u32 \
match ip dport 80 0xffff flowid 1:10
# u32: 소스 서브넷 10.0.0.0/24 → class 1:20
tc filter add dev eth0 parent 1: protocol ip prio 2 u32 \
match ip src 10.0.0.0/24 flowid 1:20
# u32: TOS 필드 최소 지연(0x10) 매칭
tc filter add dev eth0 parent 1: protocol ip prio 3 u32 \
match ip tos 0x10 0xff flowid 1:10
flower Filter
flower는 현대적인 classifier로, 직관적인 키워드 매칭을 제공합니다. L2~L4 헤더 필드를 이름으로 지정할 수 있어 u32보다 사용하기 쉽습니다. 하드웨어 오프로드도 지원합니다.
# flower: 목적지 포트 443 (HTTPS), TCP → class 1:10
tc filter add dev eth0 parent 1: protocol ip prio 1 \
flower ip_proto tcp dst_port 443 flowid 1:10
# flower: 소스 MAC + VLAN ID 매칭
tc filter add dev eth0 parent 1: protocol 802.1Q prio 1 \
flower src_mac aa:bb:cc:dd:ee:ff vlan_id 100 \
action mirred egress redirect dev eth1
# flower: DSCP 매칭 (DiffServ)
tc filter add dev eth0 parent 1: protocol ip prio 1 \
flower ip_tos 0xb8/0xfc flowid 1:10 # EF (Expedited Forwarding)
matchall, cgroup, BPF Filter
# matchall: 모든 패킷에 action 적용
tc filter add dev eth0 parent 1: protocol all prio 99 \
matchall action police rate 10mbit burst 64k conform-exceed drop
# cgroup: cgroup 기반 분류 (net_cls)
tc filter add dev eth0 parent 1: protocol ip prio 10 \
cgroup
# BPF classifier: eBPF 프로그램으로 분류
tc filter add dev eth0 parent 1: protocol all prio 1 \
bpf obj cls_prog.o sec classifier da
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
/* 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 | 체크섬(Checksum) 재계산 |
| tunnel_key | act_tunnel_key | 터널 메타데이터 설정/해제 (VXLAN, Geneve 등) |
tc-actions(8)가 강조하듯 action은 classifier에만 종속된 일회용 객체가 아닙니다.
index를 가진 action을 따로 만들고 여러 filter에서 재사용할 수 있으므로, 대규모 정책에서는 "매치 조건"과 "동작"을 분리해 설계하는 편이 좋습니다.
# mirred: eth0 수신 패킷을 eth1으로 미러링
tc filter add dev eth0 ingress protocol all prio 1 \
matchall action mirred egress mirror dev eth1
# mirred: 리다이렉트 (원본은 드롭)
tc filter add dev eth0 ingress protocol ip prio 1 \
flower dst_ip 10.0.0.1 \
action mirred egress redirect dev veth0
# police: 수신 10Mbps 초과 시 드롭
tc filter add dev eth0 ingress protocol ip prio 1 \
matchall action police rate 10mbit burst 256k \
conform-exceed drop/continue
# pedit: TTL 값 64로 설정
tc filter add dev eth0 parent 1: protocol ip prio 1 \
u32 match ip dst 10.0.0.0/8 \
action pedit ex munge ip ttl set 64
# connmark + skbedit: conntrack 마크 복원 후 skb priority 설정
tc filter add dev eth0 parent 1: protocol ip prio 1 \
matchall action connmark \
action skbedit priority 1:10
# ct: conntrack 조회 후 zone 지정
tc filter add dev eth0 ingress protocol ip prio 1 \
flower ct_state -trk \
action ct zone 1
# action 체이닝: 여러 action 순서 실행
tc filter add dev eth0 ingress protocol ip prio 1 \
flower src_ip 192.168.1.0/24 \
action pedit ex munge ip tos set 0x28 \
action skbedit priority 1:10 \
action csum ip4h
ingress qdisc와 clsact
일반적인 qdisc는 egress(송신) 경로에서만 동작합니다. 수신(ingress) 경로에서 트래픽을 제어하려면 특수한 ingress qdisc 또는 clsact qdisc를 사용합니다.
ingress qdisc
ingress qdisc는 실제 큐잉을 수행하지 않습니다. filter와 action을 부착할 수 있는 부착점만 제공하며, 수신 패킷에 대해 policing, 리다이렉트, 드롭 등을 수행합니다.
# ingress qdisc 부착
tc qdisc add dev eth0 ingress
# 수신 트래픽 policing: 100Mbps 초과 드롭
tc filter add dev eth0 ingress protocol ip prio 1 \
matchall action police rate 100mbit burst 1m \
conform-exceed drop
# 특정 소스 IP 수신 차단
tc filter add dev eth0 ingress protocol ip prio 2 \
flower src_ip 10.0.0.100 action drop
# ingress qdisc 제거
tc qdisc del dev eth0 ingress
clsact qdisc
clsact는 ingress의 상위 호환입니다. ingress와 egress 양쪽 모두에 filter/action을 부착할 수 있으며, 특히 BPF 프로그램의 부착점으로 설계되었습니다. 커널 4.5에서 도입되었으며, TC BPF 프로그래밍의 표준 진입점(Entry Point)입니다.
# clsact 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"이라고 부르는 구성이 사실은 이 우회 경로입니다.
| 방식 | 패킷 초과 시 | 장점 | 단점 |
|---|---|---|---|
| ingress police | 즉시 drop 또는 mark | 단순하고 CPU 비용이 낮음 | TCP 재전송(Retransmission)과 지터 증가 가능 |
| IFB + HTB/TBF | IFB egress 큐에 대기 | 정밀한 shaping, 기존 HTB 설계 재사용 | 가상 장치와 redirect 비용이 추가 |
| IFB + CAKE | flow/tin 기준으로 지연 제어 | 다운링크 bufferbloat 완화에 매우 강함 | 단일 병목 중심 설계라 복잡한 계층형 서비스 트리에는 덜 적합 |
# IFB 준비
modprobe ifb numifbs=1
ip link add ifb0 type ifb 2>/dev/null || true
ip link set dev ifb0 up
# 실제 NIC에는 clsact를 달고 ingress 패킷을 IFB로 redirect
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress protocol all prio 10 \
matchall action mirred egress redirect dev ifb0
# 이제 IFB의 egress에 원하는 shaping qdisc 적용
tc qdisc replace dev ifb0 root cake \
bandwidth 200mbit ingress diffserv4 nat
# 또는 IFB에 HTB 부착
tc qdisc replace dev ifb0 root handle 1: htb default 20
tc class add dev ifb0 parent 1: classid 1:1 htb rate 200mbit ceil 200mbit
tc class add dev ifb0 parent 1:1 classid 1:20 htb rate 200mbit ceil 200mbit
# 정리
tc qdisc del dev eth0 clsact
tc qdisc del dev ifb0 root
tc-mirred(8)도 경고하듯이 IFB에서 다른 IFB로 다시 redirect하는 구성은 피하세요.
커널은 action nesting을 4단계로 제한하며, IFB 연쇄는 디버깅(Debugging)과 성능 모두를 빠르게 악화시킵니다.
tc-bpf: eBPF 기반 TC 프로그램
TC BPF는 eBPF 프로그램을 TC의 filter/classifier로 부착하여, 커널 내에서 패킷을 프로그래밍 가능한 방식으로 처리합니다. XDP(BPF/XDP)와 달리 전체 sk_buff에 접근할 수 있어 L2~L4 헤더 수정, 터널링, conntrack 연동 등 풍부한 기능을 제공합니다.
direct-action (da) 모드
direct-action 모드에서는 BPF 프로그램이 classifier와 action을 하나로 통합합니다. 프로그램의 반환값이 곧 패킷 처리 결과(TC_ACT_OK, TC_ACT_SHOT 등)가 됩니다. 거의 모든 TC BPF 프로그램이 이 모드를 사용합니다.
| 반환값 | 상수 | 동작 |
|---|---|---|
TC_ACT_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 각각이 독립적인 커널 모듈(Kernel Module)로 구현됩니다.
Qdisc_ops 구조체
모든 qdisc는 struct Qdisc_ops를 구현하여 등록합니다. 이 구조체는 qdisc의 enqueue, dequeue, init, destroy 등의 콜백(Callback)을 정의합니다.
/* include/net/sch_generic.h — Qdisc 연산 테이블 */
struct Qdisc_ops {
struct Qdisc_ops *next;
const struct Qdisc_class_ops *cl_ops;
char id[IFNAMSIZ];
int priv_size;
unsigned int static_flags;
int (*enqueue)(struct sk_buff *skb,
struct Qdisc *sch,
struct sk_buff **to_free);
struct sk_buff * (*dequeue)(struct Qdisc *);
struct sk_buff * (*peek)(struct Qdisc *);
int (*init)(struct Qdisc *sch, struct nlattr *arg,
struct netlink_ext_ack *extack);
void (*reset)(struct Qdisc *);
void (*destroy)(struct Qdisc *);
int (*change)(struct Qdisc *sch, struct nlattr *arg,
struct netlink_ext_ack *extack);
int (*dump)(struct Qdisc *, struct sk_buff *);
int (*dump_stats)(struct Qdisc *, struct gnet_dump *);
struct module *owner;
};
tcf_proto_ops (Classifier 연산)
/* include/net/sch_generic.h — Classifier 연산 테이블 */
struct tcf_proto_ops {
struct list_head head;
char kind[IFNAMSIZ];
int (*classify)(struct sk_buff *,
const struct tcf_proto *,
struct tcf_result *);
int (*init)(struct tcf_proto *);
void (*destroy)(struct tcf_proto *, bool,
struct netlink_ext_ack *);
void *(*get)(struct tcf_proto *, u32 handle);
int (*change)(struct net *, struct sk_buff *,
struct tcf_proto *, unsigned long,
u32 handle, struct nlattr **,
void **, bool,
struct netlink_ext_ack *);
int (*delete)(struct tcf_proto *, void *, bool *,
bool, struct netlink_ext_ack *);
int (*dump)(struct net *, struct tcf_proto *,
void *, struct sk_buff *,
struct tcmsg *, bool);
struct module *owner;
...
};
net/sched/ 주요 소스 파일
| 파일 | 내용 |
|---|---|
sch_generic.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'
변경 감시와 트리 시각화
정적인 덤프(Dump)만 보면 qdisc/class 트리 전체가 한눈에 안 들어오는 경우가 많습니다. tc(8)의 -g 옵션은 트리 구조를 보여 주고, tc monitor는 런타임 변경을 추적합니다. 여러 자동화 도구가 규칙을 덮어쓰는 환경에서는 이 두 명령이 특히 유용합니다.
# qdisc / class 트리를 그래프 형태로 보기
tc -g qdisc show dev eth0
tc -g class show dev eth0
# RTNETLINK 기반 런타임 변경 감시
tc monitor
# 특정 공유 block 정책 확인
tc filter show block 22
# JSON 덤프와 조합하면 자동 점검에 편함
tc -j -s filter show dev eth0 ingress | python3 -m json.tool
통계 필드 해석
| 필드 | 의미 |
|---|---|
Sent | 전송된 총 바이트/패킷 수 |
dropped | 드롭된 패킷 수 (큐 가득, AQM 등) |
overlimits | 속도 제한 초과 횟수 (dequeue 시도 실패) |
requeues | 드라이버 거부로 재큐잉된 횟수 |
backlog | 현재 큐에 대기 중인 바이트/패킷 |
lended | 자체 토큰으로 전송한 횟수 (HTB) |
borrowed | 부모에서 빌려 전송한 횟수 (HTB) |
tokens | 현재 rate 토큰 수 (음수=부족, HTB) |
ctokens | 현재 ceil 토큰 수 (HTB) |
psched 시간 해상도
# TC 내부 시간 해상도 확인
cat /proc/net/psched
# 출력: 00000001 0000003e9 000f4240 3b9aca00
# [1] tick_in_usec_num [2] tick_in_usec_den
# [3] us2tick(1000000) [4] clock_res (ns)
# 실질적으로 1 tick = 1 ns (PSCHED_TICKS_PER_SEC = 10^9)
지연이 어디서 생기는지 계층별로 봅니다
TC를 붙였는데도 지연이 안 줄어드는 가장 흔한 이유는 병목이 qdisc가 아니라 TCP 송신 버퍼, BQL, 드라이버 TX ring, NIC hardware queue 쪽에 있기 때문입니다. 따라서 backlog만 보지 말고 아래 지점을 같이 봐야 합니다.
| 계층 | 무엇을 보나 | 대표 명령 | 의미 |
|---|---|---|---|
| qdisc | backlog, overlimits, dropped |
tc -s qdisc show dev eth0 |
qdisc가 실제로 병목인지 확인 |
| class | borrowed, tokens, ctokens |
tc -s class show dev eth0 |
HTB/HFSC가 ceil/rate 때문에 막히는지 확인 |
| BQL | limit, inflight |
/sys/class/net/.../byte_queue_limits/* |
드라이버 queue가 qdisc 뒤에서 다시 길어지는지 확인 |
| 드라이버 / NIC | queue stop, restart, timeout, xmit busy | ethtool -S eth0 |
실제 hardware queue 편중과 드라이버 병목 확인 |
| 링크 계층 | TX drop, carrier, qlen | ip -s link show dev eth0 |
qdisc 바깥의 인터페이스 레벨 상태 확인 |
| TCP 출력 제한 | tcp_limit_output_bytes |
sysctl net.ipv4.tcp_limit_output_bytes |
TCP가 qdisc 앞에 과도하게 적재하는지 판단 |
# 1. qdisc / class에서 backlog와 overlimits 확인
tc -s qdisc show dev eth0
tc -s class show dev eth0
# 2. BQL: qdisc 뒤 드라이버 큐가 얼마나 차는지 확인
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/inflight
# 3. 드라이버 / NIC 통계
ethtool -S eth0
ip -s link show dev eth0
# 4. TCP가 qdisc 앞에 너무 많이 쌓는지 확인
sysctl net.ipv4.tcp_limit_output_bytes
하드웨어 오프로드
일부 NIC는 TC 규칙의 하드웨어 오프로드를 지원합니다. flower filter와 matchall filter가 주요 오프로드 대상이며, 하드웨어에서 직접 패킷 분류와 action을 수행하여 CPU 부하를 줄입니다.
# 하드웨어 오프로드 지원 확인
ethtool -k eth0 | grep tc-offload
# hw-tc-offload: on
# 하드웨어 오프로드 활성화
ethtool -K eth0 hw-tc-offload on
# flower filter에 skip_sw 플래그: 하드웨어 전용 실행
tc filter add dev eth0 ingress protocol ip prio 1 \
flower skip_sw dst_port 80 ip_proto tcp \
action mirred egress redirect dev eth1
# skip_hw: 소프트웨어 전용 실행 (오프로드 비활성화)
tc filter add dev eth0 ingress protocol ip prio 2 \
flower skip_hw src_ip 10.0.0.0/8 action drop
# 오프로드 상태 확인 (in_hw 플래그)
tc -s filter show dev eth0 ingress
# ... in_hw in_hw_count 1
eSwitch와 TC Flower Offload
eSwitch (Embedded Switch)를 지원하는 SmartNIC/DPU(Mellanox/NVIDIA mlx5, Intel ice 등)에서는 TC flower 규칙이 NIC의 하드웨어 스위치로 오프로드됩니다. 이는 소프트웨어 브릿지나 OVS의 CPU 오버헤드를 획기적으로 줄여줍니다.
# ━━━ eSwitch switchdev 모드 전환 ━━━
# 1. eSwitch를 switchdev 모드로 설정
devlink dev eswitch set pci/0000:03:00.0 mode switchdev
# 2. Representor 포트 확인 (VF에 대한 가상의 제어 인터페이스)
ip link show
# enp3s0f0np0 <-- PF (uplink)
# enp3s0f0np0_0 <-- VF0 representor
# enp3s0f0np0_1 <-- VF1 representor
# 3. Representor에 TC flower 규칙 설치 (VF 간 포워딩)
tc qdisc add dev enp3s0f0np0_0 ingress
tc filter add dev enp3s0f0np0_0 ingress \
protocol 802.1Q \
flower vlan_id 100 \
action vlan pop \
action mirred egress redirect dev enp3s0f0np0_1
# 4. conntrack 상태 기반 오프로드
tc filter add dev enp3s0f0np0_0 ingress \
protocol ip \
flower ct_state +trk+new \
action ct commit \
action mirred egress redirect dev enp3s0f0np0_1
# 5. HW 오프로드 확인
tc -s filter show dev enp3s0f0np0_0 ingress | grep in_hw
# in_hw in_hw_count 1 <-- HW에서 처리됨
eSwitch TC Flower 매칭 키
eSwitch HW로 오프로드 가능한 TC flower 매칭 키와 액션입니다.
| 카테고리 | 매칭 키 | 설명 |
|---|---|---|
| L2 | eth_type | 이더타입 (0x0800=IPv4, 0x86DD=IPv6) |
vlan_id, vlan_ethtype | VLAN ID 및 태그 해제 | |
dst_mac, src_mac | MAC 주소 매칭 | |
| L3 | ip_proto | IP 프로토콜 (TCP=6, UDP=17, ICMP=1) |
src_ip, dst_ip | IP 주소 (IPv4/IPv6) | |
tos, dscp | ToS/DSCP 값 | |
ttl | TTL/Hop Limit | |
| L4 | src_port, dst_port | TCP/UDP 포트 |
tcp_flags | TCP 플래그 (SYN, ACK, FIN 등) | |
ct_state | conntrack 상태 (NEW, ESTABLISHED, RELATED) | |
ct_zone | conntrack zone | |
| 액션 | mirred | 패킷 리다이렉트 ( egress redirect) |
vlan | VLAN 태그 추가/변경/삭제 |
- 모든 매칭 키와 액션이 HW 오프로드 가능한 것은 아닙니다. 드라이버가 지원하지 않으면
skip_sw지정 시 오류 발생 - HW 플로우 테이블 크기가 제한적입니다 (수백만 엔트리). 초과 시 자동 소프트웨어 폴백
ct()오프로드는 드라이버/펌웨어(Firmware) 버전에 따라 지원 여부가 다릅니다
OVS Hardware Offload
OVS(Open vSwitch)와 eSwitch를 결합하면 가상 스위칭을 HW로 오프로드할 수 있습니다.
# ━━━ OVS + eSwitch HW Offload 설정 ━━━
# 1. eSwitch를 switchdev 모드로 전환
devlink dev eswitch set pci/0000:03:00.0 mode switchdev
# 2. OVS에서 HW offload 활성화
ovs-vsctl set Open_vSwitch . other_config:hw-offload=true
ovs-vsctl set Open_vSwitch . other_config:tc-policy=skip_sw
# 3. PF와 VF representor를 브릿지에 연결
ovs-vsctl add-br br-int
ovs-vsctl add-port br-int enp3s0f0np0 # PF (uplink)
ovs-vsctl add-port br-int enp3s0f0np0_0 # VF0 representor
ovs-vsctl add-port br-int enp3s0f0np0_1 # VF1 representor
# 4. 오프로드된 플로우 확인
ovs-appctl dpctl/dump-flows type=offloaded
# recirc_id(0),in_port(2),eth(...),ipv4(src=10.0.0.5,dst=10.0.0.10,...)
# packets:1523400, bytes:97497600, used:0.001s, flags:SFPR
성능 튜닝 팁
- leaf qdisc 선택: HTB leaf에
fq_codel을 사용하여 버퍼블로트 방지 - 단일 병목 링크: WAN uplink/downlink 한 군데를 다듬는 용도라면
CAKE가 HTB보다 단순하고 안전한 경우가 많음 - r2q 파라미터: HTB의
r2q(rate-to-quantum)은 자동 quantum 계산에 사용. 저대역폭 class에서 "quantum too small" 경고 시 r2q 조정 또는 quantum 직접 지정 - burst 크기: TBF/police의 burst가 너무 작으면 처리량 저하. 최소 MTU 이상, 권장:
rate * HZ / 8 - ingress shaping: 수신 트래픽을 늦춰야 하면 직접 police하지 말고 IFB redirect 후 egress qdisc로 shaping
- filter 순서: 자주 매칭되는 filter를 높은 prio(낮은 숫자)로 배치
- flower vs u32: 신규 설정에서는 flower 권장 (HW offload 지원, 직관적)
- clsact + BPF: 복잡한 분류 로직은 BPF 프로그램으로 구현 (성능 + 유연성)
- mqprio / queue steering: 고속 NIC에서는 qdisc뿐 아니라 실제 queue set과 BQL까지 같이 설계
- txqueuelen:
ip link set dev eth0 txqueuelen 1000— qdisc 큐와 별도의 드라이버 큐 길이
# txqueuelen 확인/설정
ip link show dev eth0 | grep qlen
ip link set dev eth0 txqueuelen 1000
# BQL (Byte Queue Limits) 확인 — 드라이버 레벨 큐 제한
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit_max
cat /sys/class/net/eth0/queues/tx-0/byte_queue_limits/limit_min
# 전체 TC 설정 백업/복원
tc -d qdisc show dev eth0 > tc_backup.txt
tc -d class show dev eth0 >> tc_backup.txt
tc -d filter show dev eth0 >> tc_backup.txt
# 모든 TC 설정 초기화
tc qdisc del dev eth0 root 2>/dev/null
tc qdisc del dev eth0 ingress 2>/dev/null
Qdisc 내부 구현
Qdisc는 단순한 큐가 아니라 enqueue → classify → queue → dequeue → xmit 파이프라인(Pipeline)을 관리하는 커널 객체입니다. 이 절에서는 Qdisc_ops의 콜백 체인, bulk dequeue 최적화, Qdisc 트리의 재귀적 구조를 상세히 분석합니다.
enqueue/dequeue/peek 콜백 체인
enqueue()는 패킷을 qdisc 내부 자료구조에 삽입하고 NET_XMIT_SUCCESS 또는 NET_XMIT_DROP을 반환합니다. classful qdisc에서는 enqueue 내부에서 tc_classify()를 호출하여 패킷이 속할 class를 결정한 뒤, 해당 class의 leaf qdisc로 재귀적으로 enqueue합니다.
/* net/sched/sch_htb.c — HTB enqueue 경로 (간략화) */
static int htb_enqueue(struct sk_buff *skb,
struct Qdisc *sch,
struct sk_buff **to_free)
{
struct htb_sched *q = qdisc_priv(sch);
struct htb_class *cl;
int ret;
/* 1. classify: filter chain을 통해 class 결정 */
cl = htb_classify(skb, sch, &ret);
if (cl == NULL) {
if (ret & __NET_XMIT_BYPASS)
qdisc_qstats_drop(sch);
__qdisc_drop(skb, to_free);
return ret;
}
/* 2. leaf qdisc로 재귀적 enqueue */
ret = qdisc_enqueue(skb, cl->leaf.q, to_free);
if (unlikely(ret != NET_XMIT_SUCCESS)) {
if (net_xmit_drop_count(ret)) {
qdisc_qstats_drop(sch);
cl->drops++;
}
return ret;
}
/* 3. class를 활성 트리에 추가 (dequeue 대상) */
htb_activate(q, cl);
sch->q.qlen++;
return NET_XMIT_SUCCESS;
}
dequeue()는 NET_TX softirq의 __qdisc_run()에서 호출됩니다. classful qdisc에서는 우선순위/토큰 상태에 따라 적절한 class를 선택하고, 해당 class의 leaf qdisc에서 패킷을 꺼냅니다.
/* net/sched/sch_generic.c — dequeue 루프 핵심 */
void __qdisc_run(struct Qdisc *q)
{
int quota = dev_tx_weight;
int packets;
while (qdisc_restart(q, &packets)) {
quota -= packets;
if (quota <= 0) {
/* 할당량 소진 → softirq 양보, 나중에 재스케줄 */
__netif_schedule(q);
break;
}
}
}
/* qdisc_restart는 dequeue → validate → xmit을 한 번 수행 */
static inline bool qdisc_restart(struct Qdisc *q, int *packets)
{
struct sk_buff *skb = dequeue_skb(q, packets);
if (unlikely(!skb))
return false;
return sch_direct_xmit(skb, q, ...);
}
Bulk Dequeue와 GSO 세그먼트
커널 4.16+에서 도입된 bulk dequeue는 한 번의 dequeue 호출에서 여러 패킷을 꺼내 sk_buff 리스트로 반환합니다. 이는 per-packet lock overhead를 줄이고 드라이버의 ndo_start_xmit() 배치 호출을 가능하게 합니다. 특히 GSO 세그먼트가 qdisc에서 분리될 때, 개별 세그먼트를 하나씩 처리하는 대신 리스트 단위로 전달합니다.
/* GSO skb가 qdisc에 들어올 때의 처리 */
/* fq_codel 등 일부 qdisc는 GSO skb를 세그먼트 단위로 분리하여 */
/* 개별 플로우 큐에 넣는다 → 공정성 향상, 하지만 CPU 비용 증가 */
/* CAKE는 split-gso / no-split-gso 옵션으로 이를 제어 */
/* 저속 링크: split-gso (정확한 shaping) */
/* 고속 링크: no-split-gso (CPU 절약) */
Qdisc 트리 구조와 탐색
커널은 enqueue 시 트리를 top-down으로 탐색(classify → leaf 도달)하고, dequeue 시에는 bottom-up으로 leaf에서 패킷을 꺼내 root까지 올립니다. 이 비대칭 구조가 TC의 핵심입니다.
HTB: 토큰 버킷과 클래스 계층
HTB(Hierarchical Token Bucket)는 리눅스 TC에서 가장 많이 사용되는 classful qdisc입니다. 이 절에서는 이중 토큰 버킷(rate/ceil), quantum과 DRR 기반 형제 스케줄링, burst/cburst 계산, HTB 내부 레벨과 Red-Black 트리를 상세히 분석합니다.
이중 토큰 버킷: rate와 ceil
HTB class는 두 개의 독립적인 토큰 버킷을 유지합니다. tokens는 rate 버킷, ctokens는 ceil 버킷입니다. 이 두 값의 부호 조합이 class의 전송 모드를 결정합니다.
quantum과 형제 클래스 스케줄링
같은 부모 아래에서 여러 class가 HTB_MAY_BORROW 상태이면, 어떤 class에게 먼저 보낼 기회를 줄 것인가? HTB는 Deficit Round Robin(DRR) 알고리즘을 사용합니다. 각 class의 quantum(한 라운드에서 보낼 수 있는 최대 바이트)이 이 결정의 핵심입니다.
# quantum 자동 계산: rate / r2q
# 기본 r2q=10, rate=10mbit → quantum = 10000000/8/10 = 125000 bytes
# rate=100kbit → quantum = 100000/8/10 = 1250 bytes (MTU 이하!)
# quantum이 MTU 이하면 경고 발생, 직접 지정 필요
tc class add dev eth0 parent 1:1 classid 1:10 htb \
rate 100kbit ceil 1mbit quantum 1514
# 또는 r2q를 낮춰 전체 자동 quantum을 키움
tc qdisc add dev eth0 root handle 1: htb default 30 r2q 1
quantum < MTU이면 한 라운드에 패킷 하나도 못 보내는 상황이 생겨 스케줄링이 비효율적입니다. 저속 class가 많은 환경에서는 r2q를 낮추거나 class별 quantum을 직접 지정하세요.
burst/cburst 계산
burst(또는 buffer)는 rate 토큰 버킷의 깊이이고, cburst(또는 cbuffer)는 ceil 토큰 버킷의 깊이입니다. 이 값이 클수록 순간 버스(Bus)트를 많이 허용하지만, 지연도 증가합니다.
# burst 계산 공식 (기본값)
# burst = rate / HZ (커널 타이머 주파수)
# HZ=250일 때: rate=100mbit → burst = 100000000/8/250 = 50000 bytes
# HZ=1000일 때: rate=100mbit → burst = 100000000/8/1000 = 12500 bytes
# burst 직접 지정 (latency-sensitive 환경)
tc class add dev eth0 parent 1:1 classid 1:10 htb \
rate 100mbit ceil 200mbit \
burst 15k cburst 15k
# burst가 너무 작으면 처리량이 rate에 못 미침
# burst가 너무 크면 순간적으로 과도한 트래픽 전송
# 일반적으로 MTU의 10~30배가 적절
HTB 내부: 레벨별 Red-Black 트리
HTB는 내부적으로 8개 레벨(0~7)의 Red-Black 트리를 유지합니다. 각 레벨에는 해당 깊이의 class 중 활성 상태인 것들이 들어있습니다. dequeue 시 가장 낮은 레벨(leaf)부터 탐색하여 전송 가능한 class를 찾습니다.
/* net/sched/sch_htb.c — HTB 스케줄러 구조 (간략화) */
struct htb_sched {
struct Qdisc_class_hash clhash;
int defcls; /* default class ID */
int rate2quantum; /* r2q */
/* 각 TC 우선순위(0~7)에 대해 레벨별 활성 class 트리 */
struct rb_root hlevel[TC_HTB_MAXDEPTH];
struct rb_root wait_pq[TC_HTB_MAXDEPTH];
/* 가장 가까운 이벤트 시각 (토큰 충전 완료 예정) */
s64 near_ev_cache[TC_HTB_MAXDEPTH];
...
};
/* dequeue 경로: 우선순위별로 CAN_SEND 또는 MAY_BORROW class를 찾는다 */
/* CAN_SEND class가 있으면 즉시 전송 */
/* 없으면 MAY_BORROW를 찾아 부모 토큰을 빌려 전송 */
/* 둘 다 없으면 가장 가까운 토큰 충전 시각에 타이머 설정 */
fq_codel: 공정 큐잉과 CoDel AQM
fq_codel은 Stochastic Fair Queuing(SFQ) 계열의 플로우 해싱과 CoDel(Controlled Delay) AQM을 결합한 qdisc입니다. 현대 리눅스의 기본 qdisc로, 버퍼블로트 문제를 효과적으로 해결합니다.
플로우 해시와 DRR 스케줄링
패킷이 도착하면 5-tuple(src IP, dst IP, src port, dst port, protocol) 해시를 계산하여 flows_cnt(기본 1024)개의 플로우 큐 중 하나에 배치합니다. 처음 보는 플로우는 new_flows 리스트에, 이미 패킷이 있는 플로우는 old_flows 리스트에 들어갑니다.
CoDel 알고리즘 상세
CoDel은 패킷 체류 시간(sojourn time)을 측정하여 큐가 과도하게 차있는지 판단합니다. 단순히 큐 길이를 보는 것이 아니라, 패킷이 큐에서 보낸 시간이 target(기본 5ms)을 초과하는 상태가 interval(기본 100ms) 동안 지속되면 드롭을 시작합니다.
/* include/net/codel.h — CoDel 핵심 파라미터 */
struct codel_params {
codel_time_t target; /* 목표 sojourn time (기본 5ms) */
codel_time_t interval; /* 측정 간격 (기본 100ms) */
codel_time_t mtu_time; /* MTU를 전송하는 데 걸리는 시간 */
u32 ce_threshold; /* CE 마킹 임계값 */
bool ecn; /* ECN 마킹 활성화 */
};
/* CoDel dequeue 판정 의사 코드 */
/* 1. dequeue하면서 sojourn_time = now - enqueue_time 계산 */
/* 2. sojourn_time < target이면 → dropping = false, 통과 */
/* 3. sojourn_time >= target이고 interval 동안 계속이면 → */
/* drop 시작, count++ */
/* 4. 다음 드롭 시각 = first_above_time + interval/√count */
/* 5. 큐가 비거나 sojourn < target이면 dropping 해제 */
CAKE: 올인원 AQM/Shaping
CAKE(Common Applications Kept Enhanced)는 HTB + fq_codel 조합을 단일 qdisc로 통합한 현대적 솔루션입니다. 이 절에서는 DiffServ tin, NAT-aware 플로우 격리(Isolation), ACK 필터, 오버헤드 보정을 상세히 분석합니다.
DiffServ 모드와 tin 구조
CAKE는 패킷을 DSCP 값에 따라 tin(우선순위 버킷)으로 분류합니다. 각 tin은 독립적인 AQM과 shaping을 수행합니다.
| 모드 | tin 수 | 분류 기준 | 용도 |
|---|---|---|---|
besteffort | 1 | 없음 (전부 동일) | 단순한 shaping + AQM |
diffserv3 | 3 | Bulk / Best Effort / Voice | 일반적인 가정용 |
diffserv4 | 4 | Bulk / BE / Video / Voice | 권장 기본값 |
diffserv8 | 8 | RFC 4594 표준 매핑(Mapping) | 엔터프라이즈 |
precedence | 8 | IP Precedence 비트 | 레거시 호환 |
NAT-aware 플로우 격리
CAKE의 nat 옵션은 conntrack에서 NAT 전 원본 주소를 추출하여 플로우 해시에 사용합니다. 이를 통해 NAT 뒤의 개별 호스트를 구분하여 공정한 대역폭 분배가 가능합니다.
# 플로우 격리 모드
# flowblind — 플로우 구분 없음 (FIFO에 가까움)
# srchost — 소스 IP 기준 공정성
# dsthost — 목적지 IP 기준 공정성
# hosts — src+dst 양방향 공정성
# flows — 5-tuple 플로우별 공정성
# dual-srchost / dual-dsthost — 호스트 + 플로우 2단계
# triple-isolate — 호스트 + 플로우 + 내부 우선순위 (기본값)
tc qdisc replace dev eth0 root cake \
bandwidth 100mbit diffserv4 nat triple-isolate
# NAT-aware: NAT 뒤 10대의 PC가 각각 ~10Mbps씩 공정하게 분배
# nat 없이는 NAT 후 IP가 같아서 모두 하나의 호스트로 취급
ACK 필터링
비대칭 링크(다운 100M / 업 10M)에서 TCP ACK가 uplink를 채우면 다운링크도 느려집니다. CAKE의 ack-filter는 동일 플로우의 중복 ACK를 감지하여 이전 것을 드롭하고 최신 ACK만 유지합니다.
# ACK 필터 활성화 (기본: ack-filter-aggressive 아닌 ack-filter)
tc qdisc replace dev pppoe-wan root cake \
bandwidth 10mbit diffserv4 nat ack-filter
# 공격적 모드: 더 많은 ACK를 제거 (약간의 재전송 위험)
tc qdisc replace dev pppoe-wan root cake \
bandwidth 10mbit diffserv4 nat ack-filter-aggressive
# 비활성화
tc qdisc replace dev pppoe-wan root cake \
bandwidth 10mbit diffserv4 nat no-ack-filter
링크 오버헤드 보정
shaping 정확도를 위해 CAKE는 실제 링크 계층 오버헤드를 보정합니다. 이더넷, PPPoE, ATM, DOCSIS 등 링크 타입에 따라 패킷 크기 계산을 조정합니다.
# PPPoE + VLAN (일반적인 한국 가정용 인터넷)
tc qdisc replace dev pppoe-wan root cake \
bandwidth 95mbit overhead 18 mpu 64 diffserv4 nat
# ATM 셀 기반 (구형 ADSL)
tc qdisc replace dev ppp0 root cake \
bandwidth 20mbit atm overhead 44
# 기본 이더넷 (오버헤드 보정 불필요)
tc qdisc replace dev eth0 root cake \
bandwidth 1gbit noatm raw
EDT (Earliest Departure Time)
EDT(Earliest Departure Time)는 패킷별로 "이 시각 이후에 전송하라"는 타임스탬프를 설정하는 방식입니다. Google이 제안하고 커널 4.20에 도입된 이 모델은 전통적인 큐 기반 shaping과 근본적으로 다릅니다.
fq qdisc와 EDT 연동
fq qdisc는 소켓이 설정한 pacing rate에 따라 패킷의 departure time을 계산합니다. TCP BBR, TCP CUBIC(fq 연동 시), BPF 프로그램 등이 EDT를 활용합니다.
/* include/linux/skbuff.h — EDT 타임스탬프 */
struct sk_buff {
...
union {
ktime_t tstamp; /* 수신 시각 또는 EDT */
};
...
};
/* net/sched/sch_fq.c — fq dequeue에서 EDT 확인 */
/* 패킷의 tstamp가 현재 시각보다 미래이면 대기 */
/* 타이머로 해당 시각에 재스케줄 */
/* BPF에서 EDT 설정 */
/* skb->tstamp = departure_time; */
/* bpf_skb_set_tstamp(skb, time, BPF_SKB_TSTAMP_DELIVERY_MONO) */
# fq qdisc + TCP BBR: EDT 기반 pacing
tc qdisc replace dev eth0 root fq
sysctl -w net.ipv4.tcp_congestion_control=bbr
# SO_TXTIME 소켓 옵션으로 사용자 공간에서 직접 EDT 지정
# 이 경우 fq가 아닌 ETF(Earliest TxTime First) qdisc 사용
tc qdisc replace dev eth0 root etf \
clockid CLOCK_TAI delta 200000 offload
# ETF + hardware timestamping: NIC 하드웨어가 정확한 시각에 전송
# TSN(Time-Sensitive Networking) 환경에서 사용
tc-bpf / tc-ebpf
TC BPF는 단순한 classifier를 넘어 프로그래밍 가능한 데이터플레인입니다. 이 절에서는 cls_bpf 내부 구현, act_bpf와의 차이, direct-action 모드의 최적화, XDP와의 처리 위치 차이를 상세히 분석합니다.
cls_bpf 내부 구현
/* net/sched/cls_bpf.c — cls_bpf classify 콜백 */
static int cls_bpf_classify(struct sk_buff *skb,
const struct tcf_proto *tp,
struct tcf_result *res)
{
struct cls_bpf_head *head = rcu_dereference_bh(tp->root);
struct cls_bpf_prog *prog;
int ret = -1;
list_for_each_entry_rcu(prog, &head->plist, link) {
/* BPF 프로그램 실행 */
int filter_res = bpf_prog_run(prog->filter, skb);
if (prog->exts_integrated) {
/* direct-action 모드: 반환값이 곧 TC_ACT_* */
res->class = 0;
res->classid = TC_H_MAJ(prog->res.classid) |
clamp(filter_res, 0, TC_ACT_VALUE_MAX);
if (filter_res == TC_ACT_UNSPEC && prog->res.classid)
res->classid = prog->res.classid;
ret = tcf_exts_exec_ex(skb, &prog->exts,
filter_res, res);
} else {
/* 전통 모드: 반환값이 classid */
if (filter_res != 0) {
res->classid = prog->res.classid;
ret = tcf_exts_exec(skb, &prog->exts, res);
}
continue;
}
break;
}
return ret;
}
TC BPF vs XDP 처리 위치
TC BPF 주요 헬퍼 함수
| 헬퍼 | 기능 | 용도 |
|---|---|---|
bpf_skb_store_bytes() | 패킷 데이터 수정 | 헤더 필드 변경, 캡슐화(Encapsulation) |
bpf_skb_load_bytes() | 패킷 데이터 읽기 | deep packet inspection |
bpf_redirect() | 패킷 리다이렉트 | 다른 인터페이스로 전달 |
bpf_redirect_neigh() | 이웃 lookup 포함 리다이렉트 | L3 포워딩 |
bpf_skb_set_tunnel_key() | 터널 키 설정 | VXLAN/Geneve 캡슐화 |
bpf_skb_get_tunnel_key() | 터널 키 조회 | 터널 디캡슐화 후 메타 |
bpf_ct_lookup() | conntrack 조회 | 상태 기반 필터링 |
bpf_skb_cgroup_id() | cgroup ID 조회 | cgroup 기반 정책 |
bpf_skb_change_head() | headroom 확장 | 헤더 추가 (캡슐화) |
bpf_skb_change_tail() | tailroom 변경 | 패킷 크기 조정 |
bpf_skb_set_tstamp() | EDT 타임스탬프 설정 | BPF 기반 pacing |
bpf_fib_lookup() | FIB 테이블 조회 | 라우팅 결정 |
/* TC BPF: VXLAN 캡슐화 + 리다이렉트 예시 */
SEC("tc")
int tc_vxlan_encap(struct __sk_buff *skb)
{
struct bpf_tunnel_key key = {};
key.remote_ipv4 = bpf_htonl(0x0a000001); /* 10.0.0.1 */
key.tunnel_id = 100; /* VNI 100 */
key.tunnel_ttl = 64;
bpf_skb_set_tunnel_key(skb, &key, sizeof(key), 0);
return bpf_redirect(ifindex_vxlan, 0);
}
하드웨어 오프로드
TC flower offload는 소프트웨어 TC 규칙을 NIC 하드웨어로 내려보내 CPU 없이 패킷을 처리합니다. 이 절에서는 드라이버 콜백 체인, switchdev 아키텍처, CT(conntrack) offload를 상세히 분석합니다.
오프로드 콜백 경로
CT(conntrack) Offload
커널 5.8+에서 도입된 CT offload는 act_ct의 conntrack 상태 추적을 하드웨어로 내립니다. 첫 패킷(NEW)은 소프트웨어에서 처리하고, connection이 ESTABLISHED되면 이후 패킷은 NIC 하드웨어가 직접 포워딩합니다.
# CT offload 설정 예시: VF 간 conntrack 기반 포워딩
# 1. NEW 상태: conntrack 조회 → commit → redirect
tc filter add dev rep0 ingress protocol ip flower \
ct_state -trk \
action ct zone 1 \
action goto chain 1
# 2. ESTABLISHED: HW에서 직접 포워딩
tc filter add dev rep0 ingress chain 1 protocol ip flower \
ct_state +trk+est \
action ct zone 1 \
action mirred egress redirect dev rep1
# 3. 확인: HW offload 상태
tc -s filter show dev rep0 ingress
# ... in_hw in_hw_count 1 ...
# conntrack 테이블에서 offload 상태 확인
conntrack -L | grep OFFLOAD
ct_state +rel(RELATED)은 대부분 HW에서 지원하지 않으므로 소프트웨어 폴백이 필요합니다.
mqprio/taprio: TSN과 시간 게이트
TSN(Time-Sensitive Networking)은 이더넷에서 실시간 트래픽 보장을 위한 IEEE 802.1 표준 집합입니다. taprio qdisc는 802.1Qbv 시간 게이트(Time-Aware Shaper)를 구현하여, 특정 시간 슬롯에만 특정 traffic class의 전송을 허용합니다.
taprio 시간 게이트 스케줄링
# taprio 설정: 802.1Qbv 시간 게이트
tc qdisc replace dev eth0 parent root handle 100 taprio \
num_tc 3 \
map 2 2 1 0 2 2 2 2 2 2 2 2 2 2 2 2 \
queues 1@0 1@1 2@2 \
base-time 0 \
sched-entry S 01 200000 \
sched-entry S 02 100000 \
sched-entry S 04 700000 \
clockid CLOCK_TAI
# sched-entry 형식: S <gate_mask_hex> <duration_ns>
# gate_mask: bit 0=TC0, bit 1=TC1, bit 2=TC2
# S 01 = TC0만 open, S 02 = TC1만 open, S 04 = TC2만 open
# HW offload 활성화 (NIC 지원 시)
tc qdisc replace dev eth0 parent root handle 100 taprio \
num_tc 3 \
map 2 2 1 0 2 2 2 2 2 2 2 2 2 2 2 2 \
queues 1@0 1@1 2@2 \
base-time 0 \
sched-entry S 01 200000 \
sched-entry S 02 100000 \
sched-entry S 04 700000 \
flags 0x2 \
clockid CLOCK_TAI
# flags 0x2 = TAPRIO_FLAGS_FULL_OFFLOAD (완전 HW 오프로드)
# flags 0x1 = TAPRIO_FLAGS_TXTIME_ASSIST (SW 시간 계산, HW 전송)
ETF(Earliest TxTime First)와 LaunchTime
ETF qdisc는 taprio의 leaf로 부착되어, 각 패킷에 정확한 전송 시각을 지정합니다. 하드웨어 LaunchTime을 지원하는 NIC에서는 패킷이 NIC 내부 큐에서 정확한 시각에 전송됩니다.
# taprio + ETF 조합: 실시간 TC0에 정밀 시각 전송
tc qdisc replace dev eth0 parent 100:1 etf \
clockid CLOCK_TAI delta 200000 offload
# delta: 스케줄링에서 실제 전송까지의 여유 시간 (ns)
# offload: NIC 하드웨어 LaunchTime 사용
# 사용자 공간에서 SO_TXTIME으로 전송 시각 지정
# struct sock_txtime st;
# st.clockid = CLOCK_TAI;
# st.flags = SOF_TXTIME_DEADLINE_MODE;
# setsockopt(fd, SOL_SOCKET, SO_TXTIME, &st, sizeof(st));
ingress/clsact: TC_ACT 반환값과 패킷 흐름
clsact의 ingress/egress hook은 커널의 sch_handle_ingress()와 sch_handle_egress()에서 호출됩니다. 이 절에서는 TC_ACT_* 반환값의 정확한 의미, 리다이렉트/미러링 내부, policing과의 상호작용을 상세히 분석합니다.
Ingress Policing 상세
ingress에서의 policing은 토큰 버킷으로 구현됩니다. conform-exceed 파라미터로 적합/초과 패킷의 처리를 세밀하게 제어할 수 있습니다.
| conform-exceed | 적합 패킷 | 초과 패킷 | 용도 |
|---|---|---|---|
drop | pass | drop | 단순 rate limit |
pass/drop | pass | drop | 명시적 2-color |
pass/pipe | pass | 다음 action으로 | 초과 시 DSCP 마킹 |
drop/continue | drop | continue | 역방향 (초과만 통과) |
ok/reclassify | pass | 재분류 | 초과 시 다른 class로 |
IFB (Intermediate Functional Block)
IFB는 가상 네트워크 디바이스로, ingress 패킷을 IFB의 egress로 리다이렉트하여 큐잉 가능한 위치에 놓습니다. 이 절에서는 IFB의 내부 동작과 실전 구성 패턴을 상세히 분석합니다.
IFB 실전 구성 패턴
# ━━━ 패턴 1: 다운링크 CAKE shaping (가장 흔한 용도) ━━━
modprobe ifb
ip link add ifb-down type ifb
ip link set ifb-down up
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress matchall \
action mirred egress redirect dev ifb-down
tc qdisc replace dev ifb-down root cake \
bandwidth 200mbit ingress diffserv4 nat wash
# ━━━ 패턴 2: 선택적 리다이렉트 (특정 트래픽만 IFB) ━━━
tc filter add dev eth0 ingress protocol ip prio 10 \
flower ip_proto tcp dst_port 80 \
action mirred egress redirect dev ifb-down
tc filter add dev eth0 ingress protocol ip prio 20 \
flower ip_proto tcp dst_port 443 \
action mirred egress redirect dev ifb-down
# 나머지 트래픽은 IFB 없이 바로 IP 스택으로
# ━━━ 패턴 3: 양방향 shaping (WAN uplink + downlink) ━━━
# uplink: eth0 egress에 직접 cake
tc qdisc replace dev eth0 root cake \
bandwidth 50mbit diffserv4 nat
# downlink: eth0 ingress → IFB → cake
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress matchall \
action mirred egress redirect dev ifb-down
tc qdisc replace dev ifb-down root cake \
bandwidth 200mbit ingress diffserv4 nat
디버깅 & 모니터링
TC 설정이 기대대로 동작하지 않을 때, 체계적인 디버깅 방법이 필요합니다. 이 절에서는 tc monitor, ftrace, BPF 트레이싱, perf를 활용한 심층 디버깅을 다룹니다.
tc monitor를 이용한 실시간 추적
tc monitor는 RTNETLINK를 감시하여 qdisc/class/filter/action의 추가/삭제/변경을 실시간으로 보여줍니다. 여러 자동화 도구(CNI, cloud-init, NetworkManager)가 TC 규칙을 동적으로 조작하는 환경에서 필수입니다.
# 모든 TC 이벤트 실시간 감시
tc monitor
# 특정 이벤트만 필터
tc monitor qdisc # qdisc 변경만
tc monitor filter # filter 변경만
tc monitor action # action 변경만
# 다른 터미널에서 규칙 추가하면 monitor에 즉시 표시
# added qdisc htb 1: dev eth0 root ...
# added class htb 1:10 dev eth0 parent 1: ...
ftrace로 Qdisc 내부 추적
# TC 관련 커널 함수 추적
echo 1 > /sys/kernel/debug/tracing/events/qdisc/enable
cat /sys/kernel/debug/tracing/trace_pipe
# 특정 함수 추적 (function_graph)
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo htb_enqueue > /sys/kernel/debug/tracing/set_graph_function
echo htb_dequeue >> /sys/kernel/debug/tracing/set_graph_function
cat /sys/kernel/debug/tracing/trace_pipe
# net:net_dev_queue tracepoint — qdisc enqueue 시점
echo 1 > /sys/kernel/debug/tracing/events/net/net_dev_queue/enable
# net:net_dev_xmit tracepoint — 실제 드라이버 xmit 시점
echo 1 > /sys/kernel/debug/tracing/events/net/net_dev_xmit/enable
# 두 시점의 시간 차이 = qdisc에서 보낸 시간
BPF 트레이싱으로 TC 분석
# bpftrace: qdisc enqueue/dequeue 지연 측정
bpftrace -e '
kprobe:htb_enqueue {
@start[arg0] = nsecs;
}
kprobe:htb_dequeue /@ start[arg0]/ {
@latency = hist(nsecs - @start[arg0]);
delete(@start[arg0]);
}'
# bpftrace: 드롭된 패킷 추적
bpftrace -e '
tracepoint:skb:kfree_skb {
@drops[kstack] = count();
}'
# tc-bpf 프로그램 자체의 실행 시간 측정
bpftrace -e '
kprobe:cls_bpf_classify {
@start[tid] = nsecs;
}
kretprobe:cls_bpf_classify /@start[tid]/ {
@classify_ns = hist(nsecs - @start[tid]);
delete(@start[tid]);
}'
# perf로 TC 관련 CPU 프로파일링
perf record -g -a -e net:net_dev_xmit -- sleep 10
perf report
TC 트러블슈팅 체크리스트
| 증상 | 확인 사항 | 명령 |
|---|---|---|
| 트래픽이 분류되지 않음 | filter prio 순서, protocol 지정, default class | tc -s filter show dev eth0 |
| shaping이 안 되는 것 같음 | backlog=0이면 qdisc가 병목이 아님, BQL/드라이버 확인 | tc -s qdisc show, ethtool -S |
| 지연이 줄지 않음 | BQL limit, 드라이버 TX ring, TCP 출력 버퍼 | cat /sys/class/net/*/queues/tx-*/byte_queue_limits/* |
| HW offload가 안 됨 | hw-tc-offload 활성화, skip_sw/in_hw 확인 | ethtool -k eth0, tc -s filter show |
| IFB 패킷이 사라짐 | ifb 디바이스 UP 상태, mirred redirect 확인 | ip link show ifb0, tc -s filter show ingress |
| "quantum too small" 경고 | r2q 조정 또는 quantum 직접 지정 | dmesg | grep quantum |
| taprio 시간이 안 맞음 | PTP 동기화, clockid, base-time | phc2sys -s eth0 -c CLOCK_REALTIME |
| BPF 프로그램이 안 붙음 | clsact qdisc 존재, 프로그램 검증 에러 | tc qdisc show dev eth0, bpftool prog |
TC 관련 커널 설정 (Kconfig)
| 옵션 | 설명 | 필수 여부 |
|---|---|---|
CONFIG_NET_SCHED | TC 프레임워크 전체 활성화 | 필수 |
CONFIG_NET_SCH_HTB | HTB qdisc | 일반적으로 필요 |
CONFIG_NET_SCH_FQ_CODEL | fq_codel qdisc (기본 qdisc) | 거의 필수 |
CONFIG_NET_SCH_CAKE | CAKE qdisc | shaping 시 |
CONFIG_NET_SCH_TAPRIO | taprio qdisc (TSN) | TSN 환경 |
CONFIG_NET_SCH_ETF | ETF qdisc (LaunchTime) | TSN 환경 |
CONFIG_NET_SCH_INGRESS | ingress/clsact qdisc | ingress 제어 시 |
CONFIG_NET_SCH_MQPRIO | mqprio qdisc | 멀티큐 환경 |
CONFIG_IFB | IFB 가상 디바이스 | ingress shaping 시 |
CONFIG_NET_CLS_FLOWER | flower classifier | 현대 설정에 필수 |
CONFIG_NET_CLS_BPF | BPF classifier (tc-bpf) | BPF 사용 시 |
CONFIG_NET_CLS_U32 | u32 classifier | 레거시 호환 |
CONFIG_NET_ACT_MIRRED | mirred action | redirect/mirror 시 |
CONFIG_NET_ACT_CT | conntrack action | CT offload 시 |
CONFIG_NET_ACT_POLICE | police action | policing 시 |
CONFIG_NET_ACT_BPF | BPF action | act_bpf 사용 시 |
참고자료
- tc(8) — 전반적인 명령 구조,
-g,monitor, block/chain, qdisc/class/filter/action 문법 - tc-cake(8) — CAKE의
bandwidth,diffserv*,autorate-ingress,split-gso, host isolation - tc-fq(8) — TCP pacing 친화적 FQ qdisc,
maxrate,flow_limit, pacing 관련 옵션 - tc-fq_codel(8) —
target,interval,flows, ECN 기본 활성화 - tc-mqprio(8) —
map,queues,hw, shaper 모드, frame preemption - tc-mirred(8) — IFB redirect, mirror/redirect, nesting 제한
- tc-bpf(8) — direct-action 모드와 TC BPF 부착
- tc-flower(8) —
skip_sw,skip_hw,hw_tc, offload 친화적 classifier - tc-actions(8) — action 재사용, control semantics, action lifecycle
- tc-cgroup(8) —
net_cls기반 cgroup classifier의 적용 범위와 한계 - Linux Kernel Documentation: TC queue based filtering —
skbedit priority,queue_mapping,hw_tc를 통한 큐 steering - Linux Kernel Documentation: HOWTO for multiqueue network device support —
multiq의 의미와 head-of-line blocking 완화 - Linux Kernel Documentation: Scaling in the Linux Networking Stack — RSS/RPS/XPS와 멀티큐 네트워킹 배경
- Linux Kernel Documentation: IP Sysctl —
tcp_limit_output_bytes같은 TCP 출력 제한 관련 sysctl - tc-htb(8) — HTB(Hierarchical Token Bucket) qdisc의 rate, ceil, burst 등 클래스 계층 설정입니다
- tc-tbf(8) — TBF(Token Bucket Filter) qdisc의 rate, burst, latency 파라미터를 설명합니다
- tc-netem(8) — 네트워크 에뮬레이션 qdisc로 지연, 손실, 중복, 재정렬을 시뮬레이션합니다
- tc-u32(8) — u32 분류자의 패턴 매칭 문법과 해시 테이블 기반 필터링을 설명합니다
- tc-red(8) — RED(Random Early Detection) qdisc의 min, max, probability 파라미터입니다
- LWN: Controlled Delay (CoDel) and FQ-CoDel — CoDel/FQ-CoDel AQM 알고리즘의 설계 원리를 분석합니다
- LWN: A piece of CAKE — CAKE qdisc의 설계 철학과 기존 fq_codel 대비 개선점을 설명합니다
- RFC 8290: Flow Queue CoDel — FQ-CoDel 알고리즘의 IETF 공식 표준 명세입니다
- RFC 8033: PIE (Proportional Integral controller Enhanced) — PIE AQM 알고리즘의 공식 표준입니다
관련 문서
이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.