Представьте, что вам нужно создать реплику для базы данных объёмом 1 TiB. У вас есть быстрый сервер с NVMe-накопителями и сетью 75 Гбит/с, но pg_basebackup обычно выдаёт всего 300–500 МиБ/с из-за своей однопоточной архитектуры, вне зависимости от того, насколько мощное у вас железо (хотя в PG18 есть сюрприз, о нём поговорим позже).

Решение: заменить pg_basebackup на pgBackRest и использовать параллельную обработку, чтобы создавать реплику существенно быстрее, почти полностью забивая пропускную способность (≈97%) на канале 75 Гбит/с.

Примечание: это исследование в духе R&D, сфокусированное на бенчмарках производительности на простаивающих системах, а не готовое руководство по автоматизации для продакшена. Многие важные для продакшн-среды вещи (мониторинг, логика повторов, интеграция с инструментами оркестрации и т. п.) намеренно опущены, чтобы сосредоточиться на ключевых характеристиках производительности.

// 2025-11-12: Перепроверено и обновлено для точности

pg_basebackup — однопоточный

Стандартный подход к созданию реплики Postgres использует pg_basebackup:

pg_basebackup \
  --write-recovery-conf \
  --wal-method=stream \
  --checkpoint=fast \
  --progress \
  --verbose \
  --host=$PRIMARY \
  --username=$REPLICATION_USER \
  --pgdata=$PG_DATA_DIR

Несмотря на быстрые NVMe-накопители и сеть 75 Гбит/с, pg_basebackup принципиально ограничен своей однопоточной архитектурой. В версиях Postgres до 18 pg_basebackup обычно выдаёт лишь 300–500 МиБ/с, независимо от возможностей железа.

Новый асинхронный ввод-вывод (I/O) в PostgreSQL 18 (мы оставили настройки по умолчанию: io_method=worker и io_workers=3) может заметно ускорить работу. В нашем эксперименте на машинах i4i.32xlarge с локальными NVMe-дисками нам удалось получить 1.08 GiB/s, и это впечатляет. Но ограничение всё равно остаётся. Для больших баз данных это превращается в серьёзное операционное узкое место, особенно когда дисковая подсистема и сеть позволяют прокачивать многие гигабайты в секунду.

Тема многопоточного pg_basebackup годами всплывает в обсуждениях на pgsql-hackers (пример 1, пример 2). Однако до реализации и включения в основную ветку дело так и не дошло. По сей день pg_basebackup остаётся однопоточным.

Примечание: производительность PostgreSQL 18 «из коробки» с его асинхронным вводом-выводом стала приятным сюрпризом и составила 1.08 GiB/s по сравнению с ожидаемыми 300–500 МиБ/с в версиях до 18. Это даёт ускорение в 2–3 раза (примерно соответствует 3 рабочим процессам I/O по умолчанию), но pg_basebackup по-прежнему однопоточный и оставляет большую часть доступной пропускной способности сети и хранилища неиспользованной.

Альтернатива: pgBackRest

pgBackRest в первую очередь предназначен для резервного копирования и восстановления Postgres, но ничто не мешает использовать его, чтобы скопировать каталог данных с одного сервера на другой. Это ровно то, что нам нужно для создания реплики. Этот подход становится очень эффективным при --process-max=8 и выше. Настройка не требует никаких специальных конфигураций на исходной базе: ни Postgres, ни pgBackRest не нужно заранее настраивать на основном сервере.

Требования

  1. Беспарольный доступ по SSH к postgres@$PRIMARY с использованием приватного ключа

  2. Беспарольное подключение к PostgreSQL (libpq/psql) для $REPLICATION_USER к $PRIMARY (обычное SQL-подключение нужно pgBackRest для проверки; настройка репликации через слот и primary_conninfo выполняется в конце)

  3. pgBackRest установлен и на основном сервере, и на реплике в одной и той же версии. Используйте свежую версию (2.55+), чтобы гарантировать поддержку PostgreSQL 18 и наличие оптимизаций производительности

Вся конфигурация выполняется на целевом сервере, аналогично pg_basebackup.

Конфигурация

Сначала проверьте окружение:

postgres --version
pgbackrest --version
uname -a
lsblk -o NAME,SIZE,MODEL

Задайте переменные:

export PRIMARY=primary-db
export REPLICATION_USER=replica
export STANZA_TMP=replica_load
export HOSTNAME=$(hostname)

export PG_DIR=/var/lib/postgresql/18
export PG_DATA_DIR="$PG_DIR/main"
export TMP_DIR="$PG_DIR/pgbackrest_tmp"
export CONFIG_FILE="$TMP_DIR/pgbackrest.conf"

# Путь к PGDATA на основном сервере (должен совпадать с фактическим путём на основном сервере)
export PRIMARY_PGDATA=/var/lib/postgresql/18/main

# Количество параллельных процессов (подбирайте под вашу сеть и CPU)
# Начните с 8–16, увеличивайте до 32 для сетей 10+ Гбит/с
export N=16

mkdir -p "$TMP_DIR" "$PG_DATA_DIR" && chmod 700 "$TMP_DIR" "$PG_DATA_DIR"

Важно: убедитесь, что $TMP_DIR (путь к репозиторию) и каталог spool у pgBackRest находятся на быстром хранилище (в той же файловой системе, что и PGDATA). Использование /tmp на медленном томе EBS вместо NVMe RAID снизило пропускную способность с 9+ GiB/s до 110 MiB/s, то есть дало 80-кратную деградацию производительности. Всегда размещайте repo и spool каталоги pgBackRest на самом быстром доступном хранилище (NVMe, а не EBS).

Создайте временный файл конфигурации, чтобы ��е перезаписывать /etc/pgbackrest.conf:

# Если делаете бэкап со standby, а не с primary, добавьте:
# pg2-host=replica-db
# pg2-path=$PG_DATA_DIR


cat <<EOS > "$CONFIG_FILE"
[$STANZA_TMP]
pg1-host=$PRIMARY
pg1-path=$PRIMARY_PGDATA


repo99-path=$TMP_DIR
repo99-type=posix
repo99-retention-full=1
spool-path=$TMP_DIR/spool
expire-auto=n
start-fast=y
log-level-console=info
compress-type=none
archive-check=n
log-path=/var/log/postgresql/
EOS

Выполнение

# Настраиваем обработку ошибок и очистку
trap cleanup EXIT
cleanup() {
   if [ $? -ne 0 ]; then
       echo "Резервное копирование не удалось, выполняю очистку..."
       rm -rf "$TMP_DIR"
       # Логируем сбой для мониторинга
       logger -t pgbackrest "Создание реплики для $PRIMARY не удалось"
   fi
}

# Создаём stanza с обработкой ошибок
pgbackrest --config="$CONFIG_FILE" --stanza="$STANZA_TMP" stanza-create || {
   echo "ОШИБКА: не удалось создать stanza. Частые причины:"
   echo "  - Нет доступа по SSH к $PRIMARY (проверка: ssh postgres@$PRIMARY 'echo ok')"
   echo "  - Неверный pg1-path в конфиге (проверьте путь PGDATA на основном сервере)"
   echo "  - pgBackRest не установлен на основном сервере (проверка: ssh postgres@$PRIMARY 'which pgbackrest')"
   echo "  - Postgres не запущен на основном сервере"
   exit 1
}


# Если делаете бэкап со standby, добавьте --backup-standby=y
# Когда увидите "INFO: wait for replay on the standby to reach",
# выполните CHECKPOINT на этом standby
time pgbackrest \
 --config="$CONFIG_FILE" \
 --stanza="$STANZA_TMP" \
 --type=full \
 --process-max="$N" \
 backup


# Начните с --process-max=8, чтобы сбалансировать скорость и потребление ресурсов
# Увеличивайте до 16–32 для максимальной скорости на быстрых сетях (10+ Гбит/с)
# Сетевая компрессия (--compress-level-network=1) даёт минимальный эффект
# на быстрых сетях (&lt;6% прироста), но полезна на более медленных (&lt;10 Гбит/с)

Если на исходном сервере ограничены ресурсы CPU, понизьте приоритет процессов pgBackRest на о��новном сервере:

# Добавьте в cron на основном сервере на время окна бэкапа
* * * * * for pid in $(pgrep pgbackrest); do renice 19 -p "$pid"; done

Завершение

# Проверка безопасности: убедитесь, что целевой каталог пуст
if [ "$(ls -A "$PG_DATA_DIR" 2>/dev/null)" ]; then
   echo "ОШИБКА: $PG_DATA_DIR не пуст. Сначала очистите его или используйте другой каталог"
   exit 1
fi

# Исследовательское упрощение: копируем файлы напрямую из репозитория pgBackRest
# (без проверки при восстановлении). Для продакшена не рекомендуется.
mv "$TMP_DIR/backup/$STANZA_TMP/latest/pg_data"/* "$PG_DATA_DIR/"
rm -rf "$TMP_DIR"  # Также удаляет временный файл конфигурации

touch "$PG_DATA_DIR/standby.signal"
cat <<EOS >> "$PG_DATA_DIR/postgresql.auto.conf"
primary_conninfo = 'user=''$REPLICATION_USER'' host=''$PRIMARY'' application_name=''$HOSTNAME'' '
EOS

# Примечание: для потоковой репликации restore_command не нужен
# Реплика будет получать всё по стримингу через primary_conninfo

# Важно: перед запуском реплики создайте слот репликации на основном сервере
# Это не даст WAL-файлам удалиться до того, как реплика их потребит:
# psql -h $PRIMARY -U $REPLICATION_USER -c "SELECT pg_create_physical_replication_slot('replica_slot_name');"

# Запустите экземпляр Postgres на реплике как обычно

Важно: после создания реплики pgBackRest больше не нужен. Реплика будет использовать стандартную потоковую репликацию через primary_conninfo. Этот приём нужен только для ускорения первоначального копирования данных. После запуска реплика работает как обычная потоковая реплика.

Для продакшен-развёртываний, где на основном сервере настроено архивирование WAL, имеет смысл добавить restore_command, чтобы включить восстановление из архива как запасной вариант, если реплика начнёт отставать от потоковой репликации. Однако это не зависит от процесса создания реплики через pgBackRest, описанного здесь.

Бенчмарки производительности

Среда тестирования:

  • База данных: 1.023 TiB (70 баз данных pgbench, ~7 млрд строк)

  • Хранилище: 8× 3 750 ГБ NVMe SSD в RAID0 (AWS i4i.32xlarge)

  • Сеть: 75 Гбит/с = 9.375 ГБ/с (десятичные) = 8.73 GiB/s (двоичные). Ниже мы приводим значения в GiB/s

  • CPU: 128 vCPU

  • PostgreSQL: 18.0 со стандартным асинхронным вводом-выводом на воркерах (io_method=worker, io_workers=3)

  • Условия тестов: без сетевой компрессии (compress-type=none), без проверки контрольных сумм для максимальной пропускной способности

  • Холодный кэш: между тестами мы перезапускали PostgreSQL на реплике и сбрасывали страничный кэш ОС на обоих серверах (sync; echo 3 > /proc/sys/vm/drop_caches). Это неидеально, но даёт достаточно «холодные» условия для каждого прогона

Результаты (холодный кэш для каждого теста):

# Базовый уровень pg_basebackup (PostgreSQL 18 с асинхронным I/O на воркерах)
time pg_basebackup \
 --pgdata="$PG_DATA_DIR" \
 --write-recovery-conf \
 --wal-method=stream \
 --checkpoint=fast \
 --progress \
 --verbose \
 --host=$PRIMARY \
 --username=$REPLICATION_USER
# real    950s (15.8 minutes) - 1.08 GiB/s


# pgbackrest с 4 процессами
time pgbackrest --stanza="$STANZA_TMP" --type=full --process-max=4 backup
# real   575s (9.6 minutes) - 1.78 GiB/s


# pgbackrest с 8 процессами
time pgbackrest --stanza="$STANZA_TMP" --type=full --process-max=8 backup
# real   298s (5.0 minutes) - 3.43 GiB/s


# pgbackrest с 16 процессами
time pgbackrest --stanza="$STANZA_TMP" --type=full --process-max=16 backup
# real   164s (2.7 minutes) - 6.24 GiB/s


# pgbackrest с 32 процессами
time pgbackrest --stanza="$STANZA_TMP" --type=full --process-max=32 backup
# real   101s (1.7 minutes) - 10.13 GiB/s - сеть насыщена


# pgbackrest с 64 процессами
time pgbackrest --stanza="$STANZA_TMP" --type=full --process-max=64 backup
# real   107s (1.8 minutes) - 9.56 GiB/s - ограничение по сети


# pgbackrest со 128 процессами
time pgbackrest --stanza="$STANZA_TMP" --type=full --process-max=128 backup
# real   133s (2.2 minutes) - 7.69 GiB/s - накладные расходы SSH снижают пропускную способность


# pgbackrest с 64 процессами + сетевая компрессия
time pgbackrest --stanza="$STANZA_TMP" --type=full --process-max=64 --compress-level-network=1 backup
# real   101s (1.7 minutes) - 10.13 GiB/s - улучшение на 6%

# pgbackrest со 128 процессами + сетевая компрессия
time pgbackrest --stanza="$STANZA_TMP" --type=full --process-max=128 --compress-level-network=1 backup
# real   127s (2.1 minutes) - 8.25 GiB/s - улучшение на 5%

Анализ

метод

максимум параллельных процессов

длительность (сек)

пропускная способность (логическая), GiB/с

TiB в час

ускорение (во сколько раз)

pg_basebackup

1

950

1.08

3.80

1.0x

pgBackRest

4

575

1.78

6.26

1.7x

pgBackRest

8

298

3.43

12.06

3.2x

pgBackRest

16

164

6.24

21.94

5.8x

pgBackRest

32

101

10.13

35.61

9.4x

pgBackRest

64

107

9.56

33.61

8.9x

pgBackRest

128

133

7.69

27.04

7.1x

pgBackRest + compress

64

101

10.13

35.61

9.4x

pgBackRest + compress

128

127

8.25

29.00

8.1x

Примечание: эти измерения включают накладные расходы на старт (установление соединений, первичные рукопожатия). Для более крупных баз (10+ TiB) эти стартовые накладные расходы становятся пренебрежимо малыми, и устойчивая скорость передачи приближается к показанным пиковым значениям. Например, перенос 10 TiB при скорости, соответствующей 32 процессам, занял бы примерно 1000 секунд, а не 1010 секунд (101 с × 10; это очень грубая оценка, приведена лишь для примера).

Ниже приведена визуализация:

Performance comparison results
Результаты сравнения производительности

Примечание о расчётах пропускной способности: эти значения получены как логический размер базы (1.023 TiB), делённый на «реальное» время выполнения (wall‑clock). Показатель 10.13 GiB/s превышает теоретический предел сети 8.73 GiB/s для 75 Гбит/с, потому что pgBackRest передаёт меньше, чем полный логический размер, за счёт следующего:

  • Разрежённые участки файлов и нулевые блоки не передаются

  • Сжатие метаданных на уровне протокола

  • Эффективная обработка частично заполненных блоков

Фактическая пропускная способность «по проводу» (on-wire) достигает пика примерно на уровне 8.5 GiB/s (по данным счётчиков сетевого интерфейса), что даёт около 97% теоретического предела с учётом протокольных накладных расходов. Более высокие «эффективные» значения показывают, что pgBackRest способен создать реплику базы на 1 TiB быстрее, чем сеть физически могла бы передать 1 TiB сырых данных.

Ключевые наблюдения:

  • Ускорение асинхронного I/O в PostgreSQL 18: pg_basebackup достигает 1.08 GiB/s с асинхронным вводом-выводом на воркерах, что в 2–3 раза быстрее старых версий PostgreSQL (обычно 300–500 MiB/s) и примерно соответствует 3 рабочим процессам I/O по умолчанию.

  • Почти линейное масштабирование: производительность почти удваивается при каждом удвоении параллелизма вплоть до 16 процессов (96% эффективности).

  • Насыщение сети на 32 процессах: насыщение «по проводу» на уровне ~8.5 GiB/s (~97% от теоретических 8.73 GiB/s); логическая пропускная способность показывает 10.13 GiB/s, потому что pgBackRest пропускает разрежённые области и нулевые блоки.

  • Оптимальная конфигурация: --process-max=32 даёт лучшую производительность (в 9.4 раза быстрее pg_basebackup).

  • Сетевая компрессия даёт минимальный эффект: --compress-level-network=1 обеспечивает лишь 5–6% улучшения на сети 75 Гбит/с, и для высокоскоростных сред это не стоит затрат CPU.

  • Убывающая отдача после 32: больший параллелизм добавляет накладные расходы SSH без роста пропускной способности.

  • Для базы 1 TiB: время создания реплики сократилось с 15.8 минут до 1.7 минуты (снижение времени на 89%)

Практические соображения

Для продакшен-развёртываний:

  1. Начните с --process-max=$(($(nproc)/2)), чтобы сбалансировать производительность и нагрузку на систему.

  2. Мониторьте загрузку CPU на исходной и целевой стороне во время бэкапа.

  3. Сетевая компрессия (--compress-level-network=1), похоже, не даёт заметного улучшения на быстрых сетях (10+ Гбит/с). Используйте её на более медленных сетях (<10 Гбит/с), где эффект может быть выше.

  4. Рассмотрите использование renice на исходном сервере, если конкуренция за CPU влияет на продакшен-нагрузку.

  5. Убедитесь, что на целевой стороне достаточно производительности I/O, чтобы переварить параллельную запись

  6. При высоком параллелизме (64+) увеличьте лимиты SSH: MaxStartups=200:30:300 и MaxSessions=200 в /etc/ssh/sshd_config. Альтернатива: SSH ControlMaster создаёт одно постоянное соединение, через которое мультиплексируются все последующие SSH-с��ссии. Мы этот подход ещё не тестировали, но его стоит изучить, чтобы снизить накладные расходы на соединения при высоком параллелизме.

  7. В продакшене включите проверку контрольных сумм (--checksum-page=y) для валидации целостности данных, хотя это снизит пропускную способность. В наших бенчмарках контрольные суммы были отключены, чтобы измерить максимально возможную «сырую» скорость передачи.

Выигрыш в производительности для больших баз данных существенный: сокращение времени создания реплики с 15.8 минут до 1.7 минуты позволяет агрессивнее тестировать аварийное восстановление, быстрее поднимать окружения и снижать операционные риски в сценариях переключения (failover).

Когда лучше использовать стандартный pg_basebackup

Хотя pgBackRest даёт заметный прирост производительности для больших баз на быстром железе, в следующих случаях лучше придерживаться стандартного pg_basebackup:

  • Небольшие базы (< 100 GiB): накладные расходы на настройку и дополнительная сложность перевешивают выигрыш по скорости. Экономия времени минимальна и не стоит лишней конфигурации.

  • Медленное хранилище (не NVMe): узким местом становится дисковый ввод-вывод ещё до того, как упрётся сеть. Параллельная обработка не поможет, если хранилище не успевает.

  • Ограниченная сеть (< 10 Гбит/с): pg_basebackup и так способен насытить канал 1–8 Гбит/с. Однопоточность не является узким местом в таких средах.

  • Ограниченные ресурсы на основном сервере: если мало CPU или памяти, параллельная обработка становится контрпродуктивной. Накладные расходы на управление множеством процессов могут ухудшить производительность основного сервера.

  • Простые окружения: когда разница «15 минут против 2» не оправдывает дополнительную сложность. Иногда операционная простота ценнее голой скорости.

Идеи для следующей итерации

Хотя эти бенчмарки показывают заметный прирост производительности, место для оптимизаций остаётся. В следующей итерации стоит изучить:

  • SSH ControlMaster для мультиплексирования соединений: вместо увеличения лимитов на SSH-подключения использовать SSH ControlMaster, чтобы создать одно постоянное соединение, через которое будут мультиплексироваться все процессы pgBackRest. Это может снизить накладные расходы на соединения при высоком параллелизме (64+ процессов).

  • Настройка сети для высокоскоростных соединений: для сетей 75+ Гбит/с тюнинг параметров ядра может помочь полнее использовать доступную полосу. Стоит рассмотреть такие настройки: net.core.rmem_max, net.core.wmem_max, net.ipv4.tcp_rmem, net.ipv4.tcp_wmem, net.core.netdev_max_backlog и net.ipv4.tcp_congestion_control (BBR)

  • Формула для числа процессов: вместо подбора методом проб и ошибок можно вывести формулу, которая оценивает оптимальное число процессов по полосе сети и числу доступных ядер CPU, например так (черновой набросок):

# Расчёт оптимального числа процессов на основе эмпирических данных:
# Наши тесты: 32 процесса насытили 75 Гбит/с (≈2.5 Гбит/с на процесс)
NETWORK_GBPS=75
BASELINE=$(( NETWORK_GBPS / 3 ))  # Консервативная отправная точка
PROCESS_COUNT=$(( BASELINE < $(nproc) ? BASELINE : $(nproc) ))

# Для 75 Гбит/с и 128 ядер: baseline = 25 процессов
# На практике добавляем 20–30% запаса: 25 × 1.3 ≈ 32 процесса (совпадает с нашим оптимумом)
# Начните с baseline и увеличивайте, если по CPU и сети остаётся запас
  • Измерять реальную скорость передачи данных по сети: наши текущие цифры показывают «эффективную» пропускную способность (логический размер ÷ время), которая может превышать физические пределы сети благодаря тому, что pgBackRest оптимизирует передачу разрежённых файлов и метаданных. В следующей итерации стоит измерять фактическую скорость «по проводу» (on-wire), используя счётчики сетевого интерфейса (NIC) или инструменты вроде ifstat/sar -n DEV во время прогона, чтобы различать логическую и физическую скорость передачи.

  • Тестирование на более новых инстансах AWS: инстансы AWS i7i и i7ie имеют скорость сети 100 Гбит/с (против 75 Гбит/с у i4i), что потенциально может дать примерно на 30% больше пропускной способности, вплоть до 13+ GiB/s при использовании pgBackRest с --process-max=32 или выше. 

  • Сравнение методов ввода-вывода PostgreSQL: протестировать производительность pg_basebackup с разными конфигурациями I/O, чтобы понять вклад каждой:

    • Более высокие значения io_workers (по умолчанию 3, максимум 32), чтобы проверить, улучшают ли дополнительные воркеры пропускную способность.

    • io_method=io_uring для систем, собранных с поддержкой liburing.

    • io_method=sync на PostgreSQL 18 и PostgreSQL 17 в качестве базовой линии, чтобы количественно оценить выигрыш от асинхронного I/O.

Эти оптимизации могут помочь поднять пропускную способность ещё выше и уменьшить н��кладные расходы, которые мы наблюдали при очень высоком параллелизме.

Ключевые выводы

  • Однопоточная архитектура pg_basebackup ограничивает пропускную способность независимо от возможностей железа (хотя асинхронный ввод-вывод на воркерах в PostgreSQL 18 даёт заметное улучшение).

  • pgBackRest с параллельной обработкой может утилизировать доступную пропускную способность сети и производительность дисковой подсистемы.

  • Имеет смысл ожидать заметно лучшую скорость передачи данных уже при --process-max=8 и выше.

  • Оптимальное число процессов зависит от доступных ядер CPU, пропускной способности сети и производительности дисковой подсистемы.

  • Для очень больших баз на быстром железе pgBackRest может сократить время создания реплики в 6–10 раз и даже сильнее.

  • Настройка не требует никакой специальной конфигурации на исходной базе.

  • Все операции выполняются с целевого сервера, по схеме, похожей на рабочий процесс pg_basebackup.

  • Расположение хранилища критически важно: всегда используйте быстрое хранилище (NVMe, а не EBS) для repo и spool каталогов pgBackRest, чтобы избежать 80-кратной деградации производительности.

Если такие эксперименты хочется превратить в устойчивую практику, на курсе «PostgreSQL для администраторов и разработчиков» разбираем настройку кластера, резервное копирование и диагностику блокировок, взаимные блокировки и многое другое. В итоге проще держать производительность и надёжность под контролем на больших объёмах данных. Готовы к обучению? Пройдите вступительный тест и выявите пробелы в знаниях.

А чтобы узнать больше о формате обучения и познакомиться с преподавателями, приходите на бесплатные уроки:

  • 16 марта, 20:00. «PostgreSQL и секционирование: „разделяй и властвуй!“». Записаться

  • 25 марта, 20:00. «Механизмы блокировок в PostgreSQL». Записаться