Привет! На связи Евгений Безручкин, DevOps-инженер компании «Флант». И мы продолжаем делиться историями из нашей практики.
В одном проекте у нас есть на обслуживании кластер PostgreSQL на виртуальной машине под управлением Patroni. В ходе работы нам нужно было уменьшить мастер кластера по CPU и памяти. Но на этапе, когда мы меняли мастер c репликой ролями (switchover), возникла проблема: живым остался только мастер, а все реплики остановились. Но мы нашли выход из ситуации, попутно найдя и интересные моменты. В этой статье расскажем, как нам с помощью синхронной репликации удалось сделать так, чтобы switchover прошёл без сбоев.
Предыстория
Кластер состоял из большого мастера 32с64g и двух реплик. Реплики асинхронные, так как кластер геораспределённый и ждать, когда в другом городе на реплике появятся данные, некогда.
В какой-то момент такой большой мастер стал не нужен. И мы решили его сдуть в целях экономии.
План был таков:
Дождаться низкой нагрузки на кластер.
Сделать switchover — сменить мастер на реплику №1.
Переконфигурировать ВМ с бывшим мастером.
Сделать switchover обратно.
Делаем switchover
Что ж, вот я дождался низкой нагрузки на кластер, приступаю к switchover. Поскольку реплики у нас асинхронные, есть шанс потерять часть (недоехавших с мастера) данных. Чтобы избежать этого, обычно мы делаем так:
CHECKPOINT;
— сбрасывает на диск изменённые буферы и создаёт контрольную точку, указывающую на консистентное состояние кластера.SELECT pg_switch_wal();
— закрывает текущий журнал WAL.
Из предположений: CHECKPOINT на репликах заставит их зафиксировать все пришедшие изменения на диск, и они будут более готовы к переключению. Возможно, мы ошибались.
Следующим запросом проверяю, как всё обстоит на репликах, насколько они отстают.
Запрос показывает разницу состояний мастера и реплик на разных стадиях жизни WAL:
SELECT
pg_wal_lsn_diff( pg_current_wal_insert_lsn(),pg_current_wal_flush_lsn()) non_flushed,
s.application_name,
pg_wal_lsn_diff( pg_current_wal_insert_lsn(),s.sent_lsn) as sent_lag,
pg_wal_lsn_diff( pg_current_wal_insert_lsn(),s.write_lsn) as write_lag,
pg_wal_lsn_diff( pg_current_wal_insert_lsn(),s.flush_lsn) as flush_lag,
pg_wal_lsn_diff( pg_current_wal_insert_lsn(),s.replay_lsn) as replay_lag
FROM pg_stat_replication s;
non_flushed | application_name | sent_lag | write_lag | flush_lag | replay_lag
-------------+----------------------+----------+-----------+-----------+------------
0 | postgres-1 | 0 | 0 | 0 | 0
0 | postgres-2 | 0 | 0 | 0 | 0
Выглядит так, будто мастер и реплики идентичны.
Выполняю patroni switchover и получаю сюрприз:
+ Cluster: main ----+--------------+---------+---------+----+-----------+------------------+
| Member | Host | Role | State | TL | Lag in MB | Tags |
+-------------------+--------------+---------+---------+----+-----------+------------------+
| postgres-0 | 10.1.4.20 | Replica | running | 8 | unknown | clonefrom: true |
+-------------------+--------------+---------+---------+----+-----------+------------------+
| postgres-1 | 10.2.4.3 | Leader | running | 9 | | clonefrom: true |
+-------------------+--------------+---------+---------+----+-----------+------------------+
| postgres-2 | 10.3.4.7 | Replica | running | 8 | unknown | clonefrom: true |
+-------------------+--------------+---------+---------+----+-----------+------------------+
Поздравляем, у вас ни одной реплики! Что же случилось?
Patroni остановила текущий мастер и передала его роль реплике postgres-1. Между этими шагами в останавливающийся мастер приехали какие-то изменения, которые применились на реплике postgres-2, но не успели в postgres-1. Роль мастера досталась самой отсталой реплике.
Тут такое правило — мастер всегда впереди, и если на реплике больше транзакций, чем на мастере, ей в кластер не попасть. Либо откатывай через pg_rewind до состояния мастера, либо переливай всё с мастера.
Мы уже ловили такие ситуации, и опыт в этом кластере говорил, что pg_rewind медленнее, и проще/быстрее просто перелить реплики. Но бывает и по-другому: например, если база огромная, дети успеют школу закончить, прежде чем данные на реплику перельются, уж лучше откатить состояние.
До того как синхронизировать базы, нужно проверить, что же не доехало при переключении. И если там было что-то важное, сохранить это и вернуть на место.
Гляжу в журналы и вижу:
postgres-0$ /usr/lib/postgresql/15/bin/pg_waldump /var/lib/postgresql/15/main/pg_wal/0000000800002265000000CE
postgres-2$ /usr/lib/postgresql/15/bin/pg_waldump /var/lib/postgresql/15/main/pg_wal/0000000800002265000000CE
CHECKPOINT_SHUTDOWN
А в postgres-1 информация о выключении мастера не записалась.
Теперь ясно, где поставить запятую в «Сохранять нечего переливать». Запускаю patronictl reinit postgres-2
не глядя. Через полчаса реплика рапортует об окончании процесса. Смотрю:
+ Cluster: main ----+--------------+---------+---------+----+-----------+------------------+
| Member | Host | Role | State | TL | Lag in MB | Tags |
+-------------------+--------------+---------+---------+----+-----------+------------------+
| postgres-0 | 10.1.4.20 | Replica | running | 8 | unknown | clonefrom: true |
+-------------------+--------------+---------+---------+----+-----------+------------------+
| postgres-1 | 10.2.4.3 | Leader | running | 9 | | clonefrom: true |
+-------------------+--------------+---------+---------+----+-----------+------------------+
| postgres-2 | 10.3.4.7 | Replica | running | 8 | unknown | clonefrom: true |
+-------------------+--------------+---------+---------+----+-----------+------------------+
Полчаса прошло, а кластер в полурабочем состоянии. Смотрю в журналы Patroni: реплика телеграфирует: «Синхронизация была с postgres-0», то есть с бывшего мастера.
Запускаю reinit снова, в журнале то же самое — реплика хочет данные из прошлого таймлайна. Это ж-ж-ж неспроста!
В любой непонятной ситуации читай исходники. А там чёрным по белому написано, что клонирование сервера базы данных выполняется с мастера только в случае отсутствия других реплик с тегом **clonefrom**. Задумка здравая — чтобы мастер не нагружать процессом резервного копирования. Но при этом Patroni не смотрит, насколько актуальна реплика.
Ну раз нам всё равно реконфигурировать бывший мастер, удаляю postgres-2, запускаю:
patronictl reinit main postgres-0
Ожидаемо забирает данные с postgres-1. И, чтобы не ждать долго, запускаю сразу вторую реплику:
patronictl reinit main postgres-2
Опять сюрпризы — postgres-0, уже скачавший десяток гигабайт, начинает литься заново. Останавливаю, запускаю снова patronictl reinit main postgres-0
, ломается второй. Журналы говорят, что Patroni для дампа всегда создаёт снапшот с одинаковым лейблом. Это путает карты процессам, и они перезапускаются.
Так и быть, будем последовательны.
Запускаю перелив заново. Но уже рабочий день в разгаре, а реплик до сих пор нет, все запросы поступают на мастер. Он начинает тормозить. Следовательно, соединения с сервером забиваются запросами, не успевающими вовремя выполниться. А pg_basebackup
, которым Patroni переливает реплику, подключается на тех же правах, поэтому ждёт своей очереди.
Приложения подключаются через haproxy, и я уменьшаю число возможных подключений там. Это помогает — pg_basebackup
стартует и репликация едет, но приложения ругаются. Увеличиваю количество соединений на haproxy обратно.
Есть время попить кофе, но нет, клиент приносит проблему: всё работает очень-очень медленно. Грубо говоря, единственный рабочий сервер должен обрабатывать x3 запросов, но скорость ниже не в три раза, а на порядок или даже хуже.
Время исследований.
Смотрю журнал БД мастера и вижу большое количество сообщений о долгих запросах COMMIT
. Кто тормозит: запросы из-за нагрузки или транзакции завершаются медленно?
Проверил: запросы выполняются быстро, а вот транзакции фиксируются медленно. Неужели репликация от pg_basebackup
так влияет на работу?
Тут на глаза попадается сообщение, что транзакция была отменена по причине долгой фиксации на реплике. При этом, напомню, у нас асинхронная репликация. Проверяю настройки мастера:
synchronous_commit=on
synchronous_standby_names='*'
Шикарно! Получается, что какое-то время назад на новом мастере была включена в настройках синхронная репликация. Откуда взялись настройки, так и осталось загадкой. Это событие сподвигло нас сделать в мониторинге метрики и алерты, что конфигурация PostgreSQL изменилась.
Просто очищаю список синхронных реплик и скорость работы приложений стремительно вырастает.
Всё хорошо, всё отлично, но пока не работает.
Впереди переключение мастера обратно, и второй раз споткнуться ой как не хочется. Возможно ли, что синхронная репликация, только что портившая жизнь приложениям, поможет нам выжить при switchover?
План родился примерно такой:
Жду перелива postgres-0.
Переливаю postgres-2, чтобы была рабочая реплика для приложений.
На мастере выставляю
synchronous_standby_names='postgres-0'
.Выполняю switchover.
Предполагаю, в этом случае мастер закроет транзакцию и выполнит checkpoint, когда это подтвердит со своей стороны кандидат на переключение.
Финал
Переконфигурированная реплика postgres-0 поднялась и догнала мастера.
Включаю синхронную репликацию:
patronictl edit-config
synchronous_standby_names='postgres-0'
Проверяю её состояние в базе. Ни-че-го.
В журнале Patroni ошибка применения команды. PostgreSQL не любит дефисы (-). Нельзя просто так называть столбцы, таблицы, базы и серверы с дефисом. Чтобы всё сработало, их имена нужно оборачивать в кавычки ("postgres-0"
). Только вот Patroni про это ничего не знает.
Назначаю все реплики синхронными: synchronous_standby_names=’*’
. Запускаю switchover. Всё проходит без сбоев, как и планировалось.
Итоги
Если ваш кластер Patroni имеет недостаточно быструю связанность между узлами, вас может подстерегать та же проблема при switchover/failover. При failover помочь трудно, но вот switchover можно чуть обезопасить:
Перевести реплики в синхронный режим, отключай автоматический
pg_rewind
.Проверить
journalctl -u patroni
, что изменения применились.Удостовериться, что реплики не отстают.
Делать switchover.
P. S.
Читайте также в нашем блоге: