Переезд продового Postgres с RDS Multi-AZ instance на Multi-AZ DB cluster
Плановый cutover почти без потери данных — и три ловушки: «техработы», которые не остановили запись, пароли, которые RDS не даёт скопировать, и дамп за 25 минут через VPN против 31 секунды внутри VPC.
В Gambchamp мы переносили живой продовый PostgreSQL с RDS Multi-AZ instance на Multi-AZ DB cluster: writer плюс две читаемые standby-реплики и отдельный reader-эндпоинт. Приложение нужно было переключить, роли, данные и гранты — перенести один в один, а потерю данных свести почти к нулю.
Это была плановая миграция, а не инцидент. Три вещи всё равно укусили.
Ловушка первая: «техработы» не остановили запись
Мы перевели приложение в режим техработ и сравнили точные количества строк по таблицам в два момента времени. Пять таблиц продолжали меняться: колбэки платёжных провайдеров, уведомления и визиты продолжали вставляться. Единственный дамп живого источника всегда отстаёт.
Настоящая заморозка — отскейлить всех писателей в ноль и доказать это: количества стабильны, пишущих соединений нет, сессий idle-in-transaction нет. Сверху, на источнике:
ALTER DATABASE app SET default_transaction_read_only = on;
плюс завершение оставшихся соединений приложения. Это блокирует любую запоздалую запись и заодно не даёт случиться split-brain после переключения.
Ловушка вторая: RDS не отдаёт пароли
pg_dumpall --globals-only — обычный способ перенести роли. На RDS даже master не может прочитать pg_authid, так что ни хеши, ни что-либо ещё оттуда не достать. pg_dumpall -g --no-role-passwords всё равно даёт точные атрибуты, членства и настройки ролей — это стоит забрать.
Пароли пришлось брать в другом месте. Роль приложения сохранила продовый пароль — его взяли из Kubernetes-секрета приложения и проверили реальным логином на источнике. Вспомогательные роли получили новые пароли. Роль, которая уже была просрочена, создали просроченной.
Ловушка третья: всё решила сеть
Дамп через VPN полз: 110 МБ примерно за 25 минут. Тот же pg_dump с EC2-хоста в той же VPC завершился за 31 секунду. Пароли передавались через stdin, никогда — аргументами команды.
Детали восстановления, которые легко упустить
- На RDS master — не владелец базы, поэтому
DROP DATABASEне проходит. Чистую цель сделали, удалив и пересоздав схемуpublicот имени роли-владельца.CREATE EXTENSIONпо-прежнему требует master. - Две ошибки при восстановлении ожидаемы.
ALTER DEFAULT PRIVILEGES FOR ROLE <app>падает под master и должен быть переигран от имени этой роли.publication already existsпоявляется потому, что публикации — объекты уровня базы и переживаютDROP SCHEMA.
Проверить, прежде чем открывать
- полный diff количеств строк по таблицам равен нулю;
- каждая последовательность сверена с максимумом своего столбца — отстающая последовательность означает
duplicate keyна первой же вставке; - реальный логин под каждой ролью и пробная запись от роли приложения.
Только после этого хост приложения переключили на writer-эндпоинт кластера, а чтение — на reader-эндпоинт. Старый instance остался изолированным: read-only и без единого соединения приложения.
Откат прост, пока первая запись не легла в кластер: сбросить default_transaction_read_only на старом instance и вернуть хост. После первой записи — точка невозврата.
Выводы
- Доказывайте заморозку diff’ом количеств. Колбэки и вебхуки продолжают писать после «техработ».
- На RDS планируйте перевыпуск паролей или берите их из секретов приложения; скопировать их из базы нельзя.
- Гоняйте дамп и восстановление изнутри VPC. Здесь это было примерно в 50 раз быстрее.
- Сверяйте последовательности со столбцами до того, как открыть запись.
- Мониторинг не следует за базой сам — переключить приложение это только половина cutover.