Привет! На связи Евгений Безручкин, DevOps-инженер компании «Флант». И мы продолжаем делиться историями из нашей практики.

В одном проекте у нас есть на обслуживании кластер PostgreSQL на виртуальной машине под управлением Patroni. В ходе работы нам нужно было уменьшить мастер кластера по CPU и памяти. Но на этапе, когда мы меняли мастер c репликой ролями (switchover), возникла проблема: живым остался только мастер, а все реплики остановились. Но мы нашли выход из ситуации, попутно найдя и интересные моменты. В этой статье расскажем, как нам с помощью синхронной репликации удалось сделать так, чтобы switchover прошёл без сбоев. 

Предыстория

Кластер состоял из большого мастера 32с64g и двух реплик. Реплики асинхронные, так как кластер геораспределённый и ждать, когда в другом городе на реплике появятся данные, некогда.

В какой-то момент такой большой мастер стал не нужен. И мы решили его сдуть в целях экономии.

План был таков:

  1. Дождаться низкой нагрузки на кластер.

  2. Сделать switchover — сменить мастер на реплику №1.

  3. Переконфигурировать ВМ с бывшим мастером.

  4. Сделать switchover обратно.

Делаем switchover

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

  1. CHECKPOINT; — сбрасывает на диск изменённые буферы и создаёт контрольную точку, указывающую на консистентное состояние кластера.

  2. 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? 

План родился примерно такой:

  1. Жду перелива postgres-0.

  2. Переливаю postgres-2, чтобы была рабочая реплика для приложений.

  3. На мастере выставляю synchronous_standby_names='postgres-0'.

  4. Выполняю 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 можно чуть обезопасить:

  1. Перевести реплики в синхронный режим, отключай автоматический pg_rewind.

  2. Проверить journalctl -u patroni, что изменения применились.

  3. Удостовериться, что реплики не отстают.

  4. Делать switchover.

P. S.

Читайте также в нашем блоге:

Комментарии (1)


  1. shurutov
    12.09.2024 06:14

    1. ещё раз убедился в том, что реплики надо наливать из бекапов, а не с рабочих хостов (patroni в такое умеет);

    2. ручное конфигурирование наличия синхронной реплики? Благородные доны не в курсе, что у patroni есть специальный параметр, отвечающий за наличие синхронной реплики? (извините, но почему-то на хабре не работает Ctrl+V/Shift+Ins, а набирать ручками - то ещё приключение)