Материал учебного циклаОткрытый материал

Данные в эксплуатации: миграции PostgreSQL, резервные копии и восстановление

Как менять схему PostgreSQL без поломки работающего API, выбирать способ резервного копирования и проверять восстановление данных на отдельном стенде.

Инфраструктура и DevOps #DevOps #PostgreSQL #восстановление #миграции #резервное копирование
Учебный цикл От Docker до работающего сервиса Материал 5 из 9

Данные в эксплуатации: миграции схемы, резервные копии и восстановление PostgreSQL

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

На локальной базе задача выглядит простой: добавить столбец и обновить код. На рабочем сервере возникает другая ситуация. Пока выкатывается новая версия, старая ещё принимает запросы. В таблице уже есть заказы без нового поля. Миграция может ждать блокировку. А если после релиза обнаружится ошибка, возврат старого образа приложения сам по себе не вернёт прежнюю схему и данные.

Здесь сходятся три практических вопроса:

  • Как изменить структуру базы, пока приложение продолжает работать?
  • Какую копию данных делать, чтобы она помогла при нужном типе сбоя?
  • Как убедиться, что из копии действительно можно восстановить работающий сервис?

Устройство таблиц, транзакций и SQL уже разобрано в материале «PostgreSQL: как устроена реляционная база данных». В статье «Django: как устроен Python-фреймворк» показано, как создаются и применяются файлы миграций в Django. Здесь нас интересует следующий уровень: изменение живой базы, согласование схемы с несколькими версиями приложения и восстановление после ошибок.

Состояние системы шире версии приложения

В предыдущих главах цикла мы довели API заказов до сервера. Приложение написано на FastAPI, PostgreSQL хранит заказы, Redis используется как кэш, контейнеры запускаются через Compose, а Caddy принимает HTTPS-запросы к api.example.com. В CI/CD собирается образ приложения, который можно выпустить или откатить по его идентификатору.

После запуска система имеет сразу несколько состояний:

  • Код приложения: какая версия образа обрабатывает запросы.
  • Схема базы: какие таблицы, столбцы, индексы и ограничения существуют.
  • Данные: какие заказы записаны и в каком виде.
  • Внешние последствия: какие уведомления отправлены и какие действия выполнены в других системах.

Эти состояния меняются с разной скоростью. Образ можно заменить за минуты. Изменение миллионов строк может занять часы. Отправленное письмо нельзя «откатить» командой к базе.

Поэтому перед релизом недостаточно знать, как запустить alembic upgrade head. Нужно определить, какие сочетания старого и нового кода со старой и новой схемой допустимы, что произойдёт с уже записанными данными и каким будет путь восстановления.

Коротко: выпуск приложения и изменение базы образуют одну операцию, но откатываются по-разному.

Миграция схемы как переход между совместимыми состояниями

Миграция - зафиксированное изменение структуры или данных базы. Например, она добавляет столбец, создаёт индекс либо переносит значения из старого представления в новое.

В приложении на Django история миграций ведётся встроенным механизмом. Для нашего FastAPI-сервиса можно использовать Alembic. Инструменты различаются, а производственный вопрос одинаков: может ли приложение работать до, во время и после применения изменения?

Допустим, текущая таблица orders содержит адрес доставки в shipping_address типа jsonb. Внутри адреса может находиться ключ postcode. Новая версия API должна хранить почтовый индекс также в отдельном столбце shipping_postcode, чтобы обращаться к нему напрямую. Это дополнение к сквозному примеру: остальная архитектура сервиса не меняется.

Опасный порядок действий выглядел бы так:

  1. Сделать новый столбец обязательным.
  2. Развернуть новую версию приложения.
  3. Обнаружить, что старые строки не имеют значения, а ещё работающие старые процессы не заполняют новый столбец.

Безопаснее провести изменение по этапам расширение -> перенос данных -> переключение чтения -> удаление старого способа, если удаление вообще необходимо. Такой подход часто называют expand and contract.

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

Состояние Что происходит
Старый код + старая схема Исходная работа сервиса
Старый код + расширенная схема Приложение продолжает работать, новый столбец пока необязателен
Новый код + расширенная схема Новый код записывает оба представления и умеет читать старые строки
Новый код + проверенные данные Чтение можно перевести на новый столбец
Только новый код + окончательная схема Старое представление можно удалить отдельным релизом, если оно больше не требуется

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

Почему миграцию запускают отдельно от процессов API

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

Поэтому миграции запускают отдельным шагом выпуска, один раз на общую базу. История применённых версий хранится в самой базе, а файлы миграций входят в репозиторий и образ релиза. Перед изменением полезно сравнить текущую версию схемы с ожидаемой. В Alembic для этого служат, например, alembic current, alembic history и alembic upgrade head.

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

Коротко: файл миграции фиксирует изменение; порядок выпуска обеспечивает его совместимость с работающим сервисом.

Сквозной пример: выносим индекс доставки в отдельный столбец

Шаг расширения: добавляем необязательное поле

Первая миграция добавляет столбец без NOT NULL и без вычисления значения для всех существующих строк:

ALTER TABLE orders
ADD COLUMN shipping_postcode text;

После этого у старых заказов значение shipping_postcode равно NULL. Старая версия приложения продолжает работать. Новая версия получает место для записи индекса.

В файле Alembic соответствующий шаг может выглядеть так:

from alembic import op
import sqlalchemy as sa


def upgrade():
    op.add_column(
        "orders",
        sa.Column("shipping_postcode", sa.Text(), nullable=True),
    )


def downgrade():
    op.drop_column("orders", "shipping_postcode")

op.add_column изменяет таблицу orders. nullable=True разрешает старые строки без индекса. Функция downgrade здесь показывает обратное структурное действие, но её вызов удалит значения, успевшие появиться в новом столбце. Поэтому наличие downgrade не делает откат данных безопасным.

Добавление столбца без заполнения всех строк обычно короче, чем массовое обновление данных. Однако команда ALTER TABLE всё равно требует блокировку таблицы. Если перед ней долго выполняется транзакция, миграция может ждать, а следующие операции выстроятся в очередь. Для рабочего выпуска задают предел ожидания блокировки, наблюдают длительные транзакции и репетируют изменение на базе сопоставимого размера. Тип блокировки зависит от конкретной операции; его нужно проверять для выбранной команды PostgreSQL. Документация ALTER TABLE.

Например, для отдельной SQL-миграции можно ограничить ожидание так:

BEGIN;
SET LOCAL lock_timeout = '3s';

ALTER TABLE orders
ADD COLUMN shipping_postcode text;

COMMIT;

lock_timeout ограничивает именно ожидание блокировки, а не общее время работы миграции. Если нужную блокировку не удалось получить за три секунды, операция завершится ошибкой, которую увидит выпускной процесс. После этого команда выясняет причину ожидания и повторяет выпуск осознанно. Значение в три секунды служит примером; его выбирают по допустимой задержке запросов сервиса.

Шаг совместимости: новый код понимает обе формы

Новая версия API при создании или изменении заказа записывает индекс и в shipping_address, и в shipping_postcode. При чтении она сначала берёт отдельный столбец, а если тот ещё пуст, обращается к старому адресу.

Логика чтения выглядит так:

def order_postcode(order):
    if order.shipping_postcode is not None:
        return order.shipping_postcode

    address = order.shipping_address or {}
    return address.get("postcode")

Такой переход даёт время перенести старые строки. Он также сохраняет возможность вернуть предыдущий образ приложения: старый код по-прежнему получает индекс из shipping_address.

Есть важная гонка. Пока старые процессы ещё обслуживают запросы, они могут изменить shipping_address, не обновив shipping_postcode. Поэтому перенос данных нельзя считать окончательным до завершения работы всех старых процессов. После их вывода из обращения нужно ещё раз сверить оба представления и исправить расхождения.

Коротко: совместимость обеспечивают не название миграции и не флаг в CI/CD, а конкретное поведение старого и нового кода при чтении и записи.

Шаг переноса: обновляем старые строки порциями

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

Пример одной порции:

WITH batch AS (
    SELECT id
    FROM orders
    WHERE shipping_postcode IS NULL
      AND NULLIF(btrim(shipping_address->>'postcode'), '') IS NOT NULL
    ORDER BY id
    LIMIT 1000
    FOR UPDATE SKIP LOCKED
)
UPDATE orders AS o
SET shipping_postcode = btrim(o.shipping_address->>'postcode')
FROM batch
WHERE o.id = batch.id;

Здесь shipping_address->>'postcode' извлекает строковое значение из JSONB. btrim убирает пробелы по краям. NULLIF(..., '') исключает пустое значение: иначе такие строки снова и снова попадали бы в следующую порцию. LIMIT 1000 задаёт размер порции. FOR UPDATE SKIP LOCKED позволяет пропустить строки, уже заблокированные другой транзакцией, вместо ожидания каждой из них.

Скрипт повторяет этот запрос, пока очередная порция не обновит ни одной строки. Размер порции подбирают по времени транзакции, задержке обычных запросов, объёму WAL и нагрузке на диск. Число 1000 не является универсальной настройкой.

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

SELECT count(*) AS orders_without_postcode
FROM orders
WHERE shipping_postcode IS NULL;

SELECT id, shipping_address
FROM orders
WHERE shipping_postcode IS NULL
ORDER BY id
LIMIT 20;

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

Если индекс обязателен для каждого заказа, после исправления исключений можно добавить ограничение и проверить существующие строки. Для крупной таблицы PostgreSQL позволяет сначала создать CHECK с NOT VALID, а затем отдельно выполнить VALIDATE CONSTRAINT: проверка старых строк происходит на отдельном этапе с менее жёсткой блокировкой. Возможность и точный план следует сверять с версией PostgreSQL и фактической таблицей. Документация ограничений ALTER TABLE.

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

CREATE INDEX CONCURRENTLY idx_orders_shipping_postcode
ON orders (shipping_postcode);

CONCURRENTLY позволяет продолжать изменения строк во время построения индекса, но требует больше работы и не выполняется внутри обычного блока транзакции. При сбое построения может остаться недействительный индекс, состояние которого нужно проверить перед повтором. Создавать индекс «на всякий случай» не следует: он занимает место и добавляет работу при записи. Документация CREATE INDEX.

Шаг завершения: удаление старого поля откладывается

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

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

Это решение связано с резервными копиями: копия, созданная до удаления, содержит старую форму данных; код, который будет её читать при восстановлении, должен быть с ней совместим.

Коротко: необратимые изменения структуры выполняют только после периода совместимости и отдельной проверки данных.

Почему откат образа не равен откату базы

Предположим, после релиза новая версия API отвечает с ошибками. Команда возвращает предыдущий образ по зафиксированному digest. Если миграция лишь добавила необязательный столбец, старое приложение обычно продолжит работать с расширенной схемой. Значения в новом столбце можно сохранить для повторного выпуска.

Теперь другой случай: миграция удалила shipping_address, а старая версия читает только его. Возврат старого образа приведёт к ошибкам запросов. Команда вынуждена восстанавливать структуру и данные отдельно. Если часть адресов была изменена после удаления, прежние значения могут быть уже недоступны.

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

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

Сначала определяем допустимую потерю данных и время восстановления

Резервную копию выбирают по последствиям сбоя. Для этого используют две величины.

RPO (Recovery Point Objective) - максимальный допустимый промежуток данных, который можно потерять. Если копия делается ежедневно в 02:00, а сервер утрачен в 17:00, последняя копия может не содержать до 15 часов заказов. Это иллюстрация верхней границы для такого сценария; фактическая потеря зависит от времени последней успешной копии.

RTO (Recovery Time Objective) - допустимое время до восстановления работы сервиса. В него входят обнаружение сбоя, получение копии, восстановление базы, проверка приложения и переключение трафика.

Например, бизнес допускает потерю не более 10 минут заказов и простой не более часа. Ежедневный pg_dump не выполняет первое условие. Наличие копии без измеренного времени восстановления не доказывает выполнение второго.

Эти показатели задаются для сервиса, а не только для PostgreSQL. Если файлы клиентов хранятся отдельно от базы, их тоже нужно восстанавливать. Redis в нашем примере является кэшем: его можно заполнить заново, если там нет единственной копии важных данных. Уведомления и действия внешних платёжных сервисов требуют отдельного согласования после восстановления.

Коротко: частота копирования отвечает за возможную потерю данных; проверенный процесс восстановления отвечает за время простоя.

Логическая копия: когда подходит pg_dump

pg_dump создаёт логический снимок одной базы. Он экспортирует структуру и данные так, чтобы их можно было загрузить в PostgreSQL. Во время работы он получает согласованное представление базы и обычно не останавливает обычные чтения и записи. Однако операции изменения схемы могут конфликтовать с его блокировками. Документация PostgreSQL о SQL dump.

Для сервиса заказов можно делать копию в custom format:

umask 077

BACKUP_FILE="/secure/backups/orders-2026-10-12.dump"

pg_dump \
  -h db.internal \
  -U backup_operator \
  -d orders \
  -Fc \
  -f "$BACKUP_FILE"

test -s "$BACKUP_FILE"
pg_restore --list "$BACKUP_FILE" > /dev/null

Параметры здесь имеют разные роли:

  • -h db.internal указывает адрес сервера базы.
  • -U backup_operator выбирает заранее настроенную роль с правами, достаточными для полной копии.
  • -d orders задаёт базу заказов.
  • -Fc создаёт архив в формате, который читает pg_restore и который допускает выборочное восстановление.
  • -f записывает архив на машину, где запущен pg_dump.
  • umask 077 ограничивает права нового файла на уровне операционной системы.
  • test -s проверяет только наличие непустого файла.
  • pg_restore --list проверяет, что инструмент способен прочитать оглавление архива.

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

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

Есть ещё одна граница. pg_dump сохраняет одну базу, но не глобальные объекты кластера, включая роли и табличные пространства. Если план предполагает восстановление полного окружения, их резервируют отдельно, например через pg_dumpall --globals-only, и проверяют порядок восстановления. Такой файл тоже содержит чувствительные сведения и требует защиты. Документация PostgreSQL.

Логическая копия подходит для переноса базы, выборочного восстановления объектов и сценариев, где допустима потеря изменений после снимка. Её нельзя соединить с архивом WAL для восстановления на произвольную секунду: это разные механизмы резервирования. Документация PostgreSQL о PITR.

Физическая копия и WAL: восстановление к моменту времени

Рассмотрим ошибку другого рода. В 14:29 оператор запустил неверный UPDATE, который изменил статусы тысяч заказов. Копия, сделанная в 02:00, слишком старая: за день появились новые заказы. Нужен способ получить состояние непосредственно перед ошибочной операцией.

PostgreSQL ведёт WAL, журнал предзаписи изменений. Для восстановления к моменту времени (PITR) сохраняют базовую физическую копию кластера и непрерывную последовательность архивированных WAL-файлов. При восстановлении база поднимается из базовой копии и воспроизводит журнал до заданного момента. Это позволяет выбрать состояние перед ошибкой, если все нужные части архива доступны. Документация PostgreSQL о непрерывном архивировании.

Базовую копию можно получать через pg_basebackup. Для этого нужна подходящая учётная запись репликации, настроенный доступ и место для данных:

pg_basebackup \
  -h db.internal \
  -U replication_backup \
  -D /secure/basebackups/orders-2026-10-12 \
  -Fp \
  -X stream \
  -P

-D задаёт каталог назначения. -Fp сохраняет копию как набор файлов. -X stream включает необходимые для самой базовой копии WAL-записи через поток. -P показывает прогресс. Для восстановления после времени создания копии требуется также отдельное непрерывное архивирование следующих WAL-сегментов. Команда pg_basebackup сама по себе не создаёт историю всех будущих изменений. Документация pg_basebackup.

Для архивирования задают wal_level, archive_mode и способ передачи готовых сегментов через archive_command или archive_library. Команда архивации должна подтверждать успех только тогда, когда сегмент надёжно сохранён. Нужно следить за ошибками архивации, местом в pg_wal и отставанием архива. На сервере с небольшим числом записей сегмент может долго не заполняться, поэтому ожидаемый RPO нужно проверять с учётом archive_timeout и реального времени появления файлов в хранилище. Документация настройки архива WAL.

Физическую копию проверяют утилитой pg_verifybackup, если она создана с манифестом, который утилита может прочитать:

pg_verifybackup /secure/basebackups/orders-2026-10-12

Проверка обнаруживает многие проблемы с файлами и манифестом. Она не заменяет запуск восстановленного сервера и проверку данных приложением. Разные форматы копии имеют разные возможности проверки WAL; это нужно сверять с используемой версией PostgreSQL. Документация pg_verifybackup.

Для учебного примера последовательность PITR выглядит так:

базовая копия 02:00 -> архив WAL -> ошибочный UPDATE 14:29 -> восстановление отдельного кластера до 14:28:59 -> проверка заказов -> план переключения

Момент 14:28:59 здесь условный. В реальном инциденте сначала устанавливают точное время или позицию ошибочной операции и учитывают часовой пояс. Восстановление начинают в изолированном окружении, чтобы не заменить рабочую базу непроверенными данными. Конфигурация восстановления указывает, откуда читать архив через restore_command и до какого момента воспроизводить WAL через recovery_target_time. Настройки и сигнальный файл восстановления зависят от порядка запуска PostgreSQL; их заранее оформляют в проверенный сценарий для конкретного окружения.

Физическая копия относится ко всему кластеру PostgreSQL и тесно связана с версией сервера. Она не является удобной заменой логического экспорта одной таблицы. Поэтому выбор между логической копией и физической копией с WAL определяется нужным сценарием восстановления, а не тем, какая команда короче.

Коротко: pg_dump восстанавливает состояние на момент снимка; базовая физическая копия вместе с непрерывным WAL даёт путь к выбранному моменту времени.

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

Успешное сообщение «резервная копия создана» проверяет лишь одну часть цепочки. Настоящее испытание начинается с пустой среды, где нет доступа к производственным данным за пределами выбранной копии.

Для логического архива порядок можно воспроизвести на отдельном тестовом экземпляре PostgreSQL. В примере клиентские утилиты запущены с машины, где доступен архив, а тестовый сервер слушает 127.0.0.1:55432. Это не рабочий кластер:

export PGHOST=127.0.0.1
export PGPORT=55432
export PGUSER=restore_admin

createdb -T template0 orders_restore

pg_restore \
  --exit-on-error \
  --no-owner \
  --no-acl \
  -d orders_restore \
  /secure/backups/orders-2026-10-12.dump

createdb -T template0 создаёт пустую базу для загрузки. --exit-on-error останавливает восстановление при SQL-ошибке: без этого pg_restore может продолжить работу и сообщить число ошибок лишь в конце. --no-owner и --no-acl позволяют провести тест без воспроизведения производственных владельцев и прав. Такой режим подходит для проверки содержимого в песочнице, но не проверяет точность восстановления производственной модели доступа. Для полного сценария отдельно испытывают роли, разрешения и глобальные объекты. Документация pg_restore.

После загрузки проверка идёт по уровням.

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

Уровень базы: восстановлены нужные таблицы, ограничения и данные. Запросы на проверку соответствуют предметной области:

SELECT count(*) AS total_orders
FROM orders;

SELECT status, count(*)
FROM orders
GROUP BY status
ORDER BY status;

SELECT id, status, shipping_postcode
FROM orders
WHERE id IN (101, 205, 309)
ORDER BY id;

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

Уровень приложения: к восстановленной базе подключают совместимую версию API заказов. Проверяют чтение старого заказа, создание нового, смену статуса и обработку заказа с отсутствующим почтовым индексом. В этом окружении отключают реальные отправки уведомлений, платежи и фоновые обработчики, способные воздействовать на внешние системы.

Уровень времени: фиксируют длительность получения архива, загрузки базы, проверки и переключения приложения. Их сумма показывает практическое время восстановления для испытанного объёма данных. Когда база вырастет, испытание повторяют: результат годичной давности уже не характеризует текущий RTO.

Для PITR стенд должен дополнительно проверить непрерывность WAL, запуск восстановленного кластера и факт, что ошибочное изменение уже отсутствует, а изменения непосредственно перед целевым временем присутствуют. pg_verifybackup полезен до такого испытания, но окончательным доказательством служит работающее восстановленное приложение с ожидаемыми данными.

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

Что делать при разных типах сбоя

Неудачный выпуск кода. Сначала проверяем, совместима ли прежняя версия приложения с уже применённой схемой. Если изменение было расширяющим и старое приложение продолжает работать, возвращаем прежний образ, сохраняя новые данные. Затем выпускаем исправление.

Ошибка переноса данных. Останавливаем задачу переноса, сохраняем сведения об изменённых строках и определяем масштаб ошибки. Копию восстанавливаем отдельно. Нужные значения сравниваем и переносим обратно контролируемой операцией. Возврат всей производственной базы к старой копии может удалить корректные заказы, пришедшие позже.

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

Потеря сервера базы. Поднимаем подготовленную инфраструктуру и восстанавливаем базу из выбранного типа копии. Проверяем роли, подключение API, состояние заказов и внешние зависимости. Только после этого переключаем трафик.

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

Во всех случаях фиксируют, какую точку восстановления выбрали и какие операции после неё требуют сверки. PostgreSQL восстановит своё состояние, но не отменит автоматически уже отправленные письма, сообщения в очередях и операции сторонних сервисов.

Рабочий порядок для команды

Перед очередной миграцией команда может пройти один короткий, но содержательный маршрут.

  1. Описать переход. Записать исходную и целевую схему, объём таблиц, поведение старого и нового кода, порядок заполнения старых строк.
  2. Проверить риск операции. Посмотреть фактический SQL миграции, требуемые блокировки, возможное сканирование или переписывание таблицы, время выполнения на данных сопоставимого размера.
  3. Подготовить выход из ошибки. Проверить свежесть копий, архив WAL при необходимости и работоспособность сценария восстановления. Зафиксировать, на каком этапе возможен возврат предыдущего образа.
  4. Выполнить расширение отдельно. Запустить одного исполнителя миграций, проверить её версию и состояние базы, затем выпускать совместимый код.
  5. Переносить данные наблюдаемо. Выполнять небольшие повторяемые порции, измерять ошибки и нагрузку, отдельно разбирать строки, которые не удалось перенести.
  6. Проверить поведение сервиса. Пройти реальные операции с новыми и старыми заказами, дождаться завершения старых процессов, повторить сверку данных.
  7. Убирать старую структуру позже. Делать это отдельным изменением после завершения периода совместимости.
  8. Репетировать восстановление. Регулярно поднимать отдельную базу из сохранённой копии и проверять её приложением, а не ограничиваться отчётом резервного задания.

Так миграции и копирование становятся частью эксплуатации сервиса. Их результат измеряется не фактом выполнения команды, а тем, продолжает ли API принимать заказы и можно ли восстановить его данные в пределах согласованных RPO и RTO.

Итог

У работающей базы есть два разных требования: изменяться без поломки текущего сервиса и возвращаться в проверенное состояние после ошибки.

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

В сквозном сервисе заказов эти требования связываются в один процесс: расширить схему -> выпустить совместимый API -> перенести и проверить заказы -> завершить переход -> сохранить и испытать путь восстановления. Тогда новая версия системы опирается на данные, которые команда умеет не только менять, но и возвращать к работе после сбоя.

Продолжить

  1. 01 PostgreSQL: как устроена реляционная база данных Введение в PostgreSQL: сервер и клиент, таблицы, ключи, связи, SQL, ограничения, JOIN, транзакции, индексы, JSONB, роли, резервные копии и подключение из FastAPI.
  2. 02 Django: как устроен Python-фреймворк для веб-приложений Разбираем Django с нуля: проект и приложения, маршрутизацию, представления, модели, ORM, шаблоны, формы, middleware, админ-панель и путь HTTP-запроса.

Обратная связь

Материал был полезен?

Нашли ошибку или неточность? Сообщить →

Начните вводить — найдём материал по всей библиотеке.