Опануйте команду find у Linux. Вона необхідна всім, хто працює з системами Linux. У цьому докладному посібнику ви дізнаєтеся, як використовувати команду find дл...
Блог компанії 3v-Hosting
14 хв.
Коли йдеться про продуктивність веб-серверів, то Nginx майже завжди згадують одним із перших. На ньому працюють інтернет-магазини, стрімінгові сервіси, API-платформи, CDN, корпоративні системи та мільйони звичайних сайтів. І справа не лише в архітектурі самого Nginx, а й у тому, наскільки ефективно він використовує можливості Linux.
Одним із таких механізмів є epoll. Це механізм, що дозволяє процесу одночасно стежити за величезною кількістю мережевих з’єднань і отримувати сповіщення лише тоді, коли з яким-небудь із них дійсно щось відбувається. Саме тому Nginx здатний обслуговувати десятки, а іноді й сотні тисяч відкритих з’єднань, не створюючи при цьому окремий потік для кожного клієнта.
У цій статті ми розберемося, як працює epoll, чим він відрізняється від select() та poll(), яким чином його використовує Nginx і чому сам по собі epoll ще не робить додаток швидшим.
Уявіть собі сервер, до якого одночасно підключено 50 000 клієнтів. Звучить лякаюче, але насправді більшість із них нічого не робить. Хтось уже отримав сторінку й чекає на наступну дію користувача, хтось тримає відкритим WebSocket, хтось повільно завантажує файл. І лише невелика частина клієнтів саме зараз надсилає дані або чекає на відповідь.
Припустимо, з 50 000 відкритих TCP-з’єднань лише близько 500 потребують обробки в даний момент, а це означає, що якщо сервер постійно перевірятиме стан усіх 50 тисяч сокетів, процесор витрачатиме більшу частину часу даремно, адже корисних подій майже немає, а перевіряти потрібно кожне з’єднання.
Перші мережеві сервери вирішували цю задачу досить прямолінійно: для кожного нового клієнта створювався окремий процес або потік. Звісно, при невеликій кількості підключень така модель працювала чудово. Кожен потік обслуговував лише свого клієнта, тому код виходив простим і зрозумілим.
Але зі зростанням навантаження почали проявлятися обмеження цього методу. Кожен потік займає пам’ять, а операційній системі доводиться постійно перемикати процесор між тисячами потоків. У якийсь момент значна частина ресурсів починає витрачатися вже не на безпосередню обробку запитів, а на обслуговування самої моделі паралелізму.
Тоді знадобився інший підхід - один процес мав навчитися працювати одразу з великою кількістю з’єднань. Так з’явився системний виклик select(), використовуючи який додаток передавав ядру список файлових дескрипторів і запитував, які з них готові до читання або запису.
Для свого часу це було серйозним кроком уперед. Тепер один процес міг обслуговувати безліч клієнтів. Але під час кожного виклику доводилося заново передавати ядру весь список дескрипторів, а потім повністю його переглядати. Крім того, існувало обмеження FD_SETSIZE, яке погано підходило для великої кількості з’єднань.
Пізніше з’явився системний виклик poll(), який позбувся обмеження FD_SETSIZE, але принцип його роботи майже не змінився порівняно з select() - програма, як і раніше, передавала ядру масив дескрипторів і отримувала їхній стан назад.
Коли кількість з’єднань сягає десятків тисяч, такий підхід теж починає помітно навантажувати процесор. І саме тут формулюється головне завдання сервера з високим навантаженням, яке полягає в тому, щоб швидко визначити, які з’єднання дійсно готові до роботи, і не витрачати ресурси на перевірку всіх інших.
Рішенням цієї проблеми став механізм epoll, який кардинально змінив підхід до обробки великої кількості з’єднань.
epoll - це вбудований механізм Linux для відстеження подій на файлових дескрипторах. У випадку веб-серверів мова майже завжди йде саме про мережеві сокети. І головна відмінність epoll від попередніх механізмів select() та poll() пов’язана не зі швидкістю мережі, а з тим, як програма взаємодіє з ядром операційної системи.
При використанні select() та poll() програма під час кожного звернення передає ядру повний список дескрипторів, за якими потрібно стежити, а потім знову перевіряє результат.
epoll працює за іншим принципом: додаток один раз реєструє сокети, що його цікавлять, після чого просто очікує на подію. Коли якийсь сокет стає готовим до читання, запису або виникає інша зареєстрована подія, ядро самостійно повідомляє про це додатку. Але якщо нічого не відбувається, процес спокійно чекає і не перебирає тисячі неактивних з’єднань. Саме тому epoll особливо ефективний у ситуаціях, коли відкритих з’єднань дуже багато, а активна одночасно лише невелика їх частина.
У таблиці нижче ми звели основні відмінності всіх трьох згаданих механізмів, щоб ця інформація була наочною.
| Механізм | Робота з дескрипторами | Обмеження | Поведінка за великої кількості з'єднань |
|---|---|---|---|
select() |
Список дескрипторів передається під час кожного виклику | Обмеження FD_SETSIZE |
Погано масштабується |
poll() |
Масив дескрипторів передається під час кожного виклику | Немає обмеження FD_SETSIZE |
Потрібно щоразу обробляти великий масив |
epoll |
Дескриптори реєструються один раз | Обмежується переважно ресурсами операційної системи | Добре підходить для десятків тисяч з'єднань |
Іноді їхні відмінності описують ще й однією фразою: select() та poll() працюють за O(n), а epoll - за O(1). Таке пояснення допомагає зрозуміти загальну ідею, хоча реальна робота ядра, звісно, влаштована набагато складніше.
Нам же, як користувачам та адміністраторам, важливіше те, що epoll позбавляє додаток від необхідності знову й знову передавати ядру один і той самий список з’єднань та перевіряти їх по колу. Воно отримує сповіщення лише тоді, коли з яким-небудь сокетом дійсно сталася подія.
Для роботи з epoll додаток зазвичай використовує три системні виклики. Спочатку створюється екземпляр epoll за допомогою системного виклику:
epoll_create1()
Потім додаток реєструє потрібні файлові дескриптори:
epoll_ctl()
Наприклад, можна повідомити ядру, що цікавить момент, коли певний сокет стане готовим до читання.
Після реєстрації починається очікування подій:
epoll_wait()
Цей виклик блокується до появи події та повертає лише ті дескриптори, які потребують обробки.
У спрощеному вигляді цикл виглядає так:

Головна ідея полягає в тому, що реєстрація з’єднань виконується окремо, а потім програма лише очікує на подію, і повторно передавати ядру весь список сокетів у кожному циклі вже не доводиться.
Програма може підписатися на різні типи подій. Їх багато, але для розуміння роботи мережевого сервера достатньо кількох основних:
| Подія | Значення |
|---|---|
EPOLLIN |
Сокет готовий до читання |
EPOLLOUT |
Сокет готовий до запису |
EPOLLERR |
Сталася помилка |
EPOLLHUP |
З'єднання закрито або більше не може використовуватися у звичайному режимі |
Наприклад, клієнт надіслав HTTP-запит. Дані з'явилися в сокеті, і в результаті ядро повідомляє про подію читання. Додаток зчитує запит і починає формувати відповідь.
Якщо сокет поки що не готовий прийняти весь обсяг даних, то сервер не зобов’язаний чекати і спокійно може переключитися на інші з’єднання, після чого може повернутися до цього клієнта пізніше, коли сокет знову стане доступним для запису. Завдяки цьому один процес здатний ефективно обслуговувати величезну кількість клієнтів.
У epoll є дві основні моделі сповіщень: level-triggered та edge-triggered. За замовчуванням використовується level-triggered, що означає: якщо дані в сокеті ще залишилися, то додаток продовжує отримувати сповіщення.

Edge-triggered працює інакше. Повідомлення надходить лише при зміні стану, тому додаток має використовувати неблокуючі сокети та читати або записувати дані доти, доки операція не поверне, наприклад, EAGAIN.
| Режим | Поведінка | Особливість |
|---|---|---|
| Level-triggered | Сповіщення надходять, доки умова залишається істинною | Простіший у реалізації |
| Edge-triggered | Сповіщення надходить лише під час зміни стану | Потребує більш ретельної реалізації |
Архітектура Nginx побудована навколо подійної моделі, коли замість створення окремого процесу або потоку для кожного клієнта Nginx запускає обмежену кількість worker-процесів.
Спрощена схема цієї моделі виглядає так:

Master-процес керує workers, завантажує конфігурацію та виконує адміністративні завдання. Клієнтські з’єднання обробляють worker-процеси, кожен із яких здатний одночасно працювати з великою кількістю сокетів.
Коли клієнт підключається до сервера, його з’єднання потрапляє в систему подій worker-процесу. Поки клієнт нічого не надсилає, worker не витрачає на нього процесорний час. Звісно, відкритий сокет займає пам’ять, файловий дескриптор та інші системні ресурси, але окремий потік для нього не створюється.
Але щойно у сокеті з’являється подія, worker обробляє її й переходить до наступної.
Припустимо, Nginx підтримує 50 000 з’єднань. Це зовсім не означає, що сервер одночасно виконує 50 000 HTTP-запитів. Реальна ситуація може виглядати приблизно так: у даний момент із цих 50 тис. з’єднань 47 тис. очікують на дані, періодично активними є 2 500, а тих, що вимагають обробки тут і зараз, лише близько 500. У цьому випадку worker працює насамперед із тими 500 з’єднаннями, які дійсно потребують уваги, а решта, хоч і можуть залишатися відкритими, але процесор майже не витрачає на них час.
Така модель добре підходить для keep-alive, WebSocket, reverse proxy, API-шлюзів та інших сценаріїв, де з’єднання можуть залишатися відкритими тривалий час.
Тому 50 000 відкритих з’єднань і 50 000 запитів, що виконуються одночасно, це абсолютно різне навантаження. Саме подієва архітектура разом з epoll дозволяє одному worker-процесу Nginx обслуговувати тисячі й десятки тисяч клієнтів.
Максимальна кількість з'єднань для одного worker задається параметром worker_connections:
events {
worker_connections 4096;
}
Якщо worker-процесів декілька, теоретична межа стає вищою. Наприклад:
worker_processes 4;
events {
worker_connections 4096;
}
Отримуємо:
4 × 4096 = 16384
Але сприймати це число як максимальну кількість одночасно підключених клієнтів не можна, оскільки якщо Nginx працює як reverse proxy, то він відкриває з’єднання не тільки з клієнтами, а й з upstream-серверами. Тому один запит може займати щонайменше два з’єднання: вхідне клієнтське та вихідне до backend.
Існують також обмеження самої операційної системи, адже кожен відкритий сокет використовує файловий дескриптор, а їхня кількість для процесу обмежена. Поточний ліміт можна перевірити командою:
ulimit -n
Якщо процесу дозволено відкрити лише 1024 файлові дескриптори, то просте збільшення значення параметра worker_connections, наприклад, до 50000, не дозволить worker обслуговувати 50 000 з’єднань.
Реальна ж межа залежить від кількох параметрів:
worker_processes;worker_connections;Це означає, що epoll дозволяє Nginx ефективно працювати з великою кількістю дескрипторів, але не знімає обмежень Linux та самого сервера.
Коли веб тільки з’явився, більшість запитів були короткими, тобто браузер отримував сторінку, після чого з’єднання незабаром закривалося.
Сьогодні ж усе влаштовано набагато складніше, оскільки браузер може одночасно завантажувати десятки ресурсів, звертатися до API та підтримувати тривалі з’єднання. Сервери працюють із WebSocket, Server-Sent Events, стрімінгом, MQTT, reverse proxy та іншими технологіями. Хорошим прикладом може слугувати WebSocket, коли з’єднання може залишатися відкритим годинами, хоча більшу частину цього часу через нього взагалі нічого не передається.
Виділяти окремий потік, який просто чекатиме на наступне повідомлення, занадто затратно, а ось подієва модель дозволяє залишити з’єднання серед тисяч інших і повернутися до нього лише тоді, коли з’являться дані. Тому epoll вже давно використовується далеко за межами Nginx. З ним безпосередньо або через бібліотеки працюють такі інструменти, як Redis, HAProxy, DNS-сервери, проксі, брокери повідомлень та інше мережеве ПЗ під Linux. Причому розробник часто взагалі не взаємодіє з epoll безпосередньо.
Наприклад, Node.js використовує libuv, яка обирає відповідний механізм вводу-виводу для конкретної операційної системи. У Python цю роботу приховує asyncio, у Java - Netty, а в Rust - Tokio. Мережевий poller Go Runtime під Linux теж використовує epoll.
У підсумку розробник може писати асинхронний мережевий код, не викликаючи epoll_wait() самостійно, а роботу з системними механізмами Linux бере на себе та чи інша бібліотека або runtime.
Саме epoll працює лише в Linux, а в інших операційних системах подібні завдання вирішують власні механізми:
Будова цих механізмів різниться, але загальне завдання у них одне: ефективно працювати з великою кількістю операцій вводу-виводу без створення окремого потоку для кожного з’єднання.
Кросплатформені бібліотеки приховують ці відмінності. Наприклад, додаток на Node.js працює через libuv, а бібліотека вже обирає відповідний механізм залежно від операційної системи.
У самому Linux з часом з’явився ще один цікавий механізм - io_uring, що використовує кільцеві черги, через які додаток передає операції ядру й отримує інформацію про їхнє завершення. Такий підхід дозволяє зменшити кількість системних викликів і охоплює ширший набір операцій вводу-виводу.
Іноді io_uring називають «новим epoll», хоча вони працюють по-різному. Зокрема, epoll насамперед повідомляє про готовність файлових дескрипторів, а io_uring дозволяє надсилати ядру самі операції вводу-виводу та отримувати інформацію про їхнє завершення.
Очевидно, що їхні можливості частково перетинаються, але io_uring не можна вважати прямою заміною epoll. Останній, як і раніше, широко використовується в мережевому ПЗ під Linux, а подієва архітектура на його основі нікуди не зникла.
На жаль, ні. Іноді можна зустріти думку, що достатньо замінити poll() на epoll, і програма одразу почне працювати швидше. Але насправді все залежить від того, де виникає вузьке місце. Сам epoll не прискорює роботу процесора, не зменшує мережеві затримки й не прискорює SQL-запити, тому якщо сервер після отримання запиту дві секунди виконує ресурсомісткі обчислення, то ці дві секунди нікуди не зникнуть.
Є ще одна проблема, пов’язана з тим, що якщо worker виконує тривалу блокуючу операцію й не повертається до циклу обробки подій, то решта готових з’єднань теж починають чекати.
Виходить, що висока продуктивність Nginx - це результат одразу кількох рішень, таких як подієва архітектура, неблокуючий ввід-вивід, невелика кількість worker-процесів, ефективна робота з пам’яттю та можливості ядра Linux, тобто сам epoll є лише однією з частин цієї системи.
На сучасних Linux-системах зазвичай нічого вмикати вручну не потрібно, оскільки цей механізм працює «з коробки». Nginx сам обирає найбільш підходящий механізм обробки подій.
Але за бажанням його можна вказати явно за допомогою інструкції:
events {
use epoll;
}
У звичайній ситуації в цьому немає необхідності. Автоматичний вибір майже завжди виявляється правильним.
Щоб переглянути версію Nginx, параметри збірки та підключені модулі, виконайте команду:
nginx -V
Вона покаже, чи ввімкнено модуль, але не підтвердить, що worker-процес зараз працює саме через epoll.
Якщо ж вам потрібна точна перевірка, то можна переглянути системні виклики.
Спочатку знайдіть PID одного з worker-процесів за допомогою команди:
ps aux | grep «nginx: worker»
Потім підключіться до нього через strace:
strace -e epoll_create1,epoll_ctl,epoll_wait -p PID
Якщо Nginx використовує epoll, то у виведенні з’являтимуться відповідні системні виклики. Варто також враховувати, що на різних версіях ядра, libc та самого Nginx список викликів може дещо відрізнятися. Підключати strace до робочого сервера слід обережно, оскільки це створює додаткове навантаження, тому такі перевірки краще виконувати на тестовій машині або в періоди мінімальної активності.
epoll - це вбудований механізм Linux, який дозволяє додатку відстежувати велику кількість файлових дескрипторів, включаючи мережеві сокети, і отримувати повідомлення лише про ті, де відбулася потрібна подія. Завдяки цьому сервер не витрачає час на постійну перевірку всіх з’єднань, а працює лише з активними.
При використанні select() та poll() додаток щоразу передає ядру список дескрипторів, що його цікавлять. У випадку з epoll цей список реєструється один раз, після чого додаток отримує лише готові події через epoll_wait(). Такий підхід краще масштабується при великій кількості відкритих з’єднань.
Так. У Linux веб-сервер Nginx зазвичай сам обирає epoll як найбільш підходящий механізм обробки подій, тому вручну вказувати use epollнайчастіше не потрібно.
Це залежить не лише від epoll. На це впливають worker_connections, ліміти файлових дескрипторів, обсяг пам’яті, налаштування Linux та характер навантаження. А якщо Nginx працює як reverse proxy, потрібно враховувати ще й з’єднання з upstream-серверами.
У Linux - так, оскільки Node.js працює через бібліотеку libuv, яка використовує epoll та інші механізми операційної системи. Розробнику зазвичай не доводиться безпосередньо взаємодіяти з API epoll.
epoll повідомляє додатку, що файловий дескриптор готовий до операції.
io_uring пропонує ширшу модель асинхронного виконання операцій вводу-виводу через черги запитів та завершень. Ці механізми вирішують різні завдання, тому io_uring не можна вважати просто новою версією epoll.
Висока продуктивність Nginx пояснюється не якимось одним «секретним механізмом», а всією архітектурою сервера.
epoll, як один із складових елементів, дозволяє відмовитися від постійного перебору тисяч сокетів і отримувати сповіщення лише про ті з’єднання, де дійсно з’явилася робота. Такий підхід є особливо ефективним, коли відкритих з’єднань багато, а активними одночасно є лише деякі з них.
При цьому epoll не усуває обмеження самої операційної системи й не прискорює повільний код. Продуктивність, як і раніше, залежить від налаштувань Nginx, лімітів файлових дескрипторів, обсягу пам’яті, параметрів мережі та швидкості роботи backend-додатка з базою даних.
Варто пам’ятати, що якщо Nginx використовується на VPS як reverse proxy, API-шлюз, сервер WebSocket або фронтенд для додатка з високим навантаженням, то можливості Linux стають такою ж частиною загальної продуктивності, як процесор, пам’ять і швидкість дискової підсистеми. Саме поєднання грамотної архітектури та механізмів ядра дозволяє навіть відносно невеликому серверу обслуговувати величезну кількість мережевих з’єднань.
Безпечне видалення старих ядер Linux у Ubuntu, Debian, AlmaLinux, Rocky Linux та CentOS. Очищення розділу /boot, робота з APT та DNF, оновлення GRUB та захист с...
Пошук найбільших файлів і каталогів у Linux за допомогою du, find та ncdu. Перевірка логів, очищення Docker, аналіз зайнятого місця на диску та способи запобіга...
Налаштування аліасів bash у Linux для швидкого адміністрування серверів. Практичні приклади, корисні команди, рекомендації щодо роботи з ~/.bash_aliases та підв...