Высоконагруженный STUN-сервер на Go
Данный проект представляет собой сверхбыстрый, отказоустойчивый и оптимизированный под экстремальные нагрузки STUN-сервер (Session Traversal Utilities for NAT), написанный на языке Go (Golang). Сервер разработан в соответствии со стандартами RFC 5389 и RFC 8489 и предназначен для использования в инфраструктурах реального времени (WebRTC, VoIP, SIP, онлайн-игры) с целью определения внешних IP-адресов и портов клиентов, находящихся за NAT. Проект полностью контейнеризирован с помощью Docker и Docker Compose, а также защищен на уровне ядра ОС Linux, что позволяет ему обрабатывать тысячи запросов в секунду (RPS) на минимальных серверных мощностях.
🚀 Архитектурные особенности и ключевые фишки
Архитектура проекта сфокусирована на достижении максимальной пропускной способности и минимального времени отклика (в тестах задержка составляет всего 20–30 мс):
- Концепция True Zero-Alloc (Нулевое выделение памяти в куче):
- Вместо аллокации нового буфера под каждый входящий UDP-пакет, каждый независимый
- воркер создает фиксированный срез байт (buf := make([]byte, 1500)) единожды
- перед запуском бесконечного цикла.
- Ответ клиенту формируется в том же самом массиве данных поверх исходного запроса (buf = buf[:cap(buf)]).
- Сборщик мусора Go (Garbage Collector) вообще не нагружается во время обработки сетевого флуда. Это исключает микропаузы (STW) сервера под нагрузкой.
- Использование современного пакета net/netip:
- Взамен старого типа net.IP (который является срезом и всегда аллоцируется в куче),
в коде применен пакет net/netip.
- Объект netip.AddrPort хранит данные в виде легковесной структуры фиксированного размера, которая обрабатывается исключительно на уровне быстрого стека процессора.
- Многопоточный пул воркеров (Worker Pool):
- Сервер автоматически определяет количество доступных логических ядер процессора
(runtime.NumCPU()) и запускает строго эквивалентное количество горутин-воркеров.
- Ядро Linux равномерно распределяет сетевые прерывания от UDP-сокета, утилизируя процессор на все 100%.
- Мягкое завершение работы (Graceful Shutdown):
- Сервер перехватывает системные сигналы остановки от операционной системы
или Docker-демона (SIGINT, SIGTERM).
- При получении команды на выключение сокет закрывается, воркеры корректно дорабатывают текущую итерацию, а sync.WaitGroup гарантирует завершение процессов без потери обрабатываемых пакетов.
🐳 Контейнеризация и Безопасность в Docker
Проект поставляется со специализированной конфигурацией развертывания, обеспечивающей максимальную производительность и изоляцию:
- Многоэтапная сборка (Multi-stage Build): Образ собирается в официальном контейнере golang:alpine, где происходит компиляция бинарника со флагами -ldflags="-s -w" (вырезаются отладочные символы, что уменьшает размер файла в 2 раза).
- Супер-легковесный и безопасный финальный слой (scratch): Финальный образ собирается на базе абсолютно пустого слоя FROM scratch. Внутри контейнера нет Linux-дистрибутива, нет пакетных менеджеров, утилит (sh, bash, ls) и зависимостей. Вес готового Docker-образа составляет всего ~15-20 МБ.
- Принцип Non-Root: Внутри контейнера процесс STUN-сервера запускается от имени непривилегированного системного пользователя stunuser. В случае гипотетической уязвимости в коде, злоумышленник не сможет выйти на хост-систему или получить права суперпользователя.
- Сетевой режим network_mode: "host": Контейнер подключен к сетевой карте VPS напрямую. Это исключает прослойку docker-proxy и встроенную маршрутизацию Docker, которая является главным «узким горлышком» для UDP-трафика и приводит к потере пакетов под нагрузкой.
🛠️ Системный тюнинг ОС Linux под Highload UDP
По умолчанию буферы сетевых сокетов в Linux настроены на низкую нагрузку. При шторме UDP-запросов буфер быстро переполняется, и ядро ОС начинает молча дропать (терять) пакеты еще до того, как Go-приложение успеет их прочитать. Для решения этой проблемы в проекте применяется сетевой тюнинг ядра через конфигурацию sysctl. Максимальные и дефолтные буферы приема (rmem) и отправки (wmem) сетевых сокетов расширяются до 25 Мегабайт (26214400 байт):
- net.core.rmem_max=26214400
- net.core.wmem_max=26214400
Эти настройки жестко фиксируются в системном файле /etc/sysctl.conf хост-машины, благодаря чему сервер гарантированно выдерживает кратковременные всплески трафика (микро-штормы), удерживая пакеты в памяти ОС до момента их вычитки воркерами.
📋 Структура конфигурационных файлов проекта
Проект имеет модульную и простую структуру, состоящую из следующих компонентов:
- main.go — Весь исходный код сервера. Содержит логику инициализации, пул воркеров, парсинг сырых байт STUN-заголовка, генерацию XOR-маппинга для обхода NAT и отправку ответов.
- go.mod — Файл описания Go-модуля (проект STUN на базе Go 1.27.1) с автоматической фиксацией низкоуровневых системных зависимостей (golang.org/x/sys).
- Dockerfile — Инструкция сборки безопасного микро-контейнера на базе scratch от лица non-root пользователя.
- docker-compose.yml — Манифест для оркестрации: настраивает запуск в режиме host, прокидывает переменные окружения, ограничивает ресурсы (Limits: 2 CPU, 128MB RAM) и защищает диск от переполнения логами (ротация логов ограничена 3 файлами по 10МБ).
- .env — Файл конфигурации переменных среды, где хранится внешний публичный IPv4-адрес сервера (PUBLIC_IP), необходимый для корректного заполнения STUN-аттрибутов ответа.
📈 Результаты тестирования пропускной способности
В ходе стресс-тестирования методом асинхронного UDP-флуда (параллельный запуск тысячи клиентов turnutils_stunclient), сервер показал следующие результаты:
- Линейная масштабируемость: Обработка пакета из 2000 одновременных запросов выполняется за ~1.19 сек, демонстрируя чистую скорость ~1550-1700 RPS (запросов в секунду) на один клиентский поток тестирования.
- Параллельная утилизация ядер: Время работы в пользовательском пространстве (user) и пространстве ядра (sys) превышает реальное время теста (real), что доказывает идеальную параллельную работу пула воркеров на всех доступных ядрах процессора хоста.