Потоковая репликация — это хорошо зарекомендовавшая себя техника физической репликации в режиме мастер-реплики. Она проста в настройке, стабильна и демонстрирует высокую производительность. Многие удивляются, когда узнают о конфликтах при репликации — ведь реплики работает только в режиме чтения.
В статье описываются конфликты репликации и как с ними бороться.
Что такое конфликт репликации?
Конфликт репликации возникает, когда процесс восстановления (startup) не может применить изменения из WAL, полученных с мастера, так как эти изменения препятствуют продолжению выполнения запросов, уже выполняющихся на реплике. Такие конфликты не могут препятствовать выполнению запросов на мастере, но возникают на реплике, из-за того, что мастер не знает о том, какие запросы выполняются на реплике.
Виды конфликтов:
1) Конфликты моментальных снимков
Это наиболее часто возникающий конфликт.
Могут возникать, если VACUUM (любой процесс, выполняющий вакуумирование: автоматическое или ручное) обрабатывает таблицу и вычищает мёртвые версии строк (dead tuples). Удаление строки логируется, передаётся через WAL и применяется на реплике. Запрос на реплике мог быть запущен раньше, чем VACUUM на мастере (у запроса старый моментальный снимок), поэтому запрос может захотеть видеть версии строк, которые должны удалиться при применении WAL.
2) Конфликты блокировок
Запросы на реплике блокируют таблицы в режиме ACCESS SHARE. Поэтому любая ACCESS EXCLUSIVE блокировка на мастере (блокировка уровня ACCESS EXCLUSIVE конфликтует с ACCESS SHARE) должна быть воспроизведена на реплике, чтобы предотвратить действия с таблицей, несовместимые с запросом. PostgreSQL воспроизводит такую блокировку для команд, конфликтующих с SELECT, например DROP TABLE, TRUNCATE и большинства вариаций команды ALTER TABLE. Если реплика должна воспроизвести блокировку ACCESS EXCLUSIVE на таблице, с которой работает запрос, возникает конфликт блокировок.
3) Конфликты закреплений блока в буфере (BufferPin)
Один из способов уменьшить необходимость в VACUUM — это быстрая очистка блоков (HOT cleanup). Запрос на мастере, который работает с блоком, может получить эксклюзивную блокировку блока и очистить его от мёртвых версий строк. PostgreSQL удерживает блокировки блоков для очистки в течение короткого времени, поэтому конфликта с другими процессами на мастере нет. Есть и другие причины эксклюзивных блокировок блоков, но эта самая распространённая.
Перераспределение записей в индексных блоках также может требовать эксклюзивную блокировку блока индекса. К другим причинам также относится заморозка строк в блоках, выполняемая автовакуумом или командой. В 18 версии вероятность резкого увеличения числа конфликтов закреплений из-за заморозки была уменьшена и для настройки ранней заморозки введён параметр vacuum_max_eager_freeze_failure_rate.
Когда на реплике нужно воспроизвести такую эксклюзивную блокировку блока, а какой-нибудь запрос на реплике уже работает с блоком («закрепил блок»), то возникает конфликт. Блоки могут быть закреплены на более долгое время, например, при последовательном сканировании внешней таблицы в соединении способом Nested Loop.
HOT cleanup может привести и к конфликтам моментальных снимков.
4) Редкие виды конфликтов
Встречаются редко и, обычно, не создают проблем:
-
Взаимоблокировка: запрос на реплике вызывает взаимоблокировку во время использования закреплённого запросом буфера, который нужно изменить при проигрывании WAL. Такой запрос немедленно прерывается.
-
Конфликты репликации табличных пространств: если на реплике табличное пространство указано в параметре конфигурации temp_tablespaces, то запрос на реплике, при нехватке памяти, будет создавать в этом табличном пространстве файлы для хранения временных данных. Если на мастере выполнить команду DROP TABLESPACE, то возникает конфликт на реплике. Запрос, использующий временные файлы немедленно прервётся.
-
Конфликты репликации базы данных: репликация команды DROP DATABASE вызывает конфликт, если на реплике есть активные сессии с удаляемой базой данных. В этом случае, разрываются все сессии с удаляемой базой данных.
Мониторинг конфликтов репликации
В представлении pg_stat_database_conflicts содержится подробная информация обо всех конфликтах репликации, которые возникали с момента последнего сброса статистики. Просматривать это представление нужно на реплике, а не на мастере, так как именно там возникают конфликты.
В этом представлении отображаются не все конфликты репликации, а только те, которые привели к отмене запроса на реплике.
Как реплика разрешает конфликты?
Параметр max_standby_streaming_delay определяет, что происходит, когда воспроизведение WAL-файлов сталкивается с конфликтом репликации (существует аналогичный параметр max_standby_archive_delay, выполняющий ту же функцию для восстановления из архива журналов). PostgreSQL приостанавливает применение WAL до устранения конфликта или на max_standby_streaming_delay. Если запрос не завершился до истечения max_standby_streaming_delay, то PostgreSQL отменяет запрос с сообщением об ошибке типа:
ERROR: canceling statement due to conflict with recovery
DETAIL: User query might have needed to see row versions that must be removed.
В DETAIL указано, что запрос прерван из-за конфликта моментальных снимков.
У параметра max_standby_streaming_delay по умолчанию установлено значение 30 секунд, поэтому запросы имеют 30 секунд для завершения, прежде чем они будут отменены, с момента возникновения конфликта. Это промежуточное значение между значениями 0 (PostgreSQL отменяет запросы немедленно, как только возник конфликт) и -1 (PostgreSQL не отменяет запросы, задержка применения WAL не ограничивается).
Параметр max_standby_archive_delay, по умолчанию, 30 секунд. Применяется при восстановлении из архива журналов. Определяет не максимальнкю длительность запроса на реплике, а максимальное время, за которое должны быть применены изменения из одного WAL-файла (размер 16Мб). Если один запрос привёл к задержке в применении журнальных записей из WAL-файла, то прервутся все запросы, которые мешают применять журнальные записи из этого же файла, независимо от того, сколько времени они работали.
Чтобы понять, как настроить PostgreSQL для разрешения конфликтов, нужно знать для чего используется репликация.
Для чего используются реплики
Высокая доступность
Репликация используется в большинстве случаев обеспечения высокой доступности. В сочетании с программным обеспечением типа Patroni, которое управляет переключением на реплику при сбое мастера, репликация обеспечивает надежную shared-nothing архитектуру отказоустойчивой системы.
Основная цель решений по высокой доступности — минимизировать отставание реплики. В этом случае, продвижение реплики до мастера происходит быстро. Стоит установить минимальное значение для max_standby_streaming_delay.
Если реплика отстаёт в применении WAL, это не влияет на потери данных при переключении, так как передача WAL не зависит от скорости применения, WAL записываются в директорию pg_wal по мере их получения. Однако, реплике потребуется больше времени, чтобы применить полученные WAL, поэтому время на переключение увеличится.
Перенос на реплику долгих запросов
Долгие запросы для составления отчетов или аналитики могут создавать повышенную нагрузку на мастер. Лучшим решением является хранилище данных, специально разработанное для таких запросов. Но, обычно, реплика подходит для недорогих решений вместо выделенных хранилищ данных.
Еще один пример разгрузки мастера — перенос резервирования на реплику. Резервирование реплики не создает нагрузки на мастер.
Главная задача в данном случае — обеспечить бесперебойное выполнение запросов (или создания бэкапов).
В этом случае нужно установить max_standby_streaming_delay в значение, превышающее время выполнения самого длинного запроса и задержки при применении WAL не должны считается проблемой.
Горизонтальное масштабирование
Реплики можно использовать для распределения нагрузки между несколькими машинами. На практике это ограничено несколькими факторами:
-
Все запросы на запись должны отправляться на мастер, поэтому масштабируемой может быть только операция чтения.
-
Приложение должно уметь направлять запросы по одному адресу (реплик), а вносить изменения по другому адресу (мастера).
-
Приложение должно справляться с проблемой согласованности: изменения могут быть не сразу видны запросам (синхронная репликация позволяет этого избежать, но она снижает TPS, так как задержки при фиксации транзакций увеличиваются).
Ещё одна сложность заключается в отсутствии подходящей настройки для параметра max_standby_streaming_delay: низкое значение приведёт к сбоям запросов на реплике, а высокое значение приведёт к тому, что запросы на реплике будут выдавать устаревшие данные.
Как справляться с противоречивыми требованиями
В идеале, реплика должна выполнять только одну функцию и тогда вы сможете устанавливать параметры max_standby_streaming_delay и hot_standby в подходящие значения. Поэтому оптимальный вариант — иметь выделенную реплику для переключения на неё и реплики для обслуживания запросов.
Но не всегда можно позволить себе вторую реплику, или вы можете оказаться в ситуации, подобной описанной выше в сценарии «горизонтального масштабирования». В этом случае, единственный вариант — максимально сократить число конфликтов репликации.
Как избегать конфликтов
Запретить режим горячей реплики (hot_standby), чтобы избежать конфликтов
Конфликтов не может быть, если на реплике нет запросов. Поэтому, если установить hot_standby = off на реплике, о конфликтах можно не беспокоиться.
Хотя это просто и эффективно, это имеет смысл только, если реплика используется исключительно для обеспечения высокой доступности.
Предотвращение конфликтов блокировок
Для предотвращения конфликтов можно не использовать команды, которые требуют блокировок уровня ACCESS EXCLUSIVE. Наиболее часто используемые команды, которые требуют этой блокировки:
-
DROP TABLE -
TRUNCATE -
LOCK -
DROP INDEX -
DROP TRIGGER -
ALTER TABLE
Однако существует один тип блокировок ACCESS EXCLUSIVE: блокировки, возникающие при VACUUM (truncate). Когда VACUUM завершил обработку таблицы и блоки в конце таблицы стали пустыми, система пытается получить на небольшое время блокировку ACCESS EXCLUSIVE. Если это удаётся, VACUUM убирает пустые блоки из таблицы и снимает блокировку. Хотя такие блокировки не влияют на работу мастера, они могут вызывать конфликты на резервном сервере.
Существует два способа избежать VACUUM (truncate):
-
Начиная с версии PostgreSQL 12, вы можете отключить эту фазу для отдельных таблиц с помощью параметра на уровне таблицы:
ALTER TABLE имя SET (vacuum_truncate = off); -
До 17 версии PostgreSQL можно установить значение параметра конфигурации old_snapshot_threshold отличное от -1. Это отключает VACUUM (truncate), что является документированным побочным эффектом параметра.
Начиная с версии PostgreSQL 18 можно установить значение параметра конфигурации vacuum_truncate = off.
Предотвращение конфликтов моментальных снимков
Способ уменьшить подобные конфликты — предотвратить удаление на мастере версии строк, которые могут оставаться видимыми запросам на реплике. В этом помогают два параметра:
-
Установить hot_standby_feedback = on на реплике. Тогда сообщения обратной связи от резервного сервера к основному серверу будут содержать снимок xmin самой старой активной транзакции на резервном сервере, и мастер не будет удалять кортежи, которые эта транзакция все еще может видеть.
Это устранит большинство конфликтов репликации, но длительно выполняющиеся запросы на реплике могут привести к раздуванию таблиц на основном сервере, поэтому этот параметр не включен по умолчанию. Тщательно оцените риски. -
Установите vacuum_defer_cleanup_age на мастере в значение больше 0. В этом случае VACUUM не будет удалять версии строк, которые моложе vacuum_defer_cleanup_age транзакций. Но это менее избирательно, чем hot_standby_feedback и может привести к раздуванию таблиц.
Обратите внимание, что хотя hot_standby_feedback = on устранит большинство конфликтов репликации снимков, он исключит конфликты закрепления блоков в буферах, поскольку блок, используемый на реплике, может содержать очень старые версии строк. Более того, на старых версиях PostgreSQL Лоренц наблюдал конфликты моментальных снимков в базах данных даже при hot_standby_feedback = on, хотя, по исходному коду такого не должно было происходить.
Предотвращение конфликтов закреплений блоков в буферах
Нет надежного способа избежать этих конфликтов. Возможно, можно уменьшить количество HOT cleanup, но это негативно скажется на производительности мастера.
Заключение
Лучший способ избегать конфликтов репликации — использовать выделенные реплики: одну для обеспечения высокой доступности, а другую для обслуживания запросов, перенесённых с мастера для его разгрузки и для резервирования. В этом случае вы сможете легко настроить каждую реплику, чтобы избегать конфликтов репликации.
Если вы не можете позволить себе иметь несколько реплик или хотите использовать реплики для горизонтального масштабирования, вам придется настроить параметр конфигурации hot_standby_feedback, чтобы свести к минимуму число отмененных запросов. Чтобы избежать чрезмерного раздувания таблиц и отставания реплик, установите на мастере параметр max_standby_streaming_delay. Также, отключите vacuum_truncate.
ссылка на оригинал статьи https://habr.com/ru/articles/1027704/