SeniorДизайнРедкоЕщё не отвечали
Вам нужно спроектировать один сервер, выдерживающий 100k одновременных клиентских соединений, в большинстве своём простаивающих, но долгоживущих. Наивная модель «поток на соединение» рушится на таком масштабе, поэтому объясните, какую модель ввода-вывода и конкурентности вы построите вместо неё, почему она масштабируется там, где наивная — нет, сколько рабочих потоков вы запустите относительно числа ядер CPU и какие лимиты операционной системы придётся настроить.
Откажитесь от модели «поток на соединение» — 100k потоков исчерпают память и планировщик. Используйте событийный мультиплексор ввода-вывода (epoll/kqueue/IOCP) с неблокирующими сокетами, небольшой пул примерно из одного потока на ядро и уведомление о готовности за O(1). Это классическая проблема C10k/C10M.
- ✗Брать
select()/poll()при масштабе — ониO(n)на вызов и упираются около 1024 дескрипторов - ✗Плодить неограниченное число потоков, исчерпывая память стеков и топя планировщик в переключениях
- ✗Забывать настроить лимиты ядра —
ulimit -n, диапазон эфемерных портов, буферы сокетов
- →Почему edge-triggered режим
epollэффективнее, но хитрее level-triggered? - →Как
SO_REUSEPORTпомогает распределять accept между рабочими потоками?
Оглавление
Почему не «поток на соединение»
100k потоков — это 100k стеков (по умолчанию ~1–8 МБ каждый) плюс постоянные переключения контекста. Планировщик ядра захлёбывается, а большая часть потоков всё равно простаивает в блокирующем read(). Это и есть проблема C10k (а в современной постановке — C10M).
Событийная модель
Один поток обслуживает тысячи сокетов: ядро сообщает, какие дескрипторы готовы, и поток обрабатывает только их.
select()/poll()—O(n)на каждый вызов: ядро каждый раз заново сканирует весь набор. Не масштабируется.epoll(Linux),kqueue(BSD/macOS), IOCP (Windows) —O(1): набор интересующих дескрипторов регистрируется один раз, ядро возвращает только готовые.
int ep = epoll_create1(0);
epoll_event ev{};
ev.events = EPOLLIN | EPOLLET; // edge-triggered
ev.data.fd = listen_fd;
epoll_ctl(ep, EPOLL_CTL_ADD, listen_fd, &ev);
std::vector<epoll_event> events(1024);
for (;;) {
int n = epoll_wait(ep, events.data(), events.size(), -1);
for (int i = 0; i < n; ++i) {
int fd = events[i].data.fd;
if (fd == listen_fd) accept_new_connections(ep, listen_fd);
else handle_ready_socket(fd); // неблокирующий read/write
}
}
Архитектура целиком
- Неблокирующие сокеты (
O_NONBLOCK) — иначе один медленный клиент застопорит весь цикл событий. - Пул потоков ≈ по одному на ядро CPU, у каждого свой
epoll;SO_REUSEPORTраспределяет входящиеaccept. - Настройка ядра: поднять
ulimit -n(лимит дескрипторов), расширить диапазон эфемерных портов, увеличить буферы сокетов и backlog очередиaccept. - За цикл событий — никаких блокирующих операций: тяжёлую работу (диск, БД) выносить в отдельный пул.
Оглавление