Привет! Меня зовут Григорий Тарасенко, я инженер команды SQL в Авито. Сегодня мы разберем, как построить архивную репликацию в PostgreSQL.
Кратко про репликацию в PostgreSQL
Практически все решения по отказоустойчивости используют потоковую репликацию, например Patroni, Stolon, repmgr. Но есть нюанс: потоковая репликация не гарантирует, что все изменения — WAL-ы (Write Ahead Log) — будут доставлены. Иначе говоря, на лидере могут спокойно удалиться те WAL-ы, которые не успели примениться на реплике.
Да, можно защититься при помощи завышения параметра wal_keep_size. Но у этого метода есть минусы:
Лидер временно может хранить WAL-ов больше, чем указано в параметре
wal_keep_size
(например, из-за долгой транзакции), которые позже будут удалены. Нет гарантий, что удаленные WAL-ы будут доставлены до реплик;wal_keep_size
вынуждает лидера хранить ненужные WAL-ы, которые применены на репликах и просто занимают место.
И чтобы не завышать wal_keep_size
, в PostgreSQL 9.4 ввели сущность «слот репликации». Его цель: указать лидеру, чтобы он не удалял WAL-файлы, которые реплика не успела применить.
В каких случаях может использоваться слот репликации
Слоты репликации позволяют нам отработать несколько сценариев, например:
Когда долго работает
pg_basebackup
, слоты репликации помогут реплике скачать с лидера недостающие WAL-файлы, чтобы запустить процесс потоковой репликации;Реплика в принципе не успевает за потоковой репликацией, лидер будет удерживать только недошедшие изменения (а не все подряд, как при
wal_keep_size
);Отработка логической репликации, где используются сторонние решения вроде Debezium, либо при использовании модели публикации PostgreSQL.
История из практики
Но, как говорится, с большой силой приходит и большая ответственность. В моей практике был случай: базу из Москвы необходимо было реплицировать в Новосибирск. Это было сделано с помощью потоковой репликации плюс слоты репликации. База так существовала с самого начала до тех пор, пока не была запущена массовая вставка строк.
Реплика перестала получать изменения и слот репликации начал удерживать WAL-ы. В какой-то момент WAL-ов накопилось столько, что на лидере просто закончилось место. Пришлось в ручном режиме в 12 ночи выключать реплику и удалять слот репликации.
Архивная репликация
Слоты репликации сами по себе хотя и несут добро, но при неумелом использовании могут привести к аварии в продакшене. Потоковая репликация не гарантирует, что все WAL-ы будут доставлены, но это может гарантировать archive_command
при условии, что запускаемая команда в archive_command
тоже это гарантирует. В PostgreSQL заложено такое поведение: если при использовании потоковой репликации WAL-ов будет не хватать, то надо получить их через restore_command
.
Таким образом мы можем организовать архивную репликацию (передачу изменений через archive_command
и restore_command
). И, если в базе не используется инструмент логической репликации, и мы имеем относительно быстрое хранилище, куда складируются WAL-ы, то нам ничего не мешает отказаться от слотов репликации полностью.
Как настроить архивную репликацию
Давайте попробуем настроить репликацию в PostgreSQL без слотов репликации, но с аналогичными гарантиями. Я покажу процесс на примере Patroni, так как это основной HA-менеджер PostgreSQL в Авито. Подробнее о том, как организован HA PostgreSQL в Avito.
Шаг 1. Откроем SSH-соединение до лидера и реплики. Они работают на PostgreSQL 14 и объединены в кластер Patroni. Топология выглядит следующим образом:
user@leader:~$ patronictl --config-file /etc/patroni.yml list
+ Cluster: pgsql -----------+---------+---------+----+-----------+
| Member | Host | Role | State | TL | Lag in MB |
+---------+-----------------+---------+---------+----+-----------+
| leader | 192.168.122.5 | Leader | running | 6 | |
| replica | 192.168.122.254 | Replica | running | 6 | 0 |
+---------+-----------------+---------+---------+----+-----------+
Конфигурация Patroni тривиальна и она выглядит вот так:
scope: pgsql
namespace: /cluster/
name: leader
restapi:
listen: 0.0.0.0:8008
connect_address: 192.168.122.5:8008
etcd:
hosts: replica:2379
bootstrap:
dcs:
ttl: 100
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576
postgresql:
use_pg_rewind: true
use_slots: true
parameters:
archive_command: wal-g --config /var/lib/postgresql/.walg.json wal-push %p
archive_mode: always
checkpoint_timeout: 30
hot_standby: 'on'
log_destination: csvlog,syslog
log_directory: /var/log/postgresql
log_disconnections: true
log_file_mode: 420
log_filename: postgresql.log
log_line_prefix: '%n,%r,%c,%l,%s,%v,%x,%e,%u,%d,%i,%a,'
max_replication_slots: 5
max_wal_senders: 5
wal_keep_segments: 20
wal_level: replica
initdb:
- encoding: UTF8
- data-checksums
- locale: en_US.UTF8
pg_hba:
- local all all trust
- local replication all trust
- host replication postgres 127.0.0.1/8 trust
- host replication postgres 0.0.0.0/0 md5
- host all all 0.0.0.0/0 md5
postgresql:
listen: 0.0.0.0:5432
connect_address: 192.168.122.5:5432
data_dir: /var/lib/postgresql/14/main
bin_dir: /usr/lib/postgresql/14/bin
pgpass: /tmp/pgpass
authentication:
replication:
username: postgres
password: postgres
superuser:
username: postgres
password: postgres
parameters:
archive_command: wal-g --config /var/lib/postgresql/.walg.json wal-push %p
archive_mode: always
checkpoint_timeout: 30
hot_standby: 'on'
log_destination: csvlog,syslog
log_directory: /var/log/postgresql
log_disconnections: true
log_file_mode: 420
log_filename: postgresql.log
log_line_prefix: '%n,%r,%c,%l,%s,%v,%x,%e,%u,%d,%i,%a,'
max_replication_slots: 5
max_wal_senders: 5
unix_socket_directories: '/var/run/postgresql'
wal_keep_segments: 20
wal_level: replica
И конфигурация WAL-G, который используется в качестве инструмента для резервного копирования, выглядит вот так:
user@leader:~$ cat /var/lib/postgresql/.walg.json
{"PGDATA":"/var/lib/postgresql/14/main","AWS_ACCESS_KEY_ID":"admin",
"AWS_SECRET_ACCESS_KEY":
"password","WALE_S3_PREFIX":"s3://backup-bucket",
"AWS_ENDPOINT":"http://leader:9000","AWS_S3_FORCE_PATH_STYLE":
"True","AWS_REGION":"us-east-1"}
В качестве S3 используется локально развернутый minio.
Итак, сейчас работает репликация, используя слоты репликации. В этом можно убедиться, спросив системную view pg_replication_slots
. По ней видно, что слот сейчас активен.
user@leader:~$ psql --username postgres --command 'select * from pg_replication_slots' --expanded
-[ RECORD 1 ]-------+----------
slot_name | replica
plugin |
slot_type | physical
datoid |
database |
temporary | f
active | t
active_pid |
xmin |
catalog_xmin |
restart_lsn | 4/D50001F8
confirmed_flush_lsn |
wal_status | reserved
safe_wal_size |
two_phase | f
Шаг 2. Отредактируем конфигурацию Patroni следующим образом:
user@leader:~$ EDITOR=vim patronictl --config-file /etc/patroni.yml edit-config
---
+++
@@ -2,6 +2,9 @@
maximum_lag_on_failover: 1048576
postgresql:
parameters:
+ min_wal_size: 32MB
+ max_wal_size: 48MB
+ wal_keep_segments: 1
archive_command: wal-g --config /var/lib/postgresql/.walg.json wal-push %p
archive_mode: always
checkpoint_timeout: 30
@@ -16,6 +19,8 @@
max_replication_slots: 5
max_wal_senders: 5
wal_level: replica
+ recovery_conf:
+ restore_command: wal-g --config /var/lib/postgresql/.walg.json wal-fetch %f %p
- use_slots: true
+ use_slots: false
retry_timeout: 10
ttl: 100
Apply these changes? [y/N]: y
Configuration changed
EDITOR=vim
указан только для удобства, так как иначе patronictl может открыть для редактирования конфигурации в текстовом редакторе по умолчанию (например nano);Параметры
max_wal_size, min_wal_size, wal_keep_segments
в секцииpostgresql.parameters
нужны для демонстрации архивной репликации, чтобы на лидере не копились WAL-ы;Параметр
restore_command
в секцииpostgresql.recovery_conf
подсказывает: откуда реплике достать недостающие WAL-файлы;И отключаем использование слотов репликации через параметр
postgresql.use_slots
.
Через некоторое время, Patroni применит конфигурацию и выключит слот репликации. Убедимся, что слота репликации больше нет:
user@leader:~$ psql --username postgres --command 'select * from pg_replication_slots' --expanded
(0 rows)
Шаг 3. Дадим маленькую тестовую нагрузку, чтобы убедиться, что кластер продолжает жить.
user@leader:~$ pgbench --transactions 1000 --jobs 2 --client 6 --username postgres testdb
pgbench (14.8 (Debian 14.8-1.pgdg110+1))
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 250
query mode: simple
number of clients: 6
number of threads: 2
number of transactions per client: 1000
number of transactions actually processed: 6000/6000
latency average = 1.760 ms
initial connection time = 5.194 ms
tps = 3409.123838 (without initial connection time)
user@leader:~$ patronictl -c /etc/patroni.yml list
+ Cluster: pgsql -----------+---------+---------+----+-----------+
| Member | Host | Role | State | TL | Lag in MB |
+---------+-----------------+---------+---------+----+-----------+
| leader | 192.168.122.5 | Leader | running | 6 | |
| replica | 192.168.122.254 | Replica | running | 6 | 0 |
+---------+-----------------+---------+---------+----+-----------+
Реплика в состоянии running и лага репликации нет.
Шаг 4. Теперь остановим реплику.
user@replica:~$ sudo systemctl stop patroni
Проверим состояние кластера:
user@leader:~$ patronictl --config-file /etc/patroni.yml list
+ Cluster: pgsql -----------+---------+---------+----+-----------+
| Member | Host | Role | State | TL | Lag in MB |
+---------+-----------------+---------+---------+----+-----------+
| leader | 192.168.122.5 | Leader | running | 6 | |
| replica | 192.168.122.254 | Replica | stopped | | unknown |
+---------+-----------------+---------+---------+----+-----------+
Реплика в состоянии stopped, потому что мы выполнили graceful-shutdown.
Шаг 5. Теперь дадим больше тестовой нагрузки, чтобы сгенерировать больше WAL-ов.
user@leader:~$ pgbench --transactions 10000 --jobs 2 --client 6 --username postgres testdb
pgbench (14.8 (Debian 14.8-1.pgdg110+1))
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 250
query mode: simple
number of clients: 6
number of threads: 2
number of transactions per client: 10000
number of transactions actually processed: 60000/60000
latency average = 1.675 ms
initial connection time = 4.289 ms
tps = 3581.818403 (without initial connection time)
Самое главное — это убедиться, что у нас в системе хранится достаточно маленькое число WAL-ов (из-за того, что выставили малые max_wal_size, min_wal_size
, wal_keep_segments
) и они все уходили в minio:
user@leader:~$ sudo -u postgres du -sh /var/lib/postgresql/14/main/pg_wal
49M /var/lib/postgresql/14/main/pg_wal
Может возникнуть ситуация, что WAL-ов не 48 Мегабайт, а чуть больше. Но на самом деле на процесс это влиять не должно, потому что у нас и так малое число WAL-журналов. Если WAL-ов все же больше 48 Мегабайт, то поможет либо команда CHECKPOINT
, либо повторный прогон pgbench
.
Шаг 6. Теперь поднимем реплику назад и увидим, что WAL-G стягивает недостающие журналы из minio.
user@replica:~$ ps -fu postgres
UID PID PPID C STIME TTY TIME CMD
user@replica:~$ sudo systemctl start patroni
user@replica:~$ ps -fu postgres
UID PID PPID C STIME TTY TIME CMD
postgres 1396 1 6 14:12 ? 00:00:00 /usr/bin/python3 /usr/local/bin/patroni /etc/patroni.yml
postgres 1407 1 0 14:12 ? 00:00:00 /usr/lib/postgresql/14/bin/postgres -D /var/lib/postgresql/14/main --config-file=/var/lib/postgresql/14/main/postgresql.conf --listen_addresses=0.0.0.0 --port=5432 --cluster_name=pgsql --wal_level=replica --hot_standby=
postgres 1409 1407 12 14:12 ? 00:00:00 postgres: pgsql: startup waiting for 0000000600000004000000EA
postgres 1437 1407 16 14:12 ? 00:00:00 postgres: pgsql: checkpointer
postgres 1438 1407 0 14:12 ? 00:00:00 postgres: pgsql: background writer
postgres 1439 1407 0 14:12 ? 00:00:00 postgres: pgsql: archiver archiving 0000000600000004000000E5
postgres 1440 1407 0 14:12 ? 00:00:00 postgres: pgsql: stats collector
postgres 1597 1407 0 14:12 ? 00:00:00 postgres: pgsql: postgres postgres 127.0.0.1(47362) idle
postgres 1665 1439 0 14:12 ? 00:00:00 sh -c wal-g --config /var/lib/postgresql/.walg.json wal-push pg_wal/0000000600000004000000E5
postgres 1666 1665 5 14:12 ? 00:00:00 wal-g --config /var/lib/postgresql/.walg.json wal-push pg_wal/0000000600000004000000E5
postgres 1673 1409 0 14:12 ? 00:00:00 sh -c wal-g --config /var/lib/postgresql/.walg.json wal-fetch 0000000600000004000000EA pg_wal/RECOVERYXLOG
postgres 1674 1673 1 14:12 ? 00:00:00 wal-g --config /var/lib/postgresql/.walg.json wal-fetch 0000000600000004000000EA pg_wal/RECOVERYXLOG
Если мы сделаем patronictl list
, то увидим, что у реплики есть некий лаг репликации:
user@leader:~$ patronictl --config-file /etc/patroni.yml list
+ Cluster: pgsql -----------+---------+---------+----+-----------+
| Member | Host | Role | State | TL | Lag in MB |
+---------+-----------------+---------+---------+----+-----------+
| leader | 192.168.122.5 | Leader | running | 6 | |
| replica | 192.168.122.254 | Replica | running | 6 | 250 |
+---------+-----------------+---------+---------+----+-----------+
В скором времени он уменьшится и превратиться в ноль.
user@leader:~$ patronictl --config-file /etc/patroni.yml list
+ Cluster: pgsql -----------+---------+---------+----+-----------+
| Member | Host | Role | State | TL | Lag in MB |
+---------+-----------------+---------+---------+----+-----------+
| leader | 192.168.122.5 | Leader | running | 6 | |
| replica | 192.168.122.254 | Replica | running | 6 | 0 |
+---------+-----------------+---------+---------+----+-----------+
Таким образом, при помощи архивной репликации, мы сделали репликацию без использования слотов репликации, но с аналогичной гарантией доставки.
А как в вашей структуре устроена отказоустойчивость да и репликации между базами в принципе? Поделитесь своим опытом в комментариях!
Предыдущая статья: Как обычному фронтендеру стать по-настоящему классным фронтендером
Комментарии (4)
vesper-bot
22.12.2023 10:35Задача - бэкап базы с реплики, инструмент - barman, режим - асинхронная реплика со слотом, задержки - тот же сегмент сети и околонулевые объемы изменений в секунду (зато критичные). Пробовали ставить синхронный коммит, работает даже, но потом убрали.
Melkij
Как обычно, какую задачу решаем?
Если вам нужна гарантия, что все WAL сохранены где-то ещё - то начать нужно с вопроса, что вы хотите что бы делала база когда это невозможно. Ну вот буквально, транзакция хочет сделать insert данных. Что делать базе? Разрешить выполнить запись сразу и закоммитить? Всё, тут мы уже нарушили гарантию доставки WAL. У нас есть кусочек WAL, который существует только локально на этой базе и ещё не был никуда скопирован. Для этой задачи нужна синхронная репликация. При том, иметь в виду, что insert в postgresql выполнен будет в любом случае при активной синхронной репликации, синхронная репликация гарантирует что ответ базы "commit выполнен" будет получен после доставки WAL на указанную конфигурацию синхронных реплик. При проблемах с синхронной репликацией останавливается запись данных, но раз просили гарантию сохранности - то вот она цена.
Хотим чтобы реплика могла догнать после существенного отставания репликации?
гарантированно? слот репликации без ограничения, либо архив WAL если он очищается с учётом отставания этой реплики. Хочу обратить внимание, что если archive_command перестаёт работать - то база будет накапливать WAL безгранично, подобно слотам репликации max_slot_wal_keep_size. Зато скорей всего под архив WAL получится выделить куда более вместительное и более дешёвое хранилище. Цена той гарантии, что реплика догонит - потенциально неограниченный расход места на хранение WAL.
после разумного объёма отставания? wal_keep_size либо слот репликации с max_slot_wal_keep_size, либо архив WAL если тот очищается независимо от отставания реплики
Но вообще архив WAL обычно заводят когда нужен point-in-time-recovery, а не для репликации. И это другая задача. А потом уже, раз всё равно пишем архив WAL, - то можно и задействовать restore_command чтобы из него подтягивать недостающие WAL для отстающих реплик, попутно.
В большинстве production систем действительно лучше потерять реплику и переналить заново, чем уронить мастер с переполнением диска. Поэтому мы почти всегда используем только wal_keep_size. Если реплика отстала до requested WAL segment ... has already been removed и ничего не могли сделать после того как мониторинг сказал что реплика отстаёт - то переналить заново. Зато не потеряли мастер.
restore_command, кстати, имеет одно не очень явно задокументированное свойство: он имеет безусловный приоритет над WAL уже сохранёнными в pg_wal. То есть даже если у вас все WAL есть локально у базы во время старта, то всё равно они будут заново читаться из архива. Поэтому если у вас архив wal не очень быстрый на чтение - то restore_command будет замедлять все рестарты баз.
gle4er Автор
Чтобы лидер меньше зависел от состояния реплик.
По сути, вот это:
Но чтобы еще лидер от этого не помер. Опять же: либо из-за забитого места WAL-ами, либо из-за переутилизации сети. Соответственно, слот репликации без ограничения - сразу нет,
wal_keep_size
имеет недостатки (которые описаны в статье):Но, идея с
max_slot_wal_keep_size
валидна: нивелируется второй недостаток (вынужденно хранить ненужные WAL-ы), но имеет первый недостаток (который можно попробовать нивелировать "архивной репликацией" кстати).Поэтому, по сути, "архивная репликация" работает, как вы и написали:
По поводу этого:
Тоже валидное замечание, спасибо, что отметили. Поэтому, в статье отмечено:
Но да, не отмечено: что такое "относительно быстрое хранилище", поэтому, тут могут быть споры. Я понимаю, что если тащить WAL-ы из той же Москвы в Новосибирск, то идея с архивной репликацией быстро умрет :)
А по поводу поведения
restore_command
: спасибо, не знал. Век живи - век учись... :)И спасибо за ценный развернутый комментарий!
Melkij
Мастер может в любой конфигурации хранить WAL'ов больше чем указано где-либо потому что ограничения на максимальный объём WAL просто нет.
И нет, wal_keep_size не имеет никакого отношения к долгим транзакциям. У нас REDO, не волнует возраст транзакций ни для crash recovery ни для удержания WAL, нас волнует LSN который записали при чекпойнте и соответственно откуда стартовать recovery. А уже при crash recovery смотрим, встретили ли commit запись в WAL или нет.
Вот то, что wal_keep_size держит постоянно занятый объём, я, впрочем, и вовсе как недостаток не рассматриваю - это вполне неплохой удобный резерв наравне с reserved space в ext4. Надо освободить место - оговариваем, что тогда могут отвалиться дальние/медленные реплики. Зато когда медленные реплики действительно отстают - у нас не начинает исчезать свободное место.
Я бы кстати наоборот ожидал что на фиговой сети file shipping репликация будет менее проблемная. WAL'ы в виде файликов неплохо сжимаются и сжатие wal просто добавить для архива (по-моему, это вообще дефолтный режим в wal-g). А вот streaming репликация гоняет несжатые данные. При том, потоковая репликация ходит строго одним TCP коннектом. Если нет своих сетевых инженеров для тюнинга сетевого стека, то кучка маленьких параллельных TCP сессий на нестабильной сети наверняка будут работать лучше чем одно долгоживущее.
Ну и в плюсы репликации через архив можно добавить, что для этого способа не надо прорубать сетевое окно на подключение до рабочих баз через всевозможные VPN и файрволы по пути. Базу можно изолировать в закрытом сетевом контуре, куда извне не подключиться.
Плюс гибкость выбора транспортировки WAL, хоть по RFC 1149 кидать.