Диск под базу выбирают по IOPS, а коммит ждёт не IOPS, а одного fdatasync на журнал. На стенде из опыта диск отдаёт 294 тыс. случайных чтений по 8 КБ в секунду, а запись тех же 8 КБ с fdatasync проходит 1 107 раз в секунду, и один клиент PostgreSQL делает 904 коммита в секунду. Первое число зависит от глубины очереди, второе от латентности одной синхронной записи. Почему COMMIT ждёт именно её, разобрано в статье про fsync и путь записи; WAL описывает, что лежит в журнале. Здесь на fio измерено, как латентность и очередь связаны с IOPS, как fdatasync переходит в TPS коммитов и что происходит, когда провайдер ставит на диск полку IOPS.
Латентность одной операции растёт на порядки, когда она синхронная и под полкой
fdatasync ждёт, пока данные дойдут до носителя. Четвёртая показывает, что делает с той же записью ограничение IOPS: 73 мс вместо 0,9 мс.Стенд: на чём идёт опыт
docker run -d --name dba-storage-for-databases -e POSTGRES_PASSWORD=pg postgres:18
docker exec dba-storage-for-databases sh -c 'apt-get update -qq; apt-get install -y -qq fio'
docker exec dba-storage-for-databases sh -c 'fio --version; stat -f -c %T /var/lib/postgresql; grep " /var/lib/postgresql " /proc/self/mounts'
вывод:
fio-3.39
btrfs
/dev/nvme1n1p5 /var/lib/postgresql btrfs rw,seclabel,relatime,compress=zstd:1,ssd,discard=async,space_cache=v2,subvolid=258,subvol=/root 0 0
Том контейнера — btrfs со сжатием zstd:1 на NVMe хоста. Другого типа диска на стенде нет: NVMe против сетевого диска здесь не сравнивается, облачные числа взяты из документации провайдера ниже. Машина рабочая, и латентность fdatasync на ней плавает (опыт 2). Поэтому числа показывают порядок величин и связи между ними, а не паспорт диска. Все файлы fio лежат в /var/lib/postgresql/fio, не больше 512 МБ каждый, и в конце удаляются.
Опыт 1: IOPS — это очередь, делённая на латентность
Случайное чтение 8 КБ мимо page cache (direct=1), глубина очереди 1, 4, 16 и 64:
docker exec dba-storage-for-databases sh -c 'cd /var/lib/postgresql/fio && \
for q in 1 4 16 64; do fio --name=r$q --filename=rr.dat --size=512m --rw=randread \
--bs=8k --ioengine=libaio --direct=1 --iodepth=$q --runtime=10 --time_based --group_reporting; done'
Выдержка из вывода при очереди 1 (остальные прогоны того же формата):
read: IOPS=10.1k, BW=78.9MiB/s (82.8MB/s)(789MiB/10001msec)
clat (usec): min=3, max=11160, avg=91.76, stdev=65.09
lat (usec): min=66, max=11171, avg=98.20, stdev=65.39
| 99.00th=[ 149], 99.50th=[ 155], 99.90th=[ 306], 99.95th=[ 1876],
Сводка по четырём прогонам. lat — среднее от отправки запроса до завершения, перцентили взяты из clat:
| Очередь | IOPS | lat, мкс | p99 clat, мкс | p99,9 clat, мкс | очередь / lat |
|---|---|---|---|---|---|
| 1 | 10 100 | 98,2 | 149 | 306 | 10 180 |
| 4 | 41 500 | 95,9 | 165 | 243 | 41 700 |
| 16 | 135 000 | 118,1 | 660 | 857 | 135 500 |
| 64 | 294 000 | 217,6 | 469 | 2 008 | 294 100 |
Последний столбец — глубина очереди, делённая на среднюю латентность, в операциях в секунду. Он совпадает с IOPS из fio с точностью до округления: IOPS не свойство диска, а результат того, сколько запросов одновременно в полёте и сколько каждый ждёт. Скачок с 10 тыс. до 294 тыс. дала только очередь. Запрос, который читает страницы по одной, держит очередь 1, и паспортные «сотни тысяч IOPS» его не ускоряют: латентность одного чтения 98 мкс, и до очереди 16 она почти не меняется (96 и 118 мкс). Меняется хвост: p99,9 вырос с 306 до 2 008 мкс.
Опыт 2: fdatasync задаёт, сколько коммитов в секунду
Журнал PostgreSQL пишется последовательно и после каждого коммита синхронизируется. Метод по умолчанию на Linux — fdatasync; код вызова в issue_xlog_fsync (REL_18_6, строки 8753–8760 и 8780–8783):
/*
* Quick exit if fsync is disabled or write() has already synced the WAL
* file.
*/
if (!enableFsync ||
wal_sync_method == WAL_SYNC_METHOD_OPEN ||
wal_sync_method == WAL_SYNC_METHOD_OPEN_DSYNC)
return;
...
case WAL_SYNC_METHOD_FDATASYNC:
if (pg_fdatasync(fd) != 0)
msg = _("could not fdatasync file \"%s\": %m");
break;
Ранний выход возвращает управление, если fsync = off или WAL открыт с O_SYNC/O_DSYNC, иначе в ветке по умолчанию на Linux вызывается pg_fdatasync. Тот же профиль в fio: запись 8 КБ, fdatasync после каждой, один поток, затем 4 и 16 потоков в отдельных файлах:
docker exec dba-storage-for-databases sh -c 'cd /var/lib/postgresql/fio && \
for j in 1 4 16; do fio --name=w --directory=. --size=64m --rw=write --bs=8k \
--ioengine=sync --fdatasync=1 --numjobs=$j --runtime=8 --time_based --group_reporting; done'
вывод (выдержка, по потокам):
jobs=1 write: IOPS=1107 sync (usec): min=583, max=8600, avg=888.77
jobs=4 write: IOPS=2265 sync (usec): min=932, max=10781, avg=1751.16
jobs=16 write: IOPS=8018 sync (usec): min=703, max=123100, avg=1980.38
Для сравнения та же запись без синхронизации (--rw=write --bs=8k --ioengine=sync, 5 секунд) дала 154 000 операций в секунду: fdatasync делает её в 139 раз реже. Теперь тот же сценарий на уровне базы, INSERT одной строки на коммит (wal_sync_method = fdatasync, synchronous_commit = on, fsync = on):
docker exec dba-storage-for-databases sh -c 'psql -U postgres -qc "create table t(i int)"; \
echo "insert into t values (1);" > /tmp/ins.sql; \
for c in 1 4 16; do pgbench -U postgres -n -f /tmp/ins.sql -c $c -j $c -T 8 postgres | grep -E "tps|latency"; done'
вывод (выдержка):
c=1 latency average = 1.106 ms tps = 904.095382
c=4 latency average = 1.986 ms tps = 2013.634662
c=16 latency average = 2.197 ms tps = 7281.388933
| Клиентов / потоков | fio, оп/с | pgbench, TPS |
|---|---|---|
| 1 | 1 107 | 904 |
| 4 | 2 265 | 2 014 |
| 16 | 8 018 | 7 281 |
Один клиент упирается в 1/0,89 мс = около 1 100 синхронизаций в секунду, и коммитов получается 904. Отсюда и вся арифметика коммита: время транзакции — работа плюс ожидание fdatasync, и если синхронизация занимает 1–9 мс, один клиент не сделает больше 1 000–111 коммитов в секунду, как бы быстро диск читал. Параллельные клиенты делят одну синхронизацию журнала, и TPS растёт: при 16 клиентах 7 281.
Но число fdatasync на этом томе нестабильно. Серии на одном и том же томе подряд, средняя латентность синхронизации:
| Серия | Средняя латентность fdatasync |
|---|---|
| первый прогон, один поток, 256 МБ | 10 346 мкс (96 оп/с) |
| повтор, три прогона по 10 с | 7 317, 4 093 и 913 мкс |
| пять прогонов по 8 с подряд | от 903 до 942 мкс |
Между первой серией и последней разница в 11 раз; повтор и пять прогонов шли с одной и той же командой. Причина не выяснялась: машина рабочая, том общий. Для DBA вывод практический: одно измерение fdatasync ничего не доказывает, а хвост sync percentiles важнее среднего.
Опыт 3: полка IOPS
Облачный диск режет не только латентность, но и число операций. Docker умеет то же без привилегий через cgroup v2 (io.max) на блочное устройство хоста. Контейнер с полкой записи 100 операций и чтения 2 000 операций в секунду:
docker run -d --name dba-storage-for-databases-thr \
--device-write-iops /dev/nvme1n1:100 --device-read-iops /dev/nvme1n1:2000 \
-e POSTGRES_PASSWORD=pg postgres:18
docker exec dba-storage-for-databases-thr cat /sys/fs/cgroup/io.max
вывод:
259:1 rbps=max wbps=max riops=2000 wiops=100
Тем же fio и pgbench внутри этого контейнера (установка fio под полкой заняла больше двух минут вместо секунд):
read iodepth=1 IOPS=1997 lat avg=499 мкс p99=537 мкс p99,5=53 740 мкс
read iodepth=16 IOPS=1999 lat avg=8 000 мкс p99=103 285 мкс
write fdatasync IOPS=13 sync avg=73 418 мкс
write direct qd1 IOPS=100 lat avg=9 987 мкс
pgbench c=1 latency average = 69.494 ms tps = 14.389737
pgbench c=16 latency average = 153.689 ms tps = 104.106413
Чтение упёрлось ровно в 2 000: очередь 16 не добавила ни одной операции, а лишь растянула латентность с 0,5 до 8 мс. Прямая запись 8 КБ при очереди 1 дала ровно 100. fdatasync дал 13, то есть на одну синхронизацию пришлось в среднем около 7–8 операций записи на устройство (100 / 13); это вывод из двух чисел, число операций напрямую не считалось. Коммит под полкой стоит 69 мс, а 16 клиентов вытягивают 104 TPS: число одного порядка с полкой, как именно коммиты делят операции, не разбиралось. Полка записи — это не «медленный диск», а потолок TPS, который не лечится ни ядрами, ни shared_buffers.
Что говорят провайдеры
Сетевой диск в этом стенде не измерялся. Лимиты и латентность — со страниц документации AWS на 28.10.2026:
| Тип тома | Что гарантирует документация |
|---|---|
| EBS gp3 | базовые 3 000 IOPS и 125 МиБ/с включены в цену; IOPS докупаются до 80 000, пропускная способность до 2 000 МиБ/с; «single-digit millisecond latency» |
| EBS io2 Block Express | на инстансах Nitro средняя латентность меньше 500 мкс для операций 16 КиБ; до 256 000 IOPS |
Страница gp3 называет задержку «однозначными миллисекундами», то есть по арифметике из опыта 2 это 111–1 000 коммитов в секунду для одного клиента, а базовая полка в 3 000 IOPS работает так же, как полка в 100 из опыта 3, только выше. Числа тома своего облака берут тем же fio: цифра из документации гарантия провайдера, а не измерение под твоей нагрузкой. Страница лимитов Yandex Cloud при проверке отдала защиту от роботов, остальные российские провайдеры не проверялись, поэтому их лимиты здесь не приведены.
Файловая система, RAID и ZFS: что подтверждено документацией
Сравнение xfs и ext4, RAID 10 и RAID 5 на стенде не измерялось, и числа «из опыта» в статье нет: те же профили fio из опытов 1 и 2 запускаются на каждом варианте отдельно. Из документации PostgreSQL известно одно, что касается RAID: кэш контроллера может быть write-back и терять данные при отключении питания, если у него нет батарейного модуля (надёжность WAL). Значит, fdatasync в 50 мкс на массиве подозрителен и проверяется, а не принимается как рекорд.
Про ZFS документация OpenZFS советует отдельные наборы данных для данных и WAL, compression=lz4, recordsize=32K и full_page_writes = off, потому что ZFS не фиксирует частичную запись. Документация PostgreSQL про этот параметр предупреждает, что выключение может привести к неисправимому или тихому повреждению данных. На стенде ZFS нет, full_page_writes не менялся, и выключать его без собственного измерения и без проверки, что копия конфига не уедет на другую файловую систему, нельзя.
Что из этого следует
- Выбирая диск, сравнивай
fdatasyncна 8 КБ, а не IOPS при глубокой очереди: коммит ждёт первую цифру, а паспорт описывает вторую. - TPS одного клиента — это 1 / латентность синхронизации журнала: 0,89 мс дали 904 TPS, 73 мс дали 14.
- Полка IOPS у облака даёт потолок коммитов; искать её нужно в метриках тома, а не в планах запросов.
- Одно измерение ничего не стоит: повторяй, смотри хвост перцентилей и не доверяй среднему.
Чек-лист
- Замерь
fioс--fdatasync=1 --bs=8kна том же томе, что иpg_wal, минимум пять прогонов в разное время. - Смотри
sync percentiles, а не только среднее и IOPS. - Найди лимит IOPS и пропускной способности тома и инстанса в документации провайдера и сравни с замером.
- Проверь, что кэш контроллера или накопителя защищён от потери питания, прежде чем верить малой латентности.
- После опыта удали контейнеры вместе с томами:
docker rm -f -v dba-storage-for-databases dba-storage-for-databases-thr.