Point-in-time Recovery. Как уменьшить RPO для базы данных

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.


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

Меня зовут Андрей Белый, я старший разработчик 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-каналу «Данные на стероидах». В нем вы найдете все об инструментах и подходах к извлечению максимальной пользы из работы с данными: регулярные дайджесты, полезные статьи, а также анонсы конференций и вебинаров.
Источник: https://habr.com/ru/companies/vk/articles/755922/


Интересные статьи

Интересные статьи

Привет! Меня зовут Даниил Гранкин, я разработчик внутреннего бэкенд-юнита в Wrike. В этой статье я поделюсь техническими подробностями разработки Wrike Lock — основного механизма шифрования данных в н...
Привет! Меня зовут Виталий Чесноков. Эта статья — продолжение истории о том, как мы выстраивали систему управления знаниями в QSOFT.
В предыдущей статье я вкратце упомянул, что Knime умеет работать с базами данных, в том числе с NoSQL базой MongoDB. На мой взгляд, MongoDB является довольно простым и эффективным решением для хранени...
FYI: Первая часть. Бенчмарк пакетного конвейера Пакетный конвейер обрабатывает конечный объём сохранённых данных. Здесь нет потока результатов обработки, выходные данные агрегир...
Вчера вечером, 19 марта, Google провели собственную презентацию в рамках Game Developers Conference. Суть происходящего — новый видеостриминговый сервис, позволяющий играть с любого устройства бе...