Архивная репликация в 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 --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 | +---------+-----------------+---------+---------+----+-----------+

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

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/781892/