Падение любой информационной системы — это по умолчанию больно и неприятно. На Хабре вы найдете много статей о том, как этого избежать. Но что делать, если все-таки случилась одна из тех историй, которыми пугают джунов? Уборщица разлила ведро воды в ЦОДе или злосчастный экскаватор перерубил оптоволокно?

Меня зовут Андрей Белый, я старший разработчик VK Cloud в команде DBaaS. В этой статье мы на примере PostgreSQL разберем принципы работы баз данных и поговорим о том, как минимизировать последствия инцидентов с помощью оптимизации RPO (Recovery point objective).

Материал подготовлен по мотивам моего выступления на VK Databases Meetup «Point-in-time Recovery. Как уменьшить RPO для базы данных»

История жизни одной БД


Один из основных способов защиты баз данных на случай аварий или сбоев — создание резервных копий (бэкапов). Но для каждой стратегии резервного копирования характерна периодичность и интервальность: с точки зрения нагрузок на БД и загруженности памяти для хранения бэкапов нерационально делать копии часто. Это классическая практика, но у нее есть существенный недостаток: в случае аварии любого характера часть новых данных, которая не попала в последний бэкап, неизбежно теряется. Допустимый объем потери определяет параметр RPO (Recovery point objective). 



Любая авария требует времени на восстановление штатной работы БД. Это значение определяет параметр RTO (Recovery time objective).



В результате даунтайм системы и объем потерянных данных фактически зависит от двух величин — RPO и RTO.

Варианты оптимизации RPO


Есть два очевидных варианта уменьшения RPO:

  • увеличение частоты создания резервных копий;
  • репликация БД.

На практике оба варианта не идеальны:

  • Резервные копии не «бесплатные»: они влияют на производительность дисковой и сетевой подсистем, процессорное время и другие параметры. Исключить это влияние невозможно, поскольку бэкап надо делать на работающей БД, иначе не получится соблюсти консистентность. 
  • Репликация — средство обеспечения высокой доступности и отказоустойчивости. Но она не исключает необходимость создания резервных копий. Авария может быть не только с точки зрения выхода из строя железа, но и какой-то неаккуратный запрос, которой прилетит и на реплику.

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

Алгоритм записи в БД


Примечание: алгоритм записи будем разбирать на примере PostgreSQL, но другие базы данных имеют такие же концепции. 

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



С точки зрения операционной системы, запущенный PostgreSQL — набор процессов. Часть из них служебные, часть обслуживают пользовательские подключения — при создании нового подключения к БД автоматически запускается новый процесс.



Чтобы все процессы могли общаться между собой, используется разделяемая память (shared memory) — общее адресное пространство, к которому есть доступ у всех процессов. В этом участке памяти PostgreSQL хранит самую большую свою структуру — разделяемые буферы (shared buffers).

Алгоритм работы можно отследить по выполнению простого запроса Select к базе:

  • запрос попадает в PostgreSQL;
  • он проверяется и оптимизируется;
  • PostgreSQL идет на диск и поднимает страницу, в которой содержится нужная запись;
  • PostgreSQL кладет запись в разделяемые буферы, которые выступают в качестве кэша.



Соответственно, если потребуется что-то обновить внутри страницы, PostgreSQL сначала сделает это в памяти.



Но все изменения важно синхронизировать с диском, ведь PostgreSQL — ACID-совместимая база, которая должна гарантировать консистентность данных и изменений.

Самый очевидный способ синхронизации — запись всех изменений сразу в табличный файл на диск. Но у него есть недостаток: поскольку изменено может быть много строк, страниц и файлов, порядок записи на диск будет случайным, что не оптимально с точки зрения дисковой подсистемы.

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



Поскольку запись последовательная, на нее надо меньше ресурсов. При этом сразу после записи в файл можно подтверждать коммит — ничего не потеряется. Именно таким файлом является WAL (Write-Ahead Log, журнал предзаписи). 

Что такое WAL




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

Физически WAL представляет собой выделенный объем памяти, который поделен на файлы стандартного размера — 16 Мб. Как только старый файл заканчивается, PostgreSQL создает новый и начинает писать в него. Файл содержит структуры, которые указывают, где и что было изменено. Каждая такая запись имеет уникальный номер — LSN (Log Sequence Number), — который позволяет найти любое изменение. Таким образом, WAL-файл в Postgres также участвует в репликации. 

Чтобы увидеть содержание WAL и найти открытый WAL-файл, достаточно сделать мутирующий запрос и выполнить специальную команду PostgreSQL:



После этого с помощью встроенной утилиты pg_waldump можно прочитать файл. 



Кроме LSN в WAL-файле есть подтверждение вставки  и коммита. При этом каждая запись ссылается на предыдущий LSN — получается непрерывная и последовательная цепочка записей. Таким образом можно отследить отдельно примененные в памяти и записанные на диск изменения. Но в табличном файле они не синхронизированы — в нем пока хранятся старые данные. Нужен процесс синхронизации. 

Синхронизация


В PostgreSQL и других БД синхронизация подразумевает создание checkpoint (контрольных точек). Алгоритм такой:

  • в табличных файлах обновляются данные;
  • создается специальная запись checkpoint в WAL, которая содержит указания на последние изменения, которые входят в этот checkpoint;
  • PostgreSQL обновляет свою внутреннюю структуру, в которую сохраняет LSN на последнюю успешный checkpoint. 



Чтобы увидеть содержание записи в WAL-файле, достаточно сделать мутирующий запрос и вручную вызвать checkpoint. Обычно он делается периодически, фоновым процессом, но его можно вызвать и принудительно.



Внутри WAL-файла можно увидеть примерно следующее:



Самое интересное здесь — последние записи. Они указывают, что был факт создания checkpoint, и имеют указатель Redo на LSN, который был.   

Примечание: В реально работающей системе между этими записями может быть много других, потому что PostgreSQL не замирает в момент создания checkpoint. 

Восстановление после сбоя


На случай, если изменения зафиксированы в памяти и WAL-файле, но PostgreSQL аварийно выключается и checkpoint не успевает отработать, есть операция для восстановления после сбоя. При старте Postgresql через структуру pg_control определяет что был завершен некорректно и LSN который указывает на последний checkpoint и начинает искать его в WAL. После этого:

  • Все, что идет до checkpoint, — уже синхронизировано и записано, поэтому с этими записями PostgreSQL ничего не делает.
  • А все, что случилось после checkpoint, — принудительно синхронизируется.



Как мы все это можем применить?


Итого мы разобрали два механизма применения WAL:

  • Первый позволяет полностью хранить цепочку изменений в базе данных.
  • Второй позволяет проигрывать всю цепочку в случае сбоев, чтобы приводить базу в консистентное состояние. 

Их наличие позволяет заменить постоянное полное резервное копирование базы данных отправкой только WAL-файлов — например, при первой установке БД делать полную резервную копию, а потом отсылать WAL-файлы с информацией обо всех изменениях.



Но ограничиться только отправкой WAL-файлов нельзя. Причин две:

  • WAL — это цепочка. Если теряется хотя бы одно звено, будут доступны данные только до места разрыва, а остальное потеряется.
  • На восстановление из WAL нужны ресурсы. Это сложнее, чем просто скачать бэкап из внешнего хранилища и положить его на диск.

Поэтому отправку WAL-файлов оптимально сочетать с периодическим созданием полных резервных копий. 



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



Как реализовать в PostgreSQL 




Для использования всего описанного алгоритма в PostgreSQL «из коробки» есть режим архивации WAL. Он настраивается двумя опциями:

  • Первая отвечает за включение режима архивации.
  • Вторая говорит PostgreSQL, какую команду нужно запустить в момент, когда будет сделан switch WAL-файла. То есть когда старый файл будет заполнен и закрыт.

После этого с помощью переменных можно поставить абсолютный или относительный путь к созданному WAL-файлу.

Но вариант с архивированием WAL-файлов только после их заполнения подходит не всегда. Есть сценарии, когда важные данные в БД изменяются редко, и на заполнение WAL-файла и его сохранение может уйти много времени. В таком случае все операции, внесенные в WAL-файл, но не архивированные, могут быть потенциально утеряны в момент сбоя. 

Чтобы исключить такие риски, в PostgreSQL есть опция archive_timeout, которая позволяет принудительно делать pg_switchwal, то есть архивировать WAL-файл, если за определенный период времени в нем были изменения. Это кратно снижает риски.

Алгоритм восстановления тоже реализован просто.



В PostgreSQL есть опция restore_command, которая позволяет указать, с помощью какой команды Postgresql будет забирать нужный WAL-файл. Поскольку WAL-файл это непрерывная цепочка, то Postgresql знает какой следующий WAL файл ему понадобится

Также есть группа опций recovery_target. Одни из основных сценариев их использования — возможность откатиться к конкретному времени или к именованной точке, которая присваивается каждой конкретной транзакции. Например, если известно, что TRUNCATE TABLE или DROP DATABASE случился в 15:02, то можно откатиться к 15:01 и продолжить работу с минимальным ущербом для БД и всей системы. 

Благодаря этому такая стратегия резервного копирования и восстановления получила название Point-in-time Recovery, то есть восстановление на произвольный момент времени. Важно, что все этапы Point-in-time Recovery — снятие полных бэкапов по расписанию, отсылку WAL-файлов и другие — в PostgreSQL можно автоматизировать. Для этого можно использовать:

  • самописные скрипты;
  • индустриальные инструменты — например, open-source-решения WAL-G, pgBackRest, pg_probackup.

Примечание: Point-in-time Recovery в других БД реализован иначе. Так, в MySQL/MariaDB:

  • используется Binlog;
  • можно делать архивацию по cron;
  • для восстановления используется mysqlbinlog/mariadb-binlog;
  • можно автоматизировать с помощью WAL-G.


В MongoDB:

  • используется OPLog (operational log);
  • для автоматизации доступны WAL-G (beta) и Percona Backup for MongoDB.

Как мы сделали в VK Cloud


В облаке VK Cloud функциональность Point-in-time Recovery уже доступна для СУБД PostgreSQL, реализована возможность восстановления на точку во времени. Такой сценарий работы покрывает 99,9 % случаев. 

«Под капотом» функциональность реализована на WAL-G.

Кроме стандартных функций мы также даем пользователю возможность самостоятельно настроить окно доступных резервных копий: можно указать периодичность создания и количество резервных копий и бэкапов, которые нужно хранить. Благодаря этому каждый может откатиться до любой точки в большом промежутке времени.

Итоги


  • Полностью исключить сбои в БД не получится. И даже незначительный отказ на практике может привести к большим проблемам, особенно если данные в БД критически важны и их потеря недопустима.
  • Создание полных резервных копий — отличный способ повышения надежности хранения данных, но он не панацея, ведь полные бэкапы нерационально делать часто из-за большого потребления ресурсов.
  • Оптимальный способ снижения допустимого объема потерь данных — комбинировать создание полных резервных копий и WAL-файлов с историей всех изменений. Это снижает риски и позволяет реализовать стратегию Point-in-time Recovery.
  • Все этапы Point-in-time Recovery можно автоматизировать с помощью самописных скриптов или индустриальных инструментов. В VK Cloud для автоматизации использован WAL-G.

Присоединяйтесь к Telegram-каналу «Данные на стероидах». В нем вы найдете все об инструментах и подходах к извлечению максимальной пользы из работы с данными: регулярные дайджесты, полезные статьи, а также анонсы конференций и вебинаров.

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


  1. maniak26
    24.08.2023 18:02

    Благодарю за статью!
    Подскажите пожалуйста:
    1. Почему остановили выбор на WAL-G из всех вариантов?
    2. Если не секрет, какое хранилище для бекапов используете?
    3. Есть ли какой-то сценарий работы на случай длительной недоступности хранилища бекапов(чтоб wal файлы не забили весь диск)?


    1. DeBass Автор
      24.08.2023 18:02
      +1

      Спасибо!

      1. Почему WAL-G? Основные причины: написана на Go - наш стек, есть поддержка других СУБД. Поэтому при прочих равных нам этот инструмент более удобен.

      2. Для хранения используем наш S3.

      3. Пока что только мониторинг. Можно смотреть на свободное место на диске под WAL (если он на отдельном диске), либо смотреть на количество WAL сегментов. Так же можно мониторить pg_stat_archiver на предмет успешности/неуспешности выполнения archive_command (https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-ARCHIVER-VIEW)