MAATRIX / Блог / Реплика отстала на сутки, а мониторинг молчал

Реплика отстала на сутки, а мониторинг молчал

MAATRIX

Мониторинг весь месяц показывал зелёный статус: реплика в списке процессов, порт слушает, подключение проходит. А когда до этого дошли руки, оказалось, что данные на ней отстают от мастера почти на сутки — приложение, которое читало отчёты именно с реплики, всё это время показывало вчерашние цифры как актуальные. Разберём, почему «реплика жива» и «реплика актуальна» — это два разных факта, и как настроить мониторинг так, чтобы такое не повторилось.

Что такое отставание реплики и откуда оно берётся

Репликация базы данных — это непрерывный поток изменений с основного сервера (мастера) на копию (реплику): в PostgreSQL это WAL, в MySQL — бинарный лог. Реплика получает этот поток и применяет его к своим данным, оставаясь почти точной копией мастера. «Почти» — ключевое слово: между записью на мастере и применением той же записи на реплике всегда проходит какое-то время. Это время и называют отставанием, или repl lag.

В нормальном режиме лаг измеряется в миллисекундах и на практике незаметен. Но он способен вырасти на порядки, и происходит это не мгновенно — реплика не «падает», она просто перестаёт успевать. Частые причины:

  • Долгая транзакция на реплике. Пока на реплике выполняется тяжёлый аналитический запрос в рамках открытой транзакции, применение новых изменений может ждать (особенно заметно при hot_standby_feedback в PostgreSQL) или прерываться конфликтами.
  • Однопоточное применение изменений. Классическая репликация MySQL долгое время применяла события из бинлога одним потоком — если на мастере параллельно шли десятки транзакций, реплика физически не успевала применить их последовательно одна за другой.
  • Сеть между мастером и репликой. Даже кратковременные потери пакетов или возросшая задержка между дата-центрами (особенно если мастер и реплика в разных локациях) увеличивают время доставки журнала.
  • Диск на реплике не успевает. Запись применённых изменений на диск — это тоже I/O, и если диск реплики медленнее диска мастера или на нём параллельно идёт бэкап, применение тормозит.
  • Реплика недогружена по CPU/RAM относительно мастера — частая ситуация, когда реплику изначально сажали «на что было» под read-only нагрузку, а не под полноценную копию боевого сервера.

Важно: ни одна из этих причин не обязательно роняет процесс репликации. Соединение остаётся установленным, реплика отвечает на пинг, pg_stat_replication или SHOW REPLICA STATUS показывают state: streaming — просто дистанция между тем, что применено, и тем, что реально записано на мастере, растёт и растёт.

Как отставание накапливается тихо, сутками

Опасность именно в постепенности. Отставание в 5 секунд никто не заметит. Отставание в 5 минут иногда спишут на нагрузку. А дальше начинается зона, где на цифру вообще не смотрят — мониторят факт подключения, а не величину лага.

Типичный сценарий: в выходные на мастере запускают миграцию с массовым UPDATE на десятки миллионов строк. Реплика начинает отставать — сначала на минуты, потом на часы, потому что применение такого объёма записи на диске реплики идёт медленнее, чем шла сама миграция на более быстром хранилище мастера. Мониторинг видит процесс postgres в списке, видит открытый порт, ставит зелёную галочку. Отставание растёт всю ночь и часть следующего дня, пока кто-то из команды случайно не запросит с реплики свежие данные и не увидит, что их там нет — счёт к этому моменту уже идёт на часы или сутки, потому что автоматического сигнала именно на величину лага не было.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Чем опасны устаревшие данные для тех, кто читает с реплики

Если приложение направляет часть чтения на реплику — а это стандартная практика для разгрузки мастера — отставание напрямую превращается в устаревшие данные для пользователя. Логика простая: всё, что вы читаете с реплики, отражает состояние базы на момент времени «сейчас минус лаг», а не текущее состояние.

Пока лаг — доли секунды, разница не ощущается. Но по мере роста отставания появляются реальные проблемы:

  • Пользователь не видит своё же изменение. Классический сценарий read-after-write: человек обновил профиль или оформил заказ (запись ушла на мастер), тут же обновил страницу — а чтение ушло на отставшую реплику и вернуло старые данные. Выглядит как баг, хотя технически всё сработало штатно.
  • Отчёты и дашборды показывают неактуальную картину. Если аналитика или биллинг читают с реплики с лагом в часы, решения принимаются на основе данных, которые уже не отражают реальность — остатки на складе, статус заказа, баланс счёта.
  • Гонки в бизнес-логике. Проверка «есть ли ещё места» или «не превышен ли лимит» по данным с отстающей реплики может пропустить операцию, которая на самом деле уже должна была быть отклонена — потому что реплика ещё не знает о более раннем изменении на мастере.
  • Каскад по системам-потребителям. Если с реплики читает не только веб-приложение, а ещё и внешние интеграции, кеш-прогрев, поисковый индекс — устаревшие данные расползаются дальше по всей цепочке, и найти первопричину задним числом сложнее.

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

Когда реплика перестаёт быть резервом на случай аварии

Отдельная, более тяжёлая история — использование реплики как плана Б на случай отказа основного сервера. Здесь отставание превращается не в неудобство, а в прямую потерю данных.

Если мастер недоступен и вы повышаете (promote) реплику до нового мастера, всё, что было записано на старом мастере после момента, до которого реплика успела дойти, безвозвратно теряется. Формально это называют RPO — Recovery Point Objective, допустимый объём потерянных данных при аварии. При лаге в доли секунды RPO тоже доли секунды, и для большинства проектов это приемлемо. При лаге в час или сутки вы теряете час или сутки транзакций — заказы, платежи, изменения, о которых реплика просто не успела узнать. Хуже, если авария случается именно в момент, когда лаг и так уже аномально большой (например, из-за той самой ночной миграции) — тогда переключение на «резерв» на деле означает откат бизнеса на сутки назад, а не отказоустойчивость почти без потерь, на которую рассчитывали.

Именно поэтому для сценариев с RPO, близким к нулю (платежи, финансовый учёт), используют не асинхронную, а синхронную репликацию — мастер подтверждает транзакцию клиенту только после того, как реплика (или кворум реплик) её тоже подтвердила. Это дороже по задержке записи, зато исключает саму возможность «резерв отстал и потерял данные». Если у вас асинхронная схема (а это подавляющее большинство инсталляций — она проще и быстрее на запись), контроль лага — единственный способ понимать, чем вы рискуете в моменте.

Почему проверка «реплика жива» не ловит эту проблему

Здесь и кроется главная ловушка. Стандартный health-check для реплики почти всегда устроен так: проверяется, что процесс СУБД запущен, порт открыт, подключение проходит, статус репликации — streaming (PostgreSQL) или Replica_IO_Running: Yes / Replica_SQL_Running: Yes (MySQL). Всё это — сигналы о том, что репликация технически функционирует как процесс. Ни один из них не говорит о том, насколько применённые данные отстают от мастера по времени.

Реплика с лагом в сутки формально совершенно «жива» по всем этим критериям: соединение установлено, оба потока (I/O и SQL, в терминах MySQL) работают, ошибок в логе нет. Именно поэтому мониторинг вида «жив/не жив» — это проверка правильного типа объекта не по тому свойству. Он ловит разрыв репликации (реплика отвалилась, поток остановился, авторизация не прошла), но не ловит замедление применения — а именно замедление, а не разрыв, чаще всего и приводит к тем самым тихим суткам отставания.

Это тот же класс ошибки мониторинга, что и в истории с диском в RAID, который сыпался месяц, а мониторинг молчал — там тоже проверяли бинарный статус «массив в порядке / деградирован», а не деградацию, которая была видна заранее по конкретным счётчикам. С репликацией логика та же: нужна метрика величины, а не факт присутствия.

Как мониторить отставание правильно: метрики, пороги, алерты

Правильный мониторинг репликации строится на двух уровнях сразу: статус процесса (реплика подключена, потоки работают) и отдельно — количественная метрика лага с порогом и алертом. Ниже — рабочие способы получить эту метрику для двух самых частых СУБД.

PostgreSQL. На стороне реплики отставание по времени можно получить напрямую:

SELECT now() - pg_last_xact_replay_timestamp() AS lag;

Это разница между текущим временем и временем последней применённой транзакции — интуитивно понятная метрика именно в секундах отставания, а не в байтах WAL. На стороне мастера можно посмотреть отставание в терминах позиций журнала и — начиная с PostgreSQL 10 — также напрямую по времени через pg_stat_replication:

SELECT client_addr, state, sent_lsn, replay_lsn, replay_lag
FROM pg_stat_replication;

Колонка replay_lag — это уже готовый interval, отражающий реальную задержку применения, а не просто объём недоставленных данных.

MySQL / MariaDB. Классический способ — посмотреть вывод статуса реплики:

SHOW REPLICA STATUS\G

(в MySQL до версии 8.0.22 и в актуальных версиях MariaDB команда называется SHOW SLAVE STATUS). Нужное поле — Seconds_Behind_Source (в новых версиях MySQL) или Seconds_Behind_Master — это оценка отставания в секундах, посчитанная сервером. У этой оценки есть нюанс, который стоит держать в голове: она основана на метке времени последнего обработанного события и может давать неточные значения при простоях мастера или при использовании GTID с параллельным применением — для критичных случаев её стоит дополнительно сверять, например, сравнивая позиции GTID на мастере и реплике.

Экспорт в систему мониторинга. Вручную гонять эти запросы бессмысленно — метрику нужно снимать регулярно и складывать в систему, которая умеет считать пороги и слать алерты. Для связки Prometheus + Grafana это postgres_exporter (метрика pg_replication_lag_seconds) или mysqld_exporter (метрика mysql_slave_status_seconds_behind_master) — если стек ещё не поднят, порядок установки описан в статье про Prometheus и Grafana на VPS, а про то, какие метрики баз данных вообще стоит выводить на дашборд, — в статье про мониторинг баз данных через Grafana.

Пример правила алерта для Prometheus (alert.rules.yml):

groups:
  - name: replication
    rules:
      - alert: ReplicationLagWarning
        expr: pg_replication_lag_seconds > 30
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "Отставание реплики {{ $labels.instance }} больше 30 секунд"

      - alert: ReplicationLagCritical
        expr: pg_replication_lag_seconds > 300
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Отставание реплики {{ $labels.instance }} больше 5 минут — проверить немедленно"

Конкретные пороги (30 секунд и 5 минут в примере выше) — это ориентир, а не универсальное правило: для дашборда с аналитикой сутки могут быть избыточно строгим порогом, а для реплики, которая параллельно служит горячим резервом с низким RPO, критичным может быть уже отставание в 10–15 секунд. Порог должен исходить из того, для чего конкретно используется эта реплика — чтение пользователями, аналитика или аварийный резерв, — а не браться «как у всех».

Если вместо Prometheus у вас Zabbix, тот же принцип: заводится отдельный item, который опрашивает pg_last_xact_replay_timestamp() или Seconds_Behind_Master через внешний скрипт или UserParameter, и на него вешается триггер с двумя уровнями — warning и critical — а не общий health-check шаблона PostgreSQL/MySQL «из коробки», который чаще всего проверяет только доступность.

Разбор случая: почему отставание в сутки не заметили раньше

Возвращаясь к сценарию из начала статьи — стоит разобрать, что именно пошло не так, потому что комбинация обычно одна и та же на разных проектах:

  1. Мониторинг настраивали один раз при установке репликации и не пересматривали — проверялся факт, что репликация «работает», а не то, что с ней происходит под нагрузкой месяцы спустя.
  2. Алерт был привязан к статусу процесса, а не к метрике. Дашборд мог показывать график лага, но без порогового алерта на него смотрят только тогда, когда уже что-то заподозрили.
  3. Никто заранее не задавал вопрос «а что если лаг вырастет». Репликацию настраивают ради отказоустойчивости или разгрузки чтения, но не продумывают, какой лаг критичен именно для этого использования — и пороги алертов либо не ставят вовсе, либо ставят «на глаз».
  4. Нагрузка, вызвавшая отставание, была разовой и неучтённой — миграция, массовая выгрузка, восстановление из бэкапа на той же реплике. Такие события стоит сопровождать явным контролем лага, а не полагаться на мониторинг, который для этого не настроен.

Это частный случай более общей проблемы — мониторинга, который никто не смотрит: метрика может собираться и даже красиво отображаться на графике, но без порога и алерта она бесполезна в момент, когда решение нужно принимать быстро, а не после того, как кто-то случайно заметил неладное.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Какой лаг репликации считать нормальным?

Универсального числа нет. Для большинства веб-приложений с чтением с реплики приемлемы единицы секунд; для резерва на случай аварии с низким RPO — цель обычно в пределах десятков секунд или меньше. Ориентируйтесь на то, что произойдёт, если реплику придётся использовать прямо сейчас с текущим лагом.

Асинхронная репликация — это всегда риск потери данных?

Да: при отказе мастера теряются транзакции, не успевшие дойти до реплики за время лага. При лаге в доли секунды это обычно приемлемый риск; там, где потеря даже секунды транзакций критична, нужна синхронная репликация или её аналоги, а не просто более частый мониторинг.

Можно ли мониторить только лаг и отказаться от проверки статуса подключения?

Нет, это два разных отказа с разными симптомами и должны быть разными алертами. Если реплика полностью отключится, метрика лага может просто перестать обновляться — без отдельного алерта «нет свежих данных от экспортёра» это тоже легко пропустить.

Что делать, если лаг регулярно растёт по ночам из-за бэкапов?

Снимайте бэкапы с отдельной третьей реплики, а не с той, что обслуживает read-нагрузку или служит резервом, либо временно повышайте пороги алерта на известное окно обслуживания — и обязательно возвращайте их обратно автоматически.

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

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

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →