Потоковая репликация в PostgreSQL и пример фейловера

21 января 2016

Вот многие жалуются, что PostgreSQL сложно масштабировать и нужно быть в нем очень большим специалистом, чтобы настроить обычную master-slave репликацию. По-моему, это все чушь. Не так давно мне потребовалась всего лишь пара часов вдумчивого чтения документации, чтобы во всем разобраться. В этой заметке я постараюсь показать, что с репликацией в PostgreSQL все очень просто. Заодно мы также разберемся, чем потоковая репликация отличается от логической, что такое синхронная и асинхронная репликация, а также как сделать фейловер в случае падения мастера.

Перекрестная ссылка: Вас также может заинтересовать заметка Начало работы с PostgreSQL. В частности, в ней рассказывается, для чего нужны файлы pg_hba.conf и postgresql.conf, как пользоваться утилитой psql, а также как производится резервное копирование и восстановление PostgreSQL. Далее предполагается, что все это вы уже знаете.

Коротко о главном

Когда вы изменяете данные в базе, все изменения пишутся во Write Ahead Log, или WAL. После записи в WAL СУБД делает системный вызов fsync, благодаря чему данные попадают сразу на диск, а не висят в где-то в кэше файловой системы. Таким образом, если взять и обесточить сервер, при следующей загрузке СУБД прочитает последние записи из WAL и применит к базе данных соответствующие изменения.

Потоковая репликация (streaming replication) в сущности является передачей записей из WAL от мастера к репликам. Писать при этом можно только в мастер, но читать можно как с мастера, так и с реплик. Если с реплики разрешено читать, она называется hot standby, иначе — warm standby. Поскольку во многих приложениях 90% запросов являются запросами на чтение, репликация позволяет масштабировать базу данных горизонтально. Потоковая репликация бывает двух видов — синхронная и асинхронная.

При асинхронной репликации запрос тут же выполняется на мастере, а соответствующие данные из WAL доезжают до реплик отдельно, в фоне. Недостаток асинхронной репликации заключается в том, что при внезапном падении мастера (например, из-за сгоревшего диска) часть данных будет потеряна, так как они не успели доехать до реплик.

При использовании синхронной репликации данные сначала записываются в WAL как минимум одной реплики, после чего транзакция выполняется уже на мастере. Запросы на запись выполняются медленнее в результате возникающих сетевых задержек (которые, однако, внутри одного ДЦ обычно меньше типичного времени планирования запроса). Кроме того, чтобы запросы на запись не встали колом в результате падения одной из реплик, при использовании синхронной репликации рекомендуется использовать по крайней мере две реплики. Зато потерять данные становится намного сложнее.

Заметьте, что синхронная репликация не предотвращает возможности считать с реплики старые данные, так как потоковая репликация — она только про передачу WAL, а не то, что видно в базе с точки зрения пользователя. По крайней мере, так синхронная репликация работает конкретно в PostgreSQL.

В контексте репликации нельзя также не отметить еще один интересный термин. Если одна из реплик в свою очередь является мастером для другой реплики, такую конфигурацию называют каскадной репликацией.

Помимо потоковой репликации в последнее время выделяют еще и так называемую логическую репликацию (logical replication). Реализаций логической репликации в PostgreSQL существует несколько, например, slony и pglogical. Пожалуй, наиболее существенное отличие логической репликации от потоковой заключается в возможности реплицировать часть баз данных и таблиц на одни реплики, а часть — на другие. Платить за это приходится скоростью. И хотя pglogical в плане скорости выглядит многообещающе, на момент написания этих строк это очень молодое, сырое решение. В рамках этой заметки логическая репликация не рассматривается.

Дополнение: В PostgreSQL 10 добавили логическую репликацию, теперь она есть из коробки.

Fun fact! Потоковая репликация в PostgreSQL не работает между разными версиями PostgreSQL, а также если на серверах используется разная архитектура CPU, например, x86 и x64. В частности, это означает, что обновить PostgreSQL до следующей версии при использовании потоковой репликации без даунтайма нельзя. Отсюда еще больший интерес к логический репликации, лишенной этого недостатка. Согласно этой статье, типичное время упомянутого даунтайма составляет пару минут.

Дополнение: С другой стороны, на практике это может быть не такой уж большой проблемой. Дело в том, что при падении мастера факт его падения, а затем и промоутинг релпики до мастера, происходят далеко не моментально. Так что в общем случае условие «мы в любой момент времени можем писать» все равно не выполняется. Читать при обновлении мы можем, так как пару реплик можно оставить работать, пока все остальное обновляется.

Быстрая установка PostgreSQL

При написании этой заметки я использовал Ubuntu 14.04 LTS и PostgreSQL 9.5. Скорее всего, многое из написанного будет также справедливо для других дистрибутивов Linux и версий PostgreSQL. Для эмуляции нескольких машин я использовал LXC контейнеры. Далее предполагается, что машины находятся в локальной сети 10.0.3/24.

Нам понадобится две машины — master и slave. На обеих говорим:

wget --quiet https://www.postgresql.org/media/keys/ACCC4CF8.asc -O - \
  | sudo apt-key add -

В /etc/apt/sources.list.d/pgdg.list пишем:

deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main

Далее:

sudo apt-get update
sudo apt-get install postgresql-9.5

Готово, PostgreSQL установлен!

Настройка master

Правим /etc/postgresql/9.5/main/pg_hba.conf:

host    replication      postgres       10.0.3.0/24            md5
host    all              postgres       10.0.3.0/24            md5

Первая строчка нужна для работы утилиты pg_basebackup. Без второй не будет работать pg_rewind. Если хотим, чтобы в базу по сети мог ходить не только пользователь postgres, в последней строке можно написать вместо его имени all.

Правим /etc/postgresql/9.5/main/postgresql.conf:

# какие адреса слушать, замените на IP сервера
listen_addresses = '10.0.3.245'

wal_level = hot_standby

# опционально: не дожидаемся fsync на реплике при синхронной репликации
# synchronous_commit = remote_write

# это нужно, чтобы работал pg_rewind
wal_log_hints = on

max_wal_senders = 8
wal_keep_segments = 64

# если хотим синхронную репликацию на одну любую реплику
# synchronous_standby_names = '*'

hot_standby = on

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

Далее открываем psql:

sudo -u postgres psql

Меняем пароль пользователя postgres:

ALTER ROLE postgres PASSWORD 'secretpass';

Перезапускаем PostgreSQL:

sudo service postgresql restart

Мастер настроен!

Настройка slave

Останавливаем PostgreSQL:

sudo service postgresql stop

Становимся пользователем postgres:

sudo -u postgres

Под этим пользователем переливаем данные с мастера:

cd /var/lib/postgresql/9.5/
tar -cvzf main_backup-`date +%s`.tgz main
rm -rf main
mkdir main
chmod go-rwx main
pg_basebackup -P -R -X stream -c fast -h 10.0.3.245 -U postgres \
  -D ./main

Последняя команда спросит пароль пользователя postgres, который мы меняли при настройке мастера. Используйте -c fast, чтобы синкнуться как можно быстрее, или -c spread, чтобы минимизировать нагрузку. Еще есть флаг -r, позволяющий ограничить скорость передачи данных (см man).

В /var/lib/postgresql/9.5/main/recovery.conf дописываем:

recovery_target_timeline = 'latest'

Когда у нас упадет мастер и мы запромоутим реплику до мастера, этот параметр позволит тянуть данные с него. Более подробна фича объяснена здесь и в официальной документации.

Также в recovery.conf можно дописать:

recovery_min_apply_delay = 10min

… если вы хотите реплику, отстающую от мастера на заданное количество времени. Это позволит быстро восстановить данные в случае выполненного случайно drop database.

Файлы:

/etc/postgresql/9.5/main/pg_hba.conf
/etc/postgresql/9.5/main/postgresql.conf

… правим аналогично мастеру, только в postgresql.conf нужно указать другой IP. Поскольку реплики могут становиться мастером, конфиги у реплик и мастера одинаковые, вся разница только в recovery.conf.

Запускаем PostgreSQL:

sudo service postgresql start

Поздравляю, репликация настроена!

Проверка репликации

На мастере говорим:

SELECT * FROM pg_stat_replication;

Должны увидеть, что реплика действительно забирает WAL:

-[ RECORD 1 ]----+------------------------------
pid              | 5544
usesysid         | 10
usename          | postgres
application_name | walreceiver
client_addr      | 10.0.3.223
client_hostname  |
client_port      | 45095
backend_start    | 2016-01-01 16:42:30.350283+03
backend_xmin     |
state            | streaming
sent_location    | 0/3000220
write_location   | 0/3000220
flush_location   | 0/3000220
replay_location  | 0/30001E8
sync_priority    | 0
sync_state       | async

На реплике:

sudo less /var/log/postgresql/postgresql-9.5-main.log

Должны увидеть что-то на тему «read only connections»:

LOG:  entering standby mode
LOG:  redo starts at 0/2000028
LOG:  consistent recovery state reached at 0/20000F8
LOG:  database system is ready to accept read only connections
LOG:  started streaming WAL from primary at 0/3000000 on timeline 1

Еще один способ проверить, что репликация работает — сказать на реплике:

ps wuax | grep receiver

Также можно создать базу данных и пару таблиц на мастере, записать туда какие-то данные, убедиться, что на реплике появляется все то же самое. При попытке писать в реплику должны увидеть:

ERROR:  cannot execute INSERT in a read-only transaction

Если вы настроили синхронную репликацию, можете попробовать остановить реплику, попытаться выполнить INSERT на мастере, и убедиться, что он повисает. Еще при использовании синхронной репликации можно контролировать степень «синхронности» на стороне клиента.

Не дожидаемся fsync на реплике:

SET synchronous_commit = 'remote_write';

Пишем только локально, не ждем подтверждения от реплики:

SET synchronous_commit = 'local';

Возвращаем обычное поведение:

SET synchronous_commit = 'on';

Дополнение: В PostgreSQL ≥ 9.6 можно указывать 'remote_apply', что означает «ожидать, пока запись попадет в WAL реплики и применится к данным в таблицах». Нужно однако понимать, как физически работает данная опция. На мастере коммит происходит всегда. Соответствующая запись попадает в WAL и уходит на реплики. Когда мастер видит, что соответствующая запись проигралась на репликах, он уведомляет об этом пользователя. Отсюда есть ряд следствий. Например, если реплика окажется недоступна, то сессия повиснет. Если прибить сессию (кодом в драйвере или Ctr+C в psql), транзакция будет применена на мастере, но не на реплике, вопреки тому, что говорится в описании 'remote_apply'.

Также на слейве можно смотреть, как давно было последнее обновление данных с мастера:

sudo -u postgres psql -c \
  "select now()-pg_last_xact_replay_timestamp();"

Пример вывода:

    ?column?    
-----------------
 00:00:03.639424
(1 row)

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

Промоутим реплику до мастера

Остановим мастер. Допустим, какой-то мониторинг это дело запалил и теперь нам нужно ASAP сделать реплику новым мастером.

На реплике говорим:

sudo -u postgres /usr/lib/postgresql/9.5/bin/pg_ctl promote \
  -D /var/lib/postgresql/9.5/main/

В логе увидим:

LOG:  received promote request
FATAL:  terminating walreceiver process due to administrator command
LOG:  redo done at 2/63000DC0
LOG:  last completed transaction was at log time 2016-01-01 15:35:42
LOG:  selected new timeline ID: 5
LOG:  archive recovery complete
LOG:  MultiXact member wraparound protections are now enabled
LOG:  database system is ready to accept connections
LOG:  autovacuum launcher started

При этом в каталоге /var/lib/postgresql/9.5/main файл recovery.conf автоматически будет переименован в recovery.done.

Легко проверить, что в бывшую реплику теперь можно писать. Конечно, если только вы не использовали синхронную репликацию с одной-единственной репликой.

Интересно, что хотя реплику и можно промоутнуть до мастера без перезапуска PostgreSQL, на практике вы, вероятно, все же захотите его перезапустить по следующей причине. Дело в том, что приложение, которое ранее подключилось к этой реплике, так и будет использовать ее в качестве реплики даже после промоута, хотя операции чтения можно было бы размазать по остальным репликам в кластере. Перезапустив PostgreSQL, вы порвете все сетевые соединения, а значит приложению придется подключиться заново, проверить, подключился ли он к мастеру или реплике (запрос SELECT pg_is_in_recovery(); вернет false на мастере и true на репликах), и использовать сетевое соединение соответствующим образом.

Переключение на новый мастер

Переключение остальных реплик на новый мастер, а также восстановление бывшего мастера в качестве реплики происходит одинаково.

Чтобы было чуть меньше путаницы с новым мастером, старым мастером, старой репликой и новой репликой, условимся, что сервера мы называем в соответствии с их текущими ролями. То есть, мастером мы называем новый мастер, бывший репликой до фейловера, а репликой — тот, второй сервер.

В простом и не совсем правильном варианте нужно отредактировать, или создать, если его еще нет, файл /var/lib/postgresql/9.5/main/recovery.conf, указав в нем правильный IP мастера, и сделать sudo service postgresql restart (простой reload не прокатит). Кто-то для того, чтобы не править конфиги и не останавливать СУБД, использует схему с балансировщикам и DNS, но я лично так никогда не делал. В любом случае, этот способ неправильный. Для того, чтобы все хорошо работало во всяких хитрых граничных случаях, реплику следует остановить, сделать pg_rewind, затем запустить реплику.

Утилита pg_rewind находит точку в WAL, начиная с которой WAL мастера и WAL реплики начинают расходиться. Затем она «перематывает» (отсюда и название) WAL реплики на эту точку и накатывает недостающую историю с мастера. Таким образом, реплика и местер всегда приходят к консистентному состоянию. Плюс к этому pg_rewind синхронизирует файлы мастера и реплики намного быстрее, чем pg_basebackup или rsync.

Если вы считаете, что pg_rewind не требуется при использовании синхронной репликации, вот пример маловероятной, но теоретически возможной ситуации. У вас много серверов с PostgreSQL. Сервера в кластере умирают сравнительно часто, поэтому вы решили автоматизировать фейловер. Умирает мастер, запускается фейловер. Среди реплик находится та, что имеет наиболее длинный WAL, на ней делается pg_ctl promote. В этот момент с очень большой задержкой (скажем, 5 секунд — были какие-то сетевые проблемы) на другую реплику прилетает пакет от уже мертвого мастера, и WAL этой реплики становится длиннее WAL нового мастера. Вы не сможете подключить эту реплику к новому мастеру, все сломалось. Если вы хотите, чтобы фейловер работал в том числе и при таких странных граничных случаях, используйте pg_rewind.

Итак, на реплике говорим:

sudo -u postgres /usr/lib/postgresql/9.5/bin/pg_rewind \
  -D /var/lib/postgresql/9.5/main/ \
  --source-server="host=10.0.3.223 port=5432 user=postgres password=??"

Типичный вывод:

servers diverged at WAL position 2/67002170 on timeline 5
rewinding from last common checkpoint at 2/67002100 on timeline 5
Done!

Перемещаем и правим recovery.conf:

sudo mv /var/lib/postgresql/9.5/main/recovery.done \
  /var/lib/postgresql/9.5/main/recovery.conf
sudo vim /var/lib/postgresql/9.5/main/recovery.conf

Проверяем IP мастера и наличие строчки:

recovery_target_timeline = 'latest'

Запускаем реплику, смотрим в логи. Там обязательно должно быть:

LOG:  database system is ready to accept read only connections

Значит PostgreSQL работает в качестве реплики.

Если вдруг видим что-то вроде:

ERROR: requested WAL segment 0000000200000005 has already been removed

… значит реплика слишком отстала от мастера, и нужно перенести файлы с мастера при помощи pg_basebackup, как было описано в начале этой статьи.

Заключение

Очевидно, что по той же схеме можно настраивать каскадную репликацию, репликацию между датацентрами и так далее.

В контексте репликации и фейловера PostgreSQL хотелось бы также отметить следующее:

  • Вам скорее всего также понадобятся (1) мониторинг, (2) агрегация логов, (3) метрики и (4) service discovery, через который все сервисы будут находить текущиих мастеров и слейвов. В качестве service discovery подойдет Consul, etcd, или в крайнем случае ZooKeeper. Для всего остального можно воспользоваться, например, Prometheus и Grafana;
  • При наличии большого количества серверов с PostgreSQL возникает задача автоматического фейловера, которая на практике часто решается тупо написанием наколеночных скриптов на Python;
  • Если у вас небольшой кластер и машины не дохнут каждый день, лучше использовать ручной фейловер, а не автоматический;
  • На практике неплохо работает автофейловер, в котором решение о выборе нового мастера принимается за 5 секунд, и делается не более одного фейловера в час — чаще без участия человека никто ничего не фейловит;
  • При фейловере следует учитывать текущее состояние реплики и промоутить реплику с наиболее длинным WAL, иначе вы потеряете данные;
  • Если мастер падает, он должен падать совсем, чтобы не было ситуации, когда вы уже начали промоутить реплику, тут поднимается старый мастер, и все данные в кластере разъезжаются;
  • Не следует также упускать из виду важность равномерного распределения нагрузки в кластере при фейловере — порой это даже важнее, чем не терять никаких данных;
  • На ваш наколеночный автофейловер нужны тесты, очень много автоматических тестов, проверяющих как PostgreSQL, так и мониторинг с service discovery, а также работу всего кластера при нетсплитах (см заметку про iptables), медленной сети и так далее;
  • Ко всему написанному вы, вероятно, захотите еще и шардирование с автоматическим решардингом и распределенными транзакциями, но это уже очень другая история;

К пониманию многих пунктов из этого списка я пришел благодаря консультации со стороны товарища secwall, за что ему большое спасибо!

Дополнение: См также Stolon: создаем кластер PostgreSQL с автофейловером.

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.