Потоковая репликация в 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% запросов являются запросами на чтение, репликация позволяет масштабировать базу данных горизонтально. Потоковая репликация бывает двух видов — синхронная и асинхронная.

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

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

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

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

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

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 можно указывать точно, на какие реплики производить синхронную репликацию. Подробности вы найдете в официальной документации. Также в 9.6 появился новый синтаксис, позволяющий, помимо прочего, настроить репликацию на кворум.

Далее открываем 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 реплики и применится к данным в таблицах».

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

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) мониторинг, например, Nagios или Zabbix, (2) service discovery, через который все сервисы будут находить текущиих мастеров и слейвов, например, Consul, etcd, или в крайнем случае ZooKeeper, (3) агрегация логов, например, ELK-стэк и (4) метрики, скажем, Graphite и StatsD;
  • При наличии большого количества серверов с PostgreSQL возникает задача автоматического фейловера, которая на практике часто решается тупо написанием наколеночных скриптов на Python;
  • Если у вас небольшой кластер и машины не дохнут каждый день, лучше использовать ручной фейловер, а не автоматический;
  • На практике неплохо работает автофейловер, в котором решение о выборе нового мастера принимается за 5 секунд, и делается не более одного фейловера в час — чаще без участия человека никто ничего не фейловит;
  • При фейловере следует учитывать текущее состояние реплики и промоутить реплику с наиболее длинным WAL, иначе вы потеряете данные;
  • Если мастер падает, он должен падать совсем, чтобы не было ситуации, когда вы уже начали промоутить реплику, тут поднимается старый мастер, и все данные в кластере разъезжаются;
  • Не следует также упускать из виду важность равномерного распределения нагрузки в кластере при фейловере — порой это даже важнее, чем не терять никаких данных;
  • На ваш наколеночный автофейловер нужны тесты, очень много автоматических тестов, проверяющих как PostgreSQL, так и мониторинг с service discovery, а также работу всего кластера при нетсплитах (см заметку про iptables), медленной сети и так далее;
  • Ко всему написанному вы, вероятно, захотите еще и шардирование с автоматическим решардингом и распределенными транзакциями, но это уже очень другая история;

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

А пользуетесь ли вы чем-то из упомянутого в этой заметке и если да, то чем и как именно?

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

Метки: .

Подпишись через RSS, E-Mail, Google+, Facebook, Vk или Twitter!

Понравился пост? Поделись с другими: