Архивная репликация в PostgreSQL: пошаговая инструкция

Привет! Меня зовут Григорий Тарасенко, я инженер команды SQL в Авито. Сегодня мы разберем, как построить архивную репликацию в PostgreSQL.

02e2173d8ff3b722e0889fdbd0ed9fe1.png

Кратко про репликацию в 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 |
+---------+-----------------+---------+---------+----+-----------+

Таким образом, при помощи архивной репликации, мы сделали репликацию без использования слотов репликации, но с аналогичной гарантией доставки. 

А как в вашей структуре устроена отказоустойчивость да и репликации между базами в принципе? Поделитесь своим опытом в комментариях!

Предыдущая статья: Как обычному фронтендеру стать по-настоящему классным фронтендером

© Habrahabr.ru