В PolarDB появился режим zero-copyввод-вывод без промежуточной копии: данные передаются на устройство прямо из буфера вызывающего ввода-вывода для демона файловой системы pfsd: буферы из разделяемой памяти записываются на устройство напрямую, без промежуточной копии в пул, принадлежащий демону. Режим включается параметром polar_enable_zero_copy и по умолчанию выключен — прежний копирующий путь остаётся основным.
Что именно изменилось
Запись WAL в PolarDB обслуживает pfsdдемон файловой системы, который выполняет ввод-вывод устройства для файлов данных и WAL кластера. В пути по умолчанию pfsd не пишет буфер вызывающего напрямую: он копирует полезную нагрузку в собственный пул памяти и уже оттуда пишет на устройство. Эта копия — последняя, что осталась на критическом пути коммита.
Zero-copy устроен так: весь основной сегмент разделяемой памяти подкрепляется одним memfdанонимным файловым дескриптором в памяти, который можно отобразить в адресное пространство процесса. Этот memfd отображается MAP_SHARED в postmasterглавном процессе кластера, который запускается первым и порождает все остальные процессы до forkсистемного вызова, создающего копию текущего процесса и регистрируется в pfsd ровно один раз. В результате внутри одного региона оказываются буферы WAL, пул разделяемых буферов, пул копирующих буферов, страницы SLRUсегментированного журнала для служебных записей вроде статусов транзакций и scratch-буферавременный буфер, куда складываются байты перед подсчётом контрольных сумм. Обёртки pfsd_pwrite_zc / pfsd_pread_zc — это варианты обычных pwrite/pread для работы с pfsd: они распознают любой указатель внутри сегмента и выполняют ввод-вывод прямо с него, без копии.
Буферы, которым это нужно, выравниваются по PG_IO_ALIGN_SIZE — константе, задающей размер выравнивания для прямого ввода-вывода, — безусловно, не только при активном zero-copy. Но выравнивание здесь лишь условие быстрого пути, а не требование корректности: при невыровненном буфере защита выравнивания в обёртке откатывается к копирующему пути.
Включение polar_enable_zero_copy убирает копию при наличии shared storageобщего хранилища, к которому обращается pfsd и pfsd; выключение возвращает прежнее поведение. Для буферов, которые pfsd не видит, копия сохраняется.
Как было раньше
Копия нужна потому, что pfsd и postgres — разные процессы, а процесс читает только ту память, которую видит. Вызывающая сторона может захотеть ввод-вывод с любым буфером: полученным через malloc, выделенным на стеке или размещённым линкером в сегменте данных. Ни один из таких буферов нельзя сделать видимым обоим процессам по одному адресу, поэтому pfsd не может прочитать их напрямую — байты приходится копировать в пул, которым владеет сам демон.
На пути коммита это выглядело так: бэкенд собирает WAL-запись и ждёт её долговечности. Первое копирование происходит ещё при сборке записи — бэкенд переносит байты в WAL-буферыобласть разделяемой памяти, где накапливаются записи журнала перед сбросом на устройство. Затем, в пути по умолчанию, бэкенд ждёт, пока pfsd скопирует эти же байты ещё раз — уже в собственный пул демона, — прежде чем начнётся запись на устройство. Для коммита, который уже платит за memcpy в WAL-буферы и накладные расходы системного вызова при сбросе, эта вторая копия не даёт выгоды — но для произвольных буферов она неизбежна.
Почему это сделали
Копия добавляет задержку каждому коммиту под нагрузкой записи и расходует пропускную способность памяти, которую использует остальная система. Кроме того, издержки плохо масштабируются с объёмом WAL: чем больше WAL, тем больше байтов копируется. Для буферов, уже лежащих в разделяемой памяти, копия — чистые накладные расходы, и zero-copy нацелен именно на это подмножество: pfsd подключается к тому же региону, и копия для таких буферов становится ненужной.
Что это меняет на практике
Сброс WAL теперь пишет напрямую из WAL-буферов на устройство, а не через пул pfsd. Экономия не ограничивается WAL: она распространяется на любые буферы в разделяемой памяти, к которым обращается pfsd. Корректность при этом держится на том, что pfsd пропускает копию только для буферов, которые действительно видит, и откатывается к копии для всех остальных. Видимость определяется по адресу: весь главный сегмент разделяемой памяти подкреплён одним memfd, отображённым MAP_SHARED в postmaster до fork и зарегистрированным в pfsd ровно один раз, поэтому шим распознаёт любой указатель, лежащий внутри этого сегмента, и выполняет ввод-вывод прямо из него; для указателей вне сегмента остаётся копирующий путь.
Проверить поведение можно переключением параметра: включение убирает копию, выключение возвращает ровно прежнее поведение. При невыровненном буфере срабатывает откат к копирующему пути.
Ограничения и открытые вопросы
Zero-copy применим только к буферам, уже находящимся в разделяемой памяти: произвольные буферы (из malloc, со стека, из сегмента данных) нельзя сделать видимыми обоим процессам по одному адресу, и для них копирование неизбежно. Выравнивание по PG_IO_ALIGN_SIZE — квалификатор быстрого пути, а не требование корректности, поэтому невыровненный буфер просто уходит на медленный путь. Копирование пропускается только для буферов, которые pfsd реально видит, иначе используется копирующий путь.
Задержка доставки WAL на реплики — отдельный механизм, и в описании zero-copy он не затрагивается: потоковая репликацияпередача WAL с основного сервера на резервный по мере его генерации по умолчанию асинхронна, из-за чего между коммитом на primaryосновном сервере, который принимает записи и передаёт WAL и видимостью изменений на standbyрезервном сервере, который принимает WAL и воспроизводит его есть небольшая задержка.