io_uring вытесняет epoll как стандарт асинхронного I/O в Linux

Статья разбирает два подхода к асинхронному I/O в Linux, актуальные для высоконагруженных приложений. epoll, появившийся в 2002 году, уведомляет приложение о готовности данных, но требует отдельных вызовов read() или write(). Это создает двойные системные вызовы на каждую операцию плюс переключения контекста между режимами пользователя и ядра. io_uring, введенный в 2019 году (ядро 5.1+), работает по принципу уведомления о завершении операции и использует общую с ядром память через ring buffers. Это позволяет обрабатывать множество операций одним системным вызовом вместо пары на каждую.

Автор приводит практические примеры кода на C для обоих методов и подробно объясняет различия в архитектуре. При необходимости низкого jitter можно использовать IORING_SETUP_SQPOLL, который выделяет ядро для polling'а, но это приводит к потреблению CPU. Для современных систем с ядром выше 5.1 io_uring признается предпочтительным выбором, так как значительно снижает накладные расходы на обработку большого числа соединений.

Ключевые факты

  • epoll требует двойного системного вызова на операцию (epoll_wait + read/write), что создает контекстные переключения и высокие накладные расходы
  • io_uring использует ring buffers (очереди в общей памяти) для пакетной обработки операций, сокращая syscall overhead до одного вызова на пакет
  • IORING_SETUP_SQPOLL позволяет практически избежать системных вызовов в установившемся режиме за счет выделенного ядра kernel thread
  • io_uring поддерживает zero-copy операции через io_uring_register_buffers() и IORING_OP_SEND_ZC (ядро 6.0+)
  • Для новых проектов на современных системах (ядро 5.1+) io_uring, стандартный выбор, epoll теряет актуальность

Ред. epoll прослужил верой и правдой с 2002 года, что по меркам системного API почти срок выслуги. Теперь его аккуратно провожают на пенсию.

Почему это важно

Производительность приложений, обрабатывающих тысячи одновременных соединений, критически зависит от эффективности механизма I/O. каждый системный вызов стоит контекстного переключения между режимом пользователя и ядра, что создает задержку. epoll при высокой нагрузке генерирует два таких переключения на событие, в то время как io_uring может обработать сотни операций одним вызовом. Это различие определяет разницу между приложением, способным обслужить 10000 одновременных клиентов, и тем, которое может только 1000.

Ред. Разница в один системный вызов на событие звучит мелко ровно до того момента, пока событий не становится сотни тысяч в секунду.

Кому это важно

Разработчикам высоконагруженных сервисов: веб-прокси, балансировщики нагрузки, базы данных, кэши. Автор использовал этот подход при разработке TinyGate, reverse proxy сервера. Инженеры, поддерживающие системы на ядрах Linux 5.1 и выше, могут без опасений переходить на io_uring. Разработчикам legacy систем на старых ядрах придется временно оставаться на epoll, но новые проекты должны закладываться на io_uring с самого начала.

Ред. «Без опасений переходить» на технологию, которую в том же тексте обвиняют в асинхронных ошибках и горящем CPU. Опасения всё же лучше держать наготове.

Как это применить

io_uring доступен через библиотеку liburing (устанавливается как liburing-dev/liburing-devel) или прямыми системными вызовами io_uring_setup/io_uring_enter без зависимостей. Базовый процесс: создание ring буфера (одноразовая операция), добавление операций в submission queue, вызов io_uring_enter() или использование SQPOLL для автоматического polling. Для максимальной производительности рекомендуется предварительная регистрация буферов через io_uring_register_buffers(), что избегает повторного映射памяти ядром на каждую операцию.

Ред. Кратко: создать кольцо, сложить операции в очередь, дёрнуть один syscall. Кратко на бумаге, а в коде между этими шагами и прячется вся отладка.

Можно ли доверять

Статья основана на первой практическом опыте автора с TinyGate и проверяется содержанием Linux kernel документации. io_uring появился в 2019 году и к 2024 году уже используется в production в таких проектах, как tokio runtime (Rust async), некоторых конфигурациях nginx и других высоконагруженных системах. Но нужно учитывать, что io_uring постоянно эволюционирует: обработка ошибок асинхронная (results в cqe), SQPOLL требует моніторинга CPU, а код может иметь недостатки (как отмечено в примерах автора).

Ред. Автор честно признаёт, что в его же примерах могут быть баги. Редкая откровенность для текста, который продаёт новый стандарт.

Риски и подводные камни

IORING_SETUP_SQPOLL выделяет kernel thread, который горит CPU даже при пустой очереди (правда, есть sq_thread_idle timeout). Обработка ошибок в io_uring асинхронная и требует проверки res field в completion entry, а не прямого возврата из syscall. Submission queue может переполниться, io_uring_get_sqe() вернет NULL. Требуется Linux 5.1+, старые системы все еще остаются на epoll. Неправильная настройка параметров может привести к неожиданному поведению и утечкам ресурсов.

Ред. Выделенный поток, который жжёт CPU при пустой очереди, продаётся как фича для низкого jitter. Производительность редко достаётся бесплатно, обычно её оплачивают ваттами.

«io_uring is the new standard for async I/O in the modern Linux world, and honestly, I don't see much reason to still reach for epoll on a system that has it.»

— Автор статьи (Sibexico)