Как мы использовали отложенную репликацию для аварийного восстановления с PostgreSQL +25



Репликация — не бэкап. Или нет? Вот как мы использовали отложенную репликацию для восстановления, случайно удалив ярлыки.


Специалисты по инфраструктуре на GitLab отвечают за работу GitLab.com — самого большого экземпляра GitLab в природе. Здесь 3 миллиона пользователей и почти 7 миллионов проектов, и это один из самых крупных опенсорс-сайтов SaaS с выделенной архитектурой. Без системы баз данных PostgreSQL инфраструктура GitLab.com далеко не уедет, и что мы только не делаем для отказоустойчивости на случаи любых сбоев, когда можно потерять данные. Вряд ли такая катастрофа случится, но мы хорошо подготовились и запаслись разными механизмами бэкапа и репликации.


Репликация — это вам не средство бэкапа баз данных (см. ниже). Но сейчас мы увидим, как быстро восстановить случайно удаленные данные с помощью отложенной репликации: на GitLab.com пользователь удалил ярлык для проекта gitlab-ce и потерял связи с мерж-реквестами и задачами.


С отложенной репликой мы восстановили данные всего за 1,5 часа. Смотрите, как это было.


Восстановление на момент времени с PostgreSQL


У PostgreSQL есть встроенная функция, которая восстанавливает состояние базы данных на определенный момент времени. Она называется Point-in-Time Recovery (PITR) и использует те же механизмы, которые поддерживают актуальность реплики: начиная с достоверного снимка всего кластера базы данных (базовый бэкап), мы применяем ряд изменений состояния до определенного момента времени.


Чтобы использовать эту функцию для холодного бэкапа, мы регулярно делаем базовый бэкап базы данных и храним его в архиве (архивы GitLab живут в облачном хранилище Google). А еще отслеживаем изменения состояния базы данных, архивируя журнал упреждающей записи (write-ahead log, WAL). И со всем этим мы можем выполнить PITR для аварийного восстановления: начинаем со снимка, сделанного до ошибки, и применяем изменения из архива WAL вплоть до сбоя.


Что такое отложенная репликация?


Отложенная репликация — это применение изменений из WAL с задержкой. То есть транзакция произошла в час X, но в реплике она появится с задержкой d в час X + d.


В PostgreSQL есть 2 способа настроить физическую реплику базы данных: восстановление из архива и стриминговая репликация. Восстановление из архива, по сути, работает, как PITR, но непрерывно: мы постоянно извлекаем изменения из архива WAL и применяем их к реплике. А стриминговая репликация напрямую извлекает поток WAL из вышестоящего хоста базы данных. Мы предпочитаем восстановление из архива — им проще управлять и у него нормальная производительность, которая не отстает от рабочего кластера.


Как настроить отложенное восстановление из архива


Параметры восстановления описаны в файле recovery.conf. Пример:


standby_mode = 'on'
restore_command = '/usr/bin/envdir /etc/wal-e.d/env /opt/wal-e/bin/wal-e wal-fetch -p 4 "%f" "%p"'
recovery_min_apply_delay = '8h'
recovery_target_timeline = 'latest'

С этими параметрами мы настроили отложенную реплику с восстановлением из архива. Тут используется wal-e для извлечения сегментов WAL (restore_command) из архива, а изменения будут применяться через восемь часов (recovery_min_apply_delay). Реплика будет следить за изменениями временной шкалы в архиве, например, из-за отработки отказа в кластере (recovery_target_timeline).


С recovery_min_apply_delay можно настроить стриминговую репликацию с задержкой, но здесь есть пара подвохов, которые связаны со слотами репликации, обратной связью горячего резерва и проч. Архив WAL позволяет их избежать.


Параметр recovery_min_apply_delay появился только в PostgreSQL 9.3. В предыдущих версиях для отложенной репликации нужно настроить комбинацию функций управления восстановлением (pg_xlog_replay_pause(), pg_xlog_replay_resume()) или удерживать сегменты WAL в архиве на время задержки.


Как PostgreSQL это делает?


Любопытно посмотреть, как PostgreSQL реализует отложенное восстановление. Посмотрим на recoveryApplyDelay(XlogReaderState). Он вызывается из главного цикла повтора для каждой записи из WAL.


static bool
recoveryApplyDelay(XLogReaderState *record)
{
    uint8       xact_info;
    TimestampTz xtime;
    long        secs;
    int         microsecs;

    /* nothing to do if no delay configured */
    if (recovery_min_apply_delay <= 0)
        return false;

    /* no delay is applied on a database not yet consistent */
    if (!reachedConsistency)
        return false;

    /*
     * Is it a COMMIT record?
     *
     * We deliberately choose not to delay aborts since they have no effect on
     * MVCC. We already allow replay of records that don't have a timestamp,
     * so there is already opportunity for issues caused by early conflicts on
     * standbys.
     */
    if (XLogRecGetRmid(record) != RM_XACT_ID)
        return false;

    xact_info = XLogRecGetInfo(record) & XLOG_XACT_OPMASK;

    if (xact_info != XLOG_XACT_COMMIT &&
        xact_info != XLOG_XACT_COMMIT_PREPARED)
        return false;

    if (!getRecordTimestamp(record, &xtime))
        return false;

    recoveryDelayUntilTime =
        TimestampTzPlusMilliseconds(xtime, recovery_min_apply_delay);

    /*
     * Exit without arming the latch if it's already past time to apply this
     * record
     */
    TimestampDifference(GetCurrentTimestamp(), recoveryDelayUntilTime,
                        &secs, &microsecs);
    if (secs <= 0 && microsecs <= 0)
        return false;

    while (true)
    {
        // Shortened:
        // Use WaitLatch until we reached recoveryDelayUntilTime
        // and then
        break;
    }
    return true;
}

Суть в том, что задержка основана на физическом времени, записанном в метке времени коммита транзакции (xtime). Как видно, задержка применяется только к коммитам и не трогает другие записи — все изменения применяются напрямую, а коммит откладывается, так что мы увидим изменения только после настроенной задержки.


Как использовать отложенную реплику для восстановления данных


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


Когда мы узнали о проблеме, мы приостановили восстановление из архива для отложенной реплики:


SELECT pg_xlog_replay_pause();

С паузой у нас не было риска, что реплика повторит запрос DELETE. Полезная штука, если нужно время во всем разобраться.


Суть в том, что отложенная реплика должна дойти до момента перед запросом DELETE. Мы примерно знали физическое время удаления. Мы удалили recovery_min_apply_delay и добавили recovery_target_time в recovery.conf. Так реплика доходит до нужного момента без задержек:


recovery_target_time = '2018-10-12 09:25:00+00'

С метками времени лучше убавить лишнего, чтобы не промахнуться. Правда, чем больше убавка, тем больше данных теряем. Опять же, если проскочим запрос DELETE, все опять удалится и придется начинать заново (или вообще брать холодный бэкап для PITR).


Мы перезапустили отложенный экземпляр Postgres, и сегменты WAL повторялись до указанного времени. Отследить прогресс на этом этапе можно запросом:


SELECT
  -- current location in WAL
  pg_last_xlog_replay_location(),
  -- current transaction timestamp (state of the replica)
  pg_last_xact_replay_timestamp(),
  -- current physical time
  now(),
  -- the amount of time still to be applied until recovery_target_time has been reached
  '2018-10-12 09:25:00+00'::timestamptz - pg_last_xact_replay_timestamp() as delay;

Если метка времени больше не меняется, восстановление завершено. Можно настроить действие recovery_target_action, чтобы закрыть, продвинуть или приостановить экземпляр после повтора (по умолчанию он приостанавливается).


База данных пришла в состояние до того злосчастного запроса. Теперь можно, например, экспортировать данные. Мы экспортировали удаленные данные о ярлыке и все связи с задачами и мерж-реквестами и перенесли их в рабочую базу данных. Если потери масштабные, можно просто продвинуть реплику и использовать ее как основную. Но тогда потеряются все изменения после момента, до которого мы восстановились.


Вместо меток времени лучше использовать ID транзакций. Полезно записывать эти ID, например, для операторов DDL (типа DROP TABLE), с помощью log_statements = 'ddl'. Будь у нас ID транзакции, мы бы взяли recovery_target_xid и прогнали все вплоть до транзакции перед запросом DELETE.


Вернуться к работе очень просто: уберите все изменения из recovery.conf и перезапустите Postgres. Скоро в реплике снова появится восьмичасовая задержка, и мы готовы к будущим неприятностям.


Преимущества для восстановления


С отложенной репликой вместо холодного бэкапа не приходится часами восстанавливать весь снимок из архива. Нам, например, нужно пять часов, чтобы достать весь базовый бэкап на 2 ТБ. А потом еще придется применить весь суточный WAL, чтобы восстановиться до нужного состояния (в худшем случае).


Отложенная реплика лучше холодного бэкапа по двум пунктам:


  1. Не нужно доставать весь базовый бэкап из архива.
  2. Есть фиксированное восьмичасовое окно сегментов WAL, которые нужно повторить.

А еще мы постоянно проверяем, можно ли сделать PITR из WAL, и мы бы быстро заметили повреждения или другие проблемы с архивом WAL, следя за отставанием отложенной реплики.


В этом примере у нас ушло 50 минут на восстановление, то есть скорость была 110 ГБ данных WAL в час (архив тогда все еще был на AWS S3). Всего мы решили проблему и восстановили данные за 1,5 часа.


Итоги: где пригодится отложенная реплика (а где нет)


Используйте отложенную репликацию как средство первой помощи, если случайно потеряли данные и заметили эту беду в пределах настроенной задержки.


Но учтите: репликация — не бэкап.

У бэкапа и репликации разные цели. Холодный бэкап пригодится, если вы случайно сделали DELETE или DROP TABLE. Мы делаем бэкап из холодного хранилища и восстанавливаем предыдущее состояние таблицы или всей базы данных. Но при этом запрос DROP TABLE почти моментально воспроизводится во всех репликах на рабочем кластере, поэтому обычная репликация тут не спасет. Сама по себе репликация поддерживает базу данных доступной, когда сдают отдельные серверы, и распределяет нагрузку.


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


Примечание. На GitLab.com мы сейчас защищаем от потери данных только на уровне системы и не восстанавливаем данные на уровне пользователя.




К сожалению, не доступен сервер mySQL