Привет! Меня зовут Дима Веселов, уже три года я развиваю облачные технологии в команде Evolution App Services как техлид. Мой путь начинался с классической backend-разработки на Python, но со временем я все глубже погружался в то, как работает инфраструктура, сетевые протоколы, Kubernetes. Сегодня я хочу рассказать, как eBPF буквально в два присеста позволяет делать то, что раньше требовало невероятных усилий.

Кому будет полезен этот материал? В первую очередь разработчикам PaaS-платформ, DevOps-инженерам и архитекторам, которым тесно в рамках классического HTTP-only serverless. Расскажу, как обеспечить масштабирование с нуля для любых TCP-приложений без переписывания их кода.

Serverless — это только про HTTP

Представьте ситуацию: вы хотите запустить PostgreSQL в режиме бессерверных вычислений. Чтобы не платить за простаивающую виртуальную машину, а только за время реального использования. Или SSH-сервер для временного доступа, например, при организации инфраструктурного паттерна Bastion. Или даже Minecraft-сервер, чтобы пару часов поиграть с друзьями после работы, а потом забыть про него — пусть сам масштабируется в ноль активных экземпляров.

Но есть проблема: существующие serverless-платформы поддерживают публикацию только HTTP-приложений и производных, например WebSocket или gRPC, и именно это ограничение не позволяет запустить целый ряд сервисов вместе с «бессерверной семантикой».

Отчасти такое ограничение обходится, и мы можем изменить входную точку в serverless-платформе. Например, можем добавить API Gateway, который принимает соединение и умеет маршрутизировать запросы с протоколом, отличным от HTTP (так делает Neon PostgreSQL), но такое решение также имеет свои особенности, например, отсутствие универсальности.

Почему так

Взглянем на архитектуру Knative от Google, одного из самых популярных serverless-фреймворков.

Так выглядит request flow в Knative
Так выглядит request flow в Knative

Между пользователем и контейнером стоят три компонента:

  1. HTTP-роутер — это, по сути, Ingress, который слушает 80 и 443 порты 24/7 и маршрутизирует запросы.

  2. Активатор — буферизирующий компонент, который держит соединение, пока контейнер стартует.

  3. Queue-прокси, который также буферизирует запросы, но уже на уровне конкретного контейнера.

Такая схема отлично работает, но только пока у вас есть один протокол — HTTP. В таком случае Ingress запущен в круглосуточном режиме и готов принять входящее соединение, клиент не получает ошибку connection refused, даже если у запрашиваемого контейнера отсутствуют активные экземпляры.

Что нужно сделать, чтобы добавить поддержку PostgreSQL? Придется переписывать роутер, чтобы он понимал протокол PostgreSQL wire protocol. Хотите SSH? Снова переписываем. Redis? Опять переписываем. А потом еще и поддерживаем все это богатство.

Чтобы выйти за эти ограничения и не писать отдельный прокси под каждый протокол, нам нужен универсальный инструмент, работающий как можно ближе к сетевому стеку операционной системы. И здесь на помощь приходит eBPF.

Что такое eBPF и зачем он нам нужен

Три сферы, которые отлично вписываются в применение eBPF – networking, observability, security
Три сферы, которые отлично вписываются в применение eBPF – networking, observability, security

eBPF (extended Berkeley Packet Filter) — это не какая-то маргинальная технология для гиков. Это целая экосистема с поддержкой отдельной организации — eBPF Foundation.

С помощью eBPF мы можем модифицировать поведение ядра Linux без переписывания самого ядра. Хотите изменить логику работы сетевого стека? Добавить собственную фильтрацию на уровне пакетов? Перехватывать системные вызовы? Все это делается через eBPF-программы, которые загружаются в ядро динамически.

Немного ис��ории

Все началось в далеком 1992 году, когда создали BPF (Berkeley Packet Filter) для фильтрации сетевых пакетов. Уже тогда там была виртуальная машина с набором инструкций — это делало технологию кроссплатформенной. А в 2014 году разработчики ядра выпустили enhanced-версию — eBPF, добавив поддержку новых архитектур процессоров и ключевую фичу: примитивы для коммуникации с userspace.

Кто уже использует eBPF

С уверенностью можно сказать, что технология прошла проверку боем и используется в production-ready решениях:

  • Cilium — самый быстрый CNI-плагин для Kubernetes, использующий XDP для обработки пакетов. Производительность в разы выше классических решений на iptables.​

  • Pixie и Coroot — инструменты Zero Instrumentation Observability: они автоматически перехватывают трафик, парсят HTTP/Kafka/PostgreSQL пакеты и добавляют метки для телеметрии без изменения вашего приложения.​

  • Ecapture — перехватывает зашифрованный TLS-трафик внутри Kubernetes для отладки Zero Trust архитектур.​

Как это устроено под капотом

Верхнеуровневая архитектура eBPF: kernel space, user space и взаимодействие с процессом
Верхнеуровневая архитектура eBPF: kernel space, user space и взаимодействие с процессом

Любая eBPF-программа — э��о, на самом деле, две программы:

  1. Kernel space часть (код на C)

    1. Исполняется прямо в ядре Linux с невероятной скоростью.

    2. Имеет жесткие ограничения: нельзя использовать внешние библиотеки, есть лимит на длину функции и циклы. Верификатор ядра строго проверяет код перед загрузкой.

    3. Позволяет прикрепить хуки к системным вызовам ядра.

  2. User space (Go, Python, Rust)

    1. Обычная программа без ограничений.

    2. Может вызывать Kubernetes API, открывать сокеты, писать в файлы.

    3. Общается с kernel-частью через специальные примитивы для коммуникации.

Примитивы для коммуникации хочется отметить отдельно, так как это одна из ключевых фичей:

  • Глобальные переменные — например, счетчик пакетов, который kernel обновляет, а userspace читает.

  • Хэшмапы (BPF maps) — словари для передачи данных.

  • Ring buffers — очереди для уведомлений: получили пакет → отправили событие в userspace.

Таким образом, eBPF дает нам возможность менять низкоуровневое поведение системы и реагировать на события с помощью верхнеуровневого языка.

Но как применить эту технологию для создания универсальной serverless-платформы?

Еще немного теории: RFC 793 (TCP)

Ключевые механизмы, которые помогут нам реализовать serverless для любого верхнеуровневого протокола, уже описаны в спецификации TCP (RFC 793) от 1981 года.

1. TCP-рукопожатие
TCP гарантирует корректную установку соединения через трехстороннее рукопожатие (three-way handshake), но если запрашиваемого TCP-сервера нет, клиент получит RST (reset) пакет = "connection refused". Наша задача — не допустить этого.

2. TCP SYN Retransmission
Это то, на что мы будем опираться. Если ответ на SYN пакет не получен, клиент автоматически повторяет попытку подключения несколько раз с экспоненциальной задержкой. Это стандартное поведение любой ОС.​ И только по истечении некоторого количества времени (таймаута), клиент получит ошибку connection timeout (например, в Linux это около 1 минуты).

Архитектура решения

Вот как мы объединяем возможности eBPF и стандарт TCP.

Мы используем eBPF для скрытого перехвата первого пакета и запуска контейнера, пока клиентское приложение думает, что сеть просто немного лагает и делает ретраи.

Последовательная диаграмма масштабирования с нуля для TCP-приложений
Последовательная диаграмма масштабирования с нуля для TCP-приложений

Шаг 1: Первый SYN-пакет → drop
Пользователь отправляет первый TCP SYN к вашему serverless PostgreSQL. Но контейнера еще нет — он замасштабирован в ноль.

eBPF-программа в ядре перехватывает этот SYN-пакет. Проверяет состояние в BPF map: контейнер запущен? Нет.

Действие: мы просто отбрасываем (DROP) пакет. Клиент не получает ни ACK, ни RST. Он считает, что пакет потерялся в пути.

Шаг 2: Уведомление в userspace → scale up
При отбрасывании пакета, в случае если активного контейнера нет, kernel-часть отправляет событие в ring buffer – происходит уведомление нашей системы о том, что нужно масштабировать контейнер:

// Kernel space eBPF code (упрощенно)
struct event {
    __u32 src_ip;
    __u16 dst_port;
    __u8 protocol;
};
// Отправляем событие в userspace
bpf_ringbuf_submit(ring, event, sizeof(event));

User space программа (в нашем случае на Go) ловит событие и запускает масштабирование:

// User space handler
func onNewConnection(srcIP string, dstPort int) {
    // Вызываем Kubernetes API для масштабирования
    scaleUpDeployment(dstPort)
    // Ждем, пока pod станет Ready
    waitForHealthChecks(dstPort)
    // Обновляем BPF map: теперь контейнер активен
    updateBPFMap(dstPort, STATUS_READY)
}

Шаг 3: Второй SYN (retry) → pass through
Клиент автоматически (благодаря TCP retry) отправляет SYN еще раз через 1–3 секунды. К этому моменту контейнер уже поднялся. eBPF-программа видит его статус READY.

Действие: пропускаем пакет (PASS). Соединение устанавливается штатно, с помощью стандартного сетевого стека операционной системы.

Демонстрация: универсальность подхода

Давайте попробуем запустить несколько классических TCP-приложений и посмотрим, как получившееся решение будет с ними работать.

Кейс 1: iperf3 — тест скорости интернет соединения

Запускаем консольную команду, и в Kubernetes моментально стартует под, получаем скорость ~20 Гбит/с. Никаких прокси и копирования данных. Пакеты идут напрямую в контейнер, оверхед минимален.

Кейс 2: Minecraft — долгий холодный старт

Minecraft-сервер — отличный пример приложения с долгой инициализацией. В нашем примере он стартует примерно за ~30 секунд (подгружает карту, чанки, логику).

Вводим адрес в клиенте. Первый пакет дропается, контейнер создается. Пока сервер загружается, клиент делает несколько SYN retry. Как только сервер готов, происходит подключение.

Со стороны клиента все выглядит бесшовно. Единственное, что стоит учесть — увеличенная задержка при первом подключении. Но это лучше, чем платить за сервер 24/7, если играете пару часов в неделю.

Кейс 3: VNC — графический интерфейс

Подключаемся VNC-клиентом, контейнер поднимается. Вводим пароль и видим трансляцию рабочего стола. Этот кейс закрепляет гипотезу об универсальности решения. Не важно, какой L7-протокол использует приложение: VNC, PostgreSQL, SSH, главное, что используются стандартные механизмы TCP.

Почему это работает лучше альтернатив

  1. Полное отсутствие проксирующих компонентов. Меньше инфраструктурных компонентов — меньше расходов на эксплуатацию, меньше сетевых хопов — меньше финальная задержка.

  2. Совместимость из коробки. Механизм работает на любой ОС последних 25 лет. Не нужны специальные SDK на клиенте.

  3. Упрощение эксплуатации. Нет активаторов с буферами, которые могут упасть с OOM при всплеске нагрузки. Один легкий eBPF-агент на ноде решает все вопросы.

Вместо заключения

В результате перенос логики маршрутизации и буферизации на уровень операционной системы с помощью eBPF и TCP позволяет нам преодолеть фундаментальное ограничение serverless-подхода — зависимость от HTTP. Полученная архитектура поддерживает scale-to-zero для приложений, работающих поверх любого TCP-протокола, что значительно расширяет область применения бессерверных вычислений.

Кто уже попробовал, делитесь в комментариях: удалось насладиться преимуществами бессерверной жизни или, как говорят олды, «вы все еще кипятите?».