Проблема C10k (1999): как обслуживать 10 000 соединений одновременно. Классическая модель — процесс или поток на соединение (Apache prefork, старые Java-серверы): соединение читается блокирующим read, пока данных нет, поток спит. Простой код, но:
- каждый поток — стек (мегабайт виртуальной памяти, реально десятки килобайт и больше), структуры ядра;
- десятки тысяч потоков — постоянные переключения контекста и вымытые кэши;
- большинство соединений простаивают (keep-alive, вебсокеты, долгий опрос), а ресурсы на них держатся.
Неблокирующий ввод-вывод и мультиплексирование. Сокеты переводятся в неблокирующий режим: read без данных сразу возвращает EAGAIN. Один поток спрашивает ядро, какие сокеты готовы:
select и poll — каждый раз передают весь список и ядро проверяет все: O(n) на вызов, при 100 000 соединений — дорого;- epoll (Linux), kqueue (BSD, macOS), IOCP (Windows) — сокеты регистрируются один раз (
epoll_ctl), а epoll_wait возвращает только готовые. Стоимость пропорциональна числу событий, а не соединений.
Event loop: цикл epoll_wait → для каждого готового сокета вызвать обработчик → обработчик читает, сколько есть, продвигает состояние соединения (разбор запроса — это конечный автомат) и, если надо ждать, регистрирует интерес и возвращает управление. Так работают nginx, HAProxy, Redis, Node.js (libuv), Netty, asyncio, Go-рантайм внутри.
Масштабирование на ядра: event loop на каждое ядро; соединения распределяются через SO_REUSEPORT (ядро раскидывает новые соединения по слушающим сокетам) или передачей от акцептора.
Что нельзя делать в цикле: блокирующие вызовы (синхронный драйвер базы, чтение файла, DNS-резолв в старых libc) и долгие вычисления — они останавливают все соединения потока. Их выносят в пул потоков.
Что ещё ограничивает: лимит файловых дескрипторов (ulimit -n), память буферов сокетов (tcp_rmem/tcp_wmem), очередь accept (somaxconn), эфемерные порты у клиента, conntrack в NAT/фаерволе.
Дальше: io_uring — кольцевые очереди запросов и завершений между приложением и ядром: операции (включая файловые) ставятся пачкой без системного вызова на каждую. C10M — миллионы соединений с обходом сетевого стека ядра (DPDK, XDP) — для балансировщиков и специальных систем.