Если у вас PostgreSQL с включённым row-level security и ночной pg_dump по таймеру, откройте каталог с бэкапами и сравните размеры вчерашнего и позавчерашнего дампов. Если файлы подозрительно одинаковые и маленькие, возможно, у вас нет ни одного рабочего бэкапа. У нас так было не меньше двух недель.

Что мы увидели

Небольшой VPS, на нём один из наших проектов с PostgreSQL. Каждую ночь запускается systemd-таймер, он вызывает скрипт, скрипт делает pg_dump в каталог бэкапов. Файлы появлялись исправно:

appdb-<дата>.dump ~120 КБ appdb-<дата>.dump ~120 КБ appdb-<дата>.dump ~120 КБ

Одинаковый размер у дампов базы с живыми данными уже странно. В журнале юнита нашлась причина:

pg_dump: error: query failed: ERROR: query would be affected by row-level security policy for table “”

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

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

Почему pg_dump падает

По документации pg_dump по умолчанию выставляет row_security = off, чтобы выгрузить все строки таблицы без исключения. Если у роли для этого не хватает прав, он завершается с ошибкой, а не выгружает часть данных. Права обходить RLS бывают у суперпользователя, у роли с атрибутом BYPASSRLS и у владельца таблицы (если не включён FORCE ROW LEVEL SECURITY).

В нашем случае дамп шёл под ролью, которая называлась как будто хозяйская (..._owner), но таблицами не владела: их владельцем был postgres. Атрибута BYPASSRLS у неё не было, а на таблицах были включены политики, и дамп упал на первой же такой таблице.

Самый короткий фикс, который я не применил

Одна строка:

ALTER ROLE <backup_role> BYPASSRLS;

Остановило то, что эта роль не просто «для бэкапа». Под ней работает само приложение. Выдать ей BYPASSRLS означало бы отключить row-level security для приложения на всех 11 таблицах, включая таблицу с учётными данными пользователей.

Отдельная роль только для бэкапа с BYPASSRLS тоже рабочий вариант. Мы пошли другим путём.

Что сделали

Дамп стал сниматься суперпользователем postgres через unix-сокет:

runuser -u postgres – env -u PGUSER -u PGPASSWORD
pg_dump -h /var/run/postgresql -U postgres
–format=custom --no-owner --no-privileges appdb > “$backup_file”

Два замечания к этой строке.

runuser, а не sudo. В юните стоит NoNewPrivileges=true, и sudo под ним не работает: ему нужен setuid. runuser вызывается от root и просто переключает пользователя, ему повышение привилегий не нужно.

Редирект вместо --file. Каталог бэкапов принадлежит root, а pg_dump теперь работает от имени postgres и не смог бы создать в нём файл. Поэтому вывод идёт в stdout, а открывает файл оболочка скрипта, которая работает от root.

env -u PGUSER -u PGPASSWORD сбрасывает переменные окружения, которые могли бы подменить пользователя и пароль подключения.

Как мы проверили, что теперь настоящий бэкап

Файл уже один раз нас обманул, поэтому на этот раз проверок было несколько:

  • размер дампа вырос больше чем в пять раз по сравнению с битыми;

  • совпала контрольная сумма;

  • pg_restore --list прошёл без ошибок;

  • тестовое восстановление в отдельную базу прошло без ошибок;

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

Тестовую базу после этого удалили.

Что бы я заложил в скрипт сразу

Это набросок под такую же ситуацию, а не наш боевой скрипт:

set -euo pipefail

DB=appdb DIR=/var/backups/postgresql/appdb

tmp=“(mktemp -pDIR” .dump.XXXXXX)" trap ‘rm -f “$tmp”’ EXIT

runuser -u postgres – env -u PGUSER -u PGPASSWORD
pg_dump -h /var/run/postgresql -U postgres
–format=custom --no-owner --no-privileges “

"" class="formula inline">tmp”

pg_restore --list “$tmp” > /dev/null

mv “tmpDIR/DB-(date +%F).dump”

Дамп пишется во временный файл, проверяется pg_restore --list и только потом получает настоящее имя. Если pg_dump упал, под настоящим именем файла просто нет, и такой дамп невозможно принять за рабочий.

Что стоит проверить у себя

  • Откройте каталог бэкапов и посмотрите на размеры подряд идущих дампов. Одинаковый размер у меняющейся базы повод для вопросов.

  • Посмотрите журнал юнита или cron за последний месяц на ошибки pg_dump.

  • Если у вас RLS, выясните, у какой роли идёт дамп, чем она владеет и есть ли у неё BYPASSRLS. Не выдавайте BYPASSRLS роли, под которой работает приложение.

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

  • Мониторьте то, что отличает рабочий бэкап от нерабочего: размер файла относительно предыдущего и статус юнита. Факт появления файла не доказывает ничего.

Начните с первых двух пунктов: на них уйдёт несколько минут.