Падение любой информационной системы — это по умолчанию больно и неприятно. На Хабре вы найдете много статей о том, как этого избежать. Но что делать, если все-таки случилась одна из тех историй, которыми пугают джунов? Уборщица разлила ведро воды в ЦОДе или злосчастный экскаватор перерубил оптоволокно?
Меня зовут Андрей Белый, я старший разработчик 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-каналу «Данные на стероидах». В нем вы найдете все об инструментах и подходах к извлечению максимальной пользы из работы с данными: регулярные дайджесты, полезные статьи, а также анонсы конференций и вебинаров.