АйТи Фреш
Главная / Статьи / 1С и базы данных
1С и базы данных

Почему io_workers не ускоряет io_uring в PostgreSQL 18 и чем управлять на самом деле

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~18 мин чтения
Почему io_workers не ускоряет io_uring в PostgreSQL 18 и чем управлять на самом деле
Иллюстрация к статье «Почему io_workers не ускоряет io_uring в PostgreSQL 18 и чем управлять на самом деле».

Администратор включает в PostgreSQL 18 параметр `io_method = io_uring`, перезапускает сервер и видит прежние 24 минуты на ночном отчёте. Затем поднимает `io_workers`, перечитывает конфигурацию — результат снова не меняется. Причина не в сломанном AIO: эти параметры управляют разными уровнями подсистемы. Я разберу их без маркетинговых обещаний, покажу результаты стенда с базой 1,4 ТБ и дам порядок проверки, который отделяет ограничения хранилища от глубины чтения и параллельного выполнения плана.

Три параметра — три разных уровня управления

io_method выбирает механизм выполнения операций, пригодных для асинхронного ввода-вывода. В PostgreSQL 18 доступны значения worker, io_uring и sync. Значение worker использует отдельные процессы ввода-вывода и установлено по умолчанию. Значение io_uring доступно только в сборке с liburing. В режиме sync такие операции исполняются синхронно. Параметр имеет контекст postmaster: для его изменения нужен полный перезапуск сервера, а перечитывание конфигурации ничего не изменит.

io_workers задаёт число процессов ввода-вывода исключительно для метода worker; значение по умолчанию — 3. При io_method = io_uring или io_method = sync этот параметр не участвует в выполнении операций. Важная поправка к распространённому описанию: в PostgreSQL 18 у io_workers контекст sighup, поэтому новое значение можно применить перечитыванием конфигурации. Перезапуск для него не требуется, хотя задавать параметр можно только через конфигурационный файл, командную строку сервера или механизм ALTER SYSTEM, а не через обычный сеансовый SET.

effective_io_concurrency задаёт ожидаемую конкурентность хранилища и влияет на число операций, которые отдельная сессия пытается инициировать параллельно. Это не гарантированное количество постоянно активных запросов: фактическая глубина зависит от выполняемого узла плана, наличия физических чтений, объединения соседних блоков и ограничения io_max_concurrency. Допустимы значения от 1 до 1000, а 0 отключает выдачу асинхронных запросов. В PostgreSQL 18 значение по умолчанию повышено с прежней единицы до 16.

Для обслуживающих операций предусмотрен maintenance_io_concurrency, также равный 16 по умолчанию. Оба параметра можно менять в сессии, базе, роли или конфигурации, а также переопределять для конкретного tablespace. Это удобнее, чем назначать одну глубину локальному NVMe, архивному массиву и сетевому тому. Я воспринимаю io_method как способ доставки, io_workers — как размер пула только для одного способа, а effective_io_concurrency — как запрос PostgreSQL к возможностям самого хранилища.

Если изменение `io_method` не ускорило запрос, это ещё не доказывает бесполезность выбранного механизма. Сначала проверьте, есть ли физические чтения, сколько операций реально находится в полёте и не ограничивает ли их `io_max_concurrency`.
Цифры и версии: Три параметра — три разных уровня управления — схема
Цифры и версии: Три параметра — три разных уровня управления. Открыть схему в полном размере

Асинхронное чтение не равно параллельному плану запроса

Слово «параллельный» здесь легко запутывает. AIO позволяет одному процессу PostgreSQL инициировать несколько операций ввода-вывода, не ожидая завершения каждой по очереди. Параллельный запрос — другой механизм: планировщик создаёт узел Gather или Gather Merge и привлекает дополнительные процессы для выполнения плана. io_workers не является пулом parallel workers и не определяет, сколько исполнителей получит запрос.

Количество исполнителей одного параллельного плана ограничивает max_parallel_workers_per_gather; его значение по умолчанию в PostgreSQL 18 равно 2. Общий пул ограничивают max_parallel_workers, по умолчанию 8, и max_worker_processes, также по умолчанию 8. Эти параметры могут влиять на отчёт одновременно с AIO, но решают другую задачу. Поэтому сравнение методов ввода-вывода нужно проводить при одинаковом плане и одинаковом числе parallel workers.

У каждого процесса, участвующего в параллельном плане, может быть собственная активность ввода-вывода. Из-за этого суммарная нагрузка на СХД растёт быстрее, чем ожидает администратор, настроивший только одну сессию. Простого универсального произведения числа workers на effective_io_concurrency нет: часть данных окажется в кэше, соседние блоки могут объединяться, а процессы будут проходить разные фазы плана. Но при нагрузочном тесте обязательно фиксируйте фактически запущенное число исполнителей.

Для воспроизводимого сравнения я сначала сохраняю план, настройки AIO и ограничения параллельного выполнения. Затем повторяю запрос с неизменным набором параметров, меняя только одну исследуемую ручку. Иначе легко приписать io_uring ускорение, которое на самом деле появилось из-за другого плана, собранной статистики или дополнительных parallel workers.

Ускорение после изменения сразу нескольких параметров нельзя честно приписать одному из них. Особенно опасно одновременно менять `io_method`, глубину чтения и настройки параллельного плана.
Почему io_workers не ускоряет io_uring в PostgreSQL 18 и чем управлять на самом деле — схема
Схема к статье. Открыть схему в полном размере

Стенд ООО «Литейный двор»: производство, 85 рабочих мест

Условный клиент из практики — ООО «Литейный двор», производство, 85 рабочих мест. Отдельная аналитическая база PostgreSQL 18 размещена на сервере с Debian 13 и ядром Linux 6.12: 16 vCPU, 96 ГБ оперативной памяти, shared_buffers = 24GB. Размер базы — 1,4 ТБ, крупнейшая таблица документов занимает около 210 ГБ. Отчёт по движению материалов использует последовательное чтение и bitmap heap scan. Для проверки использовали локальный NVMe и отдельный том по iSCSI.

На PostgreSQL 16 отчёт выполнялся 38 минут, после перехода на PostgreSQL 18 с настройками AIO по умолчанию — 24 минуты. Само совпадение апгрейда с повышением стандартного effective_io_concurrency с 1 до 16 ещё не доказывает, что весь выигрыш дал только этот параметр: между основными версиями менялись и другие части сервера. Чтобы установить причинность, нужен отдельный контрольный прогон PostgreSQL 18 с глубиной 1. Поэтому я фиксирую 38 и 24 минуты как результат миграции, но не выдаю разницу за изолированный эффект одной настройки.

Дальше меняли по одному параметру. Каждый вариант запускали три раза, сравнивали медианы, а холодный кэш готовили только на выделенном тестовом сервере. Системный сброс page cache затрагивает все работающие приложения, поэтому на продуктивном узле так делать нельзя. Перезапуск PostgreSQL сам по себе также не очищает кэш страниц Linux. На локальном NVMe медиана для io_uring составила 21 минуту против 24 минут у исходного варианта — улучшение 12,5 %, но без данных о разбросе трёх измерений я не называю его статистически значимым.

На iSCSI-томе метод worker дал 23 минуты, а io_uring — 14 минут. Затем при активном io_uring изменили io_workers с 3 до 16 и ожидаемо не получили разницы: этот пул в таком режиме не используется. В режиме worker изменение с 3 до 16 сократило медиану с 24 до 22,5 минуты, а при 32 процессах время выросло до 26 минут. Это результат конкретного стенда, а не рекомендуемое соотношение между числом vCPU и I/O workers.

Для отчётного tablespace проверили глубины 16, 48, 64 и 128. Значение 48 сократило время с 14 до 11 минут; при 64 медиана осталась около 11 минут. На 128 отчёт выполнялся 11,5 минуты, а p95 коротких запросов к общей СХД вырос. В рабочем варианте оставили effective_io_concurrency = 48 для отчётного tablespace и 16 глобально. Выбранный метод — io_uring; значение io_workers осталось стандартным, поскольку в этой конфигурации оно не действует.

Цифры этого разбора описывают один стенд. Они показывают метод эксперимента, но не дают готового значения для чужой СХД.

Как проверить сборку, ядро и применившиеся настройки

Поддержка io_uring должна присутствовать в самой сборке PostgreSQL. Для Autoconf используется флаг --with-liburing, для Meson — опция -Dliburing, принимающая значения auto, enabled или disabled; значение по умолчанию у Meson — auto. Поиск флага в выводе pg_config полезен, но не универсален для пакетных и Meson-сборок. Надёжнее спросить работающий сервер: колонка enumvals представления pg_settings содержит только доступные этой сборке варианты io_method.

pg_config --configure | grep -E -- '--with-liburing|-Dliburing' || true
uname -r
sysctl kernel.io_uring_disabled kernel.io_uring_group 2>/dev/null || true
psql -X -Atc "SELECT name, setting, context, enumvals, pending_restart
FROM pg_settings
WHERE name IN ('io_method', 'io_workers', 'io_max_concurrency',
               'effective_io_concurrency', 'maintenance_io_concurrency',
               'io_combine_limit', 'io_max_combine_limit')
ORDER BY name;"

Параметр ядра kernel.io_uring_disabled нужно трактовать точно. Значение 0 разрешает создание новых экземпляров и является стандартным значением ядра. При 1 непривилегированный процесс должен иметь CAP_SYS_ADMIN либо состоять в группе, указанной через kernel.io_uring_group; иначе системный вызов завершится с EPERM. Значение 2 запрещает создание новых экземпляров всем процессам. Уже созданные экземпляры при переходе к 1 или 2 продолжают существовать.

Интерфейс io_uring появился в Linux 5.1, но одного номера ядра недостаточно для решения о вводе в эксплуатацию. Важны обновления безопасности дистрибутива, политика контейнера, seccomp и фактическая работа на используемой файловой системе. Я проверяю запуск на копии конфигурации до согласованного окна и оставляю worker как план отката.

В обсуждении PostgreSQL 18 beta 1 действительно приводились 6,9 мс против 3 мс на создание соединения и примерно 3,5-кратный рост CPU postmaster при io_uring. Разработчики связали это с множеством отображений памяти и обсуждали исправление до релиза. Эти измерения относятся к бета-сборке и не доказывают такое же поведение финальной PostgreSQL 18. Пулер всё равно полезен приложениям с частыми короткими соединениями, но утверждать, что PgBouncer обязателен именно из-за io_uring, по тем бета-цифрам нельзя.

# postgresql.conf — проверенный фрагмент для стенда ООО «Литейный двор»
io_method = 'io_uring'          # применяется после перезапуска сервера
io_workers = 3                  # при io_uring не используется
effective_io_concurrency = 16 # глобальное значение
maintenance_io_concurrency = 16
При `kernel.io_uring_disabled = 2` новые экземпляры io_uring запрещены даже привилегированным процессам. В исходных описаниях значения 1 и 2 часто путают местами.
Порядок действий: Как проверить сборку, ядро и применившиеся настройки — схема
Порядок действий: Как проверить сборку, ядро и применившиеся настройки. Открыть схему в полном размере

Когда имеет смысл менять io_workers

При методе worker серверные процессы помещают операции в общую очередь, а I/O workers выполняют блокирующие системные вызовы. Увеличение пула может помочь, если очередь не успевают разбирать, но само по себе оно не заставляет запрос сформировать больше операций. Потребность создают узлы плана и настройки конкурентности, а верхнюю границу одновременно исполняемых операций одного процесса задаёт io_max_concurrency.

Правило «один I/O worker на одно ядро» у PostgreSQL отсутствует. Стандартные три процесса выбраны как общий компромисс, а не как формула для любого сервера. Я повышаю значение ступенчато только при io_method = worker, одновременно слежу за CPU, загрузкой устройства, задержками соседних запросов и состоянием операций. Результат при 16 или 32 процессах с одного стенда нельзя переносить на другую файловую систему.

io_workers имеет контекст sighup. После изменения через ALTER SYSTEM или конфигурационный файл серверу достаточно перечитать настройки. Проверить источник значения и необходимость перезапуска можно через pg_settings; для этого параметра pending_restart оставаться не должен. При io_uring эксперимент бессмысленен, потому что I/O workers не обслуживают выбранный механизм.

ALTER SYSTEM SET io_workers = 6;
SELECT pg_reload_conf();

SELECT name, setting, context, source, pending_restart
FROM pg_settings
WHERE name = 'io_workers';

SELECT pid, backend_type, wait_event_type, wait_event
FROM pg_stat_activity
WHERE backend_type = 'io worker'
ORDER BY pid;

Я не использую одно состояние SUBMITTED как автоматическое доказательство нехватки workers. Оно означает, что операция передана на исполнение, но причина длительного пребывания может находиться в очереди PostgreSQL или на стороне хранилища. Нужны одновременные данные об утилизации I/O workers, задержках устройства и числе активных операций. Если диск уже насыщен, дополнительные процессы обычно лишь усиливают конкуренцию.

`io_workers` — размер обслуживающего пула, а не эквивалент `effective_io_concurrency` и не настройка parallel query.

Как подбирать effective_io_concurrency и учитывать потолки

effective_io_concurrency — основная пользовательская настройка конкурентности чтения. Значение можно временно изменить в одной сессии, проверить на выбранном запросе и только после этого закрепить. Высокие значения чаще помогают хранилищам с заметной задержкой и высокой допустимой параллельностью. Официальная документация отдельно предупреждает: чрезмерная настройка способна увеличить I/O latency для всех запросов системы.

Для разных устройств я предпочитаю параметры tablespace. PostgreSQL 18 разрешает для tablespace четыре опции: seq_page_cost, random_page_cost, effective_io_concurrency и maintenance_io_concurrency. Изменение влияет на объекты, расположенные в этом пространстве, и не требует перезапуска сервера. При этом нужно помнить, что таблицы и их индексы могут находиться в разных tablespace, а несколько пространств иногда используют одно физическое устройство.

ALTER TABLESPACE ts_reports
  SET (effective_io_concurrency = 48,
       maintenance_io_concurrency = 32);

ALTER TABLESPACE ts_archive
  SET (effective_io_concurrency = 8);

SELECT spcname, spcoptions
FROM pg_tablespace
ORDER BY spcname;

SET effective_io_concurrency = 64;
SHOW effective_io_concurrency;

Второй предел — io_max_concurrency. Значение -1 в конфигурации просит сервер вычислить лимит из shared_buffers и максимального числа процессов, включая max_connections, autovacuum_worker_slots, max_worker_processes и max_wal_senders; выбранный автоматически результат не превышает 64. Сам параметр допускает значения до 1024, но требует перезапуска. Перед экспериментом с глубиной выше стандартной я проверяю разрешённое значение на конкретном сервере и реальные операции в полёте.

io_combine_limit ограничивает размер операции, объединяющей соседний ввод-вывод. Его стандартное значение — 128kB, и его можно менять на пользовательском уровне. Серверный io_max_combine_limit, также равный 128kB по умолчанию, молча ограничивает первое значение и меняется только при старте. Типичный максимум зависит от ОС и размера блока: документация приводит 1MB для Unix и 128kB для Windows. Без профиля последовательных объединённых чтений я эти параметры не повышаю.

Число 1000 является допустимым значением, а не рекомендацией. Чем больше активных сессий, тем осторожнее нужно повышать конкурентность каждой из них.
Порядок действий: Как подбирать effective_io_concurrency и учитывать потолки — схема
Порядок действий: Как подбирать effective_io_concurrency и учитывать потолки. Открыть схему в полном размере

pg_aios, pg_stat_io и рабочий порядок проверки

Представление pg_aios содержит по одной строке на каждый используемый сейчас AIO handle. Это моментальный снимок, а не накопительный счётчик. В финальной документации PostgreSQL 18 операции называются readv, writev и invalid; в ранних материалах встречалось read, поэтому старые запросы мониторинга могут не совпасть с релизной схемой. Состояния включают HANDED_OUT, DEFINED, STAGED, SUBMITTED, COMPLETED_IO, COMPLETED_SHARED и COMPLETED_LOCAL.

SELECT state, operation, count(*) AS handles,
       sum(length) AS bytes
FROM pg_aios
GROUP BY state, operation
ORDER BY handles DESC;

SELECT pid, state, operation, off, length,
       result, target_desc, f_sync, f_buffered
FROM pg_aios
WHERE state = 'SUBMITTED'
ORDER BY pid, off;

Один снимок редко даёт вывод. Я собираю серию с коротким интервалом во время всего контрольного запроса и сопоставляю её с метриками устройства. Много операций в SUBMITTED при высокой задержке диска указывает на ожидание исполнения. Малое число handles само по себе не доказывает недостаточную глубину: данные могли попасть в shared buffers или page cache, запрос мог упереться в CPU, а один handle мог охватывать объединённое чтение. Нужны план, счётчики буферов и измерения СХД.

В PostgreSQL 18 у pg_stat_io появились read_bytes, write_bytes и extend_bytes, а прежняя колонка op_bytes удалена. После обновления нужно исправить запросы Zabbix, Grafana и собственных экспортёров. Время чтения заполняется только при включённом сборе соответствующих таймингов; нулевое значение без проверки настройки нельзя трактовать как мгновенный диск.

SELECT backend_type, object, context,
       reads, read_bytes, read_time,
       writes, write_bytes, write_time
FROM pg_stat_io
WHERE reads > 0 OR writes > 0
ORDER BY backend_type, object, context;

SHOW track_io_timing;

Тайминги I/O в EXPLAIN ANALYZE отражают ожидание процесса, а не полную стоимость работы устройства. При методе worker часть работы выполняют I/O workers, а при io_uring — ядро, поэтому прямое сравнение одной строки I/O Timings между PostgreSQL 17 и 18 вводит в заблуждение. Я сравниваю wall-clock, число прочитанных буферов и байтов, план, метрики устройства и задержки соседних запросов.

Рабочий порядок такой: доказать, что узкое место находится в физическом чтении; сохранить план и исходные настройки; проверить сборку и политику ядра; подобрать effective_io_concurrency в сессии или для tablespace; посмотреть фактическую активность через pg_aios и pg_stat_io; только затем брать окно на смену io_method. В PostgreSQL 18 заявленное ускорение AIO относится прежде всего к последовательным сканированиям, bitmap heap scan, VACUUM и другим поддержанным путям чтения. Для медленных INSERT и UPDATE сначала исследуйте WAL, checkpoint, блокировки и запись хранилища, а не обещайте эффект от этих параметров.

Самый дешёвый и безопасный эксперимент — сеансовое изменение `effective_io_concurrency`. Смена `io_method` должна идти после диагностики, потому что требует окна и не гарантирует выигрыш на конкретном устройстве.

Частые вопросы

Почему изменение io_workers не повлияло на io_uring?

Потому что `io_workers` используется только при `io_method = worker`. Методы `io_uring` и `sync` не обращаются к этому пулу. Проверяйте активный метод и контекст параметров через `pg_settings`.

Требуется ли перезапуск для изменения io_workers?

Нет. В PostgreSQL 18 параметр имеет контекст `sighup` и применяется после перечитывания конфигурации. Перезапуск нужен для `io_method`, `io_max_concurrency` и `io_max_combine_limit`.

Можно ли проверить наличие io_uring без остановки сервера?

Да. Посмотрите `enumvals` для `io_method` в `pg_settings`: вариант `io_uring` включается в перечень только в сборке с соответствующей поддержкой. Дополнительно проверьте сборочные параметры, sysctl и политику контейнера.

Какое значение effective_io_concurrency считается правильным?

Универсального числа нет. Начните со стандартных 16, меняйте настройку в одной сессии или для tablespace и проверяйте время целевого запроса вместе с p95 соседней нагрузки. Значение 1000 допустимо синтаксически, но не является рекомендацией.

Почему io_max_concurrency может помешать эксперименту?

Он ограничивает число операций, которое один процесс способен выполнять одновременно. При стандартной настройке -1 PostgreSQL вычисляет лимит автоматически, но не выше 64. Изменение параметра требует перезапуска.

Ускорит ли AIO массовые INSERT и UPDATE?

Не стоит ожидать такого эффекта. В PostgreSQL 18 среди основных поддержанных AIO-нагрузок заявлены последовательные сканирования, bitmap heap scan и VACUUM. Проблемы записи нужно отдельно искать в WAL, checkpoint, блокировках и характеристиках хранилища.

Можно ли сравнивать I/O Timings из EXPLAIN между PostgreSQL 17 и 18?

Только с оговорками. Эти значения отражают ожидание серверного процесса, а часть работы теперь может выполнять I/O worker или ядро. Для вывода используйте общее время, прочитанные буферы и байты, одинаковый план и метрики устройства.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 💬 MAX: +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи