Кейс · высоконагруженное сетевое программное обеспечение

Высоконагруженный HTTP-сервер на C++

Учебно-исследовательский HTTP-сервер на C++: вместо потока на каждого клиента он распределяет тысячи соединений между несколькими рабочими потоками и с помощью системного вызова select() определяет, какие из них готовы к обмену данными. Нагрузочные испытания показали конкретные пределы — до 1024 соединений, около 220 МБ/с и ~3500 запросов в секунду.

Роль
разработал и исследовал
Тип
HTTP-сервер на C++
Сфера
исследовательский проект

Главный результат

Один процесс с фиксированным числом рабочих потоков стабильно обслуживает до 1024 одновременных клиентов — вместо создания отдельного потока на каждое соединение.

1024
макс. соединений
~220 МБ/с
пропускная способность
~3500
запросов в секунду
01Проблема

Почему обычный сервер не выдерживает тысячи клиентов

Поток на каждого клиента

Классическая модель заводит отдельный поток на соединение — на тысячах клиентов это расходует память и время планировщика.

Ресурсы кончаются раньше нагрузки

Сервер исчерпывает ресурсы на переключении контекстов и потреблении памяти задолго до реального предела канала.

Пределы неизвестны

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

Небезопасная отдача файлов

Наивная отдача статических файлов открывает доступ к посторонним файлам и падает на больших ответах.

02Задача

Что нужно было сделать

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

03Решение

Что я сделал

Построил сервер на неблокирующем вводе-выводе: отдельный поток-приёмник принимает соединения и раздаёт их наименее загруженному из пула рабочих потоков, а каждый рабочий своим вызовом select() опрашивает закреплённые за ним соединения на готовность к чтению и записи. Число рабочих потоков задаётся параметром, а не растёт с числом клиентов, — это экономит ресурсы компьютера на тысячах подключений.

Каждое соединение проходит через конечный автомат состояний — чтение запроса, отправка заголовков, отправка тела, закрытие. Реализовал разбор запросов и корректные ответы, потоковую передачу больших статических файлов порциями по 64 КБ без полной загрузки в память и защиту от типичных атак и попыток получить доступ к посторонним файлам.

04Как это работает

Как сервер обрабатывает соединения

01

Принимает соединения

Отдельный поток-приёмник принимает подключения и передаёт каждое наименее загруженному рабочему потоку — без потока на клиента.

02

Ждёт готовности

Каждый рабочий поток своим вызовом select() опрашивает закреплённые за ним соединения и обрабатывает только готовые к чтению или записи.

03

Отдаёт данные

Разбирает запрос и потоково передаёт статический файл порциями по 64 КБ, не загружая его целиком в память.

04

Освобождает

Закрывает соединение и возвращает рабочий поток к обслуживанию остальных клиентов.

05Возможности

Что умеет сервер

Мультиплексирование соединений

Каждый рабочий поток своим вызовом select() обслуживает множество закреплённых за ним соединений — тысячи клиентов на небольшом числе потоков.

Потоковая отдача файлов

Передаёт большие статические файлы порциями по 64 КБ, без полной загрузки в память.

Основные возможности HTTP

Разбор запросов, корректные ответы и отдача статических файлов: страниц, стилей, изображений.

Защита от атак

Отсекает выход за пределы каталога через канонизацию пути (realpath) и запрет «..», блокирует скрытые файлы, ограничивает размер запроса и закрывает подвисшие соединения по таймауту.

Нагрузочное тестирование

Серия испытаний в k6 с графиками; разные подходы к обработке соединений разобраны аналитически.

Экономия ресурсов

Никакого потока на клиента — минимум переключений контекста и памяти под нагрузкой.

06Проверка нагрузкой

Пределы определены экспериментально

Испытания шли в изолированном Docker-контейнере с лимитом 2 ядра CPU и 1 ГБ памяти: нагрузку создавал k6, метрики снимались через Prometheus и Grafana. Три сценария — поиск предела одновременных соединений (долгое удержание запросов), пропускная способность на файле 1 МБ и точка деградации по числу запросов в секунду на отдаче страницы, попутно с контролем загрузки процессора.

Пределы вышли конкретные: до 1024 одновременных подключений — это ограничение системного вызова select() на число отслеживаемых дескрипторов (FD_SETSIZE), — около 220 МБ/с, дальше упор в сетевой интерфейс, и примерно 3500 запросов в секунду до заметного роста задержек. Подходы к обработке соединений разобраны аналитически — почему пул потоков с select() предпочтён схеме «поток на клиента», — а измеренные цифры показывают пределы выбранной модели.

Тест точки деградации — запросы в секунду и время отклика при росте нагрузки
07Скриншоты

Как это выглядит

08Технологии

Стек

Ядро
  • C++
Сборка
  • Make
Тестирование и метрики
  • k6
  • Prometheus
  • Grafana
09Результат

Что изменилось

Было

  • Поток на каждого клиента перегружает память и планировщик
  • Ресурсы кончаются задолго до предела канала
  • Реальные пределы сервера — неизвестны

Стало

  • Тысячи клиентов на фиксированном числе потоков
  • Большие файлы отдаются потоково, без загрузки в память
  • Пределы измерены: 1024 соединения, ~220 МБ/с, ~3500 запросов/с

Сервер показал, что модель с мультиплексированием соединений через select() и неблокирующим вводом-выводом обслуживает тысячи клиентов ограниченным числом потоков — с измеренными пределами и защитой отдачи файлов.

Хотите такой же результат?

Опишите идею или задачу — разберу бесплатно и предложу решение, сроки и смету. Ни к чему не обязывает.

Отвечу в течение 24 часов