Архивная репликация в PostgreSQL: пошаговая инструкция
Привет! Меня зовут Григорий Тарасенко, я инженер команды 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 -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 |
+---------+-----------------+---------+---------+----+-----------+
Конфигурация 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 -U postgres -c 'select * from pg_replication_slots'
slot_name | plugin | slot_type | datoid | database | temporary | active | active_pid | xmin | catalog_xmin | restart_lsn | confirmed_flush_lsn | wal_status | safe_wal_size | two_phase
-----------+--------+-----------+--------+----------+-----------+--------+------------+------+--------------+-------------+---------------------+------------+---------------+-----------
replica | | physical | | | f | t | 1342 | | | 4/D50001F8 | | reserved | | f
(1 row)
Шаг 2. Отредактируем конфигурацию Patroni следующим образом:
user@leader:~$ EDITOR=vim patronictl -c /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 -U postgres -c 'select * from pg_replication_slots'
slot_name | plugin | slot_type | datoid | database | temporary | active | active_pid | xmin | catalog_xmin | restart_lsn | confirmed_flush_lsn | wal_status | safe_wal_size | two_phase
-----------+--------+-----------+--------+----------+-----------+--------+------------+------+--------------+-------------+---------------------+------------+---------------+-----------
(0 rows)
Шаг 3. Дадим маленькую тестовую нагрузку, чтобы убедиться, что кластер продолжает жить.
user@leader:~$ pgbench -t 1000 -j 2 -c 6 -U postgres testdb
pgbench (14.8 (Debian 14.8-1.pgdg110+1))
starting vacuum...end.
transaction type:
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 -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 | stopped | | unknown |
+---------+-----------------+---------+---------+----+-----------+
Реплика в состоянии stopped, потому что мы выполнили graceful-shutdown.
Шаг 5. Теперь дадим больше тестовой нагрузки, чтобы сгенерировать больше WAL-ов.
user@leader:~$ pgbench -t 10000 -j 2 -c 6 -U postgres testdb
pgbench (14.8 (Debian 14.8-1.pgdg110+1))
starting vacuum...end.
transaction type:
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 -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 | 250 |
+---------+-----------------+---------+---------+----+-----------+
В скором времени он уменьшится и превратиться в ноль.
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 |
+---------+-----------------+---------+---------+----+-----------+
Таким образом, при помощи архивной репликации, мы сделали репликацию без использования слотов репликации, но с аналогичной гарантией доставки.
А как в вашей структуре устроена отказоустойчивость да и репликации между базами в принципе? Поделитесь своим опытом в комментариях!
Предыдущая статья: Как обычному фронтендеру стать по-настоящему классным фронтендером