Назад к блогу

PostgreSQL глазами ядра Linux: как СУБД взаимодействует с системными вызовами

PostgreSQL глазами ядра Linux: как СУБД взаимодействует с системными вызовами

PostgreSQL сознательно полагается на буферизованный ввод-вывод и страничный кэш ядра, что порождает сложный слой взаимодействия с системными вызовами — от пула виртуальных файловых дескрипторов до асинхронного io_uring. Статья разбирает, какие именно механизмы задействуются при каждом обращении к ядру и с какими ограничениями сталкивается СУБД: фрагментация при резервировании места, конкуренция за крупные страницы, лимиты памяти для io_uring и деградация производительности из-за энергосберегающих состояний процессора. Это полезный разбор для тех, кто хочет понять, где именно заканчивается логика PostgreSQL и начинается поведение ядра Linux.

PostgreSQL устроен не так, как большинство высокопроизводительных СУБД: он «famously uses buffered I/O, while most high-performance database systems use direct I/O» — то есть полагается на buffered I/O и на страничный кэш ядра, а не на прямой доступ к диску. Из этого выбора вырастает целый слой механики: пул виртуальных файловых дескрипторов, обёртки над fsync и rename, сегментация отношений, асинхронный ввод-вывод через io_uring и отдельные правила работы с памятью. Разберём, что именно происходит при каждом обращении к ядру и почему порядок шагов именно такой.

Почему buffered I/O и какие это создаёт проблемы

Переход к direct I/O требует асинхронного ввода-вывода для хорошей производительности, и хотя поддержка direct I/O появится в будущем, «it will probably never be the default»: PostgreSQL часто работает рядом с другими приложениями, а настраивать direct-I/O в таких условиях сложно.

Конкретные проблемы взаимодействия с ядром:

  • Резервирование места через fallocate() для данных контрольных точек вызывает фрагментацию и дополнительные записи в журнал на журналируемых ФС, а на copy-on-write ФС вообще не работает.
  • Крупные страницы (1GB huge pages) создают конкуренцию за folio head — в первую очередь за подсчёт ссылок — и могут вдвое снизить производительность.
  • Механизм reserved-buffers в io_uring накладывает лимит 1GB на операции ввода-вывода, вынуждая PostgreSQL использовать меньшие записи.
  • Кольца io_uring учитываются против лимита заблокированной памяти процесса, причём требуемый объём меняется между версиями ядра, что иногда делает функцию непригодной.

Отдельная история — энергетические состояния процессоров. Падение производительности при росте числа клиентов можно «решить» отключением подсистемы cpuidle: нагрузка недостаточна, чтобы удерживать процессоры в высоком энергетическом состоянии, поэтому они уходят в простой и производительность системы разрушается. Проявляется это так: при малом числе клиентов масштабирование линейно, затем падает и выходит на плато, пока число клиентов не вырастет настолько, что процессоры останутся в высоком энергетическом состоянии. Обычные фьютексы не имеют наследования приоритетов, что ведёт к большим хвостовым задержкам; фьютексы с наследованием приоритетов непригодны — они предоставляют ещё меньше состояния, а встроенная в них справедливость убивает производительность в целом. Управляемые пространством пользователя направленные пробуждения помогли бы, если бы были доступны.

Управление файловыми дескрипторами: пул VFD

Сервер открывает много файлов — базовые таблицы, временные файлы сортировки и хеширования, случайные вызовы библиотечных функций — и легко превышает системные лимиты. Поэтому реальные дескрипторы ядра не держатся открытыми постоянно, а управляются как пул.

VfdCache — это массив записей, индексируемый значениями типа File; VfdCache[0] служит заголовком списков, а не рабочим дескриптором. Каждая запись описывает один виртуальный дескриптор: хранит текущий kernel FD (или VFD_CLOSED, если он закрыт), размер файла, имя, а также флаги и режим открытия для повторного open(2).

При обращении к файлу сначала проверяется, открыт ли он. Если файл закрыт, вызывается вставка в LRU: сначала освобождаются лишние kernel FD, затем файл открывается и счётчик открытых увеличивается. Если файл уже открыт, но не является самым недавно использованным, он перемещается в голову LRU-кольца.

Решение о закрытии реального дескриптора принимает освобождение по LRU: пока сумма открытых VFD, выделенных дескрипторов и внешних дескрипторов не станет меньше безопасного лимита, закрывается наименее недавно использованный VFD.

while (nfile + numAllocatedDescs + numExternalFDs >= max_safe_fds)
{
	if (!ReleaseLruFile())
		break;
}

Как считается безопасный лимит

Лимит max_safe_fds инициализируется консервативным значением FD_MINFREE и остаётся таким в bootstrap или standalone-backend случаях; в обычной работе postmaster вызывает set_max_safe_fds() поздно при инициализации, и это значение наследуется форкнутыми подпроцессами. Функция подсчитывает доступные дескрипторы, берёт минимум из доступных и max_files_per_process, вычитает зарезервированные для system() и подобных вызовов дескрипторы и, если результат меньше FD_MINFREE, завершает процесс с сообщением «insufficient file descriptors available to start server process».

Ошибки EMFILE/ENFILE обрабатываются прямо в момент открытия: выдаётся сообщение «out of file descriptors: %m; release and retry», освобождается один дескриптор по LRU и попытка повторяется; если освободить не удалось, возвращается ошибка.

Внешние дескрипторы

внешний FD — это дескриптор, который вызывающий код держит открытым дольше короткого интервала и не может использовать другие средства модуля. Его использование только учитывается отдельным счётчиком, тогда как обычные FD представлены записями Vfd и учитываются в nfile.

Резервирование внешнего дескриптора разрешено, только пока число уже зарезервированных меньше трети безопасного лимита; иначе возвращается ошибка EMFILE. Перед увеличением счётчика освобождаются VFD, чтобы итоговое состояние оставалось в пределах лимита. Освобождение просто уменьшает счётчик.

Дескрипторы, привязанные к транзакции

Три функции — для буферизованного FILE*, для небуферизованного дескриптора и для каталога — сначала проверяют лимит на число одновременно выделенных дескрипторов и при превышении выбрасывают ошибку, затем освобождают лишние kernel FD, после чего открывают ресурс и регистрируют его в общем массиве, запоминая вид дескриптора и идентификатор подтранзакции.

Все три регистрируются в одном массиве и автоматически закрываются при commit или abort транзакции — это предотвращает утечку дескриптора при преждевременном завершении через ereport(ERROR). Освобождаются они соответствующими функциями, каждая из которых ищет запись по совпадению вида и дескриптора; при отсутствии записи выдаётся предупреждение, но ресурс всё равно закрывается напрямую.

Долговечность: fsync, rename и удаление

Порядок операций в durable_rename

durable_rename гарантирует, что после возврата эффект переименования сохранится при сбое. Порядок шагов:

  1. Синхронизируется старый файл.
  2. Открывается целевой файл и, если он существует, синхронизируется. Синхронизация цели не строго обязательна, но упрощает рассуждения о сбоях.
  3. Выполняется сам rename.
  4. Синхронизируется файл под новым именем.
  5. Синхронизируется содержащий его каталог.

Такой порядок гарантирует, что при сбое во время работы останется либо прежний, либо перемещённый файл, без смешанного состояния или усечённых файлов.

Зачем синхронизировать каталог

Синхронизация родительского каталога нужна потому, что отдельные fsync файлов не гарантируют синхронизацию записи каталога для этого файла: «It's important to fsync the destination directory itself as individual file fsyncs don't guarantee that the directory entry for the file is synced.» Если вход — просто имя файла, в качестве родительского пути подставляется текущий каталог. После rename синхронизируется новый файл и его каталог, а после удаления — каталог удалённого файла.

PANIC или повтор при ошибке fsync

Уровень ошибки фильтруется через параметр data_sync_retry: при выключенном параметре любая ошибка fsync данных приводит к PANIC. Причина в том, что данные могли быть уже удалены из буферного пула и остаться только в WAL, а повторный fsync может ложно сообщить об успехе. При включённом data_sync_retry сохраняется исходный уровень ошибки, и контрольные точки могут продолжать падать до устранения причины.

Платформенные альтернативы fsync

запись насквозь — в этом режиме при включённом enableFsync выполняется fcntl(fd, F_FULLFSYNC, 0), возвращая 0 при успехе и -1 при ошибке. В отличие от обычного fsync, который может завершиться успехом, когда данные ещё находятся в кэше накопителя, F_FULLFSYNC требует их фактической записи на носитель. Если F_FULLFSYNC не определён, устанавливается errno = ENOSYS и возвращается -1, а при выключенном enableFsync просто возвращается 0.

Платформенные альтернативы сброса грязных данных:

  • При наличии HAVE_SYNC_FILE_RANGE используется sync_file_range с флагом SYNC_FILE_RANGE_WRITE — он сообщает ОС о необходимости начать writeback указанных блоков, но не ждёт завершения; при EINTR вызов повторяется, а при ENOSYS (например, в Windows WSL) выдаётся одно предупреждение и дальнейшие попытки в этом процессе подавляются флагом.
  • Если sync_file_range недоступен, но платформа не WIN32 и определён MS_ASYNC, применяется отображение файла в память, инициирование writeback и удаление отображения; при нулевых смещении и длине размер файла определяется через lseek.

Временные файлы и temp_file_limit

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

Проверка лимита выполняется при записи: если temp_file_limit >= 0 и файл помечен флагом, вычисляется позиция после записи как смещение плюс сумма длин всех векторов; если она превышает текущий размер файла, к общему счётчику прибавляется разница, и при превышении temp_file_limit * 1024 выбрасывается ошибка «temporary file size exceeds "temp_file_limit" (%dkB)».

Согласно документации, лимит задаёт максимальный объём дискового пространства для временных файлов процесса, транзакция при превышении отменяется, значение -1 (по умолчанию) означает отсутствие лимита, а временные таблицы не учитываются.

Выбор табличного пространства

табличное пространство для временных файлов выбирается так: при старте обычного или standalone-бэкенда (не в postmaster) регистрируется хук выхода, чтобы временные файлы удалялись, пока ещё можно отчитываться в статистику. Список табличных пространств действует до конца транзакции, и при нескольких пространствах стартовая позиция выбирается случайно, иначе берётся нулевая.

При открытии временного файла, если список пространств непуст и файл не межтранзакционный, берётся очередное пространство циклическим сдвигом; если оно валидно, файл открывается в нём, иначе — в пространстве текущей базы или в пространстве по умолчанию. Имя файла формируется из префикса, идентификатора процесса и счётчика, открытие идёт с флагами O_RDWR | O_CREAT | O_TRUNC | PG_BINARY; при неудаче создаётся каталог и попытка повторяется, а при повторной неудаче выдаётся ошибка.

Очистка при старте

При старте временные файлы в каталоге по умолчанию удаляются, затем обрабатываются временные файлы отношений. Далее перебираются каталоги табличных пространств, и для каждого строится путь из имени пространства, версии каталога и каталога временных файлов. В каталоге удаляются либо только имена с префиксом временного файла, либо всё; для подкаталогов функция рекурсивно вызывает себя и затем удаляет каталог. Для отношений перебираются только числовые подкаталоги баз, и в них удаляются файлы, похожие на временные отношения. После очистки никаких fsync не выполняется; отдельно определена функция синхронизации файловой системы через syncfs, а рекурсивная синхронизация каталога данных выбирает между fsync и syncfs в зависимости от настройки метода синхронизации при восстановлении.

Слой хранения: сегменты отношений

Отношение разбивается на сегменты по RELSEG_SIZE блоков: номер целевого сегмента для блока вычисляется делением номера блока на RELSEG_SIZE, а смещение внутри сегмента — как BLCKSZ, умноженный на остаток от деления номера блока на RELSEG_SIZE.

Открытые сегменты хранятся в массиве по ответвлению отношения; каждый элемент содержит дескриптор из пула fd.c и номер сегмента, начиная с 0. Длина массива хранится отдельно, а сам массив выделяется в контексте памяти MdCxt.

При обращении к блоку за пределами текущего сегмента система сначала проверяет, открыт ли уже целевой сегмент: если да, работа идёт с ним. Иначе, если задан флаг «не открывать», обращение не обслуживается; в противном случае система последовательно доходит от последнего открытого сегмента (или открывает первый) до целевого, проверяя число блоков и открывая недостающие сегменты. Сегменты всегда открываются по возрастанию, поэтому новый сегмент добавляется в конец: после открытия массив увеличивается, и заполняются поля дескриптора и номера сегмента.

Чтение и запись

Чтение и запись обрабатывают короткие операции через внутренний цикл, который продолжается до полной передачи запрошенного объёма. При коротком чтении или записи корректируются позиция и векторы, после чего операция повторяется. Соседние векторы объединяются, если буферы непрерывны в памяти.

При чтении, если прочитано ноль байт и включён zero_damaged_pages или идёт восстановление, оставшиеся блоки заполняются нулями; иначе возникает ошибка о неполном чтении. При записи ошибка ENOSPC добавляет подсказку проверить свободное место.

Синхронизация сегментов

Немедленная синхронизация отношения сначала открывает все активные сегменты, затем временно открывает неактивные, после чего для каждого сегмента вызывается синхронизация, а неактивные сегменты сразу закрываются.

Регистрация сегмента для синхронизации на следующем checkpoint происходит при создании отношения, при усечении и при пометке всего отношения как нуждающегося в синхронизации. При усечении неактивный сегмент усекается до нуля, регистрируется, затем закрывается и удаляется из массива. При пометке отношения и при немедленной синхронизации неактивные сегменты временно открываются, обрабатываются и закрываются сразу после обработки.

Расширение через posix_fallocate

Создание нового файла отношения идёт с флагами O_CREAT | O_EXCL и не вызывает posix_fallocate. Расширение выполняет отдельная функция: для каждого сегмента, если число блоков больше 8 и метод расширения не «запись нулями», вызывается резервирование места.

Резервирование при наличии HAVE_POSIX_FALLOCATE вызывает posix_fallocate; при EINTR попытка повторяется, при EINVAL или EOPNOTSUPP управление переходит к записи нулей, а при прочих ошибках возвращается -1. Если резервирование не удалось, выдаётся ошибка «could not extend file ... with FileFallocate()» с подсказкой проверить свободное место. Если число блоков не больше 8 или выбран метод записи нулями, используется запись нулей, и при отрицательном результате также возникает ошибка.

Абстракция smgr над md

Каждый бэкенд хранит все существующие объекты SMgrRelation в хеш-таблице, а объекты без пина дополнительно связаны в двусвязный список. При первом обращении таблица создаётся с ключом из локатора файла отношения и записью данных отношения, а список инициализируется.

Новый объект получает нулевой счётчик пинов и помещается в хвост списка непинованных. Закрепление увеличивает счётчик и, если он был равен нулю, удаляет объект из списка — это защищает объект от уничтожения в конце транзакции. Освобождение уменьшает счётчик и, когда он становится нулём, снова добавляет объект в список, разрешая его уничтожение. Массовое уничтожение проходит по списку непинованных и для каждого объекта закрывает все ответвления, удаляет узел из списка и удаляет запись из хеш-таблицы.

Расширение, запись и чтение

Добавление нового блока вызывает расширение на уровне хранилища, а затем корректирует кэш числа блоков: если закэшированное значение совпало с номером блока, оно увеличивается на 1, иначе сбрасывается в недействительное.

Запись предназначена только для обновления уже существующих блоков — для расширения нужно использовать добавление блока. Запись не синхронна: блок не обязательно на диске к моменту возврата, но предусмотрен fsync перед следующим checkpoint; флаг пропуска синхронизации означает, что вызывающий сам обеспечит fsync.

Чтение диапазона блоков требует, чтобы вызывающие, читающие более одного блока, сначала спросили, сколько блоков можно объединить в одну операцию. При чтении вычисляется позиция внутри сегмента, число блоков ограничивается размером сегмента и длиной векторов, и при пересечении границы сегмента выдаётся ошибка. Далее вызывается чтение; при отрицательном результате формируется ошибка, при нулевом (EOF) — возвращаются нули, если включено обнуление повреждённых страниц или идёт восстановление, иначе это ошибка. Запись аналогично вызывает запись; при отрицательном результате формируется ошибка с подсказкой при ENOSPC, а после успешной записи при необходимости регистрирует грязный сегмент.

Кэш числа блоков

Кэшированное число блоков возвращается только во время восстановления и только если значение уже было закэшировано; иначе возвращается недействительное значение. Синхронизация с реальным состоянием обеспечивается механизмом инвалидации: при усечении рассылается сообщение об инвалидации, чтобы другие бэкенды сбросили свои копии кэша, а локально кэш сначала помечается недействительным на случай ошибки, а после успешного усечения устанавливается в новое число блоков, если оно не больше старого, иначе в старое.

Асинхронный ввод-вывод: io_uring

Контекст io_uring создаётся на бэкенд: он содержит блокировку завершений и само кольцо, а массив таких контекстов хранит контексты всех бэкендов; текущий бэкенд использует свой. Перед созданием колец проверяются возможности ядра: пробуется инициализация с пользовательской памятью, и при успехе сохраняется требуемый размер, иначе он остаётся отрицательным.

Флаг асинхронного выполнения выставляется только для операций чтения, если соответствующая проверка возвращает истину; для записей флаг намеренно не применяется.

Завершения обрабатываются под эксклюзивной блокировкой: сначала берётся текущее число готовых событий завершения, затем они извлекаются батчами, каждое помечается как просмотренное и передаётся в обработку. Все события сразу не дренируются, потому что иначе один бэкенд мог бы надолго застрять, получая события завершения без их обработки: «Don't drain more events than available right now. Otherwise it's plausible that one backend could get stuck, for a while, receiving CQEs without actually processing them.»

Как bufmgr использует AIO

Начало ввода-вывода для буфера: если буфер уже в состоянии выполнения ввода-вывода, вызывающий может присоединиться к существующему ожиданию, либо, при необходимости подождать, дождаться завершения, предварительно отправив свои отложенные операции; если работа уже сделана, возвращается соответствующий статус, иначе буфер помечается как занятый вводом-выводом.

Завершение ввода-вывода снимает флаг выполнения и флаг ошибки, при необходимости снимает флаг грязного буфера и флаг необходимости checkpoint, при освобождении уменьшает счётчик ссылок и очищает ссылку ожидания, а затем будит ожидающих.

Прерывание вызывается после ошибки: если буфер не валиден, просто снимается блокировка, иначе при наличии флага ошибки выдаётся предупреждение о повторной ошибке записи, и в любом случае ввод-вывод завершается с флагом ошибки.

Ошибки AIO обрабатываются через колбэки: задаются стадия, общий и локальный обработчики завершения и функция отчёта. Локальный обработчик нужен, чтобы сообщать о сбоях контрольных сумм только в бэкенде, начавшем ввод-вывод. Функция отчёта декодирует ошибку и формирует сообщения в зависимости от статуса, факта обнуления и факта игнорирования.

Scatter-read соседних блоков

Функция, возвращающая максимальное число блоков, объединяемых в одну операцию, включает сам начальный блок. Внутри она под запретом прерываний вызывает реализацию хранилища и возвращает результат. Вызывающие, читающие более одного блока, обязаны сначала спросить её, чтобы узнать, сколько блоков можно объединить.

В цикле scatter-read блоки должны быть последовательными; на каждом блоке начинается ввод-вывод, и если статус не готов к вводу-выводу, цикл прерывается, иначе страница добавляется в массив. Затем регистрируются колбэки — локальный для временных отношений, общий для остальных — и запускается асинхронное чтение с массивом страниц.

Ограничение на размер объединённой операции задаёт io_combine_limit: «Controls the largest I/O size in operations that combine I/O. If set higher than the io_max_combine_limit parameter, the lower value will silently be used instead, so both may need to be raised to increase the I/O size.» Значение по умолчанию — 128kB, без единиц измерения трактуется как блоки по BLCKSZ (обычно 8kB), а максимально возможный размер зависит от ОС и размера блока, обычно 1MB на Unix и 128kB на Windows.

Буферный кэш и вытеснение

При поиске буфера сначала строится тег блока и вычисляется его хеш и партиционный замок. Затем под разделяемым замком партиции ищется существующий буфер: если найден, дескриптор закрепляется, замок отпускается и возвращается признак попадания; но если закрепление вернуло недействительность, признак попадания сбрасывается.

При промахе партиционный замок отпускается и берётся жертва. После этого под эксклюзивным замком партиции делается попытка вставки: если кто-то успел вставить буфер, жертва освобождается, берётся уже вставленный буфер, закрепляется и возвращается как при попадании. Если вставка удалась, берётся заголовок жертвы и тег перезаписывается новым.

Механизм pin/unpin

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

Content lock на буфере

Захват реализован через атомарную попытку, которая читает состояние заголовка и в цикле compare & exchange пытается установить нужный режим: для эксклюзивного режима проверяется, что биты блокировки равны нулю, для разделяемого-эксклюзивного — что нет ни одного из двух эксклюзивных битов, для остальных — что нет эксклюзивного бита. Если лок свободен, функция возвращает успех, иначе неуспех, и она не блокируется — ожидание лежит на вызывающем.

Условный захват не ждёт; для локальных буферов сразу возвращает успех.

Очередь ожидания устроена так: под спинлоком ставится флаг наличия ожидающих, процесс помечается как ожидающий, сохраняется режим ожидания и процесс добавляется в хвост очереди. Пробуждение под блокировкой заголовка обходит очередь, удаляет выбранных ожидающих, переводит их в состояние ожидания пробуждения и собирает в отдельный список, прекращая пробуждение при конфликте: разделяемый конфликтует с эксклюзивным, разделяемый-эксклюзивный — с разделяемым-эксклюзивным и эксклюзивным, эксклюзивный — со всеми.

Условный захват для очистки сначала проверяет, что локальный пин-счётчик равен 1 (иначе неуспех), затем пытается взять лок, и если лок получен, читает общий счётчик ссылок; при значении 1 возвращает успех, иначе снимает лок и возвращает неуспех.

Checkpoint и массовая запись

При старте массовой записи сохраняется текущая позиция перезапуска WAL, чтобы позже определить, не начался ли checkpoint во время записи. При завершении накопленные записи при необходимости сортируются по номеру блока и логируются все страницы одним вызовом. После этого в цикле страницы записываются: если номер блока не меньше текущего размера отношения, сначала заполняются «дыры» нулевыми страницами, а затем записывается сама страница; иначе используется обычная запись.

Регистрация для следующего checkpoint выполняется в конце: если позиция перезапуска WAL не изменилась, отношение регистрируется для синхронизации на следующем checkpoint, иначе выполняется немедленная синхронизация, потому что checkpoint мог пропустить уже записанные страницы. Для временных отношений регистрация не делается, для отношений без WAL вызывается регистрация, а для WAL-логируемых постоянных отношений регистрация защищена флагом задержки checkpoint.

Порядок записи буферов при checkpoint

Порядок задаёт компаратор, который сравнивает элементы последовательно по табличному пространству, отношению, ответвлению и номеру блока. Сравнение табличного пространства идёт первым, поскольку на этом основана логика балансировки записей между табличными пространствами.

Балансировка выполняется через min-heap по прогрессу каждого табличного пространства. В цикле извлекается пространство с минимальным прогрессом, берётся буфер, и если у него установлен флаг необходимости checkpoint, он записывается, увеличивая счётчики записанных буферов. После обработки каждого буфера прогресс увеличивается на долю независимо от фактической записи, чтобы записи оставались сбалансированными, а heap обновляется заменой первого элемента, пока не обработаны все буферы пространства, иначе первый элемент удаляется. По завершении цикла выдаются отложенные записи, а статистика checkpoint увеличивается на число записанных буферов; она не включает буферы, записанные другими бэкендами или фоновым писателем.

Связь с fsync

Функция, советующая ОС начать запись грязных данных, чтобы последующие fsync/fdatasync были менее затратными, сразу возвращает управление, если fsync отключён: «Right now file flushing is primarily used to avoid making later fsync()/fdatasync() calls have less impact. Thus don't trigger flushes if fsyncs are disabled». Аналогично функция синхронизации данных при выключенном enableFsync возвращает 0, ничего не делая. Планирование отложенной записи буферов также учитывает, что при выключенном fsync нет смысла отслеживать буферы, и возвращает управление при direct I/O или выключенном enableFsync.

Память, huge pages и madvise

Параметр huge_pages управляет запросом huge pages для основной области разделяемой памяти: значения try (по умолчанию), on и off. При try сервер пытается запросить huge pages и при неудаче возвращается к обычным страницам, при on неудача не даёт серверу запуститься, при off huge pages вообще не запрашиваются. Фактическое состояние отражается серверной переменной huge_pages_status. Параметр huge_page_size задаёт размер huge pages, когда они включены; значение 0 означает размер по умолчанию в системе.

На Linux для huge pages требуется ядро с CONFIG_HUGETLBFS=y и CONFIG_HUGETLB_PAGE=y, а также настройка ОС на достаточное число huge pages нужного размера. Число требуемых huge pages сообщает runtime-computed параметр shared_memory_size_in_huge_pages, который можно посмотреть до запуска сервера командой postgres -D $PGDATA -C shared_memory_size_in_huge_pages. Это число затем используется для установки vm.nr_hugepages, например sysctl -w vm.nr_hugepages=3170.

Советы madvise

Функция обратной записи не вызывает madvise-советов: она проверяет, что длина положительна и что файл не открыт с direct I/O, затем вызывает доступ к файлу и сброс данных. Ветка с USE_POSIX_FADVISE и POSIX_FADV_WILLNEED вызывает posix_fadvise с советом POSIX_FADV_WILLNEED, повторяя вызов при EINTR; на Darwin вместо этого используется fcntl с F_RDADVISE, а при отсутствии обоих вариантов возвращается 0.

Источники

Похожее