Як ми використали відкладену реплікацію для аварійного відновлення з PostgreSQL

Як ми використали відкладену реплікацію для аварійного відновлення з PostgreSQL
Реплікація – не бекап. Чи ні? Ось як ми використали відкладену реплікацію для відновлення, випадково вилучивши ярлики.

Фахівці з інфраструктури на 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). А ще відстежуємо зміни стану бази даних, архівуючи журнал попереджувального запису (журнал попереднього запису, WAL). І з цим ми можемо виконати PITR для аварійного відновлення: починаємо зі знімка, зробленого до помилки, і застосовуємо зміни з архіву WAL аж до збою.

Що таке відкладена реплікація?

Відкладена реплікація – це застосування змін із затримкою WAL. Тобто транзакція сталася на годину X, але у репліці вона з'явиться із затримкою d в час X + d.

У PostgreSQL є два способи налаштувати фізичну репліку бази даних: відновлення з архіву та стримінгова реплікація. Відновлення з архіву, по суті, працює як 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 ми зараз захищаємо від втрати даних лише на рівні системи та не відновлюємо дані на рівні користувача.

Джерело: habr.com

Додати коментар або відгук