Шейпінг пакетного IP-трафіку за допомогою tc+cls_bpf
Ахтунг: у цій замітці є надсекретне знання, яким володіє дуже невелика кількість людей. Якщо ви усвідомлюєте, що від роботи з tc у вас може погіршитися самопочутя, може почати випадати волосся в одних місцях і рости в інших, може піти дружина й забрати дітей, ви можете захворіти на алкоголізм тощо, але вас це не лякає — читайте далі.
Зазвичай, для шейпінгу трафіку на маршрутизаторах чи програмних шейперах використовують зв’язку tc+iptables. Тут iptables маркує пакети, а tc за допомогою класифікатора fwmark їх розкидає по ієрархії шейпера (переважно, використовується HTB). Однак цей підхід має два суттєвих зауваження. Перше полягає в тому, що дуже часто використовувати такий потужний інструмент як iptables для маркування трафіку надлишково. Друге зауваження стосується того, що ця зв’язка дуже повільно працює. Я пам’ятаю, як на другому пні кілька мегабіт могли вижерти софтовими перериваннями весь проц. Незважаючи на це, надзвичайно багато мануалів у цих ваших інтернетах пропонують шейпити трафік саме так.
Другий підхід полягає в застосуванні класифікатора u32. Він значно більш обмежений у своїх функціях; навіть для того, щоб захопити фільтром певний діапазон портів, доводиться або збочуватися з масками, або застосовувати інший класифікатор, ematch, який, до того ж, має протипоказання, якщо його використовувати для шейпінгу трафіку в кілька напрямів одночасно. Мені навіть вдалося успішно зашейпити за допомогою u32 модний і мімімішний IPv6, але скрипти на 30 КіБ після цього вперто відмовлялися вміщатися в голові повністю. Однак, u32 набагато швидший за зв’язку із застосуванням iptables, і дуже багато мануалів також пропонують його використовувати. Я сам його тримав і в себе у продакшні, і кілька шейперів на попередній роботі так і побудовані, і, підозрюю, успішно працюють і досі, хоча я маю великі сумніви, що хтось після мене в них розібрався.
Зрозуміло, є ще й третій шлях, і якраз ним ми й підемо.
Класифікатор cls_bpf додали в ядро 3.13 у жовтні 2013-го року. Слава яйцям, це ядро потрапило в 14.04 LTS, а от у сімьорочкі (я про CentOS, звісно, а не про ту срамоту, про яку ви могли подумати) цього щастя нема. Ну та особисто в мене, звісно, майже завжди найостанніше ядро, тому пофігу. Окрім підтримки зі сторони ядра потрібна ще й підтримка в iproute2. Тому на це треба зважати, якщо хтось таки захоче завести цю кухню на центосі.
Так у чому ж прикол цієї кухні?
BPF — це така потенційно мегауніверсальна штука, яка задумувалася зовсім не для того, для чого її пиляють зараз (гугліть про eBPF). На наше щастя, саме ця штука дає змогу скомпілити потрібний людиночитабельний класифікатор трафіку із синтаксисом libpcap у байт-код, який зрозуміє ядро, причому, скомпілити «на льоту». Якщо вас лякає libpcap, то я скажу інше слово: tcpdump. Саме ним і компілитимемо.
Нагадаю, що звичайне правило фільтрації tc із застосуванням класифікатора u32 має приблизно такий нудний вигляд:
tc filter add dev eth0 protocol ip parent 1: u32 match ip dst 1.2.3.4 match ip dport 80 0xffff flowid 1:10
Ніфіга непонятно, якщо, звісно, вас від tc уже давно не нудить. Насправді, у класифікаторі u32 нема нічого страшного, окрім, звісно, того, що його синтаксис не гнучкий, і якщо треба ж таки забити діапазон портів, вилазе отаке страхіття:
tc filter add dev eth0 protocol ip parent 1: u32 match ip dst 1.2.3.4 match ip dport 62464 0xfc00 flowid 1:10
Правда ж, не дуже очевидно, що я хотів виокремити порти від 62464 до 63487?
А що натомість пропонує cls_bpf? А щось типу такого:
tc filter add dev eth0 parent 1: bpf run bytecode "тут-іде-байткод" flowid 1:10
Уже легше, правда? Тільки звідки брати байткод? Згадуємо, що його можна згенерувати із правил tcpdump’а, і робиться це отак:
tcpdump -i lo -ddd "ip and dst host 1.2.3.4 and portrange 62464-63487 and udp" | tr '\n' ','
Ця команда видасть послідовність чисел, які треба просто підставити в команду вище. Зверніть увагу на вираз для фільтра. Правда, так краще? Мережевий інтерфейс можете вказувати довільний, це ніщо не змінить; я підставив лупбек, бо він (майже) гарантовано є в системі.
У скриптах я б пропонував робити так:
function bpf_compile { echo $(tcpdump -i lo -ddd $@ | tr '\n' ',') } ... tc filter add dev eth0 parent 1: bpf run bytecode "$(bpf_compile ip and dst host 1.2.3.4 and portrange 62464-63487 and udp)" flowid 1:10
Здається, так узагалі супер. Синтаксис для виразів фільтрів можна знайти в man 7 pcap-filter.
Якщо говорити про вимірювання швидкодії, то можу видати таку статистику. На моєму домашньому маршрутизаторі з трафіком 30–40 мегабіт на секунду класифікатор u32 віджирав приблизно 6–7% CPU відповідно до даних perf top -U (у мене загалом 72 фільтра на вхід і вихід, я пріоритезую трафік на різні хости і порти). Після того, як я замінив його на cls_bpf, завантаженість CPU новим класифікатором склала ~2%. От вам і економія.
One last thing. Не забудьте включити JIT через sysctl. Так воно працює набагато швидше:
net.core.bpf_jit_enable = 1
UPDATE: презентація по темі.
UPDATE 2: iproute2 в 14.04 ці ніштяки не підтримує :(. Ставте 14.10.
UPDATE 3: я був би не я, якби не написав свій компілятор BPF-опкодів, який працює набагато швидше за tcpdump.
UPDATE 4: вау, у стоковому ядрі 14.10 нема BPF_JIT. Компільте руками.
UPDATE 5: здається, JIT є тільки на 64-бітних ядрах. Ну ОК.
Залишити відповідь