В Linux тема asynchronous I/O давно перестала быть «нишевой». На VDS с NVMe вы быстро упираетесь не только в пропускную способность, но и в latency (задержку I/O), а следом — в IOPS и эффективность CPU. И тут выясняется, что «асинхронщина» в Linux — это не одна технология, а минимум три семейства: POSIX AIO, libaio (Linux native AIO) и io_uring.
Ниже — обзор-практикум для админов и DevOps: чем отличаются подходы, где подвохи в тестах fio, как интерпретировать сравнение производительности и что выбирать в реальных сценариях на VDS.
Что мы сравниваем: API, реализация и «путь» запроса
У всех трёх вариантов общая цель: отправить запрос чтения/записи так, чтобы поток приложения не блокировался, а завершение операции пришло позже «событием». Но в Linux «асинхронность» может достигаться разными механизмами, и это критично для задержек, CPU и хвостов p99.
POSIX AIO: стандартный интерфейс, но не всегда «настоящий» async
POSIX AIO — семейство функций вроде aio_read/aio_write/aio_error/aio_return и уведомления через сигнал или поток. В Linux исторически частый сценарий — реализация через пул потоков в userspace (glibc): запрос фактически выполняется синхронным pread/pwrite, просто «в другом треде».
Что это означает на практике:
- асинхронность есть, но цена — контекстные переключения и работа планировщика;
- latency может ухудшаться под нагрузкой, особенно на маленьких блоках;
- на быстрых NVMe разница между «ядро делает» и «потоки делают» начинает проявляться сильнее.
Плюс POSIX AIO — переносимость и стандартный API. Иногда это решающий фактор, если вы живёте в мире нескольких ОС/платформ.
libaio (Linux native AIO): ближе к ядру, но со своими рамками
libaio — пользовательская библиотека к Linux AIO (интерфейсы io_submit/io_getevents). Исторически это был «родной» асинхронный путь в ядро для файловых дескрипторов, но с важными оговорками по режимам и типам операций.
Классический нюанс: долгое время Linux AIO был наиболее предсказуем и эффективен в сценариях direct I/O (O_DIRECT). Часть «обычного» буферизованного I/O могла вести себя не так, как ожидают от «универсального async». Поэтому многие проекты использовали libaio точечно: под бенчмарки, под прямой ввод-вывод, под специфичные storage-слои.
Сильные стороны libaio:
- меньше лишних потоков, ближе к ядру;
- хорошо ложится на очереди и пакетирование запросов;
- удобен как базовый «референс» для тестов на Linux.
Ограничения:
- историческая «неровность» по сценариям и файловым режимам;
- модель событий и композиция операций обычно менее гибкие, чем у io_uring.
io_uring: кольца очередей, меньше syscalls, больше возможностей
io_uring — интерфейс, который в Linux 6.x часто рассматривают как основной кандидат на роль универсального async I/O. Приложение и ядро разделяют две кольцевые очереди (submission/completion): приложение кладёт запросы, ядро возвращает завершения. Во многих режимах это позволяет заметно сократить число системных вызовов и оверхед переходов userspace↔kernel.
Почему это важно админам:
- при высоких IOPS стоимость syscalls и пробуждений становится сопоставимой со временем самого I/O;
- io_uring часто улучшает p95/p99 и/или снижает CPU при той же нагрузке;
- функционально io_uring шире (не только файловое I/O), что помогает event-loop архитектурам.
Почему Linux 6.x и NVMe меняют картину
На медленных дисках различия между API часто «съедаются» ожиданием устройства. На NVMe (что типично для современных VDS) задержка устройства мала, и на первый план выходят:
- стоимость системного вызова и пробуждений потоков;
- конкуренция за CPU и runqueue;
- очереди блочного слоя и поведение планировщика I/O;
- эффективность пакетирования запросов.
Поэтому сравнение «по одной цифре IOPS» часто вводит в заблуждение: можно получить больше IOPS, но хуже p99 latency — и тогда база/приложение будут медленнее именно в пиках.
Главная продовая метрика — не максимум IOPS, а стабильность задержек (p95/p99) при той глубине очереди и конкуренции, с которой реально живёт ваш сервис.

Как корректно сравнивать: fio, параметры и типовые ловушки
Для воспроизводимого сравнения чаще всего берут fio. Но в fio очень легко «случайно» протестировать не то, что вы думаете. Ниже — набор правил, которые делают сравнение io_uring vs libaio vs POSIX AIO более честным.
1) Зафиксируйте семантику: buffered vs direct
Если вы тестируете файловые нагрузки приложения (веб, кэш, генерация статики) — часто важен buffered I/O (страничный кэш). Если тестируете БД и хотите приблизиться к её I/O-пути — чаще нужен direct I/O.
В fio за direct отвечает direct=1 (O_DIRECT). Смешивать режимы при сравнении нельзя: результаты будут про разные подсистемы ядра.
2) Одинаковые iodepth и параллелизм
Async I/O раскрывается при ненулевой глубине очереди, но «правильная» iodepth зависит от workload:
- OLTP ближе к iodepth 1–16 на поток, важны хвосты p99;
- стриминг, бэкапы, обработка больших файлов — iodepth 32–256 и выше, важен throughput.
Если поставить iodepth=256 и сделать вывод «io_uring быстрее», вы можете просто протестировать сценарий, который у вашего приложения никогда не случается.
3) Контролируйте CPU: I/O может стать CPU-bound
На NVMe бывает парадокс: устройство «готово», а приложение упирается в CPU на обработке завершений. Поэтому в отчёте держите в фокусе не только IOPS/MB/s, но и:
- загрузку CPU и распределение по ядрам;
- контекстные переключения (cs);
- softirq (актуально для сетевых FS и storage over network);
- профиль задержек: min/avg/max и percentiles.
Если видите, что у вас «выросли IOPS», но CPU стал 100% и p99 ухудшился, — это важнее любых «красивых» чисел.
4) Прогрев и повторяемость
Делайте прогрев (ramp_time) и достаточную длительность (runtime), иначе получите цифры «на холодном кэше» или словите флуктуации из-за фоновых процессов и writeback. Для сравнения технологий важнее повторяемость, чем абсолютный рекорд.
Шаблон fio-профиля: запуск в трёх ioengine
Ниже — минимальный профиль, который удобно прогонять в трёх вариантах, меняя только ioengine: io_uring, libaio, posixaio. Тестируйте на отдельном файле/разделе, не на боевой FS с данными.
[global]
ioengine=io_uring
thread=1
direct=1
filename=/mnt/testfile
size=8G
time_based=1
runtime=60
ramp_time=10
group_reporting=1
[randread_4k_qd1]
bs=4k
rw=randread
iodepth=1
numjobs=1
[randread_4k_qd32]
bs=4k
rw=randread
iodepth=32
numjobs=1
[randwrite_4k_qd32]
bs=4k
rw=randwrite
iodepth=32
numjobs=1
[seqread_1m_qd32]
bs=1m
rw=read
iodepth=32
numjobs=1
Дальше меняйте только ioengine. Если добавляете специфичные опции io_uring, фиксируйте их явно в отдельном варианте, иначе вы сравните «тюнингованный io_uring» с дефолтным libaio.
Для расширенной диагностики полезно параллельно смотреть реальную картину по диску и очередям: как читать iostat/iotop и параметры fio с учётом I/O scheduler.
Что обычно видно по метрикам: latency, throughput, IOPS
Универсального «io_uring всегда быстрее» нет. Но на Linux 6.x и NVMe типовая картина выглядит так.
Мелкие случайные чтения 4k (randread): борьба за хвосты
При небольшой глубине (iodepth 1–4) IOPS могут быть близкими между движками, а разница проявится в хвостах (p95/p99). io_uring нередко даёт более ровный профиль при росте конкуренции, потому что уменьшает лишние переходы ядро/userspace и пробуждения.
POSIX AIO здесь может проседать из-за потоковой модели: растёт очередь runnable-потоков, и completion начинает задерживаться планировщиком.
Мелкие случайные записи 4k (randwrite): важны flush, fsync и семантика теста
Записи сильно зависят от того, что именно вы измеряете: подтверждённую запись на устройство или запись в кэш. На direct I/O при высокой iodepth io_uring и libaio обычно выглядят сильнее POSIX AIO.
Но для баз данных решают не «fio randwrite», а поведение с fsync, барьерами и журналированием. Поэтому fio — это индикатор потолка и накладных расходов, а не гарантированный ответ «БД будет быстрее на X%».
Последовательные операции большими блоками: потолок по устройству и экономия CPU
На seqread/seqwrite большими блоками вы часто упрётесь в лимит throughput устройства/виртуализации. Различия по MB/s могут быть небольшими, но заметными по CPU: io_uring иногда даёт тот же throughput дешевле по процессору.

Что выбрать на VDS: ориентиры по сценариям
Выбор обычно определяется не «модностью», а поддержкой в вашем ПО и реальным I/O-профилем.
Если ваш стек уже умеет io_uring
В Linux 6.x это часто лучший первый выбор, особенно если важны низкая latency и высокая плотность IOPS на NVMe. Практически это означает: при той же нагрузке можно получить либо более ровные хвосты, либо меньше CPU, либо оба эффекта.
Проверьте три вещи:
- реально ли включён io_uring в приложении (а не просто «собрано с поддержкой»);
- какой режим I/O используется (direct/buffered);
- не ограничивает ли вас среда (контейнер, sandbox, версия ядра на хосте, лимиты).
Если нужен предсказуемый «классический» режим, а io_uring недоступен
libaio остаётся рабочей лошадкой, особенно если приложение проектировалось под Linux-native AIO и direct I/O. Важно помнить: часть сценариев «обычного» буферизованного файлового I/O может не давать ожидаемого выигрыша — и это не «ошибка бенчмарка», а особенности пути данных.
Если важна переносимость и нагрузка умеренная
POSIX AIO может быть приемлемым вариантом там, где пиковые IOPS невысоки, а проще опираться на стандартный API. Но если критичны хвосты latency и вы действительно нагружаете диск — POSIX AIO в Linux часто оказывается слабее из-за потоковой реализации.
Практический чек-лист: как сравнить без самообмана
Зафиксируйте среду: одно и то же ядро, один и тот же тип диска/тома, одинаковая FS и параметры монтирования.
Опишите workload: блок (4k/16k/128k/1m), rw (rand/seq), доля read/write, целевая
iodepthиnumjobs.Запустите fio в трёх вариантах
ioengine, меняя только движок.Смотрите percentiles latency (p95/p99) и CPU, а не только средние значения.
Сделайте 3–5 прогонов и сравните разброс. Если разброс большой — ищите фоновые причины: noisy neighbor, writeback, steal time, лимиты по IOPS/throughput.
Нюансы виртуализации: почему на VDS результат отличается от «железа»
На VDS вы измеряете не только подсистему Linux, но и слой виртуализации/хранилища провайдера. На результат влияют:
- тип виртуального диска (virtio-blk, virtio-scsi, NVMe emulation);
- настройки очередей и multi-queue;
- политики шеринга NVMe и лимиты по IOPS/throughput;
- CPU steal и конкуренция на хосте (особенно заметно в хвостах latency).
Правильный админский подход: сначала понять, где вы на кривой «disk-bound vs cpu-bound», а затем уже делать выводы про io_uring/libaio/POSIX AIO и подбирать глубину очереди.
Итоги
В Linux 6.x io_uring чаще всего даёт самый современный и эффективный путь для async I/O, особенно на NVMe и при высоких IOPS, где критичны latency и стоимость syscalls. libaio остаётся актуальным в Linux-native сценариях и полезен как «понятная база» для тестов и некоторых storage-режимов. POSIX AIO в Linux нередко является компромиссом: удобный стандартный интерфейс, но потенциально более высокий оверхед из-за потоковой реализации.
Если вы выбираете технологию под конкретный сервис на VDS: начните с честного fio-профиля под ваш workload, сравните p99 и CPU, проверьте повторяемость — и только потом решайте, что включать в прод.


