Почему ваш ограничитель скорости в пользовательском пространстве, вероятно, плачет
Если вы когда-либо пытались реализовать ограничение скорости в пользовательском пространстве, то знаете это чувство. Пакеты поступают на сетевой интерфейс, проходят через несколько уровней ядра, обрабатываются системными вызовами, и к тому времени, когда ваша тщательно продуманная логика ограничения скорости получает возможность их проверить, вы уже проиграли битву за производительность. Это как пытаться остановить цунами садовым шлангом, катаясь на роликах.
eBPF (расширенный Berkeley Packet Filter) меняет всё уравнение. Переместив логику ограничения скорости непосредственно в ядро Linux, мы можем перехватывать трафик на самом раннем возможном этапе — до того, как он даже достигнет пользовательского пространства. В сочетании с Go для оркестрации и управления вы получаете лучшее из обоих миров: производительность ядра и удобную для разработчиков эргономику современной логики приложений.
Позвольте мне показать вам, как построить сетевой ограничитель скорости, который имеет смысл в производственной среде.
Понимание архитектуры: где магия встречается с физикой
Программа eBPF"] B -->|Проверка ограничения скорости| C{Решение о пакете} C -->|В пределах лимита| D["Разрешить в стек
XDP_PASS"] C -->|Превышен лимит| E["Отбросить пакет
XDP_DROP"] D --> F["Приложение в пользовательском пространстве"] B -->|Статистика| G["eBPF Maps
Общее состояние"] G -->|Программа Go
Обновляет политику| H["Конфигурационная карта
Настройка во время выполнения"] H -->|Новые лимиты| B
Архитектура здесь не сложная, но важно её понимать. Когда пакет поступает на ваш сетевой интерфейс, он сталкивается с вашей программой eBPF ещё до того, как сетевой стек Linux узнает об этом. Именно здесь вступает в игру XDP (eXpress Data Path) — это своего рода «привет» у входной двери ядра, где решения принимаются на скорости передачи данных.
Ваша программа eBPF поддерживает карты (думайте о них как о хеш-таблицах в пространстве ядра), которые отслеживают частоту запросов от клиента, хранят конфигурацию и собирают статистику. Ваше приложение Go выступает в роли маэстро, обновляя эти карты во время выполнения без необходимости перекомпиляции или перезагрузки программы ядра. Это горячее обновление для сетевых политик.
Основные строительные блоки: карты и управление состоянием
Карты eBPF — это связующее звено, удерживающее эту систему вместе. Для ограничения скорости вам обычно понадобятся три типа карт:
Хеш-карты LRU — ваше основное оружие. LRU означает «наименее недавно использованный», что означает, что ядро автоматически удаляет старые записи, когда вы достигаете предела памяти. Это идеально подходит для отслеживания IP-адресов клиентов и их текущего количества запросов — вы не хотите, чтобы память утекала, как ваши привычки отладки.
struct rate_counter {
__u64 requests;
__u64 window_start;
__u64 last_seen;
};
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 100000);
__type(key, __u32); // Исходный IP
__type(value, struct rate_counter);
} rate_limit_map SEC(".maps");
Карты массивов для конфигурации — ваша панель управления во время выполнения. Нужно настроить ограничение скорости без перекомпиляции? Обновите карту массива из вашего приложения Go, и программа eBPF подхватит её при следующем пакете.
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, __u32); // Запросы в секунду лимит
} config_map SEC(".maps");
Карты статистики позволяют вам наблюдать, что происходит внутри ядра:
struct rate_stats {
__u64 dropped_packets;
__u64 allowed_packets;
};
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, struct rate_stats);
} stats_map SEC(".maps");
Программа eBPF: где происходит реальная работа
Вот практическая реализация. Я расскажу вам о логике шаг за шагом, потому что понимание почему каждая строка существует, делает отладку бесконечно проще, когда что-то неизбежно идёт не так в 3 часа ночи.
#include <uapi/linux/bpf.h>
#include <uapi/linux/if_ether.h>
#include <uapi/linux/ip.h>
#include <linux/in.h>
#define WINDOW_SIZE_NS (1000000000UL) // 1 секунда в наносекундах
#define MAX_REQUESTS_PER_SECOND 1000
// И так далее...
Продвинутый метод: алгоритм Token Bucket
Скользящие окна работают, но они не очень изощрённые. Что если вы хотите разрешить эпизодические всплески, сохраняя при этом долгосрочную скорость? Token bucket — это ответ, и он достаточно элегантен, чтобы заслужить собственный раздел.
Концепция восхитительно проста: представьте себе ведро, в котором хранятся жетоны. Жетоны пополняются с постоянной скоростью (например, 100 в секунду). Каждый запрос стоит некоторого количества жетонов. Если ведро пустое, вы отбрасываете пакет. Давайте реализуем это:
struct token_bucket {
__u64 tokens; // Доступные жетоны (хранятся с точностью: жетоны * 1000)
__u64 burst_size; // Максимальное количество жетонов, которое может храниться в ведре
__u64 refill_rate; // Жетоны в секунду
__u64 last_refill; // Последнее время добавления жетонов
};
// И так далее...
Компиляция и развёртывание вашей программы eBPF
Давайте запустим этот код. Вам понадобится LLVM/Clang (не обычный GCC), потому что eBPF требует компиляции в набор инструкций BPF.
# Установка зависимостей (Ubuntu/Debian)
sudo apt-get install -y clang llvm linux-headers-$(uname -r)
# Компиляция программы eBPF
clang -O2 -target bpf -c rate_limiter.c -o rate_limiter.o
# Проверка компиляции
llvm-objdump -S rate_limiter.o
Теперь самое приятное — подключение его к вашему сетевому интерфейсу через XDP:
# Загрузка и подключение к вашему интерфейсу (замените eth0 на ваш интерфейс)
sudo ip link set dev eth0 xdp obj rate_limiter.o sec xdp
# Проверка подключения
sudo ip link show eth0
# Вы должны увидеть что-то вроде:
# xdp/id:123 prog/id:456 drv in
Для выгрузки:
sudo ip link set dev eth0 xdp off
Приложение Go: управление ограничителем скорости
Теперь самое интересное — управление этим монстром из пользовательского пространства. Вам понадобится программа Go, которая:
- Загружает программу eBPF: Использует пакет
ebpf - Обновляет конфигурационные карты: Изменяет ограничения скорости во время выполнения
- Читает статистику: Мониторит, что отбрасывается
package main
import (
"encoding/binary"
"flag"
"fmt"
"log"
"net"
"time"
"github.com/cilium/ebpf"
)
// И так далее...
Тестирование вашего ограничителя скорости в реальных условиях
Теория прекрасна, пока реальность не испортит вечеринку. Вот как на самом деле протестировать эту штуку: Настройка: Создайте виртуальную тестовую среду, используя сетевые пространства имён:
# Создание двух сетевых пространств имён
sudo ip netns add client
sudo ip netns add server
// И так далее...
Рассмотрение производства: когда теория встречается с реальностью
Реализация ограничения скорости в ядре мощна, но развёртывание в производстве требует мышления, выходящего за рамки счастливого пути: Ограничения для каждого клиента против глобальных ограничений: Примеры показывают отслеживание по IP, но вам могут понадобиться разные стратегии — глобальные ограничения на определённых портах, ограничения для подсетей или даже обнаружение аномалий на основе машинного обучения, поступающее в ваши карты eBPF. Ограничение с учётом GeoIP: Хотите быть более снисходительными к внутреннему трафику? Вы можете интегрировать данные GeoIP:
struct geo_rate_config {
__u32 domestic_limit; // Выше
__u32 foreign_limit; // Ниже
__u32 suspicious_limit; // Очень низкий
