Получить доступ

Как один сервер держит сотни тысяч одновременных соединений?

SeniorСеть и ввод-вывод
Ещё спрашивают так:
  • Что такое проблема C10k?
  • Как работает epoll?
  • Почему nginx держит больше соединений, чем Apache с потоком на соединение?
Коротко

Модель «поток на соединение» упирается в память стеков и переключения. Решение — неблокирующие сокеты + epoll: один поток спрашивает ядро «какие из 100 000 сокетов готовы?» и обрабатывает только их.

  • select/poll — O(n) на каждый вызов; epoll/kqueue — регистрация один раз, ответ — только готовые
  • Event loop на ядро (nginx: worker на ядро, SO_REUSEPORT), обработка короткая и неблокирующая
  • Бездействующее соединение стоит килобайты, а не мегабайт стека

А ещё Дальше — io_uring (асинхронные операции без системного вызова на каждую), C10M — обход ядра (DPDK)

Так подсказку видит пользователь sotto на живом собеседовании — за пару секунд, пока вы думаете вслух.

Подробно

Проблема 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) — для балансировщиков и специальных систем.

Пример

import selectors, socket
sel = selectors.DefaultSelector()          # epoll в Linux
srv = socket.socket(); srv.bind(("", 8080)); srv.listen(4096); srv.setblocking(False)
sel.register(srv, selectors.EVENT_READ)

while True:
    for key, _ in sel.select():            # только готовые сокеты
        if key.fileobj is srv:
            conn, _ = srv.accept(); conn.setblocking(False)
            sel.register(conn, selectors.EVENT_READ)
        else:
            data = key.fileobj.recv(4096)
            if data: key.fileobj.send(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")
            else: sel.unregister(key.fileobj); key.fileobj.close()

Что хотят услышать

  • Почему поток на соединение не масштабируется: память и переключения.
  • Неблокирующие сокеты и мультиплексирование, разница select/poll и epoll.
  • Устройство event loop и запрет блокирующих операций в нём.
  • Системные лимиты и io_uring — для senior.

Частые ошибки

  • Считать, что epoll делает обработку параллельной — один цикл всё ещё один поток.
  • Делать синхронный запрос в базу внутри обработчика event loop.

Что спросят дальше

Связанные вопросы

Закрытая бета

Подсказки на собеседовании — в реальном времени

sotto слышит вопрос интервьюера и показывает короткий ответ и план, как в блоке «Коротко». Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем