Назад к блогу

Zero-copy запись WAL в PolarDB: что меняется на пути коммита

Zero-copy запись WAL в PolarDB: что меняется на пути коммита

В PolarDB появился режим zero-copy для записи WAL, устраняющий лишнее копирование данных между процессами на критическом пути коммита. Разбираем, как устроен новый механизм, зачем понадобилось отображать разделяемую память в pfsd и что это даёт под нагрузкой записи.

В PolarDB появился режим zero-copy ввода-вывода для демона файловой системы pfsd: буферы из разделяемой памяти записываются на устройство напрямую, без промежуточной копии в пул, принадлежащий демону. Режим включается параметром polar_enable_zero_copy и по умолчанию выключен — прежний копирующий путь остаётся основным.

Что именно изменилось

Запись WAL в PolarDB обслуживает pfsd. В пути по умолчанию 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 и 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 он не затрагивается: потоковая репликация по умолчанию асинхронна, из-за чего между коммитом на primary и видимостью изменений на standby есть небольшая задержка.

Источники

Похожее