Кейс · высоконагруженное сетевое программное обеспечение
Высоконагруженный HTTP-сервер на C++
Учебно-исследовательский HTTP-сервер на C++: вместо потока на каждого клиента он распределяет тысячи соединений между несколькими рабочими потоками и с помощью системного вызова select() определяет, какие из них готовы к обмену данными. Нагрузочные испытания показали конкретные пределы — до 1024 соединений, около 220 МБ/с и ~3500 запросов в секунду.
Главный результат
Один процесс с фиксированным числом рабочих потоков стабильно обслуживает до 1024 одновременных клиентов — вместо создания отдельного потока на каждое соединение.
- 1024
- макс. соединений
- ~220 МБ/с
- пропускная способность
- ~3500
- запросов в секунду
Почему обычный сервер не выдерживает тысячи клиентов
Поток на каждого клиента
Классическая модель заводит отдельный поток на соединение — на тысячах клиентов это расходует память и время планировщика.
Ресурсы кончаются раньше нагрузки
Сервер исчерпывает ресурсы на переключении контекстов и потреблении памяти задолго до реального предела канала.
Пределы неизвестны
Без нагрузочных испытаний непонятно, сколько клиентов и какой трафик сервер выдержит на самом деле.
Небезопасная отдача файлов
Наивная отдача статических файлов открывает доступ к посторонним файлам и падает на больших ответах.
Что нужно было сделать
Написать веб-сервер для отдачи статических файлов — страниц, оформления, изображений — который обслуживает множество клиентов небольшим фиксированным числом потоков, а не выделяет поток на каждого. И измерить его реальные пределы под нагрузкой.
Что я сделал
Построил сервер на неблокирующем вводе-выводе: отдельный поток-приёмник принимает соединения и раздаёт их наименее загруженному из пула рабочих потоков, а каждый рабочий своим вызовом select() опрашивает закреплённые за ним соединения на готовность к чтению и записи. Число рабочих потоков задаётся параметром, а не растёт с числом клиентов, — это экономит ресурсы компьютера на тысячах подключений.
Каждое соединение проходит через конечный автомат состояний — чтение запроса, отправка заголовков, отправка тела, закрытие. Реализовал разбор запросов и корректные ответы, потоковую передачу больших статических файлов порциями по 64 КБ без полной загрузки в память и защиту от типичных атак и попыток получить доступ к посторонним файлам.
Как сервер обрабатывает соединения
Принимает соединения
Отдельный поток-приёмник принимает подключения и передаёт каждое наименее загруженному рабочему потоку — без потока на клиента.
Ждёт готовности
Каждый рабочий поток своим вызовом select() опрашивает закреплённые за ним соединения и обрабатывает только готовые к чтению или записи.
Отдаёт данные
Разбирает запрос и потоково передаёт статический файл порциями по 64 КБ, не загружая его целиком в память.
Освобождает
Закрывает соединение и возвращает рабочий поток к обслуживанию остальных клиентов.
Что умеет сервер
Мультиплексирование соединений
Каждый рабочий поток своим вызовом select() обслуживает множество закреплённых за ним соединений — тысячи клиентов на небольшом числе потоков.
Потоковая отдача файлов
Передаёт большие статические файлы порциями по 64 КБ, без полной загрузки в память.
Основные возможности HTTP
Разбор запросов, корректные ответы и отдача статических файлов: страниц, стилей, изображений.
Защита от атак
Отсекает выход за пределы каталога через канонизацию пути (realpath) и запрет «..», блокирует скрытые файлы, ограничивает размер запроса и закрывает подвисшие соединения по таймауту.
Нагрузочное тестирование
Серия испытаний в k6 с графиками; разные подходы к обработке соединений разобраны аналитически.
Экономия ресурсов
Никакого потока на клиента — минимум переключений контекста и памяти под нагрузкой.
Пределы определены экспериментально
Испытания шли в изолированном Docker-контейнере с лимитом 2 ядра CPU и 1 ГБ памяти: нагрузку создавал k6, метрики снимались через Prometheus и Grafana. Три сценария — поиск предела одновременных соединений (долгое удержание запросов), пропускная способность на файле 1 МБ и точка деградации по числу запросов в секунду на отдаче страницы, попутно с контролем загрузки процессора.
Пределы вышли конкретные: до 1024 одновременных подключений — это ограничение системного вызова select() на число отслеживаемых дескрипторов (FD_SETSIZE), — около 220 МБ/с, дальше упор в сетевой интерфейс, и примерно 3500 запросов в секунду до заметного роста задержек. Подходы к обработке соединений разобраны аналитически — почему пул потоков с select() предпочтён схеме «поток на клиента», — а измеренные цифры показывают пределы выбранной модели.

Как это выглядит
Стек
C++
- Make
- k6
Prometheus
Grafana
Что изменилось
Было
- —Поток на каждого клиента перегружает память и планировщик
- —Ресурсы кончаются задолго до предела канала
- —Реальные пределы сервера — неизвестны
Стало
- ✓Тысячи клиентов на фиксированном числе потоков
- ✓Большие файлы отдаются потоково, без загрузки в память
- ✓Пределы измерены: 1024 соединения, ~220 МБ/с, ~3500 запросов/с
Сервер показал, что модель с мультиплексированием соединений через select() и неблокирующим вводом-выводом обслуживает тысячи клиентов ограниченным числом потоков — с измеренными пределами и защитой отдачи файлов.