Перейти к содержимому

Возможности Postgres Pro Enterprise

Возможности Postgres Pro Enterprise для производительности, безопасности, резервного копирования и высокой доступности.

Курс сохраняет официальный порядок тем. Русские материалы извлечены из PDF в читаемый нативный Markdown для дальнейшей редакторской работы.

Состояние материалов: 官方 PDF 已提取为原生 Markdown

Базовый язык: Базовый язык: Русский. В проверенных открытых каталогах не найден текущий английский архив. Для курса Postgres Pro Enterprise базовой является русская версия.

О курсе

Возможности Postgres Pro Enterprise для производительности, безопасности, резервного копирования и высокой доступности.

Состояние материалов

ЯзыкТекущий архивВерсияТемыСтраница курса
EnglishОткрытый архив не найден--HTTP 404
РусскийPGPRO-handouts-16.zip1623HTTP 200

Содержание

Дополнительные материалы

  • pgpro_new_features.pdf
  • pgpro_student_guide.pdf

Редакторский статус

Русский комплект доступен в виде нативного Markdown. Если английский комплект не найден, английский маршрут оставлен для будущего источника.

00 Занятие 00: Postgres Pro Enterprise 16 / Возможности Postgres Pro Enterprise

Исходная страница в нативном Markdown: Postgres Pro Enterprise 16 / Возможности Postgres Pro Enterprise
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_00_introduction.html Archive: PGPRO-handouts-16.zip Страниц PDF: 12

Page 1: Postgres Pro Enterprise 16 / Возможности Postgres Pro Enterprise

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Администраторам

Source slide page 2: Администраторам{#source-figure-002 caption=“Source slide page 2: Администраторам”}

  • DBA2, 4 дня

Комментарии преподавателя

Для прохождения всех курсов необходимо знание основ языка SQL. Курса по SQL в нашей линейке нет, но имеется множество книг и образовательных ресурсов для освоения SQL. Мы рекомендуем книгу Евгения Моргунова «PostgreSQL. Основы языка SQL»:

https://postgrespro.ru/education/books/sqlprimer

Для администраторов мы предлагаем следующие курсы.

Курс DBA1 – базовый, он дает сведения об архитектуре PostgreSQL, об установке, настройке и управлении сервером. Приводятся обзоры разграничения доступа, резервного копирования и репликации.

В курсе DBA2 обсуждается настройка конфигурационных параметров исходя из понимания внутренней организации сервера; говорится о мониторинге сервера с использованием обратной связи для итеративной настройки параметров.

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

Курс DBS рассказывает про настройку подключений клиентов к СУБД и про управление доступом внутри самой СУБД.

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

Page 3: Разработчикам

Source slide page 3: Разработчикам{#source-figure-003 caption=“Source slide page 3: Разработчикам”}

  • DEV2, 4 дня

Комментарии преподавателя

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

Базовым курсом для разработчиков является курс DEV1, в котором даются общие сведения об архитектуре PostgreSQL и использовании основных объектов базы данных; изучается программирование на стороне сервера на языках SQL и PL/pgSQL с использованием основных типов данных (включая составной тип и массивы), а также рассматривается взаимодействие с клиентской частью.

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

Курсы DBS, QPT и PGPRO являются общими для разработчиков и администраторов.

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

https://postgrespro.ru/education/courses

Page 4: О курсе

Продолжительность: 4 дня

  • Предварительные знания
    • знакомство с ОС Unix
    • уверенное владение SQL (знакомство с PL/pgSQL не обязательно,
    • но полезно)
    • PostgreSQL в объеме курсов DBA1, DBA2, DBA3, DBS и QPT,
    • либо DEV1, DEV2, DBS и QPT
  • Какие навыки будут получены
    • использование дополнительных возможностей Postgres Pro Enterprise

Комментарии преподавателя

В данном курсе PGPRO рассматриваются дополнительные возможности, которые предоставляет СУБД Postgres Pro Enterprise.

Page 5: Используемые материалы

Подготовленная виртуальная машина

  • ОС Xubuntu 24.04
  • Postgres Pro Enterprise 16 с документацией на русском языке
  • дополнительные экземпляры PostgreSQL 16 и Postgres Pro Enterprise 16
  • Учебные материалы
    • презентации, демонстрации, практические задания и их решение
    • (в форматах html и pdf)

Page 6: Виртуальная машина

В виртуальной машине курса установлен Postgres Pro Enterprise 16 с дополнительными расширениями, и в каталоге /var/lib/pgpro/ent-16 инициализирован кластер, использующий порт 5432. Для файлов кластера разрешен групповой доступ, а пользователь ОС student включен в группу postgres, это позволяет ему обращаться к каталогу данных для копирования и восстановления:

student$ getent group postgres
postgres:x:124:student
student$ ls -l /var/lib/pgpro/ent-16
total 164
drwxr-x--- 1 postgres postgres  4096 июл 14 11:05 base
drwxr-x--- 2 postgres postgres  4096 июл 14 11:06 conf.d
drwxr-x--- 2 postgres postgres  4096 июл 14 11:05 crash_info
-rw-r----- 1 postgres postgres    88 июл 27 23:33 current_logfiles
drwxr-x--- 1 postgres postgres  4096 июл 27 23:33 global
drwx------ 1 postgres postgres  4096 июл 27 23:33 log
drwxr-x--- 2 postgres postgres  4096 июл 14 11:05 pg_commit_ts
drwxr-x--- 2 postgres postgres  4096 июл 14 11:05 pg_dynshmem
-rw-r----- 1 postgres postgres  5780 июл 14 11:05 pg_hba.conf
drwxr-x--- 2 postgres postgres  4096 июл 14 11:05 pg_hba.conf.d
-rw-r----- 1 postgres postgres  2640 июл 14 11:05 pg_ident.conf
drwxr-x--- 4 postgres postgres  4096 июл 14 11:11 pg_logical
drwxr-x--- 4 postgres postgres  4096 июл 14 11:05 pg_multixact
drwxr-x--- 2 postgres postgres  4096 июл 14 11:05 pg_notify
drwxr-x--- 2 postgres postgres  4096 июл 14 11:05 pg_replslot
drwxr-x--- 2 postgres postgres  4096 июл 14 11:05 pg_serial
drwxr-x--- 2 postgres postgres  4096 июл 14 11:05 pg_snapshots
drwxr-x--- 1 postgres postgres  4096 июл 27 23:33 pg_stat
drwxr-x--- 2 postgres postgres  4096 июл 14 11:05 pg_stat_tmp
drwxr-x--- 2 postgres postgres  4096 июл 14 11:05 pg_subtrans
drwxr-x--- 2 postgres postgres  4096 июл 14 11:05 pg_tblspc
drwxr-x--- 2 postgres postgres  4096 июл 14 11:05 pg_twophase
-rw-r----- 1 postgres postgres     3 июл 14 11:05 PG_VERSION
drwxr-x--- 1 postgres postgres  4096 июл 14 11:11 pg_wal
drwxr-x--- 2 postgres postgres  4096 июл 14 11:05 pg_xact
-rw-r----- 1 postgres postgres    88 июл 14 11:05 postgresql.auto.conf
-rw-r----- 1 postgres postgres 35982 июл 14 11:05 postgresql.conf
-rw-r----- 1 postgres postgres    60 июл 27 23:33 postmaster.opts
-rw-r----- 1 postgres postgres    89 июл 27 23:33 postmaster.pid

Экземпляр запускается автоматически при старте ОС. Подключимся к нему:

student$ psql
=> \conninfo
You are connected to database "student" as user "student" via socket in "/tmp" at port
"5432".

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

postgres$ grep ^include_dir /var/lib/pgpro/ent-16/postgresql.conf
include_dir='conf.d'

В конфигурационный файл аутентификации с той же целью добавлена директива include_dir:

postgres$ grep ^include_dir /var/lib/pgpro/ent-16/pg_hba.conf
include_dir pg_hba.conf.d

В кластере созданы роли: student — для удобства подключения; backup — для тем модуля «Резервное копирование»:

=> \du

Page 7: Page 7

                             List of roles
 Role name |                         Attributes
-----------+------------------------------------------------------------
 backup    | Replication
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS
 student   | Superuser

В кластер добавлены базы данных: student — для удобства подключения; demo — демонстрационная база, используется во многих темах курса (подробнее см. в курсе QPT); templateprobackup — шаблон, в котором роли backup выданы привилегии на выполнение функций, нужные для работы утилиты pg_probackup:

=> \l
                                                           List of databases
       Name        |  Owner   | Encoding | Locale Provider |   Collate   |    Ctype    |
ICU Locale | ICU Rules |   Access privileges
-------------------+----------+----------+-----------------+-------------+-------------+--
----------+-----------+-----------------------
 demo              | student  | UTF8     | icu             | en_US.UTF-8 | en_US.UTF-8 |
en-US      |           |
 postgres          | postgres | UTF8     | icu             | en_US.UTF-8 | en_US.UTF-8 |
en-US      |           |
 student           | postgres | UTF8     | icu             | en_US.UTF-8 | en_US.UTF-8 |
en-US      |           |
 template0         | postgres | UTF8     | icu             | en_US.UTF-8 | en_US.UTF-8 |
en-US      |           | =c/postgres          +
                   |          |          |                 |             |             |
          |           | postgres=CTc/postgres
 template1         | postgres | UTF8     | icu             | en_US.UTF-8 | en_US.UTF-8 |
en-US      |           | =c/postgres          +
                   |          |          |                 |             |             |
          |           | postgres=CTc/postgres
 templateprobackup | postgres | UTF8     | icu             | en_US.UTF-8 | en_US.UTF-8 |
en-US      |           |
(6 rows)

Для тем «PPEM» и «Миграция» установлен PostgreSQL 16 и инициализированы кластеры ppem и prod:

student$ pg_lsclusters
Ver Cluster Port Status Owner    Data directory              Log file
16  ppem    5435 down   postgres /var/lib/postgresql/16/ppem
/var/log/postgresql/postgresql-16-ppem.log
16  prod    5433 down   postgres /var/lib/postgresql/16/prod
/var/log/postgresql/postgresql-16-prod.log

Page 8: Организационные вопросы

День: ~8 академических часов + обед (1час)

  • Каждая тема, как правило, состоит из
    • презентации и демонстрации: ~25–60 мин
    • практических заданий: ~30 мин, включая перерыв

Page 9: День 1

Установка и сопровождение

    1. Редакции и возможности
    1. Postgres Pro Enterprise Manager
    1. Установка, настройка, обновление
    1. Миграция
    1. Планировщик заданий

Page 10: День 2

Данные

    1. CFS — сжатая файловая система
  • Производительность
      1. Транзакции и сеансы
      1. Оптимизация запросов
      1. Адаптивная оптимизация
      1. Перепланирование
      1. Сохранение планов запросов

Page 11: День 3

Производительность (продолжение)

    1. Анализ производительности
    1. Отчеты по нагрузке pgpro_pwr
  • Безопасность
      1. Профили пользователей
      1. Аудит
      1. Защищенная схема
      1. Анонимизация данных

Page 12: День 4

Резервное копирование

    1. Основы pg_probackup
    1. Использование pg_probackup
    1. Другие возможности pg_probackup
  • Отказоустойчивые решения
      1. Кластер multimaster
      1. Кластер BiHA

01 Занятие 01: Установка и сопровождение / Редакции и возможности

Исходная страница в нативном Markdown: Установка и сопровождение / Редакции и возможности
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_01_features.html Archive: PGPRO-handouts-16.zip Страниц PDF: 34

Page 1: Установка и сопровождение / Редакции и возможности

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Продукты Postgres Professional и редакции Postgres Pro
  • Возможности Postgres Pro Enterprise

Page 3: Продукты и редакции

Редакции Postgres Pro

  • Лицензионная политика
  • Другие продукты Postgres Professional

Page 4: Редакции Postgres Pro

Standard

  • Certified
  • Enterprise
  • Enterprise Certified

Комментарии преподавателя

Postgres Pro поставляется в различных редакциях.

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

https://postgrespro.ru/products/postgrespro/standard

Enterprise — существенно доработанная версия PostgreSQL c функциональностью, предназначенной для высоконагруженных систем в больших компаниях:

https://postgrespro.ru/products/postgrespro/enterprise

Обе редакции имеют сертифицированные ФСТЭК версии, которые могут применяться на объектах критической информационной инфраструктуры, в государственных информационных системах, а также в системах хранения и обработки персональных данных и конфиденциальной информации:

https://postgrespro.ru/products/postgrespro/certified

https://postgrespro.ru/products/postgrespro/enterprisecertified

Начиная с версии 15, ОС Windows не поддерживается Postgres Pro, но компания собирает дистрибутив обычного PostgreSQL под Windows.

https://postgrespro.ru/windows

В этом курсе, если не указано иное, мы рассматриваем возможности Postgres Pro Enterprise.

Page 5: Лицензионная политика

Лицензирование по количеству ядер

  • в стоимость лицензии входит годовая техническая поддержка
  • бесплатные академические лицензии для образовательных организаций
  • Для 1С возможно лицензирование по количеству серверов и пользователей

Комментарии преподавателя

Postgres Pro лицензируется по количеству ядер. В стоимость лицензии входит один год технической поддержки.

Все редакции Postgres Pro поддерживают работу с 1С: в состав входят все необходимые расширения (fasttrun, fulleq, mchar, online_analyze), а ядро включает оптимизации производительности (например, временные таблицы не требуют ввода-вывода, пока помещаются в отведенной оперативной памяти).

При использовании Postgres Pro для работы с 1С можно выбрать другую модель лицензирования — по количеству серверов и пользователей.

https://postgrespro.ru/licensing

Образовательным организациям бесплатно предоставляются академические лицензии на Postgres Pro Standard и Postgres Pro Enterprise. Такие лицензии позволяют использовать СУБД не только для обучения, научных исследований и студенческих конкурсов, но и для административных задач. Они не ограничены по времени, но не включают техническую поддержку.

https://postgrespro.ru/education/university

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

https://postgrespro.ru/services

Подробную информацию по стоимости, срокам лицензии и по вопросам предоставления услуг можно получить, написав на sales@postgrespro.ru

Page 6: Другие продукты

Postgres Pro Enterprise Manager

  • Тема 2. Postgres Pro
  • Enterprise Manager
  • интегрированная панель управления
  • централизованный интерфейс мониторинга и администрирования
  • Postgres Pro Backup Enterprise
    • Темы 18–20
    • утилита резервного копирования и восстановления pg_probackup
  • Postgres Pro Shardman
    • распределенная отказоустойчивая СУБД с ACID-гарантиями
    • горизонтальное масштабирование по чтению и записи

Комментарии преподавателя

Кроме СУБД Postgres Pro, компания Postgres Professional предлагает ряд других продуктов.

Postgres Pro Enterprise Manager — интегрированная административная панель управления для Postgres Pro Enterprise. Предоставляет централизованный интерфейс мониторинга и управления СУБД.

https://postgrespro.ru/products/PPEM

Postgres Pro Backup Enterprise — утилита pg_probackup для управления резервным копированием и восстановлением кластеров баз данных PostgreSQL. Утилита работает с обычным PostgreSQL и со всеми редакциями Postgres Pro, но в сочетании с Postgres Pro Enterprise поддерживает CFS и размещение резервных копий в S3-хранилище.

Утилита входит в состав Postgres Pro Enterprise и рассматривается в этом курсе.

https://postgrespro.ru/products/pg\_probackup

Postgres Pro Shardman — распределенная объектно-реляционная СУБД на основе PostgreSQL c дополнительными изменениями и расширениями. Shardman совместим с Postgres Pro Enterprise и использует многие ее возможности (в том числе 64-битные номера транзакций, сбор статистики, аудит, CFS, обеспечение высокой доступности другие).

https://postgrespro.ru/products/shardman

Page 7: Установка и миграция

Автоматическая настройка параметров при инициализации кластера

  • Тема 3.
  • Установка, настройка,
  • обновление
  • запуск pgpro_tune по умолчанию
  • Упрощение миграции с Oracle
    • Тема 4. Миграция
    • реализация пакетов
    • oracle_fdw — обертка сторонних данных
    • orafce — популярные функции системных пакетов Oracle
    • ora2pgpro — автоматизация миграции

Комментарии преподавателя

При инициализации кластера Postgres Pro Enterprise по умолчанию выполняется его дополнительная настройка, зависящая от конфигурации оборудования. Для этого запускается утилита pgpro_tune, которая собирает информацию о системе и дописывает в файл конфигурации необходимые параметры.

https://postgrespro.ru/docs/enterprise/16/app-pgpro-tune

Для облегчения миграции с Oracle в Postgres Pro Enterprise реализован функционал пакетов Oracle и добавлены расширения:

● oracle_fdw — обертка сторонних данных для Oracle;

● orafce — распространенные функции из пакетов Oracle;

● ora2pgpro — утилита конвертации кода и переноса объектов из Oracle.

https://postgrespro.ru/docs/enterprise/16/pgpro-migration-tools

Page 8: Планировщик заданий

pgpro\_scheduler — встроенный планировщик заданий

  • разовые и планируемые задания
  • позволяет обойтись без средств операционной системы
  • Тема 5. Планировщик
  • заданий

Комментарии преподавателя

Расширение pgpro_scheduler — интегрированный в Postgres Pro Enterprise планировщик заданий. С его помощью можно задавать сложные расписания, динамически вычислять время следующего запуска для повторяющихся заданий, выполнять SQL-команды задания в одной или в нескольких последовательных транзакциях, назначать задания для немедленного или отложенного однократного выполнения одновременно с обычными планируемыми заданиями.

https://postgrespro.ru/docs/enterprise/16/pgpro-scheduler

Page 9: Данные

Сжатая файловая система

  • Расширения и утилиты для данных
  • Диагностика и исправление повреждений данных
  • Работа с данными в памяти

Page 10: Сжатая файловая система

CFS — Compressed File System

  • включается для отдельных табличных пространств
  • позволяет сжать данные в несколько раз
  • сокращает объем дискового ввода-вывода
  • измененные страницы записываются последовательно
  • Тема 6. CFS — сжатая
  • файловая система

Комментарии преподавателя

Использование сжатой файловой системы (Compressed File System, CFS) позволяет сократить объем дискового ввода-вывода за счет хранения сжатых блоков данных и улучшить кучность хранения за счет последовательной записи сжатых блоков в конец файла на диске.

Механизм CFS включается на уровне отдельных табличных пространств.

Как правило, сжатие позволяет уменьшить объем хранимых данных в несколько раз.

https://postgrespro.ru/docs/enterprise/16/cfs

Page 11: Хранение данных

TOAST для системного каталога

  • для столбцов со списками управления доступом
  • pg_transfer
    • быстрое перемещение таблиц между серверами
  • pg_repack
    • устранение раздувания таблиц и индексов

Комментарии преподавателя

Таблицы системного каталога, содержащие потенциально длинные списки управления доступом (Access Control List), такие как pg_class и pg_attribute, могут использовать TOAST.

С помощью расширения pg_transfer можно специальным образом подготовить таблицу, содержащую много данных, и затем быстро переместить ее на другой экземпляр Postgres Pro простым копированием файлов данных. Расширение выполняет необходимые для этого корректировки системного каталога.

https://postgrespro.ru/docs/enterprise/16/pgtransfer

В Postgres Pro Enterprise включена сторонняя утилита pg_repack, которая позволяет избавиться от раздувания таблиц и индексов без установки исключительной блокировки на все время работы, как того требует команда VACUUM FULL.

https://postgrespro.ru/docs/enterprise/16/app-pgrepack

Page 12: Повреждения данных

amcheck

  • контроль целостности
  • добавлена проверка уникальности индексов
  • pg_filedump
    • низкоуровневый просмотр файлов данных
  • Просмотр всех версий строк
    • функция pg_snapshot_any для отключения правил видимости

Комментарии преподавателя

Postgres Pro Enterprise включает дополнительные средства для контроля целостности, отладки и исследования поврежденных данных.

Штатное расширение amcheck, позволяющее проверять целостность данных, доработано для проверки ограничений уникальности в B-деревьях.

https://postgrespro.ru/docs/enterprise/16/amcheck

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

https://postgrespro.ru/docs/enterprise/16/pg-filedump

Функция pg_snapshot_any выводит все версии строк таблицы, отключая правила видимости.

https://postgrespro.ru/docs/enterprise/16/functions-admin#FUNCTIONS-DE BUGGING-TABLE

Page 13: Данные в памяти

pg\_variables

  • переменные в памяти сеанса
  • in_memory
    • небольшие таблицы в общей памяти
    • Тема 7.
    • Транзакции
    • и сеансы

Комментарии преподавателя

Расширение pg_variables позволяет работать с переменными различных типов (скалярными, массивами и коллекциями) в памяти сеанса.

https://postgrespro.ru/docs/enterprise/16/pg-variables

С помощью расширения in_memory можно размещать небольшие таблицы в общей памяти сервера. Работа с таблицей происходит с помощью обертки сторонних данных.

https://postgrespro.ru/docs/enterprise/16/in-memory

Оба расширения можно рассматривать как альтернативу временным таблицам.

Page 14: Производительность

Управление транзакциями и сеансами

  • Выполнение и оптимизация запросов
  • Анализ нагрузки и мониторинг

Page 15: Транзакции и сеансы

64-разрядный счетчик транзакций

  • Автономные транзакции
  • Встроенный пул соединений
    • сохраняет контекст сеанса
    • Тема 7.
    • Транзакции
    • и сеансы

Комментарии преподавателя

32-битные идентификаторы транзакций в PostgreSQL создают проблемы с обслуживанием переполнения счетчика транзакций. в нагруженных системах. В Postgres Pro Enterprise используются 64-битные идентификаторы, упрощающие администрирование.

Автономные транзакции позволяют зафиксировать или откатить часть операторов независимо от статуса основной транзакции.

В обычном режиме работы сервера каждое подключение обслуживается отдельным процессом, что уменьшает эффективность совместной работы. В Postgres Pro Enterprise реализован встроенный пул соединений, сохраняющий окружения сеансов.

Page 16: Сеансы и подключения

pgbouncer — сторонний пул соединений

  • Параметры libpq для подключения к разным серверам
    • hostorder, failover_timeout
  • Отключение сеансов и переподключение
    • reusepass
  • Функции для изменения параметров других сеансов
    • pg_backend_set_config, pg_backend_load_library

Комментарии преподавателя

Помимо встроенного пула соединений, в Postgres Pro включен сторонний пул соединений pgbouncer. Работа с pgbouncer с точки зрения разработчика приложений рассматривается в курсе DEV2.

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

● hostorder — порядок подключения к серверам (sequential/random);

● failover_timeout — максимальное время для циклического переподключения к узлам, указанным в строке соединения.

https://postgrespro.ru/docs/enterprise/16/libpq-connect

Дополнительный конфигурационный параметр reusepass — разрешается ли использовать запрошенный пароль при повторном установлении подключения (запрет автоматического переподключения).

Добавлены функции pg_backend_set_config и pg_backend_load_library, позволяющие менять параметры других сеансов и загружать в них библиотеки.

https://postgrespro.ru/docs/enterprise/16/functions-admin#FUNCTIONS-AD MIN-SET

Page 17: Приоритизация ресурсов

pgpro\_rp — расширение для приоритизации ресурсов

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

Комментарии преподавателя

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

В Postgres Pro Enterprise добавлено расширение pgpro_rp, с помощью которого можно назначить сеансам приоритеты в отношении трех видов ресурсов:

● использование процессора;

● объем чтения (страницы локального и общего кешей);

● объем записи (страницы локального и общего кешей).

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

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

https://postgrespro.ru/docs/enterprise/16/pgpro-rp

Расширение pgpro_rp в своей работе использует механизм приоритизации ресурсов, добавленный в ядро Postgres Pro Enterprise

https://postgrespro.ru/docs/enterprise/16/kernel-resources#RESOURCE-PRI ORITIZATION

Page 18: Оптимизация запросов

dump\_stat — перенос статистики планировщика

  • pg_query_state — состояние выполняющегося запроса
  • pg_hint_plan — указания оптимизатору
  • plantuner — запрет определенных индексов
    • Тема 8.
    • Оптимизация
    • запросов

Комментарии преподавателя

Расширение dump_stat позволяет после восстановления из логической копии обойтись без выполнения команды ANALYZE для всего кластера, что для больших БД может значительно сократить время простоя.

https://postgrespro.ru/docs/enterprise/16/dump-stat

Расширение pg_query_state позволяет узнать текущее состояние выполнения запросов.

https://postgrespro.ru/docs/enterprise/16/pg-query-state

Расширение pg_hint_plan позволяет управлять планом выполнения запроса с помощью специальных указаний.

https://postgrespro.ru/docs/enterprise/16/pg-hint-plan

Расширение plantuner позволяет скрывать определенные индексы от планировщика, не удаляя их.

https://postgrespro.ru/docs/enterprise/16/plantuner

Page 19: Оптимизация запросов

AQO — адаптивная оптимизация запросов

  • накопление статистики выполнений и коррекция прогноза кардинальности Перепланирование запросов в реальном времени
    • прерывание выполнения запроса и запуск повторного планирования
  • pgpro_multiplan — сохранение планов запросов
    • использование планов без повторной оптимизации идентичных запросов
    • Тема 9. Адаптивная
    • Тема 11. Сохранение
    • оптимизация
    • планов запросов
    • Тема 10.
    • Перепланирование

Комментарии преподавателя

Расширение AQO (Adaptive Query Optimization) используется для адаптивной оптимизации запросов за счет коррекции прогноза кардинальности с учетом накопленной статистики выполнения запросов.

https://postgrespro.ru/docs/enterprise/16/aqo

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

https://postgrespro.ru/docs/enterprise/16/realtime-query-replanning

Расширение pgpro_multiplan позволяет сохранять планы запросов и использовать их для идентичных запросов без повторного планирования.

https://postgrespro.ru/docs/enterprise/16/pgpro-multiplan

Page 20: Планировщик запросов

Удаление замкнутых соединений из планов

  • enable_self_join_removal
  • Оптимизированы планы с пустыми узлами Materialize
  • Уточнение стоимости последовательного сканирования
    • seq_scan_startup_cost_first_row = off
  • Уточнение селективности соединений по нескольким условиям при наличии расширенной статистики
    • enable_compound_index_stats = on
  • Уменьшено потребление памяти планировщиком в запросах с большими массивами

Комментарии преподавателя

Имеется возможность удаления замкнутых соединений из планов запросов, что может увеличить скорость выполнения (параметр enable_self_join_removal). Полезно для систем с автоматически генерируемыми запросами.

https://postgrespro.ru/docs/enterprise/16/runtime-config-query#GUC-ENABL E-SELF-JOIN-REMOVAL

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

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

https://postgrespro.ru/docs/enterprise/16/runtime-config-query#GUC-SEQ-S CAN-STARTUP-COST-FIRST-ROW

При включенном параметре enable_compound_index_stats оптимизатор использует составные индексы для уточнения оценки селективности соединения по нескольким атрибутам, не составляющим внешний ключ (в PostgreSQL в этом случае перемножаются селективности по отдельным колонкам).

Добавлена оптимизация, которая уменьшает потребление памяти планировщиком на запросах, содержащих большие массивы (например, IN для проверки вхождения значения в массив).

Page 21: Выполнение запросов

Управление объемом кеша для подготовленных операторов

  • plan_cache_lru_memsize, plan_cache_lru_size
  • Режим автоподготовки операторов
    • autoprepare_threshold, autoprepare_limit, autoprepare_memory_limit
  • Поиск ближайших соседей для B-tree
  • Ускорено сканирование только индекса для больших таблиц
  • vops — векторные операции для OLAP-запросов

Комментарии преподавателя

Параметрами plan_cache_lru_memsize и plan_cache_lru_size можно ограничить объем памяти, занимаемый подготовленными операторами. Это может помочь, если приложение создает слишком много подготовленных операторов.

https://postgrespro.ru/docs/enterprise/16/runtime-config-resource#GUC-PLA N-CACHE-LRU-MEMSIZE

Добавлен режим автоподготовки операторов (отключен по умолчанию), в котором часто используемые операторы неявно подготавливаются. Может давать преимущество в тех случаях, когда явные команды PREPARE не работают, например, когда применяется pgbouncer.

https://postgrespro.ru/docs/enterprise/16/autoprepare

  • В индексах B-tree реализована поддержка алгоритма k-NN – поиска
  • ближайших соседей – при сортировке по расстоянию. В обычном
  • PostgreSQL для этого приходится пользоваться расширением btree_gist
  • так сортировка работает гораздо медленнее.

https://postgrespro.ru/docs/enterprise/16/k-nn-search

Увеличен размер кеша карты видимости, что позволяет сканированию только индекса быстрее работать с таблицами больше 256 Мбайт.

Экспериментальное расширение vops (vectorized operations) позволяет ускорять запросы OLAP с фильтрацией и агрегированием благодаря колоночному хранению внутри страниц («паркет») и использованию векторных операций.

https://postgrespro.ru/docs/enterprise/16/vops

Page 22: Блокировки

Оптимизация механизма проверки взаимоблокировок

  • Очередь для разделяемых легких блокировок
    • lwlock_shared_limit = 0
  • Разделение таблицы блокировок на секции
    • log2_num_lock_partitions = 4
  • Уменьшена конкуренция при расширении файлов B-tree

Комментарии преподавателя

Одновременная проверка ситуации взаимоблокировки многими процессами (что может произойти на нагруженных системах с большим количеством ядер) вызывает замедление работы сервера вплоть до полной остановки. В Postgres Pro проверка ограничена одним процессом в один момент времени.

Для улучшения работы с легкими блокировками имеется параметр lwlock_shared_limit. Он задает количество разделяемых блокировок, по достижении которого запускается «справедливый» режим ожидания — последующие блокировки добавляются в очередь. В этом случае устраняется возможность бесконечного откладывания исключительных блокировок, что важно при большой конкуренции за ресурс.

https://postgrespro.ru/docs/enterprise/16/runtime-config-locks#GUC-LWLOC K-SHARED-LIMIT

Для повышения параллелизма таблица блокировок разделяется на несколько секций. Количество секций всегда является степенью двойки, поэтому задается показатель степени. В PostgreSQL он всегда равен 4, то есть таблица делится на 16 секций. В Postgres Pro Enterprise показатель можно задать параметром log2_num_lock_partitions, если при большой конкуренции значения по умолчанию не хватает.

https://postgrespro.ru/docs/enterprise/16/runtime-config-locks#GUC-LOG2-NUM-LOCK-PARTITIONS

Уменьшена конкуренция при расширении файлов B-tree: к файлу добавляется сразу несколько страниц, как при расширении таблицы.

Page 23: Буферный кеш

Более эффективная работа с горячими страницами индексов

  • Оптимизация для NUMA-архитектур
  • Уменьшена конкуренция за хеш-таблицу
    • 1024 отдельно блокируемых секций

Комментарии преподавателя

Улучшен менеджер буферного кеша:

● устранено узкое место, возникающее при обращении к горячим страницам индексов при высокой конкуренции;

● изменена внутренняя реализация хеш-таблицы буферного кеша для более эффективной работы, в том числе на NUMA-архитектурах;

● сама хеш-таблица буферного кеша разделена на 1024 секции (в PostgreSQL — 128) для снижения конкуренции на многоядерных системах.

Page 24: Поиск в тексте и в json

Метод доступа RUM

  • фразовый поиск и ранжирование
  • pg_tsparser — альтернативный анализатор
  • Hunspell — словари для ряда языков
  • shared_ispell — размещение словарей в общей памяти JsQuery — язык запросов к данным jsonb
    • поиск во вложенных объектах и массивах
    • операторы сравнения с индексной поддержкой

Комментарии преподавателя

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

https://postgrespro.ru/docs/enterprise/16/rum

Расширение pg_tsparser изменяет стандартную стратегию разбора текста для слов, включающих подчеркивания, цифры и буквы, а также для слов, разделенных знаком минуса. Оно удобно, в частности, для поиска в коде.

https://postgrespro.ru/docs/enterprise/16/pg-tsparser

Расширение Hunspell предоставляет возможность использовать популярные словари Hunspell для разных языков.

https://postgrespro.ru/docs/enterprise/16/hunspell-dict

В отличии от стандартной реализации словарей ispell, расширение shared_ispell позволяет разместить словарь в общей памяти.

https://postgrespro.ru/docs/enterprise/16/shared-ispell

Расширение JsQuery предоставляет язык запросов для поиска в значениях типа jsonb. С добавлением в PostgreSQL поддержки стандарта SQL/JSON потеряло актуальность, но сохраняется для совместимости.

https://postgrespro.ru/docs/enterprise/16/jsquery

Page 25: Анализ нагрузки

pgpro\_stats — статистика выполнения

  • Тема 12. Анализ
  • производительности
  • развитие pg_stat_statements
  • pgpro_pwr — выявление ресурсоемких операций: выборки и отчеты
    • Тема 13. Отчеты по
    • нагрузке pgpro_pwr
    • развитие pg_profile

Комментарии преподавателя

Расширение pgpro_stats позволяет собрать статистику планирования и выполнения SQL-операторов. Основано на pg_stat_statements.

https://postgrespro.ru/docs/enterprise/16/pgpro-stats

Pgpro_pwr накапливает статистические выборки (полученные на основе других расширений и результатов работы сборщика статистики), анализирует их и строит отчеты о нагрузке.

https://postgrespro.ru/docs/enterprise/16/pgpro-pwr

Page 26: Анализ нагрузки

pg\_wait\_sampling — семплирование событий ожидания

  • auto_explain — добавлено время планирования
  • pg_waldump — добавлен вывод времени
  • Определение объема сгенерированных WAL-записей
  • pgBadger — поддержка формата журнала Postgres Pro
  • Mamonsu — агент мониторинга для Zabbix
  • pgbench — учет ошибок сериализации и взаимоблокировок

Комментарии преподавателя

Расширение pg_wait_sampling семплирует события ожидания. Расширение подробно рассматривается в курсе DBA2.

https://postgrespro.ru/docs/enterprise/16/pg-wait-sampling

В auto_explain добавлено отображение длительности планирования.

https://postgrespro.ru/docs/enterprise/16/auto-explain

В pg_waldump добавлен вывод времени для различных типов журнальных записей.

https://postgrespro.ru/docs/enterprise/16/pgwaldump

Представление pgpro_stat_wal_activity отображает объем сгенерированных каждым процессом записей WAL.

https://postgrespro.ru/docs/enterprise/16/monitoring-stats#MONITORING-P GPRO-STAT-WAL-ACTIVITY-VIEW

Добавлено стороннее расширение pgBadger, адаптированное для корректной работы с Postgres Pro.

Добавлен Mamonsu — агент мониторинга Postgres Pro для Zabbix:

https://postgrespro.ru/docs/enterprise/16/mamonsu

В pgbench скорректирован учет ошибок сериализации и взаимоблокировок.

https://postgrespro.ru/docs/enterprise/16/pgbench

Page 27: Безопасность

Профили, аудит, данные

  • Сертификация ФСТЭК

Page 28: Профили, аудит, данные

passwordcheck — запрет простых паролей

  • Тема 14. Профили
  • Профили пользователя
    • пользователей
    • парольные политики и ограничения
  • pg_proaudit — аудит действий пользователей
    • Тема 15. Аудит
  • Защищенная схема — ограничение прав администраторов
    • Тема 16.
    • Защищенная схема
  • Анонимизация данных — изменение представления исходного набора данных
    • Тема 17.
    • Анонимизация
    • данных

Комментарии преподавателя

Расширение passwordcheck запрещает использование простых паролей.

https://postgrespro.ru/docs/enterprise/16/passwordcheck

Профили пользователей ограничивают использование системы, в частности, реализуют парольные политики.

https://postgrespro.ru/docs/enterprise/16/sql-createprofile

Расширение pg_proaudit позволяет проводить аудит действий пользователей.

https://postgrespro.ru/docs/enterprise/16/pg-proaudit

Рекомендуемые организационно-технические мероприятия по разграничению прав между привилегированными пользователями СУБД позволят повысить безопасность работы с Postgres Pro.

https://postgrespro.ru/docs/enterprise/16/sod-separation-of-duties

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

https://postgrespro.ru/docs/enterprise/16/pgpro-anonymizer

Page 29: Сертификация ФСТЭК

Очистка удаленных данных

  • на диске и в оперативной памяти
  • Контроль целостности файлов

Комментарии преподавателя

Работа с персональными данными и конфиденциальной информацией требует использования сертифицированной редакции Postgres Pro Enterprise Certified. Для этой редакции сделано несколько изменений, которые отсутствуют и в несертифицированной Postgres Pro Enterprise.

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

https://postgrespro.ru/docs/enterprise/16/memory-purge-cert

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

https://postgrespro.ru/docs/enterprise/16/integrity-checks

Page 30: Резервное копирование

pg\_probackup — система резервного копирования и восстановления

  • централизованный каталог копий
  • политики хранения
  • инкрементальное копирование
  • многопоточность
  • ptrack
    • механизм для получения измененных страниц данных
    • Темы 18, 19 и 20.
    • Резервное копирование

Комментарии преподавателя

pg_probackup — система резервного копирования и восстановления кластеров баз данных PostgreSQL и Postgres Pro.

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

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

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

https://postgrespro.ru/docs/enterprise/16/app-pgprobackup

Page 31: Отказоустойчивые решения

Доработки физической репликации

  • Синхронный кластер multimaster
  • Отказоустойчивый кластер BiHA

Page 32: Репликация

Нестрогая синхронная репликация

  • ограничение отставания реплики
  • synchronous_standby_gap = 0
  • Автоматическое исправление поврежденных страниц
    • страницы запрашиваются с резервного сервера
    • page_repair = off
  • Восстановление поврежденных записей WAL
    • walsender может прочитать запись из буфера в оперативной памяти
    • wal_sender_check_crc = off

Комментарии преподавателя

Нестрогая синхронная репликация позволяет временно перевести синхронную реплику в асинхронный режим при значительном отставании.

https://postgrespro.ru/docs/enterprise/16/runtime-config-replication#GUC-SY NCHRONOUS-STANDBY-GAP

При чтении страницы с неверной контрольной суммой основной сервер может запросить эту страницу с резервного сервера и автоматически восстановить ее.

https://postgrespro.ru/docs/enterprise/16/warm-standby#REPAIR-PAGE-FR OM-STANDBY

Процесс walsender может восстановить поврежденную запись WAL из буферов оперативной памяти.

https://postgrespro.ru/docs/enterprise/16/wal-restoration

Page 33: Отказоустойчивость

multimaster — синхронный кластер без разделения ресурсов

  • все узлы ведущие
  • распределенные транзакции
  • отказоустойчивость и высокая доступность
  • масштабирование читающей нагрузки
  • Тема 21.
  • Кластер multimaster
  • Отказоустойчивый кластер BiHA
    • Тема 22.
    • Кластер BiHA
    • кластер с физической репликацией
    • отказоустойчивость и высокая доступность
    • масштабирование читающей нагрузки

Комментарии преподавателя

Расширение multimaster позволяет настроить синхронный кластер, в котором любой узел может принимать запросы как на чтение, так и на запись.

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

https://postgrespro.ru/docs/enterprise/16/multimaster

Расширение BiHA позволяет организовать отказоустойчивый кластер, где изменения данных обрабатывает узел-лидер, а затем с помощью механизма физической репликации эти изменения распространяются на узлы-последователи. Последователи доступны только для чтения.

https://postgrespro.ru/docs/enterprise/16/biha

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

Также оба расширения обеспечивают масштабируемость читающих транзакций.

Page 34: Итоги

Postgres Pro Enterprise содержит разнообразные дополнительные возможности

  • изменения в коде ядра PostgreSQL
  • дополнительные расширения
  • улучшения сторонних расширений
  • Основные возможности детально рассматриваются в курсе

02 Занятие 02: Установка и сопровождение / Postgres Pro Enterprise Manager

Исходная страница в нативном Markdown: Установка и сопровождение / Postgres Pro Enterprise Manager
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_02_ppem.html Archive: PGPRO-handouts-16.zip Страниц PDF: 17

Page 1: Установка и сопровождение / Postgres Pro Enterprise Manager

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Назначение и особенности
  • Возможности
  • Архитектура
  • Установка и настройка
  • Ролевая модель доступа
  • Интерфейс веб-приложения

Page 3: Назначение и особенности

Единая консоль мониторинга и управления

  • Централизованный доступ к разным экземплярам и базам
  • Работа из окна браузера
  • Навигация по иерархии объектов
  • Ролевая модель управления доступом
  • Поддержка различных редакций PostgreSQL и Postgres Pro

Комментарии преподавателя

Postgres Pro Enterprise Manager (PPEM) — комплексное решение для администрирования инфраструктуры баз данных. PPEM предоставляет удобный интерфейс для управления и мониторинга СУБД PostgreSQL и различных редакций СУБД Postgres Pro.

https://postgrespro.ru/docs/ppem/2/index

PPEM может эксплуатироваться на операционных системах семейства Linux. Информация по совместимости с другим ПО, а также аппаратные и программные требования приведены в документации.

https://postgrespro.ru/docs/ppem/2/ppem-supported-platforms-and-operatin g-systems#PPEM-SUPPORTED-PLATFORMS-AND-OPERATING-SYSTEM S

https://postgrespro.ru/docs/ppem/2/ppem-compatibility

https://postgrespro.ru/docs/ppem/2/ppem-hardware-and-software-requireme nts#PPEM-HARDWARE-AND-SOFTWARE-REQUIREMENTS

Page 4: Возможности

Автообнаружение, настройка и управление экземплярами

  • Управление объектами баз данных
  • Мониторинг активности, производительности и профилирование
  • Регламентные операции по обслуживанию
  • Резервное копирование и восстановление
  • Прямой доступ к SQL-консоли экземпляров

Комментарии преподавателя

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

● обнаруживает экземпляры СУБД и добавляет их в веб-приложение, позволяет развернуть новые экземпляры — как одиночные, так и кластерные конфигурации;

● содержит средства просмотра и настройки параметров конфигурации без прямого редактирования файлов конфигурации;

● позволяет работать с объектами экземпляров СУБД;

● получает и отображает данные о внутренней активности СУБД от подсистемы статистики, позволяет отменять запросы и завершать процессы;

● интегрируется с pgpro-otel-collector, по предоставляемым им метрикам строятся графики для анализа работы СУБД;

● интегрирован с pg_profile и pgpro_pwr, предоставляет удобный интерфейс для работы со снимками и отчетами;

● может выполнять служебные операции (очистка, анализ, переиндексация и др.) вручную и автоматически по расписанию;

● позволяет осуществлять резервное копирование как вручную, так и по расписанию, а также создавать экземпляры из резервных копий;

● обеспечивает доступ к SQL-консоли экземпляра.

https://postgrespro.ru/docs/ppem/2/ppem-features#PPEM-FEATURES

Page 5: Архитектура

Source slide page 5: Архитектура{#source-figure-005 caption=“Source slide page 5: Архитектура”}

Комментарии преподавателя

Веб-приложение — набор компонентов, которые обеспечивают пользовательский интерфейс, доступный через браузер. Пользователь выполняет различные действия в браузере, а веб-приложение преобразует эти действия в запросы и отправляет их менеджеру.

https://postgrespro.ru/docs/ppem/2/ppem-description-of-components#PPEM -WEB-APPLICATION

Менеджер — служба, которая работает в фоновом режиме. Менеджер принимает запросы на обслуживание инфраструктуры СУБД от веб-приложения и преобразует их в операции согласно заложенной логике. Менеджер имеет собственный планировщик для выполнения регулярных служебных заданий. Веб-приложение устанавливается на сервере вместе с менеджером PPEM.

https://postgrespro.ru/docs/ppem/2/ppem-description-of-components#PPEM -MANAGER

Репозиторий — это база данных в отдельном экземпляре СУБД, в которой менеджер хранит свою служебную информацию. Во время запуска менеджер устанавливает соединение с репозиторием и при выполнении операций читает и записывает в него данные. Доступность репозитория крайне важна, без него менеджер работать не может.

https://postgrespro.ru/docs/ppem/2/ppem-description-of-components#PPEM -REPOSITORY

Page 6: Архитектура

Source slide page 6: Архитектура{#source-figure-006 caption=“Source slide page 6: Архитектура”}

Комментарии преподавателя

Агенты — это службы, которые выполняются на управляемых серверах. Агенты принимают задания от менеджера и выполняют действия как в операционной системе, так и в самом управляемом экземпляре СУБД. Действиями могут быть запуск команд, создание каталогов и файлов, выполнение запросов и т. п.

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

Выполнив операцию, менеджер сообщает веб-приложению ее результат, а приложение отображает его в браузере.

https://postgrespro.ru/docs/ppem/2/ppem-description-of-components#PPEM -AGENT

Объект управления PPEM — это экземпляр СУБД (PostgreSQL или Postgres Pro в редакциях Standard или Enterprise). На одном сервере достаточно одного агента, который может обслуживать несколько экземпляров СУБД. С точки зрения экземпляра агент является обычным клиентом, который подключается к СУБД под заранее определенной ролью.

https://postgrespro.ru/docs/ppem/2/ppem-description-of-components#PPEM -DBMS-INSTANCE

Page 7: Архитектура

Source slide page 7: Архитектура{#source-figure-007 caption=“Source slide page 7: Архитектура”}

Комментарии преподавателя

Для расширения функций и возможностей PPEM может использовать внешние сервисы:

● LDAP — служба каталогов для внешней аутентификации пользователей.

В базовом сценарии использования PPEM аутентифицирует своих пользователей самостоятельно, нужные для этого данные хранятся в репозитории. Но при необходимости можно настроить взаимодействие с внешним каталогом пользователей и групп — поддерживается работа с сервисами OpenLDAP и Microsoft Active Directory. При использовании внешней службы учетные данные сначала проверяются в ней, а затем, если они не были обнаружены, выполняется проверка в репозитории.

● S3-совместимое объектное хранилище для резервных копий.

Для создания резервных копий экземпляров и восстановления из них PPEM использует утилиту pg_probackup. Копии при этом могут сохраняться не только в локальный каталог, но и в S3-совместимое хранилище объектов. Работа с pg_probackup подробно рассматривается далее в модуле «Резервное копирование».

Page 8: Архитектура

Source slide page 8: Архитектура{#source-figure-008 caption=“Source slide page 8: Архитектура”}

Комментарии преподавателя

Другими внешними сервисами, с которыми может взаимодействовать PPEM, являются системы хранения метрик и журналов, поддерживающие спецификацию OTLP:

● Система хранения метрик с поддержкой работы по push-и/или по pull-модели. Поддерживается работа с Victoriametrics и Prometheus.

● Система хранения журналов. Поддерживается работа с Elasticsearch.

https://opentelemetry.io/docs/specs/otlp

Запись в эти хранилища выполняет служба pgpro-otel-collector. Задача коллектора — извлекать статистические данные об экземпляре из различных источников (системные представления, таблицы, журналы) и передавать их в системы хранения и мониторинга.

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

https://postgrespro.ru/docs/otelcol/current

Page 9: Установка и настройка

Менеджер

  • установка пакета
  • репозиторий
  • настройка
  • Агенты
    • установка
    • настройка
  • Особенности
    • безопасность и разделение доступа
    • работа с системами высокой доступности

Комментарии преподавателя

После установки менеджера PPEM из пакета необходимо подготовить экземпляр СУБД PPEM для хранения служебных данных. В этом экземпляре нужно создать базу данных репозитория и роль, от имени которой менеджер будет подключаться к базе. Также необходимо внести соответствующие настройки в файл конфигурации менеджера.

https://postgrespro.ru/docs/ppem/2/ppem-quick-start-installation#PPEM-QUI CK-START-INSTALLING-THE-MANAGER

Агент, будучи установленным из пакета, тоже нуждается в настройке. В служебном экземпляре СУБД нужно создать роль для подключения агента к базе репозитория. Также нужно задать в конфигурационном файле агента его уникальное имя, API-ключ и URL для подключения к менеджеру и учетные данные для доступа к базе репозитория.

https://postgrespro.ru/docs/ppem/2/ppem-quick-start-installation#PPEM-QUI CK-START-INSTALLING-THE-AGENT

В средах с усиленными мерами безопасности службы менеджера и агента могут быть настроены для запуска от имени отдельных, непривилегированных пользователей ОС:

https://postgrespro.ru/docs/ppem/2/ppem-installation-in-secure-environment s

Page 10: Настройки менеджера и агентов PPEM

Для размещения репозитория PPEM в виртуальной машине курса создан экземпляр PostgreSQL:

student$ pg_lsclusters -s 16 ppem
Ver Cluster Port Status      Owner    Data directory              Log file
16  ppem    5435 down,manual postgres /var/lib/postgresql/16/ppem
/var/log/postgresql/postgresql-16-ppem.log

Запустим экземпляр с репозиторием и сервисы PPEM.

student$ sudo pg_ctlcluster 16 ppem start
student$ sudo systemctl start ppem-agent
student$ sudo systemctl start ppem
student$ sudo systemctl start pgpro-otel-collector

В этом экземпляре есть одноименные база данных репозитория и роль:

student$ psql -p 5435 -U postgres -c '\du ppem' -c '\x' -c '\l ppem'
     List of roles
 Role name | Attributes
-----------+------------
 ppem      |
Expanded display is on.
List of databases
-[ RECORD 1 ]-----+------------
Name              | ppem
Owner             | ppem
Encoding          | UTF8
Locale Provider   | libc
Collate           | en_US.UTF-8
Ctype             | en_US.UTF-8
ICU Locale        |
ICU Rules         |
Access privileges |

Настройки менеджера заданы следующим образом (уберем комментарии и пустые строки):

student$ sudo egrep -v '^\s*($|#)' /etc/ppem-manager.yml
log:
  level: error
repo:
  host: localhost
  port: 5435
  name: ppem
  user: ppem
  password: ppem_pwd
http:
  server:
    address: "localhost"
    port: 8180
    static_files_path: "/usr/share/ppem/web-app"
    security:
      enabled: true

А агента — вот таким:

student$ sudo egrep -v '^\s*($|#)' /etc/ppem-agent.yml
agent:
  manager:
    url: "http://localhost:8180/v1"
    api_key: "15a2374b-2c7b-4b52-b576-126b9e0d8b46"
  instance:
    connection_defaults:
      host: localhost
      database: postgres
      user: postgres
  name: local
  collectors:
    host:
      interval: 5s

Page 11: Page 11

    agent:
      interval: 5s
    instances:
      interval: 5s
    instance_objects:
      interval: 5s
    instance_settings:
      interval: 5s
log:
  level: error
http:
  server:
    address: "localhost"
    port: 8181

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

Page 12: Ролевая модель доступа

Субъекты

  • пользователи и агенты
  • Объекты
    • ресурсы PPEM: сервер, агент, экземпляр, пользователь…
  • Роли
    • набор прав на один класс объектов
    • системные роли
    • объектно-привязанные роли

Комментарии преподавателя

Управление доступом в PPEM реализовано на основе ролевой модели. Субъектами являются пользователи и агенты, объектами — ресурсы самого PPEM и их представления в репозитории. Ресурсы относятся к разным классам, таким как экземпляры или задания.

Аутентификация субъектов может выполняться:

● Встроенными средствами PPEM. Данные о субъектах хранятся в репозитории и управляются администратором PPEM.

● Через внешний каталог LDAP. Данные о субъектах хранятся во внешнем каталоге, а менеджер настроен на работу с ним.

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

Роль содержит набор прав на объекты одного класса. Обычно эти права относятся ко всем объектам класса, но можно указать и конкретный объект, тогда права и роли становятся объектно-привязанными.

Существует набор предопределенных («системных») ролей, удаление и изменение которых из веб-приложения невозможно, но можно создавать дополнительные пользовательские роли с нужными правами.

https://postgrespro.ru/docs/ppem/2/ppem-role-based-access-control-rbac-m odel

Отметим, что пользователи и роли PPEM никак не связаны с ролями обслуживаемых экземпляров СУБД.

Page 13: Веб-приложение

Source slide page 13: Веб-приложение{#source-figure-013 caption=“Source slide page 13: Веб-приложение”}

Комментарии преподавателя

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

Далее, на странице авторизации, необходимо ввести логин и пароль пользователя. После этого откроется главная страница приложения, на которой представлена сводная информация о приложении и обслуживаемых ей объектах.

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

Страницы приложения, преимущественно предназначенные для работы с инфраструктурой, и страницы, предназначенные для работы непосредственно с базами данных, сгруппированы на панели навигации в отдельные разделы «Инфраструктура» и «Базы данных».

В панели инструментов также есть пиктограмма в виде колокольчика, нажав на которую можно просмотреть уведомления самого PPEM.

https://postgrespro.ru/docs/ppem/2/ppem-web-application-interface#PPEM-WEB-APPLICATION-INTERFACE

Page 14: Интерфейс веб-приложения PPEM

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

student$ psql
=> SET log_mins_duration_statement = 0; -- неправильное название параметра
ERROR:  unrecognized configuration parameter "log_mins_duration_statement"
student$ psql 'host=localhost dbname=postgres user=admin password=admin' # неверные учетные данные
psql: error: connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL:
role "admin" does not exist

Посмотрим на интерфейс веб-приложения. Его главная страница содержит информацию о количестве обслуживаемых экземпляров, кластеров, базах данных и их размере. Тут же представлены ленты журнала сообщений экземпляров и уведомлений самого PPEM. Обратите внимание на наличие навигационной панели слева и панели инструментов наверху окна. В панели «Последние сообщения» можем найти записи журнала с информацией о наших ошибках — они были прочитаны из журнала сообщений экземпляра:

student$ sudo egrep '(ERROR|SIGHUP|FATAL)' /var/lib/pgpro/ent-16/log/postgresql-2026-07-27_233312.log
2026-07-27 23:33:20.478 MSK [789390] ERROR:  unrecognized configuration parameter
"log_mins_duration_statement"
2026-07-27 23:33:20.556 MSK [789449] FATAL:  role "admin" does not exist

Открываем http://localhost:8180…

student$ open http://localhost:8180

Page 15: Итоги

PPEM — комплексное решение для администрирования инфраструктуры баз данных

  • Удобный интерфейс для управления, обслуживания и мониторинга СУБД PostgreSQL и различных редакций Postgres Pro
  • Многокомпонентная архитектура с централизованным репозиторием и агентами на управляемых серверах
  • Собственная система управления доступом на основе ролевой модели

Page 16: Практика

  1. Войдите в PPEM как admin и создайте пользователей access-adm и ent-16-adm, которые будут управлять в PPEM ролями пользователей и экземпляром ent-16. Пользователю access-adm выдайте право Access administrator role. 2. Создайте пользовательскую роль Notifications administrator c правами работы с уведомлениями. 3. Войдите в PPEM как access-adm. Назначьте пользователю ent-16-adm роль Notifications administrator и роль Instance administrator role для экземпляра ent-16. 4. Войдите в PPEM как ent-16-adm и выполните проверку целостности каталога данных экземпляра ent-16, а затем перезапустите экземпляр. Убедитесь, что эти команды отработали успешно.

Комментарии преподавателя

  1. Значок на рабочем столе «PPEM start» запускает необходимые службы и открывает приложение в окне браузера. Для первого входа в PPEM используйте логин admin, пароль admin.

  2. Для проверки откройте «Список заданий» во вкладке «Консоль заданий» навигационной панели.

Page 17: 1. Пользователи PPEM

В навигационной панели выберите вкладку «Пользователи» и нажмите кнопку «Добавить пользователя». В открывшемся окне заполните необходимые поля; для пользователя access-adm в список «Личные роли» дополнительно к «Guest role» добавьте «Access administrator role». Нажмите кнопку «Добавить».

2. Пользовательская роль

Откройте вкладку «Пользователи» в навигационной панели, а затем вкладку «Роли». Нажмите «Добавить роль» и в открывшемся окне введите системное название новой роли (правила его формирования указаны под полем ввода) и имя роли. Пользуясь строкой поиска привилегий, найдите те, что относятся к работе с уведомлениями (notification), и отметьте их. Нажмите «Далее», а затем «Добавить».

3. Выдача прав пользователю. Личные роли

Выйдите из сеанса admin, нажав кнопку с соответствующей пиктограммой в панели инструментов. В новом окне приглашения введите логин access-adm и пароль, затем нажмите «Войти». Поскольку вход с этим логином осуществляется впервые, потребуется сменить пароль. В таблице со списком пользователей выберите ent-16-adm и нажмите на пиктограмму редактирования в столбце «Действия». В открывшемся окне в список «Личные роли» добавьте «Instance administrator role» (с указанием ent-16 в поле «Объект») и «Notifications administrator», после чего нажмите «Сохранить».

4. Управление экземпляром

Выйдите из сеанса access-adm. В окне приглашения введите логин ent-16-adm и пароль, затем нажмите «Войти» и смените пароль согласно требованию. Перейдите на главную страницу: наверху навигационной панели нажмите «Главная» или щелкните по значку с эмблемой PPEM (со слоном) в левом верхнем углу. Откройте список экземпляров, щелкнув по блоку «Экземпляры», и затем нажмите на пиктограмму «Проверить целостность каталога» напротив ent-16. Дождитесь появления уведомления в панели инструментов на пиктограмме с изображением колокольчика (красная точка) и, нажав на нее, убедитесь, что проверка завершена. Перезапустите экземпляр ent-16, нажав на соответствующую пиктограмму. Перейдите на главную страницу, выберите в навигационной панели вкладку «Консоль заданий» и убедитесь, что перезапуск выполнен.

03 Занятие 03: Установка и сопровождение / Установка, настройка, обновление

Исходная страница в нативном Markdown: Установка и сопровождение / Установка, настройка, обновление
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_03_install.html Archive: PGPRO-handouts-16.zip Страниц PDF: 23

Page 1: Установка и сопровождение / Установка, настройка, обновление

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Установка и начальная настройка
  • Обновление сервера

Page 3: Установка и настройка

Архитектуры и ОС

  • Пакеты Postgres Pro
  • Настройка Postgres Pro
  • Информация о сервере
  • Нумерация версий

Page 4: Архитектуры и ОС

64-разрядные архитектуры

  • AMD/Intel 64 (x86_64)
  • ARM64 (aarch64)
  • Эльбрус 2000 (e2kv3, e2kv4)
  • Power PC, Power ISA (ppc64le)
  • OC на базе Linux
    • Red Hat Enterprise Linux, CentOS, Rocky Linux, Oracle Linux,
    • Rosa/РОСА, РЕД ОС, AlterOS, AlmaLinux
    • Debian, Ubuntu, Astra Linux
    • Альт
    • SUSE Linux Enterprise Server

Комментарии преподавателя

Postgres Pro может работать на серверных операционных системах на базе Linux. Поддерживаются только 64-разрядные архитектуры.

Список поддерживаемых систем можно посмотреть в документации:

https://postgrespro.ru/docs/enterprise/16/binary-installation-on-linux#SUPPO RTED-LINUX-OS

Платформа Windows поддерживалась в версиях до 14 включительно.

Page 5: Пакеты Postgres Pro

Пакет верхнего уровня

  • postgrespro-ent-16
  • все доступные компоненты сервера
  • автоматическая настройка
  • Детальный выбор пакетов
    • postgrespro-ent-16-* и другие
    • только необходимые компоненты
    • ручная настройка
  • Сторонние пакеты
    • дополнительные расширения и утилиты

Комментарии преподавателя

Программное обеспечение Postgres Pro Enterprise поставляется в двоичных пакетах.

Пакет верхнего уровня postgrespro-ent-16 позволяет быстро установить и настроить Postgres Pro Enterprise для серверных и клиентских систем (будут установлены все доступные компоненты без возможности выбора). Такой способ установки не рекомендуется при обновлении или миграции системы.

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

Пакеты называются одинаково для всех операционных систем (кроме незначительных отличий в суффиксе названия пакетов для разработчиков postgrespro-ent-16-dev* и пакетов с отладочной информацией postgrespro-ent-16-dbg*).

Существуют также сторонние пакеты и библиотеки, собранные специально для Postgres Pro Enterprise. В них реализованы необходимые функции, содержатся дополнительные утилиты и расширения: в libzstd — функции сжатия данных, oracle-fdw-ent-16 — обертка сторонних данных для Oracle, pgpro-pgbadger — анализатор журналов сообщений и др.

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

https://postgrespro.ru/docs/enterprise/16/binary-installation-on-linux#CHOO SING-PGPRO-PACKAGES

Page 6: Настройка Postgres Pro

pg-wrapper — управление ссылками

  • путь поиска PATH и страницы man
  • pg-setup — управление кластером
    • инициализация кластера
    • проверка доступных портов
    • изменение конфигурации сервера
    • включение и отключение автоматического запуска службы
    • запуск, остановка или перезапуск службы
    • настройка кластера
  • Проверка контрольных сумм
    • включена по умолчанию

Комментарии преподавателя

В данном курсе рассматривается настройка и работа Postgres Pro Enterprise в среде Linux.

Для добавления ссылок в путь поиска PATH и для подключения man-страниц используется скрипт pg-wrapper.

https://postgrespro.ru/docs/enterprise/16/pg-wrapper

Для инициализации кластера Postgres Pro Enterprise рекомендуется скрипт pg-setup. Он может находить доступные для установки сервера порты, инициализировать и настраивать экземпляры Postgres Pro Enterprise. Запускать pg-setup нужно от имени пользователя root, а действия с СУБД скрипт выполняет под ролью postgres.

https://postgrespro.ru/docs/enterprise/16/pg-setup

Скрипт также позволяет запустить сервер, остановить его и настроить автоматический запуск.

https://postgrespro.ru/docs/enterprise/16/binary-installation-on-linux#STARTI NG-SERVER-AUTOMATICALLY

По умолчанию в кластере:

● включаются подсчет и проверка контрольных сумм;

● запускается утилита pgpro_tune, которая добавляет в конфигурацию экземпляра настройки, оптимизирующие работу на конкретном оборудовании;

● поддерживается библиотека ICU, в которой реализована платформонезависимая сортировка.

https://postgrespro.ru/docs/enterprise/16/collation#COLLATION-MANAGING

Page 7: Информация о сервере

pgpro\_controldata

  • вывод информации о кластере и/или сервере
  • чтение младших мажорных версий и редакций
  • сравнение параметров сервера, влияющих на совместимость
  • SQL-функции
    • проверка информации о системе после установки
    • версия и редакция Postgres Pro
    • идентификатор состояния исходного кода
  • pg_config
    • версия и редакция Postgres Pro Enterprise

Комментарии преподавателя

С помощью утилиты pgpro_controldata можно вывести управляющую информацию о кластере PostgreSQL или Postgres Pro Enterprise и параметры совместимости кластера. Без указания параметров утилита pgpro_controldata работает аналогично утилите pg_controldata. Устанавливается отдельным пакетом.

https://postgrespro.ru/docs/enterprise/16/app-pgprocontroldata

Если Postgres Pro Enterprise уже установлен, с помощью SQL-функций можно узнать версию кластера, название редакции и идентификатор состояния исходного кода (хеш коммита), из которого был собран пакет — эти сведения пригодятся для обращения в техподдержку.

https://postgrespro.ru/docs/enterprise/16/functions-info#FUNCTIONS-INFO-SESSION-TABLE

Аналогичная информация выводится и с помощью утилиты pg_config.

https://postgrespro.ru/docs/enterprise/16/app-pgconfig

Page 8: Проверка системы после установки

В виртуальной машине курса установлен пакет postgrespro-ent-16-server, исполняемые файлы находятся в каталоге /opt/pgpro/ent-16/bin/:

student$ ls -C /opt/pgpro/ent-16/bin/
bihactl       oid2name
 pg_dump
  pg_resetwal
   pg_waldump
check-db-dir  pg_amcheck
 pg_dumpall
  pg_restore
   pg-wrapper
clusterdb     pg_archivecleanup  pg_dump_anon.sh  pg_rewind
   postgres
createdb      pg_basebackup
 pg_isready
  pg-setup
   psql
createuser    pgbench
 pg_probackup
  pg_test_fsync    reindexdb
dropdb       pg_checksums
 pgpro_tune
  pg_test_timing   vacuumdb
dropuser      pg_controldata
 pg_receivewal   pg_upgrade
   vacuumlo
initdb       pg_ctl
 pg_recvlogical   pg_verifybackup

Для инициализации кластера была использована утилита pg-setup. Ключ –no-tune отключает применение дополнительных настроек:

student$ sudo /opt/pgpro/ent-16/bin/pg-setup initdb -g --no-tune -D /var/lib/pgpro/ent-16 --auth=trust

Этой же утилитой pg-setup была настроена служба с автозапуском, так что экземпляром можно управлять в стиле ОС:

student$ sudo systemctl status postgrespro-ent-16.service
● postgrespro-ent-16.service - Postgres Pro ent 16 database server
     Loaded: loaded (/usr/lib/systemd/system/postgrespro-ent-16.service; enabled; preset:
enabled)
     Active: active (running) since Mon 2026-07-27 23:33:31 MSK; 2s ago
   Main PID: 789676 (postgres)
      Tasks: 9 (limit: 3478)
     Memory: 48.6M (peak: 48.9M)
        CPU: 102ms
     CGroup: /system.slice/postgrespro-ent-16.service
             ├─789676 /opt/pgpro/ent-16/bin/postgres -D /var/lib/pgpro/ent-16
             ├─789677 "postgres: logger "
             ├─789678 "postgres: checkpointer "
             ├─789679 "postgres: background writer "
             ├─789681 "postgres: walwriter "
             ├─789682 "postgres: autovacuum launcher "
             ├─789683 "postgres: cfs-worker-0 "
             ├─789684 "postgres: logical replication launcher "
             └─789869 "postgres: student student [local] idle"
июл 27 23:33:31 course systemd[1]: Starting postgrespro-ent-16.service - Postgres Pro
ent 16 database server...
июл 27 23:33:31 course postgres[789676]: 2026-07-27 23:33:31.254 MSK [789676] LOG:
Start 1 background garbage collection workers for CFS
июл 27 23:33:31 course postgres[789676]: 2026-07-27 23:33:31.287 MSK [789676] LOG:
Start CFS version 0.55 supported compression algorithms pglz,zlib,lz4,zstd GC enabled
июл 27 23:33:31 course postgres[789676]: 2026-07-27 23:33:31.306 MSK [789676] LOG:
redirecting log output to logging collector process
июл 27 23:33:31 course postgres[789676]: 2026-07-27 23:33:31.306 MSK [789676] HINT:
Future log output will appear in directory "log".
июл 27 23:33:31 course systemd[1]: Started postgrespro-ent-16.service - Postgres Pro
ent 16 database server.

Статус можно узнать и с помощью традиционной утилиты pg_ctl:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16 \
  status
pg_ctl: server is running (PID: 789676)
/opt/pgpro/ent-16/bin/postgres "-D" "/var/lib/pgpro/ent-16"

Для запуска, остановки и перезапуска экземпляра сервера рекомендуется использовать те средства, которые уже используются в вашей компании. Информацию об установленной системе (номер версии и название редакции) можно узнать разными способами, например, с помощью утилиты pg_config или SQL-функций:

=> \pset xheader_width 90

Page 9: Page 9

Expanded header width is 90.
=> SELECT pgpro_edition(), pgpro_version() \gx
-[ RECORD 1 ]-+---------------------------------------------------------------------------
pgpro_edition | enterprise
pgpro_version | Postgres Pro (enterprise) 16.14.1 on x86_64-pc-linux-gnu, compiled by gcc
(Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0, 64-bit

Можно вывести значения конфигурационных параметров SQL-командой SHOW, как это делается в обычном PostgreSQL:

=> SHOW data_checksums;
 data_checksums
----------------
 on
(1 row)
=> SHOW data_directory;
    data_directory
-----------------------
 /var/lib/pgpro/ent-16
(1 row)

Файл postgresql.conf находится в каталоге данных:

=> SHOW config_file;
              config_file
---------------------------------------
 /var/lib/pgpro/ent-16/postgresql.conf
(1 row)

В виртуальной машине в основной конфигурационный файл Postgres Pro Enterprise добавлена директива include_dir для включения дополнительных файлов:

student$ sudo grep '^include_dir' /var/lib/pgpro/ent-16/postgresql.conf
include_dir='conf.d'
=> \q

Page 10: Нумерация версий

Source slide page 10: Нумерация версий{#source-figure-010 caption=“Source slide page 10: Нумерация версий”}

16 14 1 . .

  • основная версия PostgreSQL
  • дополнительный выпуск PostgreSQL
    • выпуск Postgres Pro Enterprise

Комментарии преподавателя

Номер версии Postgres Pro Enterprise состоит из трех частей: номера основной версии (major release), номера дополнительного выпуска (minor release) PostgreSQL и номера выпуска Postgres Pro Enterprise.

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

Дополнительные выпуски служат для переноса изменений, сделанных в соответствующем выпуске PostgreSQL, а также для исправления ошибок, проблем безопасности и производительности, найденных в предыдущих выпусках.

Выпуски Postgres Pro Enterprise служат исключительно для исправления ошибок, проблем безопасности и производительности, найденных в предыдущих выпусках Postgres Pro Enterprise.

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

https://postgrespro.ru/docs/enterprise/16/upgrading

Page 11: Обновление сервера

Обновление Postgres Pro Enterprise

  • Переход с PostgreSQL и редакций Postgres Pro

Page 12: Обновление Enterprise

Обновление в пределах основной версии

  • доп. выпуск PostgreSQL или выпуск Postgres Pro Enterprise
  • установить исполняемые файлы и перезапустить сервер
  • Обновление основной версии
    • pg_dump + pg_restore
    • pg_upgrade

Комментарии преподавателя

В целом процедура обновления сервера Postgres Pro Enterprise аналогична процедуре обновления PostgreSQL. Внимательно прочитайте замечания ко всем выпускам между текущим и целевым.

В любом случае сначала устанавливаются пакеты новой версии или выпуска.

При обновлении на дополнительный выпуск или выпуск Postgres Pro Enterprise после этого достаточно перезапустить сервер.

При обновлении основной версии для переноса данных есть два пути:

● сделать логическую резервную копию утилитами pg_dumpall или pg_dump и восстановить данные с помощью pg_restore / psql;

● воспользоваться утилитой pg_upgrade.

Если статистику планировщика запросов не переносили из старой версии, после переноса данных нужно выполнить анализ.

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

Page 13: Варианты перехода

Source slide page 13: Варианты перехода{#source-figure-013 caption=“Source slide page 13: Варианты перехода”}

Поддерживаются pg_dump+pg_restore и pg_upgrade Понижение версии не поддерживается

Комментарии преподавателя

В иерархии PostgreSQL < Postgres Pro Standard < Postgres Pro Enterprise возможен перенос данных с младшей на более старшую редакцию СУБД. При этом поддерживается как перенос с помощью логического резервирования и восстановления (pg_dump + pg_restore), так и перенос с помощью утилиты pg_upgrade. Возможные пути переноса и ограничения показаны на рисунке.

При этом возможен переход только на ту же или бóльшую основную версию СУБД, понижение версии (downgrade) не поддерживается.

Формат табличных страниц Postgres Pro Enterprise отличается от формата PostgreSQL и Postgres Pro Standard (подробности в теме «Транзакции и сеансы»). Однако непосредственно при обновлении pg_upgrade конвертация всех страниц не выполняется — это слишком затратная процедура. Такое преобразование выполняется позже постепенно — страницы конвертируются при первом обращении к ним после обновления системы.

Page 14: Перенос данных из PostgreSQL

При миграции из PostgreSQL можно использовать средства логического резервного копирования или утилиту pg_upgrade. В виртуальной машине установлен PostgreSQL 16 и инициализирован кластер prod. Запустим экземпляр.

student$ sudo pg_ctlcluster 16 prod start

В отдельной базе данных создадим таблицу:

student$ psql -p 5433
=> CREATE DATABASE pgpro_install;
CREATE DATABASE
=> \c pgpro_install
You are now connected to database "pgpro_install" as user "student".
=> CREATE TABLE test(s) AS VALUES('Строка из PostgreSQL');
SELECT 1

Остановим экземпляр PostgreSQL.

student$ sudo pg_ctlcluster 16 prod stop

Будем переносить данные с помощью pg_upgrade. Нам понадобится пустой кластер Postgres Pro Enterprise.

student$ sudo mkdir /var/lib/pgpro/ent-16-prod
student$ sudo chown postgres: /var/lib/pgpro/ent-16-prod
student$ sudo -u postgres /opt/pgpro/ent-16/bin/initdb \
  -D /var/lib/pgpro/ent-16-prod --auth=trust
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.
initdb: --icu-locale not specified, using lc_collate
Using language tag "en-US" for ICU locale "en_US.UTF-8".
The database cluster will be initialized with this locale configuration:
  provider:    icu
  ICU locale:  en-US
  LC_COLLATE:  en_US.UTF-8
  LC_CTYPE:    en_US.UTF-8
  LC_MESSAGES: en_US.UTF-8
  LC_MONETARY: ru_RU.UTF-8
  LC_NUMERIC:  ru_RU.UTF-8
  LC_TIME:     ru_RU.UTF-8
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".
Data page checksums are enabled.
fixing permissions on existing directory /var/lib/pgpro/ent-16-prod ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting default time zone ... Europe/Moscow
creating configuration files ... ok
performing automatic tuning ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok
Success. You can now start the database server using:
    /opt/pgpro/ent-16/bin/pg_ctl -D /var/lib/pgpro/ent-16-prod -l logfile start

Для начала запустим утилиту в режиме проверки. Ключами -b и -B нужно указать пути к исполняемым файлам нового и старого кластера, а ключами -d и -D — пути к postgresql.conf.

postgres$ /opt/pgpro/ent-16/bin/pg_upgrade --check \
  -b /usr/lib/postgresql/16/bin/ \
  -B /opt/pgpro/ent-16/bin/ \

Page 15: Page 15

  -d /etc/postgresql/16/prod \
  -D /var/lib/pgpro/ent-16-prod
Finding the real data directory for the source cluster        ok
Performing Consistency Checks
-----------------------------
Checking cluster versions                                     ok
Checking cluster editions                                     ok
Checking database user is the install user                    ok
Checking database connection settings                         ok
Checking for prepared transactions                            ok
Checking for system-defined composite types in user tables    ok
Checking for reg* data types in user tables                   ok
Checking for contrib/isn with bigint-passing mismatch         ok
Checking for not-null constraint inconsistencies              ok
Checking for incompatible "xid" data type                     ok
Checking for hash indexes                                     ok
Checking for spgist indexes                                   ok
Checking for brin indexes                                     ok
Checking for external indexes                                 ok
Checking for presence of required libraries                   ok
Checking database user is the install user                    ok
Checking for prepared transactions                            ok
Checking for new cluster tablespace directories               ok
*Clusters are compatible*

Все в порядке, можно переносить данные.

postgres$ /opt/pgpro/ent-16/bin/pg_upgrade \
  -b /usr/lib/postgresql/16/bin/ \
  -B /opt/pgpro/ent-16/bin/ \
  -d /etc/postgresql/16/prod \
  -D /var/lib/pgpro/ent-16-prod
Finding the real data directory for the source cluster        ok
Performing Consistency Checks
-----------------------------
Checking cluster versions                                     ok
Checking cluster editions                                     ok
Checking database user is the install user                    ok
Checking database connection settings                         ok
Checking for prepared transactions                            ok
Checking for system-defined composite types in user tables    ok
Checking for reg* data types in user tables                   ok
Checking for contrib/isn with bigint-passing mismatch         ok
Checking for not-null constraint inconsistencies              ok
Checking for incompatible "xid" data type                     ok
Checking for hash indexes                                     ok
Checking for spgist indexes                                   ok
Checking for brin indexes                                     ok
Checking for external indexes                                 ok
Creating dump of global objects                               ok
Creating dump of database schemas                             ok
Checking for presence of required libraries                   ok
Checking database user is the install user                    ok
Checking for prepared transactions                            ok
Checking for new cluster tablespace directories               ok
If pg_upgrade fails after this point, you must re-initdb the
new cluster before continuing.
Performing Upgrade
------------------
Setting locale and encoding for new cluster                   ok
Analyzing all rows in the new cluster                         ok
Freezing all rows in the new cluster                          ok
Transforming commit log segments                              ok
Setting oldest XID for new cluster                            ok
Setting next transaction ID and epoch for new cluster         ok
Deleting files from new pg_multixact/offsets                  ok
Converting pg_multixact/offsets to new format                 ok
Deleting files from new pg_multixact/members                  ok
Converting pg_multixact/members to new format                 ok
Setting next multixact ID and offset for new cluster          ok
Resetting WAL archives                                        ok
Setting frozenxid and minmxid counters in new cluster         ok
Restoring global objects in the new cluster                   ok
Restoring database schemas in the new cluster                 ok
Setting minmxid counter in new cluster                        ok

Page 16: Page 16

Copying user relation files                                   ok
Setting next OID for new cluster                              ok
Sync data directory to disk                                   ok
Creating script to delete old cluster                         ok
Checking for hash indexes                                     ok
Checking for spgist indexes                                   ok
Checking for brin indexes                                     ok
Checking for external indexes                                 ok
Checking for extension updates                                ok
Upgrade Complete
----------------
Optimizer statistics are not transferred by pg_upgrade.
Once you start the new server, consider running:
    /opt/pgpro/ent-16/bin/vacuumdb --all --analyze-in-stages
Running this script will delete the old cluster's data files:
    ./delete_old_cluster.sh

Добавим директиву include_dir в основной конфигурационный файл и создадим каталог для дополнительных файлов:

student$ echo include_dir=\'conf.d\' | sudo tee -a /var/lib/pgpro/ent-16-prod/postgresql.conf
include_dir='conf.d'
student$ sudo mkdir /var/lib/pgpro/ent-16-prod/conf.d
student$ sudo chown postgres /var/lib/pgpro/ent-16-prod/conf.d

Задаем порт в отдельном конфигурационном файле и запускаем сервер Postgres Pro Enterprise.

/var/lib/pgpro/ent-16-prod/conf.d/install.conf
port = 5434
postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-prod \
  -l /var/lib/pgpro/ent-16-prod/pgpro-ent.log \
  start
waiting for server to start.... done
server started
student$ psql -p 5434 -d pgpro_install
=> \pset xheader_width 80
Expanded header width is 80.
=> SELECT pgpro_version() \gx
-[ RECORD 1 ]-+-----------------------------------------------------------------
pgpro_version | Postgres Pro (enterprise) 16.14.1 on x86_64-pc-linux-gnu, compiled by gcc
(Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0, 64-bit
=> SELECT * FROM test;
          s
----------------------
 Строка из PostgreSQL
(1 row)

Данные перенесены в Postgres Pro Enterprise.

Page 17: Итоги

Postgres Pro Enterprise можно установить «по умолчанию» или выборочно

  • PostgreSQL и Postgres Pro Standard можно обновить до Postgres Pro Enterprise Postgres Pro Enterprise имеет собственные административные утилиты и функции

Page 18: Практика

  1. Какой размер имеет сейчас буферный кеш сервера Postgres Pro Enterprise? Увеличьте его на 10 % с помощью команды ALTER SYSTEM. 2. Настройте журнал сообщений так, чтобы в него попадала информация обо всех выполняемых запросах. Запустите стандартный тест pgbench на 90 секунд. Найдите в журнале сообщений информацию о выполненных запросах. Проанализируйте журнал с помощью утилиты pgbadger. 3. Отключите запись информации, настроенную в п.2.

Комментарии преподавателя

  1. Создайте .conf-файл с произвольным именем в подкаталоге conf.d каталога PGDATA кластера и поместите в него необходимые настройки конфигурационных параметров. Содержимое этого файла будет учтено в конфигурации благодаря директиве include_dir, находящейся в конце конфигурационного файла postgresql.conf.

  2. Удалите конфигурационный файл, созданный в п.2

Page 19: 1. Размер кеша

Текущее значение параметра:

=> SHOW shared_buffers;
 shared_buffers
----------------
 128MB
(1 row)

Задаем на 10% больше:

=> ALTER SYSTEM SET shared_buffers='141 MB';
ALTER SYSTEM

Перезапуск и проверка:

=> \q
student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql
=> SHOW shared_buffers;
 shared_buffers
----------------
 141MB
(1 row)

2. Тест pgbench и анализ журнала сообщений

Инициализируем pgbench в отдельной базе данных

=> CREATE DATABASE pgpro_install;
CREATE DATABASE
student$ /opt/pgpro/ent-16/bin/pgbench -i pgpro_install
dropping old tables...
NOTICE:  table "pgbench_accounts" does not exist, skipping
NOTICE:  table "pgbench_branches" does not exist, skipping
NOTICE:  table "pgbench_history" does not exist, skipping
NOTICE:  table "pgbench_tellers" does not exist, skipping
creating tables...
generating data (client-side)...
100000 of 100000 tuples (100%) done (elapsed 0.19 s, remaining 0.00 s)
vacuuming...
creating primary keys...
done in 0.79 s (drop tables 0.00 s, create tables 0.10 s, client-side generate 0.31 s,
vacuum 0.17 s, primary keys 0.20 s).

Включаем вывод в журнал:

/var/lib/pgpro/ent-16/conf.d/install.conf
log_min_duration_statement = 0
student$ sudo systemctl reload postgrespro-ent-16.service

Проверяем значение параметра:

=> SHOW log_min_duration_statement;
 log_min_duration_statement
----------------------------
 0
(1 row)

Запускаем тест на 90 секунд.

student$ /opt/pgpro/ent-16/bin/pgbench -T 90 -P 10 pgpro_install

Page 20: Page 20

pgbench (16.14)
starting vacuum...end.
progress: 10.0 s, 103.3 tps, lat 9.673 ms stddev 8.356, 0 failed
progress: 20.0 s, 102.6 tps, lat 9.728 ms stddev 8.196, 0 failed
progress: 30.0 s, 105.0 tps, lat 9.535 ms stddev 2.439, 0 failed
progress: 40.0 s, 106.8 tps, lat 9.353 ms stddev 1.480, 0 failed
progress: 50.0 s, 106.5 tps, lat 9.397 ms stddev 1.521, 0 failed
progress: 60.0 s, 105.0 tps, lat 9.519 ms stddev 1.876, 0 failed
progress: 70.0 s, 104.4 tps, lat 9.575 ms stddev 1.351, 0 failed
progress: 80.0 s, 108.6 tps, lat 9.207 ms stddev 2.021, 0 failed
progress: 90.0 s, 110.8 tps, lat 9.024 ms stddev 1.082, 0 failed
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
maximum number of tries: 1
duration: 90 s
number of transactions actually processed: 9531
number of failed transactions: 0 (0.000%)
latency average = 9.441 ms
latency stddev = 4.147 ms
initial connection time = 6.361 ms
tps = 105.902830 (without initial connection time)

Файлы журнала сообщений находятся в /var/lib/pgpro/ent-16/log:

student$ sudo bash -c 'cd /var/lib/pgpro/ent-16/log; ls -Ct *.log'
postgresql-2026-07-28_000640.log  postgresql-2026-07-14_110515.log
postgresql-2026-07-28_000636.log

Несколько последних записей журнала:

student$ sudo tail /var/lib/pgpro/ent-16/log/postgresql-2026-07-28_000640.log
2026-07-28 00:08:18.488 MSK [870799] LOG:  duration: 0.119 ms  statement: UPDATE
pgbench_branches SET bbalance = bbalance + -1932 WHERE bid = 1;
2026-07-28 00:08:18.489 MSK [870799] LOG:  duration: 0.098 ms  statement: INSERT INTO
pgbench_history (tid, bid, aid, delta, mtime) VALUES (10, 1, 62451, -1932,
CURRENT_TIMESTAMP);
2026-07-28 00:08:18.496 MSK [870799] LOG:  duration: 7.082 ms  statement: END;
2026-07-28 00:08:18.496 MSK [870799] LOG:  duration: 0.024 ms  statement: BEGIN;
2026-07-28 00:08:18.497 MSK [870799] LOG:  duration: 0.246 ms  statement: UPDATE
pgbench_accounts SET abalance = abalance + -2069 WHERE aid = 16482;
2026-07-28 00:08:18.497 MSK [870799] LOG:  duration: 0.156 ms  rows: 1  size: 11 bytes
statement: SELECT abalance FROM pgbench_accounts WHERE aid = 16482;
2026-07-28 00:08:18.497 MSK [870799] LOG:  duration: 0.177 ms  statement: UPDATE
pgbench_tellers SET tbalance = tbalance + -2069 WHERE tid = 5;
2026-07-28 00:08:18.498 MSK [870799] LOG:  duration: 0.119 ms  statement: UPDATE
pgbench_branches SET bbalance = bbalance + -2069 WHERE bid = 1;
2026-07-28 00:08:18.498 MSK [870799] LOG:  duration: 0.089 ms  statement: INSERT INTO
pgbench_history (tid, bid, aid, delta, mtime) VALUES (5, 1, 16482, -2069,
CURRENT_TIMESTAMP);
2026-07-28 00:08:18.505 MSK [870799] LOG:  duration: 7.151 ms  statement: END;

pgBadger уже установлен в виртуальной машине:

student$ pgbadger --version
pgBadger version 13.2

Утилите нужно передать: имя html-файла для отчета; формат журнала сообщений; файл журнала или список файлов. Запускаем:

student$ sudo pgbadger --outfile=/tmp/out.html \
  --format=stderr --no-progressbar \
  /var/lib/pgpro/ent-16/log/postgresql-2026-07-28_000640.log
LOG: Ok, generating html report...

Утилита сгенерировала отчет в файле out.html:

student$ ls -l /tmp/out.html
-rw-r--r-- 1 root root 984269 июл 28 00:08 /tmp/out.html

Заглянем в отчет:

Page 21: 3. Восстановление конфигурации

Открываем /tmp/out.html…

student$ open /tmp/out.html

Удалим созданный файл и перечитаем конфигурацию:

student$ sudo rm -f /var/lib/pgpro/ent-16/conf.d/install.conf
student$ sudo systemctl reload postgrespro-ent-16.service

Уборка

Удалим созданный отчет.

student$ sudo rm -f /tmp/out.html

Page 22: Практика PPEM

  1. Войдите в веб-приложение PPEM как admin и найдите все параметры основного экземпляра, значения которых отличаются от значений по умолчанию. 2. Выясните текущее значение параметра shared_buffers. В каком конфигурационном файле находится строка, устанавливающая это значение? Увеличьте значение в два раза и примените это изменение. 3. Создайте новый экземпляр PostgresPro Enterprise с каталогом данных в /var/lib/pgpro/ent-16-opt. Запустите экземпляр и убедитесь, что он работает.

Page 23: 1. Параметры со значениями, отличными от значений по умолчанию

На главной странице в блоке «Экземпляры» выберите ent-16. В навигационной панели выберите вкладку «Параметры» и установите соответствующий переключатель на главном экране.

2. Значение параметра shared\_buffers

В «Параметрах» найдите shared_buffers. Под названием параметра будет информация, указывающую на строку и файл, где установлено это значение. В поле ввода укажите новое значение параметра и нажмите «Сохранить». Для применения нового значения потребуется перезапуск экземпляра; его нужно дополнительно подтвердить.

3. Создание и запуск нового экземпляра

В блоке «Экземпляры» нажмите «Добавить экземпляр», а затем в открывшемся окне выберите «Создать новый экземпляр» и нажмите «Далее». В новом окне заполните необходимые поля и нажмите «Выполнить». Убедитесь, что инициализация нового экземпляра завершена — в списке он должен иметь статус «Остановлен» — и затем нажмите «Запустить» и дождитесь изменения статуса. Чтобы проверить, что создание и запуск успешны, можно воспользоваться списком заданий. Для этого нажмите на пиктограмму колокольчика в панели инструментов.

04 Занятие 04: Установка и сопровождение / Миграция

Исходная страница в нативном Markdown: Установка и сопровождение / Миграция
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_04_migration.html Archive: PGPRO-handouts-16.zip Страниц PDF: 22

Page 1: Установка и сопровождение / Миграция

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Варианты и сложности миграции
  • Миграция данных
  • Миграция объектов бизнес-логики
  • Реализация системных пакетов из Oracle
  • Пакеты в Postgres Pro Enterprise
  • Утилита ora2pgpro

Page 3: Варианты и сложности

Миграция на Postgres Pro Enterprise с других СУБД

  • Oracle
  • MS SQL Server
  • MySQL
  • PostgreSQL
  • Задачи и сложности
    • перенос схемы и данных: различия в типах, время переноса больших
    • объемов
    • перенос кода: хранимый код может сильно зависеть от СУБД
    • перенос нестандартного функционала

Комментарии преподавателя

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

● несоответствие типов переносимых данных в исходной и целевой СУБД;

● время переноса серьезных объемов данных может оказаться достаточно большим;

● трудоемкость переноса хранимого кода, в том числе из-за различий языков и их диалектов;

● сложность переноса функционала, зависящего от СУБД (например такого, как автономные транзакции и пакеты из СУБД Oracle).

Применительно к предмету курса, одним из популярных вариантов является миграция из Oracle в Postgres Pro Enterprise. Рассмотрению этой задачи посвящена почти вся эта тема.

https://postgrespro.ru/docs/enterprise/16/pgpro-migration-tools

Page 4: Миграция объектов

Объекты SQL

  • таблицы, индексы, представления, последовательности
  • схемы
  • Подпрограммы
    • процедуры, функции, триггеры
  • Другие объекты и нестандартный функционал
    • пакеты
    • автономные транзакции

Комментарии преподавателя

Миграция стандартных объектов SQL, таких как таблицы, индексы, представления и последовательности, относительно проста и поддается автоматизации.

Сложной может оказаться миграция хранимого кода — функций, процедур, триггеров — из-за разнообразия логики, а также различий в используемых языках и модулях.

Миграция других объектов — тоже сложная задача, поскольку реализация многих из них в Postgres Pro Enterprise может отличаться от других СУБД или вовсе не иметь аналогов. Например, пакеты, которые появились в версии 15, не являются прямыми аналогами пакетов Oracle — их синтаксис и возможности различаются.

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

Page 5: Миграция данных

SQL

  • выгрузка + psql
  • Обертки сторонних данных
    • универсальные для СУБД: ODBC, JDBC
    • для реляционных СУБД: Oracle (oracle_fdw), MS SQL (tds_fdw), MySQL,
    • DB2, SQLite
    • для NoSQL-СУБД: MongoDB, Redis, Cassandra
    • файловые: file_fdw, CSV, JSON

Комментарии преподавателя

При миграции данных из других СУБД можно использовать SQL как промежуточный формат. Конечно, это самый универсальный подход, но на практике при значительных объемах данных он неприменим. К тому же из-за разницы в диалектах SQL может потребоваться дополнительное преобразование данных и команд.

Для удобства работы со скриптами, часто применяемыми при такой миграции, утилита psql в Postgres Pro Enterprise может передавать им при вызове именованные и позиционные параметры.

https://postgrespro.ru/docs/enterprise/16/app-psql#APP-PSQL-META-COM MAND-INCLUDE

Дополнительные удобства и более высокую производительность можно получить, используя механизм оберток сторонних данных (foreign data wrappers), встроенный в PostgreSQL и позволяющий обращаться к внешним данным как к локальным таблицам. Штатная обертка file_fdw позволяет читать данные из файлов различных форматов. В составе дистрибутива Postgres Pro Enterprise присутствует пакет oracle-fdw-ent-16 с расширением oracle_fdw, устанавливающий одноименную обертку, и пакет tds-fdw-ent-16 для подключения к Microsoft SQL Server. Помимо этого, существует множество сторонних оберток для различных СУБД и форматов файлов.

https://wiki.postgresql.org/wiki/Foreign\_data\_wrappers

Page 6: Параметры при вызове скрипта в утилите psql

student$ psql demo

Начиная с 15-й версии Postgres Pro Enterprise поддерживается передача именованных и позиционных параметров при вызове скрипта в утилите psql. Если передается «имя=значение», создается переменная с указанным именем. Если указано только значение, оно присваивается позиционной переменной с именем PSQL_ARG1, PSQL_ARG2 и т. д. Именованные и позиционные параметры можно использовать одновременно. Для демонстрации создадим файл example.sql:

/home/student/tmp/example.sql
\echo :schema
\echo :table
\echo :PSQL_ARG1
SELECT flight_id, flight_no departure_airport, arrival_airport, status, aircraft_code
FROM :schema.:table
WHERE flight_id = :PSQL_ARG1;

Перед выполнением запроса скрипт выводит значения трех переменных, переданных через параметры. Подразумевается, что первые два параметра будут именованными, а последний — позиционным. Запустим скрипт из psql:

=> \i ~/tmp/example.sql schema=bookings table=flights 20
bookings
flights
20
 flight_id | departure_airport | arrival_airport | status  | aircraft_code
-----------+-------------------+-----------------+---------+---------------
        20 | PG0403            | LED             | Arrived | 321
(1 row)

Page 7: Системные пакеты Oracle

Расширение orafce

  • функции, представления
  • пакеты dbms_output, dbms_sql, dbms_pipe, dbms_alert, dbms_utility,
  • dbms_assert, utl_file
  • и др.
  • Реализация других системных пакетов Oracle
    • dbms_lob, dbms_application_info
    • utl_mail, utl_smtp, utl_http

Комментарии преподавателя

В дистрибутив Postgres Pro Enterprise включен пакет orafce-ent-16, устанавливающий расширение orafce. Оно добавляет в базу данных Postgres Pro Enterprise объекты, аналогичные объектам СУБД Oracle, что сильно упрощает перенос процедурной логики.

https://github.com/orafce/orafce

Несколько расширений, эмулирующих другие системные пакеты Oracle, поставляются в отдельном пакете pgpro-orautl-ent-16:

● utl_mail — управление электронными письмами;

● utl_smtp — отправка электронных писем из PL/pgSQL;

● utl_http — выполнение HTTP-вызовов из SQL и PL/pgSQL.

https://postgrespro.ru/docs/enterprise/16/utl-mail

https://postgrespro.ru/docs/enterprise/16/utl-smtp

https://postgrespro.ru/docs/enterprise/16/utl-http

Еще два расширения добавлены в состав Postgres Pro Enterprise как стандартные и включены в основной пакет:

● dbms_lob — работа с большими объектами: BLOB, CLOB, BFILE и временными LOB;

● pgpro_application_info — реализация пакета dbms_application_info.

https://postgrespro.ru/docs/enterprise/16/dbms-lob

https://postgrespro.ru/docs/enterprise/16/pgpro-application-info

Page 8: Системные пакеты Oracle

Для примера рассмотрим работу с расширением pgpro_application_info, возможности которого можно использовать для анализа производительности и отладки приложений. Расширение содержит ряд подпрограмм, позволяющих приложениям сообщать о своем состоянии и ходе выполнения. Доступ к этой информации осуществляется через представления. Начнем с установки расширения:

=> ALTER SYSTEM SET shared_preload_libraries = 'pgpro_application_info';
ALTER SYSTEM

Необходима перезагрузка экземпляра:

student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql demo

Добавим расширение в базу данных.

=> CREATE EXTENSION pgpro_application_info;
CREATE EXTENSION

В транзакции первого сеанса будем вызывать процедуры из расширения pgpro_application_info: set_client_info — задает информацию о клиенте, например его название и/или описание; set_module — передает имя выполняемого в данный момент модуля (либо подпрограммы) и действие в нем; set_action — передает название выполняющегося действия в текущем модуле. Во втором сеансе будем проверять состояние первого, используя представление v_session того же расширения:

student$ psql demo

Первый сеанс:

=> BEGIN;
BEGIN
=> CALL dbms_application_info.set_client_info('Первый сеанс');
CALL
=> CALL dbms_application_info.set_module('Модуль подсчета', 'Подготовка');
CALL

Второй сеанс:

=> SELECT * FROM v_session;
  sid   | dbname |     module      |   action   | client_info
--------+--------+-----------------+------------+--------------
 792743 | demo   | Модуль подсчета | Подготовка | Первый
сеанс
(1 row)
=> CALL dbms_application_info.set_action('Подсчет');
CALL
=> SELECT * FROM v_session;
  sid   | dbname |     module      | action  | client_info
--------+--------+-----------------+---------+--------------
 792743 | demo   | Модуль подсчета | Подсчет | Первый
сеанс
(1 row)

Обратите внимание, что расширение работает нетранзакционно — в ряде случаев это может оказаться удобным.

=> CALL dbms_application_info.set_module(NULL, NULL);
CALL
=> SELECT * FROM v_session;

Page 9: Page 9

  sid   | dbname | module | action | client_info
--------+--------+--------+--------+--------------
 792743 | demo   |        |        | Первый сеанс
(1 row)

Откатим транзакцию в первом сеансе:

=> ROLLBACK;
ROLLBACK

Page 10: Пакеты

Source slide page 10: Пакеты{#source-figure-010 caption=“Source slide page 10: Пакеты”}

Комментарии преподавателя

Пакеты в Oracle позволяют сгруппировать логически связанные объекты (подпрограммы, переменные, типы данных, курсоры и т. п.). Пакет состоит из спецификации (объявления публичных элементов, доступных извне пакета) и тела (реализации подпрограмм, объявленных в спецификации, и внутренних элементов, доступных только внутри пакета). Спецификация определяет интерфейс пакета, а тело рассматривается как черный ящик — изменение реализации не влияет на пользователей пакета.

Спецификация и тело пакета — объекты базы данных Oracle.

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

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

В PostgreSQL аналогичной цели — группировке логически связанных объектов — служат схемы. Однако схема не имеет переменных и состояния, что очень сильно усложняет миграцию. Поэтому в Postgres Pro Enterprise реализован основной функционал пакетов. Он не повторяет пакеты Oracle синтаксически, а построен на основе схем.

https://postgrespro.ru/docs/enterprise/16/packages-overview

Page 11: Пакеты

Схема с функцией инициализации

  • Состояние
    • объявленные в __init__ переменные
  • Аналог спецификации
    • заголовки публичных подпрограмм
    • типы данных
    • #export-переменные из __init__ (по умолчанию все)
  • Аналог тела
    • публичные подпрограммы
    • внутренние #private-подпрограммы

Комментарии преподавателя

Пакет в PostgresPro Enterprise — это схема, организованная специальным образом. Сначала в схеме создается функция с именем __init__ – функция инициализации.

Все переменные, объявленные в этой функции, становятся частью состояния пакета. По умолчанию все переменные будут публичными, то есть доступны снаружи пакета из подпрограмм, помеченных модификатором #import. Но можно сделать публичными только часть переменных, упомянув их в модификаторе #export, тогда все остальные будут доступны только из подпрограмм пакета. Обращаться к переменным пакета извне нужно по квалифицированному имени: имя-пакета.имя-переменной.

Затем создаются пакетные подпрограммы – их помечают модификатором #package. По умолчанию такие подпрограммы будут публичными. Если же подпрограмма служит только для внутренних нужд пакета, ее следует пометить модификатором #private.

При обращении к любой пакетной подпрограмме вызывается функция инициализации, которая формирует состояние пакета. Сбросить его можно вызовом функции инициализации или plpgsql_reset_packages (это аналог DBMS_SESSION.RESET_PACKAGES в Oracle).

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

Пакет удобно создавать специальной командой CREATE PACKAGE.

https://postgrespro.ru/docs/enterprise/16/plpgsql-package

Page 12: Пакеты

Создадим пакет, предоставляющий локальный для сеанса счетчик:

=> CREATE PACKAGE demo
  -- в команде CREATE PACKAGE все подпрограммы считаются пакетными,
  -- модификатор #package не нужен
  CREATE FUNCTION __init__() RETURNS void AS
  $$
  DECLARE
    n integer;
  BEGIN
    n := 0;
    RAISE NOTICE 'Пакет инициализирован';
  END;
  $$ -- точка с запятой только в конце пакета
  CREATE FUNCTION next() RETURNS integer AS
  $$
  BEGIN
    n := n + 1;
    RETURN n;
  END;
  $$
;
CREATE PACKAGE

При первом обращении к пакету выполняется функция инициализации:

=> SELECT demo.next();
NOTICE:  Пакет инициализирован
 next
------
    1
(1 row)
=> SELECT demo.next();
 next
------
    2
(1 row)

В другом сеансе отсчет начинается заново:

student$ psql demo
=> SELECT demo.next();
NOTICE:  Пакет инициализирован
 next
------
    1
(1 row)

А в первом сеансе счетчик продолжает увеличиваться:

=> SELECT demo.next();
 next
------
    3
(1 row)

Пакеты в Postgres Pro Enterprise реализованы как схемы. Поэтому в списке схем можно увидеть и схему demo:

=> \dn

Page 13: Page 13

              List of schemas
         Name          |       Owner
-----------------------+-------------------
 bookings              | student
 dbms_application_info | student
 demo                  | student
 public                | pg_database_owner
(4 rows)

Список схем-пакетов можно получить запросом, выводящим схемы, в которых есть функция __init__:

=> SELECT n.nspname AS schema_name
FROM pg_proc p
  JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE p.proname = '__init__';
 schema_name
-------------
 demo
(1 row)

Удалим пакет:

=> DROP PACKAGE demo;
DROP PACKAGE

Page 14: Утилита ora2pgpro

Автоматизация миграции из Oracle

  • Конвертация кода PL/SQL
    • пакеты, функции и процедуры, триггеры
    • автономные транзакции
    • VARRAY, ассоциативные массивы
  • Перенос схемы данных и объектов
    • параллельная выгрузка и загрузка данных
    • секционированные таблицы (по спискам и по диапазонам, подсекции)
    • BLOB, пользовательские типы данных
    • поддержка DBLINK, SYNONYM, DIRECTORY
    • полнотекстовый поиск
    • Oracle Spatial (пространственные данные)

Комментарии преподавателя

Утилита ora2pgpro предназначена для автоматизации конвертации кода и переноса объектов из СУБД Oracle в Postgres Pro Enterprise.

Утилита ora2pgpro создана на основе ora2pg:

https://ora2pg.darold.net/documentation.html

Однако ora2pgpro существенно расширяет возможности ora2pg по переносу кода и типов данных, отсутствующих в PostgreSQL, но реализованных в Postgres Pro Enterprise. Помимо этого, в ora2pgpro устранены ошибки, найденные за время эксплуатации ora2pg.

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

https://postgrespro.ru/docs/ora2pgpro/26/index

Для работы утилиты требуется Oracle Instant Client Package.

Утилита ora2pgpro не поддерживается на платформе ARM64 (MacBook с процессором Apple Silicon) и в виртуальной машине курса для этой платформы отсутствует.

Page 15: Утилита ora2pgpro

Source slide page 15: Утилита ora2pgpro{#source-figure-015 caption=“Source slide page 15: Утилита ora2pgpro”}

Комментарии преподавателя

Утилита ora2pgpro подключается к БД Oracle, извлекает из нее структуру базы, данные и хранимый код (все вместе или по частям), и генерирует SQL-скрипты, которые потом можно загрузить в Postgres Pro Enterprise с помощью psql. В простых случаях, не требующих ручных правок, можно сформировать объекты напрямую в базе Postgres Pro Enterprise.

Утилита управляется директивами, которые позволяют переименовывать таблицы и столбцы в схеме данных, заменять типы данных (например, CHAR(1) на boolean, если он используется для логических значений) и т. д.

Для работы с хранимым кодом (пакетами, процедурами и функциями, триггерами) в утилиту добавлен транслятор, который разбирает код PL/SQL, строит дерево разбора и генерирует по нему код PL/pgSQL, используя все возможности, предоставляемые Postgres Pro Enterprise для совместимости с Oracle.

Page 16: ora2pgpro

Утилита ora2pgpro уже установлена в виртуальной машине курса:

student$ ora2pgpro -v
Ora2PgPro v26.1.1

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

student$ mkdir -p ~/tmp/sources

Поместим в него два скрипта, описывающие простой пакет Oracle:

/home/student/tmp/sources/DEMO_PKG_spec_source.sql
-- спецификация пакета
PACKAGE demo_pkg AS
  FUNCTION next RETURN NUMBER;
END demo_pkg;
/home/student/tmp/sources/DEMO_PKG_body_source.sql
-- тело пакета
PACKAGE BODY demo_pkg
AS
  n NUMBER; -- состояние
  FUNCTION next RETURN NUMBER IS
  BEGIN
    n := n + 1;
    RETURN n;
  END next;
BEGIN
  -- инициализаци
  n := 0;
END demo_pkg;

Спецификация и тело пакета располагаются в подкаталоге sources каталога, который мы укажем утилите. В этот же каталог утилита выведет результаты своей работы.

student$ ls ~/tmp/sources
DEMO_PKG_body_source.sql
DEMO_PKG_spec_source.sql

Утилите также потребуется конфигурационный файл, в котором указываются параметры-директивы, управляющие ее работой и описывающие подключение к базам Oracle и Postgres Pro. Мы собираемся запустить утилиту в локальном режиме, поэтому не будем указывать параметры подключения к базе данных Oracle и файл будет коротким:

/home/student/tmp/ora2pgpro.conf
PG_VERSION 16

При запуске указываем тип экспорта PACKAGE (пакеты) и пути к каталогу миграции и к конфигурационному файлу:

student$ ora2pgpro --offline -t PACKAGE -b ~/tmp -c ~/tmp/ora2pgpro.conf
DEMO_PKG...
Done.
Fixing function calls in output files...

Сценарий — результат преобразования схемы — будет записан в файл:

student$ cat ~/tmp/result/DEMO_PKG.sql
-- спецификация пакета
BEGIN;
DROP SCHEMA IF EXISTS DEMO_PKG CASCADE;
CREATE SCHEMA DEMO_PKG;
-- тело пакета

Page 17: Page 17

CREATE OR REPLACE FUNCTION DEMO_PKG.__INIT__() RETURNS VOID AS $$
#package
#export off
DECLARE
  n numeric; -- состояние
BEGIN
  -- инициализаци
  n = 0;
END;
$$ LANGUAGE plpgsql;
  CREATE OR REPLACE FUNCTION DEMO_PKG.next() RETURNS numeric AS $$
  #package
  BEGIN
    DEMO_PKG.n = DEMO_PKG.n + 1;
    RETURN DEMO_PKG.n;
  END ; $$ LANGUAGE PLPGSQL;
/*END demo_pkg;*/
-- спецификация пакета
COMMIT;

Выполним этот файл с помощью psql:

student$ psql -d demo < ~/tmp/result/DEMO_PKG.sql
BEGIN
NOTICE:  schema "demo_pkg" does not exist, skipping
DROP SCHEMA
CREATE SCHEMA
CREATE FUNCTION
CREATE FUNCTION
COMMIT

В базе данных появилась новая схема-пакет с необходимыми функциями:

=> SELECT n.nspname AS schema_name
FROM pg_proc p
  JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE p.proname = '__init__';
 schema_name
-------------
 demo_pkg
(1 row)
=> \df demo_pkg.*
                          List of functions
  Schema  |   Name   | Result data type | Argument data types | Type
----------+----------+------------------+---------------------+------
 demo_pkg | __init__ | void             |                     | func
 demo_pkg | next     | numeric          |                     | func
(2 rows)

Page 18: Итоги

Миграция с других СУБД — сложная задача

  • Postgres Pro Enterprise облегчает миграцию с Oracle
    • функционал пакетов
    • утилита ora2pgpro для автоматизации переноса данных и кода
    • аналоги системных пакетов Oracle
    • параметры при вызове скрипта в утилите psql

Page 19: Практика

  1. Создайте базу данных и добавьте в нее расширение orafce. 2. Какие схемы появились после загрузки расширения? 3. Изучите возможности функции dbms_random.string.

Комментарии преподавателя

  1. Пакет orafce-ent-16 уже установлен в виртуальной машине.

  2. С документацией по orafce можно ознакомиться в репозитории

https://github.com/orafce/orafce

Page 20: 1. Добавление расширения orafce

student$ psql

Создадим базу данных pgpro_migration и подключимся к ней:

=> CREATE DATABASE pgpro_migration;
CREATE DATABASE
=> \c pgpro_migration
You are now connected to database "pgpro_migration" as user "student".

Расширение orafce распространяется в отдельном пакете orafce-ent-16, который уже установлен в виртуальной машине курса. Поэтому для установки расширения в базу данных достаточно выполнить команду CREATE EXTENSION:

=> CREATE EXTENSION orafce;
CREATE EXTENSION

2. Проверка доступных функций

Расширение orafce представляет реализации многих функций, которые существуют в СУБД Oracle, но отсутствуют (или реализованы по-другому) в Postgres Pro. Функции каждого из пакетов располагаются в отдельных схемах:

=> \dn
         List of schemas
     Name     |       Owner
--------------+-------------------
 dbms_alert   | student
 dbms_assert  | student
 dbms_output  | student
 dbms_pipe    | student
 dbms_random  | student
 dbms_sql     | student
 dbms_utility | student
 oracle       | student
 plunit       | student
 plvchr       | student
 plvdate      | student
 plvlex       | student
 plvstr       | student
 plvsubst     | student
 public       | pg_database_owner
 utl_file     | student
(16 rows)

3. Функция dbms\_random.string

Пакет dbms_random предназначен для генерации случайных чисел и строк. Посмотрим, какие функции в него входят:

=> \df dbms_random.*

Page 21: Page 21

                                        List of functions
   Schema    |    Name    | Result data type |             Argument data types
 | Type
-------------+------------+------------------+--------------------------------------------
-+------
 dbms_random | initialize | void             | integer
 | func
 dbms_random | normal     | double precision |
 | func
 dbms_random | random     | integer          |
 | func
 dbms_random | seed       | void             | integer
 | func
 dbms_random | seed       | void             | text
 | func
 dbms_random | string     | text             | opt text, len integer
 | func
 dbms_random | terminate  | void             |
 | func
 dbms_random | value      | double precision |
 | func
 dbms_random | value      | double precision | low double precision, high double
precision | func
(9 rows)

Функция string возвращает случайные строки. Ее первый параметр однобуквенный, он отвечает за вид возвращаемого значения: U/u — строка в верхнем регистре; L/l — строка в нижнем регистре; A/a — строка в смешанном регистре; X/x — строка из буквенно-цифровых символов в верхнем регистре; P/p — строка из любых печатных символов в смешанном регистре. Второй параметр управляет длиной строки:

=> SELECT * FROM dbms_random.string('u',10);
   string
------------
 BVREXBZUMB
(1 row)
=> SELECT * FROM dbms_random.string('l',10);
   string
------------
 rfgsookawm
(1 row)
=> SELECT * FROM dbms_random.string('a',10);
   string
------------
 ubvMWYlrcH
(1 row)
=> SELECT * FROM dbms_random.string('x',10);
   string
------------
 F3G3AC694N
(1 row)
=> SELECT * FROM dbms_random.string('p',10);
   string
------------
 hGX&/m8V,?
(1 row)

Уборка

Удалим созданную базу данных.

Page 22: Page 22

=> \c student
You are now connected to database "student" as user "student".
=> DROP DATABASE pgpro_migration WITH (FORCE);
DROP DATABASE

05 Занятие 05: Установка и сопровождение / Планировщик заданий

Исходная страница в нативном Markdown: Установка и сопровождение / Планировщик заданий
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_05_scheduler.html Archive: PGPRO-handouts-16.zip Страниц PDF: 24

Page 1: Установка и сопровождение / Планировщик заданий

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Назначение
  • Архитектура
  • Периодические задания
  • Управление и наблюдение
  • Разовые задания

Page 3: Назначение

Расширение pgpro\_scheduler

  • планировщик периодических и разовых заданий мониторинг разграничение прав Задание — команды SQL
    • в отдельных транзакциях или все в одной

Комментарии преподавателя

Планировщик заданий требуется для периодического запуска заданий по расписанию, а также для запуска разовых отложенных заданий. В Postgres Pro планировщик реализован в виде расширения pgpro_scheduler.

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

Задание планировщика состоит из команд SQL, которые могут быть выполнены как в одной транзакции, так и в разных.

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

https://postgrespro.ru/docs/enterprise/16/pgpro-scheduler

Page 4: Архитектура

Source slide page 4: Архитектура{#source-figure-004 caption=“Source slide page 4: Архитектура”}

  • …
  • ↑

Комментарии преподавателя

Планировщик задач использует механизм фоновых рабочих процессов.

Основной процесс-диспетчер планировщика (один на весь кластер баз данных) запускается функцией schedule.enable, а для автоматического его запуска при старте сервера нужно установить параметр schedule.auto_enabled = true.

Диспетчерский процесс проверяет настройки и для каждой из указанных в параметре schedule.database баз данных запускает отдельный процесс-менеджер (для показанной на слайде конфигурации нужно задать значение ‘DB1,DB2’). Также сразу запускаются обработчики разовых заданий планировщика, их количество ограничено значением schedule.max_parallel_workers.

Максимальное количество обработчиков для планируемых заданий в каждой базе ограничено параметром schedule.max_workers.

Оба ограничения можно задавать на уровне базы данных командой ALTER DATABASE. Обработчики периодических и разовых заданий работают независимо.

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

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

Page 5: Установка расширения pgpro_scheduler

Создадим базу данных:

=> CREATE DATABASE pgpro_scheduler;
CREATE DATABASE

Для работы расширения необходимо подключить библиотеку:

=> ALTER SYSTEM SET shared_preload_libraries = 'pgpro_scheduler';
ALTER SYSTEM

Общее ограничение на количество фоновых рабочих процессов в системе, значения по умолчанию достаточно для работы с одной базой:

=> SHOW max_worker_processes;
 max_worker_processes
----------------------
 8
(1 row)

Подключение библиотеки требует перезапуска СУБД.

student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql

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

=> \c pgpro_scheduler
You are now connected to database "pgpro_scheduler" as user "student".
=> CREATE EXTENSION pgpro_scheduler;
CREATE EXTENSION

Объекты расширения находятся в схеме schedule. Например, представления для мониторинга разовых заданий:

=> \dv schedule.*
             List of relations
  Schema  |      Name      | Type |  Owner
----------+----------------+------+---------
 schedule | all_job_status | view | student
 schedule | all_jobs_log   | view | student
 schedule | job_status     | view | student
 schedule | jobs_log       | view | student
(4 rows)

Настройка

Укажем имена баз данных, для которых будут запускаться задания:

=> ALTER SYSTEM SET schedule.database = 'pgpro_scheduler';
ALTER SYSTEM

Применим изменение конфигурации.

=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

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

=> ALTER DATABASE pgpro_scheduler SET schedule.max_workers = 3;
ALTER DATABASE

Page 6: Page 6

Ограничение на число рабочих процессов, выполняющих разовые задания:

=> ALTER DATABASE pgpro_scheduler SET schedule.max_parallel_workers = 3;
ALTER DATABASE

Включим планировщик заданий:

=> SELECT schedule.enable();
NOTICE:  the number of allowed background workers could be insufficient for normal work
HINT:  With the current configuration, the pgpro_scheduler extension may start up to 8
background worker processes. You might need to increase "max_worker_processes" at least
to 10.
 enable
--------
 t
(1 row)

Состояние процессов планировщика заданий:

=> SELECT * FROM schedule.status();
 pid |    database     |       type
-----+-----------------+------------------
     | pgpro_scheduler | database manager
(1 row)

Информация об этих процессах доступна и в pg_stat_activity:

=> SELECT pid, datname, application_name, backend_type, query
FROM pg_stat_activity
WHERE application_name ~ '^pgp-s' \gx
(0 rows)

Page 7: Периодические задания

Команды SQL

  • выполняются в одной или в нескольких транзакциях
  • Расписание
    • в формате cron
    • массив дат
    • объект JSON
    • выражение SQL ­— время следующего запуска
    • диапазон времени
  • Дополнительные атрибуты
    • пользователь, длительность, время ожидания, обработчик ошибок

Комментарии преподавателя

Функция schedule.create_job позволяет запланировать периодическое задание. Параметры планирования задаются в виде одного объекта jsonb.

Атрибут commands задает одну или несколько команд для выполнения. Если задать команды в виде символьной строки, они выполнятся в одной транзакции. Если же задан массив строк, то каждому элементу массива будет соответствовать отдельная транзакция.

Расписание для задания задается атрибутами:

● cron — в формате crontab;

● dates — значение или массив значений типа timestamptz;

● rule — объектом jsonb.

Атрибутами start_date и end_date можно ограничить время, в которое задание может выполняться; вне диапазона работающее задание будет остановлено, а новое не запустится.

Время следующего запуска задания будет определяться динамически, если в атрибуте next_time_statement задать SQL-выражение. Оно вычисляется после каждого выполнения задания.

Также можно запланировать выполнение задания с правами другого пользователя (атрибут run_as), ограничить длительность выполнения и ожидания, подключить обработчик ошибок в виде SQL-команды.

https://postgrespro.ru/docs/enterprise/16/pgpro-scheduler#PGPRO-SCHED ULER-USAGE

Page 8: Управление и мониторинг

Управление

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

Комментарии преподавателя

Расширение pgpro_scheduler использует функции в схеме schedule для управления периодическими заданиями и их мониторинга:

● set_job_attribute() — изменение одного атрибута задания;

● set_job_attributes() — изменение нескольких атрибутов задания;

● deactivate_job — деактивировать задание;

● activate_job — активировать задание;

● drop_job — удалить задание;

● get_job — информация о задании;

● get_owned_cron — задания пользователя;

● get_cron — список заданий, выполняемых пользователем;

● get_active_jobs — список выполняющихся заданий;

● get_log — список всех завершенных заданий;

● get_user_log — список завершенных заданий пользователя;

● clean_log — удалить все записи о завершенных заданиях.

Имеется функция timetable, которая возвращает таблицу с описанием всех запланированных выполнений заданий (многократно повторяемых и разовых), попадающих в заданный диапазон времени.

Page 9: Периодические задания

Создадим таблицу:

=> CREATE TABLE t (
  bknd int DEFAULT pg_backend_pid(),
  txid bigint DEFAULT txid_current(),
  ttime timestamptz DEFAULT now()
);
CREATE TABLE

А теперь создадим периодическое задание, используя формат crontab. Формат использует пять полей для идентификации времени: минуты часа (0-59); часы (0-23); дни месяца (1-31); месяц (1-12 или трехбуквенное сокращение); дни недели (0 — воскресенье … 6 — суббота, или трехбуквенные сокращения). Если в поле после звездочки через косую черту указано число — это шаг выполнения. Например, */10 * * * * означает «раз в десять минут». Расширенный формат cron использует шесть полей с секундами. Например, */30 * * * * * означает «раз в тридцать секунд». Запланируем задание на выполнение раз в минуту:

=> SELECT schedule.create_job(
  '{ "commands": "INSERT INTO t DEFAULT VALUES",
     "cron": "*/1 * * * *" }'
);
 create_job
------------
          1
(1 row)

Получим информацию о запланированном задании:

=> SELECT id, commands, run_as, active
FROM schedule.get_cron();
 id |             commands             | run_as  | active
----+----------------------------------+---------+--------
  1 | {"INSERT INTO t DEFAULT VALUES"} | student | t
(1 row)

Функция get_job информирует о задании, возвращая значение типа cron_rec. Проверим поле rule, описывающее расписание, преобразовав расписание из представления JSONB в текстовые строки и урезав их по ширине:

=> SELECT substring(jsonb_each_text(rule)::text FOR 75) AS rule
FROM schedule.get_job(1);
                                    rule
-----------------------------------------------------------------------------
 (days,"[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
 (hours,"[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
 (wdays,"[0, 1, 2, 3, 4, 5, 6]")
 (months,"[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]")
 (crontab,"*/1 * * * *")
 (minutes,"[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18
 (seconds,[0])
(7 rows)

Page 10: Page 10

Через минуту сработает первое задание. Подождем… Проверим, что было добавлено в таблицу t:

=> SELECT * FROM t;
  bknd  | txid |             ttime
--------+------+-------------------------------
 797284 |  971 | 2026-07-27 23:36:00.048241+03
(1 row)

Функция timetable информирует о выполненных и запланированных заданиях. Получим список заданий за 5 минут:

=> SELECT id, type, commands, scheduled_at, status
FROM schedule.timetable(
  now() - interval '3 minutes',
  now() + interval '2 minutes'
)
ORDER BY scheduled_at;
 id |    type    |             commands             |      scheduled_at      |  status
----+------------+----------------------------------+------------------------+-----------
  1 | periodical | {"INSERT INTO t DEFAULT VALUES"} | 2026-07-27 23:36:00+03 | done
  1 | periodical | {"INSERT INTO t DEFAULT VALUES"} | 2026-07-27 23:37:00+03 | submitted
  1 | periodical | {"INSERT INTO t DEFAULT VALUES"} | 2026-07-27 23:38:00+03 | submitted
(3 rows)

Отчет о выполненных заданиях планировщика:

=> SELECT cron, scheduled_at, started, status, message
FROM schedule.get_log();
 cron |      scheduled_at      |            started            | status | message
------+------------------------+-------------------------------+--------+---------
    1 | 2026-07-27 23:36:00+03 | 2026-07-27 23:36:00.036693+03 | done   |
(1 row)

Управление периодическими заданиями

До сих пор задание выполнялось в ноль секунд каждой минуты:

=> SELECT jsonb_array_elements(rule->'seconds') FROM schedule.get_job(1);
 jsonb_array_elements
----------------------
 0
(1 row)

Пусть задание выполняется чаще — раз в десять секунд. Используем расширенный формат cron из шести полей, где первое поле — секунды:

=> SELECT schedule.set_job_attribute(1, 'cron','*/10 * * * * *');
 set_job_attribute
-------------------
 t
(1 row)

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

=> SELECT jsonb_array_elements(rule->'seconds') FROM schedule.get_job(1);
 jsonb_array_elements
----------------------
 0
 10
 20
 30
 40
 50
(6 rows)

Подождем немного… Проверим, что было добавлено в таблицу t:

Page 11: Page 11

=> SELECT * FROM t;
  bknd  | txid |             ttime
--------+------+-------------------------------
 797284 |  971 | 2026-07-27 23:36:00.048241+03
 797640 |  975 | 2026-07-27 23:36:10.016049+03
(2 rows)

Временно приостановим задание. Новые записи не будут попадать в таблицу.

=> SELECT schedule.deactivate_job(1);
 deactivate_job
----------------
 t
(1 row)

Проверим состояние задания — оно должно быть не активно:

=> SELECT id, commands, run_as, active FROM schedule.get_job(1);
 id |             commands             | run_as  | active
----+----------------------------------+---------+--------
  1 | {"INSERT INTO t DEFAULT VALUES"} | student | f
(1 row)

Подождем немного… Снова запустим задание и проверим состояние:

=> SELECT schedule.activate_job(1);
 activate_job
--------------
 t
(1 row)
=> SELECT id, commands, run_as, active FROM schedule.get_job(1);
 id |             commands             | run_as  | active
----+----------------------------------+---------+--------
  1 | {"INSERT INTO t DEFAULT VALUES"} | student | t
(1 row)

Подождем еще немного… Проверим записи в таблице t:

=> SELECT * FROM t;
  bknd  | txid |             ttime
--------+------+-------------------------------
 797284 |  971 | 2026-07-27 23:36:00.048241+03
 797640 |  975 | 2026-07-27 23:36:10.016049+03
 797887 |  980 | 2026-07-27 23:36:40.012964+03
 797890 |  983 | 2026-07-27 23:36:50.01538+03
(4 rows)

Выполненные задания отображаются в журнале:

=> SELECT cron, scheduled_at, started, status, message
FROM schedule.get_log() WHERE cron = 1;
 cron |      scheduled_at      |            started            | status | message
------+------------------------+-------------------------------+--------+---------
    1 | 2026-07-27 23:36:00+03 | 2026-07-27 23:36:00.036693+03 | done   |
    1 | 2026-07-27 23:36:10+03 | 2026-07-27 23:36:10.008657+03 | done   |
    1 | 2026-07-27 23:36:40+03 | 2026-07-27 23:36:40.007297+03 | done   |
    1 | 2026-07-27 23:36:50+03 | 2026-07-27 23:36:50.008664+03 | done   |
(4 rows)

Снова приостановим задание и очистим таблицу.

=> SELECT schedule.deactivate_job(1);

Page 12: Команды в отдельных транзакциях

 deactivate_job
----------------
 t
(1 row)
=> DELETE FROM t;
DELETE 4

В атрибуте commands значения могут задаваться в виде текста или массива. Если отдельные команды SQL заданы текстовой строкой через точку с запятой, то все команды задания будут выполнены в одной транзакции. Если команды SQL заданы JSON-массивом, то каждая из них будет выполнена в отдельной транзакции. Создадим задание для заполнения таблицы t раз в десять секунд. Каждая SQL-команда выполняется в отдельной транзакции. Значения now() для каждой транзакции — свои, как и идентификаторы транзакции.

=> SELECT schedule.create_job(
  '{ "commands": [
       "INSERT INTO t DEFAULT VALUES",
       "SELECT pg_sleep(5)",
       "INSERT INTO t DEFAULT VALUES"
     ],
     "cron": "*/10 * * * * *" }'
);
 create_job
------------
          2
(1 row)

Снова немного подождем…

=> SELECT schedule.drop_job(2);
 drop_job
----------
 t
(1 row)

Проверим, что было добавлено в таблицу:

=> SELECT * FROM t;
  bknd  | txid |             ttime
--------+------+-------------------------------
 798536 |  989 | 2026-07-27 23:37:00.01162+03
 798536 |  991 | 2026-07-27 23:37:05.024718+03
(2 rows)

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

Команды в одной транзакции

Даже если команды заданы массивом, можно задать атрибуту use_same_transaction значение true, и все команды будут выполнены в одной транзакции. Удалим все записи в таблице.

=> DELETE FROM t;
DELETE 2

В одной транзакции значения времени, которые возвращает функция now(), будут одинаковыми.

Page 13: Page 13

=> SELECT schedule.create_job(
  '{ "commands": [
       "INSERT INTO t DEFAULT VALUES",
       "SELECT pg_sleep(5)",
       "INSERT INTO t DEFAULT VALUES"
     ],
     "cron": "*/10 * * * * *",
     "use_same_transaction": true}'
);
 create_job
------------
          3
(1 row)

Подождем чуть-чуть… Удалим задание и проверим, что было добавлено в таблицу.

=> SELECT schedule.drop_job(3);
 drop_job
----------
 t
(1 row)
=> SELECT * FROM t;
  bknd  | txid |             ttime
--------+------+-------------------------------
 799236 |  997 | 2026-07-27 23:37:10.010298+03
 799236 |  997 | 2026-07-27 23:37:10.010298+03
(2 rows)

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

=> DELETE FROM t;
DELETE 2
=> SELECT schedule.activate_job(1);
 activate_job
--------------
 t
(1 row)

Page 14: Разовые задания

  • работает независимо
  • от периодических
  • Выполнить задание
    • немедленно
    • в определенный момент
    • после завершения других заданий
  • Назначить повторное выполнение
  • Отменить еще не выполняющееся задание

Комментарии преподавателя

Функция submit_job создает разовое задание. По умолчанию задание будет выполнено немедленно. Можно задать время запуска параметром run_after. Также можно в параметре depends_on передать массив идентификаторов других заданий — в этом случае данное задание будет выполнено сразу после их завершения.

Функция resubmit, вызванная в SQL-запросе задания, позволяет задать время следующего запуска этого задания.

Функция cancel_job отменяет задание. Если задание уже выполняется, оно не будет прервано, но повторно назначить его будет нельзя.

Получить список выполненных и запланированных заданий (как периодических, так и разовых) позволяет функция timetable.

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

Page 15: Разовые задания

Запланируем разовое задание, удаляющее через десять секунд таблицу t:

=> SELECT schedule.submit_job(
  'DROP TABLE t',
  run_after := now() + interval '10 seconds'
);
 submit_job
------------
          1
(1 row)

Таблица t пока на месте:

=> SELECT * FROM t;
 bknd | txid | ttime
------+------+-------
(0 rows)

Подождем немного…

=> SELECT * FROM t;
ERROR:  relation "t" does not exist
LINE 1: SELECT * FROM t;
                      ^

Таблица уже удалена. В таблице заданий выводится информация и о разовых заданиях:

=> SELECT type, commands, scheduled_at, status
FROM schedule.timetable(
  now() - interval '1 minutes',
  now()
)
WHERE id = 1
ORDER BY scheduled_at;
    type    |             commands             |         scheduled_at          | status
------------+----------------------------------+-------------------------------+--------
 periodical | {"INSERT INTO t DEFAULT VALUES"} | 2026-07-27 23:36:40+03        | done
 periodical | {"INSERT INTO t DEFAULT VALUES"} | 2026-07-27 23:36:50+03        | done
 periodical | {"INSERT INTO t DEFAULT VALUES"} | 2026-07-27 23:37:20+03        | done
 onetime    | {"DROP TABLE t"}                 | 2026-07-27 23:37:25.645247+03 | done
(4 rows)

Свойство onrollback периодического задания задает действия при сбое команды задания — будем восстанавливать таблицу, предполагая ее отсутствие:

=> SELECT schedule.set_job_attribute(1, 'onrollback',
  'CREATE TABLE t (
     bknd int DEFAULT pg_backend_pid(),
     txid bigint DEFAULT txid_current(),
     ttime timestamptz DEFAULT now()
   )'
);
 set_job_attribute
-------------------
 t
(1 row)

Проверим состояние задания:

=> SELECT id, commands, run_as, active
FROM schedule.get_job(1);

Page 16: Page 16

 id |             commands             | run_as  | active
----+----------------------------------+---------+--------
  1 | {"INSERT INTO t DEFAULT VALUES"} | student | t
(1 row)

Подождем еще немного… Теперь таблица восстановлена и пока пуста:

=> SELECT * FROM t;
 bknd | txid | ttime
------+------+-------
(0 rows)

Подождем еще… Теперь таблица уже не пуста:

=> SELECT * FROM t;
  bknd  | txid |             ttime
--------+------+-------------------------------
 801308 | 1020 | 2026-07-27 23:37:40.010017+03
(1 row)

Посмотрим список завершенных заданий:

=> SELECT scheduled_at, commands, started, status, message
FROM schedule.get_log()
WHERE cron = 1 \gx
-[ RECORD 1 ]+-------------------------------------------------
scheduled_at | 2026-07-27 23:36:00+03
commands     | {"INSERT INTO t DEFAULT VALUES"}
started      | 2026-07-27 23:36:00.036693+03
status       | done
message      |
-[ RECORD 2 ]+-------------------------------------------------
scheduled_at | 2026-07-27 23:36:10+03
commands     | {"INSERT INTO t DEFAULT VALUES"}
started      | 2026-07-27 23:36:10.008657+03
status       | done
message      |
-[ RECORD 3 ]+-------------------------------------------------
scheduled_at | 2026-07-27 23:36:40+03
commands     | {"INSERT INTO t DEFAULT VALUES"}
started      | 2026-07-27 23:36:40.007297+03
status       | done
message      |
-[ RECORD 4 ]+-------------------------------------------------
scheduled_at | 2026-07-27 23:36:50+03
commands     | {"INSERT INTO t DEFAULT VALUES"}
started      | 2026-07-27 23:36:50.008664+03
status       | done
message      |
-[ RECORD 5 ]+-------------------------------------------------
scheduled_at | 2026-07-27 23:37:20+03
commands     | {"INSERT INTO t DEFAULT VALUES"}
started      | 2026-07-27 23:37:20.002598+03
status       | done
message      |
-[ RECORD 6 ]+-------------------------------------------------
scheduled_at | 2026-07-27 23:37:30+03
commands     | {"INSERT INTO t DEFAULT VALUES"}
started      | 2026-07-27 23:37:30.009847+03
status       | error
message      | error in command #1: relation "t" does not exist
-[ RECORD 7 ]+-------------------------------------------------
scheduled_at | 2026-07-27 23:37:40+03
commands     | {"INSERT INTO t DEFAULT VALUES"}
started      | 2026-07-27 23:37:40.002633+03
status       | done
message      |

Удалим запланированное задание:

=> SELECT schedule.drop_job(1);

Page 17: Page 17

 drop_job
----------
 t
(1 row)

Page 18: Итоги

Расширение pgpro\_scheduler позволяет планировать задания

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

Page 19: Практика

1. Подготовьте систему к работе с расширениями pgpro\_scheduler и pg\_stat\_statements. 2. Установите расширения pgpro\_scheduler и pg\_stat\_statements в базу данных и настройте их.

    1. Запланируйте задание, собирающее статистику по наиболее часто выполняемым запросам каждые 20 секунд, а также разовое задание, копирующее набранную статистику в файл.

Комментарии преподавателя

  1. Для установки расширений потребуется изменить значение параметра shared_preload_libraries и перезагрузить сервер.

  2. Подключив расширения с помощью CREATE EXTENSION, настройте число рабочих процессов по аналогии с демонстрацией и запустите планировщик.

  3. Для сбора статистики используйте расширение pg_stat_statements и таблицу с такой же структурой, как у представления pg_stat_statements, но с дополнительным полем, в которое будет записываться момент сбора статистики. Ограничьте время работы периодического задания одной минутой с помощью атрибута end_date в аргументе функции schedule.create_job.

Page 20: 1. Подготовка СУБД для работы с расширениями pgpro_scheduler и pg_stat_statements

student$ psql

Создадим базу данных pgpro_scheduler:

=> CREATE DATABASE pgpro_scheduler;
CREATE DATABASE

Подключим разделяемые библиотеки:

=> ALTER SYSTEM SET shared_preload_libraries = pg_stat_statements, pgpro_scheduler;
ALTER SYSTEM
=> ALTER SYSTEM SET track_io_timing = 'on';
ALTER SYSTEM

Общее ограничение на количество параллельных процессов:

=> SHOW max_worker_processes;
 max_worker_processes
----------------------
 8
(1 row)

Перезапустим СУБД.

student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql pgpro_scheduler

2. Подключение расширений. Запуск планировщика

Установим расширения:

=> CREATE EXTENSION pgpro_scheduler;
CREATE EXTENSION
=> CREATE EXTENSION pg_stat_statements;
CREATE EXTENSION

База данных для нагрузочного тестирования:

=> ALTER SYSTEM SET schedule.database = 'pgpro_scheduler';
ALTER SYSTEM

Применим изменения конфигурации:

=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Включим планировщик заданий:

=> SELECT schedule.enable();
 enable
--------
 t
(1 row)

Состояние процессов планировщика заданий:

=> SELECT * FROM schedule.status();

Page 21: 3. Планирование задач

 pid |    database     |       type
-----+-----------------+------------------
     | pgpro_scheduler | database manager
(1 row)

Создадим таблицу для сохранения выборок статистики:

=> CREATE TABLE stat_tab (LIKE pg_stat_statements);
CREATE TABLE
=> ALTER TABLE stat_tab
  ADD COLUMN time timestamp with time zone DEFAULT now();
ALTER TABLE

Запланируем копирование статистики в таблицу stat_tab раз в 20 секунд, причем периодическое задание должно работать одну минуту:

=> SELECT schedule.create_job(
  ('{
    "commands": "INSERT INTO stat_tab SELECT * FROM pg_stat_statements ORDER BY calls DESC LIMIT 3;",
    "cron": "*/20 * * * * *",
    "end_date": "'||(now()+'1 minute'::interval)||'"
  }')::jsonb
);
 create_job
------------
          1
(1 row)

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

student$ sudo rm -f /tmp/rep.stat
=> SELECT schedule.submit_job(
  'COPY stat_tab TO ''/tmp/rep.stat''',
  run_after => now() + interval '1 minutes'
);
 submit_job
------------
          1
(1 row)

Подождем минуту… Проверим выполненные задания:

=> SELECT cron, scheduled_at, started, status, message
FROM schedule.get_log();
 cron |      scheduled_at      |            started            | status | message
------+------------------------+-------------------------------+--------+---------
    1 | 2026-07-28 00:09:40+03 | 2026-07-28 00:09:40.00418+03  | done   |
    1 | 2026-07-28 00:10:00+03 | 2026-07-28 00:10:00.01965+03  | done   |
    1 | 2026-07-28 00:10:20+03 | 2026-07-28 00:10:20.019042+03 | done   |
(3 rows)

В таблице собраны сведения о наиболее частых запросах:

=> SELECT time, queryid, calls FROM stat_tab;
             time              |       queryid        | calls
-------------------------------+----------------------+-------
 2026-07-28 00:09:40.058619+03 |  3454082965944108383 |    30
 2026-07-28 00:09:40.058619+03 |  6968726711676132615 |     2
 2026-07-28 00:09:40.058619+03 | -6449100197244153197 |     1
 2026-07-28 00:10:00.028057+03 |  3454082965944108383 |    68
 2026-07-28 00:10:00.028057+03 | -6449100197244153197 |     2
 2026-07-28 00:10:00.028057+03 |  6968726711676132615 |     2
 2026-07-28 00:10:20.026808+03 |  3454082965944108383 |   108
 2026-07-28 00:10:20.026808+03 |  -425920193254469305 |     3
 2026-07-28 00:10:20.026808+03 |  6968726711676132615 |     3
(9 rows)

Page 22: Уборка

Разовое задание скопировало таблицу в файл:

student$ sudo ls -l /tmp/rep.stat
-rw-r--r-- 1 postgres postgres 4496 июл 28 00:10 /tmp/rep.stat

Удалим созданную базу данных и файл, сбросим настройки параметров конфигурации.

=> \c student
You are now connected to database "student" as user "student".
=> DROP DATABASE pgpro_scheduler WITH (FORCE);
DROP DATABASE
student$ sudo rm -f /tmp/rep.stat
=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service

Page 23: Практика PPEM

Эти задания используют планировщик PPEM, а не расширение pgpro_scheduler.

    1. Запланируйте выполнение очистки базы demo через одну-две минуты.
    1. Создайте задание для переиндексации таблиц в табличном пространстве pg_default базы demo и запланируйте его выполнение на регулярной основе каждый будний день в 20:00. 3. Убедитесь, что задание из п. 1 выполнено.
    1. Деактивируйте задание из п. 2.

Комментарии преподавателя

1, 2. Создавайте задания из панели «Базы данных».

3, 4. Используйте вкладку «Консоль заданий» в навигационной панели PPEM.

Page 24: 1. Однократное задание очистки базы данных

В навигационной панели выберите «Базы данных» и найдите базу demo экземпляра ent-16. В колонке «Действия» (крайняя справа) нажмите на пиктограмму «Очистить-Vacuum», и в открывшемся окне, в блоке «Планирование задачи», выберите «Отложенное по времени». Дайте новому заданию имя и назначьте время выполнения, после чего нажмите «Применить». В навигационной панели, во вкладке «Консоль заданий», выберите вложенную вкладку «Расписание» и убедитесь в наличии нового задания.

2. Переиндексация таблиц по расписанию

В навигационной панели откройте вкладку «Базы данных» и найдите базу demo. В колонке «Действия», в выпадающем меню (пиктограмма с тремя точками), выберите «Перестроить индексы-Reindex». В открывшемся окне выберите нужное табличное пространство, запланируйте задание по требуемому расписанию и нажмите «Применить».

3. Контроль списка заданий

Откройте «Консоль заданий» в навигационной панели, а затем вкладку «Задания». Проверьте, что созданное в п. 1 задание помечено статусом «Completed».

4. Деактивация задания

Во вкладке «Консоль заданий», в списке «Расписание» найдите задание, созданное в п. 2 и в колонке «Действия» нажмите пиктограмму «Деактивировать».

06 Занятие 06: Данные / CFS — сжатая файловая система

Исходная страница в нативном Markdown: Данные / CFS — сжатая файловая система
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_06_cfs.html Archive: PGPRO-handouts-16.zip Страниц PDF: 24

Page 1: Данные / CFS — сжатая файловая система

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Возможность и польза сжатия
  • Реализация сжатия в Postgres Pro Enterprise
  • Использование CFS
  • Объем данных и степень сжатия
  • Фрагментация и сбор мусора

Page 3: Возможности и польза

Данные БД обычно хорошо сжимаются

  • текстовые данные
  • дублирование
  • Преимущества сжатия
    • уменьшение объема хранения
    • экономия ресурсов ввода-вывода
    • последовательное чтение и запись
  • Сжатие при записи грязного буфера
    • в буферном кеше нужны несжатые данные

Комментарии преподавателя

В базах данных обычно хранятся большие объемы текста и повторяющейся информации. Поэтому для большинства баз сжатие может быть довольно эффективным и позволяет сократить объем хранимых данных в несколько раз. PostgreSQL сжимает данные в TOAST-таблицах, но небольшие текстовые поля, умещающиеся в странице, не сжимаются. Сжатие может быть полезно не только для таблиц, но и для индексов по текстовым ключам или для индексов с большим количеством повторяющихся значений.

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

Работать со сжатыми данными в общей памяти сервера (в буферном кеше) непроизводительно, поэтому логично выполнять сжатие страницы при сохранении грязного буфера и разжимать ее при чтении в буферный кеш.

Page 4: Реализация сжатия

Source slide page 4: Реализация сжатия{#source-figure-004 caption=“Source slide page 4: Реализация сжатия”}

Комментарии преподавателя

Буферный кеш для эффективности использует страницы фиксированного размера (обычно 8 Кбайт), а сжатые страницы имеют разный размер, который к тому же меняется при изменении данных. Это вынуждает перемещать страницы внутри сегмента, и поэтому вводится дополнительный уровень адресации (отображение страниц), который представляет собой набор указателей на текущее местоположение всех страниц.

Сжатые данные также хранятся в файлах-сегментах, но для каждого из них имеется своя карта отображения страниц. Карта имеет размер 1 Мбайт (32 байта на заголовок + по 8 байт на каждый из 131072 указателей) и поэтому обычно находится в кеше ОС.

https://postgrespro.ru/docs/enterprise/16/cfs-implementation

Page 5: Реализация сжатия

Source slide page 5: Реализация сжатия{#source-figure-005 caption=“Source slide page 5: Реализация сжатия”}

Комментарии преподавателя

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

Механизм сбора мусора будет рассмотрен позже в этой теме.

Page 6: Использование CFS

CREATE TABLESPACE ...
WITH (compression=on|true)
WITH (compression=zstd|pglz|zlib|lz4)
  • Системные отношения не сжимаются
  • Проверка контрольной суммы — при чтении в буфер
    • pg_checksums и pg_basebackup не проверяют

Комментарии преподавателя

Сжатие можно включить только на уровне табличного пространства и только в момент его создания, при этом нужно задать алгоритм сжатия. Изменить факт и алгоритм сжатия в дальнейшем нельзя.

Поддерживаются библиотеки zstd, pglz, zlib, lz4. По умолчанию используется алгоритм zstd.

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

Проверка контрольных сумм сжатых страниц возможна, только когда они находятся в развернутом виде в буферном кеше. Поэтому утилиты pg_checksums и pg_basebackup контрольные суммы страниц не проверяют.

https://postgrespro.ru/docs/enterprise/16/cfs-usage

Page 7: Настройка табличного пространства

Убедимся, что сжатие поддерживается:

=> SELECT cfs_version();
       cfs_version
-------------------------
 0.55 pglz,zlib,lz4,zstd
(1 row)

Создадим табличное пространство со сжатием.

student$ sudo mkdir /var/lib/postgresql/ts_dir
student$ sudo chown postgres: /var/lib/postgresql/ts_dir
=> CREATE TABLESPACE cts LOCATION '/var/lib/postgresql/ts_dir' WITH (compression=true);
CREATE TABLESPACE
=> \x \db+ cts \x
Expanded display is on.
List of tablespaces
-[ RECORD 1 ]-----+---------------------------
Name              | cts
Owner             | student
Location          | /var/lib/postgresql/ts_dir
Access privileges |
Options           | {compression=true}
Size              | 4 bytes
Description       |
Expanded display is off.

Page 8: Объем данных

Стандартные функции

  • pg_relation_size
  • pg_table_size
  • pg_indexes_size
  • pg_total_relation_size
  • работают корректно
  • pg_database_size
    • файлы отображения
  • pg_tablespace_size
    • файлы отображения + pg_compression

Комментарии преподавателя

Стандартные функции pg_*_size, возвращающие занимаемый на диске объем, корректно работают со сжатыми табличными пространствами.

В объеме базы данных учитываются файлы отображения, а в объеме табличного пространства — файлы отображения и служебный файл pg_compression.

https://postgrespro.ru/docs/enterprise/16/functions-admin#FUNCTIONS-CO MPRESSION-CONTROL

Page 9: Степень сжатия

Параметры

  • cfs_level = 1
  • 0 — без сжатия, 1 — самое быстрое
  • cfs_compress_temp_relations = off
  • Функции
    • cfs_estimate
    • прогноз степени сжатия
    • (среднее по первым 10% страниц)
    • cfs_compression_ratio
    • фактическое сжатие по всем сегментам

Комментарии преподавателя

Уровень сжатия настраивается параметром cfs_level. Значение 0 отключает сжатие, значение 1 (по умолчанию) соответствует максимальной скорости. Значения от 2 до 100 позволяют указать конкретный уровень сжатия; максимальное значение зависит от используемого алгоритма.

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

Функция cfs_estimate оценивает степень сжатия для всех доступных алгоритмов с учетом текущего значения cfs_level. Анализируются первые 10% (но не более 100) страниц таблицы.

Функция cfs_compression_ratio возвращает фактическую степень сжатия таблицы. Степень сжатия — это отношение объема несжатых данных к объему сжатых. В данном случае объем страниц в буферном кеше делится на объем сжатых страниц, сохраненных в файле.

https://postgrespro.ru/docs/enterprise/16/runtime-config-cfs

https://postgrespro.ru/docs/enterprise/16/functions-admin#FUNCTIONS-CO MPRESSION-CONTROL

Page 10: Сжатая таблица

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

=> CREATE DATABASE pgpro_cfs TABLESPACE cts;
CREATE DATABASE
=> \c pgpro_cfs
You are now connected to database "pgpro_cfs" as user "student".

Создадим таблицу с минимальным fillfactor и расположим ее сначала вне сжатого пространства. Мы не хотим, чтобы автоочистка изменяла данные в страницах во время демонстрации, поэтому отключим ее:

=> CREATE TABLE test (
  s char(500)
)
WITH (fillfactor=10, autovacuum_enabled=off)
TABLESPACE pg_default;
CREATE TABLE

Вставим в таблицу 100000 одинаковых строк:

=> INSERT INTO test
SELECT repeat('a', 500) FROM generate_series(1, 100_000);
INSERT 0 100000
=> ANALYZE;
ANALYZE

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

=> SELECT reltuples, relpages, pg_size_pretty(relpages * 8192::numeric)
FROM pg_class
WHERE relname = 'test';
 reltuples | relpages | pg_size_pretty
-----------+----------+----------------
    100000 |   100000 | 781 MB
(1 row)

А сколько на диске?

=> SELECT pg_size_pretty(pg_relation_size('test', 'main'));
 pg_size_pretty
----------------
 781 MB
(1 row)

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

=> CHECKPOINT;
CHECKPOINT

Прогнозируемая по части страниц степень сжатия:

=> SELECT cfs_estimate('test');

Page 11: Page 11

NOTICE:  Compression ratios and timings:
  pglz  : ratio=53.56, time=5.752 block/ms
  zlib  : ratio=65.60, time=33.245 block/ms
  lz4   : ratio=81.99, time=348.432 block/ms
  zstd  : ratio=107.82, time=170.068 block/ms
Best ratio: "zstd" with 107.82
Best speed: "lz4" with 348.43 blocks/ms
    cfs_estimate
--------------------
 107.81784680178994
(1 row)

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

=> ALTER TABLE test SET TABLESPACE cts;
ALTER TABLE
=> SELECT pg_size_pretty(pg_relation_size('test', 'main'));
 pg_size_pretty
----------------
 7455 kB
(1 row)

Фактическая степень сжатия немного отличается от прогнозируемой:

=> SELECT cfs_compression_ratio('test');
 cfs_compression_ratio
-----------------------
    107.30707191853045
(1 row)

Таблица сжалась в десятки раз. Системный каталог тоже находится в пространстве по умолчанию (cts), но системные таблицы никогда не сжимаются.

=> SELECT relname, reltablespace, relpages * 8192 buffers,
  pg_relation_size(oid,'main') file
FROM pg_class
WHERE relname = 'pg_class';
 relname  | reltablespace | buffers |  file
----------+---------------+---------+--------
 pg_class |             0 |  122880 | 122880
(1 row)

Page 12: Фрагментация

Source slide page 12: Фрагментация{#source-figure-012 caption=“Source slide page 12: Фрагментация”}

  • мусорные страницы
  • процент мусора > cfs_gc_threshold

Комментарии преподавателя

Чтобы бороться с фрагментацией, в Postgres Pro Enterprise реализован механизм сбора мусора.

Файл данных подлежит обработке, если суммарный размер мусорных страниц превышает cfs_gc_threshold процентов от размера файла или размер файла больше 2 Гбайт. Одновременно с файлом данных обрабатывается соответствующий файл отображения.

Page 13: Сбор мусора

Source slide page 13: Сбор мусора{#source-figure-013 caption=“Source slide page 13: Сбор мусора”}

Комментарии преподавателя

На время обработки файл данных полностью блокируется — ставится легкая блокировка cfs file lock в исключительном режиме, которая запрещает обслуживающим процессам обращаться к файлу. Если не удается получить блокировку немедленно, а размер файла менее 3 Гбайт, процедура сбора мусора его пропустит, а если файл имеет больший размер — будет ждать получения блокировки.

Затем нужные страницы копируются в новый файл данных и строится новый файл отображения. По окончании обработки старые файлы удаляются, а новые переименовываются; это происходит атомарно.

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

Page 14: Сбор мусора

Настройки

  • cfs_gc = on
  • cfs_gc_workers = 1
  • cfs_gc_threshold = 30
  • cfs_gc_delay = 0
  • cfs_gc_period = 5000ms
  • Запуск
    • cfs_start_gc( число-процессов ), 0 — синхронно
    • cfs_gc_relation( отношение )

Комментарии преподавателя

Сбор мусора выполняется одним или несколькими процессами в ручном или фоновом режиме.

По умолчанию фоновый сбор мусора включен (cfs_gc = on) и использует один рабочий процесс (cfs_gc_workers = 1).

Если выключить фоновый сбор (cfs_gc = off) и задать параметр cfs_gc_workers = 0, можно вызовом функции cfs_start_gc(0) запустить сбор мусора синхронно, а вызовом cfs_start_gc(N) — асинхронно N процессами. Функция cfs_gc_relation собирает мусор в файлах определенного отношения, возвращая число обработанных файлов.

Параметр cfs_gc_threshold задает минимальный процент мусора, при котором файл размером до 2 Гбайт подлежит обработке (файлы больше 2 Гбайт обрабатываются всегда). Эти условия действуют независимо от способа запуска.

После обработки каждого файла процесс приостанавливается на cfs_gc_delay миллисекунд. По окончании обработки всех файлов процесс бездействует cfs_gc_period миллисекунд.

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

Page 15: Сбор мусора

Функции для мониторинга

  • cfs_fragmentation (отношение)
  • cfs_gc_activity_scanned_files
  • cfs_gc_activity_processed_files
  • cfs_gc_activity_processed_pages
  • cfs_gc_activity_processed_bytes
  • Запись в журнал
    • cfs_gc_respond_time = 3600ms
    • cfs_log_verbose = off

Комментарии преподавателя

Для мониторинга процессов сбора мусора имеется несколько функций. Статистика накапливается с момента запуска сервера.

Параметр cfs_gc_respond_time задает интервал, в течение которого CFS ждет снятия блокировки с файла при сборке мусора, прежде чем в журнал сообщений будет записано предупреждение.

CFS может выводить в журнал информацию о процессе сбора мусора; уровнем детализации сообщений при этом управляет параметр cfs_log_verbose. При его значении on (по умолчанию) устанавливается уровень LOG, а при значении off — уровень INFO.

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

https://postgrespro.ru/docs/enterprise/16/functions-admin#FUNCTIONS-CO MPRESSION-CONTROL

https://postgrespro.ru/docs/enterprise/16/runtime-config-cfs#RUNTIME-CON FIG-CFS

Page 16: Сбор мусора

Сейчас таблица не фрагментирована:

=> SELECT pg_size_pretty(pg_table_size('test')), cfs_fragmentation('test');
 pg_size_pretty | cfs_fragmentation
----------------+-------------------
 7671 kB        |                 0
(1 row)

Посмотрим, что будет, если отключить сбор мусора.

=> ALTER SYSTEM SET cfs_gc = off;
ALTER SYSTEM
=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Изменим все строки таблицы.

=> UPDATE test SET s = s;
UPDATE 100000
=> CHECKPOINT;
CHECKPOINT
=> SELECT pg_size_pretty(pg_table_size('test')), cfs_fragmentation('test');
 pg_size_pretty | cfs_fragmentation
----------------+--------------------
 17 MB          | 0.4387452984900283
(1 row)

Объем хранения вырос вдвое, фрагментация превысила умолчательный порог 30%. При дальнейшем изменении таблицы рост продолжится. Новые версии строк попадут в новые страницы, которые запишутся в конец файла.

=> UPDATE test SET s = s;
UPDATE 100000
=> CHECKPOINT;
CHECKPOINT
=> SELECT pg_size_pretty(pg_table_size('test')), cfs_fragmentation('test');
 pg_size_pretty | cfs_fragmentation
----------------+--------------------
 28 MB          | 0.5948607639094725
(1 row)

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

=> SELECT count(*) FROM test;
 count
--------
 100000
(1 row)
=> SELECT pg_size_pretty(pg_table_size('test')), cfs_fragmentation('test');

Page 17: Page 17

 pg_size_pretty | cfs_fragmentation
----------------+--------------------
 38 MB          | 0.6973050132790315
(1 row)

Почему так происходит? Из курса DBA2 нам известно, что при первом обращении к измененным строкам в них устанавливаются биты-подсказки. А это означает, что такие измененные страницы тоже будут записаны в конец файла, увеличивая его размер. Выполним очистку таблицы. Работа очистки приведет к изменениям в страницах данных, что также увеличит размер файла таблицы и количество мусорных страниц:

=> VACUUM test;
VACUUM
=> SELECT pg_size_pretty(pg_table_size('test')), cfs_fragmentation('test');
 pg_size_pretty | cfs_fragmentation
----------------+--------------------
 45 MB          | 0.8124817627505403
(1 row)

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

=> SET cfs_gc_threshold = 0;
SET
=> SELECT cfs_gc_relation('test');
 cfs_gc_relation
-----------------
               1
(1 row)

Неиспользуемые страницы удалены, размер файла уменьшился:

=> SELECT pg_size_pretty(pg_table_size('test')), cfs_fragmentation('test');
 pg_size_pretty |   cfs_fragmentation
----------------+-----------------------
 8793 kB        | 0.0013327771523916733
(1 row)

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

=> UPDATE test SET s = s;
UPDATE 100000
=> CHECKPOINT;
CHECKPOINT
=> SELECT pg_size_pretty(pg_table_size('test')), cfs_fragmentation('test');
 pg_size_pretty |  cfs_fragmentation
----------------+---------------------
 20 MB          | 0.49664379519474555
(1 row)

Объем опять вырос (примерно вдвое), фрагментация около 50%. Теперь вернем автоматический сбор мусора и сбросим порог, который мы задавали на уровне сеанса.

=> ALTER SYSTEM RESET cfs_gc;
ALTER SYSTEM
=> SELECT pg_reload_conf();

Page 18: Page 18

 pg_reload_conf
----------------
 t
(1 row)
=> RESET cfs_gc_threshold; SHOW cfs_gc_threshold;
RESET
 cfs_gc_threshold
------------------
 30
(1 row)

Сбор мусора выполнится автоматически (немного подождем).

=> SELECT pg_size_pretty(pg_table_size('test')), cfs_fragmentation('test');
 pg_size_pretty | cfs_fragmentation
----------------+-------------------
 10209 kB       |                 0
(1 row)

Page 19: Итоги

Сжатие настраивается для табличных пространств

  • Можно выбирать алгоритм и управлять уровнем сжатия
  • Встроенный механизм сбора мусора сокращает объем хранимых данных

Page 20: Практика

  1. Создайте сжатое табличное пространство, использующее алгоритм zstd с максимальным уровнем сжатия 19. 2. Поместите копию одной из таблиц демобазы в сжатое табличное пространство. Насколько уменьшился ее размер? 3. Удалите созданную в п. 2 базу и сжатое пространство.

Комментарии преподавателя

  1. Чтобы контролировать изменения данных в страницах, отключите автоматическую очистку для таблицы.

Page 21: 1. Сжатое табличное пространство

student$ sudo mkdir /var/lib/postgresql/ts_dir
student$ sudo chown postgres: /var/lib/postgresql/ts_dir
=> CREATE TABLESPACE cts LOCATION '/var/lib/postgresql/ts_dir' WITH (compression=zstd);
CREATE TABLESPACE
=> ALTER SYSTEM SET cfs_level = 19;
ALTER SYSTEM
=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)
=> \x \db+ cts \x
Expanded display is on.
List of tablespaces
-[ RECORD 1 ]-----+---------------------------
Name              | cts
Owner             | student
Location          | /var/lib/postgresql/ts_dir
Access privileges |
Options           | {compression=zstd}
Size              | 4 bytes
Description       |
Expanded display is off.

2. База данных

=> CREATE DATABASE pgpro_cfs TEMPLATE template0 TABLESPACE cts;
CREATE DATABASE
=> \c pgpro_cfs
You are now connected to database "pgpro_cfs" as user "student".
=> CREATE SCHEMA bookings;
CREATE SCHEMA

Загружаем определение таблицы и ее строки в новую базу:

student$ /opt/pgpro/ent-16/bin/pg_dump -d demo --table=tickets --section=pre-data --section=data | psql
-d pgpro_cfs
SET
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
SET
SET
SET
CREATE TABLE
ALTER TABLE
COMMENT
COMMENT

Page 22: Page 22

COMMENT
COMMENT
COMMENT
COMMENT
COPY 366733
=> ALTER TABLE bookings.tickets SET (autovacuum_enabled = off);
ALTER TABLE
=> ANALYZE bookings.tickets;
ANALYZE
=> CHECKPOINT;
CHECKPOINT
=> SELECT relname, relpages,
  pg_size_pretty(relpages*8192::numeric) original,
  pg_size_pretty(pg_table_size(oid)) compressed,
  cfs_compression_ratio(oid) ratio
FROM pg_class
WHERE relname = 'tickets';
 relname | relpages | original | compressed |       ratio
---------+----------+----------+------------+-------------------
 tickets |     6176 | 48 MB    | 22 MB      | 2.191779169781274
(1 row)

3. Удаление табличного пространства

=> \c student
You are now connected to database "student" as user "student".
=> DROP DATABASE pgpro_cfs (FORCE);
DROP DATABASE
=> DROP TABLESPACE cts;
DROP TABLESPACE
student$ sudo rm -rf /var/lib/postgresql/ts_dir

Уборка

Сбросим настройку параметра конфигурации.

=> ALTER SYSTEM RESET cfs_level;
ALTER SYSTEM
student$ sudo systemctl reload postgrespro-ent-16.service

Page 23: Практика PPEM

  1. В экземпляре ent-16 создайте сжатое табличное пространство, использующее алгоритм lz4, и разместите его в каталоге /var/lib/postgresql/lz4_cfs. 2. Создайте в сжатом табличном пространстве базу данных demo_cfs из шаблона demo.
    1. Сравните размеры баз данных demo и demo_cfs. 4. Удалите базу данных и сжатое пространство.

Page 24: 1. Создание табличного пространства

На главной странице откройте список экземпляров, щелкнув по блоку «Экземпляры», и выберите ent-16. В навигационной панели выберите вкладку «Табличные пространства» и нажмите «Создать пространство». В открывшемся окне введите название, требуемый каталог размещения и способ сжатия, а затем нажмите «Сохранить».

2. Создание базы данных из шаблона

В навигационной панели, на вкладке «Базы данных», нажмите «Создать базу данных». В диалоговом окне введите название, выберите требуемый шаблон и табличное пространство и нажмите «Сохранить».

3. Сравнение размеров данных таблиц

Дождитесь обновления информации по созданной базе в списке баз данных — в нем должны появиться сведения о количестве таблиц и индексов и их размерах, а также о размере базы. Сравните размер исходной базы шаблона и новой базы.

4. Удаление табличного пространства

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

07 Занятие 07: Производительность / Транзакции и сеансы

Исходная страница в нативном Markdown: Производительность / Транзакции и сеансы
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_07_transactions.html Archive: PGPRO-handouts-16.zip Страниц PDF: 36

Page 1: Производительность / Транзакции и сеансы

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • 64-битные номера транзакций
  • Автономные транзакции
  • Данные в памяти
  • Встроенный пул соединений

Page 3: 64-битные транзакции

Формат данных

  • Заморозка

Page 4: 32-битные номера

Source slide page 4: 32-битные номера{#source-figure-004 caption=“Source slide page 4: 32-битные номера”}

PostgreSQL, Postgres Pro Standard

  • 32-х разрядный xid (~ 4 млрд. транзакций)
  • зацикливание идентификаторов
  • необходимость заморозки

Комментарии преподавателя

32-битные идентификаторы транзакций допускают примерно четыре миллиарда уникальных значений. В современных реалиях при большой нагрузке идентификаторы могут закончиться через несколько дней и работа СУБД станет невозможна. Поэтому применяют заморозку: версии строк с достаточно большими xmin помечаются как очень старые и, следовательно, видимые во всех снимках данных. После заморозки версий с определенным номером во всех таблицах кластера номер можно использовать повторно (т. н. transaction wraparound).

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

Page 5: 64-битные номера

Source slide page 5: 64-битные номера{#source-figure-005 caption=“Source slide page 5: 64-битные номера”}

Postgres Pro Enterprise

  • 64-х разрядный счетчик транзакций
  • не подвержены зацикливанию
00000006
000000c3 0fe9a1be
000000c3 0fe9a1c4

Комментарии преподавателя

Использование 64-битного счетчика позволило бы иметь практически бесконечное число идентификаторов транзакций, отказаться от их повторного использования и связанных с этим действий по администрированию.

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

Поэтому в Postgres Pro Enterprise реализован компромиссный вариант 64-х битных идентификаторов: в специальной области в конце страницы хранится 64-битное базовое значение xid, а в полях xmin и xmax заголовка версии строки — 32-битное смещение. 64-битный номер транзакции xmin или xmax вычисляется сложением базового значения и значения, хранящегося в заголовке версии строки.

Такой же формат применяется для идентификаторов мультитранзакций: базовое значение хранится в специальной области страницы, а смещение — в поле xmax заголовка версии строки.

https://habr.com/ru/company/postgrespro/blog/707968/

Page 6: Заморозка

Синхронная

  • если смещения не хватает и необходимо увеличить базовое значение
  • При очистке
    • уменьшает pg_xact и pg_multixact
    • vacuum_freeze_min_age = 50 млн
    • vacuum_freeze_table_age = 150 млн
    • autovacuum_freeze_max_age = 10 млрд
    • vacuum_failsafe_age = 1.6 млрд
    • vacuum_multixact_freeze_min_age = 5 млн
    • vacuum_multixact_freeze_table_age = 150 млн
    • autovacuum_multixact_freeze_max_age = 20 млрд
    • vacuum_multixact_failsafe_age = 1.6 млрд

Комментарии преподавателя

Поскольку смещения в заголовке версии строки 32-битные, диапазон значений xid в пределах страницы — от базового до базового плюс 232−1. Если в какую-либо версию строки на странице нужно записать больший номер, сервер увеличивает базовое значение xid. Если при этом версии строк с небольшими номерами xid препятствуют такому увеличению, сервер замораживает эти версии «на лету».

Поскольку 64-битные номера транзакций не используются повторно, своевременная заморозка не критична для работы многоверсионности: параметр autovacuum_freeze_max_age имеет значение по умолчанию 10 млрд вместо 200 млн в обычном PostgreSQL, и значение autovacuum_multixact_freeze_max_age также увеличено. Однако заморозка все же выполняется как часть процедуры очистки, чтобы уменьшать размер CLOG (подкаталога pg_xact) и статусов мультитранзакций (pg_multixact).

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

В PostgresPro Enterprise 17 появилась фоновая заморозка: дополнительный процесс выполняет очистку и заморозку недавно измененных страниц. Ее рекомендуется включать, если автоочистка не справляется со своими задачами.

https://postgrespro.ru/docs/enterprise/16/routine-vacuuming#VACUUM-FOR -WRAPAROUND

Page 7: Структура страниц

Чтобы заглянуть в страницы, воспользуемся расширением pageinspect.

=> CREATE DATABASE pgpro_transactions;
CREATE DATABASE
=> \c pgpro_transactions
You are now connected to database "pgpro_transactions" as user "student".
=> CREATE EXTENSION pageinspect;
CREATE EXTENSION

В тестовой таблице будет две страницы по две версии строки в каждой.

=> CREATE TABLE test(
  s char(300)
)
WITH (fillfactor = 10);
CREATE TABLE
=> INSERT INTO test
SELECT g.n FROM generate_series(1,4) AS g(n);
INSERT 0 4

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

=> SELECT p.page, h.xid_base
FROM
  (VALUES (0), (1)) p(page),
  page_header(get_raw_page('test', p.page)) h;
 page | xid_base
------+----------
    0 |      962
    1 |      962
(2 rows)

В заголовках версий строк хранится 32-битное смещение (хотя значения xmin и xmax для удобства выводятся уже приведенными к 64-битному виду):

=> SELECT ctid, xmin, xmax FROM test;
 ctid  | xmin | xmax
-------+------+------
 (0,1) |  965 |    0
 (0,2) |  965 |    0
 (1,1) |  965 |    0
 (1,2) |  965 |    0
(4 rows)

Текущее значение счетчика транзакций:

SELECT pg_current_xact_id();
966

Пусть теперь сервер выполнит 2^32 транзакций. Выполнение такого количества транзакций работающим сервером заняло бы очень много времени, поэтому остановим его и воспользуемся утилитой pg_resetwal, чтобы поменять текущее значение счетчика.

student$ sudo systemctl stop postgrespro-ent-16.service
postgres$ /opt/pgpro/ent-16/bin/pg_resetwal -x 4294968262 -D /var/lib/pgpro/ent-16
Write-ahead log reset

Для гарантированного запуска сервера после изменения счетчика создадим пустой файл статусов транзакций:

student$ sudo -u postgres truncate /var/lib/pgpro/ent-16/pg_xact/000000000040 --size=16M
student$ sudo systemctl start postgrespro-ent-16.service

Page 8: Page 8

student$ psql pgpro_transactions
=> SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
         4294968262
(1 row)
Примечание

Внимание! Категорически запрещается применять утилиту pg_resetwal в производственной среде за исключением использования ее по прямому указанию технической поддержки. Утилита изменяет внутренние структуры данных, что может привести к потере или искажению информации. Теперь изменим одну строку таблицы.

=> UPDATE test SET s = '10' WHERE s = '1';
UPDATE 1

При этом сервер запишет номер текущей транзакции в поле xmax старой версии строки и в поле xmin новой версии, которая будет размещена в нулевой странице. Поскольку номер текущей транзакции отличается от значений xmin в существующих версиях более чем на 2^32, сервер увеличит базовый xid и выполнит локальную заморозку в нулевой странице. Для этого он выставит в заголовках версий соответствующий флаг, а в поле xmin запишет зарезервированное значение 2, означающее «номер заморожен». В первой странице заморозка не делается, поэтому базовый номер xid изменился только в нулевой странице.

=> SELECT (p.page, i.lp) ctid, i.t_xmin, i.t_xmax, h.xid_base, f.combined_flags
FROM
  (VALUES (0), (1)) p(page),
  heap_page_items(get_raw_page('test', p.page)) i,
  page_header(get_raw_page('test', p.page)) h,
  heap_tuple_infomask_flags(t_infomask, t_infomask2) f;
 ctid  |   t_xmin   |   t_xmax   |  xid_base  |   combined_flags
-------+------------+------------+------------+--------------------
 (0,1) |          2 | 4294968263 | 4278191045 | {HEAP_XMIN_FROZEN}
 (0,2) |          2 |          0 | 4278191045 | {HEAP_XMIN_FROZEN}
 (0,3) | 4294968263 |          0 | 4278191045 | {}
 (1,1) |        965 |          0 |        962 | {}
 (1,2) |        965 |          0 |        962 | {}
(5 rows)

Page 9: Автономные транзакции

Устройство автономных транзакций

  • Видимость данных в автономных транзакциях
  • Автономные транзакции в SQL
  • Автономные транзакции в PL/pgSQL и PL/Python
  • Ограничения автономных транзакций

Page 10: Автономные транзакции

Автономная транзакция — это независимая транзакция, которая запускается внутри родительской и должна быть завершена (зафиксирована или отменена) до завершения родительской.

BEGIN;
  команды ...
  BEGIN AUTONOMOUS [ISOLATION LEVEL ...];
    команды ...
  COMMIT [AUTONOMOUS];  -- или ROLLBACK
  команды ...
COMMIT;    -- или ROLLBACK

Автономная транзакция может включать в себя другие автономные транзакции (транзакция верхнего уровня не может быть автономной). Когда вложенная автономная транзакция запускается, ее родительская транзакция приостанавливается и помещается в стек автономных транзакций. Когда автономная транзакция фиксируется или отменяется, родительская транзакция извлекается из стека и продолжает работу. Таким образом, на каждом уровне вложенности активной является только одна транзакция. Поэтому внутри автономной транзакции команды COMMIT или ROLLBACK будут завершать эту автономную транзакцию, а не родительскую. В отличие от вложенной транзакции, которая возникает в обычном PostgreSQL при выполнении команды SAVEPOINT (и, возможно, ROLLBACK TO), результат автономной транзакции не зависит от исхода родительской. В процедурных языках автономные транзакции тоже завершаются до завершения родительских, но происходит это неявно. Например, в PL/pgSQL добавлена конструкция для блока, операторы которого должны выполняться в автономной транзакции:

BEGIN AUTONOMOUS
  операторы ...
END;

По сути, такая запись — просто сокращение для

BEGIN
  BEGIN AUTONOMOUS;
    операторы ...
  COMMIT;
END;

Ошибка в блоке с автономной транзакцией приводит к ее откату, после чего ошибка обрабатывается в секции EXCEPTION или передается в вызывающий блок. В PL/Python добавлен метод, позволяющий выполнить SQL в автономной транзакции:

with plpy.autonomous():
  plpy.execute(...)

Page 11: Видимость

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

  • Read Committed видит изменения
  • Repeatable Read, Serializable не видят изменений
  • Возможны взаимоблокировки

Комментарии преподавателя

Родительская и дочерняя автономная транзакция используют собственные снимки данных, время жизни которых зависит от уровня изоляции. Поэтому родительская транзакция с уровнем изоляции Read Committed будет видеть все изменения дочерних автономных транзакций, а родительская с уровнем изоляции Repeatable Read или Serializable не увидит никаких изменений.

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

Page 12: Ограничения

max\_autonomous\_transactions = 100

  • Вложенность до 128 уровней
  • Не поддерживается блокировка FOR UPDATE
  • Нельзя обращаться к временным схемам
  • Нельзя изменить уровень изоляции в PL/pgSQL и PL/Python
  • Не поддерживаются в расширениях
    • in_memory
    • online_analyze
  • Нельзя создавать слоты логической репликации

Комментарии преподавателя

Одновременно можно запустить не более max_autonomous_transactions автономных транзакций во всех сеансах (по умолчанию 100).

Глубина вложенности автономных транзакций — не более 128.

FOR UPDATE не поддерживается, поэтому, например, нельзя заблокировать строку в родительской транзакции, а в дочерней автономной пропустить ее с помощью SKIP LOCKED.

В автономной транзакции нельзя обращаться к объектам временных схем, в частности, ко временным таблицам.

В блоке PL/pgSQL и методе PL/Python нельзя задать уровень изоляции автономной транзакции, отличающийся от текущего.

Автономные транзакции не работают с расширениями in_memory и online_analyze.

Также нельзя создавать слоты логической репликации из автономной транзакции.

https://postgrespro.ru/docs/enterprise/16/atx

Page 13: Использование автономных транзакций

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

=> TRUNCATE test;
TRUNCATE TABLE

Мы хотим записывать в таблицу test_audit информацию обо всех изменениях таблицы test, в том числе и о тех, которые не были зафиксированы.

=> CREATE TABLE test_audit(
  time timestamptz,
  username text,
  operation text
);
CREATE TABLE

Для этого понадобятся триггер и триггерная функция, тело которой выполняется в автономной транзакции.

=> CREATE FUNCTION test_audit() RETURNS trigger AS $$
BEGIN AUTONOMOUS
  INSERT INTO test_audit VALUES (now(), current_user, tg_op);
  RETURN new;
END;
$$ LANGUAGE plpgsql;
CREATE FUNCTION
=> CREATE TRIGGER test_audit
AFTER INSERT OR UPDATE OR DELETE
ON test
FOR EACH ROW
EXECUTE FUNCTION test_audit();
CREATE TRIGGER

Теперь информация об изменениях будет сохраняться в таблице test_audit, даже если транзакция впоследствии обрывается.

=> BEGIN;
BEGIN
=> INSERT INTO test VALUES ('value1'),('value2');
INSERT 0 2
=> UPDATE test SET s = 'value3';
UPDATE 2
=> DELETE FROM test;
DELETE 2

Откатим транзакцию:

=> ROLLBACK;
ROLLBACK

В таблице ничего не осталось:

=> SELECT * FROM test;
 s
---
(0 rows)
=> SELECT * FROM test_audit;

Page 14: Page 14

             time              | username | operation
-------------------------------+----------+-----------
 2026-07-27 23:39:08.42909+03  | student  | INSERT
 2026-07-27 23:39:08.42909+03  | student  | INSERT
 2026-07-27 23:39:08.488845+03 | student  | UPDATE
 2026-07-27 23:39:08.488845+03 | student  | UPDATE
 2026-07-27 23:39:08.537416+03 | student  | DELETE
 2026-07-27 23:39:08.537416+03 | student  | DELETE
(6 rows)

Однако попытки изменить данные записаны в таблицу аудита.

Page 15: Данные в памяти

pg\_variables

  • in_memory

Page 16: Расширение pg_variables

Функции для работы с переменными различных типов

  • скалярные
  • массивы
  • коллекции записей и произвольных типов
  • Поддержка транзакционности
  • Хранение в памяти сеанса
    • работает на реплике
    • несовместимо с пулами соединений

Комментарии преподавателя

Расширение pg_variables позволяет работать в памяти сеанса со скалярными переменными, массивами и коллекциями. Переменные объединяются в так называемые пакеты, выполняющие роль пространств имен.

Коллекции представляют собой множества пар ключ—значение. В качестве таких пар могут выступать обычные составные значения (записи), в которых первый атрибут считается ключом. Это позволяет использовать коллекции в качестве быстрой альтернативы временным таблицам, которая к тому же работает и на физических репликах.

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

Утилита ora2pgpro, рассмотренная в теме «Миграция», использует pg_variables для экспорта ассоциативных массивов Oracle в коллекции.

Расширение несовместимо с пулом соединений, включая встроенный пул Postgres Pro.

https://postgrespro.ru/docs/enterprise/16/pg-variables

Page 17: Расширение pg_variables

Установим расширение в базе данных:

=> CREATE EXTENSION pg_variables;
CREATE EXTENSION

Расширение оперирует термином «пакет», под которым понимается пространство имен переменных. Команды ниже автоматически создадут пакет vars (первый параметр), поместят в него две переменные int_n и int_t (второй параметр) целочисленного типа (тип определяется по фактическому типу третьего параметра) и зададут им значения. Первая переменная нетранзакционная (по умолчанию):

=> SELECT pgv_set('vars', 'int_n', 10);
 pgv_set
---------
(1 row)

Вторая — транзакционная:

=> SELECT pgv_set('vars', 'int_t', 20, is_transactional => true);
 pgv_set
---------
(1 row)

С помощью функций можно получить информацию о переменных и прочитать их значения:

=> SELECT * FROM pgv_list() ORDER BY package, name;
 package | name  | is_transactional
---------+-------+------------------
 vars    | int_n | f
 vars    | int_t | t
(2 rows)
=> SELECT pgv_get('vars', 'int_n', NULL::integer), pgv_get('vars', 'int_t', NULL::integer);
 pgv_get | pgv_get
---------+---------
      10 |      20
(1 row)

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

=> BEGIN;
BEGIN
=> SELECT pgv_set('vars', 'int_n', 111), pgv_set('vars', 'int_t', 222, true);
 pgv_set | pgv_set
---------+---------
         |
(1 row)
=> ROLLBACK;
ROLLBACK

Читаем значения переменных:

=> SELECT pgv_get('vars', 'int_n', NULL::int), pgv_get('vars', 'int_t', NULL::int);
 pgv_get | pgv_get
---------+---------
     111 |      20
(1 row)

Page 18: Page 18

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

=> SELECT pgv_set('vars', 'int_t', 333);
ERROR:  variable "int_t" already created as TRANSACTIONAL

Работа с массивами организована аналогично. Рассмотрим работу с коллекциями записей. Запрос ниже вставляет в коллекцию tbl_1 пакета tbls несколько записей. Если пакет или коллекция не существуют (как в нашем случае), они будут созданы автоматически:

=> SELECT pgv_insert('tbls', 'tbl_1', tbl) FROM
   (SELECT gs, 'Line_' || gs FROM generate_series(1, 4) AS gs) AS tbl;
 pgv_insert
------------
(4 rows)

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

=> SELECT pgv_insert('tbls', 'tbl_1', (2, 'Another 2nd line'));
ERROR:  there is an element in the variable "tbl_1" with same key

Мы можем изменять или удалять записи, обращаясь к ним по ключу:

=> SELECT pgv_update('tbls', 'tbl_1', (2, '2nd line again'));
 pgv_update
------------
 t
(1 row)
=> SELECT pgv_delete('tbls', 'tbl_1', 3);
 pgv_delete
------------
 t
(1 row)

Что получилось в результате? Прочитаем все данные из коллекции:

=> SELECT pgv_select('tbls', 'tbl_1');
      pgv_select
----------------------
 (1,Line_1)
 (2,"2nd line again")
 (4,Line_4)
(3 rows)

Данные в коллекциях хранятся в порядке возрастания, по записям удобно перемещаться с помощью итераторов:

=> DO $$
DECLARE
  key integer;
BEGIN
  key := pgv_first('tbls', 'tbl_1', NULL::int);
  WHILE (key IS NOT NULL) LOOP
    RAISE NOTICE '%', key;
    key := pgv_next('tbls', 'tbl_1', key);
  END LOOP;
END;
$$;
NOTICE:  1
NOTICE:  2
NOTICE:  4
DO

Можно двигаться и в обратную сторону с помощью функций pgv_last и pgv_prior.

Page 19: Page 19

Количество элементов в коллекции вернет функция pgv_count:

=> SELECT pgv_count('tbls', 'tbl_1');
 pgv_count
-----------
         3
(1 row)

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

=> SELECT * FROM pgv_stats();
 package | allocated_memory
---------+------------------
 tbls    |            16384
 vars    |            16384
(2 rows)

Удалить переменную и весь пакет целиком можно при помощи функции pgv_remove:

=> SELECT pgv_remove('vars', 'int_n');  -- удаляем переменную
 pgv_remove
------------
(1 row)
=> SELECT pgv_exists('vars', 'int_n');  -- проверка существования переменной: ее уже нет
 pgv_exists
------------
 f
(1 row)
=> SELECT pgv_remove('vars');  -- удаляем весь пакет
 pgv_remove
------------
(1 row)

Функция pgv_free удаляет все пакеты и переменные:

=> SELECT pgv_free();
 pgv_free
----------
(1 row)

Page 20: Расширение in_memory

Обертка сторонних данных для оперативных таблиц

  • таблицы организованы по индексу
  • данные располагаются в листовых страницах B-дерева
  • Поддержка транзакционности
  • Хранение в общей памяти сервера
    • работает на реплике
    • несовместимо со встроенным пулом соединений и подготовленными
    • транзакциями

Комментарии преподавателя

С помощью расширения in_memory можно создавать небольшие оперативные таблицы, то есть таблицы в общей памяти сервера. Работа с таблицей происходит с помощью обертки сторонних данных.

Оперативные таблицы организованы по индексу — строки таблиц хранятся в страницах листьев индекса B-tree, что обеспечивает быстрый доступ к данным по первичному ключу (вторичные индексы не поддерживаются).

Для экономии памяти используются небольшие страницы размером 1 Кбайт. Поскольку индекс B-tree должен содержать на странице минимум три записи, максимальная длина строки ограничена 304 байтами (TOAST не поддерживается). Когда строки удаляются из оперативной таблицы, соответствующие страницы данных не освобождаются; для освобождения ресурсов требуется опустошение или удаление таблицы.

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

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

Расширение нельзя использовать с подготовленными (двухфазными) транзакциями и при работе со встроенным пулом соединений.

https://postgrespro.ru/docs/enterprise/16/in-memory

Page 21: Расширение in_memory

Для работы расширения необходимо загрузить библиотеку и перезапустить СУБД:

=> ALTER SYSTEM SET shared_preload_libraries = 'in_memory';
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql pgpro_transactions

Теперь установим расширение в базу:

=> CREATE EXTENSION in_memory;
CREATE EXTENSION

Доступ к оперативным таблицам организуется с помощью обертки сторонних данных, для этого создан сторонний сервер in_memory:

=> \des
          List of foreign servers
   Name    |  Owner  | Foreign-data wrapper
-----------+---------+----------------------
 in_memory | student | in_memory_fdw
(1 row)

Далее следует создать стороннюю таблицу на этом сервере — это и будет оперативная таблица:

=> CREATE FOREIGN TABLE optable (
  id integer NOT NULL,
  note text
)
SERVER in_memory;
CREATE FOREIGN TABLE

По умолчанию в таблице будет создан уникальный индекс B-tree по первому столбцу, с сортировкой по возрастанию. При необходимости можно указать другую структуру индекса в параметре indices предложения OPTIONS (несмотря на множественное число названия параметра, индекс может быть только один):

=> CREATE FOREIGN TABLE optable_bis (
  id integer,
  note text NOT NULL
)
SERVER in_memory
OPTIONS (indices 'UNIQUE (note)');
CREATE FOREIGN TABLE

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

=> \det+
                           List of foreign tables
 Schema |    Table    |  Server   |        FDW options        | Description
--------+-------------+-----------+---------------------------+-------------
 public | optable     | in_memory |                           |
 public | optable_bis | in_memory | (indices 'UNIQUE (note)') |
(2 rows)

С таблицами можно выполнять все основные операции DML. Внесем данные и прочитаем несколько строк:

=> INSERT INTO optable (SELECT gs, 'Line_' || gs FROM generate_series(1, 100) AS gs);
INSERT 0 100
=> SELECT * FROM optable LIMIT 3;

Page 22: Page 22

 id |  note
----+--------
  1 | Line_1
  2 | Line_2
  3 | Line_3
(3 rows)

В отличие от коллекций расширения pg_variables, оперативные таблицы хранятся в общей памяти сервера и доступны другим сеансам:

student$ psql pgpro_transactions
=> SELECT count(*) FROM optable;
 count
-------
   100
(1 row)
=> UPDATE optable SET note = 'Updated line' WHERE id = 100;
UPDATE 1
=> SELECT * FROM optable WHERE id = 100;
 id  |     note
-----+--------------
 100 | Updated line
(1 row)

Объем памяти, выделяемый для оперативных таблиц, задается параметром in_memory.shared_pool_size. Количество использованных и свободных страниц можно получить с помощью функции:

=> SELECT * FROM in_memory_page_stats();
 busy_pages | free_pages | all_pages
------------+------------+-----------
         10 |       8182 |      8192
(1 row)

При удалении строк из оперативных таблиц командой DELETE страницы данных не освобождаются:

=> DELETE FROM optable;
DELETE 100
=> SELECT busy_pages FROM in_memory_page_stats();
 busy_pages
------------
         10
(1 row)

Чтобы освободить страницы, можно опустошить (TRUNCATE) или удалить таблицу. Также содержимое таблицы пропадает при перезапуске сервера:

student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql pgpro_transactions
=> SELECT busy_pages FROM in_memory_page_stats();
 busy_pages
------------
          2
(1 row)

Page 23: Пул соединений

Проблема и решения

  • Встроенный пул
  • Подключение
  • Ограничения пула

Page 24: Проблема и решения

Один сеанс — один обслуживающий процесс

  • повышенный расход ресурсов
  • увеличение конкуренции
  • прогрев локальных кешей
  • Пул соединений в сервере приложений или драйвере
    • J2EE, JDBC, ODBC, node-postgres, Psycopg2
  • Пул соединений как сторонний продукт
    • PgBouncer, Odyssey
  • Встроенный пул Postgres Pro Enterprise сохраняет контекст сеанса

Комментарии преподавателя

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

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

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

Встроенный пул соединений Postgres Pro Enterprise позволяет воспользоваться преимуществами пула, сохранив при этом большинство возможностей, связанных с контекстом сеанса.

В 17-й версии Postgres Pro Enterprise на смену встроенному пулу придет proxima — расширение, объединяющее функциональность прокси-сервера и пула соединений.

Page 25: Встроенный пул

Source slide page 25: Встроенный пул{#source-figure-025 caption=“Source slide page 25: Встроенный пул”}

Комментарии преподавателя

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

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

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

Если клиент соединяется с базой данных, входящей в список dedicated_databases, или подключается под ролью, входящей в dedicated_users, для него будет создан выделенный процесс, не входящий в пулы. По умолчанию это сеансы пользователя postgres и подключения к базам template0, template1 и postgres. Количество выделенных процессов не ограничено.

https://postgrespro.ru/docs/enterprise/16/how-connection-pooler-works

Page 26: Подключение

Source slide page 26: Подключение{#source-figure-026 caption=“Source slide page 26: Подключение”}

Комментарии преподавателя

Подключение нового клиента устроено следующим образом.

Процесс postmaster слушает входящие соединения. Подключаясь, клиент передает стартовый пакет данных, в котором указаны параметры соединения — имя базы данных, имя роли. Postmaster передает этот пакет одному из процессов-приемников для анализа. Количество процессов-приемников определяется значением параметра connection_pool_workers и должно быть достаточным, чтобы анализ входящих соединений не стал узким местом.

Получив от приемника информацию о базе данных и роли, postmaster направляет соединение в подходящий пул. Применяется одна из трех стратегий, которые можно указать в параметре session_schedule: round-robin назначает процессы по очереди, random — случайным образом, load-balancing выбирает процесс с наименьшим числом сеансов.

Когда все соединения, использующие какой-то процесс, завершаются, процесс продолжает работать. Автоматическое завершение неиспользуемого процесса по истечении определенного времени настраивается параметром idle_pool_backend_timeout.

Для перезагрузки рабочих процессов можно установить параметр restart_pooler_on_reload = true и перечитать конфигурацию.

https://postgrespro.ru/docs/enterprise/16/connection-pooler-configuration

Page 27: Ограничения пула

Не поддерживаются SSL-соединения

  • Долгие транзакции задерживают работу других соединений
  • Не поддерживаются рекомендательные блокировки уровня сеанса
  • Нельзя перехватывать ошибку «connection to client lost»
  • Не работают модули, полагающиеся на состояние сеанса
  • Нельзя изменять параметры в других сеансах
  • Остановка сервера в режиме smart работает как fast
  • Параметр idle_session_timeout игнорируется

Комментарии преподавателя

Нужно учитывать следующие ограничения при использовании пула.

● SSL-соединения в текущей реализации не поддерживаются.

● Долгая транзакция будет задерживать работу транзакций в других соединениях, использующих тот же процесс.

● Ошибка «connection to client lost» обрабатывается пулом, чтобы завершать зависшие соединения по тайм-ауту, перехватывать ее не нужно.

● Не работают модули, использующие состояние на уровне сеанса, такие как plantuner, а также расширения multimaster, in_memory, online_analyze, pg_variables.

● Нельзя изменять параметры других сеансов.

● Не поддерживается передача сообщений между процессами (команды LISTEN, NOTIFY и т. п.).

● Остановка сервера в режиме smart работает как fast.

● Решения 1С не поддерживаются, они используют собственный пул соединений.

● Тайм-аут idle_session_timeout не работает.

https://postgrespro.ru/docs/enterprise/16/connection-pooler-limitations

Page 28: Встроенный пул соединений

Выделим в пуле по два процесса для каждой базы данных:

=> ALTER SYSTEM SET session_pool_size=2;
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service

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

student$ psql
=> \c pgpro_transactions
You are now connected to database "pgpro_transactions" as user "student".
=> BEGIN;
BEGIN
=> SELECT pg_backend_pid();
 pg_backend_pid
----------------
         806894
(1 row)

Сеанс может хранить свое состояние в пользовательских параметрах:

=> SELECT set_config('my.name', 'Я — сеанс 1', false);
 set_config
-------------
 Я — сеанс 1
(1 row)

Транзакции, начавшейся в другом сеансе, будет выделен другой процесс:

student$ psql
=> \c pgpro_transactions
You are now connected to database "pgpro_transactions" as user "student".
=> BEGIN;
BEGIN
=> SELECT pg_backend_pid();
 pg_backend_pid
----------------
         807105
(1 row)
=> SELECT set_config('my.name', 'Я — сеанс 2', false);
 set_config
-------------
 Я — сеанс 2
(1 row)

По умолчанию процессы выделяются по очереди (стратегия round-robin), поэтому третьему сеансу будет назначен процесс 806894. Но этот процесс занят выполнением транзакции первого сеанса, поэтому третий сеанс будет ждать:

student$ psql
=> \c pgpro_transactions

Когда транзакция первого сеанса заканчивается…

=> COMMIT;
COMMIT

…третий сеанс может воспользоваться освободившимся процессом 806894:

Page 29: Page 29

Source slide page 29: Page 29{#source-figure-029 caption=“Source slide page 29: Page 29”}

You are now connected to database "pgpro_transactions" as user "student".
=> SELECT pg_backend_pid();
 pg_backend_pid
----------------
         806894
(1 row)
=> SELECT set_config('my.name', 'Я — сеанс 3', false);
 set_config
-------------
 Я — сеанс 3
(1 row)

Заметим, что состояние сеансов сохраняется, несмотря на то что процесс выполняет транзакции разных сеансов:

=> SELECT pg_backend_pid(), current_setting('my.name');
 pg_backend_pid | current_setting
----------------+-----------------
         806894 | Я — сеанс 1
(1 row)
=> SELECT pg_backend_pid(), current_setting('my.name');
 pg_backend_pid | current_setting
----------------+-----------------
         806894 | Я — сеанс 3
(1 row)

Page 30: Итоги

64-битные идентификаторы транзакций упрощают обслуживание данных при высокой нагрузке

  • Автономные транзакции дают возможность удобно и эффективно реализовать аудит
  • Расширения pg_variables и in_memory могут служить альтернативой временным таблицам
  • Встроенный пул позволяет ограничить число соединений, не жертвуя контекстом сеанса

Page 31: Практика

  1. Воспроизведите взаимоблокировку основной транзакции и вложенной в нее автономной. 2. Реализуйте аудит изменений данных в таблице двумя способами: используя расширение dblink и автономные транзакции. Сравните производительность. 3. Убедитесь, что при использовании встроенного пула соединений сохраняется содержимое временных таблиц уровня сеанса.

Page 32: 1. Взаимоблокировка в автономной транзакции

Создадим базу данных:

=> CREATE DATABASE pgpro_transactions;
CREATE DATABASE
=> \c pgpro_transactions
You are now connected to database "pgpro_transactions" as user "student".
=> CREATE TABLE t1 AS SELECT 1 n;
SELECT 1

Начинаем транзакцию и блокируем строку:

=> BEGIN;
BEGIN
=> UPDATE t1 SET n=2;
UPDATE 1

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

=> BEGIN AUTONOMOUS;
BEGIN
=> UPDATE t1 SET n=3;
ERROR:  an ATX waiting for an ancestor transaction will cause a deadlock
=> END AUTONOMOUS;
ROLLBACK

Основную транзакцию можно зафиксировать:

=> COMMIT;
COMMIT

Тестовая таблица:

=> CREATE TABLE test(n int);
CREATE TABLE

Таблица для аудита:

=> CREATE TABLE test_audit(
  time timestamptz,
  username text,
  operation text
);
CREATE TABLE

Первый вариант триггерной функции использует расширение dblink:

=> CREATE EXTENSION dblink;
CREATE EXTENSION
=> CREATE OR REPLACE FUNCTION test_audit() RETURNS trigger AS $$
BEGIN
  PERFORM dblink(
    'dbname='||current_database(),
    'INSERT INTO test_audit VALUES (now(), current_user, '''||tg_op||''')'
  );
  RETURN new;
END;
$$ LANGUAGE plpgsql;
CREATE FUNCTION

Page 33: Page 33

Триггер:

=> CREATE TRIGGER test_audit
AFTER INSERT OR UPDATE OR DELETE
ON test
FOR EACH ROW
EXECUTE FUNCTION test_audit();
CREATE TRIGGER

Замеряем время.

=> \timing on
Timing is on.
=> INSERT INTO test SELECT g.s FROM generate_series(1,500) AS g(s);
INSERT 0 500
Time: 8614,633 ms (00:08,615)
=> \timing off
Timing is off.

Опустошим обе таблицы и заменим триггерную функцию на вариант, использующий автономную транзакцию.

=> TRUNCATE test;
TRUNCATE TABLE
=> TRUNCATE test_audit;
TRUNCATE TABLE
=> CREATE OR REPLACE FUNCTION test_audit() RETURNS trigger AS $$
BEGIN AUTONOMOUS
  INSERT INTO test_audit VALUES (now(), current_user, tg_op);
  RETURN new;
END;
$$ LANGUAGE plpgsql;
CREATE FUNCTION

Еще раз замеряем время.

=> \timing on
Timing is on.
=> INSERT INTO test SELECT g.s FROM generate_series(1,500) AS g(s);
INSERT 0 500
Time: 3739,022 ms (00:03,739)
=> \timing off
Timing is off.

Автономные транзакции работают быстрее, поскольку нет накладных расходов на установку соединения.

3. Временные таблицы и пул соединений

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

=> ALTER SYSTEM SET session_pool_size=1;
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql
student$ psql

В каждом из двух сеансов создадим временную таблицу.

=> CREATE TEMP TABLE temp (
  session int DEFAULT 1,
  pid int DEFAULT pg_backend_pid()
);
CREATE TABLE

Page 34: Page 34

Source slide page 34: Page 34{#source-figure-034 caption=“Source slide page 34: Page 34”}

=> CREATE TEMP TABLE temp (
  session int DEFAULT 2,
  pid int DEFAULT pg_backend_pid()
);
CREATE TABLE

Теперь пусть сеансы по очереди вставляют строки в таблицы.

=> INSERT INTO temp SELECT;
INSERT 0 1
=> INSERT INTO temp SELECT;
INSERT 0 1
=> INSERT INTO temp SELECT;
INSERT 0 1
=> INSERT INTO temp SELECT;
INSERT 0 1

Вот содержимое временных таблиц:

=> SELECT * FROM temp;
 session |  pid
---------+--------
       1 | 876870
       1 | 876870
(2 rows)
=> SELECT * FROM temp;
 session |  pid
---------+--------
       2 | 876870
       2 | 876870
(2 rows)

Встроенный пул соединений Postgres Pro Enterprise сохраняет временные таблицы уровня сеанса. При использовании сторонних менеджеров пула сохранение контекста сеанса не гарантируется.

Уборка

Удалим созданную базу данных и выключим пул соединений.

=> \c student
You are now connected to database "student" as user "student".
=> DROP DATABASE pgpro_transactions WITH (FORCE);
DROP DATABASE
=> ALTER SYSTEM RESET session_pool_size;
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service

Page 35: Практика PPEM

  1. Создайте базу данных newbase и в ней схему dblink. 2. Установите расширение dblink в одноименную схему базы newbase. 3. Используя расширение dblink и консоль psql, выведите список моделей самолетов из базы данных demo.

Комментарии преподавателя

  1. Получить данные по моделям самолетов можно с помощью запроса SELECT * FROM dblink.dblink( ‘dbname=demo’, ‘SELECT model FROM aircrafts’ ) AS (m text);

Page 36: 1. Создание базы данных и схемы

В навигационной панели выберите «Базы данных» и на главной странице нажмите «Создать базу данных». В открывшемся окне заполните необходимые поля и нажмите «Сохранить» — новая база появится в списке. Выбрав новую базу, нажмите «Создать схему», заполните поля на форме и выберите «Создать».

Выберите новую базу в списке на главной странице и на вкладке «Расширения» навигационной панели нажмите «Установить расширение». В открывшейся форме выберите из списка баз newbase, в списке доступных расширений найдите dblink и выберите для установки созданную в п. 1 одноименную схему. Нажмите кнопку «Установить».

В списке «Базы данных» главной страницы найдите newbase, и в столбце «Действия» нажмите на пиктограмму «{/}» (открыть PSQL-консоль) — откроется окно утилиты с приглашением, указывающим на подключение к выбранной базе. Для получения требуемой информации наберите и выполните запрос:

SELECT *
FROM dblink.dblink(
  'dbname=demo',
  'SELECT model FROM aircrafts'
) AS (m text);

Чтобы скопировать текст запроса из буфера обмена, используйте клавиши Ctrl+V, затем в контекстном меню выберите Paste или нажмите клавишу P.

08 Занятие 08: Производительность / Оптимизация запросов

Исходная страница в нативном Markdown: Производительность / Оптимизация запросов
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_08_qoptim.html Archive: PGPRO-handouts-16.zip Страниц PDF: 26

Page 1: Производительность / Оптимизация запросов

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Перенос статистики
  • Мониторинг выполнения
  • Указания оптимизатору

Page 3: Перенос статистики

Расширение dump\_stat

Page 4: Расширение dump_stat

Сохраняет статистику планировщика

  • SQL-скрипт с командами INSERT
  • Восстанавливает сохраненную статистику
    • можно восстановить на другом сервере (той же основной версии)
    • не требуется выполнение ANALYZE

Комментарии преподавателя

Модуль dump_stat позволяет выгрузить и восстановить содержимое таблицы pg_statistic.

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

Функция dump_statistic модуля выдает SQL-команды INSERT, которые можно записать в скрипт, а затем применить к совместимой базе данных.

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

При обновлении версии СУБД структура таблицы pg_statistic может поменяться, поэтому скрипт может оказаться несовместимым.

Подробная информация о модуле:

https://postgrespro.ru/docs/enterprise/16/dump-stat

Page 5: Расширение dump_stat

Создадим таблицу по образцу bookings.flights и соберем статистику.

=> \c demo
You are now connected to database "demo" as user "student".
=> CREATE TABLE public.flights
AS SELECT * FROM bookings.flights;
SELECT 33121
=> ANALYZE public.flights;
ANALYZE
=> SELECT attname, null_frac, avg_width, n_distinct
FROM pg_stats
WHERE tablename = 'flights' AND schemaname = 'public';
       attname       | null_frac  | avg_width | n_distinct
---------------------+------------+-----------+-------------
 flight_id           |          0 |         4 |          -1
 flight_no           |          0 |         7 |         710
 scheduled_departure |          0 |         8 | -0.30741826
 scheduled_arrival   |          0 |         8 | -0.28586093
 departure_airport   |          0 |         4 |         104
 arrival_airport     |          0 |         4 |         104
 status              |          0 |         8 |           6
 aircraft_code       |          0 |         4 |           8
 actual_departure    |     0.4935 |         8 |  -0.3595302
 actual_arrival      | 0.49523333 |         8 |  -0.3512575
(10 rows)

Подключим расширение dump_stat:

=> CREATE EXTENSION dump_stat;
CREATE EXTENSION

Перегруженная функция dump_statistic позволяет выгрузить статистику по всей базе данных, по таблицам и индексам заданной схемы или по конкретной таблице. Выгрузим статистику по таблице public.flights:

=> COPY (SELECT dump_statistic('public','flights')) TO '/tmp/flights.stat';
COPY 10

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

=> DROP TABLE public.flights;
DROP TABLE
=> CREATE TABLE public.flights
WITH (autovacuum_enabled = off)
AS SELECT * FROM bookings.flights;
SELECT 33121

Статистики пока нет:

=> SELECT attname, null_frac, avg_width, n_distinct
FROM pg_stats
WHERE tablename = 'flights' AND schemaname = 'public';
 attname | null_frac | avg_width | n_distinct
---------+-----------+-----------+------------
(0 rows)

Загрузим сохраненную ранее статистику:

=> \i /tmp/flights.stat

Page 6: Page 6

INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
=> SELECT attname, null_frac, avg_width, n_distinct
FROM pg_stats
WHERE tablename = 'flights' and schemaname = 'public';
       attname       | null_frac  | avg_width | n_distinct
---------------------+------------+-----------+-------------
 flight_id           |          0 |         4 |          -1
 flight_no           |          0 |         7 |         710
 scheduled_departure |          0 |         8 | -0.30741826
 scheduled_arrival   |          0 |         8 | -0.28586093
 departure_airport   |          0 |         4 |         104
 arrival_airport     |          0 |         4 |         104
 status              |          0 |         8 |           6
 aircraft_code       |          0 |         4 |           8
 actual_departure    |     0.4935 |         8 |  -0.3595302
 actual_arrival      | 0.49523333 |         8 |  -0.3512575
(10 rows)

Статистика успешно загрузилась. Расширение больше не требуется, удалим его.

=> DROP EXTENSION dump_stat;
DROP EXTENSION

Page 7: Мониторинг выполнения

Расширение pg\_query\_state

Page 8: Расширение pg_query_state

Информация о выполняющемся запросе

  • текущее состояние выполнения
  • актуальная статистика
  • Формат вывода как у EXPLAIN ANALYZE

Комментарии преподавателя

Модуль pg_query_state выводит текущее состояние выполнения запроса в работающем обслуживающем процессе. В отличие от команды EXPLAIN ANALYZE, предоставляющей данные об уже выполненном запросе, модуль pg_query_state информирует о ходе выполнения запроса, который работает в настоящий момент. Формат вывода практически идентичен выводу обычной команды EXPLAIN ANALYZE.

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

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

Подробная информация о модуле:

https://postgrespro.ru/docs/enterprise/16/pg-query-state

Page 9: Расширение pg_query_state

Подключим расширение и соответствующую разделяемую библиотеку.

=> CREATE EXTENSION pg_query_state;
CREATE EXTENSION
=> ALTER SYSTEM SET shared_preload_libraries = 'pg_query_state';
ALTER SYSTEM

Необходима перезагрузка экземпляра.

student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql demo

Откроем еще один сеанс:

student$ psql demo

Номер обслуживающего процесса второго сеанса:

=> SELECT * FROM pg_backend_pid();
 pg_backend_pid
----------------
         808573
(1 row)

Запустим во втором сеансе запрос. Запрос написан таким образом, что перебирает в цикле все перелеты из таблицы ticket_flights, для каждого из них выбирает номер бронирования из таблицы tickets, и по номеру находит дату бронирования в таблице booking. Это очень неэффективный способ получения результата, но большое время выполнения запроса позволит отследить в первом сеансе ход его выполнения. Мы выведем оценку стоимости и кардинальности (аналогичную той, что выдает команда EXPLAIN) и актуальную информацию о выполнении узлов плана.

=> SELECT tf.*, (
  SELECT (
    SELECT b.book_date FROM bookings b WHERE b.book_ref = t.book_ref
  )
  FROM tickets t WHERE t.ticket_no = tf.ticket_no
)
FROM ticket_flights tf;
=> SELECT plan FROM pg_query_state(808573, costs => true);
                                                                    plan
------------------------------------------------------------------------------------------
---------------------------------------------------
 Seq Scan on ticket_flights tf  (cost=0.00..17671152.14 rows=1045726 width=40) (Current
loop: actual rows=1, loop number=1)                 +
   SubPlan 2
                                                  +
     ->  Index Scan using tickets_pkey on tickets t  (cost=0.42..16.88 rows=1 width=8)
(actual rows=1 loops=1)                              +
           Index Cond: (ticket_no = tf.ticket_no)
                                                  +
           SubPlan 1
                                                  +
             ->  Index Scan using bookings_pkey on bookings b  (cost=0.42..8.44 rows=1
width=8) (Current loop: actual rows=1, loop number=1)+
                   Index Cond: (book_ref = t.book_ref)
(1 row)

Небольшая пауза…

=> SELECT plan FROM pg_query_state(808573, costs => true);

Page 10: Page 10

                                                              plan
------------------------------------------------------------------------------------------
--------------------------------------
 Seq Scan on ticket_flights tf  (cost=0.00..17671152.14 rows=1045726 width=40) (Current
loop: actual rows=65797, loop number=1)+
   SubPlan 2
                                     +
     ->  Index Scan using tickets_pkey on tickets t  (cost=0.42..16.88 rows=1 width=8)
(actual rows=1 loops=65797)             +
           Index Cond: (ticket_no = tf.ticket_no)
                                     +
           SubPlan 1
                                     +
             ->  Index Scan using bookings_pkey on bookings b  (cost=0.42..8.44 rows=1
width=8) (actual rows=1 loops=65797)    +
                   Index Cond: (book_ref = t.book_ref)
(1 row)

Сравните для узла Seq Scan прогноз числа строк (rows) с актуальной информацией (Current loop: rows). Таким образом можно обоснованно предположить, какая часть работы уже проделана. Узлы Index Scan вложенных планов выполняются во вложенных циклах. Для них выводятся среднее значение по прошедшим циклам (actual, по аналогии с выводом команды EXPLAIN ANALYZE) и информация о текущем, еще не завершенном цикле (Current loop). Как и команда EXPLAIN ANALYZE, расширение позволяет выводить информацию о времени выполнения узлов и о количестве использованных буферов, а также отслеживать параллельные запросы и запросы, выполняющиеся в вызываемых функциях. Удалим расширение и перезапустим экземпляр.

=> DROP EXTENSION pg_query_state;
DROP EXTENSION
=> ALTER SYSTEM RESET shared_preload_libraries;
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service

Page 11: Указания оптимизатору

Расширение pg\_hint\_plan

  • Расширение plantuner

Page 12: Расширение pg_hint_plan

Source slide page 12: Расширение pg_hint_plan{#source-figure-012 caption=“Source slide page 12: Расширение pg_hint_plan”}

Корректировка плана выполнения с помощью указаний

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

Комментарии преподавателя

Модуль pg_hint_plan разработан японской компанией NTT и включен в состав Postgres Pro Enterprise. Репозиторий проекта:

https://github.com/ossc-db/pg\_hint\_plan

Расширение предназначено для принудительной корректировки планов выполнения с помощью указаний (hints), помещаемых непосредственно в код SQL в виде комментариев специального вида /*+ … */. В случаях, когда принудительно выбранный план выполнить нельзя, планировщик игнорирует указания.

Обработчик pg_hint_plan считывает указания только из первого блочного комментария и немедленно прекращает разбор, обнаруживая недопустимый символ. Допустимыми символами являются буквы, цифры, пробелы, подчеркивания, запятые и скобки. Комментарий с указаниями не может содержать в себе другой блочный комментарий. Указания можно размещать в нескольких строках для улучшения читаемости.

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

Подробности в документации:

https://postgrespro.ru/docs/enterprise/16/pg-hint-plan

Page 13: Типы указаний

Метод доступа

  • Способ соединения
  • Порядок соединений
  • Корректировка числа строк соединения
  • Параллельная обработка таблицы
  • Установка параметра на время планирования

Комментарии преподавателя

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

Модуль поддерживает шесть типов указаний:

https://postgrespro.ru/docs/enterprise/16/pg-hint-plan#PG-HINT-PLAN-HINT -LIST

Методы доступа, способы соединения и другие вопросы, относящиеся к планам выполнения запросов, подробно рассматриваются в курсе QPT «Оптимизация запросов».

Page 14: Расширение pg_hint_plan

Расширение pg_hint_plan входит в Postgres Pro Enterprise в виде отдельного пакета, который устанавливается средствами ОС из репозитория. В виртуальной машине курса этот пакет уже установлен. Так как функционал расширения реализован в разделяемой библиотеке, она должна быть предварительно загружена в сеансе с помощью команды LOAD или указана в конфигурационном параметре shared_preload_libraries.

student$ psql demo
=> LOAD 'pg_hint_plan';
LOAD

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

=> EXPLAIN (costs off) SELECT count(*) FROM ticket_flights;
                      QUERY PLAN
-------------------------------------------------------
 Finalize Aggregate
   ->  Gather
         Workers Planned: 2
         ->  Partial Aggregate
               ->  Parallel Seq Scan on ticket_flights
(5 rows)

Для управления параллелизмом применяется указание Parallel. С параметром hard оно принудительно задает количество процессов. В этом примере мы отключаем параллельное выполнение, указывая ноль:

=> /*+ Parallel(ticket_flights 0 hard) */
EXPLAIN (costs off) SELECT count(*) FROM ticket_flights;
            QUERY PLAN
----------------------------------
 Aggregate
   ->  Seq Scan on ticket_flights
(2 rows)

Принудительно установим три рабочих процесса:

=> /*+ Parallel(ticket_flights 3 hard) */
EXPLAIN (costs off) SELECT count(*) FROM ticket_flights;
                      QUERY PLAN
-------------------------------------------------------
 Finalize Aggregate
   ->  Gather
         Workers Planned: 3
         ->  Partial Aggregate
               ->  Parallel Seq Scan on ticket_flights
(5 rows)

Параметр soft лишь меняет значение параметра max_parallel_workers_per_gather, а в остальном планировщику остается свобода выбора. В данном случае планировщик ограничился двумя процессами из-за относительного небольшого размера таблицы:

=> /*+ Parallel(ticket_flights 3 soft) */
EXPLAIN (costs off) SELECT count(*) FROM ticket_flights;
                      QUERY PLAN
-------------------------------------------------------
 Finalize Aggregate
   ->  Gather
         Workers Planned: 2
         ->  Partial Aggregate
               ->  Parallel Seq Scan on ticket_flights
(5 rows)

Указание Leading позволяет влиять на порядок соединений. Если указаны двойные скобки, то задается не только порядок, но и направление (внешняя таблица и внутренняя

Page 15: Page 15

таблица) соединений:

=> /*+ Leading((t (tf f))) */
EXPLAIN (costs off)
SELECT t.passenger_name, tf.flight_id, f.flight_no
FROM tickets t, ticket_flights tf, flights f
WHERE t.ticket_no = tf.ticket_no AND tf.flight_id = f.flight_id ;
                      QUERY PLAN
-------------------------------------------------------
 Hash Join
   Hash Cond: (t.ticket_no = tf.ticket_no)
   ->  Seq Scan on tickets t
   ->  Hash
         ->  Hash Join
               Hash Cond: (tf.flight_id = f.flight_id)
               ->  Seq Scan on ticket_flights tf
               ->  Hash
                     ->  Seq Scan on flights f
(9 rows)

Попробуем другой порядок:

=> /*+ Leading(((tf f) t)) */
EXPLAIN (costs off)
SELECT t.passenger_name, tf.flight_id, f.flight_no
FROM tickets t, ticket_flights tf, flights f
WHERE t.ticket_no = tf.ticket_no AND tf.flight_id = f.flight_id ;
                   QUERY PLAN
-------------------------------------------------
 Hash Join
   Hash Cond: (tf.ticket_no = t.ticket_no)
   ->  Hash Join
         Hash Cond: (tf.flight_id = f.flight_id)
         ->  Seq Scan on ticket_flights tf
         ->  Hash
               ->  Seq Scan on flights f
   ->  Hash
         ->  Seq Scan on tickets t
(9 rows)

Часто достаточно указать лишь несколько начальных таблиц, оставив планировщику свободу выбора для остальных соединений. В этом примере планировщик начнет с соединения таблиц f и tf, и поскольку пара помещена в одинарные скобки, сможет сам выбрать направление соединения:

=> /*+ Leading(f tf) */
EXPLAIN (costs off)
SELECT t.passenger_name, tf.flight_id, f.flight_no
FROM tickets t, ticket_flights tf, flights f
WHERE t.ticket_no = tf.ticket_no AND tf.flight_id = f.flight_id ;
                       QUERY PLAN
--------------------------------------------------------
 Nested Loop
   ->  Hash Join
         Hash Cond: (tf.flight_id = f.flight_id)
         ->  Seq Scan on ticket_flights tf
         ->  Hash
               ->  Seq Scan on flights f
   ->  Memoize
         Cache Key: tf.ticket_no
         Cache Mode: logical
         ->  Index Scan using tickets_pkey on tickets t
               Index Cond: (ticket_no = tf.ticket_no)
(11 rows)

Page 16: Таблица указаний

Таблица hint\_plan.hints

  • шаблон для выбора запросов
  • имя приложения для выбора сеанса
  • код указания
  • Указания в таблице приоритетнее, чем указания в коде
    • pg_hint_plan.enable_hint_table = on

Комментарии преподавателя

Часто бывает затруднительно изменить код SQL в приложении для явной вставки указаний. В таких случаях указания можно внести в специальную таблицу hint_plan.hints, разрешив ее использование с помощью параметра pg_hint_plan.enable_hint_table.

Идентификатор указания в этой таблице (поле id) заполняется автоматически.

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

Столбец application_name определяет, к каким сеансам будет применяться указание. Если указана пустая строка, будут выбираться все сеансы.

Столбец hint задает указания (без обрамляющей разметки комментариев).

Указания, заданные в таблице, имеют бо́льший приоритет, чем указания в комментариях.

Page 17: Таблица указаний

Вместо редактирования текстов запросов воспользуемся таблицей hint_plan.hints. Она создается при добавлении расширения в базу данных.

=> CREATE EXTENSION pg_hint_plan;
CREATE EXTENSION

Возьмем запрос с индексным доступом:

=> EXPLAIN (costs off)
SELECT * FROM bookings WHERE book_ref = 'CDE08B';
                 QUERY PLAN
---------------------------------------------
 Index Scan using bookings_pkey on bookings
   Index Cond: (book_ref = 'CDE08B'::bpchar)
(2 rows)

Поместим в таблицу указаний требование использовать последовательное сканирование. Константы, фигурирующие в целевом запросе, в столбце norm_query_string таблицы указаний должны заменяться знаками «?». В остальном текст запроса должен посимвольно совпадать, включая переводы строк:

=> INSERT INTO hint_plan.hints (norm_query_string, application_name, hints)
VALUES (
  E'EXPLAIN (costs off)\nSELECT * FROM bookings WHERE book_ref = ?;',
  'psql',
  'SeqScan(bookings)'
);
INSERT 0 1

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

=> SET pg_hint_plan.enable_hint_table TO on;
SET

Теперь план запроса содержит последовательное сканирование:

=> EXPLAIN (costs off)
SELECT * FROM bookings WHERE book_ref = 'CDE08B';
                  QUERY PLAN
-----------------------------------------------
 Gather
   Workers Planned: 1
   ->  Parallel Seq Scan on bookings
         Filter: (book_ref = 'CDE08B'::bpchar)
(4 rows)

Отключим расширение.

=> DROP EXTENSION pg_hint_plan;
DROP EXTENSION

Page 18: Расширение plantuner

Исключает индексы из рассмотрения оптимизатором

  • plantuner.disable_index
  • plantuner.enable_index
  • Обнуляет статистику для только что созданных таблиц
    • plantuner.fix_empty_table

Комментарии преподавателя

Расширение plantuner позволяет исключить из рассмотрения оптимизатора определенные индексы (pg_hint_plan не дает такой возможности). Оптимизатор не будет рассматривать планы, которые используют индексы, указанные в параметре plantuner.disable_index. Однако если имя индекса указать в параметре plantuner.enable_index, он все же будет доступен оптимизатору – таким образом можно разрешить или даже заставить оптимизатор использовать определенный индекс для конкретного запроса, задав параметр на уровне сеанса или функции.

Если таблица создана недавно, ее страницы находятся только в буферном кеше. В таком случае оптимизатор, оценивая число страниц и строк в таблице, иногда ошибается в бóльшую сторону. Указание plantuner.fix_empty_table = on обнуляет эти оценки.

https://postgrespro.ru/docs/enterprise/16/plantuner

Page 19: Расширение plantuner

Создадим для таблицы ticket_flights индексы по стоимости билета и по классу обслуживания:

=> \c demo
You are now connected to database "demo" as user "student".
=> CREATE INDEX ticket_flights_amount_idx ON ticket_flights(amount);
CREATE INDEX
=> CREATE INDEX ticket_flights_fare_conditions_idx ON ticket_flights(fare_conditions);
CREATE INDEX

Следующий запрос будет использовать индекс по стоимости для любого класса обслуживания:

=> \set fare Business
=> EXPLAIN (costs off)
SELECT avg(amount)
FROM ticket_flights
WHERE fare_conditions=:'fare' and amount>50000;
                          QUERY PLAN
--------------------------------------------------------------
 Aggregate
   ->  Bitmap Heap Scan on ticket_flights
         Recheck Cond: (amount > '50000'::numeric)
         Filter: ((fare_conditions)::text = 'Business'::text)
         ->  Bitmap Index Scan on ticket_flights_amount_idx
               Index Cond: (amount > '50000'::numeric)
(6 rows)
=> \set fare Economy
=> EXPLAIN (costs off)
SELECT avg(amount)
FROM ticket_flights
WHERE fare_conditions=:'fare' and amount>50000;
                         QUERY PLAN
-------------------------------------------------------------
 Aggregate
   ->  Bitmap Heap Scan on ticket_flights
         Recheck Cond: (amount > '50000'::numeric)
         Filter: ((fare_conditions)::text = 'Economy'::text)
         ->  Bitmap Index Scan on ticket_flights_amount_idx
               Index Cond: (amount > '50000'::numeric)
(6 rows)

Расширение plantuner позволяет исключить из рассмотрения индекс по стоимости, сохранив при этом возможность выбора метода доступа:

=> LOAD 'plantuner';
LOAD
=> SET plantuner.disable_index='ticket_flights_amount_idx';
SET

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

=> \set fare Business
=> EXPLAIN (costs off)
SELECT avg(amount)
FROM ticket_flights
WHERE fare_conditions=:'fare' and amount>50000;

Page 20: Page 20

                               QUERY PLAN
------------------------------------------------------------------------
 Aggregate
   ->  Bitmap Heap Scan on ticket_flights
         Recheck Cond: ((fare_conditions)::text = 'Business'::text)
         Filter: (amount > '50000'::numeric)
         ->  Bitmap Index Scan on ticket_flights_fare_conditions_idx
               Index Cond: ((fare_conditions)::text = 'Business'::text)
(6 rows)

А если селективность низкая, планировщик выбирает полное сканирование:

=> \set fare Economy
=> EXPLAIN (costs off)
SELECT avg(amount)
FROM ticket_flights
WHERE fare_conditions=:'fare' and amount>50000;
                                                QUERY PLAN
------------------------------------------------------------------------------------------
-----------------
 Finalize Aggregate
   ->  Gather
         Workers Planned: 2
         ->  Partial Aggregate
               ->  Parallel Seq Scan on ticket_flights
                     Filter: ((amount > '50000'::numeric) AND ((fare_conditions)::text =
'Economy'::text))
(6 rows)

Следует понимать, что указания оптимизатору, как правило, дают эффект лишь в частных случаях: при определенных значениях констант и определенном состоянии данных в таблицах.

Page 21: Итоги

Модуль dump\_stat позволяет выгружать и загружать статистику без необходимости выполнять ANALYZE

  • Расширение pg_query_state помогает отслеживать ход выполнения долгих запросов
  • Модули pg_hint_plan и plantuner позволяют управлять планами запросов

Page 22: Практика

  1. В базе данных demo выполните запрос с соединением таблиц tickets и ticket_flights по равенству значений столбцов ticket_no. Проверьте, что соединение выполняется с помощью слияния (merge join).
    1. Используя расширение pg_hint_plan, заставьте планировщик выбрать соединение хешированием (hash join).
    1. С помощью этого же расширения добейтесь, чтобы для доступа к таблице ticket_flights использовалось сканирование по битовой карте.
    1. Подключите расширение plantuner и запретите использовать индекс tickets_pkey при выполнении запроса из п. 3.

Комментарии преподавателя

  1. Используйте следующий запрос:
EXPLAIN (analyze, summary off, timing off, costs off)
SELECT t.ticket_no, t.passenger_name, tf.flight_id, tf.amount
FROM tickets t
  JOIN ticket_flights tf ON t.ticket_no = tf.ticket_no
ORDER BY t.ticket_no
2. Загрузите библиотеку командой LOAD 'pg_hint_plan'.
Используйте указание /*+ HashJoin(t tf) */.
3. Используйте указание /*+ BitmapScan(t) */.
4. Загрузите библиотеку командой LOAD 'plantuner'.

Запретите использовать индекс с помощью команды:

SET plantuner.disable_index='tickets_pkey';

Page 23: 1. Запрос с соединением слиянием

=> \c demo
You are now connected to database "demo" as user "student".

Получим план запроса с соединением таблиц tickets и ticket_flights.

=> EXPLAIN (analyze, summary off, timing off, costs off)
SELECT t.ticket_no, t.passenger_name, tf.flight_id, tf.amount
FROM tickets t JOIN ticket_flights tf ON t.ticket_no = tf.ticket_no
ORDER BY t.ticket_no;
                                          QUERY PLAN
------------------------------------------------------------------------------------------
-----
 Merge Join (actual rows=1045726 loops=1)
   Merge Cond: (t.ticket_no = tf.ticket_no)
   ->  Index Scan using tickets_pkey on tickets t (actual rows=366733 loops=1)
   ->  Index Scan using ticket_flights_pkey on ticket_flights tf (actual rows=1045726
loops=1)
(4 rows)

Соединение выполнено слиянием (merge join).

2. Указание выполнить соединение хешированием

Загрузим динамическую библиотеку pg_hint_plan:

=> LOAD 'pg_hint_plan';
LOAD

Используем указание HashJoin:

=> EXPLAIN (analyze, summary off, timing off, costs off)
/*+ HashJoin(t tf) */
SELECT t.ticket_no, t.passenger_name, tf.flight_id, tf.amount
FROM tickets t JOIN ticket_flights tf ON t.ticket_no = tf.ticket_no
ORDER BY t.ticket_no;
                                      QUERY PLAN
---------------------------------------------------------------------------------------
 Gather Merge (actual rows=1045726 loops=1)
   Workers Planned: 2
   Workers Launched: 2
   ->  Sort (actual rows=348575 loops=3)
         Sort Key: t.ticket_no
         Sort Method: external merge  Disk: 15328kB
         Worker 0:  Sort Method: external merge  Disk: 24048kB
         Worker 1:  Sort Method: external merge  Disk: 13840kB
         ->  Parallel Hash Join (actual rows=348575 loops=3)
               Hash Cond: (tf.ticket_no = t.ticket_no)
               ->  Parallel Seq Scan on ticket_flights tf (actual rows=348575 loops=3)
               ->  Parallel Hash (actual rows=122244 loops=3)
                     Buckets: 131072  Batches: 4  Memory Usage: 6944kB
                     ->  Parallel Seq Scan on tickets t (actual rows=122244 loops=3)
(14 rows)

3. Указание использовать сканирование по битовой карте

Используем указание BitmapScan:

=> EXPLAIN (analyze, summary off, timing off, costs off)
/*+ BitmapScan(t) */
SELECT t.ticket_no, t.passenger_name, tf.flight_id, tf.amount
FROM tickets t JOIN ticket_flights tf ON t.ticket_no = tf.ticket_no
ORDER BY t.ticket_no;

Page 24: Page 24

                                                     QUERY PLAN
------------------------------------------------------------------------------------------
--------------------------
 Gather Merge (actual rows=1045726 loops=1)
   Workers Planned: 2
   Workers Launched: 2
   ->  Sort (actual rows=348575 loops=3)
         Sort Key: t.ticket_no
         Sort Method: external merge  Disk: 15336kB
         Worker 0:  Sort Method: external merge  Disk: 23288kB
         Worker 1:  Sort Method: external merge  Disk: 14600kB
         ->  Nested Loop (actual rows=348575 loops=3)
               ->  Parallel Seq Scan on ticket_flights tf (actual rows=348575 loops=3)
               ->  Memoize (actual rows=1 loops=1045726)
                     Cache Key: tf.ticket_no
                     Cache Mode: logical
                     Hits: 81840  Misses: 219468  Evictions: 159603  Overflows: 0  Memory
Usage: 8193kB
                     Worker 0:  Hits: 117490  Misses: 339900  Evictions: 280032
Overflows: 0  Memory Usage: 8193kB
                     Worker 1:  Hits: 76196  Misses: 210832  Evictions: 150964
Overflows: 0  Memory Usage: 8193kB
                     ->  Bitmap Heap Scan on tickets t (actual rows=1 loops=770200)
                           Recheck Cond: (ticket_no = tf.ticket_no)
                           Heap Blocks: exact=219468
                           ->  Bitmap Index Scan on tickets_pkey (actual rows=1
loops=770200)
                                 Index Cond: (ticket_no = tf.ticket_no)
(21 rows)

4. Запрет использования индекса

Загрузим динамическую библиотеку plantuner:

=> LOAD 'plantuner';
LOAD

Запретим использовать индекс tickets_pkey:

=> SET plantuner.disable_index='tickets_pkey';
SET

Получим план:

=> EXPLAIN (analyze, summary off, timing off, costs off)
SELECT t.ticket_no, t.passenger_name, tf.flight_id, tf.amount
FROM tickets t JOIN ticket_flights tf ON t.ticket_no = tf.ticket_no
ORDER BY t.ticket_no;
                                          QUERY PLAN
------------------------------------------------------------------------------------------
-----
 Merge Join (actual rows=1045726 loops=1)
   Merge Cond: (tf.ticket_no = t.ticket_no)
   ->  Index Scan using ticket_flights_pkey on ticket_flights tf (actual rows=1045726
loops=1)
   ->  Materialize (actual rows=1045725 loops=1)
         ->  Sort (actual rows=366733 loops=1)
               Sort Key: t.ticket_no
               Sort Method: external merge  Disk: 14432kB
               ->  Seq Scan on tickets t (actual rows=366733 loops=1)
(8 rows)

Page 25: Практика PPEM

  1. Подготовьте базу данных demo основного экземпляра для работы с расширением pg_query_state.
    1. Откройте psql-консоль и запустите в ней длительно выполняющийся запрос из демонстрации.
    1. Используя вкладку «Активность» экземпляра, исследуйте ход выполнения запроса из п. 2.

Комментарии преподавателя

  1. Запрос для исследования выполнения:
SELECT tf.*, (
  SELECT (
    SELECT b.book_date FROM bookings b WHERE b.book_ref =
t.book_ref
  )
  FROM tickets t WHERE t.ticket_no = tf.ticket_no
)
FROM ticket_flights tf;
  1. Удобно, не закрывая psql-консоль, открыть вкладку «Активность» и установить в ней фильтр по базе данных.

Page 26: 1. Установка расширения pg_query_state

Выберите основной экземпляр ent-16 и на вкладке «Расширения» навигационной панели нажмите «Установить расширение». Заполните поля открывшейся формы и нажмите кнопку «Установить». На вкладке «Параметры» добавьте в параметр shared_preload_libraries библиотеку pg_query_state. Сохраните и примените параметр (потребуется перезапуск экземпляра).

2. Длительно выполняющийся запрос

В списке «Базы данных» найдите demo и в столбце «Действия» нажмите на пиктограмму «{/}» (открыть PSQL-консоль). В открывшемся окне наберите и запустите на выполнение запрос:

SELECT tf.*, (
  SELECT (
    SELECT b.book_date FROM bookings b WHERE b.book_ref = t.book_ref
  )
  FROM tickets t WHERE t.ticket_no = tf.ticket_no
)
FROM ticket_flights tf;

3. Отображение хода выполнения запроса

Не закрывая окно psql, откройте вкладку «Активность» и для процесса, выполняющего запрос, в столбце «Действия» нажмите на пиктограмму «План активного запроса». Откроется окно, в котором для отслеживания хода выполнения будет кнопка «Обновить статистику выполнения».

09 Занятие 09: Производительность / Адаптивная оптимизация

Исходная страница в нативном Markdown: Производительность / Адаптивная оптимизация
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_09_aqo.html Archive: PGPRO-handouts-16.zip Страниц PDF: 29

Page 1: Производительность / Адаптивная оптимизация

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Предназначение AQO
  • Принцип работы AQO
  • Обучение без классификации
  • Классификация запросов
  • Песочница

Page 3: Предназначение AQO

Исправление неверных оценок кардинальности, когда штатные средства не помогают

  • отклонение кардинальности от вычисленной планировщиком
  • AQO улучшает оценки в запросах с простыми условиями
    • атрибут оператор-сравнения константа
  • Основная сфера применения — сложные аналитические запросы

Комментарии преподавателя

Модуль адаптивной оптимизации запросов AQO (Adaptive Query Optimization) можно применить в тех случаях, когда стандартные инструменты улучшения оценок селективности и кардинальности (подробно описанные в курсе QPT) не срабатывают.

Модуль AQO работает с простыми условиями вида «атрибут = константа», «атрибут > константа» и подобными.

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

Основная сфера применения AQO — сложные аналитические запросы с повторяющейся структурой, для которых EXPLAIN ANALYZE выдает неверную оценку кардинальности.

Page 4: Пример запроса с неверной оценкой

Начнем с установки расширения:

=> ALTER SYSTEM SET shared_preload_libraries = 'aqo';
ALTER SYSTEM

Необходима перезагрузка экземпляра.

student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql demo

Добавим расширение AQO в базу данных:

=> CREATE EXTENSION aqo;
CREATE EXTENSION

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

=> EXPLAIN SELECT * FROM flights
WHERE departure_airport = 'VKO'
  AND flight_no = 'PG0007';
                                               QUERY PLAN
------------------------------------------------------------------------------------------
---------------
 Bitmap Heap Scan on flights  (cost=4.62..132.14 rows=2 width=63)
   Recheck Cond: (flight_no = 'PG0007'::bpchar)
   Filter: (departure_airport = 'VKO'::bpchar)
   ->  Bitmap Index Scan on flights_flight_no_scheduled_departure_key  (cost=0.00..4.62
rows=44 width=0)
         Index Cond: (flight_no = 'PG0007'::bpchar)
(5 rows)

Точное количество строк отличается от оценки:

=> SELECT count(*)
FROM flights
WHERE departure_airport = 'VKO'
  AND flight_no = 'PG0007';
 count
-------
    61
(1 row)

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

Page 5: Принцип работы AQO

Source slide page 5: Принцип работы AQO{#source-figure-005 caption=“Source slide page 5: Принцип работы AQO”}

Сбор статистики по выполняемым запросам и улучшение оценок количества строк с помощью машинного обучения

  • Лучшие оценки — основа более оптимальных планов

Комментарии преподавателя

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

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

Page 6: Принцип работы AQO

Source slide page 6: Принцип работы AQO{#source-figure-006 caption=“Source slide page 6: Принцип работы AQO”}

Комментарии преподавателя

Расширение AQO включается параметром aqo.enable.

При исполнении плана запроса AQO получает статистическую информацию (признаки, features) о реальной кардинальности каждого узла плана — происходит обучение модели. Полученные данные сохраняются в файлах и при запуске AQO загружаются в разделяемую память.

Когда статистическая информация накоплена, модель может использоваться для уточнения оценки планировщика.

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

Если установить параметру aqo.advanced значение on, выполняется классификация: в один класс попадают запросы, отличающиеся только константами. В таком случае статистика будет собираться и применяться для каждого класса отдельно.

Как именно обучается и используется модель, определяется режимом работы (параметр aqo.mode).

https://postgrespro.ru/docs/enterprise/16/aqo

Page 7: Без классификации

Режим learn

  • статистика применяется и собирается
  • начальное обучение
  • Режим frozen
    • статистика применяется, но новая не собирается
    • стабилизация

Комментарии преподавателя

Если классификация не применяется (aqo.advanced = off), все запросы относятся к одному общему классу. В этом случае имеют смысл два режима работы: learn и frozen.

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

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

Затем расширение переводится в режим frozen, в котором собранная статистика продолжает использоваться, но новые данные не собираются.

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

Page 8: Без классификации: режим learn

С помощью параметра aqo.enable включим aqo на уровне кластера и перечитаем конфигурацию:

=> ALTER SYSTEM SET aqo.enable = on;
ALTER SYSTEM
=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

По умолчанию устанавливается режим learn:

=> SHOW aqo.mode;
 aqo.mode
----------
 learn
(1 row)

Выполним показанный ранее запрос:

=> EXPLAIN (analyze, timing off, summary off)
SELECT * FROM flights
WHERE departure_airport = 'VKO'
  AND flight_no = 'PG0007';
                                                            QUERY PLAN
------------------------------------------------------------------------------------------
----------------------------------------
 Bitmap Heap Scan on flights  (cost=4.62..132.14 rows=2 width=63) (actual rows=61 loops=1)
   AQO not used, fss=-4388671362309763767
   Recheck Cond: (flight_no = 'PG0007'::bpchar)
   Filter: (departure_airport = 'VKO'::bpchar)
   Heap Blocks: exact=2
   ->  Bitmap Index Scan on flights_flight_no_scheduled_departure_key  (cost=0.00..4.62
rows=44 width=0) (actual rows=61 loops=1)
         Index Cond: (flight_no = 'PG0007'::bpchar)
 Using aqo: true
 AQO mode: LEARN
 AQO advanced: OFF
 Query hash: -6712624238167333493
 JOINS: 0
(12 rows)

Для AQO этот запрос является новым (AQO not used), поэтому для планирования использовались обычные оценки. Но AQO уже получил данные для обучения модели. Найдем статистику AQO по идентификатору запроса (query hash):

=> SELECT * FROM aqo_query_stat
WHERE queryid = -6712624238167333493 \gx
-[ RECORD 1 ]-----------------+---------------------
queryid                       | -6712624238167333493
dbid                          | 16388
execution_time_with_aqo       | {0.000174149}
execution_time_without_aqo    | {}
planning_time_with_aqo        | {0.001055677}
planning_time_without_aqo     | {}
cardinality_error_with_aqo    | {3.417726683613366}
cardinality_error_without_aqo | {}
executions_with_aqo           | 1
executions_without_aqo        | 0

Выполним запрос еще раз. Теперь используется уточненная оценка (AQO: error=0%):

=> EXPLAIN (analyze, timing off, summary off)
SELECT * FROM flights
WHERE departure_airport = 'VKO'
  AND flight_no = 'PG0007';

Page 9: Page 9

                                                            QUERY PLAN
------------------------------------------------------------------------------------------
----------------------------------------
 Bitmap Heap Scan on flights  (cost=4.64..132.16 rows=61 width=63) (actual rows=61
loops=1)
   AQO: rows=61, error=0%, fss=-4388671362309763767
   Recheck Cond: (flight_no = 'PG0007'::bpchar)
   Filter: (departure_airport = 'VKO'::bpchar)
   Heap Blocks: exact=2
   ->  Bitmap Index Scan on flights_flight_no_scheduled_departure_key  (cost=0.00..4.62
rows=44 width=0) (actual rows=61 loops=1)
         Index Cond: (flight_no = 'PG0007'::bpchar)
 Using aqo: true
 AQO mode: LEARN
 AQO advanced: OFF
 Query hash: -6712624238167333493
 JOINS: 0
(12 rows)

Количество выполнений с AQO увеличилось:

=> SELECT * FROM aqo_query_stat
WHERE queryid = -6712624238167333493 \gx
-[ RECORD 1 ]-----------------+--------------------------
queryid                       | -6712624238167333493
dbid                          | 16388
execution_time_with_aqo       | {0.000174149,0.000170702}
execution_time_without_aqo    | {}
planning_time_with_aqo        | {0.001055677,0.000129444}
planning_time_without_aqo     | {}
cardinality_error_with_aqo    | {3.417726683613366,0}
cardinality_error_without_aqo | {}
executions_with_aqo           | 2
executions_without_aqo        | 0

Спрогнозировать правильную кардинальность для других констант модель пока не может:

=> EXPLAIN (analyze, timing off, summary off)
SELECT * FROM flights
WHERE departure_airport = 'OVB'
  AND flight_no = 'PG0047';
                                                           QUERY PLAN
------------------------------------------------------------------------------------------
---------------------------------------
 Bitmap Heap Scan on flights  (cost=4.64..132.16 rows=61 width=63) (actual rows=9 loops=1)
   AQO: rows=61, error=85%, fss=-4388671362309763767
   Recheck Cond: (flight_no = 'PG0047'::bpchar)
   Filter: (departure_airport = 'OVB'::bpchar)
   Heap Blocks: exact=1
   ->  Bitmap Index Scan on flights_flight_no_scheduled_departure_key  (cost=0.00..4.62
rows=44 width=0) (actual rows=9 loops=1)
         Index Cond: (flight_no = 'PG0047'::bpchar)
 Using aqo: true
 AQO mode: LEARN
 AQO advanced: OFF
 Query hash: -6712624238167333493
 JOINS: 0
(12 rows)

Но, получив новые данные, модель выдает исправленную оценку:

=> EXPLAIN (analyze, timing off, summary off)
SELECT *
FROM flights f
WHERE f.departure_airport = 'OVB' AND f.flight_no = 'PG0047';

Page 10: Page 10

                                                           QUERY PLAN
------------------------------------------------------------------------------------------
---------------------------------------
 Bitmap Heap Scan on flights f  (cost=4.62..132.14 rows=9 width=63) (actual rows=9
loops=1)
   AQO: rows=9, error=0%, fss=-4388671362309763767
   Recheck Cond: (flight_no = 'PG0047'::bpchar)
   Filter: (departure_airport = 'OVB'::bpchar)
   Heap Blocks: exact=1
   ->  Bitmap Index Scan on flights_flight_no_scheduled_departure_key  (cost=0.00..4.62
rows=44 width=0) (actual rows=9 loops=1)
         Index Cond: (flight_no = 'PG0047'::bpchar)
 Using aqo: true
 AQO mode: LEARN
 AQO advanced: OFF
 Query hash: -6712624238167333493
 JOINS: 0
(12 rows)

Обратите внимание, что расширение ориентируется на дерево разбора, и поэтому не чувствительно к таким изменениям, как регистр текста, переносы строк, псевдонимы таблиц и т. п. Более того: поскольку статистика не делится по классам запросов, накопленная информация об узлах будет применяться к любым планам, которые содержат известные узлы (сравните подписи к узлам Nested Loop и Bitmap Heap Scan).

=> EXPLAIN (analyze, timing off, summary off)
SELECT * FROM flights f
  JOIN airports a ON a.airport_code = f.departure_airport
WHERE f.departure_airport = 'VKO'
  AND f.flight_no = 'PG0007';
                                                               QUERY PLAN
------------------------------------------------------------------------------------------
----------------------------------------------
 Nested Loop  (cost=4.64..167.87 rows=61 width=162) (actual rows=61 loops=1)
   AQO not used, fss=-7344153368946762870
   ->  Seq Scan on airports_data ml  (cost=0.00..4.30 rows=1 width=145) (actual rows=1
loops=1)
         AQO not used, fss=702648804789554594
         Filter: (airport_code = 'VKO'::bpchar)
         Rows Removed by Filter: 103
   ->  Bitmap Heap Scan on flights f  (cost=4.64..132.16 rows=61 width=63) (actual
rows=61 loops=1)
         AQO: rows=61, error=0%, fss=-4388671362309763767
         Recheck Cond: (flight_no = 'PG0007'::bpchar)
         Filter: (departure_airport = 'VKO'::bpchar)
         Heap Blocks: exact=2
         ->  Bitmap Index Scan on flights_flight_no_scheduled_departure_key
(cost=0.00..4.62 rows=44 width=0) (actual rows=61 loops=1)
               Index Cond: (flight_no = 'PG0007'::bpchar)
 Using aqo: true
 AQO mode: LEARN
 AQO advanced: OFF
 Query hash: -1728112134488687408
 JOINS: 1
(18 rows)

Без классификации: режим frozen

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

=> SET aqo.mode = frozen;
SET

Для известных констант сохраняется уточненная оценка:

=> EXPLAIN (analyze, timing off, summary off)
SELECT * FROM flights
WHERE departure_airport = 'OVB'
  AND flight_no = 'PG0047';

Page 11: Page 11

                                                           QUERY PLAN
------------------------------------------------------------------------------------------
---------------------------------------
 Bitmap Heap Scan on flights  (cost=4.62..132.14 rows=9 width=63) (actual rows=9 loops=1)
   AQO: rows=9, error=0%, fss=-4388671362309763767
   Recheck Cond: (flight_no = 'PG0047'::bpchar)
   Filter: (departure_airport = 'OVB'::bpchar)
   Heap Blocks: exact=1
   ->  Bitmap Index Scan on flights_flight_no_scheduled_departure_key  (cost=0.00..4.62
rows=44 width=0) (actual rows=9 loops=1)
         Index Cond: (flight_no = 'PG0047'::bpchar)
 Using aqo: true
 AQO mode: FROZEN
 AQO advanced: OFF
 Query hash: -6712624238167333493
 JOINS: 0
(12 rows)

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

=> EXPLAIN (analyze, timing off, summary off)
SELECT * FROM flights
WHERE departure_airport = 'DME'
  AND flight_no = 'PG0005';
                                                            QUERY PLAN
------------------------------------------------------------------------------------------
----------------------------------------
 Bitmap Heap Scan on flights  (cost=4.63..132.15 rows=31 width=63) (actual rows=26
loops=1)
   AQO: rows=31, error=16%, fss=-4388671362309763767
   Recheck Cond: (flight_no = 'PG0005'::bpchar)
   Filter: (departure_airport = 'DME'::bpchar)
   Heap Blocks: exact=2
   ->  Bitmap Index Scan on flights_flight_no_scheduled_departure_key  (cost=0.00..4.62
rows=44 width=0) (actual rows=26 loops=1)
         Index Cond: (flight_no = 'PG0005'::bpchar)
 Using aqo: true
 AQO mode: FROZEN
 AQO advanced: OFF
 Query hash: -6712624238167333493
 JOINS: 0
(12 rows)

Page 12: Классификация запросов

Режимы learn и frozen

  • Режим controlled
    • статистика применяется (use_aqo),
    • но обновляется (learn_aqo) только для известных запросов
    • стабилизация без «заморозки»
  • Режим intelligent
    • автоматическое управление обучением и применением (auto_tune)
    • сначала только сбор статистики;
    • применение при реальном улучшении, иначе — отказ от использования
  • Тонкая настройка
    • ручное управление свойствами use_aqo, learn_aqo, auto_tune

Комментарии преподавателя

При включенной классификации (aqo.advanced = on) статистика собирается отдельно для каждого класса запросов; класс составляют запросы, у которых деревья разбора отличаются только константами. В этом случае к уже рассмотренным режимам работы learn и frozen добавляются еще два: controlled и intelligent (без классификации они не отличаются от обычного learn).

В режиме controlled модель применяется для уже известных классов запросов: она продолжает обучаться на них (свойство learn_aqo) и используется для уточнения оценок (use_aqo). Но для новых классов запросов статистика не собирается. Этот режим можно использовать вместо frozen, чтобы тратить ресурсы только на нужные запросы, но при этом не «замораживать» статистику, а продолжать обновлять ее.

В режиме intelligent расширение самостоятельно управляет обучением и использованием (свойство auto_tune). Новые запросы сначала выполняются шесть раз без AQO, а затем обученная модель начинает использоваться (включается свойство use_aqo). Далее модель может выключаться или включаться в зависимости от того, уменьшает ли ее использование время планирования и выполнения запроса. Если после 50 итераций улучшения не происходит, AQO перестает собирать статистику по данному классу запросов и пытаться ее применить (выключает свойства use_aqo и learn_aqo).

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

Свойствами use_aqo, learn_aqo и auto_tune можно управлять вручную отдельно для каждого класса запросов.

Page 13: С классификацией: режим controlled

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

=> SET aqo.advanced = on;
SET
=> SELECT aqo_reset();
 aqo_reset
-----------
         5
(1 row)

Сначала обучим модель на запросах одного класса (класс составляют запросы с одинаковым деревом разбора без учета констант).

=> SET aqo.mode = learn;
SET
=> EXPLAIN (analyze, timing off, summary off)
SELECT * FROM flights
WHERE departure_airport = 'VKO'
  AND flight_no = 'PG0007';
                                                            QUERY PLAN
------------------------------------------------------------------------------------------
----------------------------------------
 Bitmap Heap Scan on flights  (cost=4.62..132.14 rows=2 width=63) (actual rows=61 loops=1)
   AQO not used, fss=-4388671362309763767
   Recheck Cond: (flight_no = 'PG0007'::bpchar)
   Filter: (departure_airport = 'VKO'::bpchar)
   Heap Blocks: exact=2
   ->  Bitmap Index Scan on flights_flight_no_scheduled_departure_key  (cost=0.00..4.62
rows=44 width=0) (actual rows=61 loops=1)
         Index Cond: (flight_no = 'PG0007'::bpchar)
 Using aqo: true
 AQO mode: LEARN
 AQO advanced: ON
 Query hash: -6712624238167333493
 JOINS: 0
(12 rows)
=> EXPLAIN (analyze, timing off, summary off)
SELECT * FROM flights
WHERE departure_airport = 'OVB'
  AND flight_no = 'PG0047';
                                                           QUERY PLAN
------------------------------------------------------------------------------------------
---------------------------------------
 Bitmap Heap Scan on flights  (cost=4.64..132.16 rows=61 width=63) (actual rows=9 loops=1)
   AQO: rows=61, error=85%, fss=-4388671362309763767
   Recheck Cond: (flight_no = 'PG0047'::bpchar)
   Filter: (departure_airport = 'OVB'::bpchar)
   Heap Blocks: exact=1
   ->  Bitmap Index Scan on flights_flight_no_scheduled_departure_key  (cost=0.00..4.62
rows=44 width=0) (actual rows=9 loops=1)
         Index Cond: (flight_no = 'PG0047'::bpchar)
 Using aqo: true
 AQO mode: LEARN
 AQO advanced: ON
 Query hash: -6712624238167333493
 JOINS: 0
(12 rows)

Если раньше информация об известных узлах применялась ко всем запросам, то теперь для каждого класса собирается собственная статистика. Поэтому в следующем запросе все узлы имеют подпись AQO not used — именно для такого класса узел Bitmap Heap Scan еще не встречался:

Page 14: Page 14

=> EXPLAIN (analyze, timing off, summary off)
SELECT * FROM flights f
  JOIN airports a ON a.airport_code = f.departure_airport
WHERE f.departure_airport = 'VKO'
  AND f.flight_no = 'PG0007';
                                                               QUERY PLAN
------------------------------------------------------------------------------------------
----------------------------------------------
 Nested Loop  (cost=4.62..137.47 rows=2 width=162) (actual rows=61 loops=1)
   AQO not used, fss=-7344153368946762870
   ->  Seq Scan on airports_data ml  (cost=0.00..4.30 rows=1 width=145) (actual rows=1
loops=1)
         AQO not used, fss=702648804789554594
         Filter: (airport_code = 'VKO'::bpchar)
         Rows Removed by Filter: 103
   ->  Bitmap Heap Scan on flights f  (cost=4.62..132.14 rows=2 width=63) (actual rows=61
loops=1)
         AQO not used, fss=-4388671362309763767
         Recheck Cond: (flight_no = 'PG0007'::bpchar)
         Filter: (departure_airport = 'VKO'::bpchar)
         Heap Blocks: exact=2
         ->  Bitmap Index Scan on flights_flight_no_scheduled_departure_key
(cost=0.00..4.62 rows=44 width=0) (actual rows=61 loops=1)
               Index Cond: (flight_no = 'PG0007'::bpchar)
 Using aqo: true
 AQO mode: LEARN
 AQO advanced: ON
 Query hash: -1728112134488687408
 JOINS: 1
(18 rows)

Теперь переведем расширение в режим controlled:

=> SET aqo.mode = controlled;
SET

В этом режиме работы запросы из неизвестных классов не обрабатываются (Using aqo: false):

=> EXPLAIN (analyze, timing off, summary off)
SELECT count(*) FROM flights;
                                          QUERY PLAN
------------------------------------------------------------------------------------------
-----
 Aggregate  (cost=808.01..808.02 rows=1 width=8) (actual rows=1 loops=1)
   ->  Seq Scan on flights  (cost=0.00..725.21 rows=33121 width=0) (actual rows=33121
loops=1)
 Using aqo: false
 AQO mode: CONTROLLED
 AQO advanced: ON
 Query hash: 5250232765826456330
 JOINS: -1
(7 rows)

Но для уже известных классов запросов модель продолжает обучаться. Оценка для новой пары «аэропорт — номер рейса» не точна:

=> EXPLAIN (analyze, timing off, summary off)
SELECT * FROM flights f
WHERE f.departure_airport = 'SVO' AND f.flight_no = 'PG0505';

Page 15: Page 15

                                                            QUERY PLAN
------------------------------------------------------------------------------------------
----------------------------------------
 Bitmap Heap Scan on flights f  (cost=4.63..132.15 rows=32 width=63) (actual rows=61
loops=1)
   AQO: rows=32, error=-91%, fss=-4388671362309763767
   Recheck Cond: (flight_no = 'PG0505'::bpchar)
   Filter: (departure_airport = 'SVO'::bpchar)
   Heap Blocks: exact=1
   ->  Bitmap Index Scan on flights_flight_no_scheduled_departure_key  (cost=0.00..4.62
rows=44 width=0) (actual rows=61 loops=1)
         Index Cond: (flight_no = 'PG0505'::bpchar)
 Using aqo: true
 AQO mode: CONTROLLED
 AQO advanced: ON
 Query hash: -6712624238167333493
 JOINS: 0
(12 rows)

Но в следующий раз модель исправляется, получив новые данные:

=> EXPLAIN (analyze, timing off, summary off)
SELECT * FROM flights
WHERE departure_airport = 'SVO'
  AND flight_no = 'PG0505';
                                                            QUERY PLAN
------------------------------------------------------------------------------------------
----------------------------------------
 Bitmap Heap Scan on flights  (cost=4.64..132.16 rows=61 width=63) (actual rows=61
loops=1)
   AQO: rows=61, error=0%, fss=-4388671362309763767
   Recheck Cond: (flight_no = 'PG0505'::bpchar)
   Filter: (departure_airport = 'SVO'::bpchar)
   Heap Blocks: exact=1
   ->  Bitmap Index Scan on flights_flight_no_scheduled_departure_key  (cost=0.00..4.62
rows=44 width=0) (actual rows=61 loops=1)
         Index Cond: (flight_no = 'PG0505'::bpchar)
 Using aqo: true
 AQO mode: CONTROLLED
 AQO advanced: ON
 Query hash: -6712624238167333493
 JOINS: 0
(12 rows)

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

=> SELECT queryid, learn_aqo, use_aqo
FROM aqo_queries
WHERE queryid != 0;
       queryid        | learn_aqo | use_aqo
----------------------+-----------+---------
 -1728112134488687408 | t         | t
 -6712624238167333493 | t         | t
(2 rows)

Например, можно временно отключить использование модели, но продолжить обучение (NULL сохраняет существующее значение):

=> SELECT aqo_queries_update(
  queryid => -6712624238167333493,
  dbid => NULL,
  fs => NULL,
  learn_aqo => true,
  use_aqo => false,
  auto_tuning => NULL
);
 aqo_queries_update
--------------------
 t
(1 row)

Page 16: Page 16

=> EXPLAIN (analyze, timing off, summary off)
SELECT * FROM flights
WHERE departure_airport = 'DME'
  AND flight_no = 'PG0005';
                                                            QUERY PLAN
------------------------------------------------------------------------------------------
----------------------------------------
 Bitmap Heap Scan on flights  (cost=4.62..132.14 rows=4 width=63) (actual rows=26 loops=1)
   AQO not used, fss=0
   Recheck Cond: (flight_no = 'PG0005'::bpchar)
   Filter: (departure_airport = 'DME'::bpchar)
   Heap Blocks: exact=2
   ->  Bitmap Index Scan on flights_flight_no_scheduled_departure_key  (cost=0.00..4.62
rows=44 width=0) (actual rows=26 loops=1)
         Index Cond: (flight_no = 'PG0005'::bpchar)
 Using aqo: false
 AQO mode: CONTROLLED
 AQO advanced: ON
 Query hash: -6712624238167333493
 JOINS: 0
(12 rows)

Сбросив свойство learn_aqo, можно «заморозить» отдельный класс запросов:

=> SELECT aqo_queries_update(
  queryid => -6712624238167333493,
  dbid => NULL,
  fs => NULL,
  learn_aqo => false,
  use_aqo => true,
  auto_tuning => NULL
);
 aqo_queries_update
--------------------
 t
(1 row)

Теперь оценка учитывает данные, полученные в прошлый раз, но дальнейшее обучение остановлено:

=> EXPLAIN (analyze, timing off, summary off)
SELECT * FROM flights
WHERE departure_airport = 'DME'
  AND flight_no = 'PG0005';
                                                            QUERY PLAN
------------------------------------------------------------------------------------------
----------------------------------------
 Bitmap Heap Scan on flights  (cost=4.63..132.15 rows=51 width=63) (actual rows=26
loops=1)
   AQO: rows=51, error=49%, fss=-4388671362309763767
   Recheck Cond: (flight_no = 'PG0005'::bpchar)
   Filter: (departure_airport = 'DME'::bpchar)
   Heap Blocks: exact=2
   ->  Bitmap Index Scan on flights_flight_no_scheduled_departure_key  (cost=0.00..4.62
rows=44 width=0) (actual rows=26 loops=1)
         Index Cond: (flight_no = 'PG0005'::bpchar)
 Using aqo: true
 AQO mode: CONTROLLED
 AQO advanced: ON
 Query hash: -6712624238167333493
 JOINS: 0
(12 rows)

В режиме intelligent можно управлять и свойством auto_tune.

С классификацией: режим intelligent

Отключим параллельное выполнение для упрощения планов запросов:

=> SET max_parallel_workers_per_gather = 0;
SET

Сбросим еще раз статистику AQO и установим режим intelligent:

Page 17: Page 17

=> SELECT aqo_reset();
 aqo_reset
-----------
        10
(1 row)
=> SET aqo.mode = intelligent;
SET

Включим параметр, позволяющий собирать статистику при отключенном использовании AQO:

=> SET aqo.force_collect_stat = on;
SET

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

=> EXPLAIN (analyze) SELECT *
FROM ticket_flights tf
  JOIN tickets t ON t.ticket_no = tf.ticket_no
WHERE tf.fare_conditions = 'Business'
  AND tf.amount > 50000
  AND length(tf.ticket_no) = 13;
                                                             QUERY PLAN
------------------------------------------------------------------------------------------
------------------------------------------
 Nested Loop  (cost=0.42..30075.24 rows=38 width=136) (actual time=0.194..403.252
rows=30133 loops=1)
   AQO not used, fss=0
   ->  Seq Scan on ticket_flights tf  (cost=0.00..29754.52 rows=38 width=32) (actual
time=0.145..211.525 rows=30133 loops=1)
         AQO not used, fss=0
         Filter: ((amount > '50000'::numeric) AND ((fare_conditions)::text =
'Business'::text) AND (length(ticket_no) = 13))
         Rows Removed by Filter: 1015593
   ->  Index Scan using tickets_pkey on tickets t  (cost=0.42..8.44 rows=1 width=104)
(actual time=0.006..0.006 rows=1 loops=30133)
         AQO not used, fss=0
         Index Cond: (ticket_no = tf.ticket_no)
 Planning Time: 842.994 ms
 Execution Time: 405.288 ms
 Using aqo: false
 AQO mode: INTELLIGENT
 AQO advanced: ON
 Query hash: 4602601022030847932
 JOINS: 1
(16 rows)
=> \g /dev/null
=> \g /dev/null
=> \g /dev/null
=> \g /dev/null
=> \g /dev/null

На седьмой раз AQO начинает использовать модель и исправляет оценку кардинальности. Обратите внимание, что при этом поменялся и алгоритм соединения — вложенному циклу планировщик предпочел хеширование:

=> EXPLAIN (analyze) SELECT *
FROM ticket_flights tf
  JOIN tickets t ON t.ticket_no = tf.ticket_no
WHERE tf.fare_conditions = 'Business'
  AND tf.amount > 50000
  AND length(tf.ticket_no) = 13;

Page 18: Page 18

                                                              QUERY PLAN
------------------------------------------------------------------------------------------
--------------------------------------------
 Hash Join  (cost=30131.18..42567.92 rows=30133 width=136) (actual time=215.235..370.196
rows=30133 loops=1)
   AQO: rows=30133, error=0%, fss=8105558446173165231
   Hash Cond: (t.ticket_no = tf.ticket_no)
   ->  Seq Scan on tickets t  (cost=0.00..9843.33 rows=366733 width=104) (actual
time=0.037..55.035 rows=366733 loops=1)
         AQO not used, fss=-782071859942470067
   ->  Hash  (cost=29754.52..29754.52 rows=30133 width=32) (actual time=214.908..214.910
rows=30133 loops=1)
         Buckets: 32768  Batches: 1  Memory Usage: 2258kB
         ->  Seq Scan on ticket_flights tf  (cost=0.00..29754.52 rows=30133 width=32)
(actual time=0.168..205.589 rows=30133 loops=1)
               AQO: rows=30133, error=0%, fss=3731686215164005158
               Filter: ((amount > '50000'::numeric) AND ((fare_conditions)::text =
'Business'::text) AND (length(ticket_no) = 13))
               Rows Removed by Filter: 1015593
 Planning Time: 0.500 ms
 Execution Time: 372.079 ms
 Using aqo: true
 AQO mode: INTELLIGENT
 AQO advanced: ON
 Query hash: 4602601022030847932
 JOINS: 1
(18 rows)
=> SELECT queryid, learn_aqo, use_aqo, auto_tuning
FROM aqo_queries
WHERE queryid != 0;
       queryid       | learn_aqo | use_aqo | auto_tuning
---------------------+-----------+---------+-------------
 4602601022030847932 | t         | t       | t
(1 row)
=> SELECT * FROM aqo_query_stat WHERE queryid != 0 \gx
-[ RECORD 1
]-----------------+-----------------------------------------------------------------------
---------------------------------------
queryid                       | 4602601022030847932
dbid                          | 16388
execution_time_with_aqo       | {0.371713365}
execution_time_without_aqo    |
{0.40541465,0.382183913,0.399997997,0.404536646,0.388363817,0.398315885}
planning_time_with_aqo        | {0.000472313}
planning_time_without_aqo     |
{0.842976038,0.000458545,0.000399877,0.000655214,0.000358467,0.000393757}
cardinality_error_with_aqo    | {0}
cardinality_error_without_aqo |
{4.450526690651791,4.450526690651791,4.450526690651791,4.450526690651791,4.450526690651791
,4.450526690651791}
executions_with_aqo           | 1
executions_without_aqo        | 6

Теперь, когда модель используется, автоматика режима intelligent будет смотреть на среднее время планирования и выполнения запроса с AQO и без AQO, включая или выключая использование модели. Выполним запрос еще несколько раз и проверим статистику:

=> SELECT *
FROM ticket_flights tf
  JOIN tickets t ON t.ticket_no = tf.ticket_no
WHERE tf.fare_conditions = 'Business'
  AND tf.amount > 50000
  AND length(tf.ticket_no) = 13\g /dev/null
=> \g /dev/null
=> \g /dev/null
=> \g /dev/null
=> \g /dev/null
=> \g /dev/null

Page 19: Page 19

=> \g /dev/null
=> \g /dev/null
=> \g /dev/null
=> \g /dev/null
=> SELECT
  executions_with_aqo,
  (SELECT avg(e) FROM unnest(execution_time_with_aqo) e) execution_time_with_aqo,
  executions_without_aqo,
  (SELECT avg(e) FROM unnest(execution_time_without_aqo) e) execution_time_without_aqo
FROM aqo_query_stat
WHERE queryid != 0 \gx
-[ RECORD 1 ]--------------+--------------------
executions_with_aqo        | 8
execution_time_with_aqo    | 0.517654343875
executions_without_aqo     | 9
execution_time_without_aqo | 0.46125627288888893

Если AQO не даст выигрыша по времени выполнения в течение 50 выполнений, оптимизация для данного класса запроса совсем отключится.

Page 20: Песочница

Отдельная база знаний

  • в общей памяти сервера
  • изолирована от основной базы
  • общая для всех сеансов, где включена (aqo.sandbox = on)
  • Позволяет
    • экспериментировать с AQO, не влияя на основную активность
    • работать на физической реплике

Комментарии преподавателя

В AQO есть возможность организовать дополнительную базу знаний, не связанную с основной — песочницу.

На сервере существует единственная песочница, которая располагается в его общей памяти. Во всех сеансах, в которых задано значение aqo.sandbox = on, расширение AQO будет работает не с основной базой знаний, а с общей песочницей.

Это дает возможность экспериментировать с настройками, не влияя на выполняющиеся запросы пользователей, а затем переносить статистику в основную базу знаний.

Также песочница позволяет обучать AQO на запросах, выполняющихся на физической реплике. По умолчанию (если aqo.wal_rw = true) изменения основной базы знаний записываются в WAL и передаются на реплику. Поэтому запросы на реплике могут использовать статистику AQO основного сервера. Однако обучение на реплике невозможно, поскольку на ней нельзя изменять данные. Песочница позволяет обойти это ограничение.

Page 21: Песочница

Переключим AQO на песочницу в текущем сеансе:

=> SET aqo.sandbox = on;
SET
=> SET aqo.advanced = off;
SET
=> SET aqo.mode = learn;
SET

Сбросим статистику песочницы, не затрагивая основную базу знаний:

=> SELECT aqo_reset();
 aqo_reset
-----------
         0
(1 row)

Выполняем запрос, пока оценка кардинальности не станет удовлетворительной:

=> EXPLAIN (analyze) SELECT * FROM ticket_flights
WHERE fare_conditions = 'Business' AND amount > 50000;
                                                      QUERY PLAN
------------------------------------------------------------------------------------------
----------------------------
 Seq Scan on ticket_flights  (cost=0.00..24525.89 rows=7605 width=32) (actual
time=0.504..263.187 rows=30133 loops=1)
   AQO not used, fss=8156007843730226217
   Filter: ((amount > '50000'::numeric) AND ((fare_conditions)::text = 'Business'::text))
   Rows Removed by Filter: 1015593
 Planning Time: 0.194 ms
 Execution Time: 264.801 ms
 Using aqo: true
 AQO mode: LEARN
 AQO advanced: OFF
 Query hash: 5235755627948238838
 JOINS: 0
(11 rows)
=> EXPLAIN (analyze) SELECT * FROM ticket_flights
WHERE fare_conditions = 'Business' AND amount > 50000;
                                                      QUERY PLAN
------------------------------------------------------------------------------------------
-----------------------------
 Seq Scan on ticket_flights  (cost=0.00..24525.89 rows=30133 width=32) (actual
time=0.185..266.773 rows=30133 loops=1)
   AQO: rows=30133, error=0%, fss=8156007843730226217
   Filter: ((amount > '50000'::numeric) AND ((fare_conditions)::text = 'Business'::text))
   Rows Removed by Filter: 1015593
 Planning Time: 0.314 ms
 Execution Time: 268.548 ms
 Using aqo: true
 AQO mode: LEARN
 AQO advanced: OFF
 Query hash: 5235755627948238838
 JOINS: 0
(11 rows)

Теперь мы можем скопировать статистику из песочницы в таблицу:

=> CREATE TABLE aqo_data_sandbox AS SELECT * FROM aqo_data;
SELECT 1

Переключаемся на основную базу знаний и импортируем статистику, полученную из песочницы:

Page 22: Page 22

=> SET aqo.sandbox = off;
SET
=> SELECT aqo_reset();
 aqo_reset
-----------
         7
(1 row)
=> SELECT aqo_data_update(fs, fss, dbid, nfeatures, features, targets, reliability, oids, tmpoids)
FROM aqo_data_sandbox WHERE fs = 0;
 aqo_data_update
-----------------
 t
(1 row)
=> DROP TABLE aqo_data_sandbox;
DROP TABLE

Запрос сразу же использует AQO:

=> EXPLAIN (analyze) SELECT * FROM ticket_flights
WHERE fare_conditions = 'Business' AND amount > 50000;
                                                      QUERY PLAN
------------------------------------------------------------------------------------------
-----------------------------
 Seq Scan on ticket_flights  (cost=0.00..24525.89 rows=30133 width=32) (actual
time=0.365..255.763 rows=30133 loops=1)
   AQO: rows=30133, error=0%, fss=8156007843730226217
   Filter: ((amount > '50000'::numeric) AND ((fare_conditions)::text = 'Business'::text))
   Rows Removed by Filter: 1015593
 Planning Time: 0.256 ms
 Execution Time: 257.376 ms
 Using aqo: true
 AQO mode: LEARN
 AQO advanced: OFF
 Query hash: 5235755627948238838
 JOINS: 0
(11 rows)

Page 23: Итоги

Адаптивная оптимизация запросов AQO — инструмент для улучшения планов методом машинного обучения

  • Автоматическое исправление оценок кардинальности, которого сложно достичь традиционными методами
  • Несколько режимов работы и возможность тонкой настройки на уровне отдельных классов запросов

Page 24: Практика

  1. В базе данных demo создайте индекс для таблицы flights по столбцу scheduled_departure и для ticket_flights по столбцу fare_conditions.
    1. Отключите параллельное выполнение, увеличьте размер рабочей памяти до 256 Мбайт и выполните запрос для получения количества пассажиров, летавших бизнес-классом с 01.08.2016 и прибывших с опозданием не более часа.
    1. Оптимизируйте запрос с помощью AQO.

Комментарии преподавателя

  1. Используйте следующий запрос:
SELECT t.ticket_no
FROM flights f
  JOIN ticket_flights tf ON f.flight_id = tf.flight_id
  JOIN tickets t ON tf.ticket_no = t.ticket_no
WHERE f.scheduled_departure > '2016-08-01'::timestamptz
  AND f.actual_arrival < f.scheduled_arrival + interval '1 hour'
  AND tf.fare_conditions = 'Business';
  1. Сбросьте статистику AQO и исследуйте план запроса в режиме обучения (learn).

Page 25: 1. Индексы

=> \c demo
You are now connected to database "demo" as user "student".

Чтобы у планировщика PostgreSQL было больше вариантов соединения таблиц, создадим два индекса.

=> CREATE INDEX flights_sd_idx ON flights (scheduled_departure);
CREATE INDEX
=> CREATE INDEX ticket_flights_fc_idx ON ticket_flights (fare_conditions);
CREATE INDEX

2. Запрос с обычным планированием

Отключим параллельную обработку и увеличим work_mem, чтобы вся работа происходила в оперативной памяти.

=> ALTER SYSTEM SET max_parallel_workers_per_gather = 0;
ALTER SYSTEM
=> ALTER SYSTEM SET work_mem = '256MB';
ALTER SYSTEM
=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Получим количество пассажиров, которые летели бизнес-классом начиная с 01.08.2016 и прибыли с опозданием не более часа.

=> EXPLAIN (analyze, buffers, timing off)
SELECT t.ticket_no
FROM flights f
  JOIN ticket_flights tf ON f.flight_id = tf.flight_id
  JOIN tickets t ON tf.ticket_no = t.ticket_no
WHERE f.scheduled_departure > '2016-08-01'::timestamptz
  AND f.actual_arrival < f.scheduled_arrival + interval '1 hour'
  AND tf.fare_conditions = 'Business';

Page 26: Page 26

                                                                                    QUERY
PLAN
------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------
 Hash Join  (cost=13223.28..24799.72 rows=35786 width=14) (actual rows=56111 loops=1)
   Hash Cond: (t.ticket_no = tf.ticket_no)
   Buffers: shared hit=406 read=13628
   ->  Seq Scan on tickets t  (cost=0.00..9843.33 rows=366733 width=14) (actual
rows=366733 loops=1)
         Buffers: shared read=6176
   ->  Hash  (cost=12775.96..12775.96 rows=35786 width=14) (actual rows=56111 loops=1)
         Buckets: 65536  Batches: 1  Memory Usage: 3033kB
         Buffers: shared hit=406 read=7452
         ->  Hash Join  (cost=2312.09..12775.96 rows=35786 width=14) (actual rows=56111
loops=1)
               Hash Cond: (tf.flight_id = f.flight_id)
               Buffers: shared hit=406 read=7452
               ->  Bitmap Heap Scan on ticket_flights tf  (cost=1200.47..11382.49
rows=107361 width=18) (actual rows=107642 loops=1)
                     Recheck Cond: ((fare_conditions)::text = 'Business'::text)
                     Heap Blocks: exact=7369
                     Buffers: shared hit=12 read=7452
                     ->  Bitmap Index Scan on ticket_flights_fc_idx  (cost=0.00..1173.63
rows=107361 width=0) (actual rows=107642 loops=1)
                           Index Cond: ((fare_conditions)::text = 'Business'::text)
                           Buffers: shared read=95
               ->  Hash  (cost=973.62..973.62 rows=11040 width=4) (actual rows=15918
loops=1)
                     Buckets: 16384  Batches: 1  Memory Usage: 688kB
                     Buffers: shared hit=394
                     ->  Seq Scan on flights f  (cost=0.00..973.62 rows=11040 width=4)
(actual rows=15918 loops=1)
                           Filter: ((scheduled_departure > '2016-08-01
00:00:00+03'::timestamp with time zone) AND (actual_arrival < (scheduled_arrival +
'01:00:00'::interval)))
                           Rows Removed by Filter: 17203
                           Buffers: shared hit=394
 Planning:
   Buffers: shared hit=299 read=47
 Planning Time: 613.970 ms
 Execution Time: 319.226 ms
(29 rows)

Имеется несоответствие по количествам планируемых и реально полученных строк.

3. AQO

Чтобы заработал AQO, нужно загрузить библиотеку:

=> ALTER SYSTEM SET shared_preload_libraries = 'aqo';
ALTER SYSTEM
=> \q
student$ sudo systemctl restart postgrespro-ent-16.service

Установим расширение:

student$ psql demo
=> CREATE EXTENSION aqo;
CREATE EXTENSION

Режим по умолчанию — learn:

=> SHOW aqo.mode;
 aqo.mode
----------
 learn
(1 row)

Сбросим предыдущую статистику AQO:

=> SELECT aqo_reset();

Page 27: Page 27

 aqo_reset
-----------
         0
(1 row)

Включим AQO:

=> SET aqo.enable = on;
SET

Выполним запрос и исследуем его план:

=> EXPLAIN (analyze, buffers, timing off)
SELECT t.ticket_no
FROM flights f
  JOIN ticket_flights tf ON f.flight_id = tf.flight_id
  JOIN tickets t ON tf.ticket_no = t.ticket_no
WHERE f.scheduled_departure > '2016-08-01'::timestamptz
  AND f.actual_arrival < f.scheduled_arrival + interval '1 hour'
  AND tf.fare_conditions = 'Business';
                                                                                    QUERY
PLAN
------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------
 Hash Join  (cost=13223.28..24799.72 rows=35786 width=14) (actual rows=56111 loops=1)
   AQO not used, fss=-1169867161196126870
   Hash Cond: (t.ticket_no = tf.ticket_no)
   Buffers: shared read=14034
   ->  Seq Scan on tickets t  (cost=0.00..9843.33 rows=366733 width=14) (actual
rows=366733 loops=1)
         AQO not used, fss=-782071859942470067
         Buffers: shared read=6176
   ->  Hash  (cost=12775.96..12775.96 rows=35786 width=14) (actual rows=56111 loops=1)
         Buckets: 65536  Batches: 1  Memory Usage: 3033kB
         Buffers: shared read=7858
         ->  Hash Join  (cost=2312.09..12775.96 rows=35786 width=14) (actual rows=56111
loops=1)
               AQO not used, fss=-1710832237659048278
               Hash Cond: (tf.flight_id = f.flight_id)
               Buffers: shared read=7858
               ->  Bitmap Heap Scan on ticket_flights tf  (cost=1200.47..11382.49
rows=107361 width=18) (actual rows=107642 loops=1)
                     AQO not used, fss=4723136667746782336
                     Recheck Cond: ((fare_conditions)::text = 'Business'::text)
                     Heap Blocks: exact=7369
                     Buffers: shared read=7464
                     ->  Bitmap Index Scan on ticket_flights_fc_idx  (cost=0.00..1173.63
rows=107361 width=0) (actual rows=107642 loops=1)
                           Index Cond: ((fare_conditions)::text = 'Business'::text)
                           Buffers: shared read=95
               ->  Hash  (cost=973.62..973.62 rows=11040 width=4) (actual rows=15918
loops=1)
                     Buckets: 16384  Batches: 1  Memory Usage: 688kB
                     Buffers: shared read=394
                     ->  Seq Scan on flights f  (cost=0.00..973.62 rows=11040 width=4)
(actual rows=15918 loops=1)
                           AQO not used, fss=-1465870163991728921
                           Filter: ((scheduled_departure > '2016-08-01
00:00:00+03'::timestamp with time zone) AND (actual_arrival < (scheduled_arrival +
'01:00:00'::interval)))
                           Rows Removed by Filter: 17203
                           Buffers: shared read=394
 Planning:
   Buffers: shared hit=306 read=52
 Planning Time: 3.450 ms
 Execution Time: 299.720 ms
 Using aqo: true
 AQO mode: LEARN
 AQO advanced: OFF
 Query hash: 5090759953117466762
 JOINS: 2
(39 rows)

AQO не оптимизирует запрос, если встречает его впервые. Повторим запрос:

Page 28: Page 28

=> EXPLAIN (analyze, buffers, timing off)
SELECT t.ticket_no
FROM flights f
  JOIN ticket_flights tf ON f.flight_id = tf.flight_id
  JOIN tickets t ON tf.ticket_no = t.ticket_no
WHERE f.scheduled_departure > '2016-08-01'::timestamptz
  AND f.actual_arrival < f.scheduled_arrival + interval '1 hour'
  AND tf.fare_conditions = 'Business';
                                                                                    QUERY
PLAN
------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------
 Hash Join  (cost=13539.13..25777.23 rows=56111 width=14) (actual rows=56111 loops=1)
   AQO: rows=56111, error=0%, fss=-1169867161196126870
   Hash Cond: (t.ticket_no = tf.ticket_no)
   Buffers: shared hit=7890 read=6144
   ->  Seq Scan on tickets t  (cost=0.00..9843.33 rows=366733 width=14) (actual
rows=366733 loops=1)
         AQO: rows=366733, error=0%, fss=-782071859942470067
         Buffers: shared hit=32 read=6144
   ->  Hash  (cost=12837.74..12837.74 rows=56111 width=14) (actual rows=56111 loops=1)
         Buckets: 65536  Batches: 1  Memory Usage: 3033kB
         Buffers: shared hit=7858
         ->  Hash Join  (cost=2373.14..12837.74 rows=56111 width=14) (actual rows=56111
loops=1)
               AQO: rows=56111, error=0%, fss=-1710832237659048278
               Hash Cond: (tf.flight_id = f.flight_id)
               Buffers: shared hit=7858
               ->  Bitmap Heap Scan on ticket_flights tf  (cost=1200.54..11382.56
rows=107642 width=18) (actual rows=107642 loops=1)
                     AQO: rows=107642, error=0%, fss=4723136667746782336
                     Recheck Cond: ((fare_conditions)::text = 'Business'::text)
                     Heap Blocks: exact=7369
                     Buffers: shared hit=7464
                     ->  Bitmap Index Scan on ticket_flights_fc_idx  (cost=0.00..1173.63
rows=107361 width=0) (actual rows=107642 loops=1)
                           Index Cond: ((fare_conditions)::text = 'Business'::text)
                           Buffers: shared hit=95
               ->  Hash  (cost=973.62..973.62 rows=15918 width=4) (actual rows=15918
loops=1)
                     Buckets: 16384  Batches: 1  Memory Usage: 688kB
                     Buffers: shared hit=394
                     ->  Seq Scan on flights f  (cost=0.00..973.62 rows=15918 width=4)
(actual rows=15918 loops=1)
                           AQO: rows=15918, error=0%, fss=-1465870163991728921
                           Filter: ((scheduled_departure > '2016-08-01
00:00:00+03'::timestamp with time zone) AND (actual_arrival < (scheduled_arrival +
'01:00:00'::interval)))
                           Rows Removed by Filter: 17203
                           Buffers: shared hit=394
 Planning:
   Buffers: shared hit=27
 Planning Time: 0.795 ms
 Execution Time: 244.128 ms
 Using aqo: true
 AQO mode: LEARN
 AQO advanced: OFF
 Query hash: 5090759953117466762
 JOINS: 2
(39 rows)

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

Уборка

Удалим индексы и расширение, созданные в базе demo и сбросим настройки параметров конфигурации.

=> DROP INDEX flights_sd_idx;
DROP INDEX
=> DROP INDEX ticket_flights_fc_idx;
DROP INDEX
=> DROP EXTENSION aqo;
DROP EXTENSION

Page 29: Page 29

=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service

10 Занятие 10: Производительность / Перепланирование запросов

Исходная страница в нативном Markdown: Производительность / Перепланирование запросов
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_10_replan.html Archive: PGPRO-handouts-16.zip Страниц PDF: 14

Page 1: Производительность / Перепланирование запросов

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Проблема и решение
  • Механизм работы
  • Условия перепланирования
  • Особенности реализации

Page 3: Проблема и решение

Ошибки в оценке кардинальности

  • статистика устаревает из-за частых изменений данных
  • неравномерное распределение данных
  • параметризованные запросы и подготовленные операторы
  • При сильном несоответствии
    • выполнение прерывается
    • запрос планируется повторно с учетом полученных данных
  • Классы запросов
    • тяжелые аналитические OLAP-запросы
    • проблемные OLTP-запросы

Комментарии преподавателя

Как правило, ошибки при планировании вызваны некорректной оценкой кардинальности. Частая причина этого — неактуальная статистика планировщика. Например, в OLTP-системах это может быть вызвано устареванием статистики из-за часто изменяющихся данных, неравномерным распределением или использованием подготовленных запросов с параметрами (когда оптимизатор переключается на общий план выполнения).

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

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

Page 4: Перепланирование запросов

student$ psql demo

Рассмотрим запрос:

=> EXPLAIN (analyze, timing off, summary off) SELECT a1.airport_name, a2.airport_name
FROM airports a1, airports a2
WHERE a1.city = 'Москва';
                                             QUERY PLAN
------------------------------------------------------------------------------------------
-----------
 Nested Loop  (cost=0.00..88.16 rows=104 width=64) (actual rows=312 loops=1)
   ->  Seq Scan on airports_data ml  (cost=0.00..30.56 rows=1 width=61) (actual rows=3
loops=1)
         Filter: ((city ->> lang()) = 'Москва'::text)
         Rows Removed by Filter: 101
   ->  Seq Scan on airports_data ml_1  (cost=0.00..4.04 rows=104 width=61) (actual
rows=104 loops=3)
(5 rows)

Точное количество строк отличается от оценки. Посмотрим на параметры перепланирования:

=> \dconfig replan_*
  List of configuration parameters
          Parameter          | Value
-----------------------------+-------
 replan_enable               | off
 replan_max_attempts         | 100
 replan_memory_limit         | -1
 replan_overrun_limit        | -1
 replan_query_execution_time | -1
 replan_regression_mode      | off
 replan_show_signature       | off
(7 rows)

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

Page 5: Механизм работы

Source slide page 5: Механизм работы{#source-figure-005 caption=“Source slide page 5: Механизм работы”}

Комментарии преподавателя

Перепланирование запросов встраивается в стандартную схему работы планировщика Postgres Pro.

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

Кардинальность сохраняется только для тех узлов плана, в которых реальная кардинальность оказалась больше плановой. Эта информация хранится только в процессе перепланирования текущего запроса. Если запрос выполняется часто, новый план можно запомнить с помощью расширения pgpro_multiplan (рассматривается в теме «Сохранение планов запросов»).

Перед попыткой планирования создается вложенная транзакция. При срабатывании условия перепланирования эта транзакция обрывается и начинается новая.

Все выбранные планы для всех попыток перепланирования сохраняются в журнале сообщений сервера.

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

Подробности можно прочитать в статье Алены Рыбакиной и Андрея Лепихова: https://habr.com/ru/companies/postgrespro/articles/819911/

Page 6: Условия перепланирования

Автоматически

  • время выполнения превысило replan_query_execution_time
  • узел отработал и ошибка прогноза кардинальности оказалась больше
  • replan_overrun_limit
  • рабочий процесс использует более replan_memory_limit памяти
  • и ошибка прогноза кардинальности превысила replan_overrun_limit
  • Вручную
    • функция replan_signal

Комментарии преподавателя

По умолчанию перепланирование запросов отключено. Для включения достаточно установить конфигурационный параметр replan_enable и настроить условия перепланирования, или триггеры (не имеющие отношения к обычным триггерам базы данных).

В Postgres Pro Enterprise 16 перепланирование срабатывает в трех следующих случаях:

● По времени выполнения.

Запрос выполняется дольше, чем указано в параметре replan_query_execution_time.

● По ошибке оценки кардинальности после выполнения узла.

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

● По перерасходу памяти и ошибке оценки кардинальности во время выполнения узла.

Во время выполнения узла плана потребление памяти рабочим процессом превысило значение replan_memory_limit, а количество уже обработанных строк превысило число, ожидаемое планировщиком, умноженное на значение replan_overrun_limit.

Перепланирование можно инициировать и вручную с помощью функции replan_signal.

https://postgrespro.ru/docs/enterprise/16/realtime-query-replanning-how#RE ALTIME-QUERY-REPLANNING-TRIGGERS

Page 7: Пример перепланирования запросов

Повторим запрос:

=> EXPLAIN (analyze, timing off, summary off) SELECT a1.airport_name, a2.airport_name
FROM airports a1, airports a2
WHERE a1.city = 'Москва';
                                             QUERY PLAN
------------------------------------------------------------------------------------------
-----------
 Nested Loop  (cost=0.00..88.16 rows=104 width=64) (actual rows=312 loops=1)
   ->  Seq Scan on airports_data ml  (cost=0.00..30.56 rows=1 width=61) (actual rows=3
loops=1)
         Filter: ((city ->> lang()) = 'Москва'::text)
         Rows Removed by Filter: 101
   ->  Seq Scan on airports_data ml_1  (cost=0.00..4.04 rows=104 width=61) (actual
rows=104 loops=3)
(5 rows)

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

=> SET replan_enable = true;
SET

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

=> SET replan_overrun_limit = 2;
SET
=> EXPLAIN (analyze, timing off, summary off) SELECT a1.airport_name, a2.airport_name
FROM airports a1, airports a2
WHERE a1.city = 'Москва';
                                              QUERY PLAN
------------------------------------------------------------------------------------------
------------
 Nested Loop  (cost=0.00..196.07 rows=312 width=64) (actual rows=312 loops=1)
   ->  Seq Scan on airports_data ml_1  (cost=0.00..4.04 rows=104 width=61) (actual
rows=104 loops=1)
   ->  Materialize  (cost=0.00..30.58 rows=3 width=61) (actual rows=3 loops=104)
         ->  Seq Scan on airports_data ml  (cost=0.00..30.56 rows=3 width=61) (actual
rows=3 loops=1)
               Filter: ((city ->> lang()) = 'Москва'::text)
               Rows Removed by Filter: 101
(6 rows)

При перепланировании использовалась реальная кардинальность узлов, полученная при выполнении плана. Это привело к выбору плана с материализацией, который в данном случае более эффективен. Повторим запрос, включив вывод информации о перепланировании и добавив к команде EXPLAIN параметр verbose:

=> SET replan_show_signature = true;
SET
=> EXPLAIN (analyze, timing off, verbose) SELECT a1.airport_name, a2.airport_name
FROM airports a1, airports a2
WHERE a1.city = 'Москва';

Page 8: Page 8

                                                  QUERY PLAN
------------------------------------------------------------------------------------------
---------------------
 Nested Loop  (cost=0.00..196.07 rows=312 width=64) (actual rows=312 loops=1)
   NodeSign: 395408946556254163
   Cardinality: 312
   Groups Number: -1
   Finished: false
   Output: (ml.airport_name ->> lang()), (ml_1.airport_name ->> lang())
   ->  Seq Scan on bookings.airports_data ml_1  (cost=0.00..4.04 rows=104 width=61)
(actual rows=104 loops=1)
         NodeSign: 6649604475734118433
         Cardinality: 104
         Groups Number: -1
         Finished: true
         Output: ml_1.airport_code, ml_1.airport_name, ml_1.city, ml_1.coordinates,
ml_1.timezone
   ->  Materialize  (cost=0.00..30.58 rows=3 width=61) (actual rows=3 loops=104)
         NodeSign: subordinate
         Output: ml.airport_name
         ->  Seq Scan on bookings.airports_data ml  (cost=0.00..30.56 rows=3 width=61)
(actual rows=3 loops=1)
               NodeSign: 953600500042743201
               Cardinality: 3
               Groups Number: -1
               Finished: true
               Output: ml.airport_name
               Filter: ((ml.city ->> lang()) = 'Москва'::text)
               Rows Removed by Filter: 101
 Planning Time: 1.775 ms
 Execution Time: 2.405 ms
 Replan Active: true
 Table Entries: 4
 Controlled Statements: 1
 Replanning Attempts: 1
 Final Run Planning Time: 0.077 ms
 Total Time Elapsed: 4.179 ms
(31 rows)

Теперь для узлов плана отображается более подробная информация. В нижней части вывода видно, что потребовалась одна попытка перепланирования (Replanning Attempts: 1).

Page 9: Особенности

Является частью ядра СУБД

  • не нужно устанавливать расширения
  • Ограничения
    • поддерживает только SELECT
    • не поддерживает курсоры и изменчивые функции

Комментарии преподавателя

Механизм перепланирования запросов в реальном времени является частью Postgres Pro, поэтому установки дополнительных расширений не требуется.

На данный момент у функциональности перепланирования имеются некоторые ограничения: поддерживаются только операторы SELECT (за исключением SELECT FOR UPDATE и SELECT FOR SHARE) и не поддерживаются курсоры и изменчивые (volatile) функции.

https://postgrespro.ru/docs/enterprise/16/realtime-query-replanning

В 17-й версии Postgres Pro Enterprise функциональность перепланирования переименована в адаптивное выполнение запросов (AQE, Adaptive Query Execution).

Page 10: Итоги

Если запрос выполняется неэффективно, сервер может прервать его и повторить планирование

  • При перепланировании используется накопленная информация о кардинальности узлов плана
  • Перепланирование позволяет автоматически ускорить проблемные запросы

Page 11: Практика

  1. Создайте таблицу с одним столбцом типа char(2000), с отключенной автоочисткой. Заполните ее данными.
    1. Получите план выполнения запроса, выполняющего декартово произведение таблицы самой с собой. Убедитесь в наличии ошибки оценки кардинальности.
    1. Включите перепланирование, ограничив количество попыток одной. Получите новый план.
    1. Снимите ограничение, установленное на количество попыток перепланирования. Сравните три получившихся плана.

Комментарии преподавателя

  1. Воспользуйтесь параметром конфигурации replan_max_attempts.

https://postgrespro.ru/docs/enterprise/16/runtime-config-query#GUC-REPLA N-MAX-ATTEMPTS

Page 12: 1. Таблица

Создадим базу данных и таблицу:

=> CREATE DATABASE pgpro_replan;
CREATE DATABASE
=> \c pgpro_replan
You are now connected to database "pgpro_replan" as user "student".
=> CREATE TABLE t(
  a char(2000)
)
WITH (autovacuum_enabled = off);
CREATE TABLE

Вставим некоторое количество строк:

=> INSERT INTO t SELECT 'A' FROM generate_series(1, 1_000);
INSERT 0 1000

2. Стандартный планировщик

Перепланирование отключено.

=> SHOW replan_enable;
 replan_enable
---------------
 off
(1 row)

Получим план выполнения запроса, соединяющего таблицу саму с собой:

=> EXPLAIN (analyze, timing off)
SELECT *
FROM t t1
  CROSS JOIN t t2;
                                            QUERY PLAN
------------------------------------------------------------------------------------------
--------
 Nested Loop  (cost=0.00..1286.88 rows=62500 width=16008) (actual rows=1000000 loops=1)
   ->  Seq Scan on t t1  (cost=0.00..252.50 rows=250 width=8004) (actual rows=1000
loops=1)
   ->  Materialize  (cost=0.00..253.75 rows=250 width=8004) (actual rows=1000 loops=1000)
         ->  Seq Scan on t t2  (cost=0.00..252.50 rows=250 width=8004) (actual rows=1000
loops=1)
 Planning Time: 56.533 ms
 Execution Time: 156.360 ms
(6 rows)

Налицо ошибка оценки кардинальности в обоих узлах Seq Scan. Она связана с отключенной автоочисткой: анализ не выполнялся, планировщик не знает количество строк в таблице. Планировщик считает, что 250 строк поместятся в оперативной памяти (work_mem), поэтому вставляет узел Materialize, чтобы не считывать таблицу t2 тысячу раз с диска (или из буферного кеша). Обратите внимание, что декартово произведение всегда выполняется только вложенным циклом (подробности можно узнать из курса «QPT. Оптимизация запросов»), поэтому план в любом случае не изменится — пример носит иллюстративный, а не практический характер.

3. Перепланирование с одной попыткой

Включим перепланирование:

=> SET replan_enable = true;
SET

Page 13: Page 13

=> SET replan_overrun_limit = 2; -- достаточно для нашего примера
SET
=> SET replan_max_attempts = 1;  -- количество попыток перепланирования
SET

Получим новый план:

=> EXPLAIN (analyze, timing off)
SELECT *
FROM t t1
  CROSS JOIN t t2;
                                            QUERY PLAN
------------------------------------------------------------------------------------------
--------
 Nested Loop  (cost=0.00..3630.62 rows=250000 width=16008) (actual rows=1000000 loops=1)
   ->  Seq Scan on t t2  (cost=0.00..252.50 rows=1000 width=8004) (actual rows=1000
loops=1)
   ->  Materialize  (cost=0.00..253.75 rows=250 width=8004) (actual rows=1000 loops=1000)
         ->  Seq Scan on t t1  (cost=0.00..252.50 rows=250 width=8004) (actual rows=1000
loops=1)
 Planning Time: 3.327 ms
 Execution Time: 163.357 ms
(6 rows)

Что произошло? После завершения внутреннего цикла по таблице t2 (и одной итерации внешнего) планировщику стало известно, что в таблице t2 находится 1000 строк. Сработал триггер перепланирования. Теперь внутренний цикл выполняется по таблице t1 с промежуточным узлом материализации, поскольку планировщик все еще считает, что в t1 всего 250 строк. Кардинальность узла Seq Scan по t2 оценена верно.

4. Перепланирование с двумя попытками

Снимем ограничение на количество попыток перепланирования:

=> RESET replan_max_attempts;
RESET

Снова получаем план:

=> EXPLAIN (analyze, timing off)
SELECT *
FROM t t1
  CROSS JOIN t t2;
                                           QUERY PLAN
------------------------------------------------------------------------------------------
------
 Nested Loop  (cost=0.00..262752.50 rows=1000000 width=16008) (actual rows=1000000
loops=1)
   ->  Seq Scan on t t1  (cost=0.00..252.50 rows=1000 width=8004) (actual rows=1000
loops=1)
   ->  Seq Scan on t t2  (cost=0.00..252.50 rows=1000 width=8004) (actual rows=1000
loops=1000)
 Planning Time: 2.841 ms
 Execution Time: 449.833 ms
(5 rows)

Поскольку информация перепланирования не сохраняется между запросами, процесс повторяется. После завершения внутреннего цикла по таблице t2 планировщику стало известно, что в таблице t2 находится 1000 строк. Сработал триггер перепланирования, план был перестроен так, как мы видели в п. 3, а запрос начал выполняться с начала. Теперь после завершения внутреннего цикла по таблице t1 планировщику стало известно, что и в таблице t1 находится 1000 строк. Снова сработал триггер перепланирования, план был перестроен во второй раз. Теперь кардинальность обоих узлов Seq Scan оценена правильно; узел материализации пропал, поскольку тысяча строк не помещается в память work_mem.

Уборка

Удалим созданную базу данных.

=> \c student

Page 14: Page 14

You are now connected to database "student" as user "student".
=> DROP DATABASE pgpro_replan WITH (FORCE);
DROP DATABASE

11 Занятие 11: Производительность / Сохранение планов запросов

Исходная страница в нативном Markdown: Производительность / Сохранение планов запросов
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_11_multiplan.html Archive: PGPRO-handouts-16.zip Страниц PDF: 33

Page 1: Производительность / Сохранение планов запросов

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Модуль pgpro_multiplan
  • Замороженные планы
  • Песочница
  • Планы с шаблонами
  • Базовые планы
  • Совместная работа с перепланированием

Page 3: Модуль pgpro_multiplan

Стабилизация планов выполнения запросов

  • заморозка плана для конкретного запроса или группы запросов
  • разделяемый кеш замороженных планов
  • планы сохраняются в файлах сервера,
  • для репликации: pgpro_multiplan.wal_rw
  • Заменяет устаревшее расширение sr_plan

Комментарии преподавателя

Расширение pgpro_multiplan позволяет «заморозить» определенный план выполнения запроса (или привязать план к целой группе запросов) и при дальнейшем исполнении запроса использовать этот план вместо того, который построил бы планировщик. Стабилизация некоторых планов полезна, например, при обновлении сервера (в новых версиях планировщик в среднем работает лучше, но планы небольшой части запросов могут измениться и в худшую сторону), или для оптимизации отдельного запроса, когда не удается иными средствами убедить планировщик построить нужный план.

Замороженные планы сохраняются в файлы в отдельном каталоге, а работа с ними происходит в разделяемой памяти. Таким образом, в отличие от обычного локального кеша разобранных запросов и их планов, кеш замороженных планов — общий для всех сеансов.

Планы хранятся в файлах, а не в таблицах БД, чтобы обеспечить совместимость расширения с системой 1С. Обратной стороной такого решения является то, что замороженные планы не передаются с мастера на реплику автоматически: если это необходимо, требуется включить параметр pgpro_multiplan.wal_rw на обоих серверах.

https://postgrespro.ru/docs/enterprise/16/pgpro-multiplan

До версии Postgres Pro Enterprise 16 включительно в продукте присутствует расширение sr_plan с аналогичным функционалом. Начиная с версии 15, оно считается устаревшим и вместо него рекомендуется использовать расширение pgpro_multiplan.

https://postgrespro.ru/docs/enterprise/16/pgpro-multiplan#PGPRO-MULTIPL AN-MIGRATION-FROM-SR-PLAN

Page 4: Типы планов

Сериализованный — serialized

  • хранится внутреннее представление плана
  • зависит от системного каталога: OID, структуры таблиц и т. п.
  • действует только в текущей базе данных
  • планирование не требуется
  • Набор указаний — hintset
    • хранятся указания расширения pg_hint_plan
    • запрос планируется с учетом указаний

Комментарии преподавателя

План может сохраняться в двух вариантах.

Сериализованный план — это дерево плана, представленное в виде текстовой строки.

Сериализованный план действителен только в текущей базе данных, он зависит от идентификаторов объектов, структуры таблиц и индексов. Например, если пересоздать используемую в плане таблицу (даже с тем же именем), то сохраненный план станет недействительным.

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

Набор указаний — хранится как указания расширения pg_hint_plan. Для этого требуется, чтобы было установлено и само расширение pg_hint_plan, иначе указания будут проигнорированы. В таком случае план не зависит от идентификаторов объектов. Запрос при выполнении приходится планировать, но указания подсказывают планировщику, какой именно план требуется получить.

https://postgrespro.ru/docs/enterprise/16/pgpro-multiplan#PGPRO-MULTIPL AN-SUPPORTED-MODES

Page 5: Заморозка плана

Source slide page 5: Заморозка плана{#source-figure-005 caption=“Source slide page 5: Заморозка плана”}

SELECT *
FROM bookings
WHERE book_ref > '800000'
 Sort
   Sort Key
   ->  Seq Scan
  sql hash: 457033572132829198
const hash: 1320965230
  sql: 457033572132829198
const: 1320965230
Sort
   Sort Key
   ->  Seq Scan

Комментарии преподавателя

Первый из режимов работы расширения — заморозка плана — состоит из трех этапов: регистрации запроса, построения плана и собственно заморозки.

Зарегистрировать запрос можно одним из двух способов:

● функцией pgpro_multiplan_register_query, в которую необходимо передать текст запроса (можно в параметризованном виде) и список типов параметров (можно доверить расширению автоматически определить типы);

● с помощью автоматической регистрации запросов (параметр pgpro_multiplan.auto_tracking, по умолчанию выключен).

При этом запрос идентифицируется двумя хеш-кодами:

● sql_hash — вычисляется по дереву разбора без учета параметров и констант (тот же хеш-код используется, например, в расширении pg_stat_statements; требуется включенный compute_query_id);

● const_hash — вычисляется по типам и значениям констант.

Далее запрос планируется, при этом допускается влиять на план любыми доступными способами: параметрами оптимизатора, с помощью расширений AQO или pg_hint_plan и т. п.

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

Page 6: Замороженные планы

Начнем с установки расширения:

=> ALTER SYSTEM SET shared_preload_libraries = pg_hint_plan, pgpro_multiplan;
ALTER SYSTEM

Обратите внимание, что библиотека pg_hint_plan загружается перед pgpro_multiplan. Необходим перезапуск экземпляра:

student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql demo

Добавим расширение pgpro_multiplan в базу данных:

=> CREATE EXTENSION pgpro_multiplan;
CREATE EXTENSION

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

=> SET pgpro_multiplan.mode = frozen;
SET

Запрос, выбирающий половину бронирований, использует индексный доступ:

=> \pset format wrapped
Output format is wrapped.
=> \pset columns 90
Target width is 90.
=> EXPLAIN (costs off)
SELECT * FROM bookings WHERE book_ref > '800000';
                 QUERY PLAN
---------------------------------------------
 Index Scan using bookings_pkey on bookings
   Index Cond: (book_ref > '800000'::bpchar)
(2 rows)

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

=> SELECT * FROM pgpro_multiplan_register_query('SELECT * FROM bookings WHERE book_ref > $1;');
      sql_hash       | const_hash
---------------------+------------
 3221999367686871845 |          0
(1 row)

Теперь в плане видно, что запрос отслеживается расширением (в корне плана появился узел Custom Scan (MultiplanScan) с пометкой Plan is: tracked):

=> EXPLAIN (costs off)
SELECT * FROM bookings WHERE book_ref > '800000';
                    QUERY PLAN
--------------------------------------------------
 Custom Scan (MultiplanScan)
   Plan is: tracked
   SQL hash: 3221999367686871845
   Const hash: 0
   Plan hash: 0
   ->  Index Scan using bookings_pkey on bookings
         Index Cond: (book_ref > $1)
(7 rows)

Запретим использовать индексный доступ:

=> SET enable_indexscan = off;

Page 7: Page 7

SET
=> SET enable_bitmapscan = off;
SET
=> EXPLAIN (costs off)
SELECT * FROM bookings WHERE book_ref > '800000';
           QUERY PLAN
---------------------------------
 Custom Scan (MultiplanScan)
   Plan is: tracked
   SQL hash: 3221999367686871845
   Const hash: 0
   Plan hash: 0
   ->  Seq Scan on bookings
         Filter: (book_ref > $1)
(7 rows)

Чтобы заморозить полученный план, воспользуемся функцией pgpro_multiplan_freeze:

=> SELECT pgpro_multiplan_freeze(); -- 'serialized'
 pgpro_multiplan_freeze
------------------------
 t
(1 row)

Вернем оптимизатору все возможности для построения планов:

=> RESET enable_indexscan;
RESET
=> RESET enable_bitmapscan;
RESET

И повторно выполним запрос — возьмем для примера другую константу:

=> EXPLAIN (costs off)
SELECT * FROM bookings WHERE book_ref > '777777';
           QUERY PLAN
---------------------------------
 Custom Scan (MultiplanScan)
   Plan is: frozen, serialized
   SQL hash: 3221999367686871845
   Const hash: 0
   Plan hash: 0
   ->  Seq Scan on bookings
         Filter: (book_ref > $1)
(7 rows)

Теперь сервер использует замороженный план: Plan is: frozen, serialized. Информацию о замороженных планах можно увидеть в представлении pgpro_multiplan_storage:

=> \pset xheader_width 90
Expanded header width is 90.
=> SELECT dbid, sql_hash, const_hash, valid, query_string, paramtypes, plan_type
FROM pgpro_multiplan_storage \gx
-[ RECORD 1 ]+--------------------------------------------
dbid         | 16388
sql_hash     | 3221999367686871845
const_hash   | 0
valid        | t
query_string | SELECT * FROM bookings WHERE book_ref > $1;
paramtypes   | {character}
plan_type    | serialized

План можно заморозить и с типом hintset, то есть в виде указаний, сформированных с помощью расширения pg_hint_plan. Добавим расширение pg_hint_plan в текущую базу данных:

Page 8: Page 8

=> CREATE EXTENSION pg_hint_plan;
CREATE EXTENSION

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

=> EXPLAIN SELECT * FROM ticket_flights WHERE flight_id < 1000;
                                      QUERY PLAN
--------------------------------------------------------------------------------------
 Gather  (cost=1000.00..20812.09 rows=55256 width=32)
   Workers Planned: 2
   ->  Parallel Seq Scan on ticket_flights  (cost=0.00..14286.49 rows=23023 width=32)
         Filter: (flight_id < 1000)
(4 rows)

Зарегистрируем его — на этот раз используем не параметр, а константу:

=> SELECT * FROM pgpro_multiplan_register_query('SELECT * FROM ticket_flights WHERE flight_id < 1000;');
       sql_hash       | const_hash
----------------------+------------
 -8587368766006511412 | 1941396791
(1 row)

Попросим планировщик использовать сканирование индекса, используя подсказки pg_hint_plan:

=> /*+ IndexScan(ticket_flights ticket_flights_pkey) */
 EXPLAIN SELECT * FROM ticket_flights WHERE flight_id < 1000;
                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Custom Scan (MultiplanScan)  (cost=0.42..64454.01 rows=55256 width=32)
   Plan is: tracked
   SQL hash: -8587368766006511412
   Const hash: 1941396791
   Plan hash: 0
   ->  Index Scan using ticket_flights_pkey on ticket_flights  (cost=0.42..64454.01 rows=.
.55256 width=32)
         Index Cond: (flight_id < 1000)
(7 rows)

Заморозим план с типом hintset:

=> SELECT pgpro_multiplan_freeze('hintset');
 pgpro_multiplan_freeze
------------------------
 t
(1 row)

И проверим план — в нем без указаний pg_hint_plan используется сканирование индекса:

=> EXPLAIN SELECT * FROM ticket_flights WHERE flight_id < 1000;
                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Custom Scan (MultiplanScan)  (cost=0.42..64454.01 rows=55256 width=32)
   Plan is: frozen, hintset
   SQL hash: -8587368766006511412
   Const hash: 1941396791
   Plan hash: 0
   ->  Index Scan using ticket_flights_pkey on ticket_flights  (cost=0.42..64454.01 rows=.
.55256 width=32)
         Index Cond: (flight_id < 1000)
(7 rows)

На этот раз замена константы приводит к запросу, для которого план не замораживался, поэтому планировщик выбирает план с последовательным сканированием:

=> EXPLAIN SELECT * FROM ticket_flights WHERE flight_id < 1001;

Page 9: Page 9

                                      QUERY PLAN
--------------------------------------------------------------------------------------
 Gather  (cost=1000.00..20814.09 rows=55276 width=32)
   Workers Planned: 2
   ->  Parallel Seq Scan on ticket_flights  (cost=0.00..14286.49 rows=23032 width=32)
         Filter: (flight_id < 1001)
(4 rows)

Посмотрим на информацию о замороженных планах:

=> SELECT dbid, sql_hash, const_hash, valid, query_string, paramtypes, plan_type, hintstr
FROM pgpro_multiplan_storage \gx
-[ RECORD 1 ]+---------------------------------------------------------------------------
dbid         | 16388
sql_hash     | 3221999367686871845
const_hash   | 0
valid        | t
query_string | SELECT * FROM bookings WHERE book_ref > $1;
paramtypes   | {character}
plan_type    | serialized
hintstr      | Set(enable_bitmapscan "off") Set(enable_indexscan "off") Set(search_path ".
             |.bookings, public") SeqScan("bookings")
-[ RECORD 2 ]+---------------------------------------------------------------------------
dbid         | 16388
sql_hash     | -8587368766006511412
const_hash   | 1941396791
valid        | t
query_string | SELECT * FROM ticket_flights WHERE flight_id < 1000;
paramtypes   |
plan_type    | hintset
hintstr      | Set(search_path "bookings, public") IndexScan("ticket_flights" "ticket_fli.
             |.ghts_pkey")

Page 10: Песочница

Альтернативное хранилище планов

  • pgpro_multiplan.sandbox = off
  • необходимы права суперпользователя
  • Поддерживается перенос планов в основное хранилище
    • ручное копирование из представления pgpro_multiplan_storage
    • данные не реплицируются из песочницы на ведомый сервер

Комментарии преподавателя

Расширение pgpro_multiplan позволяет задействовать для хранения замороженных планов песочницу: отдельную, независимую от основного хранилища область разделяемой памяти и каталог на диске. Переключением на альтернативное хранилище планов управляет параметр pgpro_multiplan.sandbox (по умолчанию он выключен). Менять значение этого параметра могут только суперпользователи.

И к основному хранилищу, и к песочнице существует только один общий интерфейс доступа — представление pgpro_multiplan_storage.

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

Данные из песочницы не реплицируются на ведомый сервер.

Page 11: Песочница

Включим параметр pgpro_multiplan.sandbox, чтобы переключиться на альтернативное хранилище:

=> SET pgpro_multiplan.sandbox = on;
SET

Теперь в представлении pgpro_multiplan_storage пусто, так как в песочнице пока нет ни одного плана:

=> SELECT dbid, sql_hash, valid, query_string, paramtypes, plan_type
FROM pgpro_multiplan_storage;
 dbid | sql_hash | valid | query_string | paramtypes | plan_type
------+----------+-------+--------------+------------+-----------
(0 rows)

Повторим предыдущие действия, но для нового запроса к таблице tickets, который использует индексное сканирование:

=> EXPLAIN (costs off)
SELECT * FROM tickets WHERE ticket_no = '0005432001001';
                     QUERY PLAN
-----------------------------------------------------
 Index Scan using tickets_pkey on tickets
   Index Cond: (ticket_no = '0005432001001'::bpchar)
(2 rows)

Зарегистрируем запрос:

=> SELECT * FROM pgpro_multiplan_register_query('SELECT * FROM tickets WHERE ticket_no = $1;');
       sql_hash       | const_hash
----------------------+------------
 -8230881202064992936 |          0
(1 row)

Запретим использовать индексный доступ:

=> SET enable_indexscan = off;
SET
=> SET enable_bitmapscan = off;
SET
=> EXPLAIN (costs off)
SELECT * FROM tickets WHERE ticket_no = '0005432001001';
                QUERY PLAN
------------------------------------------
 Custom Scan (MultiplanScan)
   Plan is: tracked
   SQL hash: -8230881202064992936
   Const hash: 0
   Plan hash: 0
   ->  Gather
         Workers Planned: 2
         ->  Parallel Seq Scan on tickets
               Filter: (ticket_no = $1)
(9 rows)

Заморозим полученный план:

=> SELECT pgpro_multiplan_freeze();
 pgpro_multiplan_freeze
------------------------
 t
(1 row)

Вернем оптимизатору все возможности для построения планов:

=> RESET enable_indexscan;

Page 12: Page 12

RESET
=> RESET enable_bitmapscan;
RESET

И повторно выполним запрос:

=> EXPLAIN (costs off)
SELECT * FROM tickets WHERE ticket_no = '0005432001001';
                QUERY PLAN
------------------------------------------
 Custom Scan (MultiplanScan)
   Plan is: frozen, serialized
   SQL hash: -8230881202064992936
   Const hash: 0
   Plan hash: 0
   ->  Gather
         Workers Planned: 2
         ->  Parallel Seq Scan on tickets
               Filter: (ticket_no = $1)
(9 rows)

Этот план сохранен в песочнице:

=> SELECT dbid, sql_hash, valid,
query_string, sample_string, paramtypes, plan_type
FROM pgpro_multiplan_storage \gx
-[ RECORD 1 ]-+---------------------------------------------------------
dbid          | 16388
sql_hash      | -8230881202064992936
valid         | t
query_string  | SELECT * FROM tickets WHERE ticket_no = $1;
sample_string | SELECT * FROM tickets WHERE ticket_no = '0005432001001';
paramtypes    | {character}
plan_type     | serialized

План можно перенести из песочницы в основное хранилище, сохранив данные из представления pgpro_multiplan_storage в таблицу:

=> CREATE TABLE storage_copy AS SELECT *
FROM pgpro_multiplan_storage;
SELECT 1

Если переключиться на основное хранилище, расширение будет видеть сохраненные ранее данные:

=> SET pgpro_multiplan.sandbox = off;
SET
=> SELECT dbid, sql_hash, valid, query_string, paramtypes, plan_type
FROM pgpro_multiplan_storage \gx
-[ RECORD 1 ]+-----------------------------------------------------
dbid         | 16388
sql_hash     | 3221999367686871845
valid        | t
query_string | SELECT * FROM bookings WHERE book_ref > $1;
paramtypes   | {character}
plan_type    | serialized
-[ RECORD 2 ]+-----------------------------------------------------
dbid         | 16388
sql_hash     | -8587368766006511412
valid        | t
query_string | SELECT * FROM ticket_flights WHERE flight_id < 1000;
paramtypes   |
plan_type    | hintset

Например, для запроса к таблице bookings с неравенством будет снова использоваться сохраненный план:

=> EXPLAIN (costs off)
SELECT * FROM bookings WHERE book_ref > '800000';

Page 13: Page 13

           QUERY PLAN
---------------------------------
 Custom Scan (MultiplanScan)
   Plan is: frozen, serialized
   SQL hash: 3221999367686871845
   Const hash: 0
   Plan hash: 0
   ->  Seq Scan on bookings
         Filter: (book_ref > $1)
(7 rows)

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

=> SELECT pgpro_multiplan_restore(query_string, sample_string, hintstr, paramtypes, plan_type, NULL, NULL)
FROM storage_copy;
 pgpro_multiplan_restore
--------------------------
 (-8230881202064992936,0)
(1 row)
=> SELECT dbid, sql_hash, valid, query_string, paramtypes, plan_type FROM pgpro_multiplan_storage \gx
-[ RECORD 1 ]+-----------------------------------------------------
dbid         | 16388
sql_hash     | 3221999367686871845
valid        | t
query_string | SELECT * FROM bookings WHERE book_ref > $1;
paramtypes   | {character}
plan_type    | serialized
-[ RECORD 2 ]+-----------------------------------------------------
dbid         | 16388
sql_hash     | -8587368766006511412
valid        | t
query_string | SELECT * FROM ticket_flights WHERE flight_id < 1000;
paramtypes   |
plan_type    | hintset
-[ RECORD 3 ]+-----------------------------------------------------
dbid         | 16388
sql_hash     | -8230881202064992936
valid        | t
query_string | SELECT * FROM tickets WHERE ticket_no = $1;
paramtypes   | {character}
plan_type    | serialized

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

Page 14: Планы с шаблонами

Source slide page 14: Планы с шаблонами{#source-figure-014 caption=“Source slide page 14: Планы с шаблонами”}

SELECT *
FROM bookings
WHERE book_ref > '800000'
 Sort
   Sort Key
   ->  Seq Scan
  sql hash: 457033572132829198
table hash: 289967942
      sql: 457033572132829198
wildcards: ^t[0-9]\_bookings
Sort
   Sort Key
   ->  Seq Scan

Комментарии преподавателя

План с шаблонами (template) хранится в виде набора указаний, как и план с указаниями (hintset). Отличие состоит в том, что такой план применяется ко всех запросам, совпадающим с исходным с точностью до имен таблиц.

Говоря точнее, запрос идентифицируется двумя хеш-кодами:

● sql_hash — построен по дереву разбора,

● table_hash — построен по именам таблиц, не соответствующим регулярному выражению из параметра pgpro_multiplan.wildcards.

Константы (const_hash) не учитываются. Значение параметра pgpro_multiplan.wildcards сохраняется вместе с замороженным планом (table_hash не сохраняется, а вычисляется при необходимости).

План с шаблонами позволяет применять один и тот же план к запросам с одинаковой структурой, но разными названиями таблиц (например, запросы к временным таблицам, генерируемые системой 1С).

Такой план применяется, только если не нашлось точного соответствия (плана с типом serialized или hintset).

https://postgrespro.ru/docs/enterprise/16/pgpro-multiplan#PGPRO-MULTIPL AN-SUPPORTED-MODES

Page 15: Планы с шаблонами

Для планов с шаблонами нужно расширение pg_hint_plan, оно уже загружено в базу данных. Добавим режим wildcards, позволяющий использовать шаблоны:

=> SET pgpro_multiplan.mode = 'frozen,wildcards';
SET

Удалим всю собранную информацию из хранилища расширения:

=> SELECT pgpro_multiplan_reset();
 pgpro_multiplan_reset
-----------------------
                     3
(1 row)

Для экспериментов нам понадобятся две временные таблицы. Создадим их на основе таблицы bookings и дадим схожие имена:

=> CREATE TEMP TABLE t1_bookings AS SELECT * FROM bookings;
SELECT 262788
=> CREATE TEMP TABLE t2_bookings AS SELECT * FROM bookings;
SELECT 262788
=> ANALYZE t1_bookings, t2_bookings;
ANALYZE

Построим индексы, чтобы у планировщика было больше вариантов доступа к таблице:

=> CREATE INDEX ON t1_bookings(book_ref);
CREATE INDEX
=> CREATE INDEX ON t2_bookings(book_ref);
CREATE INDEX

Как и в предыдущих примерах, в запросе используется индексное сканирование:

=> EXPLAIN (costs off)
SELECT * FROM t1_bookings WHERE book_ref > '800000';
                        QUERY PLAN
----------------------------------------------------------
 Index Scan using t1_bookings_book_ref_idx on t1_bookings
   Index Cond: (book_ref > '800000'::bpchar)
(2 rows)

Укажем для параметра pgpro_multiplan.wildcards регулярное выражение:

=> SET pgpro_multiplan.wildcards = '^t[0-9]\_bookings';
SET

Этому регулярному выражению соответствуют имена, состоящие из символа t, цифры и суффикса _bookings — как у наших временных таблиц:

=> SELECT 't1_bookings' ~ '^t[0-9]\_bookings';
 ?column?
----------
 t
(1 row)

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

=> SET enable_indexscan = off;
SET

Page 16: Page 16

=> SET enable_bitmapscan = off;
SET

Зарегистрируем запрос к таблице t1_bookings:

=> SELECT * FROM pgpro_multiplan_register_query('SELECT * FROM t1_bookings WHERE book_ref > ''800000'';');
      sql_hash       | const_hash
---------------------+------------
 3221999366451720086 | 3711756922
(1 row)

Выполним запрос:

=> EXPLAIN (costs off)
SELECT * FROM t1_bookings WHERE book_ref > '800000';
                  QUERY PLAN
-----------------------------------------------
 Custom Scan (MultiplanScan)
   Plan is: tracked
   SQL hash: 3221999366451720086
   Const hash: 3711756922
   Plan hash: 0
   ->  Seq Scan on t1_bookings
         Filter: (book_ref > '800000'::bpchar)
(7 rows)

Он использует последовательное сканирование и отслеживается расширением (tracked). Сохраним полученный план как шаблонный:

=> SELECT pgpro_multiplan_freeze('template');
 pgpro_multiplan_freeze
------------------------
 t
(1 row)

Вместе с планом расширение сохранило текущее значение параметра pgpro_multiplan.wildcards:

=> SELECT sql_hash, query_string, paramtypes, plan_type, wildcards, hintstr
FROM pgpro_multiplan_storage \gx
-[ RECORD 1 ]+---------------------------------------------------------------------------
sql_hash     | 4146829285245706965
query_string | SELECT * FROM t1_bookings WHERE book_ref > '800000';
paramtypes   |
plan_type    | template
wildcards    | ^t[0-9]\_bookings
hintstr      | Set(enable_bitmapscan "off") Set(enable_indexscan "off") Set(search_path ".
             |.bookings, public") SeqScan("pgpro_multiplan_0")

В столбце hintstr находится набор указаний для pg_hint_plan. Вернем оптимизатору все возможности для построения планов:

=> RESET enable_indexscan;
RESET
=> RESET enable_bitmapscan;
RESET

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

=> EXPLAIN (costs off)
SELECT * FROM t2_bookings WHERE book_ref > '777777';

Page 17: Page 17

                  QUERY PLAN
-----------------------------------------------
 Custom Scan (MultiplanScan)
   Plan is: frozen, template
   SQL hash: 4146829285245706965
   Const hash: 2654435769
   Plan hash: 0
   ->  Seq Scan on t2_bookings
         Filter: (book_ref > '777777'::bpchar)
(7 rows)

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

=> EXPLAIN (costs off)
SELECT * FROM bookings WHERE book_ref > '800000';
                 QUERY PLAN
---------------------------------------------
 Index Scan using bookings_pkey on bookings
   Index Cond: (book_ref > '800000'::bpchar)
(2 rows)

Используется индексное сканирование, поскольку замороженного плана для этого запроса нет.

Page 18: Базовые планы

Source slide page 18: Базовые планы{#source-figure-018 caption=“Source slide page 18: Базовые планы”}

 Sort
   Sort Key
   ->  Seq Scan
SELECT *
FROM bookings
WHERE book_ref > '800000'
 sql: 457033572132829198
plan: 1320965230
 sql: 457033572132829198
plan: 1320965230
Sort
   Sort Key
   ->  Seq Scan
Sort
   Sort Key
   ->  Seq Scan

Комментарии преподавателя

Базовые планы применяют, когда для одного запроса может существовать несколько удачных планов. Если планировщик выбрал один из базовых планов, запрос выполняется с этим планом. В противном случае расширение принудительно устанавливает для запроса самый дешевый из существующих базовых планов. Базовые планы рассматриваются, только если не нашлось замороженного плана (с типом serialized, hintset или template).

Базовые планы идентифицируются двумя хеш-кодами:

● sql_hash — вычисляется так же, как и для замороженных планов;

● plan_hash — внутренний идентификатор плана.

Константы не учитываются, а const_hash всегда равен 0.

Чтобы добавить план как базовый, можно действовать, как в случае с заморозкой: зарегистрировать запрос, выполнить планирование и заморозить план функцией pgpro_multiplan_freeze с аргументом ‘baseline’.

Другой вариант — использовать автозахват, установив параметр pgpro_multiplan.auto_capturing. В этом случае все выполняющиеся запросы захватываются и их планы становятся кандидатами на сохранение. Захваченные запросы с планами видны в представлении pgpro_multiplan_captured_queries. Захваченный план можно одобрить, вызвав функцию pgpro_multiplan_captured_approve, после чего он попадает в общее хранилище и будет применяться.

https://postgrespro.ru/docs/enterprise/16/pgpro-multiplan#PGPRO-MULTIPL AN-SUPPORTED-MODES

Page 19: Базовые планы

Чтобы захватывать запросы, включаем конфигурационный параметр:

=> SET pgpro_multiplan.auto_capturing = on;
SET

Воспользуемся тем же примером:

=> SET enable_indexscan = off;
SET
=> SET enable_bitmapscan = off;
SET

Запрос использует последовательное сканирование:

=> EXPLAIN (costs off)
SELECT * FROM bookings WHERE book_ref > '80000';
               QUERY PLAN
----------------------------------------
 Seq Scan on bookings
   Filter: (book_ref > '80000'::bpchar)
(2 rows)

Захваченный запрос и план его выполнения можно увидеть в представлении:

=> SELECT dbid, sql_hash, plan_hash, query_string, cost, explain_plan
FROM pgpro_multiplan_captured_queries \gx
-[ RECORD 1 ]+-------------------------------------------------------------------------
dbid         | 16388
sql_hash     | 3221999367686871845
plan_hash    | -395657591727867684
query_string | SELECT * FROM bookings WHERE book_ref > $1;
cost         | 4996.85
explain_plan | Seq Scan on bookings.bookings  (cost=0.00..4996.85 rows=131394 width=21)+
             |   Output: book_ref, book_date, total_amount                             +
             |   Filter: (bookings.book_ref > '80000'::bpchar)                         +
             | Query Identifier: -8197269004796392149                                  +
             |

Чтобы планировщик мог использовать захваченный план, его надо одобрить:

=> SELECT pgpro_multiplan_captured_approve(
  dbid => 16388,
  sql_hash => 3221999367686871845,
  plan_hash => -395657591727867684
);
 pgpro_multiplan_captured_approve
----------------------------------
 t
(1 row)

После одобрения план удаляется из хранилища захваченных планов:

=> SELECT * FROM pgpro_multiplan_captured_queries \gx
(0 rows)

Получим и одобрим еще один план:

=> RESET enable_bitmapscan;
RESET
=> SET enable_seqscan = off;
SET
=> EXPLAIN (costs off)
SELECT * FROM bookings WHERE book_ref > '80000';

Page 20: Page 20

                    QUERY PLAN
--------------------------------------------------
 Bitmap Heap Scan on bookings
   Recheck Cond: (book_ref > '80000'::bpchar)
   ->  Bitmap Index Scan on bookings_pkey
         Index Cond: (book_ref > '80000'::bpchar)
(4 rows)
=> SELECT dbid, sql_hash, plan_hash, query_string, cost, explain_plan
FROM pgpro_multiplan_captured_queries \gx
-[ RECORD 1 ]+---------------------------------------------------------------------------
dbid         | 16388
sql_hash     | 3221999367686871845
plan_hash    | -6676099641389655070
query_string | SELECT * FROM bookings WHERE book_ref > $1;
cost         | 5821.151
explain_plan | Bitmap Heap Scan on bookings.bookings  (cost=2466.73..5821.15 rows=131394 .
             |.width=21)                                                                 +
             |   Output: book_ref, book_date, total_amount                               +
             |   Recheck Cond: (bookings.book_ref > '80000'::bpchar)                     +
             |   ->  Bitmap Index Scan on bookings_pkey  (cost=0.00..2433.88 rows=131394 .
             |.width=0)                                                                  +
             |         Index Cond: (bookings.book_ref > '80000'::bpchar)                 +
             | Query Identifier: -8197269004796392149                                    +
             |
=> SELECT pgpro_multiplan_captured_approve(
  dbid => 16388,
  sql_hash => 3221999367686871845,
  plan_hash => -6676099641389655070
);
 pgpro_multiplan_captured_approve
----------------------------------
 t
(1 row)

Сбросим параметры и отключим автоматический захват планов:

=> RESET enable_indexscan;
RESET
=> RESET enable_seqscan;
RESET
=> SET pgpro_multiplan.auto_capturing = off;
SET

Одобренные планы находятся в общем хранилище:

=> SELECT sql_hash, query_string, cost, plan_type, hintstr
FROM pgpro_multiplan_storage
WHERE plan_type = 'baseline' \gx
-[ RECORD 1 ]+---------------------------------------------------------------------------
sql_hash     | 3221999367686871845
query_string | SELECT * FROM bookings WHERE book_ref > $1;
cost         | 4996.85
plan_type    | baseline
hintstr      | Set(enable_bitmapscan "off") Set(enable_indexscan "off") Set(search_path ".
             |.bookings, public") SeqScan("bookings")
-[ RECORD 2 ]+---------------------------------------------------------------------------
sql_hash     | 3221999367686871845
query_string | SELECT * FROM bookings WHERE book_ref > $1;
cost         | 5821.151
plan_type    | baseline
hintstr      | Set(enable_indexscan "off") Set(enable_seqscan "off") Set(search_path "boo.
             |.kings, public") BitmapScan("bookings" "bookings_pkey")

Теперь добавим baseline к списку режимов расширения, чтобы планировщик выбирал только одобренные базовые планы. Добавим также режим statistics, чтобы включить сбор статистики:

=> SET pgpro_multiplan.mode = 'frozen,wildcards,baseline,statistics';
SET

Page 21: Page 21

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

=> SELECT * FROM pgpro_multiplan_stats \gx
(0 rows)

Повторим запрос:

=> EXPLAIN (costs off)
SELECT * FROM bookings WHERE book_ref > '800000';
                  QUERY PLAN
-----------------------------------------------
 Custom Scan (MultiplanScan)
   Plan is: baseline
   SQL hash: 3221999367686871845
   Const hash: 0
   Plan hash: -395657591727867684
   ->  Seq Scan on bookings
         Filter: (book_ref > '800000'::bpchar)
(7 rows)

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

=> SET enable_seqscan = off;
SET
=> SET enable_indexscan = off;
SET
=> EXPLAIN (costs off)
SELECT * FROM bookings WHERE book_ref > '777777';
                       QUERY PLAN
---------------------------------------------------------
 Custom Scan (MultiplanScan)
   Plan is: baseline
   SQL hash: 3221999367686871845
   Const hash: 0
   Plan hash: -6676099641389655070
   ->  Bitmap Heap Scan on bookings
         Recheck Cond: (book_ref > '777777'::bpchar)
         ->  Bitmap Index Scan on bookings_pkey
               Index Cond: (book_ref > '777777'::bpchar)
(9 rows)

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

=> SELECT * FROM pgpro_multiplan_stats \gx
-[ RECORD 1 ]--------------------
dbid       | 16388
sql_hash   | 3221999367686871845
const_hash | 0
plan_hash  | -395657591727867684
planid     | -8936307906260647768
counter    | 1
-[ RECORD 2 ]--------------------
dbid       | 16388
sql_hash   | 3221999367686871845
const_hash | 0
plan_hash  | -6676099641389655070
planid     | 6680313372433506506
counter    | 1

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

Page 22: Page 22

=> RESET enable_seqscan;
RESET
=> RESET enable_indexscan;
RESET

Page 23: Совместная работа

Если план неадекватен, срабатывает условие перепланирования

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

Комментарии преподавателя

Сервер может совместно использовать перепланирование и сохранение планов.

Если стандартный планировщик ошибается и срабатывает одно из условий перепланирования, запрос прерывается и отправляется на повторное планирование. Расширение pgpro_multiplan может захватить и одобрить новый план, а при последующих вызовах запроса сразу подставить сохраненный качественный план. Система больше не тратит ресурсы на перепланирование этого конкретного запроса — план стабилен.

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

● auto_approve_plan — управление планами в реальном времени.

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

● individual_triggers — индивидуальные триггеры перепланирования для отдельных запросов.

● statistics — сбор статистики перепланирования в реальном времени.

https://postgrespro.ru/docs/enterprise/16/pgpro-multiplan#PGPRO-MULTIPL AN-USING-REAL-TIME-QUERY-REPLANNING

Page 24: Совместная работа

Модуль pgpro_multiplan можно использовать совместно с перепланированием запросов.

=> LOAD 'pg_hint_plan';
LOAD
=> LOAD 'pgpro_multiplan';
LOAD

Выполним запрос:

=> EXPLAIN (analyze, timing off)
SELECT *
FROM ticket_flights tf
  JOIN tickets t ON t.ticket_no = tf.ticket_no
WHERE tf.fare_conditions = 'Business'
  AND length(tf.ticket_no) = 13
  AND tf.amount >  50000;
                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Gather  (cost=1000.42..18693.22 rows=38 width=136) (actual rows=30133 loops=1)
   Workers Planned: 2
   Workers Launched: 2
   ->  Nested Loop  (cost=0.42..17689.42 rows=16 width=136) (actual rows=10044 loops=3)
         ->  Parallel Seq Scan on ticket_flights tf  (cost=0.00..17554.38 rows=16 width=3.
.2) (actual rows=10044 loops=3)
               Filter: ((amount > '50000'::numeric) AND ((fare_conditions)::text = 'Busin.
.ess'::text) AND (length(ticket_no) = 13))
               Rows Removed by Filter: 338531
         ->  Index Scan using tickets_pkey on tickets t  (cost=0.42..8.44 rows=1 width=10.
.4) (actual rows=1 loops=30133)
               Index Cond: (ticket_no = tf.ticket_no)
 Planning Time: 30.136 ms
 Execution Time: 307.999 ms
(11 rows)

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

=> SET replan_enable = true;
SET
=> SET replan_overrun_limit = 2;
SET
=> SET replan_show_signature = true;
SET

Включим автоматическое одобрение планов:

=> SET pgpro_multiplan.aqe_mode = 'auto_approve_plans';
SET

Префикс aqe_ в именах параметров отражает тот факт, что с 17-й версии Postgres Pro Enterprise перепланирование запросов называется AQE (adaptive query execution).

=> SELECT pgpro_multiplan_reset();
 pgpro_multiplan_reset
-----------------------
                     5
(1 row)

Выполним запрос еще раз:

Page 25: Page 25

=> EXPLAIN (analyze, timing off)
SELECT *
FROM ticket_flights tf
  JOIN tickets t ON t.ticket_no = tf.ticket_no
WHERE tf.fare_conditions = 'Business'
  AND length(tf.ticket_no) = 13
  AND tf.amount >  50000;
                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Gather  (cost=18711.32..30054.00 rows=30133 width=136) (actual rows=30133 loops=1)
   NodeSign: 9024223846054589848
   Workers Planned: 2
   Workers Launched: 2
   ->  Parallel Hash Join  (cost=17711.32..26040.70 rows=12555 width=136) (actual rows=10.
.044 loops=3)
         NodeSign: 9024223846054589848
         Hash Cond: (t.ticket_no = tf.ticket_no)
         ->  Parallel Seq Scan on tickets t  (cost=0.00..7704.05 rows=152805 width=104) (.
.actual rows=122244 loops=3)
               NodeSign: 6333310549343027579
         ->  Parallel Hash  (cost=17554.38..17554.38 rows=12555 width=32) (actual rows=10.
.044 loops=3)
               NodeSign: 16504425124700661118
               Buckets: 32768  Batches: 1  Memory Usage: 2432kB
               ->  Parallel Seq Scan on ticket_flights tf  (cost=0.00..17554.38 rows=1255.
.5 width=32) (actual rows=10044 loops=3)
                     NodeSign: 16504425124700661118
                     Filter: ((amount > '50000'::numeric) AND ((fare_conditions)::text = .
.'Business'::text) AND (length(ticket_no) = 13))
                     Rows Removed by Filter: 338531
 Planning Time: 290.358 ms
 Execution Time: 333.601 ms
 Replan Active: true
 Table Entries: 2
 Controlled Statements: 1
 Replanning Attempts: 1
 Final Run Planning Time: 0.485 ms
 Total Time Elapsed: 623.959 ms
(24 rows)

Запрос был перепланирован в реальном времени (Replanning Attempts: 1). Обратите внимание, что оценки кардинальности улучшились и план изменился. Заглянем в хранилище планов:

=> SELECT sql_hash, const_hash, plan_hash, cost, sample_string, plan_type, hintstr
FROM pgpro_multiplan_storage \gx
-[ RECORD 1 ]-+--------------------------------------------------------------------------
sql_hash      | 3551150519806575491
const_hash    | 0
plan_hash     | 7463164036004167325
cost          | 30054.003749999996
sample_string | SELECT *                                                                 +
              | FROM ticket_flights tf                                                   +
              |   JOIN tickets t ON t.ticket_no = tf.ticket_no                           +
              | WHERE tf.fare_conditions = 'Business'                                    +
              |   AND length(tf.ticket_no) = 13                                          +
              |   AND tf.amount >  50000;
plan_type     | baseline
hintstr       | Set(search_path "bookings, public") Leading(("t" "tf" )) HashJoin("tf" "t.
              |.")  SeqScan("t") SeqScan("tf")

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

=> RESET replan_enable;
RESET

Отключим автоматическое одобрение и разрешим использование сохраненных базовых планов:

=> RESET pgpro_multiplan.aqe_mode;
RESET
=> SET pgpro_multiplan.mode = 'baseline';

Page 26: Page 26

SET

При следующих выполнениях запроса время планирования заметно сократится — используется сохраненный план:

=> EXPLAIN (analyze, timing off, costs off)
SELECT *
FROM ticket_flights tf
  JOIN tickets t ON t.ticket_no = tf.ticket_no
WHERE tf.fare_conditions = 'Business'
  AND length(tf.ticket_no) = 13
  AND tf.amount >  50000;
                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Custom Scan (MultiplanScan) (actual rows=30133 loops=1)
   Plan is: baseline
   SQL hash: 3551150519806575491
   Const hash: 0
   Plan hash: 7463164036004167325
   ->  Gather (actual rows=30133 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Parallel Hash Join (actual rows=10044 loops=3)
               Hash Cond: (t.ticket_no = tf.ticket_no)
               ->  Parallel Seq Scan on tickets t (actual rows=122244 loops=3)
               ->  Parallel Hash (actual rows=10044 loops=3)
                     Buckets: 32768 (originally 1024)  Batches: 1 (originally 1)  Memory .
.Usage: 2648kB
                     ->  Parallel Seq Scan on ticket_flights tf (actual rows=10044 loops=.
.3)
                           Filter: ((amount > '50000'::numeric) AND ((fare_conditions)::t.
.ext = 'Business'::text) AND (length(ticket_no) = 13))
                           Rows Removed by Filter: 338531
 Planning Time: 0.545 ms
 Execution Time: 311.506 ms
(18 rows)

Выполним теперь запрос с другим значением константы:

=> EXPLAIN (analyze, timing off, costs off)
SELECT *
FROM ticket_flights tf
  JOIN tickets t ON t.ticket_no = tf.ticket_no
WHERE tf.fare_conditions = 'Business'
  AND length(tf.ticket_no) = 13
  AND tf.amount >  10000;
                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Custom Scan (MultiplanScan) (actual rows=102038 loops=1)
   Plan is: baseline
   SQL hash: 3551150519806575491
   Const hash: 0
   Plan hash: 7463164036004167325
   ->  Gather (actual rows=102038 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Parallel Hash Join (actual rows=34013 loops=3)
               Hash Cond: (t.ticket_no = tf.ticket_no)
               ->  Parallel Seq Scan on tickets t (actual rows=122244 loops=3)
               ->  Parallel Hash (actual rows=34013 loops=3)
                     Buckets: 131072 (originally 1024)  Batches: 1 (originally 1)  Memory.
. Usage: 9304kB
                     ->  Parallel Seq Scan on ticket_flights tf (actual rows=34013 loops=.
.3)
                           Filter: ((amount > '10000'::numeric) AND ((fare_conditions)::t.
.ext = 'Business'::text) AND (length(ticket_no) = 13))
                           Rows Removed by Filter: 314563
 Planning Time: 0.867 ms
 Execution Time: 359.372 ms
(18 rows)

Сервер использует тот же базовый план.

Page 27: Итоги

Расширение pgpro\_multiplan позволяет стабилизировать выполнение запросов, сохраняя план или набор указаний оптимизатору

  • Поддерживаются несколько вариантов сопоставления запросов с планами
  • Дополнительное хранилище планов удобно для отладки
  • Расширение интегрировано с перепланированием запросов

Page 28: Практика

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

Комментарии преподавателя

  1. Используйте запрос из темы «Подходы к настройке» курса QPT-16:
SELECT count(*)
FROM flights f
  JOIN ticket_flights tf ON tf.flight_id = f.flight_id
  JOIN boarding_passes bp ON
       bp.flight_id = tf.flight_id AND bp.ticket_no = tf.ticket_no
WHERE f.actual_departure — f.scheduled_departure BETWEEN
       interval '1 min' AND interval '4 hours'
  AND f.actual_arrival — f.scheduled_arrival BETWEEN
       interval '1 min' AND interval '4 hours';
  1. Вспомните тему «Перепланирование запросов» этого курса.

Page 29: 1. Неоптимизированный запрос

Подключимся к базе данных demo и получим план выполнения запроса:

=> \c demo
You are now connected to database "demo" as user "student".
=> \pset format wrapped
Output format is wrapped.
=> \pset columns 90
Target width is 90.
=> EXPLAIN (analyze, buffers, timing off)
SELECT count(*)
FROM flights f
  JOIN ticket_flights tf ON tf.flight_id = f.flight_id
  JOIN boarding_passes bp ON bp.flight_id = tf.flight_id AND bp.ticket_no = tf.ticket_no
WHERE f.actual_departure - f.scheduled_departure BETWEEN interval '1 min' AND interval '4 hours'
  AND f.actual_arrival - f.scheduled_arrival BETWEEN interval '1 min' AND interval '4 hours';
                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Aggregate  (cost=1635.64..1635.65 rows=1 width=8) (actual rows=1 loops=1)
   Buffers: shared hit=1562335 read=10013
   ->  Nested Loop  (cost=5.35..1635.59 rows=18 width=0) (actual rows=504286 loops=1)
         Join Filter: (f.flight_id = tf.flight_id)
         Buffers: shared hit=1562335 read=10013
         ->  Nested Loop  (cost=4.92..1626.71 rows=18 width=22) (actual rows=504286 loops.
.=1)
               Buffers: shared hit=53304 read=6185
               ->  Seq Scan on flights f  (cost=0.00..1387.63 rows=1 width=4) (actual row.
.s=14677 loops=1)
                     Filter: (((actual_departure - scheduled_departure) >= '00:01:00'::in.
.terval) AND ((actual_departure - scheduled_departure) <= '04:00:00'::interval) AND ((act.
.ual_arrival - scheduled_arrival) >= '00:01:00'::interval) AND ((actual_arrival - schedul.
.ed_arrival) <= '04:00:00'::interval))
                     Rows Removed by Filter: 18444
                     Buffers: shared read=394
               ->  Bitmap Heap Scan on boarding_passes bp  (cost=4.92..238.44 rows=64 wid.
.th=18) (actual rows=34 loops=14677)
                     Recheck Cond: (f.flight_id = flight_id)
                     Heap Blocks: exact=13714
                     Buffers: shared hit=53304 read=5791
                     ->  Bitmap Index Scan on boarding_passes_flight_id_seat_no_key  (cos.
.t=0.00..4.91 rows=64 width=0) (actual rows=34 loops=14677)
                           Index Cond: (flight_id = f.flight_id)
                           Buffers: shared hit=43802 read=1579
         ->  Index Only Scan using ticket_flights_pkey on ticket_flights tf  (cost=0.42...
.0.48 rows=1 width=18) (actual rows=1 loops=504286)
               Index Cond: ((ticket_no = bp.ticket_no) AND (flight_id = bp.flight_id))
               Heap Fetches: 0
               Buffers: shared hit=1509031 read=3828
 Planning:
   Buffers: shared hit=383 read=70
 Planning Time: 1536.847 ms
 Execution Time: 3872.919 ms
(26 rows)

В плане заметны ошибка оценки кардинальности в узле Seq Scan on flights f и большое число прочитанных страниц.

2. Перепланирование и базовый план в песочнице

Добавим библиотеки в shared_preload_libraries:

=> ALTER SYSTEM SET shared_preload_libraries = pg_hint_plan, pgpro_multiplan;
ALTER SYSTEM

Перезапустим экземпляр:

student$ sudo systemctl restart postgrespro-ent-16.service

Page 30: Page 30

student$ psql demo

Добавим расширение pgpro_multiplan в базу данных demo:

=> CREATE EXTENSION pgpro_multiplan;
CREATE EXTENSION

Используем автоматическое перепланирование, которое будет срабатывать при ошибке кардинальности более, чем в два раза.

=> SET replan_enable = true;
SET
=> SET replan_overrun_limit = 2;
SET
=> SET replan_show_signature = true;
SET

Планы должны захватываться и одобряться автоматически.

=> SET pgpro_multiplan.mode = 'baseline';
SET
=> SET pgpro_multiplan.aqe_mode = 'auto_approve_plans';
SET

Включим режим песочницы:

=> SET pgpro_multiplan.sandbox = true;
SET

Выполним запрос:

=> EXPLAIN (analyze, timing off)
SELECT count(*)
FROM flights f
  JOIN ticket_flights tf ON tf.flight_id = f.flight_id
  JOIN boarding_passes bp ON bp.flight_id = tf.flight_id AND bp.ticket_no = tf.ticket_no
WHERE f.actual_departure - f.scheduled_departure BETWEEN interval '1 min' AND interval '4 hours'
  AND f.actual_arrival - f.scheduled_arrival BETWEEN interval '1 min' AND interval '4 hours';

Page 31: Page 31

                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Finalize Aggregate  (cost=10919.90..10919.91 rows=1 width=8) (actual rows=1 loops=1)
   NodeSign: 277254305835914273
   ->  Gather  (cost=10919.69..10919.90 rows=2 width=8) (actual rows=3 loops=1)
         NodeSign: not supported
         Workers Planned: 2
         Workers Launched: 2
         ->  Partial Aggregate  (cost=9919.69..9919.70 rows=1 width=8) (actual rows=1 loo.
.ps=3)
               NodeSign: not supported
               ->  Nested Loop  (cost=1571.52..9914.91 rows=1909 width=0) (actual rows=16.
.8095 loops=3)
                     NodeSign: 16877879010983805578
                     Join Filter: (f.flight_id = tf.flight_id)
                     ->  Hash Join  (cost=1571.09..8972.56 rows=1909 width=22) (actual ro.
.ws=168095 loops=3)
                           NodeSign: 5919140159923417660
                           Hash Cond: (bp.flight_id = f.flight_id)
                           ->  Parallel Seq Scan on boarding_passes bp  (cost=0.00..6767..
.36 rows=241536 width=18) (actual rows=193229 loops=3)
                                 NodeSign: 11059471175340681660
                           ->  Hash  (cost=1387.63..1387.63 rows=14677 width=4) (actual r.
.ows=14677 loops=3)
                                 NodeSign: 10237082780770536679
                                 Buckets: 16384  Batches: 1  Memory Usage: 644kB
                                 ->  Seq Scan on flights f  (cost=0.00..1387.63 rows=1467.
.7 width=4) (actual rows=14677 loops=3)
                                       NodeSign: 10237082780770536679
                                       Filter: (((actual_departure - scheduled_departure).
. >= '00:01:00'::interval) AND ((actual_departure - scheduled_departure) <= '04:00:00'::i.
.nterval) AND ((actual_arrival - scheduled_arrival) >= '00:01:00'::interval) AND ((actual.
._arrival - scheduled_arrival) <= '04:00:00'::interval))
                                       Rows Removed by Filter: 18444
                     ->  Index Only Scan using ticket_flights_pkey on ticket_flights tf  .
.(cost=0.42..0.48 rows=1 width=18) (actual rows=1 loops=504286)
                           NodeSign: 17341206759017999067
                           Index Cond: ((ticket_no = bp.ticket_no) AND (flight_id = bp.fl.
.ight_id))
                           Heap Fetches: 0
 Planning Time: 74.175 ms
 Execution Time: 2322.304 ms
 Replan Active: true
 Table Entries: 3
 Controlled Statements: 1
 Replanning Attempts: 2
 Replan Deactivation Reason: repeated plan
 Final Run Planning Time: 0.814 ms
 Total Time Elapsed: 2396.480 ms
(36 rows)

Сработало перепланирование, время выполнения снизилось. План попал в хранилище:

=> SELECT sql_hash, const_hash, plan_hash, cost, sample_string, plan_type, hintstr
FROM pgpro_multiplan_storage \gx
-[ RECORD 1 ]-+--------------------------------------------------------------------------
sql_hash      | -8124575748975674992
const_hash    | 0
plan_hash     | 8234005806038340721
cost          | 10919.910869265286
sample_string | SELECT count(*)                                                          +
              | FROM flights f                                                           +
              |   JOIN ticket_flights tf ON tf.flight_id = f.flight_id                   +
              |   JOIN boarding_passes bp ON bp.flight_id = tf.flight_id AND bp.ticket_no.
              |. = tf.ticket_no                                                          +
              | WHERE f.actual_departure - f.scheduled_departure BETWEEN interval '1 min'.
              |. AND interval '4 hours'                                                  +
              |   AND f.actual_arrival - f.scheduled_arrival BETWEEN interval '1 min' AND.
              |. interval '4 hours';
plan_type     | baseline
hintstr       | Set(search_path "bookings, public") Leading((("bp" "f" )"tf" )) HashJoin(.
              |."f" "bp") NestLoop("f" "tf" "bp")  SeqScan("bp") IndexOnlyScan("tf" "tick.
              |.et_flights_pkey") SeqScan("f")

3. Проверка одобренного плана

Page 32: Page 32

Отключим перепланирование:

=> RESET replan_enable;
RESET
=> RESET replan_overrun_limit;
RESET
=> RESET replan_show_signature;
RESET

Отключим автоматическое одобрение:

=> RESET pgpro_multiplan.aqe_mode;
RESET

Для нашего запроса используется одобренный план:

=> EXPLAIN (analyze, timing off)
SELECT count(*)
FROM flights f
  JOIN ticket_flights tf ON tf.flight_id = f.flight_id
  JOIN boarding_passes bp ON bp.flight_id = tf.flight_id AND bp.ticket_no = tf.ticket_no
WHERE f.actual_departure - f.scheduled_departure BETWEEN interval '1 min' AND interval '4 hours'
  AND f.actual_arrival - f.scheduled_arrival BETWEEN interval '1 min' AND interval '4 hours';
                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Custom Scan (MultiplanScan)  (cost=10065.01..10065.02 rows=1 width=8) (actual rows=1 loo.
.ps=1)
   Plan is: baseline
   SQL hash: -8124575748975674992
   Const hash: 0
   Plan hash: 8234005806038340721
   ->  Finalize Aggregate  (cost=10065.01..10065.02 rows=1 width=8) (actual rows=1 loops=.
.1)
         ->  Gather  (cost=10064.80..10065.01 rows=2 width=8) (actual rows=3 loops=1)
               Workers Planned: 2
               Workers Launched: 2
               ->  Partial Aggregate  (cost=9064.80..9064.81 rows=1 width=8) (actual rows.
.=1 loops=3)
                     ->  Nested Loop  (cost=1388.07..9064.78 rows=8 width=0) (actual rows.
.=168095 loops=3)
                           Join Filter: (f.flight_id = tf.flight_id)
                           ->  Hash Join  (cost=1387.64..9060.83 rows=8 width=22) (actual.
. rows=168095 loops=3)
                                 Hash Cond: (bp.flight_id = f.flight_id)
                                 ->  Parallel Seq Scan on boarding_passes bp  (cost=0.00..
..6767.36 rows=241536 width=18) (actual rows=193229 loops=3)
                                 ->  Hash  (cost=1387.63..1387.63 rows=1 width=4) (actual.
. rows=14677 loops=3)
                                       Buckets: 16384 (originally 1024)  Batches: 1 (orig.
.inally 1)  Memory Usage: 644kB
                                       ->  Seq Scan on flights f  (cost=0.00..1387.63 row.
.s=1 width=4) (actual rows=14677 loops=3)
                                             Filter: (((actual_departure - scheduled_depa.
.rture) >= '00:01:00'::interval) AND ((actual_departure - scheduled_departure) <= '04:00:.
.00'::interval) AND ((actual_arrival - scheduled_arrival) >= '00:01:00'::interval) AND ((.
.actual_arrival - scheduled_arrival) <= '04:00:00'::interval))
                                             Rows Removed by Filter: 18444
                           ->  Index Only Scan using ticket_flights_pkey on ticket_flight.
.s tf  (cost=0.42..0.48 rows=1 width=18) (actual rows=1 loops=504286)
                                 Index Cond: ((ticket_no = bp.ticket_no) AND (flight_id =.
. bp.flight_id))
                                 Heap Fetches: 0
 Planning Time: 2.810 ms
 Execution Time: 2118.608 ms
(25 rows)

4. Проверка с выключенной песочницей

Отключим песочницу:

=> RESET pgpro_multiplan.sandbox;
RESET

Page 33: Page 33

Убедимся, что в хранилище плана нет:

=> SELECT sql_hash, const_hash, plan_hash, cost, sample_string, plan_type, hintstr
FROM pgpro_multiplan_storage \gx
(0 rows)

Повторим запрос:

=> EXPLAIN (analyze, timing off)
SELECT count(*)
FROM flights f
  JOIN ticket_flights tf ON tf.flight_id = f.flight_id
  JOIN boarding_passes bp ON bp.flight_id = tf.flight_id AND bp.ticket_no = tf.ticket_no
WHERE f.actual_departure - f.scheduled_departure BETWEEN interval '1 min' AND interval '4 hours'
  AND f.actual_arrival - f.scheduled_arrival BETWEEN interval '1 min' AND interval '4 hours';
                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Aggregate  (cost=1635.64..1635.65 rows=1 width=8) (actual rows=1 loops=1)
   ->  Nested Loop  (cost=5.35..1635.59 rows=18 width=0) (actual rows=504286 loops=1)
         Join Filter: (f.flight_id = tf.flight_id)
         ->  Nested Loop  (cost=4.92..1626.71 rows=18 width=22) (actual rows=504286 loops.
.=1)
               ->  Seq Scan on flights f  (cost=0.00..1387.63 rows=1 width=4) (actual row.
.s=14677 loops=1)
                     Filter: (((actual_departure - scheduled_departure) >= '00:01:00'::in.
.terval) AND ((actual_departure - scheduled_departure) <= '04:00:00'::interval) AND ((act.
.ual_arrival - scheduled_arrival) >= '00:01:00'::interval) AND ((actual_arrival - schedul.
.ed_arrival) <= '04:00:00'::interval))
                     Rows Removed by Filter: 18444
               ->  Bitmap Heap Scan on boarding_passes bp  (cost=4.92..238.44 rows=64 wid.
.th=18) (actual rows=34 loops=14677)
                     Recheck Cond: (f.flight_id = flight_id)
                     Heap Blocks: exact=13714
                     ->  Bitmap Index Scan on boarding_passes_flight_id_seat_no_key  (cos.
.t=0.00..4.91 rows=64 width=0) (actual rows=34 loops=14677)
                           Index Cond: (flight_id = f.flight_id)
         ->  Index Only Scan using ticket_flights_pkey on ticket_flights tf  (cost=0.42...
.0.48 rows=1 width=18) (actual rows=1 loops=504286)
               Index Cond: ((ticket_no = bp.ticket_no) AND (flight_id = bp.flight_id))
               Heap Fetches: 0
 Planning Time: 0.894 ms
 Execution Time: 2899.823 ms
(17 rows)

Сервер вернулся к исходному плану.

Уборка

Удалим расширение, созданное в базе demo и очистим список загружаемых библиотек.

=> DROP EXTENSION pgpro_multiplan;
DROP EXTENSION
=> ALTER SYSTEM RESET shared_preload_libraries;
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service

12 Занятие 12: Производительность / Анализ производительности

Исходная страница в нативном Markdown: Производительность / Анализ производительности
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_12_perfanalysis.html Archive: PGPRO-handouts-16.zip Страниц PDF: 26

Page 1: Производительность / Анализ производительности

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Расширение pgpro_stats
  • Агрегированная статистика
  • Дополнительные метрики
  • Статистика очистки
  • Статистика аннулирования кеша
  • Трассировка сеансов

Page 3: Расширение pgpro_stats

Статистика запросов

  • затраченные ресурсы, планы и события ожиданий
  • Основные настройки
    • pgpro_stats.max = 5000
    • предел количества запросов
    • pgpro_stats.track = top
    • верхний уровень или все (all)
    • pgpro_stats.query_sample_rate = 1.0
    • размер выборки (доля запросов)
    • pgpro_stats.enable_profile = true
    • профиль ожиданий
    • pgpro_stats.profile_period = 10 ms
    • период семплирования
  • Представление pgpro_stats_statements

Комментарии преподавателя

Расширение pgpro_stats собирает статистику по выполняемым операторам. По сравнению с pg_stat_statements добавлен сбор данных о планах запросов и событиях ожидания для каждого типа запроса. Расширения pgpro_stats и pg_stat_statements несовместимы.

Статистика доступна через представление pgpro_stats_statements, максимальное число строк в нем определяет параметр pgpro_stats.max. Значение по умолчанию — 5000.

Параметр pgpro_stats.track определяет, будут отслеживаться лишь только непосредственно выполненные команды SQL (значение top — по умолчанию), или также и вложенные операторы (значение all).

Для снижения нагрузки можно уменьшить размер выборки, заданный параметром pgpro_stats.query_sample_rate. Тогда для вычисления статистики будет случайно выбираться заданная доля запросов.

Семплинг событий ожидания выполняется с периодичностью раз в pgpro_stats.profile_period единиц времени (10 мс по умолчанию), если установлен параметр pgpro_stats.enable_profile.

Статистика ожиданий доступна в поле pgpro_stats_statements.wait_stats типа jsonb. Все показатели представлены в миллисекундах и кратны параметру конфигурации pgpro_stats.profile_period.

Также собирается информация о затраченных ресурсах с точки зрения операционной системы с помощью вызова rusage:

https://postgrespro.ru/docs/enterprise/16/pgpro-stats#PGPRO-STATS-RUSA GE

Page 4: Агрегированная статистика

Статистика запросов, агрегированная по объектам

  • весь кластер, база данных, пользователь, адрес, приложение, процесс
  • Основные настройки
    • pgpro_stats.track_totals = on
    • pgpro_stats.track_cluster, pgpro_stats.track_databases и т. п.
    • pgpro_stats.max_totals = 1000
    • предел количества объектов
  • Представление pgpro_stats_totals

Комментарии преподавателя

Модуль pgpro_stats позволяет агрегировать статистику запросов по различным объектам:

● cluster — весь кластер баз данных;

● database — отдельная база данных;

● user — пользователь;

● client_addr — адрес клиента;

● application — приложение;

● backend — обслуживающий процесс;

● session — сеанс.

Необходимость агрегации по конкретному типу объекта настраивается параметрами pgpro_stats.track_cluster, pgpro_stats.track_databases и т. п.

Агрегированная статистика доступна в представлении pgpro_stats_totals. Каждая строка в этом представлении содержит значения по конкретному объекту. Количество объектов, по которым отслеживается статистика, ограничено параметром pgpro_stats.max_totals (по умолчанию — 1000). Когда количество объектов превышает заданное, информация о наименее активно используемых объектах удаляется.

Page 5: Сбор статистики по операторам и планам

student$ psql

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

=> ALTER SYSTEM SET shared_preload_libraries = 'pgpro_stats';
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service

Создадим тестовую базу данных на основе demo:

student$ psql
=> CREATE DATABASE pgpro_perfanalysis TEMPLATE demo;
CREATE DATABASE

Для удобства настроим путь поиска:

=> ALTER DATABASE pgpro_perfanalysis SET search_path TO bookings, public;
ALTER DATABASE

В схеме bookings копия стандартной демобазы минимального размера.

=> \c pgpro_perfanalysis
You are now connected to database "pgpro_perfanalysis" as user "student".
=> \dt
              List of relations
  Schema  |      Name       | Type  |  Owner
----------+-----------------+-------+---------
 bookings | aircrafts_data  | table | student
 bookings | airports_data   | table | student
 bookings | boarding_passes | table | student
 bookings | bookings        | table | student
 bookings | flights         | table | student
 bookings | seats           | table | student
 bookings | ticket_flights  | table | student
 bookings | tickets         | table | student
(8 rows)

Подключим расширение:

=> CREATE EXTENSION pgpro_stats;
CREATE EXTENSION

Выполним несколько запросов к таблице билетов tickets, предварительно добавив индекс по фамилиям пассажиров.

=> CREATE INDEX ticket_pname ON tickets(passenger_name);
CREATE INDEX

Пара запросов с параллельным последовательным сканированием таблицы.

=> SELECT count(book_ref) FROM tickets WHERE passenger_name < 'Z';
 count
--------
 363794
(1 row)
=> SELECT count(book_ref) FROM tickets WHERE passenger_name < 'Y';
 count
--------
 351813
(1 row)

Посмотрим, что накопилось в статистике операторов:

Page 6: Page 6

=> SELECT query, plan, calls
FROM pgpro_stats_statements
WHERE query LIKE 'SELECT count(book_ref) FROM tickets WHERE passenger_name%' \gx
-[ RECORD 1
]--------------------------------------------------------------------------------------
query | SELECT count(book_ref) FROM tickets WHERE passenger_name < $1
plan  | Finalize Aggregate
         +
      |   Output: count(book_ref)
         +
      |   ->  Gather
         +
      |         Output: (PARTIAL count(book_ref))
         +
      |         Workers Planned: 2
         +
      |         ->  Partial Aggregate
         +
      |               Output: PARTIAL count(book_ref)
         +
      |               ->  Parallel Seq Scan on bookings.tickets
         +
      |                     Output: ticket_no, book_ref, passenger_id, passenger_name,
contact_data+
      |                     Filter: (tickets.passenger_name < $1)
         +
      | Query Identifier: -7272530214084821635
         +
      |
calls | 2

Два оператора, отличающиеся константами, объединены в одну запись. Поле calls показывает количество выполнений. В следующих запросах используется индексный доступ:

=> SELECT count(book_ref) FROM tickets WHERE passenger_name > 'Z';
 count
-------
  2939
(1 row)
=> SELECT count(book_ref) FROM tickets WHERE passenger_name > 'Y';
 count
-------
 14920
(1 row)

Теперь у нас два разных плана.

=> SELECT query, plan, calls
FROM pgpro_stats_statements
WHERE query LIKE 'SELECT count(book_ref) FROM tickets WHERE passenger_name%' \gx

Page 7: Page 7

-[ RECORD 1
]--------------------------------------------------------------------------------------
query | SELECT count(book_ref) FROM tickets WHERE passenger_name < $1
plan  | Finalize Aggregate
         +
      |   Output: count(book_ref)
         +
      |   ->  Gather
         +
      |         Output: (PARTIAL count(book_ref))
         +
      |         Workers Planned: 2
         +
      |         ->  Partial Aggregate
         +
      |               Output: PARTIAL count(book_ref)
         +
      |               ->  Parallel Seq Scan on bookings.tickets
         +
      |                     Output: ticket_no, book_ref, passenger_id, passenger_name,
contact_data+
      |                     Filter: (tickets.passenger_name < $1)
         +
      | Query Identifier: -7272530214084821635
         +
      |
calls | 2
-[ RECORD 2
]--------------------------------------------------------------------------------------
query | SELECT count(book_ref) FROM tickets WHERE passenger_name > $1
plan  | Aggregate
         +
      |   Output: count(book_ref)
         +
      |   ->  Bitmap Heap Scan on bookings.tickets
         +
      |         Output: ticket_no, book_ref, passenger_id, passenger_name, contact_data
         +
      |         Recheck Cond: (tickets.passenger_name > $1)
         +
      |         ->  Bitmap Index Scan on ticket_pname
         +
      |               Index Cond: (tickets.passenger_name > $1)
         +
      | Query Identifier: -9112409754780737962
         +
      |
calls | 2

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

=> SELECT object_type, object_name, queries_executed, total_exec_time,
  (total_exec_rusage).reads, (total_exec_rusage).writes,
  (total_exec_rusage).user_time, (total_exec_rusage).system_time,
  jsonb_pretty(wait_stats) AS wait_stats
FROM pgpro_stats_totals
WHERE object_type = 'user'
  AND object_id = 'student'::regrole \gx

Page 8: Page 8

-[ RECORD 1 ]----+------------------------------------
object_type      | user
object_name      | student
queries_executed | 13
total_exec_time  | 10205.154014999996
reads            | 0
writes           | 661045248
user_time        | 1.666614
system_time      | 1.1153609999999998
wait_stats       | {                                  +
                 |     "IO": {                        +
                 |         "WALSync": 280,            +
                 |         "WALWrite": 60,            +
                 |         "BufFileWrite": 10,        +
                 |         "DataFileRead": 120,       +
                 |         "DataFileWrite": 190,      +
                 |         "DataFileExtend": 50,      +
                 |         "VersionFileSync": 10,     +
                 |         "RelationMapReplace": 30,  +
                 |         "DataFileImmediateSync": 30+
                 |     },                             +
                 |     "Total": {                     +
                 |         "IO": 780,                 +
                 |         "Total": 1020,             +
                 |         "LWLock": 240              +
                 |     },                             +
                 |     "LWLock": {                    +
                 |         "WALWrite": 240            +
                 |     }                              +
                 | }

В выводе этого запроса получены количество выполненных запросов, общее время выполнения запросов, а также несколько полей записи в столбце total_exec_rusage: reads — количество байтов, прочитанное из файловой системы; writes — количество байтов, записанное в файловую систему; user_time — время, потраченное на работу запроса в непривилегированном режиме (user space); system_time — время, потраченное на работу запроса на стороне ядра ОС (kernel space). Столбец total_exec_rusage составного типа pgpro_stats_rusage содержит статистику использования ресурсов на этапах планирования и выполнения запросов. Собранная статистика сбрасывается функцией pgpro_stats_statements_reset для заданного пользователя (userid), базы данных (dbid), запроса (queryid) или плана (planid). Если не указать ни один аргумент, будет очищена вся статистика:

=> SELECT pgpro_stats_statements_reset();
 pgpro_stats_statements_reset
-------------------------------
 2026-07-27 23:41:24.958237+03
(1 row)

Статистика по запросам удалена:

=> SELECT query, plan, calls
FROM pgpro_stats_statements
WHERE query LIKE 'SELECT count(*) FROM tickets WHERE passenger_name%' \gx
(0 rows)

Агрегированная статистика для заданного типа и идентификатора объекта очищается функцией pgpro_stats_totals_reset. Например, сбросим агрегатную статистику по роли student:

=> SELECT pgpro_stats_totals_reset('user', 'student'::regrole::bigint);
   pgpro_stats_totals_reset
-------------------------------
 2026-07-27 23:41:25.126849+03
(1 row)

Page 9: Page 9

=> SELECT object_type, object_name, queries_executed, total_exec_time,
  (total_exec_rusage).reads, (total_exec_rusage).writes,
  (total_exec_rusage).user_time, (total_exec_rusage).system_time,
  jsonb_pretty(wait_stats) AS wait_stats
FROM pgpro_stats_totals
WHERE object_type = 'user'
  AND object_id = 'student'::regrole \gx
-[ RECORD 1 ]----+-----------------------
object_type      | user
object_name      | student
queries_executed | 1
total_exec_time  | 0.010332999999999998
reads            | 0
writes           | 0
user_time        | 1.7000000000044757e-05
system_time      | 0
wait_stats       |

После сброса статистики был только что выполнен единственный запрос. Поэтому queries_executed равно единице. Статистика по базе данных сохранилась, так как она не была стерта:

=> SELECT object_type, object_name, queries_executed, total_exec_time,
  (total_exec_rusage).reads, (total_exec_rusage).writes,
  (total_exec_rusage).user_time, (total_exec_rusage).system_time,
  jsonb_pretty(wait_stats) AS wait_stats
FROM pgpro_stats_totals
WHERE object_type = 'database'
  AND object_id = (SELECT oid FROM pg_database WHERE datname = 'pgpro_perfanalysis') \gx
-[ RECORD 1 ]----+------------------------------------
object_type      | database
object_name      | pgpro_perfanalysis
queries_executed | 15
total_exec_time  | 1042.906253
reads            | 0
writes           | 31039488
user_time        | 0.774153
system_time      | 0.13438300000000003
wait_stats       | {                                  +
                 |     "IO": {                        +
                 |         "BufFileWrite": 10,        +
                 |         "DataFileRead": 60,        +
                 |         "DataFileWrite": 20,       +
                 |         "DataFileExtend": 10,      +
                 |         "DataFileImmediateSync": 30+
                 |     },                             +
                 |     "Total": {                     +
                 |         "IO": 130,                 +
                 |         "Total": 130               +
                 |     }                              +
                 | }

Page 10: Дополнительные метрики

Произвольные метрики на основе SQL-запросов

  • собираются фоновыми рабочими процессами
  • кольцевой буфер
  • Основные настройки
    • pgpro_stats.metrics_buffer_size = 16kB
    • pgpro_stats.metrics_workers = 2
    • pgpro_stats.metric_N_name
    • имя метрики
    • pgpro_stats.metric_N_query
    • SQL-запрос
    • pgpro_stats.metric_N_period = 1 min
    • интервал сбора
    • pgpro_stats.metric_N_db
    • список баз данных
    • pgpro_stats.metric_N_user
    • пользователь
  • Представление pg_stats_metrics

Комментарии преподавателя

Используя pgpro_stats, можно определить дополнительные метрики. Собираемые статистические данные будут накапливаться в кольцевом буфере (представление pgpro_stats_metrics) для последующей передачи их в систему мониторинга.

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

Для определения дополнительной метрики в конфигурацию СУБД необходимо поместить соответствующие параметры. Номер N в имени параметра служит уникальным идентификатором метрики, к которой будет применяться задаваемое значение; он должен быть положительным и уникальным среди всех метрик. Параметры:

● pgpro_stats.metric_N_name — имя метрики;

● pgpro_stats.metric_N_query — запрос для получения метрики;

● pgpro_stats.metric_N_period — периодичность измерений;

● pgpro_stats.metric_N_db — список баз данных;

● pgpro_stats.metric_N_user — пользователь, от имени которого будет собираться метрика. Пользователь должен иметь доступ к базе данных, в которой будет выполняться запрос для сбора метрики.

Обсуждение того, какие именно метрики имеет смысл собирать, выходит за рамки курса, но порекомендуем книгу Алексея Лесовского «Мониторинг PostgreSQL»: https://postgrespro.ru/education/books/monitoring

Page 11: Сбор дополнительных метрик

Предположим, что требуется раз в 30 секунд собирать количество выполнений контрольной точки по требованию. Запишем параметры в файл conf.d/metrics.conf в каталоге данных кластера и перезапустим СУБД.

/var/lib/pgpro/ent-16/conf.d/metrics.conf
pgpro_stats.metric_1_name = 'bgwriter'
pgpro_stats.metric_1_query = 'SELECT * FROM pg_stat_bgwriter'
pgpro_stats.metric_1_db = 'pgpro_perfanalysis'
pgpro_stats.metric_1_user = 'student'
pgpro_stats.metric_1_period = '30s'

Добавленная метрика — первая, поэтому N=1.

student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql -p 5432 -d pgpro_perfanalysis

Заглянем в представление для дополнительных метрик:

=> SELECT metric_number, db_name, ts, value #> '{0,checkpoints_req}' AS checkpoints_req
FROM pgpro_stats_metrics;
 metric_number | db_name | ts | checkpoints_req
---------------+---------+----+-----------------
(0 rows)

Столбец value содержит результат запроса, вычисляющего метрику, в виде массива объектов jsonb. Здесь извлечен первый по порядку (нулевой) элемент, в котором по ключу checkpoints_req получено искомое значение. Поменяем сразу много строк, например, фамилии пассажиров во всех купленных авиабилетах:

=> UPDATE tickets SET passenger_name = initcap(passenger_name);
UPDATE 366733

Выполним контрольную точку и немного подождем.

=> CHECKPOINT;
CHECKPOINT

В представлении pgpro_stats_metrics появится одна или несколько записей. Обратите внимание на значение checkpoints_req:

=> SELECT metric_number, db_name, ts, value #> '{0,checkpoints_req}' AS checkpoints_req
FROM pgpro_stats_metrics;
 metric_number |      db_name       |              ts               | checkpoints_req
---------------+--------------------+-------------------------------+-----------------
             1 | pgpro_perfanalysis | 2026-07-27 23:41:32.319727+03 | 3
             1 | pgpro_perfanalysis | 2026-07-27 23:42:02.091546+03 | 4
(2 rows)

Удалим настройки дополнительных метрик:

postgres$ rm /var/lib/pgpro/ent-16/conf.d/metrics.conf

Снова перезагрузим сервер.

student$ sudo systemctl restart postgrespro-ent-16.service

Page 12: Статистика очистки

Собирается ядром Postgres Pro Enterprise

  • Представления
    • pg_stats_vacuum_database — очистка баз данных
    • pg_stats_vacuum_tables — очистка таблиц текущей базы
    • pg_stats_vacuum_indexes — очистка индексов текущей базы

Комментарии преподавателя

Представления pg_stats_vacuum_database, pg_stats_vacuum_tables и pg_stats_vacuum_indexes содержат статистику очистки, соответственно, баз данных, таблиц в текущей базе данных и индексов в ней. Все эти представления основаны на одноименных функциях и доступны без установки расширения pgpro_stats.

Данные собираются ядром Postgres Pro Enterprise: стандартный сборщик статистики расширен для получения этой дополнительной информации.

Отслеживается подробная информация на уровне страниц (в количественном и временнóм представлении) и журнальных записей.

Page 13: Статистика очистки

student$ psql -p 5432 -d pgpro_perfanalysis

Выключим автоочистку для таблицы tickets:

=> ALTER TABLE tickets SET (autovacuum_enabled = off);
ALTER TABLE

Поскольку статистика по очистке собирается ядром СУБД, для ее сброса используется стандартная функция:

=> SELECT pg_stat_reset();
 pg_stat_reset
---------------
(1 row)

Статистика очистки для таблицы tickets отсутствует:

=> SELECT * FROM pg_stats_vacuum_tables(
  (SELECT oid FROM pg_database WHERE datname = 'pgpro_perfanalysis'),
  'tickets'::regclass
) \gx
-[ RECORD 1 ]---------+-
relid                 |
total_blks_read       |
total_blks_hit        |
total_blks_dirtied    |
total_blks_written    |
rel_blks_read         |
rel_blks_hit          |
pages_scanned         |
pages_removed         |
pages_frozen          |
pages_all_visible     |
tuples_deleted        |
tuples_frozen         |
dead_tuples           |
index_vacuum_count    |
rev_all_frozen_pages  |
rev_all_visible_pages |
wal_records           |
wal_fpi               |
wal_bytes             |
blk_read_time         |
blk_write_time        |
delay_time            |
system_time           |
user_time             |
total_time            |
interrupts            |

Вернем имена пассажиров к верхнему регистру и выполним очистку.

=> UPDATE tickets SET passenger_name = upper(passenger_name);
UPDATE 366733
=> VACUUM tickets;
VACUUM

Немного ожидания… Снова проверим статистику очистки для таблицы tickets. Теперь статистика очистки собрана:

=> SELECT * FROM pg_stats_vacuum_tables(
  (SELECT oid FROM pg_database WHERE datname = 'pgpro_perfanalysis'),
  'tickets'::regclass
) \gx

Page 14: Page 14

-[ RECORD 1 ]---------+-------------------
relid                 | 16443
total_blks_read       | 261
total_blks_hit        | 31665
total_blks_dirtied    | 262
total_blks_written    | 228
rel_blks_read         | 261
rel_blks_hit          | 31648
pages_scanned         | 61595
pages_removed         | 0
pages_frozen          | 16731
pages_all_visible     | 47071
tuples_deleted        | 1833665
tuples_frozen         | 0
dead_tuples           | 0
index_vacuum_count    | 3
rev_all_frozen_pages  | 12310
rev_all_visible_pages | 12310
wal_records           | 26846
wal_fpi               | 3
wal_bytes             | 3141770
blk_read_time         | 0
blk_write_time        | 0
delay_time            | 0
system_time           | 9.074000000000002
user_time             | 135.62900000000002
total_time            | 222.19
interrupts            | 0

Вернем автоочистку для таблицы tickets:

=> ALTER TABLE tickets SET (autovacuum_enabled = on);
ALTER TABLE
=> \q

Page 15: Аннулирование кешей

Сообщения об аннулировании метаданных измененных объектов в локальных кешах процессов

  • Представления
    • pgpro_stats_inval_status — состояние очереди сообщений
    • pgpro_stats_statements
    • pgpro_stats_totals
    • счетчики сообщений для операторов

Комментарии преподавателя

Обслуживающие процессы кешируют метаданные объектов в своих локальных кешах. Когда процесс меняет метаданные объекта, нужно обновить информацию в кешах других обслуживающих процессов. Для этого процесс отправляет другим процессам сообщение аннулирования (Cache Invalidation), которое помещается в специальную очередь.

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

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

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

Представление pgpro_stats_inval_status содержит одну строку с текущим состоянием очереди аннулирования кеша. Представления pgpro_stats_statements и pgpro_stats_totals для каждого объекта показывают запись со счетчиками сообщений аннулирования кеша.

https://postgrespro.ru/docs/enterprise/16/pgpro-stats#PGPRO-STATS-CAC HE-INVALIDATION-METRICS

Page 16: Аннулирование кешей

Откроем новый сеанс и получим PID обслуживающего процесса:

student$ psql -d pgpro_perfanalysis
=> SELECT pg_backend_pid();
 pg_backend_pid
----------------
         824559
(1 row)

Пока ни одного сообщения об аннулировании кеша нет:

=> SELECT object_type, (inval_msgs).total
FROM pgpro_stats_totals
WHERE object_type = 'backend'
  AND object_id = 824559;
 object_type | total
-------------+-------
 backend     |     0
(1 row)

Изменим, например, параметр хранения таблицы.

=> ALTER TABLE seats SET (autovacuum_enabled = on);
ALTER TABLE

Сообщения об аннулировании кеша, сгенерированные текущим обслуживающим процессом:

=> SELECT object_type,
  (inval_msgs).total,
  (inval_msgs).catcache,
  (inval_msgs).relcache
FROM pgpro_stats_totals
    WHERE object_type = 'backend'
      AND object_id = 824559;
 object_type | total | catcache | relcache
-------------+-------+----------+----------
 backend     |     3 |        2 |        1
(1 row)

Здесь: total — общее число сообщений аннулирования; catcache — число сообщений избирательного аннулирования кеша каталога; relcache — число сообщений избирательного аннулирования кеша отношений.

=> \q

Page 17: Трассировка сеансов

Трассировка сеансов в журнал сообщений или файл

  • Трассировка выполняется только для команд, удовлетворяющих критериям фильтрации
    • pgpro_stats_trace_insert
    • создает фильтр
    • pgpro_stats_trace_update — изменяет фильтр
    • pgpro_stats_trace_delete — удаляет фильтр
    • pgpro_stats_trace_reset
    • удаляет все фильтры
    • pgpro_stats_trace_show
    • показывает фильтры

Комментарии преподавателя

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

Для управления фильтрами расширение pgpro_stats предоставляет функции, показанные на слайде.

Изменения фильтров сразу влияют на трассировку во всех сеансах экземпляра СУБД.

Условия фильтров включают:

● идентифицирующие поля — проверяются на равенство;

● ресурсные поля — проверяются на превышение порога;

● поля, определяющие параметры EXPLAIN.

Например, database_name — идентифицирующее поле: сеанс, в котором выполнена команда, должен быть в указанной базе данных. Если же в фильтре указано ресурсное поле duration, то команда должна выполняться более указанной длительности.

Подробности об условиях фильтров в документации: https://postgrespro.ru/docs/enterprise/16/pgpro-stats#PGPRO-STATS-TRAC E-SHOW-OUTPUT

Page 18: Трассировка сеансов

student$ psql -d pgpro_perfanalysis

Откроем второй сеанс и получим PID обслуживающего процесса.

student$ psql -d pgpro_perfanalysis
=> SELECT pg_backend_pid();
 pg_backend_pid
----------------
         824863
(1 row)

Добавим фильтр pa1, который будет записывать в файл трассировки pa1.trace вывод EXPLAIN ANALYZE для каждой команды, выполняющейся во втором сеансе:

=> SELECT pgpro_stats_trace_insert(
  'alias', 'pa1',
  'database_name', current_database(),
  'pid', 824863,
  'explain_analyze', true,
  'tracefile', 'pa1'
);
 pgpro_stats_trace_insert
--------------------------
                        1
(1 row)

Проверим получившийся фильтр трассировки:

=> SELECT * from pgpro_stats_trace_show() \gx

Page 19: Page 19

-[ RECORD 1 ]-------+-------------------
filter_id           | 1
active              | t
alias               | pa1
tracefile           | pa1
pid                 | 824863
database_name       | pgpro_perfanalysis
client_addr         |
application_name    |
username            |
queryid             |
planid              |
duration            |
plan_time           |
exec_time           |
user_time           |
system_time         |
rows                |
shared_blks_hit     |
shared_blks_read    |
shared_blks_fetched |
shared_blks_dirtied |
shared_blks_written |
local_blks_hit      |
local_blks_read     |
local_blks_fetched  |
local_blks_dirtied  |
local_blks_written  |
temp_blks_read      |
temp_blks_written   |
wal_bytes           |
total_wait_time     |
total_inval_msgs    |
explain_analyze     | t
explain_verbose     |
explain_costs       |
explain_settings    |
explain_buffers     |
explain_wal         |
explain_timing      |
explain_format      |
time_info           | f

Во втором сеансе выполним запрос.

=> SELECT count(*) FROM tickets WHERE passenger_name > 'Z';
 count
-------
  2939
(1 row)

Закроем второй сеанс и проверим файл трассировки.

=> \q
postgres$ cat /var/lib/pgpro/ent-16/pg_stat/pa1.trace
Query Text: SELECT count(*) FROM tickets WHERE passenger_name > 'Z';
Aggregate  (cost=12.74..12.75 rows=1 width=8) (actual time=1.325..1.326 rows=1 loops=1)
  ->  Index Only Scan using ticket_pname on tickets  (cost=0.42..11.70 rows=416 width=0)
(actual time=0.040..1.064 rows=2939 loops=1)
        Index Cond: (passenger_name > 'Z'::text)
        Heap Fetches: 0

Удалим фильтр трассировки.

=> SELECT pgpro_stats_trace_delete(1);
 pgpro_stats_trace_delete
--------------------------
 t
(1 row)

Page 20: Итоги

Расширение pgpro\_stats позволяет выявлять и анализировать ресурсоемкие операции

  • Собирается статистика по затраченным ресурсам, планам и событиям ожиданий
  • Отдельно собирается агрегированная статистика
  • Можно использовать метрики, определенные пользователем
  • Имеется статистика по очистке и аннулированию кешей
  • Можно трассировать сеансы

Page 21: Практика

  1. Подготовьте СУБД для работы с расширением pgpro_stats. 2. Выполните запрос с двухпроходным хеш-соединением в условиях недостаточной памяти work_mem. 3. Получите отчет pgpro_stats. 4. Выполните этот же запрос в условиях достаточной памяти work_mem и снова получите отчет.

Комментарии преподавателя

  1. Подключите разделяемую библиотеку pgpro_stats, создайте базу данных — копию базы данных demo и подключите к ней расширение.

  2. Запустите транзакцию с установленными локальными параметрами work_mem = ‘12MB’ и hash_mem_multiplier = 1. В транзакции получите реальный план запроса SELECT * FROM bookings b JOIN tickets t ON b.book_ref = t.book_ref

  3. Получите отчет pgpro_stats с помощью представления pgpro_stats_statements, из которого необходимо вывести поля: query, plan, calls, temp_blks_read, temp_blks_written.

  4. Очистите статистику и второй раз запустите транзакцию с настройками work_mem = ‘48MB’ и hash_mem_multiplier = 3 и снова получите такой же отчет. Обратите внимание во втором отчете на отсутствие операций по работе с временными файлами.

Page 22: 1. Подготовка СУБД для работы с расширением pgpro_stats

student$ psql

Подключим разделяемую библиотеку:

=> ALTER SYSTEM SET shared_preload_libraries = pgpro_stats;
ALTER SYSTEM

Перезагрузим сервер.

student$ sudo systemctl restart postgrespro-ent-16.service

2. Запрос при недостатке памяти

Создадим тестовую базу данных на основе demo:

student$ psql
=> CREATE DATABASE pgpro_perfanalysis TEMPLATE demo;
CREATE DATABASE

Для удобства настроим путь поиска:

=> ALTER DATABASE pgpro_perfanalysis SET search_path TO bookings, public;
ALTER DATABASE

Подключимся к базе данных.

=> \c pgpro_perfanalysis
You are now connected to database "pgpro_perfanalysis" as user "student".

Подключим расширение:

=> CREATE EXTENSION pgpro_stats;
CREATE EXTENSION

Зададим размер памяти:

=> SET work_mem = '12MB';
SET
=> SET hash_mem_multiplier = 1;
SET

Выполняем запрос:

=> EXPLAIN (analyze, buffers, costs off, timing off, summary off)
SELECT * FROM bookings b
  JOIN tickets t ON b.book_ref = t.book_ref;
                            QUERY PLAN
-------------------------------------------------------------------
 Hash Join (actual rows=366733 loops=1)
   Hash Cond: (t.book_ref = b.book_ref)
   Buffers: shared hit=3760 read=4128, temp read=3491 written=3491
   ->  Seq Scan on tickets t (actual rows=366733 loops=1)
         Buffers: shared hit=2048 read=4128
   ->  Hash (actual rows=262788 loops=1)
         Buckets: 262144  Batches: 2  Memory Usage: 9071kB
         Buffers: shared hit=1712, temp written=690
         ->  Seq Scan on bookings b (actual rows=262788 loops=1)
               Buffers: shared hit=1712
 Planning:
   Buffers: shared hit=191 read=3
(12 rows)

3. Отчет по нагрузке

Соединение хешированием при недостатке оперативной памяти выполняется в двухпроходном режиме и использует

Page 23: Page 23

временные файлы. Проверим, что накопилось в статистике.

=> SELECT query, plan, calls, temp_blks_read, temp_blks_written
FROM pgpro_stats_statements
WHERE query LIKE 'EXPLAIN (analyze, buffers,%' \gx
-[ RECORD 1 ]-----+---------------------------------------------------------------
query             | EXPLAIN (analyze, buffers, costs off, timing off, summary off)+
                  | SELECT * FROM bookings b                                      +
                  |   JOIN tickets t ON b.book_ref = t.book_ref
plan              |
calls             | 1
temp_blks_read    | 3491
temp_blks_written | 3491

По столбцам temp_blks_read и temp_blks_written можно судить об активности использования временных файлов.

4. Отчет по нагрузке при достаточном объеме памяти

Сбросим статистику.

=> SELECT pgpro_stats_statements_reset();
 pgpro_stats_statements_reset
------------------------------
 2026-07-28 00:13:57.07154+03
(1 row)

Увеличим значения параметров:

=> SET work_mem = '48MB';
SET
=> SET hash_mem_multiplier = 3;
SET

Соединение хешированием станет однопроходным и не будет использовать временные файлы.

=> EXPLAIN (analyze, buffers, costs off, timing off, summary off)
SELECT * FROM bookings b
  JOIN tickets t ON b.book_ref = t.book_ref;
                           QUERY PLAN
-----------------------------------------------------------------
 Hash Join (actual rows=366733 loops=1)
   Hash Cond: (t.book_ref = b.book_ref)
   Buffers: shared hit=3792 read=4096
   ->  Seq Scan on tickets t (actual rows=366733 loops=1)
         Buffers: shared hit=2080 read=4096
   ->  Hash (actual rows=262788 loops=1)
         Buckets: 524288  Batches: 1  Memory Usage: 18190kB
         Buffers: shared hit=1712
         ->  Seq Scan on bookings b (actual rows=262788 loops=1)
               Buffers: shared hit=1712
 Planning:
   Buffers: shared hit=8
(12 rows)

Проверим, что накопилось в статистике.

=> SELECT query, plan, calls, temp_blks_read, temp_blks_written
FROM pgpro_stats_statements
WHERE query LIKE 'EXPLAIN (analyze, buffers,%' \gx
-[ RECORD 1 ]-----+---------------------------------------------------------------
query             | EXPLAIN (analyze, buffers, costs off, timing off, summary off)+
                  | SELECT * FROM bookings b                                      +
                  |   JOIN tickets t ON b.book_ref = t.book_ref
plan              |
calls             | 1
temp_blks_read    | 0
temp_blks_written | 0

Временные файлы не использовались.

Page 24: Уборка

Удалим созданную базу данных и очистим список загружаемых библиотек.

=> \c student
You are now connected to database "student" as user "student".
=> DROP DATABASE pgpro_perfanalysis WITH (FORCE);
DROP DATABASE
=> ALTER SYSTEM RESET shared_preload_libraries;
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service

Page 25: Практика PPEM

  1. В основном экземпляре создайте из шаблона demo базу данных stats_ppem и подготовьте ее для работы с pgpro_stats. 2. С помощью psql-консоли выполните запрос из оператора EXPLAIN п. 2 задания основной практики. 3. Модифицируйте запрос так, чтобы он ограничивал результаты определенным количеством строк. Выполните запрос. 4. В статистике, собранной расширением pgpro_stats, найдите оба запроса и сравните их представления и планы.

Комментарии преподавателя

  1. Для удобства дальнейшей работы подключитесь к базе как пользователь student с помощью метакоманды \c — student и включите в путь поиска схему bookings (или используйте запросы с квалифицированными именами таблиц): SELECT * FROM bookings.bookings AS b JOIN bookings.tickets AS t ON b.book_ref = t.book_ref;

  2. Дополненный запрос: SELECT * FROM bookings.bookings AS b JOIN bookings.tickets AS t ON b.book_ref = t.book_ref LIMIT 555;

  3. В разделе Мониторинг/SQL-статистика удобно поставить фильтры на базу данных и пользователя и отсортировать данные по количеству возвращенных строк.

Обратите внимание на представление константы в запросе из п. 3.

Page 26: 1. Создание базы из шаблона и установка расширения pgpro_stats

Выберите вкладку «Базы данных» и нажмите «Создать базу данных». При заполнении формы укажите шаблон — базу demo. На вкладке «Параметры» добавьте в shared_preload_libraries библиотеку pgpro_stats. Сохраните измененный параметр и перезапустите сервер. Далее на вкладке «Расширения» нажмите «Установить расширение», выберите базу, в списке расширений найдите pgpro_stats и установите его.

2,3. Выполнение запросов

В списке «Базы данных» найдите stats_ppem и в столбце «Действия» нажмите на пиктограмму «{/}» (открыть PSQL-консоль). Выполните запросы: SELECT * FROM bookings.bookings AS b JOIN bookings.tickets AS t ON b.book_ref = t.book_ref; SELECT * FROM bookings.bookings AS b JOIN bookings.tickets AS t ON b.book_ref = t.book_ref LIMIT 555;

4. Сохраненные планы запросов

На вкладке «SQL-статистика» отфильтруйте запросы для базы stats_ppem и пользователя student. Для просмотра плана запроса нажмите на значок «Раскрыть» в самом левом столбце таблицы. Запросы, различающиеся константами, объединяются расширением в одну запись, а сами эти константы заменяются обозначением параметра; в плане запроса п.3 константа 555 заменена на $1.

13 Занятие 13: Производительность / Отчеты по нагрузке pgpro_pwr

Исходная страница в нативном Markdown: Производительность / Отчеты по нагрузке pgpro_pwr
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_13_pwr.html Archive: PGPRO-handouts-16.zip Страниц PDF: 23

Page 1: Производительность / Отчеты по нагрузке pgpro_pwr

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Обзор pgpro_pwr
  • Архитектура pgpro_pwr
  • Установка pgpro_pwr
  • Выборки
  • Политика хранения
  • Выборочные линии
  • Размеры отношений
  • Отчеты

Page 3: Обзор pgpro_pwr

Расширение для выявления ресурсоемких операций в БД

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

Комментарии преподавателя

Расширение pgpro_pwr предназначено для выявления наиболее ресурсоемких операций в базе данных. PWR произносится как «пауэр» (power) — это сокращение от Postgres Pro Workload Reporting.

Расширение основано на pg_profile — расширении с открытым исходным кодом для PostgreSQL:

https://github.com/zubkov-andrei/pg\_profile

Предназначение:

● поиск запросов, потребляющих больше всего ресурсов СУБД;

● получение информации о типе и интенсивности нагрузки;

● исследование нагрузки на «горячие» объекты;

● сохранение и анализ нагрузки в историческом разрезе;

● сравнение профилей нагрузки, имевших место в разное время;

● облегчение расследования инцидентов;

● проведение анализа нагрузочного тестирования.

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

Документация:

https://postgrespro.ru/docs/enterprise/16/pgpro-pwr

Page 4: Архитектура pgpro_pwr

Source slide page 4: Архитектура pgpro_pwr{#source-figure-004 caption=“Source slide page 4: Архитектура pgpro_pwr”}

Статистика собирается с целевых серверов расширениями pgpro\_stats или pg\_stat\_statements

  • Выборки-снимки помещаются в репозиторий истории
  • По выборкам создаются отчеты об изменениях

Комментарии преподавателя

Исходные данные собираются с целевых серверов с помощью представлений сборщика статистики и расширения pgpro_stats или pg_stat_statements. Рекомендуется использовать расширение pgpro_stats, так как оно выдает планы операторов, информацию о событиях ожидания и нагрузке по отдельным базам данных, ролям, клиентам и приложениям. Сводная статистика ожиданий поступает от расширения pg_wait_sampling. Если нет возможности использовать pgpro_stats, можно использовать pg_stat_kcache как источник информации об использовании ресурсов процессора и о нагрузке на файловую систему (rusage).

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

Расширение pgpro_pwr состоит из следующих частей:

● репозиторий истории — хранилище собранных данных;

● механизм управления выборками — предоставляет функции для получения выборок и поддержки репозитория в актуальном состоянии посредством удаления устаревших данных;

● механизм отчетов — представляет функции построения отчетов по данным из репозитория;

● административные функции для создания серверов, линий выборок и для управления ими.

Page 5: Установка pgpro_pwr

Пакет pgpro\_pwr устанавливается средствами ОС

  • Упрощенная установка
    • pgpro_pwr непосредственно на целевом сервере,
    • отслеживание нагрузки от имени суперпользователя
  • Развернутая установка
    • нагрузка отслеживается на одном или нескольких удаленных серверах
    • от имени специально зарегистрированного пользователя

Комментарии преподавателя

Предполагается, что уже установлено расширение pgpro_stats или pg_stat_statements и настроен сборщик статистики Postgres Pro.

Само расширение pgpro_pwr собрано в отдельный установочный пакет и по умолчанию не устанавливается.

Упрощенная установка предназначена для случаев, когда pgpro_pwr устанавливается непосредственно на целевой сервер и будет отслеживать его нагрузку от имени суперпользователя. Она сводится к созданию отдельной схемы для объектов расширения и установке расширения. Так как pgpro_pwr зависит от расширения dblink, то либо в команду CREATE EXTENSION добавляют ключевое слово CASCADE, либо устанавливают dblink заранее.

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

Описание вариантов установки в документации: https://postgrespro.ru/docs/enterprise/16/pgpro-pwr#PGPRO-PWR-INSTALL ATION

Page 6: Установка расширения pgpro_pwr

Создадим тестовую базу данных на основе demo:

student$ psql
=> CREATE DATABASE pgpro_pwr TEMPLATE demo;
CREATE DATABASE

Для удобства настроим путь поиска:

=> ALTER DATABASE pgpro_pwr SET search_path TO bookings, public;
ALTER DATABASE

Пакет расширения pgpro_pwr уже установлен в виртуальной машине. Добавим загрузку библиотек вспомогательных расширений и зададим параметры сбора статистики.

=> ALTER SYSTEM SET shared_preload_libraries = pg_wait_sampling, pgpro_stats;
ALTER SYSTEM
=> ALTER SYSTEM SET track_io_timing = 'on';
ALTER SYSTEM
=> ALTER SYSTEM SET track_functions = 'all';
ALTER SYSTEM

Перезагрузим сервер.

student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql -p 5432 -d pgpro_pwr

Создадим схему для объектов расширения и добавим расширение в базу данных.

=> CREATE SCHEMA profile;
CREATE SCHEMA
=> CREATE EXTENSION pgpro_stats;
CREATE EXTENSION
=> CREATE EXTENSION pgpro_pwr SCHEMA profile CASCADE;
NOTICE:  installing required extension "dblink"
CREATE EXTENSION
=> CREATE EXTENSION pg_wait_sampling;
CREATE EXTENSION
=> \dx
                                      List of installed extensions
       Name       | Version |   Schema   |                         Description
------------------+---------+------------+------------------------------------------------
--------------
 dblink           | 1.2     | profile    | connect to other PostgreSQL databases from
within a database
 pg_wait_sampling | 1.1     | bookings   | sampling based statistics of wait events
 pgpro_pwr        | 4.13    | profile    | PostgreSQL load profile repository and report
builder
 pgpro_stats      | 1.10    | bookings   | track execution statistics of all SQL
statements executed
 plpgsql          | 1.0.1   | pg_catalog | PL/pgSQL procedural language
(5 rows)

Page 7: Выборки

Source slide page 7: Выборки{#source-figure-007 caption=“Source slide page 7: Выборки”}

Данные собираются с зарегистрированных серверов

  • Выборка содержит данные с момента предыдущей выборки

Комментарии преподавателя

Расширение pgpro_pwr собирает данные с зарегистрированных серверов. По умолчанию создается активное определение сервера с именем local, соответствующее текущему кластеру.

Данные с активных серверов собираются функцией take_sample; неактивный сервер можно указывать явно. Если имеется несколько серверов, можно получить выборки с их подмножества функцией take_sample_subset.

Выборка содержит информацию об изменениях в измеряемых параметрах СУБД, происшедших с момента предыдущей выборки. Аккумулируемые регулярные выборки позволяют строить отчеты о нагрузке базы данных за предыдущее время. Обычно достаточно получать одну-две выборки в час.

При формировании выборки предыдущая статистика сбрасывается функцией pgpro_stats_statements_reset или pg_stat_statements_reset в зависимости от того, какое расширение поставляет данные.

https://postgrespro.ru/docs/enterprise/16/pgpro-pwr#PGPRO-PWR-MANAGI NG-SERVERS

https://postgrespro.ru/docs/enterprise/16/pgpro-pwr#PGPRO-PWR-SAMPL ES

Page 8: Политика хранения

Source slide page 8: Политика хранения{#source-figure-008 caption=“Source slide page 8: Политика хранения”}

Настройка

  • pgpro_pwr.max_sample_age = 7
  • срок хранения в днях, глобально
  • set_server_max_sample_age()
  • для конкретного сервера

Комментарии преподавателя

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

● глобально с помощью параметра конфигурации pgpro_pwr.max_sample_age;

● для конкретного сервера с помощью вызова функции set_server_max_sample_age.

Page 9: Выборочные линии

Source slide page 9: Выборочные линии{#source-figure-009 caption=“Source slide page 9: Выборочные линии”}

Именованная последовательность выборок

  • можно использовать для построения отчета
  • можно задать срок хранения (имеет приоритет над политикой)

Комментарии преподавателя

Для построения отчета по нагрузке требуется две выборки; они задают период, за который были собраны данные для отчета.

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

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

Для управления выборочными линиями предназначены функции:

● create_baseline — создает выборочную линию;

● drop_baseline — удаляет выборочную линию;

● keep_baseline — задает срок хранения;

● show_baselines — выдает информацию об имеющихся выборочных линиях.

https://postgrespro.ru/docs/enterprise/16/pgpro-pwr#PGPRO-PWR-BASELI NES

Page 10: Получение выборки и создание выборочной линии

Source slide page 10: Получение выборки и создание выборочной линии{#source-figure-010 caption=“Source slide page 10: Получение выборки и создание выборочной линии”}

Откроем второй сеанс.

student$ psql -p 5432 -d pgpro_pwr

Получим первую выборку. Предыдущая статистика сбрасывается.

=> SELECT * FROM profile.take_sample();
 server | result |  elapsed
--------+--------+------------
 local  | OK     | 00:00:02.3
(1 row)

Используем пример соединения хешированием из учебного курса QPT. Во втором сеансе выполним транзакцию, выделив недостаточный объем оперативной памяти; двухпроходное хеш-соединение будет использовать временные файлы:

=> BEGIN;
BEGIN
=> SET LOCAL work_mem = '12MB';
SET
=> SET LOCAL hash_mem_multiplier = 1;
SET
=> EXPLAIN (analyze, buffers, costs off, timing off, summary off)
SELECT * FROM bookings b
  JOIN tickets t ON b.book_ref = t.book_ref;
                            QUERY PLAN
------------------------------------------------------------------
 Hash Join (actual rows=366733 loops=1)
   Hash Cond: (t.book_ref = b.book_ref)
   Buffers: shared read=7888, temp read=3491 written=3491
   I/O Timings: shared read=39.412, temp read=10.989 write=38.067
   ->  Seq Scan on tickets t (actual rows=366733 loops=1)
         Buffers: shared read=6176
         I/O Timings: shared read=19.168
   ->  Hash (actual rows=262788 loops=1)
         Buckets: 262144  Batches: 2  Memory Usage: 9071kB
         Buffers: shared read=1712, temp written=690
         I/O Timings: shared read=20.244, temp write=9.460
         ->  Seq Scan on bookings b (actual rows=262788 loops=1)
               Buffers: shared read=1712
               I/O Timings: shared read=20.244
 Planning:
   Buffers: shared hit=208 read=12 dirtied=1
   I/O Timings: shared read=0.269
(17 rows)
=> COMMIT;
COMMIT

Получим вторую выборку — в нее попадут данные о нагрузке после первой выборки:

=> SELECT * FROM profile.take_sample();
 server | result |   elapsed
--------+--------+-------------
 local  | OK     | 00:00:00.79
(1 row)

Теперь во втором сеансе выполним транзакцию, выделив достаточно рабочей памяти для хеш-соединения:

=> BEGIN;
BEGIN
=> SET LOCAL work_mem = '48MB';

Page 11: Page 11

SET
=> SET LOCAL hash_mem_multiplier = 3;
SET
=> EXPLAIN (analyze, buffers, costs off, timing off, summary off)
SELECT * FROM bookings b
  JOIN tickets t ON b.book_ref = t.book_ref;
                           QUERY PLAN
-----------------------------------------------------------------
 Hash Join (actual rows=366733 loops=1)
   Hash Cond: (t.book_ref = b.book_ref)
   Buffers: shared hit=1744 read=6144
   I/O Timings: shared read=21.886
   ->  Seq Scan on tickets t (actual rows=366733 loops=1)
         Buffers: shared hit=32 read=6144
         I/O Timings: shared read=21.886
   ->  Hash (actual rows=262788 loops=1)
         Buckets: 524288  Batches: 1  Memory Usage: 18190kB
         Buffers: shared hit=1712
         ->  Seq Scan on bookings b (actual rows=262788 loops=1)
               Buffers: shared hit=1712
 Planning:
   Buffers: shared hit=8
(14 rows)
=> COMMIT;
COMMIT

А сейчас получим третью выборку — в нее попадут данные о нагрузке после второй выборки:

=> SELECT * FROM profile.take_sample();
 server | result |   elapsed
--------+--------+-------------
 local  | OK     | 00:00:00.66
(1 row)

Список выборок:

=> SELECT * FROM profile.show_samples();
 sample |      sample_time       | sizes_collected | dbstats_reset | clustats_reset |
archstats_reset
--------+------------------------+-----------------+---------------+----------------+-----
------------
      1 | 2026-07-27 23:42:53+03 | f               |               |                |
      2 | 2026-07-27 23:42:57+03 | f               |               |                |
      3 | 2026-07-27 23:43:00+03 | f               |               |                |
(3 rows)

Сформируем из имеющихся выборок две выборочные линии.

=> SELECT profile.create_baseline(baseline=>'BaseLine1', start_id=>1, end_id=>2);
 create_baseline
-----------------
               1
(1 row)
=> SELECT profile.create_baseline(baseline=>'BaseLine2', start_id=>2, end_id=>3);
 create_baseline
-----------------
               2
(1 row)

Список выборочных линий:

=> SELECT * FROM profile.show_baselines();

Page 12: Page 12

 baseline  | min_sample | max_sample | keep_until_time
-----------+------------+------------+-----------------
 BaseLine1 |          1 |          2 |
 BaseLine2 |          2 |          3 |
(2 rows)

Page 13: Размеры отношений

Source slide page 13: Размеры отношений{#source-figure-013 caption=“Source slide page 13: Размеры отношений”}

Политика анализа размеров

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

Комментарии преподавателя

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

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

● окно в течение суток, когда разрешается сбор сведений о размерах отношений;

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

Политику можно задать с помощью функции set_server_size_sampling.

Просмотреть политики для серверов можно, вызвав функцию show_servers_size_sampling.

https://postgrespro.ru/docs/enterprise/16/pgpro-pwr#PGPRO-PWR-RELATI ON-SIZES-COLLECTION

Page 14: Отчеты

Отчеты pgpro\_pwr формируются в формате HTML

  • get_report() — обычные отчеты, предоставляющие статистику
  • по нагрузке для заданного интервала
  • get_diffreport() — разностные отчеты со статистикой
  • по одинаковым объектам в двух сравниваемых интервалах
  • Выбранный элемент отчета автоматически выделяется в других разделах отчета
  • Настройка
    • pgpro_pwr.max = 7
    • число первых по некоторому критерию
    • объектов, выбираемых для каждой
    • таблицы отчета

Комментарии преподавателя

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

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

Разделы отчета подробно описаны в документации: https://postgrespro.ru/docs/enterprise/16/pgpro-pwr#PGPRO-PWR-REPOR T-GENERATION

Page 15: Отчеты

Получим отчет. Для построения обычного отчета достаточно указать две выборки или одну выборочную линию. В плане запроса, выполненного в условиях недостатка рабочей памяти, видно, что используются временные файлы для двухпроходного хеш-соединения. Раздел отчета “Top SQL by temp usage” должен содержать сведения об этом запросе.

student$ psql -d pgpro_pwr -Aqtc 'SELECT profile.get_report(1,2)' -o /tmp/report_1_2.html

Открываем /tmp/report_1_2.html…

student$ open /tmp/report_1_2.html

Получим разностный отчет. Используем две выборочные линии. В разностном отчете должно быть заметно, что во втором запросе рабочей памяти хватило для выполнения однопроходного хеш-соединения и временные файлы не использовались. В отчете просмотрите разделы: Top SQL by temp usage; Load distribution among heavily loaded databases; Load distribution.

student$ psql -d pgpro_pwr -Aqtc "SELECT profile.get_diffreport('BaseLine1','BaseLine2')" -o
/tmp/report_diff.html

Открываем /tmp/report_diff.html…

student$ open /tmp/report_diff.html

Page 16: Итоги

Расширение pgpro\_pwr предназначено для выявления ресурсоемких операций в БД

  • Статистика собирается расширениями pgpro_stats или pg_stat_statements
  • Поддерживается политика хранения выборок
  • Можно получать обычные и разностные отчеты в HTML

Page 17: Практика

  1. Подготовьте СУБД для работы с расширением pgpro_pwr. 2. Создайте базу данных и установите расширение pgpro_pwr. 3. Проведите нагрузочное тестирование с помощью pgbench, получив две выборки — до и после тестирования. 4. Создайте новое табличное пространство и сделайте выборку. 5. Снова проведите тестирование с помощью pgbench так, чтобы индексы были в новом табличном пространстве, после чего сделайте еще одну выборку. 6. Получите разностный отчет для первого и второго тестов.

Комментарии преподавателя

  1. Подключите разделяемые библиотеки pg_wait_sampling и pgpro_stats.

  2. В базе данных создайте схему для объектов расширения pgpro_pwr.

  3. Выполните обычную инициализацию таблиц pgbench и обычный тест нагрузки, например, на 60 секунд.

  4. Предварительно создайте каталог для табличного пространства, например, в домашнем каталоге пользователя postgres.

  5. Заново проведите инициализацию таблиц pgbench, указав для табличного пространства ключ –index-tablespace. Сделайте новую выборку.

  6. Получите разностный отчет с двумя промежутками.

Page 18: 1. Подготовка СУБД для работы с расширением pgpro_pwr

student$ psql

Подключим разделяемые библиотеки. Расширение pgpro_pwr получает сводную статистику ожиданий от расширения pg_wait_sampling:

=> ALTER SYSTEM SET shared_preload_libraries = pg_wait_sampling, pgpro_stats;
ALTER SYSTEM

Параметр track_io_timing нужен для анализа времени, затраченного операторами на чтение или запись блоков:

=> ALTER SYSTEM SET track_io_timing = 'on';
ALTER SYSTEM

Параметр track_functions позволяет отслеживать вызовы пользовательских функций:

=> ALTER SYSTEM SET track_functions = 'all';
ALTER SYSTEM

Перезагрузим сервер.

student$ sudo systemctl restart postgrespro-ent-16.service

2. Создание базы данных и установка расширения pgpro\_pwr

student$ psql

Создадим базу данных.

=> CREATE DATABASE pgpro_pwr;
CREATE DATABASE
=> \c pgpro_pwr
You are now connected to database "pgpro_pwr" as user "student".

Создадим схему для хранения объектов расширения и установим расширение.

=> CREATE SCHEMA profile;
CREATE SCHEMA
=> CREATE EXTENSION pgpro_pwr SCHEMA profile CASCADE;
NOTICE:  installing required extension "dblink"
CREATE EXTENSION
=> CREATE EXTENSION pg_wait_sampling;
CREATE EXTENSION
=> CREATE EXTENSION pgpro_stats;
CREATE EXTENSION
=> \dx
                                      List of installed extensions
       Name       | Version |   Schema   |                         Description
------------------+---------+------------+------------------------------------------------
--------------
 dblink           | 1.2     | profile    | connect to other PostgreSQL databases from
within a database
 pg_wait_sampling | 1.1     | public     | sampling based statistics of wait events
 pgpro_pwr        | 4.13    | profile    | PostgreSQL load profile repository and report
builder
 pgpro_stats      | 1.10    | public     | track execution statistics of all SQL
statements executed
 plpgsql          | 1.0.1   | pg_catalog | PL/pgSQL procedural language
(5 rows)

Page 19: 3. Нагрузочное тестирование

Сделаем выборку:

=> SELECT * FROM profile.take_sample();
 server | result |   elapsed
--------+--------+-------------
 local  | OK     | 00:00:02.64
(1 row)

Инициализируем нагрузочные таблицы pgbench:

student$ /opt/pgpro/ent-16/bin/pgbench -i pgpro_pwr
dropping old tables...
NOTICE:  table "pgbench_accounts" does not exist, skipping
NOTICE:  table "pgbench_branches" does not exist, skipping
NOTICE:  table "pgbench_history" does not exist, skipping
NOTICE:  table "pgbench_tellers" does not exist, skipping
creating tables...
generating data (client-side)...
100000 of 100000 tuples (100%) done (elapsed 0.17 s, remaining 0.00 s)
vacuuming...
creating primary keys...
done in 0.76 s (drop tables 0.00 s, create tables 0.10 s, client-side generate 0.35 s,
vacuum 0.12 s, primary keys 0.19 s).

Запустим pgbench на 30 секунд:

student$ /opt/pgpro/ent-16/bin/pgbench -T30 pgpro_pwr
pgbench (16.14)
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
maximum number of tries: 1
duration: 30 s
number of transactions actually processed: 3258
number of failed transactions: 0 (0.000%)
latency average = 9.208 ms
initial connection time = 3.571 ms
tps = 108.595587 (without initial connection time)

Получим вторую выборку, в которую попадут данные о нагрузке при инициализации и нагрузочном тесте:

=> SELECT * FROM profile.take_sample();
 server | result |  elapsed
--------+--------+------------
 local  | OK     | 00:00:00.9
(1 row)

4. Создание нового табличного пространства

Создадим табличное пространство.

postgres$ mkdir /var/lib/postgresql/ts_dir
=> CREATE TABLESPACE ts_idx LOCATION '/var/lib/postgresql/ts_dir';
CREATE TABLESPACE

Сделаем выборку:

=> SELECT * FROM profile.take_sample();
 server | result |   elapsed
--------+--------+-------------
 local  | OK     | 00:00:00.75
(1 row)

5. Нагрузочное тестирование при размещении индексов в отдельном табличном пространстве

Page 20: Page 20

Инициализируем нагрузочные таблицы pgbench:

student$ /opt/pgpro/ent-16/bin/pgbench -i --index-tablespace=ts_idx pgpro_pwr
dropping old tables...
creating tables...
generating data (client-side)...
100000 of 100000 tuples (100%) done (elapsed 0.11 s, remaining 0.00 s)
vacuuming...
creating primary keys...
done in 0.63 s (drop tables 0.02 s, create tables 0.03 s, client-side generate 0.30 s,
vacuum 0.11 s, primary keys 0.15 s).

Запустим pgbench на 30 секунд:

student$ /opt/pgpro/ent-16/bin/pgbench -T30 pgpro_pwr
pgbench (16.14)
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 1
number of threads: 1
maximum number of tries: 1
duration: 30 s
number of transactions actually processed: 3280
number of failed transactions: 0 (0.000%)
latency average = 9.146 ms
initial connection time = 5.295 ms
tps = 109.340623 (without initial connection time)

Получим еще одну выборку:

=> SELECT * FROM profile.take_sample();
 server | result |   elapsed
--------+--------+-------------
 local  | OK     | 00:00:00.74
(1 row)

Получим список выборок:

=> SELECT * FROM profile.show_samples();
 sample |      sample_time       | sizes_collected | dbstats_reset | clustats_reset |
archstats_reset
--------+------------------------+-----------------+---------------+----------------+-----
------------
      1 | 2026-07-28 00:14:18+03 | f               |               |                |
      2 | 2026-07-28 00:14:52+03 | f               |               |                |
      3 | 2026-07-28 00:14:53+03 | f               |               |                |
      4 | 2026-07-28 00:15:25+03 | f               |               |                |
(4 rows)

5. Получение разностного отчета

Получим разностный отчет.

student$ psql -d pgpro_pwr -Aqtc "SELECT profile.get_diffreport(1,2,3,4)" -o /tmp/report_diff.html

Открываем /tmp/report_diff.html…

student$ open /tmp/report_diff.html

Уборка

Удалим созданную базу данных, табличное пространство и файл отчета.

=> \c student
You are now connected to database "student" as user "student".
=> DROP DATABASE pgpro_pwr WITH (FORCE);
DROP DATABASE
=> DROP TABLESPACE ts_idx;
DROP TABLESPACE

Page 21: Page 21

student$ sudo rm -rf /var/lib/postgresql/ts_dir
student$ sudo rm -f /tmp/report_diff.html

Сбросим настройки параметров конфигурации.

=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service

Page 22: Практика PPEM

  1. В основном экземпляре создайте базу данных demo_pwr из шаблона demo. Подготовьте новую базу к работе с pgpro_pwr. 2. Сделайте снимок статистики новой базы с учетом размеров отношений. 3. Удалите схему bookings из базы данных demo_pwr. 4. Снова сделайте снимок статистики, подобный снимку из п. 2. 5. Постройте отчет по снимкам, построенным в пунктах 2 и 4, и найдите в нем данные, отражающие изменения, сделанные в п. 3.

Комментарии преподавателя

  1. В созданной базе установите необходимые расширения (нужные библиотеки уже были загружены в основном практическом задании) и создайте сервер профилировщика.

Page 23: 1. Создание базы и подготовка к работе с pgpro_pwr

На вкладке «Базы данных» нажмите «Создать базу данных» и заполните форму, указав базу demo в качестве шаблона. Выбрав новую базу, из вкладки «Расширения» создайте в ней расширение pgpro_pwr, включая установку зависимых расширений. Используя вкладку «Профилировщик» / «Серверы», создайте сервер профилировщика для новой базы — в строке подключения укажите dbname=demo_pwr.

2. Снимок статистики

Выберите вкладку «Снимки статистики» и нажмите «Сделать снимок» с выключенным параметром skip_sizes.

3. Изменения в данных

На вкладке «Базы данных» выберите demo_pwr, а в ней схему bookings. В столбце «Действия» для указанной схемы нажмите на пиктограмму «Удалить». Подтвердите удаление схемы, включив параметр каскадного удаления.

4. Повторный снимок статистики

Сделайте еще один снимок статистики, как в п. 2

5. Построение и просмотр отчета

На вкладке «Профилировщик» / «Построение отчетов» создайте стандартный отчет по снимкам, созданным в пунктах 2 и 4. В полученном отчете, в разделе «Server statistics» / «Database statistics», крайний правый столбец будет отражать изменение объема данных.

14 Занятие 14: Безопасность / Профили пользователей

Исходная страница в нативном Markdown: Безопасность / Профили пользователей
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_14_profile.html Archive: PGPRO-handouts-16.zip Страниц PDF: 16

Page 1: Безопасность / Профили пользователей

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Запрет простых паролей
  • Профили пользователей

Page 3: Запрет простых паролей

Модуль passwordcheck

Page 4: Модуль passwordcheck

  • user123
  • passwordcheck
  • R0cvjyfdnbrf+
  • passwordcheck
  • Проверка надежности паролей пользователей
    • CREATE ROLE
    • ALTER ROLE
    • \password

Комментарии преподавателя

Модуль passwordcheck проверяет надежность паролей, задаваемых командами CREATE ROLE и ALTER ROLE, а также устанавливаемых метакомандой \password в psql.

Модуль должен быть загружен с помощью shared_preload_libraries или командой LOAD.

Модуль passwordcheck проверяет, что пароль:

● содержит не менее 8 символов;

● содержит и буквы, и иные символы;

● не содержит имя роли.

Модуль не предусматривает конфигурационных настроек.

https://postgrespro.ru/docs/enterprise/16/passwordcheck#PASSWORDCHE CK

Page 5: Модуль passwordcheck

student$ psql

Расширение passwordcheck требует загрузки одноименной разделяемой библиотеки:

=> ALTER SYSTEM SET shared_preload_libraries = 'passwordcheck';
ALTER SYSTEM

После подключения библиотеки необходимо перезапустить СУБД.

student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql

Создадим базу данных и роль:

=> CREATE DATABASE pgpro_profile;
CREATE DATABASE
=> CREATE ROLE bob LOGIN;
CREATE ROLE

Запишем в дополнительный конфигурационный файл правило для парольной аутентификации нового пользователя при подключении к БД pgpro_profile:

/var/lib/pgpro/ent-16/pg_hba.conf.d/pgpro_profile.conf
# TYPE  DATABASE       USER   ADDRESS  METHOD
host    pgpro_profile  bob    all      scram-sha-256
=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Проверим, можно ли установить пользователю нестойкий пароль bob123:

=> ALTER ROLE bob PASSWORD 'bob123';
ERROR:  password is too short

Теперь попробуем установить сложный пароль:

=> ALTER ROLE bob PASSWORD 'GGU2015ujlf';
ALTER ROLE

Удалим модуль passwordcheck и снова перезапустим СУБД.

=> ALTER SYSTEM RESET shared_preload_libraries;
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service

Page 6: Профили пользователей

Профиль

  • Попытки входа
  • Время жизни пароля
  • Уровень сложности пароля

Page 7: Профиль

Профиль ограничивает использование системы пользователем

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

Комментарии преподавателя

Ядро Postgres Pro обеспечивает гораздо более гибкое управление парольными политиками, чем простое расширение passwordcheck.

Профиль ограничивает использование базы данных и устанавливает парольную политику для ролей. Профили определяются на уровне кластера баз данных и назначаются ролям. Новый профиль создает команда CREATE PROFILE, изменяет — ALTER PROFILE, удаляет — DROP PROFILE.

Профиль назначается при создании роли или командой ALTER ROLE. Новым ролям по умолчанию назначается профиль default. Исходно профиль default не устанавливает ограничений, но их можно добавить командой ALTER PROFILE. Если роли назначен профиль, отличный от default, рассмотренный выше модуль passwordcheck не работает.

Управлять профилями с помощью указанных команд разрешено только суперпользователям или пользователям, включенным в предустановленную роль pg_manage_profiles.

https://postgrespro.ru/docs/enterprise/16/sql-createprofile

Page 8: Попытки входа

FAILED\_LOGIN\_ATTEMPTS

  • количество неудачных попыток входа до блокировки пользователя
  • ALTER ROLE … ACCOUNT UNLOCK
  • FAILED_AUTH_KEEP_TIME
    • время в днях до сброса счетчика неудачных попыток

Комментарии преподавателя

Параметр профиля FAILED_LOGIN_ATTEMPTS ограничивает количество неудачных попыток входа. По превышении этого количества пользователь блокируется. Суперпользователь может разблокировать заблокированную роль командой ALTER ROLE с предложением ACCOUNT UNLOCK.

Параметр профиля FAILED_AUTH_KEEP_TIME задает время в днях с момента первой неудачной попытки входа; при попытке входа по истечении этого времени счетчик неудачных попыток сбрасывается, а пользователь разблокируется.

Page 9: Время жизни пароля

Source slide page 9: Время жизни пароля{#source-figure-009 caption=“Source slide page 9: Время жизни пароля”}

Комментарии преподавателя

Параметр профиля PASSWORD_LIFE_TIME задает период в днях, в течение которого пароль действителен. При установленном для роли атрибуте VALID UNTIL будет выбираться наиболее короткий срок действия пароля, исходя из этих двух значений.

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

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

Page 10: Уровень сложности пароля

PASSWORD\_MIN\_UNIQUE\_CHARS

  • минимальное количество уникальных символов
  • PASSWORD_MIN_LEN
    • минимальная длина
  • PASSWORD_REQUIRE_COMPLEX
    • проверка надежности пароля
  • PASSWORD_REUSE_TIME и PASSWORD_REUSE_MAX
    • время в днях и количество смен паролей, после которых старый пароль
    • можно использовать повторно

Комментарии преподавателя

Параметры профиля PASSWORD_MIN_UNIQUE_CHARS и PASSWORD_MIN_LEN ограничивают минимальное количество уникальных символов в пароле и его минимальную длину.

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

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

Page 11: Профили пользователей

student$ psql -d pgpro_profile

Пока ничего не мешает пользователю установить простой пароль:

=> ALTER ROLE bob PASSWORD 'bob';
ALTER ROLE

Создадим профиль:

=> CREATE PROFILE IF NOT EXISTS prof LIMIT
  PASSWORD_LIFE_TIME 60
  PASSWORD_GRACE_TIME 7
  PASSWORD_MIN_LEN 8
  PASSWORD_REQUIRE_COMPLEX;
CREATE PROFILE

Этот профиль определяет, что пароль необходимо менять через 60 дней. Еще в течение 7 дней будут выводиться предупреждения о необходимости поменять пароль. Минимальная длина пароля — 8 символов, причем пароль должен содержать как буквы, так и другие символы, а имя пользователя не должно входить в пароль. Назначим роли bob профиль prof:

=> ALTER ROLE bob PROFILE prof;
ALTER ROLE

Проверим, какой профиль связан с ролью:

=> SELECT r.rolname, p.pflname
FROM pg_authid r
  JOIN pg_profile p ON p.oid = r.rolprofile
WHERE r.rolname = 'bob';
 rolname | pflname
---------+---------
 bob     | prof
(1 row)

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

=> ALTER ROLE bob PASSWORD 'bob123';
ERROR:  password must be at least 8 characters long

Попробуем сделать пароль длиннее:

=> ALTER ROLE bob PASSWORD 'bob12345';
ERROR:  password must not contain user name

Усложним:

=> ALTER ROLE bob PASSWORD 'GGU2015ujlf';
ALTER ROLE

Модифицируем профиль prof, установив ограничение в три неудачные попытки начать сеанс до блокировки учетной записи.

=> ALTER PROFILE prof LIMIT FAILED_LOGIN_ATTEMPTS 3;
ALTER PROFILE

Боб пытается подключиться три раза с неправильными паролями:

student$ psql 'host=localhost dbname=pgpro_profile user=bob password=12345678'
psql: error: connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL:
password authentication failed for user "bob"
student$ psql 'host=localhost dbname=pgpro_profile user=bob password=qwerty'
psql: error: connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL:
password authentication failed for user "bob"

Page 12: Page 12

student$ psql 'host=localhost dbname=pgpro_profile user=bob password=q1w2e3r4'
psql: error: connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL:
password authentication failed for user "bob"

Теперь роль заблокирована:

=> SELECT CASE rolstatus
  WHEN 0 THEN 'роль активна'
  WHEN 1 THEN 'заблокирована вручную'
  WHEN 2 THEN 'заблокирована из-за бездействия'
  WHEN 4 THEN 'заблокирована по превышению числа попыток входа'
END status
FROM pg_roles
WHERE rolname = 'bob';
                     status
-------------------------------------------------
 заблокирована по превышению числа попыток входа
(1 row)

Разблокируем роль, дадим Бобу шанс вспомнить пароль:

=> ALTER ROLE bob ACCOUNT UNLOCK;
ALTER ROLE
student$ psql 'host=localhost dbname=pgpro_profile user=bob password=GGU2015ujlf' -c '\conninfo'
You are connected to database "pgpro_profile" as user "bob" on host "localhost" (address
"127.0.0.1") at port "5432".

Теперь сократим срок действия пароля индивидуально для Боба, оставив профиль неизменным:

=> SELECT now();
2026-07-27 23:43:30.462072+03
=> ALTER ROLE bob VALID UNTIL '2026-07-27 23:43:30.462072+03';
ALTER ROLE

Попытка подключения (пароль правильный):

student$ psql 'host=localhost dbname=pgpro_profile user=bob password=GGU2015ujlf' -c '\conninfo'
psql: error: connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL:
password authentication failed for user "bob"

В журнале сообщений сервера видим причину отказа в соединении:

student$ sudo tail -n 20 /var/lib/pgpro/ent-16/log/postgresql-2026-07-27_234328.log | grep -B 2 expired
2026-07-27 23:43:30.484 MSK [827845] STATEMENT:  UPDATE pg_authid SET
rolpasssetat=2026-07-27 23:43:30.462072+03-interval '1 min' WHERE rolname = 'bob';
2026-07-27 23:43:30.643 MSK [828401] FATAL:  password authentication failed for user "bob"
2026-07-27 23:43:30.643 MSK [828401] DETAIL:  User "bob" has an expired password.

Следующим образом можно вернуть для роли профиль по умолчанию:

=> ALTER ROLE bob PROFILE default;
ALTER ROLE

Удалим профиль:

=> DROP PROFILE prof;
DROP PROFILE

Удалим роль:

=> DROP ROLE bob;
DROP ROLE

Page 13: Итоги

Расширение passwordcheck проверяет надежность паролей

  • Встроенный механизм профилей позволяет настраивать политики использования системы пользователями
  • Профиль позволяет задавать уровень сложности пароля и ограничивать время его жизни
  • Можно ограничивать количество неудачных попыток ввода пароля

Page 14: Практика

  1. Создайте профиль с ограничением на минимальное количество разных символов в пароле и проверьте его. 2. Установите ограничение в две неудачные попытки ввода пароля для пользователя и проверьте его работу.

Комментарии преподавателя

  1. Создайте профиль mgr1 с ограничением PASSWORD_MIN_UNIQUE_CHARS = 8. Зарегистрируйте пользователя mgr1 с профилем mgr и проверьте возможность установки пароля не менее, чем с восемью отличающимися символами.

  2. Используйте настройку LIMIT FAILED_LOGIN_ATTEMPTS по аналогии с демонстрацией.

Page 15: 1. Профиль пользователя

Создадим базу данных pgpro_profile:

student$ /opt/pgpro/ent-16/bin/createdb pgpro_profile
student$ psql -d pgpro_profile

Создадим профиль с ограничением на минимальное количество отличающихся символов в пароле:

=> CREATE PROFILE mgr LIMIT PASSWORD_MIN_UNIQUE_CHARS 8;
CREATE PROFILE

Зарегистрируем пользователя с этим профилем:

=> CREATE ROLE mgr1 LOGIN PROFILE mgr;
CREATE ROLE

Правило для парольной аутентификации нового пользователя при подключении к БД pgpro_profile:

/var/lib/pgpro/ent-16/pg_hba.conf.d/pgpro_profile.conf
# TYPE  DATABASE       USER   ADDRESS  METHOD
host    pgpro_profile  mgr1   all      scram-sha-256
=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Проверим профиль роли:

=> SELECT r.rolname, p.pflname, p.pflpasswordminuniqchars
FROM pg_roles r
  JOIN pg_profile p ON r.rolprofile = p.oid
WHERE r.rolname = 'mgr1';
 rolname | pflname | pflpasswordminuniqchars
---------+---------+-------------------------
 mgr1    | mgr     |                       8
(1 row)

Пароль, разрешенный профилем:

=> ALTER ROLE mgr1 PASSWORD '12345678';
ALTER ROLE

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

=> ALTER ROLE mgr1 PASSWORD '12345677';
ERROR:  password must contain at least 8 unique characters

2. Ограничение неудачных попыток ввода пароля

Разрешим лишь две попытки.

=> ALTER PROFILE mgr LIMIT FAILED_LOGIN_ATTEMPTS 2;
ALTER PROFILE

Пару раз введем неверный пароль.

student$ psql 'host=localhost dbname=pgpro_profile user=mgr1 password=manager'
psql: error: connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL:
password authentication failed for user "mgr1"
student$ psql 'host=localhost dbname=pgpro_profile user=mgr1 password=manager'

Page 16: Page 16

psql: error: connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL:
password authentication failed for user "mgr1"

Блокировка должна быть уже установлена:

=> SELECT CASE rolstatus
  WHEN 0 THEN 'роль активна'
  WHEN 1 THEN 'заблокирована вручную'
  WHEN 2 THEN 'заблокирована из-за бездействия'
  WHEN 4 THEN 'заблокирована по превышению числа попыток входа'
END status
FROM pg_roles
WHERE rolname = 'mgr1';
                     status
-------------------------------------------------
 заблокирована по превышению числа попыток входа
(1 row)

Удалим роль и профиль:

=> DROP ROLE mgr1;
DROP ROLE
=> DROP PROFILE mgr;
DROP PROFILE

Удалим дополнительный файл конфигурации:

student$ sudo rm /var/lib/pgpro/ent-16/pg_hba.conf.d/pgpro_profile.conf

15 Занятие 15: Безопасность / Аудит

Исходная страница в нативном Markdown: Безопасность / Аудит
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_15_audit.html Archive: PGPRO-handouts-16.zip Страниц PDF: 21

Page 1: Безопасность / Аудит

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Расширение pg_proaudit
  • Возможности pg_proaudit
  • Аудит ролей и объектов
  • Сохранение настроек
  • Запись событий аудита

Page 3: Расширение pg_proaudit

Аудит — средство сбора событий безопасности

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

Комментарии преподавателя

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

Расширение pg_proaudit разработано в Postgres Professional для регистрации событий, связанных с безопасностью. Оно реализует концепцию независимости подсистемы аудита. Для этого запускается отдельный фоновый процесс pg_proaudit logging worker, который регистрирует события безопасности независимо от записи в обычный журнал сообщений сервера.

Место размещения журнала событий безопасности (журнал аудита) в файловой системе и настройки для передачи сообщений в syslog или во внешнюю систему определяются параметрами конфигурации расширения pg_proaudit. Каждое событие описывается фиксированным набором полей, для него формируется отдельная строка журнала или отдельное сообщение.

Доступ к расширению pg_proaudit и журналам аудита следует предоставлять только пользователям, исполняющим роль администратора информационной безопасности (например, администратору безопасности; см. тему «Защищенная схема»).

https://postgrespro.ru/docs/enterprise/16/pg-proaudit#PG-PROAUDIT

Page 4: Установка расширения pg_proaudit

Создадим базу данных.

=> CREATE DATABASE pgpro_audit;
CREATE DATABASE

Расширение pg_proaudit требует загрузки одноименной разделяемой библиотеки:

=> ALTER SYSTEM SET shared_preload_libraries = 'pg_proaudit';
ALTER SYSTEM

Необходимо рестартовать СУБД.

student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql -d pgpro_audit

Создадим расширение pg_proaudit:

=> CREATE EXTENSION pg_proaudit;
CREATE EXTENSION

В результате в списке процессов экземпляра должен быть виден процесс pg_proaudit logging worker.

student$ ps -o pid,command --ppid 828958
    PID COMMAND
 828960 postgres: logger
 828961 postgres: checkpointer
 828962 postgres: background writer
 828964 postgres: walwriter
 828965 postgres: autovacuum launcher
 828966 postgres: cfs-worker-0
 828967 postgres: pg_proaudit logging worker
 828968 postgres: logical replication launcher
 829006 postgres: student pgpro_audit [local] idle

Page 5: Возможности pg_proaudit

Source slide page 5: Возможности pg_proaudit{#source-figure-005 caption=“Source slide page 5: Возможности pg_proaudit”}

Классы фиксируемых событий

  • команды DDL — создание, изменение и удаление объектов баз данных
  • команды DCL — управление доступом к объектам баз данных
  • команды DML — обращение к объектам баз данных
  • подключения и отключения
  • действия, выполняемые заданной ролью

Комментарии преподавателя

Расширение pg_proaudit позволяет отслеживать действия заданных ролей, а также следить за операциями с заданными объектами (например, конкретными таблицами) или с определенными классами объектов и событий:

● команды создания, изменения и удаления объектов СУБД;

● команды управления доступом к объектам баз данных;

● команды обращения к объектам баз данных;

● события аутентификации или отключения от баз данных;

● любые команды, выполняемые заданным пользователем.

В том числе можно регистрировать в журнале аудита обращения к системному каталогу и отслеживать работу с профилями. Есть возможность исключить аудит временных объектов.

Полный список регистрируемых событий и классов событий: https://postgrespro.ru/docs/enterprise/16/pg-proaudit#PG-PROAUDIT-EVEN T-TYPES

Page 6: Аудит ролей и объектов

Управление аудитом ролей, объектов или типов объектов

  • pg_proaudit_set_rule()
  • pg_proaudit_remove_rule()
  • Отключение аудита всех событий
    • pg_proaudit_reset()
  • Текущие настройки
    • представление pg_proaudit_settings

Комментарии преподавателя

Для отслеживания событий или классов событий, выполняемых ролями (в том числе групповыми), а также событий, связанных с определенным объектом или типом объектов базы данных, создаются соответствующие правила. Для работы с правилами используются функции:

● pg_proaudit_set_rule — создание правила регистрации;

● pg_proaudit_remove_rule — удаление правила регистрации.

Функция pg_proaudit_reset отключает регистрацию всех событий.

Регистрация событий аудита запускается и останавливается немедленно при вызове этих функций.

Текущие настройки аудита (список правил) можно получить с помощью представления pg_proaudit_settings.

Page 7: Аудит ролей и объектов

Включим аудит всех событий, связанных с управлением ролями — для текущей базы, для всех объектов и всех ролей:

=> SELECT pg_proaudit_set_rule(
  db_name => current_database(),
  event_type => 'ALL',
  object_type => 'ROLE',
  object_name => NULL,
  role_name => NULL
);
 pg_proaudit_set_rule
----------------------
 t
(1 row)

Аудит включается немедленно. Для проверки настроек аудита используется представление:

=> SELECT * FROM pg_proaudit_settings;
   db_name   | event_type | object_type | object_name | role_name | comment
-------------+------------+-------------+-------------+-----------+---------
 pgpro_audit | ALL        | role        |             |           |
(1 row)

Зарегистрируем нового пользователя и отдельной командой дадим право начинать сеанс.

=> CREATE ROLE alice;
CREATE ROLE
=> ALTER ROLE alice LOGIN;
ALTER ROLE

Теперь назначим пользователю пароль:

=> ALTER ROLE alice PASSWORD 'alice';
ALTER ROLE

Ожидаем, что в журнал аудита попали три записи, по числу выполненных команд. Выведем последние строки из журнала аудита, имеющего самую свежую дату модификации:

student$ sudo tail -10 /var/lib/pgpro/ent-16/pg_proaudit/postgresql-2026-07-27_234348.log
2026-07-27 23:43:49.533 MSK,"student","pgpro_audit",829006,INFO,1,1,"CREATE
ROLE","ROLE","alice",SUCCESS,"","CREATE ROLE
alice;",,"student",019FA551-CD9D-7F02-B259-AD66B00330DB,964,4/9,,[local]
2026-07-27 23:43:49.583 MSK,"student","pgpro_audit",829006,INFO,2,1,"ALTER
ROLE","ROLE","alice",SUCCESS,"","ALTER ROLE alice
LOGIN;",,"student",019FA551-CDCF-7465-91E4-C9A68E5391D5,965,4/10,,[local]
2026-07-27 23:43:49.728 MSK,"student","pgpro_audit",829006,INFO,3,1,"ALTER
ROLE","ROLE","alice",SUCCESS,"","ALTER ROLE alice password
<REDACTED>",,"student",019FA551-CE60-734F-A2B0-C02883666153,966,4/11,,[local]

Обратите внимание, что конфиденциальная информация (в данном случае — пароли) скрыта за пометкой . Текстовый CSV-файл удобен для автоматизированной обработки, но для ручного анализа может быть удобнее обращаться к журналу с помощью SQL, используя обертку сторонних данных. Установим расширение file_fdw и определим сторонний сервер.

=> CREATE EXTENSION file_fdw;
CREATE EXTENSION
=> CREATE SERVER pg_proaudit_server FOREIGN DATA WRAPPER file_fdw;
CREATE SERVER

Создадим стороннюю таблицу:

Page 8: Page 8

=> CREATE FOREIGN TABLE pg_proaudit_log(
  log_time timestamp(3) with time zone,
  current_usr_name text,
  database_name text,
  session_pid text,
  error_severity text,
  session_line_num bigint,
  session_line_subcommand_num bigint,
  event_type text,
  object_type text,
  object_name text,
  status text,
  error_message text,
  query_text text,
  query_args text,
  session_usr_name text,
  uuid text,
  xid text,
  vxid text,
  rows text,
  host text
)
SERVER pg_proaudit_server
OPTIONS (filename '/var/lib/pgpro/ent-16/pg_proaudit/postgresql-2026-07-27_234348.log',
         format 'csv' );
CREATE FOREIGN TABLE

И прочитаем из нее записи, касающиеся изменений в данных таблицы tab1:

=> SELECT log_time, current_usr_name, event_type, status, query_text
FROM pg_proaudit_log
LIMIT 3 \gx
-[ RECORD 1 ]----+-------------------------------------
log_time         | 2026-07-27 23:43:49.533+03
current_usr_name | student
event_type       | CREATE ROLE
status           | SUCCESS
query_text       | CREATE ROLE alice;
-[ RECORD 2 ]----+-------------------------------------
log_time         | 2026-07-27 23:43:49.583+03
current_usr_name | student
event_type       | ALTER ROLE
status           | SUCCESS
query_text       | ALTER ROLE alice LOGIN;
-[ RECORD 3 ]----+-------------------------------------
log_time         | 2026-07-27 23:43:49.728+03
current_usr_name | student
event_type       | ALTER ROLE
status           | SUCCESS
query_text       | ALTER ROLE alice password <REDACTED>

Включим аудит всех событий подключения. Здесь функции pg_proaudit_set_rule вместо типа объекта передаем NULL.

=> SELECT pg_proaudit_set_rule(
  db_name => current_database(),
  event_type => 'AUTHENTICATE',
  object_type => NULL,
  object_name => NULL,
  role_name => NULL
);
 pg_proaudit_set_rule
----------------------
 t
(1 row)

Что теперь покажет представление pg_proaudit_settings?

=> SELECT * FROM pg_proaudit_settings;
   db_name   |  event_type  | object_type | object_name | role_name | comment
-------------+--------------+-------------+-------------+-----------+---------
 pgpro_audit | ALL          | role        |             |           |
 pgpro_audit | authenticate | ALL         |             |           |
(2 rows)

Page 9: Page 9

Начнем другой сеанс от имени Алисы:

student$ psql 'host=localhost dbname=pgpro_audit user=alice password=alice'

И снова проверим журнал аудита:

=> SELECT log_time, current_usr_name, event_type, status
FROM pg_proaudit_log
ORDER BY log_time DESC
LIMIT 1 \gx
-[ RECORD 1 ]----+---------------------------
log_time         | 2026-07-27 23:43:51.497+03
current_usr_name | alice
event_type       | AUTHENTICATE
status           | SUCCESS

Следующая задача — включить регистрацию всех действий Алисы. Для этого укажем имя роли в соответствующем параметре функции pg_proaudit_set_rule:

=> SELECT pg_proaudit_set_rule(
  db_name => current_database(),
  event_type => 'ALL',
  object_type => NULL,
  object_name => NULL,
  role_name => 'alice'
);
 pg_proaudit_set_rule
----------------------
 t
(1 row)

Как это отобразилось в представлении pg_proaudit_settings?

=> SELECT * FROM pg_proaudit_settings;
   db_name   |  event_type  | object_type | object_name | role_name | comment
-------------+--------------+-------------+-------------+-----------+---------
 pgpro_audit | ALL          | role        |             |           |
 pgpro_audit | authenticate | ALL         |             |           |
 pgpro_audit | ALL          | ALL         |             | alice     |
(3 rows)

Алиса собирается создать таблицу в схеме public, предоставим ей такие полномочия:

=> GRANT CREATE ON SCHEMA public TO alice;
GRANT

Алиса создает таблицу в своем сеансе:

=> CREATE TABLE tab1(
  id integer,
  msg text
);
CREATE TABLE

…и сведения об этом сразу появляются в журнале аудита:

=> SELECT log_time, current_usr_name, event_type, object_type, object_name, status, query_text
FROM pg_proaudit_log
ORDER BY log_time DESC
LIMIT 1 \gx
-[ RECORD 1 ]----+---------------------------
log_time         | 2026-07-27 23:43:51.946+03
current_usr_name | alice
event_type       | CREATE TABLE
object_type      | TABLE
object_name      | public.tab1
status           | SUCCESS
query_text       | CREATE TABLE tab1(        +
                 |   id integer,             +
                 |   msg text                +
                 | );

Сосредоточимся теперь на отслеживании изменений данных в таблице tab1. Отменим аудит всех действий Алисы и

Page 10: Page 10

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

=> SELECT pg_proaudit_remove_rule(
  db_name => current_database(),
  event_type => 'ALL',
  object_type => NULL,
  object_name => NULL,
  role_name => 'alice'
);
 pg_proaudit_remove_rule
-------------------------
 t
(1 row)
=> SELECT pg_proaudit_set_rule(
  db_name => current_database(),
  event_type => 'ALL_MOD',
  object_type => 'TABLE',
  object_name => 'public.tab1',
  role_name => NULL
);
 pg_proaudit_set_rule
----------------------
 t
(1 row)
=> SELECT * FROM pg_proaudit_settings;
   db_name   |  event_type  | object_type | object_name | role_name | comment
-------------+--------------+-------------+-------------+-----------+---------
 pgpro_audit | ALL          | role        |             |           |
 pgpro_audit | authenticate | ALL         |             |           |
 pgpro_audit | ALL_MOD      | table       | public.tab1 |           |
(3 rows)

В сеансе Алисы выполним вставку в таблицу tab1 и проверим журнал аудита.

=> INSERT INTO tab1 VALUES (1984, 'Big brother is watching you...');
INSERT 0 1
=> SELECT * FROM tab1;
  id  |              msg
------+--------------------------------
 1984 | Big brother is watching you...
(1 row)
=> SELECT log_time, current_usr_name, event_type, status, query_text
FROM pg_proaudit_log
ORDER BY log_time DESC
LIMIT 1 \gx
-[ RECORD 1 ]----+------------------------------------------------------------------
log_time         | 2026-07-27 23:43:53.391+03
current_usr_name | alice
event_type       | INSERT
status           | SUCCESS
query_text       | INSERT INTO tab1 VALUES (1984, 'Big brother is watching you...');

Обратите внимание на отсутствие записи о выполнении SELECT. Создадим второго пользователя:

=> CREATE USER bob PASSWORD 'bob';
CREATE ROLE

Откроем от его имени еще один сеанс, в котором Боб попытается опустошить таблицу tab1:

student$ psql 'host=localhost dbname=pgpro_audit user=bob password=bob'
=> TRUNCATE TABLE tab1;
ERROR:  permission denied for table tab1

Page 11: Page 11

Поскольку у Боба нет доступа к таблице, в журнале аудита увидим ошибку:

=> SELECT log_time, current_usr_name, event_type, status, query_text
FROM pg_proaudit_log
ORDER BY log_time DESC
LIMIT 3 \gx
-[ RECORD 1 ]----+------------------------------------
log_time         | 2026-07-27 23:43:53.802+03
current_usr_name | bob
event_type       | TRUNCATE TABLE
status           | FAILURE
query_text       | TRUNCATE TABLE tab1;
-[ RECORD 2 ]----+------------------------------------
log_time         | 2026-07-27 23:43:53.753+03
current_usr_name | bob
event_type       | AUTHENTICATE
status           | SUCCESS
query_text       |
-[ RECORD 3 ]----+------------------------------------
log_time         | 2026-07-27 23:43:53.65+03
current_usr_name | student
event_type       | CREATE ROLE
status           | SUCCESS
query_text       | CREATE USER bob password <REDACTED>

Page 12: Настройки

Настройки, сделанные функциями, необходимо сохранить в файле PGDATA/pg\_proaudit.conf

  • pg_proaudit_save()
  • Файл настроек аудита можно отредактировать напрямую, но после этого надо перечитать
    • pg_proaudit_reload()

Комментарии преподавателя

Настройки аудита, произведенные вызовами функций расширения pg_proaudit, будут потеряны после перезагрузки сервера, если не сохранить их в файле настроек pg_proaudit.conf.

Файл pg_proaudit.conf размещается в каталоге кластера данных (PGDATA), изменить его расположение нельзя.

Для сохранения текущих настроек в этот файл предназначена функция pg_proaudit_save. Аудит можно настраивать и непосредственным редактированием файла pg_proaudit.conf, но это не так удобно, как вызов функций расширения.

Если изменить настройки непосредственно в файле pg_proaudit.conf, их нужно прочитать и применить функцией pg_proaudit_reload.

При запуске сервера настройки из файла считываются автоматически.

Page 13: Настройки

Функция pg_proaudit_save записывает настройки аудита в файл pg_proaudit.conf. Если не сохранить настройки, они будут потеряны при остановке сервера.

=> SELECT pg_proaudit_save();
 pg_proaudit_save
------------------
(1 row)

Заглянем в файл pg_proaudit.conf:

/var/lib/pgpro/ent-16/pg_proaudit.conf
"pgpro_audit",ALL,role,"","",""
"pgpro_audit",authenticate,,"","",""
"pgpro_audit",ALL_MOD,table,"public.tab1","",""

А теперь сбросим все настройки аудита:

=> SELECT pg_proaudit_reset();
 pg_proaudit_reset
-------------------
(1 row)

Никакие настройки не действуют:

=> SELECT * FROM pg_proaudit_settings;
 db_name | event_type | object_type | object_name | role_name | comment
---------+------------+-------------+-------------+-----------+---------
(0 rows)

Можно (хотя это не очень удобно) изменять файл pg_proaudit.conf любыми средствами редактирования текста. Например, удалим строку конфигурации аудита событий подключения:

/var/lib/pgpro/ent-16/pg_proaudit.conf
"pgpro_audit",ALL,role,"","",""
"pgpro_audit",ALL_MOD,table,"public.tab1","",""

А теперь считаем настройки из файла:

=> SELECT pg_proaudit_reload();
 pg_proaudit_reload
--------------------
(1 row)
=> SELECT * FROM pg_proaudit_settings;
   db_name   | event_type | object_type | object_name | role_name | comment
-------------+------------+-------------+-------------+-----------+---------
 pgpro_audit | ALL        | role        |             |           |
 pgpro_audit | ALL_MOD    | table       | public.tab1 |           |
(2 rows)

Настройки аудита восстановлены, за исключением регистрации подключений.

Page 14: Запись событий аудита

Source slide page 14: Запись событий аудита{#source-figure-014 caption=“Source slide page 14: Запись событий аудита”}

Комментарии преподавателя

Параметр расширения pg_proaudit.log_destination определяет способ регистрации (один или несколько) событий безопасности:

● csvlog — записывать события в файл CSV.

Параметр pg_proaudit.log_filename задает шаблон имен файлов CSV, а параметр pg_proaudit.log_rotation_size — максимальный размер файла в килобайтах. Другой способ настройки ротации файлов журнала — по времени — устанавливается параметром pg_proaudit.log_rotation_age.

● ceflog — передавать события в SIEM-систему в формате CEF.

Поддержка открытого формата событий CEF (Common Event Format) позволяет отправлять собранные данные различным системам управления информацией и событиями безопасности (SIEM, Security Information and Event Management).

● syslog и cefsyslog — передавать события в службу syslog в формате syslog или CEF.

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

Полный список параметров конфигурации записи событий аудита в журнал содержится в документации: https://postgrespro.ru/docs/enterprise/16/pg-proaudit#PG-PROAUDIT-SECU RITY-EVENT-LOG-CONFIGURATION-PARAMETERS

Page 15: Настройка записи событий аудита

Посмотрим значения параметров регистрации событий по умолчанию:

=> SELECT name, setting
FROM pg_settings
WHERE name LIKE 'pg_proaudit.log_%';
                 name                 |            setting
--------------------------------------+--------------------------------
 pg_proaudit.log_catalog_access       | off
 pg_proaudit.log_command_text         | on
 pg_proaudit.log_destination          | csvlog
 pg_proaudit.log_directory            | pg_proaudit
 pg_proaudit.log_file_mode            | 0600
 pg_proaudit.log_filename             | postgresql-%Y-%m-%d_%H%M%S.log
 pg_proaudit.log_rotation_age         | 1440
 pg_proaudit.log_rotation_size        | 10240
 pg_proaudit.log_rows                 | off
 pg_proaudit.log_truncate_on_rotation | off
(10 rows)

Добавим запись в журнал аудита количества строк, затронутых SQL-командой:

=> ALTER SYSTEM SET pg_proaudit.log_rows = on;
ALTER SYSTEM

Перечитаем конфигурацию.

=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Удалим строки в таблице tab1 в сеансе Алисы:

=> DELETE FROM tab1;
DELETE 1

Теперь в конце записи о событии появилось еще одно поле. Сравните последнюю запись с записью вставки: удалена одна строка.

=> SELECT log_time, current_usr_name, event_type, status, query_text, rows
FROM pg_proaudit_log
WHERE event_type IN ('INSERT', 'DELETE') \gx
-[ RECORD 1 ]----+------------------------------------------------------------------
log_time         | 2026-07-27 23:43:53.391+03
current_usr_name | alice
event_type       | INSERT
status           | SUCCESS
query_text       | INSERT INTO tab1 VALUES (1984, 'Big brother is watching you...');
rows             |
-[ RECORD 2 ]----+------------------------------------------------------------------
log_time         | 2026-07-27 23:43:54.98+03
current_usr_name | alice
event_type       | DELETE
status           | SUCCESS
query_text       | DELETE FROM tab1;
rows             | 1

Page 16: Итоги

Расширение pg\_proaudit регистрирует события аудита независимо от обычного журнала сообщений

  • Аудиту подлежат подключения и отключения, операции с объектами баз данных и действия ролей
  • Журнал аудита может сохраняться как в формате CSV, так и передаваться службам SIEM или syslog

Page 17: Практика

  1. Настройте аудит для записи в CSV и syslog одновременно. Включите аудит регистрации ролей и создания таблиц. В следующих заданиях сравните сообщения, попадающие в CVS и в syslog. 2. Зарегистрируйте новую роль observed1 и настройте аудит подключений и отключений для этой роли. 3. Создайте таблицу tab1 так, чтобы роль observed1 могла выполнять команды DML с этой таблицей; владельцем таблицы должен быть другой пользователь. 4. Настройте регистрацию команд DML, выполненных любым пользователем, для таблицы tab1.

Комментарии преподавателя

  1. После подключения аудита используйте команду
ALTER SYSTEM SET pg_proaudit.log_destination = 'csvlog,syslog';

для записи сообщений аудита и в файл, и в syslog.

Для настройки аудита используйте функцию pg_proaudit_set_rule.

В заданиях с 2 по 4 просматривать сообщения аудита в файле CSV нужно в каталоге /var/lib/pgpro/ent-16/pg_proaudit.

Идентификатор, заранее записанный в параметр сервера syslog_ident, поможет отфильтровать необходимые строки журнала сообщений syslog, который находится в файле /var/log/syslog. Воспользуйтесь следующей командой:

sudo grep -a идентификатор /var/log/syslog | tail

Page 18: 1. Настройка аудита

Создадим базу данных:

=> CREATE DATABASE pgpro_audit;
CREATE DATABASE

Загрузим библиотеку:

=> ALTER SYSTEM SET shared_preload_libraries = 'pg_proaudit';
ALTER SYSTEM

Подключение библиотеки требует перезагрузки СУБД.

student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql -d pgpro_audit

Создадим расширение pg_proaudit:

=> CREATE EXTENSION pg_proaudit;
CREATE EXTENSION

Настроим запись сообщений аудита одновременно в CSV-файл и syslog:

=> ALTER SYSTEM SET pg_proaudit.log_destination = 'csvlog,syslog';
ALTER SYSTEM

Установим имя программы — идентификатор в записях syslog:

=> ALTER SYSTEM SET syslog_ident = 'MY_audit';
ALTER SYSTEM
=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

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

=> SELECT pg_proaudit_set_rule(current_database(), 'ALL', 'ROLE', NULL, NULL);
 pg_proaudit_set_rule
----------------------
 t
(1 row)

Аудит событий создания таблиц:

=> SELECT pg_proaudit_set_rule(current_database(), 'CREATE TABLE', 'TABLE', NULL, NULL);
 pg_proaudit_set_rule
----------------------
 t
(1 row)

Получившиеся настройки аудита:

=> SELECT * FROM pg_proaudit_settings;
   db_name   |  event_type  | object_type | object_name | role_name | comment
-------------+--------------+-------------+-------------+-----------+---------
 pgpro_audit | ALL          | role        |             |           |
 pgpro_audit | create table | table       |             |           |
(2 rows)

Page 19: 2. Регистрация событий создания и управления пользователями

Регистрация нового пользователя:

=> CREATE ROLE observed1 LOGIN PASSWORD 'obs123';
CREATE ROLE

Второй сеанс от имени observed1:

student$ psql 'dbname=pgpro_audit user=observed1 password=obs123'

Командой ls -ltr $PGDATA/pg_proaudit можно получить список файлов аудита, отсортированный по времени модификации:

student$ sudo ls -ltr /var/lib/pgpro/ent-16/pg_proaudit
total 4
-rw------- 1 postgres postgres 228 июл 28 00:16 postgresql-2026-07-28_001605.log

Проверяем CSV-файл аудита, который был изменен последним:

student$ sudo tail -1 /var/lib/pgpro/ent-16/pg_proaudit/postgresql-2026-07-28_001605.log
2026-07-28 00:16:06.494 MSK,"student","pgpro_audit",887175,INFO,1,1,"CREATE
ROLE","ROLE","observed1",SUCCESS,"","CREATE ROLE observed1 LOGIN password
<REDACTED>",,"student",019FA56F-5BDE-7215-80D8-E869B0DBA039,964,4/13,,[local]

Получим последние две записи в журнале /var/log/syslog по установленному идентификатору MY_audit:

student$ sudo grep -a MY_audit /var/log/syslog | tail -2
2026-07-28T00:16:06.494916+03:00 course MY_audit[887175]: AUDIT [1-1] INFO: USER:
student, DB: pgpro_audit, PID: 887175, EVENT TYPE: CREATE ROLE, OBJECT TYPE: ROLE, OBJECT
NAME: observed1, SUCCESS, SESSION USER: student, UUID:
019FA56F-5BDE-7215-80D8-E869B0DBA039, XID: 964, VXID: 4/13, SRC: [local]
2026-07-28T00:16:06.495262+03:00 course MY_audit[887175]: AUDIT [1-1] STATEMENT: CREATE
ROLE observed1 LOGIN password <REDACTED>

3. Создание таблицы и предоставление прав на нее

Суперпользователь student создает таблицу и выдает на нее права observed1.

=> CREATE TABLE tab1(n integer, txt text);
CREATE TABLE
=> GRANT SELECT,INSERT,UPDATE,DELETE ON tab1 TO observed1;
GRANT

Проверяем CSV-файл аудита: должно быть видно CREATE TABLE, но не GRANT.

student$ sudo tail -1 /var/lib/pgpro/ent-16/pg_proaudit/postgresql-2026-07-28_001605.log
2026-07-28 00:16:07.975 MSK,"student","pgpro_audit",887175,INFO,2,1,"CREATE
TABLE","TABLE","public.tab1",SUCCESS,"","CREATE TABLE tab1(n integer, txt
text);",,"student",019FA56F-61A7-716F-ABB1-ABA3E461C9D9,965,4/14,,[local]

Поиск записей в журнале /var/log/syslog по установленному идентификатору MY_audit:

student$ sudo grep -a MY_audit /var/log/syslog | tail -2
2026-07-28T00:16:07.975974+03:00 course MY_audit[887175]: AUDIT [2-1] INFO: USER:
student, DB: pgpro_audit, PID: 887175, EVENT TYPE: CREATE TABLE, OBJECT TYPE: TABLE,
OBJECT NAME: public.tab1, SUCCESS, SESSION USER: student, UUID:
019FA56F-61A7-716F-ABB1-ABA3E461C9D9, XID: 965, VXID: 4/14, SRC: [local]
2026-07-28T00:16:07.976100+03:00 course MY_audit[887175]: AUDIT [2-1] STATEMENT: CREATE
TABLE tab1(n integer, txt text);

4. Регистрация команд DML с таблицей tab1

Установка регистрации команд DML для tab1.

=> SELECT pg_proaudit_set_rule(current_database(), 'ALL_DML', 'TABLE', 'public.tab1', NULL);
 pg_proaudit_set_rule
----------------------
 t
(1 row)

Page 20: Page 20

Получившиеся настройки аудита:

=> SELECT * FROM pg_proaudit_settings;
   db_name   |  event_type  | object_type | object_name | role_name | comment
-------------+--------------+-------------+-------------+-----------+---------
 pgpro_audit | ALL          | role        |             |           |
 pgpro_audit | create table | table       |             |           |
 pgpro_audit | ALL_DML      | table       | public.tab1 |           |
(3 rows)

Выполним несколько команд в сеансе observed1, сразу проверяя CSV-файл и журнал событий:

=> INSERT INTO tab1 VALUES (1, 'Один');
INSERT 0 1
student$ sudo tail -1 /var/lib/pgpro/ent-16/pg_proaudit/postgresql-2026-07-28_001605.log
2026-07-28 00:16:09.474
MSK,"observed1","pgpro_audit",887533,INFO,1,1,"INSERT","TABLE","public.tab1",SUCCESS,"","I
NSERT INTO tab1 VALUES (1,
'Один');",,"observed1",019FA56F-6782-7868-B88C-EEF51B601E9C,,5/4,,[local]
student$ sudo grep -a MY_audit /var/log/syslog | tail -2
2026-07-28T00:16:09.475221+03:00 course MY_audit[887533]: AUDIT [1-1] INFO: USER:
observed1, DB: pgpro_audit, PID: 887533, EVENT TYPE: INSERT, OBJECT TYPE: TABLE, OBJECT
NAME: public.tab1, SUCCESS, SESSION USER: observed1, UUID:
019FA56F-6782-7868-B88C-EEF51B601E9C, XID: 0, VXID: 5/4, SRC: [local]
2026-07-28T00:16:09.475452+03:00 course MY_audit[887533]: AUDIT [1-1] STATEMENT: INSERT
INTO tab1 VALUES (1, 'Один');
=> SELECT * FROM tab1;
 n | txt
---+------
 1 | Один
(1 row)
student$ sudo tail -1 /var/lib/pgpro/ent-16/pg_proaudit/postgresql-2026-07-28_001605.log
2026-07-28 00:16:10.719
MSK,"observed1","pgpro_audit",887533,INFO,2,1,"SELECT","TABLE","public.tab1",SUCCESS,"","S
ELECT * FROM tab1;",,"observed1",019FA56F-6C5F-7365-AE2B-B6716031846C,,5/5,,[local]
student$ sudo grep -a MY_audit /var/log/syslog | tail -2
2026-07-28T00:16:10.719912+03:00 course MY_audit[887533]: AUDIT [2-1] INFO: USER:
observed1, DB: pgpro_audit, PID: 887533, EVENT TYPE: SELECT, OBJECT TYPE: TABLE, OBJECT
NAME: public.tab1, SUCCESS, SESSION USER: observed1, UUID:
019FA56F-6C5F-7365-AE2B-B6716031846C, XID: 0, VXID: 5/5, SRC: [local]
2026-07-28T00:16:10.719996+03:00 course MY_audit[887533]: AUDIT [2-1] STATEMENT: SELECT *
FROM tab1;
=> UPDATE tab1 SET n=2;
UPDATE 1
student$ sudo tail -1 /var/lib/pgpro/ent-16/pg_proaudit/postgresql-2026-07-28_001605.log
2026-07-28 00:16:11.970
MSK,"observed1","pgpro_audit",887533,INFO,3,1,"UPDATE","TABLE","public.tab1",SUCCESS,"","U
PDATE tab1 SET n=2;",,"observed1",019FA56F-7142-743C-9EF1-92E5AA3AED8A,,5/6,,[local]
student$ sudo grep -a MY_audit /var/log/syslog | tail -2
2026-07-28T00:16:11.971276+03:00 course MY_audit[887533]: AUDIT [3-1] INFO: USER:
observed1, DB: pgpro_audit, PID: 887533, EVENT TYPE: UPDATE, OBJECT TYPE: TABLE, OBJECT
NAME: public.tab1, SUCCESS, SESSION USER: observed1, UUID:
019FA56F-7142-743C-9EF1-92E5AA3AED8A, XID: 0, VXID: 5/6, SRC: [local]
2026-07-28T00:16:11.971413+03:00 course MY_audit[887533]: AUDIT [3-1] STATEMENT: UPDATE
tab1 SET n=2;
=> DELETE FROM tab1;
DELETE 1
student$ sudo tail -1 /var/lib/pgpro/ent-16/pg_proaudit/postgresql-2026-07-28_001605.log
2026-07-28 00:16:13.201

Page 21: Page 21

MSK,"observed1","pgpro_audit",887533,INFO,4,1,"DELETE","TABLE","public.tab1",SUCCESS,"","D
ELETE FROM tab1;",,"observed1",019FA56F-7611-7DC3-BBA7-6D315A10132C,,5/7,,[local]
student$ sudo grep -a MY_audit /var/log/syslog | tail -2
2026-07-28T00:16:13.202695+03:00 course MY_audit[887533]: AUDIT [4-1] INFO: USER:
observed1, DB: pgpro_audit, PID: 887533, EVENT TYPE: DELETE, OBJECT TYPE: TABLE, OBJECT
NAME: public.tab1, SUCCESS, SESSION USER: observed1, UUID:
019FA56F-7611-7DC3-BBA7-6D315A10132C, XID: 0, VXID: 5/7, SRC: [local]
2026-07-28T00:16:13.203005+03:00 course MY_audit[887533]: AUDIT [4-1] STATEMENT: DELETE
FROM tab1;

Уборка

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

=> \c student
You are now connected to database "student" as user "student".
=> DROP DATABASE pgpro_audit WITH (FORCE);
DROP DATABASE
=> DROP ROLE observed1;
DROP ROLE
student$ sudo rm -rf /var/lib/pgpro/ent-16/pg_proaudit
=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service

16 Занятие 16: Безопасность / Защищенная схема

Исходная страница в нативном Markdown: Безопасность / Защищенная схема
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_16_vault.html Archive: PGPRO-handouts-16.zip Страниц PDF: 31

Page 1: Безопасность / Защищенная схема

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Разделение ролей
  • Создание защищенной схемы
  • Предоставление доступа к защищаемым данным
  • Логическая копия защищенной схемы и ее восстановление

Page 3: Разделение ролей

Администратор ОС

  • управление подключением суперпользователя Суперпользователь
    • создание административных ролей
    • решение особых задач
  • Административные роли
  • управление СУБД обеспечение работы пользователей Пользователи
    • работа с объектами

Комментарии преподавателя

При развертывании СУБД и на начальных этапах работы с ней активно используется роль суперпользователя. Однако ее использование для выполнения различных рутинных операций по обслуживанию системы в дальнейшем является небезопасным.

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

https://postgrespro.ru/docs/enterprise/16/sod-separation-of-duties#SOD-SE PARATION-OF-DUTIES

Для этого администратор ОС может вообще запретить подключение к серверу под учетной записью суперпользователя с помощью правил в файле pg_hba.conf.

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

https://postgrespro.ru/docs/enterprise/16/restrict-dbms-admin-data-access# REVOKE-SUPERUSER-ACCESS-1

https://postgrespro.ru/docs/enterprise/16/app-postgres#APP-POSTGRES-SI NGLE-USER

Page 4: Защищенная схема

Зона повышенной безопасности на основе схемы

  • инициализируется суперпользователем закрыт доступ для административных ролей Администратор безопасности
  • выдает разрешения на доступ к объектам схемы не может работать с объектами схемы Владелец схемы
    • может работать с объектами
    • не может управлять доступом к объектам

Комментарии преподавателя

Данные хранятся в таблицах, которые находятся в схемах баз данных. По умолчанию административные роли имеют доступ к этим данным, что может быть нежелательным. Для защиты чувствительных данных Postgres Pro Enterprise позволяет поместить их в специально созданную защищенную схему и определить правила доступа к ней.

У защищенной схемы есть владелец и выделенный администратор безопасности. Администратор безопасности (менеджер прав доступа) может лишь выдавать разрешения пользователям на доступ к объектам схемы, но сам работать с объектами не может. Владелец схемы, наоборот, может работать с объектами, но не может управлять правами доступа к ним.

https://postgrespro.ru/docs/enterprise/16/ddl-schemas#DDL-SCHEMAS-VA ULT

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

Page 5: Роли

Source slide page 5: Роли{#source-figure-005 caption=“Source slide page 5: Роли”}

Суперпользователь

  • создает роли
  • Администратор ОС
    • запрещает доступ суперпользователя

Комментарии преподавателя

Организацию схемы для размещения защищенных данных начнем с создания ролей суперпользователем.

Роль db_admin будет служить для таких административных задач, как создание баз данных.

Далее нам потребуются роли для владельца защищенной схемы (v_owner) и для администратора безопасности этой схемы (v_admin).

https://postgrespro.ru/docs/enterprise/16/restrict-dbms-admin-data-access# CREATE-MANAGERS-OF-SECURED-SCHEMA

Если эти роли были созданы некоторой административной ролью (не суперпользователем), то у них будет право ADMIN OPTION, от которого они сами отказаться не смогут. Чтобы исключить риски, связанные с доверием к указанным ролям, суперпользователю следует это право отозвать.

https://postgrespro.ru/docs/enterprise/16/restrict-dbms-admin-data-access# PROTECT-SECURED-SCHEMA-USERS-FROM-ADMINS

Наконец, роль v_user предназначена для обычного пользователя, который должен обращаться к защищенным данным.

После создания ролей администратор ОС может запретить суперпользовательское подключение к экземпляру СУБД.

Page 6: Роли

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

=> \conninfo
You are connected to database "student" as user "student" via socket in "/tmp" at port
"5432".
=> CREATE USER db_admin WITH CREATEDB;
CREATE ROLE

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

=> CREATE USER v_owner;
CREATE ROLE
=> CREATE USER v_admin;
CREATE ROLE
=> CREATE USER v_user;
CREATE ROLE

Чтобы пользователь ОС postgres, под которым работает сервер СУБД, не мог изменить конфигурацию, администратор ОС меняет владельца конфигурационных файлов.

student$ sudo chown -c student: /var/lib/pgpro/ent-16/pg_hba.conf*
changed ownership of '/var/lib/pgpro/ent-16/pg_hba.conf' from postgres:postgres to
student:student
changed ownership of '/var/lib/pgpro/ent-16/pg_hba.conf.d' from postgres:postgres to
student:student

Но postgres все же нужен доступ на чтение и просмотр подкаталога:

student$ sudo chmod -c a+rX /var/lib/pgpro/ent-16/pg_hba.conf{,.d}
mode of '/var/lib/pgpro/ent-16/pg_hba.conf' changed from 0640 (rw-r-----) to 0644
(rw-r--r--)
mode of '/var/lib/pgpro/ent-16/pg_hba.conf.d' changed from 0750 (rwxr-x---) to 0755
(rwxr-xr-x)

Теперь можно запретить доступ суперпользователя к СУБД:

/var/lib/pgpro/ent-16/pg_hba.conf.d/pgpro_vault.conf
# TYPE  DATABASE  USER              ADDRESS  METHOD
local   all       postgres,student           reject
host    all       postgres,student  all      reject

Перечитаем конфигурацию и попробуем переподключиться:

student$ sudo systemctl reload postgrespro-ent-16.service
=> \c
\connect: connection to server on socket "/tmp/.s.PGSQL.5432" failed: FATAL:  pg_hba.conf
rejects connection for host "[local]", user "student", database "student", no encryption

Суперпользователь подключиться не может.

Page 7: База данных

Source slide page 7: База данных{#source-figure-007 caption=“Source slide page 7: База данных”}

Суперпользователь

  • создает роли
  • Администратор ОС
    • запрещает доступ суперпользователя
  • Административная роль
    • создает базу

Комментарии преподавателя

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

Page 8: Схема

Source slide page 8: Схема{#source-figure-008 caption=“Source slide page 8: Схема”}

Суперпользователь

  • создает роли
  • Администратор ОС
    • запрещает доступ суперпользователя
  • Административная роль
    • создает базу
  • Суперпользователь (временный доступ)
    • создает схему
    • назначает администратора безопасности
    • назначает владельца

Комментарии преподавателя

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

Команда ALTER SCHEMA … SECURITY OFFICER TO … устанавливает для схемы атрибут — роль администратора безопасности. После этого только указанная роль может управлять доступом к схеме и объектам в ней — схема становится защищенной.

Атрибут можно сбросить командой ALTER SCHEMA … RESET SECURITY OFFICER. Тогда схема снова станет незащищенной.

https://postgrespro.ru/docs/enterprise/16/sql-alterschema

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

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

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

Page 9: Защищенная схема

Подключимся к серверу под административной ролью:

student$ psql -U db_admin -d postgres

Создадим базу данных:

db_admin=> CREATE DATABASE pgpro_vault;
CREATE DATABASE

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

/tmp/v_schema.sql
-- схема для защищаемых данных
CREATE SCHEMA v_schema;
-- администратор безопасности
ALTER SCHEMA v_schema SECURITY OFFICER TO v_admin;
-- право обращаться к объектам схемы
GRANT USAGE ON SCHEMA v_schema TO v_admin;
-- владелец данных
ALTER SCHEMA v_schema OWNER TO v_owner;
student$ sudo systemctl stop postgrespro-ent-16.service
postgres$ /opt/pgpro/ent-16/bin/postgres \
  --single \
  -D /var/lib/pgpro/ent-16 \
  pgpro_vault \
< /tmp/v_schema.sql

Снова запустим сервер в обычном режиме, подключимся к базе и посмотрим на схему — она защищенная (столбец Security officer):

student$ sudo systemctl start postgrespro-ent-16.service
student$ psql -U db_admin -d pgpro_vault
db_admin=> \dn+ v_schema
                             List of schemas
   Name   |  Owner  | Security officer | Access privileges  | Description
----------+---------+------------------+--------------------+-------------
 v_schema | v_owner | v_admin          | v_owner=UC/v_owner+|
          |         |                  | v_admin=U/v_owner  |
(1 row)

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

Page 10: Защищаемые данные

Source slide page 10: Защищаемые данные{#source-figure-010 caption=“Source slide page 10: Защищаемые данные”}

Владелец схемы

  • размещает данные в схеме

Комментарии преподавателя

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

Разместить данные в защищенной схеме можно двумя способами:

  1. Владелец защищенной схемы может создать объект непосредственно в этой схеме и затем наполнить его данными.

https://postgrespro.ru/docs/enterprise/16/restrict-dbms-admin-data-acces s#CREATE-TABLE-IN-SECURED-SCHEMA

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

https://postgrespro.ru/docs/enterprise/16/restrict-dbms-admin-data-acces s#MOVE-TABLE-TO-SECURED-SCHEMA

Page 11: Доступ к данным

Source slide page 11: Доступ к данным{#source-figure-011 caption=“Source slide page 11: Доступ к данным”}

Владелец схемы

  • размещает данные в схеме
  • Администратор безопасности
    • выдает разрешения пользователям

Комментарии преподавателя

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

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

Page 12: Данные в защищенной схеме

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

db_admin=> SELECT rn.rolname AS role, mn.rolname member, gn.rolname grantor, am.admin_option
FROM pg_auth_members am
  JOIN pg_roles rn ON rn.oid = am.roleid
  JOIN pg_roles mn ON mn.oid = am.member
  JOIN pg_roles gn ON gn.oid = am.grantor
WHERE rn.rolname IN ('v_user','v_admin','v_owner')
  AND am.admin_option
ORDER BY member;
 role | member | grantor | admin_option
------+--------+---------+--------------
(0 rows)

Никто — это правильно. Теперь владелец схемы создает в защищенной схеме таблицу:

student$ psql -U v_owner -d pgpro_vault
v_owner=> CREATE TABLE v_schema.tbl AS (SELECT 'vault data by ' || current_user AS data);
SELECT 1

Но он не может выдавать права на доступ к данным в защищенной схеме:

v_owner=> GRANT ALL ON SCHEMA v_schema TO v_user;
WARNING:  no privileges were granted for "v_schema"
GRANT
v_owner=> GRANT ALL ON TABLE v_schema.tbl TO v_user;
WARNING:  no privileges were granted for "tbl"
GRANT

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

student$ psql -U v_admin -d pgpro_vault
v_admin=> GRANT ALL ON SCHEMA v_schema TO v_user;
GRANT
v_admin=> \dn+ v_schema
                             List of schemas
   Name   |  Owner  | Security officer | Access privileges  | Description
----------+---------+------------------+--------------------+-------------
 v_schema | v_owner | v_admin          | v_owner=UC/v_owner+|
          |         |                  | v_admin=U/v_owner +|
          |         |                  | v_user=UC/v_owner  |
(1 row)
v_admin=> GRANT ALL ON TABLE v_schema.tbl TO v_user;
GRANT
v_admin=> \dp v_schema.tbl
                                Access privileges
  Schema  | Name | Type  |    Access privileges    | Column privileges | Policies
----------+------+-------+-------------------------+-------------------+----------
 v_schema | tbl  | table | v_owner=arwdDxt/v_owner+|                   |
          |      |       | v_user=arwdDxt/v_owner  |                   |
(1 row)

Теперь пользователь может, например, читать строки таблицы:

db_admin=> \c - v_user

Page 13: Page 13

Source slide page 13: Page 13{#source-figure-013 caption=“Source slide page 13: Page 13”}

You are now connected to database "pgpro_vault" as user "v_user".
v_user=> SELECT * FROM v_schema.tbl;
         data
-----------------------
 vault data by v_owner
(1 row)

Сам администратор безопасности не имеет доступа к данным в защищенной схеме:

v_admin=> SELECT * FROM v_schema.tbl;
ERROR:  permission denied for table tbl

И не может выдать права ни себе, ни псевдороли public:

v_admin=> GRANT ALL ON TABLE v_schema.tbl TO v_admin, public;
GRANT

Права остались без изменений:

v_admin=> \dp v_schema.tbl
                                Access privileges
  Schema  | Name | Type  |    Access privileges    | Column privileges | Policies
----------+------+-------+-------------------------+-------------------+----------
 v_schema | tbl  | table | v_owner=arwdDxt/v_owner+|                   |
          |      |       | v_user=arwdDxt/v_owner  |                   |
(1 row)

Page 14: Логическая копия

Source slide page 14: Логическая копия{#source-figure-014 caption=“Source slide page 14: Логическая копия”}

Администратор

  • выгружает базу и все схемы, кроме защищенной

Комментарии преподавателя

Логическая резервная копия базы с зоной повышенной безопасности создается в несколько этапов без участия суперпользователя.

Сначала с помощью pg_dump администратор базы выгружает все данные, кроме данных защищенной схемы. Для этого используется параметр командной строки утилиты –exclude-schema.

https://postgrespro.ru/docs/enterprise/16/restrict-dbms-admin-data-access# BACKUP-APPLICATION-CHANGES

Page 15: Логическая копия

Source slide page 15: Логическая копия{#source-figure-015 caption=“Source slide page 15: Логическая копия”}

Администратор

  • выгружает базу и все схемы, кроме защищенной
  • Владелец схемы
    • выгружает данные защищенной схемы

Комментарии преподавателя

Затем владелец защищенной схемы выгружает данные зоны повышенной безопасности, используя параметры –schema и –no-privileges.

Page 16: Логическая копия

Source slide page 16: Логическая копия{#source-figure-016 caption=“Source slide page 16: Логическая копия”}

Администратор

  • выгружает базу и все схемы, кроме защищенной
  • Владелец схемы
    • выгружает данные защищенной схемы
    • выгружает права на объекты защищенной схемы

Комментарии преподавателя

Далее владелец защищенной схемы выгружает права доступа к объектам этой схемы. Для этого используются параметры –schema и –privileges-only.

Page 17: Логическая копия

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

student$ /opt/pgpro/ent-16/bin/pg_dump -U db_admin -d pgpro_vault \
--exclude-schema=v_schema \
> ~/tmp/data.sql

Затем владелец защищенной схемы выгружает данные этой схемы без прав доступа:

student$ /opt/pgpro/ent-16/bin/pg_dump -U v_owner -d pgpro_vault \
--schema=v_schema \
--no-privileges \
> ~/tmp/v_data.sql

После этого он же выгружает права доступа защищенной схемы:

student$ /opt/pgpro/ent-16/bin/pg_dump -U v_owner -d pgpro_vault \
--schema v_schema \
--privileges-only \
> ~/tmp/v_privs.sql

Page 18: Восстановление

Администратор

  • восстанавливает базу и схемы, кроме защищенной
  • v_db
  • схема
  • схема
  • схема

Комментарии преподавателя

Восстановление также выполняется в несколько этапов.

Сначала администратор СУБД восстанавливает базу со всеми данными, кроме данных защищенной схемы.

Page 19: Восстановление

Администратор

  • восстанавливает базу и схемы, кроме защищенной
  • v_db
  • Суперпользователь
    • схема
    • схема
    • схема
    • создает защищенную схему
    • v_schema

Комментарии преподавателя

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

Page 20: Восстановление

Source slide page 20: Восстановление{#source-figure-020 caption=“Source slide page 20: Восстановление”}

Администратор

  • восстанавливает базу и схемы, кроме защищенной
  • Суперпользователь
    • создает защищенную схему
  • Владелец схемы
    • восстанавливает данные защищенной схемы

Комментарии преподавателя

Далее владелец защищенной схемы восстанавливает объекты и данные этой схемы.

Page 21: Восстановление

Source slide page 21: Восстановление{#source-figure-021 caption=“Source slide page 21: Восстановление”}

Администратор

  • восстанавливает базу и схемы, кроме защищенной
  • Суперпользователь
    • создает защищенную схему
  • Владелец схемы
    • восстанавливает данные защищенной схемы
  • Администратор безопасности
    • восстанавливает права на объекты
    • защищенной схемы

Комментарии преподавателя

На завершающем этапе администратор безопасности восстанавливает права доступа к объектам защищенной схемы.

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

Снова отметим, что назначение администратора безопасности желательно выполнять раньше, чем назначение владельца схемы.

Page 22: Восстановление логической копии

Для восстановления защищенной схемы понадобятся два разных администратора: один из них сможет восстановить объекты, а другой — права доступа. Сначала администратор базы данных восстанавливает все данные, кроме защищенной схемы. Мы будем использовать тот же экземпляр, но создадим в нем новую базу:

student$ psql -U db_admin -d pgpro_vault -c 'CREATE DATABASE pgpro_vault_new'
CREATE DATABASE
student$ psql -U db_admin -d pgpro_vault_new -f ~/tmp/data.sql
SET
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
SET

Администратор ОС временно открывает вход для суперпользователя (переименуем дополнительный файл конфигурации):

student$ mv -v /var/lib/pgpro/ent-16/pg_hba.conf.d/pgpro_vault.{conf,save}
renamed '/var/lib/pgpro/ent-16/pg_hba.conf.d/pgpro_vault.conf' ->
'/var/lib/pgpro/ent-16/pg_hba.conf.d/pgpro_vault.save'
student$ sudo systemctl reload postgrespro-ent-16.service

Суперпользователь создает в новой базе схему, назначает ей администратора безопасности (схема становится защищенной) и меняет владельца на v_owner:

student$ psql -d pgpro_vault_new -c 'CREATE SCHEMA v_schema'
CREATE SCHEMA
student$ psql -d pgpro_vault_new -c 'ALTER SCHEMA v_schema SECURITY OFFICER TO v_admin'
ALTER SCHEMA
student$ psql -d pgpro_vault_new -c 'ALTER SCHEMA v_schema OWNER TO v_owner'
ALTER SCHEMA

Также суперпользователь выдает администратору безопасности право обращаться к объектам в защищенной схеме новой базы — оно понадобится при восстановлении привилегий:

student$ psql -d pgpro_vault_new -c 'GRANT USAGE ON SCHEMA v_schema TO v_admin'
GRANT

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

student$ mv -v /var/lib/pgpro/ent-16/pg_hba.conf.d/pgpro_vault.{save,conf}
renamed '/var/lib/pgpro/ent-16/pg_hba.conf.d/pgpro_vault.save' ->
'/var/lib/pgpro/ent-16/pg_hba.conf.d/pgpro_vault.conf'
student$ sudo systemctl reload postgrespro-ent-16.service

В логической копии данных есть команда создания схемы — удалим ее, чтобы исключить ошибку при восстановлении:

student$ sed -i '/CREATE SCHEMA/d' ~/tmp/v_data.sql

Владелец защищенной схемы восстанавливает в ней данные:

student$ psql -U v_owner -d pgpro_vault_new -f ~/tmp/v_data.sql

Page 23: Page 23

SET
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
SET
ALTER SCHEMA
SET
SET
CREATE TABLE
ALTER TABLE
COPY 1

После этого администратор безопасности восстанавливает права доступа для защищенной схемы:

student$ psql -U v_admin -d pgpro_vault_new -f ~/tmp/v_privs.sql
SET
SET
SET
SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
SET
GRANT
GRANT
ALTER SCHEMA
GRANT

Проверим, что получилось:

v_user=> \c pgpro_vault_new
You are now connected to database "pgpro_vault_new" as user "v_user".
v_user=> \dn+ v_schema
                             List of schemas
   Name   |  Owner  | Security officer | Access privileges  | Description
----------+---------+------------------+--------------------+-------------
 v_schema | v_owner | v_admin          | v_owner=UC/v_owner+|
          |         |                  | v_admin=U/v_owner +|
          |         |                  | v_user=UC/v_owner  |
(1 row)
v_user=> \dp v_schema.tbl
                                Access privileges
  Schema  | Name | Type  |    Access privileges    | Column privileges | Policies
----------+------+-------+-------------------------+-------------------+----------
 v_schema | tbl  | table | v_owner=arwdDxt/v_owner+|                   |
          |      |       | v_user=arwdDxt/v_owner  |                   |
(1 row)

Пользователь v_user успешно читает данные из таблицы в новой базе:

student$ psql -U v_user -d pgpro_vault_new -c 'SELECT * FROM v_schema.tbl'
         data
-----------------------
 vault data by v_owner
(1 row)

Page 24: Page 24

Page 25: Итоги

Безопасная работа с данными подразумевает разделение полномочий, в том числе административных

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

Page 26: Практика

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

Комментарии преподавателя

  1. Параметр членства ADMIN дает возможность включения в роль других ролей с правом наследования доступа к объектам.

  2. Перенесите таблицу, созданную в схеме public административной ролью, не имеющей доступа к защищенной схеме.

Page 27: 1. Уязвимость ADMIN OPTION

Создадим административную роль (администратора СУБД) с правом создания ролей, а также роли владельца защищаемых данных и будущего администратора безопасности. Это делает суперпользователь:

=> \conninfo
You are connected to database "student" as user "student" via socket in "/tmp" at port
"5432".
=> CREATE USER dbms_admin WITH CREATEROLE;
CREATE ROLE
=> CREATE USER v_owner;
CREATE ROLE
=> CREATE USER v_admin;
CREATE ROLE

Также он создает базу данных и схему в ней:

=> CREATE DATABASE pgpro_vault OWNER dbms_admin;
CREATE DATABASE
=> \c pgpro_vault
You are now connected to database "pgpro_vault" as user "student".
=> CREATE SCHEMA v_schema;
CREATE SCHEMA

Назначает администратора безопасности схемы:

=> ALTER SCHEMA v_schema SECURITY OFFICER TO v_admin;
ALTER SCHEMA

И предоставляет ему право обращаться к объектам в этой схеме:

=> GRANT USAGE ON SCHEMA v_schema TO v_admin;
GRANT

А также делает владельцем схемы v_schema пользователя v_owner:

=> ALTER SCHEMA v_schema OWNER TO v_owner;
ALTER SCHEMA

Администратор СУБД создает пользователя, который будет работать с данными в защищенной схеме:

=> \c - dbms_admin
You are now connected to database "pgpro_vault" as user "dbms_admin".
dbms_admin=> CREATE USER v_user;
CREATE ROLE

Владелец защищенной схемы создает в ней таблицу и помещает туда данные:

student$ psql -U v_owner -d pgpro_vault
v_owner=> CREATE TABLE v_schema.tbl (
  id int GENERATED ALWAYS AS IDENTITY,
  note text,
  who name DEFAULT current_user
);
CREATE TABLE
v_owner=> INSERT INTO v_schema.tbl (note) VALUES ('Line 1');
INSERT 0 1

Администратор безопасности предоставляет пользователю v_user доступ к схеме и таблице:

student$ psql -U v_admin -d pgpro_vault

Page 28: Page 28

Source slide page 28: Page 28{#source-figure-028 caption=“Source slide page 28: Page 28”}

v_admin=> GRANT ALL ON SCHEMA v_schema TO v_user;
GRANT
v_admin=> GRANT ALL ON TABLE v_schema.tbl TO v_user;
GRANT

Получается так:

v_admin=> \dn+ v_schema
                             List of schemas
   Name   |  Owner  | Security officer | Access privileges  | Description
----------+---------+------------------+--------------------+-------------
 v_schema | v_owner | v_admin          | v_owner=UC/v_owner+|
          |         |                  | v_admin=U/v_owner +|
          |         |                  | v_user=UC/v_owner  |
(1 row)
v_admin=> \dp v_schema.tbl
                                Access privileges
  Schema  | Name | Type  |    Access privileges    | Column privileges | Policies
----------+------+-------+-------------------------+-------------------+----------
 v_schema | tbl  | table | v_owner=arwdDxt/v_owner+|                   |
          |      |       | v_user=arwdDxt/v_owner  |                   |
(1 row)

Теперь пользователь может работать с данными в защищенной схеме:

student$ psql -U v_user -d pgpro_vault
v_user=> INSERT INTO v_schema.tbl (note) VALUES ('Line 2');
INSERT 0 1

Администратор dbms_admin замечает, что может воспользоваться ADMIN OPTION в отношении созданной им роли v_user:

dbms_admin=> \drg
             List of role grants
 Role name  | Member of | Options | Grantor
------------+-----------+---------+----------
 dbms_admin | v_user    | ADMIN   | postgres
(1 row)

Обладая параметром членства ADMIN в роли v_user, администратор dbms_admin может включить себя в эту роль с параметром INHERIT:

dbms_admin=> GRANT v_user TO dbms_admin WITH INHERIT OPTION;
GRANT ROLE
dbms_admin=> \drg
                List of role grants
 Role name  | Member of |   Options    |  Grantor
------------+-----------+--------------+------------
 dbms_admin | v_user    | INHERIT, SET | dbms_admin
 dbms_admin | v_user    | ADMIN        | postgres
(2 rows)

Теперь у него есть такой же доступ к данным в защищенной схеме, как у доверенного пользователя v_user:

dbms_admin=> SELECT * FROM v_schema.tbl;
 id |  note  |   who
----+--------+---------
  1 | Line 1 | v_owner
  2 | Line 2 | v_user
(2 rows)

Чтобы исключить эту уязвимость, суперпользователь отзывает ADMIN OPTION у роли dbms_admin:

dbms_admin=> \c - postgres
You are now connected to database "pgpro_vault" as user "postgres".

Page 29: Page 29

=> REVOKE ADMIN OPTION FOR v_user FROM dbms_admin CASCADE;
REVOKE ROLE
=> \drg v_user
            List of role grants
 Role name | Member of | Options | Grantor
-----------+-----------+---------+---------
(0 rows)

Теперь доступ к данным закрыт:

=> \c - dbms_admin
You are now connected to database "pgpro_vault" as user "dbms_admin".
dbms_admin=> SELECT * FROM v_schema.tbl;
ERROR:  permission denied for schema v_schema
LINE 1: SELECT * FROM v_schema.tbl;
                      ^

И исключена возможность включения в роль:

dbms_admin=> GRANT v_user TO dbms_admin;
ERROR:  permission denied to grant role "v_user"
DETAIL:  Only roles with the ADMIN option on role "v_user" may grant this role.

2. Перенос данных в защищенную схему

Администратор СУБД создает новую таблицу в схеме public и наполняет ее данными:

dbms_admin=> CREATE TABLE tbl_bis (
  id int GENERATED ALWAYS AS IDENTITY,
  note text,
  who name DEFAULT current_user
);
CREATE TABLE
dbms_admin=> INSERT INTO tbl_bis (note) VALUES ('Line 1');
INSERT 0 1

В дальнейшем в таблице будут храниться конфиденциальные данные, поэтому стоит задача поместить ее в зону повышенной безопасности. Для этого роль dbms_admin передаст таблицу во владение пользователю v_owner, который перенесет ее в свою защищенную схему:

dbms_admin=> ALTER TABLE tbl_bis OWNER TO v_owner;
ERROR:  must be able to SET ROLE "v_owner"

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

dbms_admin=> \c - postgres
You are now connected to database "pgpro_vault" as user "postgres".
=> ALTER TABLE tbl_bis OWNER TO v_owner;
ALTER TABLE

Новый владелец перемещает таблицу в защищенную схему:

v_owner=> ALTER TABLE tbl_bis SET SCHEMA v_schema;
ALTER TABLE

Администратор безопасности предоставляет доступ к таблице пользователю v_user:

v_admin=> GRANT SELECT ON TABLE v_schema.tbl_bis TO v_user;
GRANT

Теперь пользователь работает с данными:

v_user=> SELECT * FROM v_schema.tbl_bis;

Page 30: Page 30

 id |  note  |    who
----+--------+------------
  1 | Line 1 | dbms_admin
(1 row)

А у роли dbms_admin доступа к таблице уже нет:

=> \c - dbms_admin
You are now connected to database "pgpro_vault" as user "dbms_admin".
dbms_admin=> SELECT * FROM v_schema.tbl_bis;
ERROR:  permission denied for schema v_schema
LINE 1: SELECT * FROM v_schema.tbl_bis;
                      ^

3. Схема без защиты

Суперпользователь делает схему v_schema обычной, убрав атрибут security officer:

dbms_admin=> \c - postgres
You are now connected to database "pgpro_vault" as user "postgres".
=> ALTER SCHEMA v_schema RESET SECURITY OFFICER;
ALTER SCHEMA
=> \dn+ v_schema
                             List of schemas
   Name   |  Owner  | Security officer | Access privileges  | Description
----------+---------+------------------+--------------------+-------------
 v_schema | v_owner |                  | v_owner=UC/v_owner+|
          |         |                  | v_admin=U/v_owner +|
          |         |                  | v_user=UC/v_owner  |
(1 row)

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

v_owner=> GRANT USAGE ON SCHEMA v_schema TO dbms_admin;
GRANT
v_owner=> GRANT SELECT ON TABLE v_schema.tbl, v_schema.tbl_bis TO dbms_admin;
GRANT

Администратор СУБД может видеть данные в таблицах схемы v_schema:

=> \c - dbms_admin
You are now connected to database "pgpro_vault" as user "dbms_admin".
dbms_admin=> SELECT * FROM v_schema.tbl;
 id |  note  |   who
----+--------+---------
  1 | Line 1 | v_owner
  2 | Line 2 | v_user
(2 rows)
dbms_admin=> SELECT * FROM v_schema.tbl_bis;
 id |  note  |    who
----+--------+------------
  1 | Line 1 | dbms_admin
(1 row)

Уборка

Удалим созданную базу данных и роли.

dbms_admin=> \c student student
You are now connected to database "student" as user "student".
student=# DROP DATABASE pgpro_vault WITH (FORCE);

Page 31: Page 31

DROP DATABASE
student=# DROP USER dbms_admin, v_owner, v_admin, v_user;
DROP ROLE

17 Занятие 17: Безопасность / Анонимизация данных

Исходная страница в нативном Markdown: Безопасность / Анонимизация данных
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_17_anonymize.html Archive: PGPRO-handouts-16.zip Страниц PDF: 23

Page 1: Безопасность / Анонимизация данных

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Анонимизация
  • Стратегии анонимизации
  • Расширение pgpro_anonymizer
  • Правила маскирования
  • Метки безопасности
  • Динамическое изменение данных
  • Статическое изменение по правилам
  • Безопасность

Page 3: Анонимизация

Source slide page 3: Анонимизация{#source-figure-003 caption=“Source slide page 3: Анонимизация”}

Статическое Динамическое изменение изменение

Комментарии преподавателя

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

Дополнительным средством защиты данных в таких случаях может стать анонимизация — сохранение конфиденциальности путем изменения исходных данных или их представления в измененном виде:

● Статическое изменение — сохранение анонимизированных данных и дальнейшее их предоставление в уже измененном виде.

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

● Динамическое изменение (маскирование) — исходные данные не изменяются, а анонимизация и представление в измененном виде выполняются «на лету», непосредственно при обращении к ним.

Такой вариант может применяться, например, для защиты данных от просмотра недоверенными пользователями в приложениях.

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

Page 4: Стратегии анонимизации

Удаление

  • Замена
  • Искажение
  • Рандомизация и подмена
  • Псевдонимизация
  • Обобщение
  • Перестановка
  • Пользовательские правила

Комментарии преподавателя

Для анонимизации данных могут использоваться разные стратегии:

● Удаление (сокрытие) полностью исключает доступ к данным.

● Замена — все значения заменяются одним. Возможна частичная замена, когда часть данных остается нетронутой.

● Искажение (отклонение) «сдвигает» даты и числовые значения.

● Рандомизация и подмена (фальсификация) заменяют конфиденциальные данные случайными, но правдоподобными значениями.

● Псевдонимизация защищает данные, скрывая их с помощью дополнительной информации, при этом данные остаются связаны с исходными. Примерами могут служить шифрование и хеширование.

● Обобщение заменяет данные диапазоном значений.

● Перестановка перемешивает значения в рамках столбца.

● Пользовательские правила предназначены для анонимизации данных в соответствии с особыми требованиями — например, при необходимости логически согласованных изменений в столбцах.

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

Page 5: pgpro_anonymizer

Инструменты внутри СУБД

  • для статического и динамического изменения
  • Набор функций
    • для разных стратегий анонимизации
  • Декларативные конструкции

Комментарии преподавателя

Расширение pgpro_anonymizer позволяет анонимизировать данные внутри экземпляра СУБД без использования внешних инструментов, что уменьшает риск утечки данных. Расширение поставляется в виде отдельного пакета pgpro-anonymizer-ent-16; за его основу был взят проект PostgreSQL Anonymizer.

https://postgrespro.ru/docs/enterprise/16/pgpro-anonymizer#PGPRO-ANON YMIZER

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

https://postgrespro.ru/docs/enterprise/16/pgpro-anonymizer#MASKING-FUN CTIONS

Чтобы связать данные со стратегией анонимизации, используется декларативный подход: стратегия описывается с помощью DDL-команд.

Page 6: Статическое изменение с помощью функций

Для работы расширения требуется загрузить разделяемую библиотеку.

=> ALTER SYSTEM SET shared_preload_libraries = 'anon';
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service

Будем работать с демобазой. Подключимся к ней как суперпользователь:

student$ psql demo

Создадим расширение:

=> CREATE EXTENSION anon;
CREATE EXTENSION

Появилась одноименная схема, в которой расширение хранит свои объекты:

=> \dn anon
List of schemas
 Name |  Owner
------+---------
 anon | student
(1 row)

Создадим схему, в которой будем размещать анонимизированные данные:

=> CREATE SCHEMA m_schema;
CREATE SCHEMA

Для удобства временно добавим эту схему в путь поиска:

=> SELECT set_config('search_path', 'm_schema, ' || current_setting('search_path'), false);
         set_config
----------------------------
 m_schema, bookings, public
(1 row)

Считаем, что нам необходимо создать небольшой набор данных для тестировщиков, в котором нужно скрыть имена и фамилии пассажиров и исказить даты и суммы бронирований. Создадим в схеме m_schema таблицы m_tickets и m_bookings, в которые скопируем часть данных из таблиц tickets и bookings схемы bookings:

=> CREATE TABLE m_tickets AS
  SELECT * FROM tickets ORDER BY book_ref LIMIT 5;
SELECT 5
=> CREATE TABLE m_bookings AS
  SELECT * FROM bookings WHERE book_ref IN (SELECT book_ref from m_tickets);
SELECT 4

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

=> SELECT proname FROM pg_proc
WHERE pronamespace = 'anon'::regnamespace AND proname ~ 'fake|random';

Page 7: Page 7

        proname
-----------------------
 random
 random_date_between
 random_date
 random_int_between
 random_bigint_between
 random_string
 random_zip
 random_in_daterange
 random_in_int4range
 random_in_int8range
 random_in_numrange
 random_in_tsrange
 random_in_tstzrange
 random_phone
 random_hash
 random_in
 random_in_enum
 fake_first_name
 fake_last_name
 fake_email
 fake_address
 fake_city
 fake_company
 fake_country
 fake_iban
 fake_postcode
 fake_siret
(27 rows)

Функции подмены случайным образом выбирают значения из таблиц-словарей, также расположенных в схеме anon:

=> \dt anon.*
                List of relations
 Schema |         Name         | Type  |  Owner
--------+----------------------+-------+---------
 anon   | address              | table | student
 anon   | city                 | table | student
 anon   | company              | table | student
 anon   | country              | table | student
 anon   | email                | table | student
 anon   | first_name           | table | student
 anon   | iban                 | table | student
 anon   | identifier           | table | student
 anon   | identifiers_category | table | student
 anon   | last_name            | table | student
 anon   | lorem_ipsum          | table | student
 anon   | postcode             | table | student
 anon   | siret                | table | student
(13 rows)

Функция init импортирует в эти таблицы наборы случайных данных. Они невелики — 1000 значений для каждой категории, на английском языке.

=> SELECT anon.init();
 init
------
 t
(1 row)

Изменим данные в столбце passenger_name, используя значения, возвращаемые функциями fake_first_name и fake_last_name:

=> UPDATE m_tickets
SET passenger_name = anon.concat(anon.fake_first_name(), ' ', anon.fake_last_name());
UPDATE 5

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

=> SELECT anon.shuffle_column('m_tickets', 'book_ref', 'ticket_no');

Page 8: Page 8

 shuffle_column
----------------
 t
(1 row)

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

=> SELECT anon.add_noise_on_datetime_column('m_bookings', 'book_date', '3 days 1 hour');
 add_noise_on_datetime_column
------------------------------
 t
(1 row)
=> SELECT anon.add_noise_on_numeric_column('m_bookings', 'total_amount', '0.2');
 add_noise_on_numeric_column
-----------------------------
 t
(1 row)
=> UPDATE m_bookings SET total_amount = round(total_amount, -2); -- уберем лишние разряды
UPDATE 4
=> UPDATE m_bookings SET book_date = date_trunc('minute', book_date); -- обрежем до минут
UPDATE 4

Исходные данные — бронирования и билеты:

=> SELECT book_ref, book_date, passenger_name, total_amount
FROM bookings
  JOIN tickets USING (book_ref)
ORDER BY book_ref LIMIT 5;
 book_ref |       book_date        |  passenger_name  | total_amount
----------+------------------------+------------------+--------------
 00000F   | 2017-07-05 03:12:00+03 | ANNA ANTONOVA    |    265700.00
 000012   | 2017-07-14 09:02:00+03 | TAMARA ZAYCEVA   |     37900.00
 000068   | 2017-08-15 14:27:00+03 | TATYANA PETROVA  |     18100.00
 000181   | 2017-08-10 13:28:00+03 | EVGENIYA KARPOVA |    131800.00
 000181   | 2017-08-10 13:28:00+03 | ALEKSANDR ZHUKOV |    131800.00
(5 rows)

Измененные данные:

=> SELECT book_ref, book_date, passenger_name, total_amount
FROM m_bookings
  JOIN m_tickets USING (book_ref)
ORDER BY book_ref;
 book_ref |       book_date        | passenger_name | total_amount
----------+------------------------+----------------+--------------
 00000F   | 2017-07-06 09:59:00+03 | Connor Fritz   |    248100.00
 000012   | 2017-07-15 17:13:00+03 | Joan Stein     |     45400.00
 000068   | 2017-08-17 14:35:00+03 | Colton Gilmore |     20900.00
 000181   | 2017-08-12 03:47:00+03 | Monique Norton |    156000.00
 000181   | 2017-08-12 03:47:00+03 | Karl Valdez    |    156000.00
(5 rows)

Данные бронирований и билетов анонимизированы.

Page 9: Метки и правила

Метки безопасности

  • штатный универсальный механизм хранения атрибутов объектов
  • назначаются администратором
  • интерпретируются модулем — провайдером меток
  • Правила изменения данных
    • описываются меткой безопасности для провайдера anon
    • на уровне столбцов таблиц
    • определяют преобразование: замену на константу или значение
    • функции

Комментарии преподавателя

Штатный механизм меток безопасности PostgreSQL позволяет ассоциировать метки с отдельными объектами, в том числе и со столбцами таблиц.

Метка — это атрибут объекта (текстовая строка), который устанавливается администратором, а интерпретируется при обращении к объекту загружаемым модулем — провайдером. Провайдер также определяет, допустима ли метка и разрешено ли применять ее к указанному объекту. PostgreSQL не накладывает ограничений на то, как провайдер должен интерпретировать метки безопасности; он лишь обеспечивает механизм их хранения и поддержку работы модулей-провайдеров. На базе этого механизма может быть реализована, например, система мандатного управления доступом (MAC).

https://postgrespro.ru/docs/enterprise/16/sql-security-label

Для расширения pgpro_anonymizer провайдером меток безопасности выступает модуль anon.

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

https://postgrespro.ru/docs/enterprise/16/pgpro-anonymizer#DECLARE-MA SKING-RULES

Page 10: Динамическое изменение

Работает для ролей, помеченных как недоверенные

  • Обращение к таблице подменяется на обращение к представлению
    • anon.sourceschema — схема исходных данных
    • anon.maskschema — схема анонимизирующих представлений
  • Ограничения и особенности
    • поддержка только одной схемы
    • необходимость каскадного удаления таблиц
    • запрет прямого изменения типа данных
    • возможное снижение производительности

Комментарии преподавателя

Динамическое маскирование работает для ролей с установленной меткой безопасности MASKED (недоверенных ролей).

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

https://postgrespro.ru/docs/enterprise/16/pgpro-anonymizer#DYNAMIC-MA SKING

У динамического маскирования есть ряд особенностей и ограничений:

● Чтобы удалить исходную замаскированную таблицу, для нее нужно выполнять DROP … CASCADE, что позволяет сразу удалить и зависящее от нее маскирующее представление.

● Система работает только с одной схемой, указанной в параметре anon.sourceschema (по умолчанию — public).

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

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

Page 11: Динамическое изменение

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

=> CREATE USER looser;
CREATE ROLE
=> SECURITY LABEL FOR anon ON ROLE looser IS 'MASKED';
SECURITY LABEL

Установленные метки безопасности объектов можно увидеть в системном каталоге:

=> SELECT classoid::regclass, objoid::regrole, provider, label FROM pg_shseclabel;
 classoid  | objoid | provider | label
-----------+--------+----------+--------
 pg_authid | looser | anon     | MASKED
(1 row)

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

=> SECURITY LABEL FOR anon ON COLUMN tickets.passenger_id
IS 'MASKED WITH FUNCTION anon.partial(passenger_id, 2, $$** ***$$, 3)';
SECURITY LABEL

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

=> SELECT objoid::regnamespace, provider, label FROM pg_seclabel
WHERE classoid = 'pg_namespace'::regclass;
   objoid   | provider |  label
------------+----------+---------
 anon       | anon     | TRUSTED
 pg_catalog | anon     | TRUSTED
(2 rows)
=> SHOW anon.restrict_to_trusted_schemas;
 anon.restrict_to_trusted_schemas
----------------------------------
 on
(1 row)

Из соображений безопасности в правилах маскирования надо явно указывать имя схемы — это не позволит пользователю-злоумышленнику подменить функцию на собственную:

=> CREATE FUNCTION anon.fake_contact_data() RETURNS jsonb
  RETURN pg_catalog.jsonb_build_object('email', anon.fake_email(), 'phone', anon.random_phone());
CREATE FUNCTION

В правиле для столбца укажем вызов этой функции:

=> SECURITY LABEL FOR anon ON COLUMN tickets.contact_data
IS 'MASKED WITH FUNCTION anon.fake_contact_data()';
SECURITY LABEL

Вот созданные нами метки для столбцов:

=> SELECT objoid::regclass,
  (SELECT attname FROM pg_attribute WHERE attrelid = objoid AND attnum = objsubid),
  provider, label
FROM pg_seclabel
WHERE classoid = 'pg_class'::regclass;

Page 12: Page 12

 objoid  |   attname    | provider |                               label
---------+--------------+----------+------------------------------------------------------
-------------
 tickets | passenger_id | anon     | MASKED WITH FUNCTION anon.partial(passenger_id, 2,
$$** ***$$, 3)
 tickets | contact_data | anon     | MASKED WITH FUNCTION anon.fake_contact_data()
(2 rows)

Более удобный способ получения информации о правилах изменения — представление anon.pg_masking_rules:

=> SELECT relnamespace, relname, attname, format_type, col_description FROM anon.pg_masking_rules \gx
-[ RECORD 1 ]---+------------------------------------------------------------------
relnamespace    | bookings
relname         | tickets
attname         | passenger_id
format_type     | character varying(20)
col_description | MASKED WITH FUNCTION anon.partial(passenger_id, 2, $$** ***$$, 3)
-[ RECORD 2 ]---+------------------------------------------------------------------
relnamespace    | bookings
relname         | tickets
attname         | contact_data
format_type     | jsonb
col_description | MASKED WITH FUNCTION anon.fake_contact_data()

Далее нам необходимо установить исходную (bookings) и анонимизирующую (mask по умолчанию) схемы для базы данных:

=> ALTER DATABASE demo SET anon.sourceschema TO 'bookings';
ALTER DATABASE
=> SHOW anon.maskschema;
 anon.maskschema
-----------------
 mask
(1 row)

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

=> \c
You are now connected to database "demo" as user "student".
=> SELECT anon.start_dynamic_masking();
 start_dynamic_masking
-----------------------
 t
(1 row)

Вызов этой функции: проверит состояние расширения и при необходимости вызовет anon.init; при необходимости создаст схему, указанную в параметре anon.maskschema; для таблиц в sourceschema создаст в maskschema одноименные представления, через которые недоверенные пользователи будут видеть маскированные данные; отзовет все права на sourceschema у всех недоверенных ролей; назначит недоверенным ролям необходимые права на схемы anon и anon.maskschema, а также на объекты в них; установит недоверенным ролям путь поиска к схеме anon.maskschema. Вот список созданных маскирующих представлений:

=> \dv mask.*

Page 13: Page 13

             List of relations
 Schema |      Name       | Type |  Owner
--------+-----------------+------+---------
 mask   | aircrafts_data  | view | student
 mask   | airports_data   | view | student
 mask   | boarding_passes | view | student
 mask   | bookings        | view | student
 mask   | flights         | view | student
 mask   | seats           | view | student
 mask   | ticket_flights  | view | student
 mask   | tickets         | view | student
(8 rows)

Подключаемся к базе от имени недоверенного пользователя и выполняем запрос:

student$ psql -d demo -U looser
looser=> SELECT ticket_no, passenger_id, passenger_name, contact_data FROM tickets LIMIT 5;
   ticket_no   | passenger_id |   passenger_name   |
contact_data
---------------+--------------+--------------------+--------------------------------------
-----------------------------
 0005432000987 | 81** ***011  | VALERIY TIKHONOV   | {"email": "zjohnson@example.net",
"phone": "0129939052"}
 0005432000988 | 84** ***203  | EVGENIYA ALEKSEEVA | {"email": "alexander90@example.org",
"phone": "0360226263"}
 0005432000989 | 10** ***484  | ARTUR GERASIMOV    | {"email":
"christophernorris@example.net", "phone": "0999795800"}
 0005432000990 | 48** ***049  | ALINA VOLKOVA      | {"email": "iharmon@example.org",
"phone": "0412376527"}
 0005432000991 | 66** ***589  | MAKSIM ZHUKOV      | {"email":
"joshuahodges@example.com", "phone": "0534237840"}
(5 rows)

В полученных данных видно, что такая анонимизация никак не учитывает смысловой взаимосвязи столбцов, содержащих имя пассажира и его контактные данные (адрес электронной почты). Для получения более правдоподобных данных можно использовать пользовательские подменяющие функции. Путь поиска и права для недоверенного пользователя изменены таким образом, что вместо схемы bookings ему видны объекты из схемы mask:

looser=> SELECT current_schemas(true);
  current_schemas
-------------------
 {pg_catalog,mask}
(1 row)

Доступа к исходным данным у пользователя looser нет:

looser=> SELECT * FROM bookings.tickets;
ERROR:  permission denied for schema bookings
LINE 1: SELECT * FROM bookings.tickets;
                      ^

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

looser=> \d+ tickets

Page 14: Page 14

                                       View "mask.tickets"
     Column     |         Type          | Collation | Nullable | Default | Storage  |
Description
----------------+-----------------------+-----------+----------+---------+----------+-----
--------
 ticket_no      | character(13)         |           |          |         | extended |
 book_ref       | character(6)          |           |          |         | extended |
 passenger_id   | character varying(20) |           |          |         | extended |
 passenger_name | text                  |           |          |         | extended |
 contact_data   | jsonb                 |           |          |         | extended |
View definition:
 SELECT ticket_no,
    book_ref,
    anon.partial(passenger_id::text, 2, '** ***'::text, 3)::character varying(20) AS
passenger_id,
    passenger_name,
    anon.fake_contact_data() AS contact_data
   FROM bookings.tickets;

Изменим правила маскирования для столбцов. Номер билета будем считать конфиденциальным и скроем его, заменив на константу:

=> SECURITY LABEL FOR anon ON COLUMN tickets.ticket_no
IS 'MASKED WITH VALUE $$PRIVATE$$';
SECURITY LABEL

А для документа, идентифицирующего пассажира, маскирование отменим:

=> SECURITY LABEL FOR anon ON COLUMN tickets.passenger_id
IS NULL;
SECURITY LABEL
looser=> SELECT ticket_no, passenger_id, passenger_name, contact_data FROM tickets LIMIT 5;
   ticket_no   | passenger_id |   passenger_name   |
contact_data
---------------+--------------+--------------------+--------------------------------------
---------------------------
 PRIVATE       | 8149 604011  | VALERIY TIKHONOV   | {"email":
"harmonmatthew@example.com", "phone": "0888938751"}
 PRIVATE       | 8499 420203  | EVGENIYA ALEKSEEVA | {"email": "john90@example.org",
"phone": "0853715519"}
 PRIVATE       | 1011 752484  | ARTUR GERASIMOV    | {"email": "ashley83@example.com",
"phone": "0739380089"}
 PRIVATE       | 4849 400049  | ALINA VOLKOVA      | {"email":
"kimberlyjohnson@example.org", "phone": "0394258303"}
 PRIVATE       | 6615 976589  | MAKSIM ZHUKOV      | {"email": "ydavidson@example.com",
"phone": "0409638085"}
(5 rows)

Page 15: Статическое изменение

Данные в таблицах изменяются по заданным правилам

  • anon.anonymize_database() — все данные в базе
  • anon.anonymize_table() — отдельная таблица
  • anon.anonymize_column() — отдельный столбец таблицы

Комментарии преподавателя

Статическое изменение данных также может выполняться на основе определенных правил маскирования.

https://postgrespro.ru/docs/enterprise/16/pgpro-anonymizer#APPLYING-MA SKING-RULES

Функции anon.anonymize_database, anon.anonymize_table и anon.anonymize_column позволяют преобразовать исходные таблицы в соответствии с заранее определенными правилами.

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

Иногда может оказаться быстрее экспортировать анонимизированные данные и повторно загрузить их в базу данных. Если нужно экспортировать базу вместе с анонимизируемыми данными, следует воспользоваться скриптом pg_dump_anon.sh.

https://postgrespro.ru/docs/enterprise/16/pgpro-anonymizer#ANONYMOUS-DUMPS

Page 16: Статическое изменение по правилам

Сейчас данные в схеме m_schema выглядят следующим образом:

=> SELECT book_ref, book_date, passenger_name, total_amount
FROM m_schema.m_bookings
  JOIN m_schema.m_tickets USING (book_ref)
ORDER BY passenger_name;
 book_ref |       book_date        | passenger_name | total_amount
----------+------------------------+----------------+--------------
 000068   | 2017-08-17 14:35:00+03 | Colton Gilmore |     20900.00
 00000F   | 2017-07-06 09:59:00+03 | Connor Fritz   |    248100.00
 000012   | 2017-07-15 17:13:00+03 | Joan Stein     |     45400.00
 000181   | 2017-08-12 03:47:00+03 | Karl Valdez    |    156000.00
 000181   | 2017-08-12 03:47:00+03 | Monique Norton |    156000.00
(5 rows)

Определим правила, которые скроют номер бронирования, не нарушив взаимосвязь таблиц по столбцу book_ref. Применим хеширование: это преобразование является детерминированным. Вычисленное значение хеша будет слишком длинным…

=> SELECT anon.hash('000181');
                               hash
------------------------------------------------------------------
 2b4c7a7391af921b2049fc14539989e56cb5003c11dd2ce863c06ed1acbc1df9
(1 row)

…поэтому мы идем на то, чтобы сохранить только его часть. Конечно, такое сокращение повысит риск коллизии значений.

=> SECURITY LABEL FOR anon ON COLUMN m_schema.m_tickets.book_ref
IS 'MASKED WITH FUNCTION pg_catalog.left(anon.hash(book_ref), 6)';
SECURITY LABEL
=> SECURITY LABEL FOR anon ON COLUMN  m_schema.m_bookings.book_ref
IS 'MASKED WITH FUNCTION pg_catalog.left(anon.hash(book_ref), 6)';
SECURITY LABEL

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

looser=> SHOW anon.algorithm;
ERROR:  permission denied to examine "anon.algorithm"
DETAIL:  Only roles with privileges of the "pg_read_all_settings" role may examine this
parameter.
looser=> SHOW anon.salt;
ERROR:  permission denied to examine "anon.salt"
DETAIL:  Only roles with privileges of the "pg_read_all_settings" role may examine this
parameter.

Проверим правила:

=> SELECT relnamespace, relname, attname, format_type, col_description FROM anon.pg_masks
WHERE relnamespace = 'm_schema'::regnamespace \gx
-[ RECORD 1 ]---+-------------------------------------------------------------
relnamespace    | m_schema
relname         | m_tickets
attname         | book_ref
format_type     | character(6)
col_description | MASKED WITH FUNCTION pg_catalog.left(anon.hash(book_ref), 6)
-[ RECORD 2 ]---+-------------------------------------------------------------
relnamespace    | m_schema
relname         | m_bookings
attname         | book_ref
format_type     | character(6)
col_description | MASKED WITH FUNCTION pg_catalog.left(anon.hash(book_ref), 6)

Page 17: Page 17

Преобразуем таблицы с применением этих правил:

=> SELECT anon.anonymize_table('m_schema.m_tickets'), anon.anonymize_table('m_schema.m_bookings');
 anonymize_table | anonymize_table
-----------------+-----------------
 t               | t
(1 row)

Результат:

=> SELECT book_ref, book_date, passenger_name, total_amount
FROM m_schema.m_bookings
  JOIN m_schema.m_tickets USING (book_ref)
ORDER BY passenger_name;
 book_ref |       book_date        | passenger_name | total_amount
----------+------------------------+----------------+--------------
 5561d8   | 2017-08-17 14:35:00+03 | Colton Gilmore |     20900.00
 bcc517   | 2017-07-06 09:59:00+03 | Connor Fritz   |    248100.00
 b2836e   | 2017-07-15 17:13:00+03 | Joan Stein     |     45400.00
 2b4c7a   | 2017-08-12 03:47:00+03 | Karl Valdez    |    156000.00
 2b4c7a   | 2017-08-12 03:47:00+03 | Monique Norton |    156000.00
(5 rows)

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

=> SELECT proname, CASE WHEN prosecdef THEN 'DEFINER' ELSE 'INVOKER' END security
FROM pg_proc
WHERE pronamespace = 'anon'::regnamespace AND proname LIKE 'anonymize%';
      proname       | security
--------------------+----------
 anonymize_column   | INVOKER
 anonymize_database | INVOKER
 anonymize_table    | INVOKER
(3 rows)

Page 18: Итоги

Анонимизация позволяет защитить конфиденциальные данные, когда разграничение ролей неприменимо

  • Правила изменения данных определяются декларативно с помощью меток безопасности
  • Поддерживается статическое и динамическое изменение
  • Анонимизация данных влечет накладные расходы

Page 19: Практика

  1. Настройте правило маскирования столбца с контактными данными таблицы m_tickets так, чтобы почтовые адреса содержали имена и фамилии пассажиров. Экспортируйте данные таблицы в анонимизированном виде от лица недоверенного пользователя. 2. Включите динамическое маскирование. Объявите правило для подмены имени пассажира в таблице m_tickets. Выполните несколько запросов к одной из строк таблицы. Затем перепишите правило так, чтобы использовалась псевдонимизация и снова выполните запросы. Сравните результаты.

Комментарии преподавателя

  1. Установите расширение и скопируйте небольшую часть данных в таблицу m_tickets из bookings.tickets так, как это сделано в демонстрации.

Напишите функцию, формирующую контактные данные, в которых почтовый адрес имеет вид имя.фамилия@example.com. Поместите эту функцию в доверенную схему и используйте ее в правиле изменения.

Создайте недоверенного пользователя и сделайте выгрузку данных с помощью скрипта pg_dump_anon.sh от его имени.

  1. Для подмены имен используйте изменчивую функцию anon.fake_first_name. Для псевдонимизации замените ее на anon.pseudo_first_name.

Page 20: 1. Пользовательские правила анонимизации

Установим расширение pgpro_anonymizer в демобазе и инициализируем его.

=> ALTER SYSTEM SET shared_preload_libraries = 'anon';
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql demo
=> CREATE EXTENSION anon;
CREATE EXTENSION
=> SELECT anon.init();
 init
------
 t
(1 row)

Создадим в схеме public таблицу m_tickets, в которую скопируем часть данных из таблицы tickets схемы bookings:

=> CREATE TABLE public.m_tickets AS SELECT * FROM tickets LIMIT 5;
SELECT 5

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

=> CREATE FUNCTION anon.fake_contact_data(name text) RETURNS jsonb
  RETURN pg_catalog.jsonb_build_object(
    'email', (SELECT pg_catalog.concat(nm[1], '.', nm[2], '@example.com')
              FROM pg_catalog.regexp_split_to_array(pg_catalog.lower(name), '\s+') nm),
    'phone', anon.random_phone()
  );
CREATE FUNCTION

Проверим:

=> SELECT anon.fake_contact_data('Vasily Bulkin');
                       fake_contact_data
---------------------------------------------------------------
 {"email": "vasily.bulkin@example.com", "phone": "0828806024"}
(1 row)

В правиле маскирования укажем вызов этой функции:

=> SECURITY LABEL FOR anon ON COLUMN m_tickets.contact_data
IS 'MASKED WITH FUNCTION anon.fake_contact_data(passenger_name)';
SECURITY LABEL

Взглянем на созданное правило:

=> SELECT * FROM anon.pg_masking_rules \gx
-[ RECORD 1 ]----+------------------------------------------------------------
attrelid         | 16913
attnum           | 5
relnamespace     | public
relname          | m_tickets
attname          | contact_data
format_type      | jsonb
col_description  | MASKED WITH FUNCTION anon.fake_contact_data(passenger_name)
masking_function | anon.fake_contact_data(passenger_name)
masking_value    |
priority         | 100
masking_filter   | anon.fake_contact_data(passenger_name)
trusted_schema   | t

И на исходные данные (динамическое маскирование не включено):

=> SELECT * FROM m_tickets;

Page 21: Page 21

   ticket_no   | book_ref | passenger_id |   passenger_name   |
     contact_data
---------------+----------+--------------+--------------------+---------------------------
--------------------------------------------------
 0005432000987 | 06B046   | 8149 604011  | VALERIY TIKHONOV   | {"phone": "+70127117011"}
 0005432000988 | 06B046   | 8499 420203  | EVGENIYA ALEKSEEVA | {"phone": "+70378089255"}
 0005432000989 | E170C3   | 1011 752484  | ARTUR GERASIMOV    | {"phone": "+70760429203"}
 0005432000990 | E170C3   | 4849 400049  | ALINA VOLKOVA      | {"email":
"volkova.alina_03101973@postgrespro.ru", "phone": "+70582584031"}
 0005432000991 | F313DD   | 6615 976589  | MAKSIM ZHUKOV      | {"email":
"m-zhukov061972@postgrespro.ru", "phone": "+70149562185"}
(5 rows)

Создадим недоверенного пользователя и выполним выгрузку данных из таблицы под его аккаунтом.

=> CREATE USER novice;
CREATE ROLE
=> SECURITY LABEL FOR anon ON ROLE novice IS 'MASKED';
SECURITY LABEL
=> GRANT SELECT ON m_tickets TO novice;
GRANT
student$ /opt/pgpro/ent-16/bin/pg_dump_anon.sh \
-U novice -t m_tickets demo \
2>/dev/null | grep -A 5 COPY
COPY public.m_tickets FROM STDIN WITH CSV;
0005432000987,06B046,8149 604011,VALERIY TIKHONOV,"{""email"":
""valeriy.tikhonov@example.com"", ""phone"": ""0804160992""}"
0005432000988,06B046,8499 420203,EVGENIYA ALEKSEEVA,"{""email"":
""evgeniya.alekseeva@example.com"", ""phone"": ""0322424618""}"
0005432000989,E170C3,1011 752484,ARTUR GERASIMOV,"{""email"":
""artur.gerasimov@example.com"", ""phone"": ""0329808084""}"
0005432000990,E170C3,4849 400049,ALINA VOLKOVA,"{""email"":
""alina.volkova@example.com"", ""phone"": ""0584090983""}"
0005432000991,F313DD,6615 976589,MAKSIM ZHUKOV,"{""email"":
""maksim.zhukov@example.com"", ""phone"": ""0787065850""}"

Данные таблицы выгрузились в анонимизированном виде.

2. Подмена и псевдонимизация

Активируем механизм динамического маскирования:

=> SELECT anon.start_dynamic_masking();
 start_dynamic_masking
-----------------------
 t
(1 row)

Сформируем правило, подменяющее имя пассажира. Для этого воспользуемся функцией anon.fake_last_name:

=> SECURITY LABEL FOR anon ON COLUMN m_tickets.passenger_name
IS 'MASKED WITH FUNCTION anon.fake_first_name()';
SECURITY LABEL

Подключаемся как недоверенный пользователь и выполняем несколько одинаковых запросов.

=> \c - novice
You are now connected to database "demo" as user "novice".
=> SELECT passenger_name FROM m_tickets WHERE passenger_id = '8149 604011';
 passenger_name
----------------
 Janet
(1 row)
=> \g

Page 22: Page 22

 passenger_name
----------------
 Richard
(1 row)
=> \g
 passenger_name
----------------
 Tyrone
(1 row)

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

=> \c - student
You are now connected to database "demo" as user "student".
=> \df anon.pseudo_*
                                          List of functions
 Schema |       Name        | Result data type |              Argument data types
     | Type
--------+-------------------+------------------+------------------------------------------
-----+------
 anon   | pseudo_city       | text             | seed anyelement, salt text DEFAULT
NULL::text | func
 anon   | pseudo_company    | text             | seed anyelement, salt text DEFAULT
NULL::text | func
 anon   | pseudo_country    | text             | seed anyelement, salt text DEFAULT
NULL::text | func
 anon   | pseudo_email      | text             | seed anyelement, salt text DEFAULT
NULL::text | func
 anon   | pseudo_first_name | text             | seed anyelement, salt text DEFAULT
NULL::text | func
 anon   | pseudo_iban       | text             | seed anyelement, salt text DEFAULT
NULL::text | func
 anon   | pseudo_last_name  | text             | seed anyelement, salt text DEFAULT
NULL::text | func
 anon   | pseudo_siret      | text             | seed anyelement, salt text DEFAULT
NULL::text | func
(8 rows)

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

=> SECURITY LABEL FOR anon ON COLUMN m_tickets.passenger_name
IS 'MASKED WITH FUNCTION anon.pseudo_first_name(passenger_id)';
SECURITY LABEL
=> \c - novice
You are now connected to database "demo" as user "novice".
novice=> SELECT passenger_name FROM m_tickets WHERE passenger_id = '8149 604011';
 passenger_name
----------------
 Eddie
(1 row)
novice=> \g
 passenger_name
----------------
 Eddie
(1 row)
novice=> \g

Page 23: Уборка

 passenger_name
----------------
 Eddie
(1 row)

Удалим расширение и объекты, созданные в базе demo, удалим созданную роль и очистим список загружаемых библиотек.

novice=> \c - student
You are now connected to database "demo" as user "student".
student=# DROP EXTENSION anon CASCADE;
NOTICE:  drop cascades to 2 other objects
DETAIL:  drop cascades to function anon.fake_contact_data(text)
drop cascades to view mask.m_tickets
DROP EXTENSION
student=# DROP SCHEMA mask;
DROP SCHEMA
student=# DROP TABLE m_tickets;
DROP TABLE
student=# DROP USER novice;
DROP ROLE
student=# ALTER SYSTEM RESET shared_preload_libraries;
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service

18 Занятие 18: Резервное копирование / Основы pg_probackup

Исходная страница в нативном Markdown: Резервное копирование / Основы pg_probackup
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_18_probackup_1.html Archive: PGPRO-handouts-16.zip Страниц PDF: 31

Page 1: Резервное копирование / Основы pg_probackup

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Резервное копирование
  • Полное копирование

Page 3: Резервное копирование

Риски и требования

  • Характеристики
  • Общая классификация
  • Инструменты PostgreSQL
  • Возможности pg_probackup

Page 4: Риски и требования

Резервное копирование снижает риски потери данных

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

Комментарии преподавателя

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

Основные причины, приводящие к возможной потере данных:

● природные и антропогенные катастрофы;

● отказы и сбои оборудования;

● ошибки в программном обеспечении;

● намеренная порча или кража оборудования;

● взлом или компрометация компьютерных систем;

● человеческие ошибки.

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

● расписание резервного копирования;

● процедуры проверки целостности и исправности копий;

● систему хранения резервных копий в надлежащих условиях;

● план восстановления системы с помощью резервных копий;

● средства защиты системы резервного копирования и самих копий.

Желательно обеспечить минимальное время копирования и восстановления и ограничить пространство для хранения копий.

Page 5: Характеристики

Source slide page 5: Характеристики{#source-figure-005 caption=“Source slide page 5: Характеристики”}

Идеальные характеристики

  • отношение времени работы системы к общему времени → 100 %
  • RTO — целевое время восстановления
  • → 0
  • RPO — целевая точка восстановления
  • → 0

Комментарии преподавателя

Идеальная система никогда не простаивает, и отношение времени продуктивной работы системы к общему времени ее эксплуатации равно 100%.

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

Оценка максимального периода времени, за который данные могут быть потеряны вследствие инцидента, связана с периодичностью и способами выполнения резервного копирования. Такая оценка выражается показателем Recovery Point Objective (RPO).

После обнаружения инцидента требуется время для восстановления системы. Показатель Recovery Time Objective (RTO) указывает целевое время, по истечении которого система восстанавливается для обслуживания клиентов.

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

Page 6: Общая классификация

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

  • неконтролируемое (unattended)
  • контролируемое (attended)
  • По плановости
    • незапланированное (non-scheduled)
    • календарное (scheduled)
  • По полноте резервирования данных
    • полное (full)
    • инкрементальное (incremental)

Комментарии преподавателя

Принятая в системах резервного копирования классификация подразделяет резервное копирование по следующим критериям:

● по степени вовлеченности администратора в процесс резервного копирования на требующее контроля и автоматическое неконтролируемое копирование;

● является ли резервное копирование незапланированным, или же оно выполняется в соответствии с календарным расписанием;

● копируются ли все данные (возможно, в сжатом виде) или лишь часть, измененная с момента предыдущего резервного копирования.

Незапланированное резервное копирование осуществляется с помощью команды администратора (attended non-scheduled backup).

Однако чаще всего заранее создается календарный план резервного копирования, в котором фиксируется периодичность автоматического выполнения полного (full) или инкрементального (incremental) резервного копирования. Другое название инкрементальных копий — разностные копии (в конкретных системах резервного копирования понятия инкрементальных и разностных копий могут отличаться). Инкрементальные копии содержат данные, измененные с момента последнего резервного копирования, поэтому размер такой копии меньше, чем у полной.

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

Page 7: Инструменты PostgreSQL

COPY, pg\_dump, pg\_dumpall

  • высокое RPO
  • pg_basebackup
    • нет каталога и хранилища резервных копий
    • нет политик хранения и удержания копий
    • нет инкрементального копирования
    • невозможна параллельная работа

Комментарии преподавателя

Собственные инструменты PostgreSQL позволяют выполнять как логическое копирование всего кластера или отдельных объектов, так и физическое копирование кластера. Эти инструменты подробно рассмотрены в курсе DBA3.

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

Средства логического резервирования не удовлетворяют поставленным требованиям из-за крайне высокого RPO.

Средства физического резервирования не предоставляют возможности ведения каталога резервных копий, который содержал бы информацию о копиях (метаданные). Например, в каталоге резервных копий можно было бы хранить тип резервной копии (полная или инкрементальная), время и способ ее создания. Такой каталог позволяет выполнять проверку исправности резервных копий, а также упрощает и ускоряет восстановление данных.

Другой недостаток штатных инструментов — отсутствие политик, определяющих правила хранения копий и их последующего удаления.

Кроме того, производительность штатной утилиты pg_basebackup из-за ее однопоточности явно недостаточна для резервирования больших объемов данных.

Page 8: Возможности pg_probackup

Централизованный каталог копий и политики хранения

  • Локальное или удаленное копирование, поддержка S3
  • Постраничное инкрементальное копирование
  • Восстановление выбранных баз данных
  • Параллельное резервирование и восстановление
  • Сжатие данных и поддержка CFS
  • Резервирование файлов вне каталога данных

Комментарии преподавателя

Утилита pg_probackup — система резервного копирования и восстановления корпоративного уровня, разработанная компанией Postgres Professional.

Утилита работает с централизованным каталогом копий и поддерживает инкрементальное копирование и восстановление.

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

Хранилище копий позволяет выполнять сложные операции, такие как слияние копий, и реализовывать политики их хранения. Имея каталог копий, можно проверять целостность данных и возможность восстановления на заданный момент времени (PITR, Point In Time Recovery), не выполняя само восстановление.

Утилита также поддерживает работу в параллельном режиме и может выполнять роль архиватора журнала предзаписи.

Другие важные возможности pg_probackup: восстановление из копии отдельных баз данных, поддержка сжатия данных и файловой системы CFS (Compressed File System), резервирование с физической реплики и резервирование файлов, находящихся вне PGDATA (например, файлов конфигурации), поддержка хранилищ S3, возможность ограничивать скорость записи данных на диск (для снижения нагрузки на систему ввода-вывода).

https://postgrespro.ru/docs/enterprise/16/app-pgprobackup

Page 9: Полное копирование

Локальная работа

  • Полная копия
  • Сжатие
  • Удаленная работа
  • Архивирование WAL

Page 10: Локальная работа

Source slide page 10: Локальная работа{#source-figure-010 caption=“Source slide page 10: Локальная работа”}

Базы данных и каталог резервных копий на одном сервере

  • Простота
    • каталог резервных копий на локальной машине
    • архива WAL нет
    • используется потоковый режим

Комментарии преподавателя

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

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

Пользователь, запускающий pg_probackup, должен иметь полный доступ к каталогу копий. Каталог может содержать резервные копии разных кластеров баз данных; перед первым копированием кластера в каталог нужно выполнить команду pg_probackup add-instance для инициализации подкаталогов и метаданных.

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

https://postgrespro.ru/docs/enterprise/16/app-initdb#APP-INITDB-ALLOW-G ROUP-ACCESS

Page 11: Подготовка pg_probackup

Чтобы не вводить в командной строке полный путь к pg_probackup, мы использовали символическую ссылку:

student$ ls -l $(which pg_probackup)
lrwxrwxrwx 1 root root 34 июл 14 11:04 /usr/bin/pg_probackup ->
/opt/pgpro/ent-16/bin/pg_probackup

При локальной работе пользователь, запускающий pg_probackup, должен иметь доступ на чтение файлов каталога данных копируемого экземпляра. Для этого достаточно: при инициализации кластера запустить утилиту initdb с ключом -g; дать группе postgres право чтения каталога PGDATA; включить пользователя в группу postgres. Все эти действия были выполнены при настройке виртуальной машины курса. Работа с pg_probackup начинается с создания и инициализации каталога резервных копий.

student$ sudo mkdir /var/probackup

Утилите pg_probackup необходим доступ в этот каталог. Мы будем запускать утилиту от имени пользователя student, поэтому сделаем его владельцем каталога.

student$ sudo chown student: /var/probackup

Для инициализации каталога резервных копий используется команда init утилиты pg_probackup, параметр -B указывает местоположение каталога:

student$ pg_probackup init \
  -B /var/probackup
INFO: Backup catalog '/var/probackup' successfully initialized

Заглянем в каталог, чтобы проверить результаты инициализации.

student$ tree --noreport /var/probackup
/var/probackup
├── backups
└── wal

При инициализации были созданы два подкаталога: backups — для резервных копий, wal — для архива журнала предзаписи. Утилита pg_probackup имеет встроенную систему помощи. Получить информацию об отдельных командах утилиты можно командой help:

student$ pg_probackup help init

Или так:

student$ pg_probackup init --help
pg_probackup init -B backup-dir
  -B, --backup-path=backup-dir     location of the backup storage area
                 [--s3=s3-interface-provider]
      --dry-run                   perform a trial run without any changes

Следующий шаг — добавление копируемого экземпляра в каталог копий — выполняется командой add-instance: ключ -D указывает путь к каталогу данных копируемого экземпляра (PGDATA); ключ –instance задает имя экземпляра в каталоге копий.

student$ pg_probackup add-instance \
  -B /var/probackup --instance ent-16 \
  -D /var/lib/pgpro/ent-16
INFO: Instance 'ent-16' successfully initialized

Снова проверим содержимое каталога копий.

Page 12: Page 12

student$ tree --noreport /var/probackup
/var/probackup
├── backups
│   └── ent-16
│       ├── locks
│       └── pg_probackup.conf
└── wal
    └── ent-16

В результате: в каталогах backups и wal появились подкаталоги с именем экземпляра; для экземпляра создан файл конфигурации pg_probackup.conf. Содержимое файла конфигурации можно посмотреть средствами ОС, а полную информацию о конфигурации покажет команда show-config утилиты pg_probackup:

student$ pg_probackup show-config \
  -B /var/probackup --instance ent-16
# Backup instance information
pgdata = /var/lib/pgpro/ent-16
system-identifier = 7662291690466855210
xlog-seg-size = 16777216
postgres-edition = Postgres Pro enterprise 16
wal-tree = false
# Connection parameters
pgdatabase = student
# Archive parameters
archive-timeout = 5min
# Logging parameters
log-level-console = INFO
log-level-file = OFF
log-format-console = PLAIN
log-format-file = PLAIN
log-filename = pg_probackup.log
log-rotation-size = 0TB
log-rotation-age = 0d
# Retention parameters
retention-redundancy = 0
retention-window = 0
wal-depth = 0
# Compression parameters
compress-algorithm = none
compress-level = 1
# Remote access parameters
remote-proto = ssh
# Backup instance information
write-rate-limit = 0GBps

Утилита сохранила в параметре system-identifier уникальный идентификатор экземпляра. Перед выполнением резервного копирования сохраненный идентификатор сверяется с идентификатором резервируемого экземпляра; при несовпадении резервирование не выполняется. Это защищает копии от ошибочных действий администратора. Рекомендуется использовать для резервного копирования непривилегированную роль с атрибутом replication. Такая роль создана заранее:

student$ psql
=> \du backup
      List of roles
 Role name | Attributes
-----------+-------------
 backup    | Replication

Создадим базу данных, к которой будет подключаться роль backup:

=> CREATE DATABASE pgpro_probackup_1 OWNER backup TEMPLATE templateprobackup;
CREATE DATABASE
=> \c pgpro_probackup_1
You are now connected to database "pgpro_probackup_1" as user "student".

Мы использовали в качестве шаблона базу данных, в которой для роли backup уже предоставлены привилегии на несколько функций, необходимых для работы утилиты pg_probackup. Вот они:

Page 13: Page 13

=> SELECT routine_name
FROM information_schema.routine_privileges
WHERE grantee = 'backup';
      routine_name
-------------------------
 pg_backup_stop
 txid_current
 txid_current_snapshot
 txid_snapshot_xmax
 pg_control_checkpoint
 current_setting
 set_config
 pg_last_wal_replay_lsn
 pg_is_in_recovery
 pg_create_restore_point
 pg_switch_wal
 pg_backup_start
(12 rows)

Утилита pg_probackup использует стандартные параметры подключения к базе данных. Пользовательские параметры удобно записать в файл конфигурации с помощью команды set-config:

student$ pg_probackup set-config \
  -B /var/probackup --instance ent-16 \
  -d pgpro_probackup_1 \
  -U backup \
  --log-level-console=ERROR

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

student$ pg_probackup show-config \
  -B /var/probackup --instance ent-16 \
  | egrep '^(pg|log-level-console)'
pgdata = /var/lib/pgpro/ent-16
pgdatabase = pgpro_probackup_1
pguser = backup
log-level-console = ERROR

Page 14: Полная копия

Source slide page 14: Полная копия{#source-figure-014 caption=“Source slide page 14: Полная копия”}

Полная резервная копия содержит все данные экземпляра

  • Потоковый способ доставки → автономная копия

Комментарии преподавателя

Команда pg_probackup backup -b FULL формирует резервную копию, состоящую из базовой копии кластера и сегментов журнала предзаписи.

Чтобы полная резервная копия была автономной, то есть самодостаточной, она должна включать в себя все записи WAL, необходимые для восстановления согласованности из базовой копии на момент начала копирования.

Если указать параметр –stream, записи журнала передаются потоковым способом. Если параметр не указать, утилита журнальные записи не получает; предполагается, что для экземпляра СУБД настроена файловая непрерывная архивация.

Оба варианта передачи WAL рассматриваются в курсе DBA3 «Резервное копирование и репликация».

Page 15: Сжатие

Source slide page 15: Сжатие{#source-figure-015 caption=“Source slide page 15: Сжатие”}

Сжимаются файлы данных и файлы сегментов WAL

--compress-algorithm=zstd
--compress-level=1

Комментарии преподавателя

Работа pg_probackup может быть существенно оптимизирована посредством сжатия файлов данных и сегментов WAL. Во-первых, экономится место хранения резервных копий, а во-вторых, потоки данных меньше загружают подсистему ввода-вывода. Взамен требуются ресурсы процессора для выполнения операций сжатия.

Сжатие настраивается с помощью параметров –compress-algorithm и – compress-level, в которых можно указать алгоритм и уровень сжатия. Поддерживаются алгоритмы zstd, lz4, zlib и pglz, если соответствующие библиотеки установлены в системе, а значение возможных уровней сжатия зависит от алгоритма.

Параметр –compress задает алгоритм сжатия по умолчанию и устанавливает его уровень в единицу, то есть переопределяет параметры –compress-algorithm и –compress-level и не может быть использован одновременно с ними.

https://postgrespro.ru/docs/enterprise/16/app-pgprobackup#PBK-COMPRE SSION-OPTS

Page 16: Полная потоковая резервная копия

Получим автономную резервную копию c потоковой доставкой записей WAL: параметр -b FULL запрашивает полную копию; параметр –stream включает потоковую доставку WAL (по умолчанию используется файловая архивация); параметр –compress включает сжатие методом, установленным по умолчанию.

student$ pg_probackup backup \
  -B /var/probackup  --instance ent-16 \
  -b FULL \
  --stream \
  --compress

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

student$ pg_probackup show \
  -B /var/probackup --instance ent-16
==========================================================================================
===================================================
 Instance  Version  ID      Recovery Time                  Mode  WAL Mode  TLI  Time
Data   WAL  Zalg  Zratio  Start LSN  Stop LSN   Status
==========================================================================================
===================================================
 ent-16    16       TIUPNC  2026-07-27 23:45:19.717871+03  FULL  STREAM    1/0   36s
83MB  16MB  zstd    3,93  0/C000028  0/C001820  OK

При выполнении резервного копирования pg_probackup вычисляет и сохраняет контрольные суммы для всех файлов. По умолчанию после копирования и перед восстановлением выполняется проверка целостности копии путем сравнения контрольных сумм. Можно проверить целостность копии и вручную с помощью команды validate:

student$ pg_probackup validate \
  -B /var/probackup --instance ent-16 \
  --log-level-console=INFO \
  -i TIUPNC
INFO: Validating backup TIUPNC
INFO: validate_tablespace_map_content start
INFO: Backup TIUPNC data files are valid
INFO: Backup TIUPNC WAL segments are valid
INFO: Backup TIUPNC is valid.
INFO: Validate of backup TIUPNC completed.

Команда show может показывать информацию о копии по ее идентификатору.

student$ pg_probackup show \
  -B /var/probackup --instance ent-16 \
  -i TIUPNC
#Configuration
backup-mode = FULL
stream = true
compress-alg = zstd
compress-level = 1
from-replica = false
#Compatibility
block-size = 8192
xlog-block-size = 8192
checksum-version = 1
program-version = 2.8.14
server-version = 16
cfs-backup-version = 1
hostname = course
#Result backup info
timelineid = 1
start-lsn = 0/C000028
stop-lsn = 0/C001820
start-time = '2026-07-27 23:45:12+03'
end-time = '2026-07-27 23:45:48+03'

Page 17: Page 17

backup-threads = 1
validate-threads = 1
end-validation-time = '2026-07-27 23:45:49+03'
recovery-xid = 963
recovery-time = '2026-07-27 23:45:19.717871+03'
data-bytes = 87369329
wal-bytes = 16777216
uncompressed-bytes = 343261318
pgdata-bytes = 343260869
status = OK
primary_conninfo = 'user=backup reusepass=1 channel_binding=prefer port=5432
sslmode=prefer sslcompression=0 sslcertmode=allow sslsni=1
ssl_min_protocol_version=TLSv1.2 gssencmode=prefer krbsrvname=postgres gssdelegation=0
target_session_attrs=any target_server_type=any hostorder=sequential
load_balance_hosts=disable'
content-crc = 1928193629

Поддерживается также вывод информации о копии в формате JSON. Эти данные сохраняются в каталоге резервной копии в файле backup.control, а информация о контрольных суммах — в файле backup_content.control. Удалим всю информацию об экземпляре, включая созданные резервные копии:

student$ pg_probackup del-instance \
  -B /var/probackup --instance ent-16

Page 18: Удаленная работа

Source slide page 18: Удаленная работа{#source-figure-018 caption=“Source slide page 18: Удаленная работа”}

Утилита запускается на сервере резервного копирования

  • Подключение к целевым серверам по SSH
  • Удаленная работа на целевом сервере выполняется агентами

Комментарии преподавателя

В удаленном режиме утилиту pg_probackup запускают на сервере резервного копирования (на котором расположен каталог копий); она подключается к целевому серверу по протоколу SSH (Secure Shell), запускает на нем процессы-агенты и с их помощью может выполнять резервное копирование, восстановление и архивирование WAL. Количество запускаемых агентов определяется параметром –threads. При ошибке любого агента аварийно завершаются остальные агенты экземпляра и породившая их команда pg_probackup.

На всех серверах должна быть установлена одна и та же версия pg_probackup.

При подключении по SSH аутентификация выполняется без ввода пароля, по ключу.

Параметры SSH-соединения:

● –remote-host — целевой сервер для копирования и восстановления;

● –remote-user — пользователь ОС, от имени которого работает агент на целевом сервере (владелец экземпляра Postgres);

● –remote-port — TCP-порт для подключения по SSH к целевому серверу.

https://postgrespro.ru/docs/enterprise/16/app-pgprobackup#PBK-CONFIGU RING-THE-REMOTE-MODE

Page 19: Удаленная работа

Для подготовки каталога копий экземпляра, работающего на удаленном узле сети, необходимо сообщить агенту SSH адрес или имя этого удаленного узла, а также имя пользователя, с правами которого будет работать агент.

student$ pg_probackup add-instance \
  -B /var/probackup --instance ent-16 \
  -D /var/lib/pgpro/ent-16 \
  --log-level-console=ERROR \
  --remote-host=localhost \
  --remote-user=postgres

Не забудем о настройках конфигурации:

student$ pg_probackup set-config \
  -B /var/probackup --instance ent-16 \
  -d pgpro_probackup_1 \
  -U backup

Теперь выполним полное резервное копирование удаленного сервера, также используя потоковую доставку записей WAL:

student$ pg_probackup backup \
  -B /var/probackup --instance ent-16 \
  -b FULL \
  --stream

В каталоге резервных копий появилась запись:

student$ pg_probackup show \
  -B /var/probackup --instance ent-16
==========================================================================================
====================================================
 Instance  Version  ID      Recovery Time                  Mode  WAL Mode  TLI  Time
Data   WAL  Zalg  Zratio  Start LSN  Stop LSN   Status
==========================================================================================
====================================================
 ent-16    16       TIUPOG  2026-07-27 23:45:54.041458+03  FULL  STREAM    1/0   23s
328MB  16MB  none    1,00  0/E000028  0/E0001E8  OK

Page 20: Архивирование WAL

Source slide page 20: Архивирование WAL{#source-figure-020 caption=“Source slide page 20: Архивирование WAL”}

Архив WAL сохраняется в каталог резервных копий

Комментарии преподавателя

Для инкрементального резервного копирования в режиме PAGE необходим архив журнала предзаписи, который дает возможность восстановления на момент в прошлом (PITR, Point-In-Time Recovery).

При создании резервной копии pg_probackup предполагает, что на копируемом экземпляре настроен механизм пополнения архива — обычно файловым способом. Команда архивирования должна корректно работать в случае сбоев, гарантировать сохранение файлов на диске и быть достаточно производительной. Чтобы не настраивать такую команду самостоятельно, удобно задать в параметре archive_command команду pg_probackup archive-push.

В данном случае утилиту pg_probackup вызывает СУБД, поэтому при работе через SSH целевым сервером, который указывается в параметрах –remote-*, будет сервер резервного копирования с каталогом резервных копий.

Утилиту можно использовать для архивации и при локальной работе. Тогда она будет запускаться от имени владельца файлов кластера данных (обычно postgres), и каталог копий в таком случае также должен принадлежать postgres. Это особенность текущей версии pg_probackup.

https://postgrespro.ru/docs/enterprise/16/app-pgprobackup#PBK-SETTING-UP-CONTINUOUS-WAL-ARCHIVING ​

Page 21: Архивирование WAL

Подготовим экземпляр к работе с файловым архивированием WAL.

=> ALTER SYSTEM SET archive_mode = on;
ALTER SYSTEM

Для команды архивирования целевым будет сервер резервного копирования, в нашем случае это localhost. Команда подключается к серверу по SSH от имени владельца каталога резервных копий — пользователя student.

=> ALTER SYSTEM SET archive_command =
'pg_probackup archive-push '
'-B /var/probackup --instance=ent-16'
' --wal-file-path=%p'
' --wal-file-name=%f'
' --remote-host=localhost'
' --remote-user=student';
ALTER SYSTEM

Экземпляр необходимо перезапустить.

student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql pgpro_probackup_1

Посмотрим на содержимое архива WAL:

student$ pg_probackup show \
  -B /var/probackup \
  --archive
ARCHIVE INSTANCE 'ent-16'
==========================================================================================
=======
 TLI  Parent TLI  Switchpoint  Min Segno  Max Segno  N segments  Size  Zratio  N backups
Status
==========================================================================================
=======

Пока архив пуст. Создадим таблицу и принудительно переключим сегмент WAL.

=> CREATE TABLE t AS SELECT 'Перед копированием в режиме ARCHIVE.' col1;
SELECT 1
=> SELECT pg_switch_wal();
 pg_switch_wal
---------------
 0/F00FBC4
(1 row)

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

=> VACUUM t;
VACUUM

Сегмент WAL попал в архив:

student$ pg_probackup show \
  -B /var/probackup \
  --archive
ARCHIVE INSTANCE 'ent-16'
==========================================================================================
=====================================
 TLI  Parent TLI  Switchpoint  Min Segno                 Max Segno                 N
segments  Size  Zratio  N backups  Status
==========================================================================================
=====================================
 1    0           0/0          00000001000000000000000F  00000001000000000000000F  1
     16MB  1,00    1          OK

Page 22: Page 22

Выполним полное резервное копирование. Поскольку настроена непрерывная файловая архивация, копировать WAL не нужно — параметр –stream не указываем.

student$ pg_probackup backup \
  -B /var/probackup --instance ent-16 \
  -b FULL

Теперь в каталоге имеются две полные копии:

student$ pg_probackup show \
  -B /var/probackup --instance ent-16
==========================================================================================
======================================================
 Instance  Version  ID      Recovery Time                  Mode  WAL Mode  TLI  Time
Data   WAL  Zalg  Zratio  Start LSN   Stop LSN    Status
==========================================================================================
======================================================
 ent-16    16       TIUPP6  2026-07-27 23:46:20.915738+03  FULL  ARCHIVE   1/0   18s
328MB  16MB  none    1,00  0/11000028  0/120000C0  OK
 ent-16    16       TIUPOG  2026-07-27 23:45:54.041458+03  FULL  STREAM    1/0   23s
328MB  16MB  none    1,00  0/E000028   0/E0001E8   OK

Если при восстановлении не указать идентификатор копии, будет использована последняя. Остановим экземпляр и удалим каталог данных кластера.

student$ sudo systemctl stop postgrespro-ent-16.service
student$ sudo rm -rf /var/lib/pgpro/ent-16/*

Восстанавливаемый экземпляр обычно запускается от имени пользователя ОС postgres, а каталог PGDATA принадлежит пользователю postgres и группе postgres. Есть два варианта задания прав на файлы каталога PGDATA: полные права только для владельца; дополнительно права на чтение и доступ в каталог для членов группы postgres. Обычно восстановление производится от имени суперпользователя ОС root с дальнейшей установкой владельца и прав на PGDATA. Восстанавливаем кластер от имени root:

student$ sudo -E /opt/pgpro/ent-16/bin/pg_probackup restore \
  -B /var/probackup --instance ent-16 \
  --allow-group-access \
  --archive-host=localhost \
  --archive-user=student

В команде использован параметр –allow-group-access, чтобы разрешить доступ к файлам от имени группы postgres. Процедура восстановления СУБД будет вызывать утилиту pg_probackup с командой archive-get для получения сегментов WAL из архива. При этом: –archive-host задает адрес сервера, откуда нужно получать эти сегменты — он подставляется в параметр –remote-host; –archive-user задает имя пользователя, от которого будет работать команда pg_probackup archive-get — оно подставляется в параметр –remote-user. Вот как выглядит сформированное значение конфигурационного параметра restore_command:

student$ sudo grep '^restore_command' /var/lib/pgpro/ent-16/postgresql.auto.conf
restore_command = '"/opt/pgpro/ent-16/bin/pg_probackup" archive-get -B "/var/probackup"
--instance "ent-16" --wal-file-path=%p --wal-file-name=%f --remote-host=localhost
--remote-user=student'

Устанавливаем владельца файлов и группу:

student$ sudo chown -R postgres:postgres /var/lib/pgpro/ent-16

Запускаем экземпляр.

student$ sudo systemctl start postgrespro-ent-16.service
student$ psql pgpro_probackup_1
=> SELECT * FROM t;
                 col1
--------------------------------------
 Перед копированием в режиме ARCHIVE.
(1 row)

Page 23: Page 23

Кластер восстановлен из самой свежей резервной копии.

Page 24: Итоги

Штатные средства резервного копирования и восстановления не всегда достаточны, pg\_probackup предоставляет дополнительные возможности

  • Утилита pg_probackup поддерживает каталог резервных копий, который может находиться на отдельном сервере
  • Для WAL возможно файловое архивирование и потоковая передача
  • Поддерживается полное и инкрементальное резервное копирование, сжатие, параллельное резервирование и восстановление

Page 25: Практика

  1. Подготовьте каталог резервных копий и зарегистрируйте в нем копируемый экземпляр для работы в удаленном режиме. 2. Настройте экземпляр на работу в режиме архивирования WAL, зарегистрируйте роль backup и создайте базу данных. 3. Выполните полное резервное копирование. 4. Измените какие-либо данные в базе и переключите сегмент WAL. Убедитесь, что сегмент попал в архив. 5. Удалите каталог данных кластера и восстановите его из резервной копии. Проверьте, восстановились ли последние изменения.

Комментарии преподавателя

  1. Создайте базу данных из шаблона templateprobackup.

  2. Убедиться, что сегмент попал в архив, можно разными способами: средствами PostgreSQL, средствами ОС или с помощью pg_probackup.

Page 26: 1. Каталог резервных копий и регистрация экземпляра

Подготавливаем каталог в файловой системе:

student$ sudo mkdir /var/probackup
student$ sudo chown student: /var/probackup

Инициализируем каталог копий:

student$ pg_probackup init \
  -B /var/probackup
INFO: Backup catalog '/var/probackup' successfully initialized

Регистрируем экземпляр:

student$ pg_probackup add-instance \
  -B /var/probackup --instance ent-16 \
  -D /var/lib/pgpro/ent-16 \
  --log-level-console=ERROR \
  --remote-host=localhost \
  --remote-user=postgres

2. Настройка архивирования WAL, роль и база данных

student$ psql

Подготовка экземпляра к работе с архивированием WAL.

=> ALTER SYSTEM SET archive_mode = on;
ALTER SYSTEM
=> ALTER SYSTEM SET archive_command =
'pg_probackup archive-push'
' -B /var/probackup'
' --instance=ent-16'
' --wal-file-path=%p'
' --wal-file-name=%f'
' --remote-host=localhost'
' --remote-user=student';
ALTER SYSTEM

Перезапуск экземпляра.

student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql

Создадим базу данных, к которой будет подключаться роль backup:

=> CREATE DATABASE pgpro_probackup_1 OWNER backup TEMPLATE templateprobackup;
CREATE DATABASE
=> \c pgpro_probackup_1
You are now connected to database "pgpro_probackup_1" as user "student".

Запишем в конфигурацию настройку для выполнения резервной копии от имени пользователя backup:

student$ pg_probackup set-config \
  -B /var/probackup --instance ent-16 \
  -d pgpro_probackup_1 \
  -U backup

3. Полная копия

Полное копирование в режиме архивирования WAL:

student$ pg_probackup backup \
  -B /var/probackup --instance ent-16 \
  -b FULL

Посмотрим на состояние каталога резервных копий:

Page 27: Page 27

student$ pg_probackup show \
  -B /var/probackup --instance ent-16
==========================================================================================
====================================================
 Instance  Version  ID      Recovery Time                  Mode  WAL Mode  TLI  Time
Data   WAL  Zalg  Zratio  Start LSN  Stop LSN   Status
==========================================================================================
====================================================
 ent-16    16       TIUR4H  2026-07-28 00:17:11.878560+03  FULL  ARCHIVE   1/0   23s
328MB  16MB  none    1,00  0/C000028  0/D001700  OK

4. Изменение данных и переключение сегмента WAL

Изменяем данные.

=> CREATE TABLE t AS SELECT 'Привет!' hi;
SELECT 1

Переключаем сегмент WAL.

=> SELECT pg_switch_wal();
 pg_switch_wal
---------------
 0/E00FAD4
(1 row)

Увеличим счетчик транзакций, чтобы сегмент оказался в архиве.

=> SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
                964
(1 row)

Какой последний сегмент WAL был архивирован?

=> SELECT * FROM pg_ls_archive_statusdir() ORDER BY 3,1;
                     name                      | size |      modification
-----------------------------------------------+------+------------------------
 00000001000000000000000C.done                 |    0 | 2026-07-28 00:17:09+03
 00000001000000000000000C.00000028.backup.done |    0 | 2026-07-28 00:17:11+03
 00000001000000000000000D.done                 |    0 | 2026-07-28 00:17:11+03
 00000001000000000000000E.done                 |    0 | 2026-07-28 00:17:28+03
(4 rows)

Проверка в ОС:

student$ ls -R /var/probackup/wal
/var/probackup/wal:
ent-16
/var/probackup/wal/ent-16:
00000001000000000000000B
00000001000000000000000C
00000001000000000000000C.00000028.backup
00000001000000000000000D
00000001000000000000000E

Проверка средствами pg_probackup:

student$ pg_probackup show \
  -B /var/probackup \
  --archive
ARCHIVE INSTANCE 'ent-16'
==========================================================================================
=====================================
 TLI  Parent TLI  Switchpoint  Min Segno                 Max Segno                 N
segments  Size  Zratio  N backups  Status
==========================================================================================
=====================================
 1    0           0/0          00000001000000000000000B  00000001000000000000000E  4

Page 28: 5. Восстановление из резервной копии

     64MB  1,00    1          OK

Останавливаем экземпляр.

student$ sudo systemctl stop postgrespro-ent-16.service

Удаляем содержимое PGDATA:

student$ sudo rm -rf /var/lib/pgpro/ent-16/*

Восстанавливаем с правами root:

student$ sudo -E /opt/pgpro/ent-16/bin/pg_probackup restore \
  -B /var/probackup --instance ent-16 \
  --allow-group-access \
  --archive-host=localhost \
  --archive-user=student

Меняем владельца и группу:

student$ sudo chown -R postgres:postgres /var/lib/pgpro/ent-16

Устанавливаем для группы права на чтение и доступ к каталогам; это требуется для потоковой передачи WAL.

student$ sudo chmod -R g+rX /var/lib/pgpro/ent-16
student$ sudo ls -l /var/lib/pgpro/ent-16
total 148
-rw-r-----  1 postgres postgres   240 июл 28 00:17 backup_label
drwxr-x--- 10 postgres postgres  4096 июл 28 00:17 base
drwxr-x---  2 postgres postgres  4096 июл 28 00:17 conf.d
drwxr-x---  2 postgres postgres  4096 июл 28 00:17 crash_info
-rw-r-----  1 postgres postgres    88 июл 28 00:17 current_logfiles
drwxr-x---  2 postgres postgres  4096 июл 28 00:17 global
drwxr-x---  2 postgres postgres  4096 июл 28 00:17 log
drwxr-x---  2 postgres postgres  4096 июл 28 00:17 pg_commit_ts
drwxr-x---  2 postgres postgres  4096 июл 28 00:17 pg_dynshmem
-rw-r-----  1 postgres postgres  5780 июл 28 00:17 pg_hba.conf
drwxr-x---  2 postgres postgres  4096 июл 28 00:17 pg_hba.conf.d
-rw-r-----  1 postgres postgres  2640 июл 28 00:17 pg_ident.conf
drwxr-x---  4 postgres postgres  4096 июл 28 00:17 pg_logical
drwxr-x---  4 postgres postgres  4096 июл 28 00:17 pg_multixact
drwxr-x---  2 postgres postgres  4096 июл 28 00:17 pg_notify
drwxr-x---  2 postgres postgres  4096 июл 28 00:17 pg_replslot
drwxr-x---  2 postgres postgres  4096 июл 28 00:17 pg_serial
drwxr-x---  2 postgres postgres  4096 июл 28 00:17 pg_snapshots
drwxr-x---  2 postgres postgres  4096 июл 28 00:17 pg_stat
drwxr-x---  2 postgres postgres  4096 июл 28 00:17 pg_stat_tmp
drwxr-x---  2 postgres postgres  4096 июл 28 00:17 pg_subtrans
drwxr-x---  2 postgres postgres  4096 июл 28 00:17 pg_tblspc
drwxr-x---  2 postgres postgres  4096 июл 28 00:17 pg_twophase
-rw-r-----  1 postgres postgres     3 июл 28 00:17 PG_VERSION
drwxr-x---  3 postgres postgres  4096 июл 28 00:17 pg_wal
drwxr-x---  2 postgres postgres  4096 июл 28 00:17 pg_xact
-rw-r-----  1 postgres postgres   654 июл 28 00:17 postgresql.auto.conf
-rw-r-----  1 postgres postgres 35982 июл 28 00:17 postgresql.conf
-rw-r-----  1 postgres postgres     0 июл 28 00:17 recovery.signal

Запускаем экземпляр.

student$ sudo systemctl start postgrespro-ent-16.service
student$ psql pgpro_probackup_1
=> SELECT * FROM t;
   hi
---------
 Привет!
(1 row)

Данные восстановлены, включая записи из архивированных сегментов WAL.

Уборка

Удалим созданную базу данных и каталог копий, сбросим настройки параметров конфигурации.

Page 29: Page 29

=> \c student
You are now connected to database "student" as user "student".
=> DROP DATABASE pgpro_probackup_1 WITH (FORCE);
DROP DATABASE
student$ sudo rm -rf /var/probackup
=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service

Page 30: Практика PPEM

  1. Создайте локальное хранилище резервных копий для экземпляра ent-16. Назовите его ent-16_local, а каталог данных разместите в /var/lib/pgpro/pg_probackup.
    1. Сделайте автономную резервную копию основного экземпляра в созданное в п. 1 хранилище. Копия должна быть максимально сжата алгоритмом lz4 и включать журнал сообщений сервера. 3. Сделайте еще одну копию экземпляра, но уже без сжатия. Сравните размер полученной копии с полученной в п. 2 и оцените выигрыш от сжатия. 4. Создайте новый экземпляр ent-16-restored из резервной копии п. 3 в каталоге /var/lib/pgpro/ent-16-restored и запустите его.

Комментарии преподавателя

  1. Выбор алгоритма будет доступен после установки уровня сжатия копии.

Page 31: 1. Создание локального хранилища копий

В навигационной панели выберите вкладку «Резервное копирование», в ней — «Хранилища РК» и нажмите «Добавить хранилище». Выберите локальный тип хранилища, заполните поля открывшейся формы и нажмите «Добавить».

2. Автономная резервная копия со сжатием

Выберите вкладку «Резервные копии» и нажмите кнопку «Создать резервную копию». Далее в последовательно открывающихся формах заполните необходимые поля и нажмите «Выполнить РК».

3. Автономная резервная копия без сжатия

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

4. Создание нового экземпляра из резервной копии

На главной странице, в блоке «Экземпляры», нажмите «Добавить экземпляр» и выберите вариант создания из резервной копии. Выберите для восстановления резервную копию без сжатия и заполните необходимые поля в формах. Далее, после проверки доступного пространства, появится возможность восстановления — нажмите кнопку «Выполнить».

19 Занятие 19: Резервное копирование / Использование pg_probackup

Исходная страница в нативном Markdown: Резервное копирование / Использование pg_probackup
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_19_probackup_2.html Archive: PGPRO-handouts-16.zip Страниц PDF: 38

Page 1: Резервное копирование / Использование pg_probackup

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Инкрементальные копии
  • Способы восстановления
  • Синхронизация

Page 3: Инкрементальные копии

Разностное копирование (DELTA)

  • Копирование изменений (PTRACK)
  • Страничное копирование (PAGE)
  • Объединение копий

Page 4: Копирование DELTA

Source slide page 4: Копирование DELTA{#source-figure-004 caption=“Source slide page 4: Копирование DELTA”}

Полная и инкрементальные копии образуют цепочку

  • Разностное копирование — вариант инкрементального
  • Читаются все файлы данных, но в копию попадают только измененные с последнего копирования страницы

Комментарии преподавателя

Инкрементальная копия включает в себя все изменения, произошедшие после создания какой-либо другой копии.

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

Утилита pg_probackup поддерживает три режима инкрементального резервного копирования: DELTA, PAGE и PTRACK.

Режим DELTA — разностное копирование. Страницы файлов в каталоге данных сравниваются со страницами из последней резервной копии. В новую резервную копию попадают только измененные с момента последнего копирования страницы.

Достоинство режима DELTA — его простота: не требуется архив WAL и не надо устанавливать дополнительные расширения. Однако есть и недостаток: объем ввода-вывода может оказаться почти таким же, как при полном резервном копировании.

https://postgrespro.ru/docs/enterprise/16/app-pgprobackup#PBK-MODES-D ELTA

Page 5: Восстановление

Source slide page 5: Восстановление{#source-figure-005 caption=“Source slide page 5: Восстановление”}

Команда restore восстанавливает кластер из копии

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

Комментарии преподавателя

Если команде restore утилиты указать инкрементальную копию, pg_probackup восстановит родительскую полную копию и затем последовательно применит нужные инкрементальные копии.

Если идентификатор копии не указан, выполняется восстановление из самой последней имеющейся копии (включая копии из цепочки, если последняя копия — инкрементальная).

Page 6: Копирование DELTA

Создадим базу данных, к которой будет подключаться роль backup.

=> CREATE DATABASE pgpro_probackup_2 OWNER backup TEMPLATE templateprobackup;
CREATE DATABASE
=> \c pgpro_probackup_2
You are now connected to database "pgpro_probackup_2" as user "student".

Мы будем запускать утилиту локально от имени пользователя ОС student, а агент на удаленном сервере будет обращаться к файлам кластера от имени postgres. Необходимые настройки в виртуальной машине курса уже произведены. Подготовим локальный каталог копий, владелец файлов — student.

student$ sudo mkdir /var/probackup
student$ sudo chown student: /var/probackup
student$ pg_probackup init \
  -B /var/probackup
INFO: Backup catalog '/var/probackup' successfully initialized

Добавим в локальный каталог копий экземпляр БД, работающий на целевом сервере.

student$ pg_probackup add-instance \
  -B /var/probackup --instance ent-16 \
  -D /var/lib/pgpro/ent-16 \
  --remote-host=localhost \
  --remote-user=postgres
INFO: Instance 'ent-16' successfully initialized

Не забудем о настройках конфигурации:

student$ pg_probackup set-config \
  -B /var/probackup --instance ent-16 \
  --log-level-console=ERROR \
  --allow-group-access \
  --remote-host=localhost \
  --remote-user=postgres \
  -d pgpro_probackup_2 \
  -U backup

Каталог PGDATA принадлежит пользователю ОС postgres и группе postgres, и экземпляр обычно запускается от имени postgres. Если указан параметр –allow-group-access, при восстановлении дополнительно выдаются права на чтение и доступ в каталог группе postgres — это позволит в будущем выполнять резервное копирование от имени членов группы, не используя максимальные привилегии root. Сначала сформируем полную резервную копию удаленного сервера, используя потоковую доставку записей WAL:

student$ pg_probackup backup \
  -B /var/probackup --instance ent-16 \
  -b FULL \
  --stream

В каталоге резервных копий появилась запись:

student$ pg_probackup show \
  -B /var/probackup --instance ent-16
==========================================================================================
====================================================
 Instance  Version  ID      Recovery Time                  Mode  WAL Mode  TLI  Time
Data   WAL  Zalg  Zratio  Start LSN  Stop LSN   Status
==========================================================================================
====================================================
 ent-16    16       TIUPRH  2026-07-27 23:47:51.513336+03  FULL  STREAM    1/0   36s
328MB  16MB  none    1,00  0/C000028  0/C001820  OK

Произведем какие-нибудь изменения, например, создадим таблицу:

=> CREATE TABLE IF NOT EXISTS t1 (
  msg text
);
CREATE TABLE

Page 7: Page 7

И запишем в нее данные:

=> INSERT INTO t1 VALUES ('Полная копия создана.'), ('Теперь сделаем разностную.');
INSERT 0 2

Получим разностную резервную копию:

student$ pg_probackup backup \
  -B /var/probackup --instance ent-16 \
  -b DELTA --stream

Что теперь в каталоге копий?

student$ pg_probackup show \
  -B /var/probackup --instance ent-16
==========================================================================================
======================================================
 Instance  Version  ID      Recovery Time                  Mode   WAL Mode  TLI  Time
Data   WAL  Zalg  Zratio  Start LSN  Stop LSN   Status
==========================================================================================
======================================================
 ent-16    16       TIUPSI  2026-07-27 23:48:21.207076+03  DELTA  STREAM    1/1    5s
2758kB  16MB  none    1,00  0/E000028  0/E0001A0  OK
 ent-16    16       TIUPRH  2026-07-27 23:47:51.513336+03  FULL   STREAM    1/0   36s
328MB  16MB  none    1,00  0/C000028  0/C001820  OK

Обратите внимание, насколько разностная копия меньше полной. Восстановим кластер из последней копии. Остановим экземпляр:

student$ sudo systemctl stop postgrespro-ent-16.service

Удалим содержимое PGDATA.

student$ sudo rm -rf /var/lib/pgpro/ent-16/*

При восстановлении указываем разностную копию:

student$ pg_probackup restore \
  -B /var/probackup --instance ent-16 \
  -i TIUPSI

Запускаем экземпляр.

student$ sudo systemctl start postgrespro-ent-16.service
student$ psql pgpro_probackup_2
=> SELECT * FROM t1;
            msg
----------------------------
 Полная копия создана.
 Теперь сделаем разностную.
(2 rows)

Мы восстановили данные, использовав цепочку из полной и разностной копий.

Page 8: Объединение копий

Source slide page 8: Объединение копий{#source-figure-008 caption=“Source slide page 8: Объединение копий”}

Команда merge объединяет родительскую полную копию и дочерние инкрементальные копии в одну общую

  • Не нагружает сеть передачей данных

Комментарии преподавателя

Инкрементальная резервная копия строится быстрее полной и занимает меньше места. Однако по мере роста цепочки занимаемый объем и время восстановление будут увеличиваться.

Утилита pg_probackup позволяет объединить цепочку копий в одну полную копию командой merge.

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

Указав команде идентификатор родительской копии, мы получим объединенную копию, включающую родительскую полную и первую инкрементальную.

При объединении вся работа производится в каталоге резервных копий, команда merge не передает данные по сети.

https://postgrespro.ru/docs/enterprise/16/app-pgprobackup#PBK-MERGING -BACKUPS

Page 9: Объединение копий

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

=> CREATE DATABASE pgpro_probackup_2_bis;
CREATE DATABASE

Получим вторую разностную резервную копию:

student$ pg_probackup backup \
  -B /var/probackup --instance ent-16 \
  -b DELTA --stream

Состояние каталога копий:

student$ pg_probackup show \
  -B /var/probackup --instance ent-16
==========================================================================================
========================================================
 Instance  Version  ID      Recovery Time                  Mode   WAL Mode  TLI  Time
Data   WAL  Zalg  Zratio  Start LSN   Stop LSN    Status
==========================================================================================
========================================================
 ent-16    16       TIUPTT  2026-07-27 23:49:12.963247+03  DELTA  STREAM    1/1   20s
18MB  16MB  none    1,00  0/10000028  0/100003C0  OK
 ent-16    16       TIUPSI  2026-07-27 23:48:21.207076+03  DELTA  STREAM    1/1    5s
2758kB  16MB  none    1,00  0/E000028   0/E0001A0   OK
 ent-16    16       TIUPRH  2026-07-27 23:47:51.513336+03  FULL   STREAM    1/0   36s
328MB  16MB  none    1,00  0/C000028   0/C001820   OK

В информации о резервной копии есть номер родительской копии:

student$ pg_probackup show \
  -B /var/probackup --instance ent-16 \
  -i TIUPTT | grep 'parent-backup-id'
parent-backup-id = 'TIUPSI'

Третья копия, последняя в цепочке, содержит изменения данных, произошедшие после построения копии с этим идентификатором. По мере добавления инкрементальных копий они будут занимать все больше дискового пространства. Можно выполнить полное копирование и удалить все предыдущие копии, но удобнее воспользоваться объединением копий.

student$ pg_probackup merge \
  -B /var/probackup --instance ent-16 \
  -i TIUPTT

В результате имеем единственную полную копию, объединяющую в себе три предыдущие:

student$ pg_probackup show \
  -B /var/probackup --instance ent-16
==========================================================================================
======================================================
 Instance  Version  ID      Recovery Time                  Mode  WAL Mode  TLI  Time
Data   WAL  Zalg  Zratio  Start LSN   Stop LSN    Status
==========================================================================================
======================================================
 ent-16    16       TIUPTT  2026-07-27 23:49:12.963247+03  FULL  STREAM    1/0   22s
335MB  16MB  none    1,00  0/10000028  0/100003C0  OK

Сохраним эту копию для дальнейшей демонстрации.

Page 10: Копирование PTRACK

Source slide page 10: Копирование PTRACK{#source-figure-010 caption=“Source slide page 10: Копирование PTRACK”}

Механизм PTRACK отслеживает изменения страниц

  • В резервную копию помещаются лишь измененные страницы, обнаруженные PTRACK
  • Требуется расширение ptrack

Комментарии преподавателя

PTRACK — еще один режим инкрементального резервного копирования экземпляра Postgres Pro. В копию попадают только изменившиеся страницы, но, в отличие от DELTA, утилите не требуется сравнивать данные с резервной копией: она получает список страниц с помощью механизма PTRACK.

Механизм PTRACK встроен в ядро Postgres Pro Enterprise. Он отслеживает и отмечает модификации страниц в специальной карте и предоставляет API для получения списка файлов, измененных с произвольного момента LSN, и битовых карт измененных в этих файлах страниц.

https://postgrespro.ru/docs/enterprise/16/ptrack

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

https://postgrespro.ru/docs/enterprise/16/app-pgprobackup#PBK-MODES-P TRACK

Page 11: Копирование PTRACK

Подключим библиотеку PTRACK к экземпляру.

=> ALTER SYSTEM SET shared_preload_libraries = 'ptrack';
ALTER SYSTEM

Экземпляр необходимо перезагрузить.

student$ sudo systemctl restart postgrespro-ent-16.service

Установим расширение в базу pgpro_probackup_2.

student$ psql pgpro_probackup_2
=> CREATE EXTENSION IF NOT EXISTS ptrack;
CREATE EXTENSION

Необходимо задать значение параметра ptrack.map_size, рекомендуется 1/1024 объема кластера Postgres Pro:

=> ALTER SYSTEM SET ptrack.map_size = '1MB';
ALTER SYSTEM

Изменение этого параметра также требует перезапуска.

student$ sudo systemctl restart postgrespro-ent-16.service

Существующая полная копия была сделана до того, как PTRACK стал отслеживать изменения, поэтому для демонстрации работы PTRACK нам потребуется новая полная копия.

student$ pg_probackup backup \
  -B /var/probackup --instance ent-16 \
  -b FULL \
  --allow-group-access \
  --stream

Добавим в таблицу еще одну строку.

student$ psql pgpro_probackup_2
=> INSERT INTO t1 VALUES ('Проверим PTRACK.');
INSERT 0 1

Теперь выполним инкрементальное резервное копирование в режиме PTRACK.

student$ pg_probackup backup \
  -B /var/probackup --instance ent-16 \
  -b PTRACK \
  --allow-group-access \
  --stream

В каталоге резервных копий появилась инкрементальная копия:

student$ pg_probackup show \
  -B /var/probackup --instance ent-16
==========================================================================================
=========================================================
 Instance  Version  ID      Recovery Time                  Mode    WAL Mode  TLI  Time
Data   WAL  Zalg  Zratio  Start LSN   Stop LSN    Status
==========================================================================================
=========================================================
 ent-16    16       TIUPW4  2026-07-27 23:50:30.218220+03  PTRACK  STREAM    1/1    2s
1201kB  16MB  none    1,00  0/14000028  0/14002630  OK
 ent-16    16       TIUPV3  2026-07-27 23:49:57.033426+03  FULL    STREAM    1/0   35s
336MB  16MB  none    1,00  0/12000028  0/120001A0  OK
 ent-16    16       TIUPTT  2026-07-27 23:49:12.963247+03  FULL    STREAM    1/0   22s
335MB  16MB  none    1,00  0/10000028  0/100003C0  OK

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

student$ sudo systemctl stop postgrespro-ent-16.service

Очистим каталог данных.

student$ sudo rm -rf /var/lib/pgpro/ent-16/*

Page 12: Page 12

Восстанавливаем кластер, не указывая идентификатор копии.

student$ pg_probackup restore \
  -B /var/probackup --instance ent-16

Запускаем экземпляр.

student$ sudo systemctl start postgrespro-ent-16.service
student$ psql pgpro_probackup_2
=> SELECT * FROM t1;
            msg
----------------------------
 Полная копия создана.
 Теперь сделаем разностную.
 Проверим PTRACK.
(3 rows)

Итак, была использована самая свежая копия. Удалим эти копии, больше они не потребуются.

student$ pg_probackup delete \
  -B /var/probackup --instance ent-16 \
  -i TIUPV3

PTRACK тоже больше не нужен.

=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
=> DROP EXTENSION ptrack;
DROP EXTENSION

Мы перезапустим экземпляр позже.

Page 13: Копирование PAGE

Source slide page 13: Копирование PAGE{#source-figure-013 caption=“Source slide page 13: Копирование PAGE”}

В архиве должны быть все сегменты WAL, записанные после предыдущей копии

Комментарии преподавателя

При наличии архива WAL для создания инкрементальных копий можно использовать еще один режим инкрементального резервного копирования pg_probackup — страничное копирование (PAGE).

При копировании PAGE pg_probackup сканирует все записи WAL в архиве, начиная с момента создания предыдущей полной или инкрементальной копии, и определяет по ним список изменившихся страниц. При этом необходимо, чтобы в архиве присутствовали все файлы WAL, сгенерированные после предыдущей копии.

Режим PAGE особенно эффективен, когда в базах данных происходит немного изменений. Если же объем журнальных файлов сравним с общим объемом файлов базы данных, ускорение будет незначительным, однако объем инкрементальной копии все равно будет меньше, чем полной.

https://postgrespro.ru/docs/enterprise/16/app-pgprobackup#PBK-MODES-P AGE

Page 14: Восстановление

Source slide page 14: Восстановление{#source-figure-014 caption=“Source slide page 14: Восстановление”}

Удаленное восстановление с обменом данными по SSH

Комментарии преподавателя

Архив WAL заполняется с целевого сервера командой pg_probackup archive-push (указанной в конфигурационном параметре archive_command) по SSH. И при восстановлении целевой сервер будет читать необходимые сегменты журнала по SSH командой pg_probackup archive-get (указанной в параметре restore_command).

Здесь мы имеем дело с удаленным восстановлением. Поэтому при вызове pg_probackup restore требуется указать два набора параметров.

Remote-параметры указывают на целевой сервер с восстанавливаемым экземпляром:

● –remote-host — адрес целевого сервера;

● –remote-user — владелец экземпляра (postgres);

● –remote-port — порт SSH.

Archive-параметры указывают на сервер резервного копирования, хранящий архив WAL:

● –archive-host — адрес сервера резервного копирования;

● –archive-user — владелец каталога резервных копий;

● –archive-port — порт SSH.

Archive-параметры используются, чтобы сформировать на целевом сервере значение параметра restore_command.

Page 15: Копирование PAGE

Подготовим экземпляр к работе с архивацией WAL.

=> ALTER SYSTEM SET archive_mode = on;
ALTER SYSTEM

Для команды архивирования целевым является сервер резервного копирования, здесь ему соответствует localhost. Команда архивирования будет подключаться к серверу по SSH под именем владельца каталога резервных копий — пользователя student.

=> ALTER SYSTEM SET archive_command =
' pg_probackup archive-push'
' -B /var/probackup --instance=ent-16'
' --wal-file-path=%p'
' --wal-file-name=%f'
' --remote-host=localhost'
' --remote-user=student';
ALTER SYSTEM

Экземпляр необходимо перезапустить.

student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql pgpro_probackup_2

Посмотрим содержимое архива WAL:

student$ pg_probackup show \
  -B /var/probackup \
  --archive
ARCHIVE INSTANCE 'ent-16'
==========================================================================================
=======
 TLI  Parent TLI  Switchpoint  Min Segno  Max Segno  N segments  Size  Zratio  N backups
Status
==========================================================================================
=======

Пока он пуст. Переключим сегмент WAL.

=> SELECT pg_switch_wal();
 pg_switch_wal
---------------
 0/15004FE4
(1 row)
=> INSERT INTO t1 VALUES ('Архивация WAL работает.');
INSERT 0 1

Сегмент WAL попал в архив:

student$ pg_probackup show \
  -B /var/probackup \
  --archive
ARCHIVE INSTANCE 'ent-16'
==========================================================================================
=====================================
 TLI  Parent TLI  Switchpoint  Min Segno                 Max Segno                 N
segments  Size  Zratio  N backups  Status
==========================================================================================
=====================================
 1    0           0/0          000000010000000000000015  000000010000000000000015  1
     16MB  1,00    1          OK

Выполним полное резервное копирование. Поскольку настроена файловая архивация, копировать WAL не нужно, ключ – stream не указываем.

student$ pg_probackup backup \

Page 16: Page 16

  -B /var/probackup --instance ent-16 \
  -b FULL

Проверим каталог резервных копий:

student$ pg_probackup show \
  -B /var/probackup --instance ent-16
==========================================================================================
======================================================
 Instance  Version  ID      Recovery Time                  Mode  WAL Mode  TLI  Time
Data   WAL  Zalg  Zratio  Start LSN   Stop LSN    Status
==========================================================================================
======================================================
 ent-16    16       TIUPXO  2026-07-27 23:51:32.411384+03  FULL  ARCHIVE   1/0   40s
335MB  16MB  none    1,00  0/17000028  0/180000C0  OK
 ent-16    16       TIUPTT  2026-07-27 23:49:12.963247+03  FULL  STREAM    1/0   22s
335MB  16MB  none    1,00  0/10000028  0/100003C0  OK

Создадим еще одну таблицу.

=> CREATE TABLE t2 AS SELECT 'Проверка режима PAGE.' msg;
SELECT 1

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

=> SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
                971
(1 row)

Предположим, что оператор по ошибке удалил таблицу t1:

=> DROP TABLE t1;
DROP TABLE

Выполним инкрементальное резервное копирование в режиме PAGE.

student$ pg_probackup backup \
  -B /var/probackup --instance ent-16 \
  -b PAGE

Проверим каталог резервных копий:

student$ pg_probackup show \
  -B /var/probackup --instance ent-16
==========================================================================================
======================================================
 Instance  Version  ID      Recovery Time                  Mode  WAL Mode  TLI  Time
Data   WAL  Zalg  Zratio  Start LSN   Stop LSN    Status
==========================================================================================
======================================================
 ent-16    16       TIUPYT  2026-07-27 23:52:08.698267+03  PAGE  ARCHIVE   1/1    5s
417kB  16MB  none    1,00  0/1A000028  0/1B0000C0  OK
 ent-16    16       TIUPXO  2026-07-27 23:51:32.411384+03  FULL  ARCHIVE   1/0   40s
335MB  16MB  none    1,00  0/17000028  0/180000C0  OK
 ent-16    16       TIUPTT  2026-07-27 23:49:12.963247+03  FULL  STREAM    1/0   22s
335MB  16MB  none    1,00  0/10000028  0/100003C0  OK

Сначала восстановим данные, используя инкрементальную копию. Останавливаем сервер:

student$ sudo systemctl stop postgrespro-ent-16.service

Удаляем содержимое PGDATA:

student$ sudo rm -rf /var/lib/pgpro/ent-16/*

Восстанавливаем из цепочки копий:

student$ pg_probackup restore \
  -B /var/probackup \
  --instance ent-16 \
  --remote-user=postgres \
  --remote-host=localhost \
  --archive-host=localhost \
  --archive-user=student \
  -i TIUPYT

Запускаем экземпляр.

Page 17: Page 17

student$ sudo systemctl start postgrespro-ent-16.service
student$ psql pgpro_probackup_2

Для восстановления использовалась такая команда получения сегментов из архива WAL:

=> SHOW restore_command;
restore_command
------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------
 "/opt/pgpro/ent-16/bin/pg_probackup" archive-get -B "/var/probackup" --instance "ent-16"
--wal-file-path=%p --wal-file-name=%f --remote-host=localhost --remote-user=student
(1 row)

Ее сформировала утилита по переданным ей archive-параметрам. Восстановление прошло успешно, но, естественно, таблица t2 по-прежнему на месте, а t1 — отсутствует:

=> SELECT * FROM t2;
          msg
-----------------------
 Проверка режима PAGE.
(1 row)
=> SELECT * FROM t1;
ERROR:  relation "t1" does not exist
LINE 1: SELECT * FROM t1;
                      ^

Page 18: Способы восстановления

Восстановление PITR

  • Частичное восстановление
  • Восстановление страниц
  • Синхронизация

Page 19: Восстановление PITR

Source slide page 19: Восстановление PITR{#source-figure-019 caption=“Source slide page 19: Восстановление PITR”}

При наличии архива WAL можно восстановить систему на момент времени — point-in-time recovery

  • Требуется архив WAL от момента создания резервной копии

Комментарии преподавателя

Если до выполнения резервного копирования уже был настроен архив WAL, то с помощью pg_probackup можно выполнить восстановление на момент времени в прошлом (Point-In-Time Recovery, PITR). Независимо от того, является ли резервная копия автономной (включает журнальные файлы) или нет, требуется наличие архива WAL как минимум с момента создания резервной копии.

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

Указать точку восстановления можно параметрами:

● –recovery-target-time — на заданное время в формате timestamp;

● –recovery-target-xid — до заданной транзакции;

● –recovery-target-lsn — до заданного LSN;

● –recovery-target-name — до заранее созданной именованной точки;

● –recovery-target=‘latest’ — до последнего согласованного состояния;

● –recovery-target=‘immediate’ — до самого раннего согласованного состояния.

https://postgrespro.ru/docs/enterprise/16/app-pgprobackup#PBK-PERFORM ING-POINT-IN-TIME-PITR-RECOVERY

Page 20: Восстановление PITR

Итак, мы обнаружили, что таблица была удалена, и решили восстановить данные на момент до удаления таблицы. Ранее мы выяснили номер транзакции — 971. Восстановим данные до этого номера.

student$ sudo systemctl stop postgrespro-ent-16.service
student$ sudo rm -rf /var/lib/pgpro/ent-16/*

Восстанавливаемся, используя цепочку копий, до XID 971:

student$ pg_probackup restore \
  -B /var/probackup --instance ent-16 \
  --log-level-console=INFO \
  --recovery-target-xid=971 \
  --remote-user=postgres \
  --remote-host=localhost \
  --archive-host=localhost \
  --archive-user=student
INFO: Validating backup TIUPXO
INFO: validate_tablespace_map_content start
INFO: Backup TIUPXO data files are valid
INFO: Backup validation completed successfully on time 2026-07-27 23:52:04.749237+03, xid
971 and LSN 0/190103B0
INFO: Backup TIUPXO is valid.
INFO: Restoring the database from backup TIUPXO on localhost
INFO: Start restoring backup files. PGDATA size: 335MB
INFO: Backup files are restored. Transferred bytes: 335MB, time elapsed: 6s
INFO: Restore incremental ratio (less is better): 100% (335MB/335MB)
INFO: Syncing restored files to disk
INFO: Restored backup files are synced, time elapsed: 30s
INFO: Restore of backup TIUPXO completed.

Запускаем экземпляр.

student$ sudo systemctl start postgrespro-ent-16.service

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

student$ psql pgpro_probackup_2

Проверим, восстановились ли уже таблицы (если нет, нужно подождать):

=> SELECT * FROM t1;
            msg
----------------------------
 Полная копия создана.
 Теперь сделаем разностную.
 Проверим PTRACK.
 Архивация WAL работает.
(4 rows)
=> SELECT * FROM t2;
          msg
-----------------------
 Проверка режима PAGE.
(1 row)

Поскольку цель — момент времени в прошлом, сервер останется в режиме восстановления:

=> SELECT pg_is_in_recovery();
 pg_is_in_recovery
-------------------
 t
(1 row)

Переведем сервер в обычный режим.

=> SELECT pg_promote();

Page 21: Page 21

 pg_promote
------------
 t
(1 row)

Резервные копии, использующие архив WAL, больше не нужны. Удалим их.

student$ pg_probackup delete \
  -B /var/probackup --instance ent-16 \
  -i TIUPXO

Page 22: Частичное восстановление

Source slide page 22: Частичное восстановление{#source-figure-022 caption=“Source slide page 22: Частичное восстановление”}

Восстановление только требуемых баз данных кластера

  • -db-include — база данных для восстановления
  • -db-exclude — база данных, которую не надо восстанавливать

Комментарии преподавателя

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

Параметром –db-include можно задать имя базы данных для восстановления. Базы template0 и template1 восстанавливаются всегда. Можно указать несколько таких параметров, по одному для каждой восстанавливаемой базы данных.

Используя параметр –db-exclude, можно попросить утилиту восстановить все базы, кроме указанных.

Все файлы пропущенных баз данных все равно создаются утилитой, но создаются пустыми. Это необходимо, поскольку при старте экземпляра процесс startup проигрывает журнальные записи (как минимум для восстановления согласованности) и ожидает обнаружить соответствующие файлы данных. Затем ненужные базы данных можно удалить.

https://postgrespro.ru/docs/enterprise/16/app-pgprobackup#PBK-PARTIAL-RESTORE

Page 23: Частичное восстановление

Восстановим из единственной оставшейся полной копии все базы данных кластера, кроме pgpro_probackup_2_bis.

student$ pg_probackup show \
   -B /var/probackup --instance ent-16
==========================================================================================
======================================================
 Instance  Version  ID      Recovery Time                  Mode  WAL Mode  TLI  Time
Data   WAL  Zalg  Zratio  Start LSN   Stop LSN    Status
==========================================================================================
======================================================
 ent-16    16       TIUPTT  2026-07-27 23:49:12.963247+03  FULL  STREAM    1/0   22s
335MB  16MB  none    1,00  0/10000028  0/100003C0  OK

Останавливаем экземпляр.

student$ sudo systemctl stop postgrespro-ent-16.service

Удаляем содержимое PGDATA:

student$ sudo rm -rf /var/lib/pgpro/ent-16/*

Добавим к командной строке параметр –db-exclude, чтобы база pgpro_probackup_2_bis не восстанавливалась:

student$ pg_probackup restore \
  -B /var/probackup --instance ent-16 \
  --remote-user=postgres \
  --remote-host=localhost \
  --archive-host=localhost \
  --archive-user=student \
  --db-exclude=pgpro_probackup_2_bis \
  -i TIUPTT

Запускаем экземпляр.

student$ sudo systemctl start postgrespro-ent-16.service
student$ psql pgpro_probackup_2

База данных pgpro_probackup_2_bis есть в списке:

=> \l
                                                             List of databases
         Name          |  Owner   | Encoding | Locale Provider |   Collate   |    Ctype
 | ICU Locale | ICU Rules |   Access privileges
-----------------------+----------+----------+-----------------+-------------+------------
-+------------+-----------+-----------------------
 demo                  | student  | UTF8     | icu             | en_US.UTF-8 |
en_US.UTF-8 | en-US      |           |
 pgpro_probackup_2     | backup   | UTF8     | icu             | en_US.UTF-8 |
en_US.UTF-8 | en-US      |           |
 pgpro_probackup_2_bis | student  | UTF8     | icu             | en_US.UTF-8 |
en_US.UTF-8 | en-US      |           |
 postgres              | postgres | UTF8     | icu             | en_US.UTF-8 |
en_US.UTF-8 | en-US      |           |
 student               | postgres | UTF8     | icu             | en_US.UTF-8 |
en_US.UTF-8 | en-US      |           |
 template0             | postgres | UTF8     | icu             | en_US.UTF-8 |
en_US.UTF-8 | en-US      |           | =c/postgres          +
                       |          |          |                 |             |
 |            |           | postgres=CTc/postgres
 template1             | postgres | UTF8     | icu             | en_US.UTF-8 |
en_US.UTF-8 | en-US      |           | =c/postgres          +
                       |          |          |                 |             |
 |            |           | postgres=CTc/postgres
 templateprobackup     | postgres | UTF8     | icu             | en_US.UTF-8 |
en_US.UTF-8 | en-US      |           |
(8 rows)

Однако подключиться к ней нельзя:

student$ psql pgpro_probackup_2_bis
psql: error: connection to server on socket "/tmp/.s.PGSQL.5432" failed: FATAL:
"base/24733" is not a valid data directory

Page 24: Page 24

DETAIL:  File "base/24733/PG_VERSION" does not contain valid data.
HINT:  You might need to initdb.

В подкаталоге PGDATA этой базы только пустые файлы:

student$ sudo ls -l /var/lib/pgpro/ent-16/base/24733 | tail
-rw-r----- 1 postgres postgres 0 июл 27 23:54 827
-rw-r----- 1 postgres postgres 0 июл 27 23:54 828
-rw-r----- 1 postgres postgres 0 июл 27 23:54 9001
-rw-r----- 1 postgres postgres 0 июл 27 23:54 9002
-rw-r----- 1 postgres postgres 0 июл 27 23:54 9003
-rw-r----- 1 postgres postgres 0 июл 27 23:54 9004
-rw-r----- 1 postgres postgres 0 июл 27 23:54 9040
-rw-r----- 1 postgres postgres 0 июл 27 23:54 9041
-rw-r----- 1 postgres postgres 0 июл 27 23:54 pg_filenode.map
-rw-r----- 1 postgres postgres 0 июл 27 23:54 PG_VERSION

Эту базу данных можно удалить из системного каталога, файлы при этом тоже удалятся.

=> DROP DATABASE pgpro_probackup_2_bis;
DROP DATABASE
student$ sudo ls -l /var/lib/pgpro/ent-16/base/24733
ls: cannot access '/var/lib/pgpro/ent-16/base/24733': No such file or directory

Целостность восстановленных баз данных можно проверить командой checkdb:

student$ pg_probackup checkdb \
  -B /var/probackup --instance ent-16 \
  --remote-user=postgres \
  --remote-host=localhost \
  -d pgpro_probackup_2

Теперь можно работать с восстановленными данными:

=> \c pgpro_probackup_2
You are now connected to database "pgpro_probackup_2" as user "student".
=> INSERT INTO t1 VALUES ('После частичного восстановления.');
INSERT 0 1
=> SELECT * FROM t1;
               msg
----------------------------------
 Полная копия создана.
 Теперь сделаем разностную.
 После частичного восстановления.
(3 rows)

Page 25: Восстановление страниц

Source slide page 25: Восстановление страниц{#source-figure-025 caption=“Source slide page 25: Восстановление страниц”}

CHECKSUM

  • восстановление испорченных страниц и страниц,
  • отличающихся от резервной копии

Комментарии преподавателя

При инкрементальном восстановлении (восстановлении страниц) каталог данных экземпляра не перезаписывается полностью, а только обновляется теми страницами, состояние которых изменилось по сравнению с резервной копией. Если каталог данных не сильно отличается от копии, такое восстановление будет работать значительно быстрее обычного.

Для инкрементального восстановления в командную строку добавляется параметр -I, после которого указывается один из двух режимов: CHECKSUM или LSN. Эти режимы определяют, как именно будут выбираться страницы, требующие восстановления.

В режиме CHECKSUM восстанавливаются страницы с некорректной контрольной суммой или с номером LSN, отличающимся от LSN в резервной копии. Для этого утилита читает все файлы данных экземпляра, получает их контрольные суммы и LSN, а затем сравнивает эту информацию с содержимым резервной копии. Если имеет место цепочка инкрементальных резервных копий, в более ранних копиях ищутся только те страницы, которые не были найдены в более поздних.

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

https://postgrespro.ru/docs/enterprise/16/app-pgprobackup#PBK-INCREME NTAL-RESTORE

Page 26: Восстановление страниц

Source slide page 26: Восстановление страниц{#source-figure-026 caption=“Source slide page 26: Восстановление страниц”}

LSN

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

Комментарии преподавателя

В режиме LSN утилита пытается найти в инкрементальной цепочке целевой резервной копии наиболее позднюю копию, соответствующую какому-либо состоянию каталога данных экземпляра в прошлом. Момент завершения создания этой копии называется точкой сдвига (Shift LSN) и принадлежит некоторой линии времени. Точка сдвига важна тем, что страницы экземпляра с LSN ≤ Shift LSN заведомо совпадают со страницами, которые можно восстановить из целевой резервной копии, то есть при восстановлении такие страницы можно просто игнорировать.

Если подходящей на роль точки сдвига копии нет, восстановление в режиме LSN невозможно (режим CHECKSUM доступен всегда).

На слайде копия Δ2 не может служить точкой сдвига, поскольку не соответствует никакому прошлому состоянию экземпляра: она была сделана в первой линии времени, а экземпляр в некоторый момент переключился на вторую линию. А копия Δ1 удовлетворяет условиям и будет выбрана в качестве точки сдвига.

Режим LSN требует корректности файла pg_control, поскольку из него считывается Redo LSN и линия времени экземпляра, и наличия необходимых файлов истории в архиве WAL. Также должны быть включены контрольный суммы.

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

Page 27: Восстановление страниц (инкрементальное)

student$ sudo systemctl stop postgrespro-ent-16.service

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

student$ /opt/pgpro/ent-16/bin//pg_controldata -D /var/lib/pgpro/ent-16 \
| grep -e ' TimeLineID' -e ' REDO location'
Latest checkpoint's REDO location:    0/11000A68
Latest checkpoint's TimeLineID:       1

В хранилище резервных копий хранится информация о линии времени и LSN завершения создания копии:

student$ pg_probackup show \
  -B /var/probackup --instance ent-16 \
  -i TIUPTT \
  | grep -e stop-lsn -e timeline
timelineid = 1
stop-lsn = 0/100003C0

В нашем случае линия времени всего одна, а имеющаяся копия отражает прошлое состояние экземпляра, так что может служить точкой сдвига для режима LSN (Backup TIUPTT is chosen as shiftpoint):

student$ pg_probackup restore \
  -B /var/probackup --instance ent-16 \
  -i TIUPTT \
  -I LSN \
  --log-level-console=INFO \
  --remote-user=postgres \
  --remote-host=localhost
INFO: Running incremental restore into nonempty directory: "/var/lib/pgpro/ent-16"
INFO: Backup TIUPTT is chosen as shiftpoint, its Stop LSN will be used as shift LSN
INFO: Destination directory redo point 0/11000A68 on tli 1 is within reach of backup
TIUPTT with Stop LSN 0/100003C0 on tli 1
INFO: shift LSN: 0/100003C0
INFO: Validating backup TIUPTT
INFO: validate_tablespace_map_content start
INFO: Backup TIUPTT data files are valid
INFO: Backup TIUPTT WAL segments are valid
INFO: Backup TIUPTT is valid.
INFO: Restoring the database from backup TIUPTT on localhost
INFO: Extracting the content of destination directory for incremental restore
INFO: Destination directory content extracted, time elapsed: 0
INFO: Removing redundant files in destination directory
INFO: Redundant files are removed, time elapsed: 1s
INFO: Start restoring backup files. PGDATA size: 343MB
INFO: Backup files are restored. Transferred bytes: 24MB, time elapsed: 3s
INFO: Restore incremental ratio (less is better): 7% (24MB/343MB)
INFO: Syncing restored files to disk
INFO: Restored backup files are synced, time elapsed: 20s
INFO: Restore of backup TIUPTT completed.

Обратите внимание на строку «Restore incremental ratio (less is better)». Это отношение объема восстановленных страниц к общему объему данных в кластере. Чем меньше это значение, тем выше эффективность и скорость инкрементального восстановления.

student$ sudo systemctl start postgrespro-ent-16.service
student$ psql pgpro_probackup_2

База данных pgpro_probackup_2_bis восстановлена.

=> \c pgpro_probackup_2_bis
You are now connected to database "pgpro_probackup_2_bis" as user "student".
=> SELECT current_catalog;

Page 28: Page 28

    current_catalog
-----------------------
 pgpro_probackup_2_bis
(1 row)

Page 29: Синхронизация

Source slide page 29: Синхронизация{#source-figure-029 caption=“Source slide page 29: Синхронизация”}

Копирование данных из одного экземпляра в другой

  • синхронизация отставшего ведомого сервера с ведущим
  • добавление нового ведомого сервера

Комментарии преподавателя

Быстро клонировать экземпляр или синхронизировать отставшую реплику позволяет команда catchup утилиты pg_probackup. Она не использует каталог резервных копий, а берет данные непосредственно из каталога данных копируемого экземпляра.

Ускорение относительно обычных средств может достигаться за счет:

● многопоточной передачи данных;

● инкрементального копирования (с дополнению к режиму FULL доступны DELTA и PTRACK).

Использование команды catchup для синхронизации отставшей реплики позволяет избежать сложных ручных процедур с использованием rsync и подобных инструментов.

Команда catchup отличается от других операций, выполняемых утилитой pg_probackup:

● по умолчанию конфигурационные файлы целевого сервера заменяются файлами исходного сервера;

● возможна только потоковая доставка WAL (архив не используется);

● не копируются внешние каталоги;

● запрещено одновременное выполнение команд DDL, создающих или удаляющих табличные пространства.

https://postgrespro.ru/docs/enterprise/16/app-pgprobackup#PBK-CREATIN G-BACKUP-TO-CATCHUP

Page 30: Синхронизация

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

student$ sudo rm -rf /var/lib/pgpro/ent-16-prod
student$ pg_probackup restore \
  -B /var/probackup --instance ent-16 \
  -D /var/lib/pgpro/ent-16-prod \
  -i TIUPTT \
  -j 2 \
  --allow-group-access \
  --restore-as-replica \
  --remote-user=postgres \
  --remote-host=localhost

Здесь были использованы следующие параметры командной строки pg_probackup: -D — целевой каталог для восстановления; -j — копировать в несколько потоков; –restore-as-replica — создать файл standby.signal и настроить строку подключения к мастеру. Запустим реплику, изменив перед этим номер порта в конфигурации.

/var/lib/pgpro/ent-16-prod/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
# recovery settings added by pg_probackup restore of backup TIUPSI at '2026-07-27
23:48:24+03'
# recovery settings added by pg_probackup restore of backup TIUPTT at '2026-07-27
23:55:48+03'
## standby settings
primary_conninfo = 'user=backup reusepass=1 channel_binding=prefer host=localhost
port=5432 sslmode=prefer sslcompression=0 sslcertmode=allow sslsni=1
ssl_min_protocol_version=TLSv1.2 gssencmode=prefer krbsrvname=postgres gssdelegation=0
target_session_attrs=any target_server_type=any hostorder=sequential
load_balance_hosts=disable'
port = 5434
postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-prod \
  -l /var/lib/pgpro/ent-16-prod/pgpro-ent.log \
  start
waiting for server to start...................... done
server started

Проверим работу репликации.

=> \c pgpro_probackup_2
You are now connected to database "pgpro_probackup_2" as user "student".
=> TRUNCATE t1;
TRUNCATE TABLE
=> INSERT INTO t1 SELECT 'string #' || g.i AS msg FROM generate_series(1, 1_000_000) AS g(i);
INSERT 0 1000000

Репликация работает:

student$ psql -p 5434
=> \c pgpro_probackup_2
You are now connected to database "pgpro_probackup_2" as user "student".
=> SELECT pg_is_in_recovery();

Page 31: Page 31

 pg_is_in_recovery
-------------------
 t
(1 row)
=> SELECT count(*) FROM t1;
  count
---------
 1000000
(1 row)

На реплике аварийно завершается основной процесс:

student$ sudo kill -QUIT $(sudo head -n 1 /var/lib/pgpro/ent-16-prod/postmaster.pid)

Пока реплика простаивает, на основном сервере изменим тысячу строк:

=> UPDATE t1 SET msg = msg || '!' WHERE msg ~ '000$';
UPDATE 1000

Если реплика долго простаивала, ее синхронизации может занять долгое время, даже если все нужные журнальные файлы еще сохранились на основном сервере. Мы хотим ускорить синхронизацию, передав данные с мастера на реплику командой catchup, которая будет передавать только измененные страницы. Используем режим инкрементального копирования DELTA, укажем пути к каталогам данных. Дополнительный файл конфигурации не копируем:

postgres$ /opt/pgpro/ent-16/bin/pg_probackup catchup \
  -b DELTA \
  --threads=2 \
  --source-pgdata=/var/lib/pgpro/ent-16 \
  --destination-pgdata=/var/lib/pgpro/ent-16-prod \
  --exclude-path=postgresql.auto.conf \
  --log-level-console=INFO \
  -U backup \
  -d pgpro_probackup_2
INFO: --stream is required, forcing stream mode
INFO: This PostgreSQL instance was initialized with data block checksums. Data block
corruption will be detected
INFO: Catchup start, pg_probackup version: 2.8.14, PostgreSQL version: 16, remote: false,
source-pgdata: /var/lib/pgpro/ent-16, destination-pgdata: /var/lib/pgpro/ent-16-prod
INFO: Database catchup start
INFO: Destination file "/var/lib/pgpro/ent-16-prod/postgresql.auto.conf" excluded with
--exclude-path option
INFO: syncLSN = 0/10000028
INFO: Enable CFS flock feature
INFO: wait for pg_backup_start()
INFO: pg_start_backup START LSN 0/16000028
INFO: Source file "/var/lib/pgpro/ent-16/postgresql.auto.conf" excluded with
--exclude-path option
INFO: Source PGDATA size: 377MB (excluded 280B)
INFO: Start LSN (source): 0/16000028, TLI: 1
INFO: LSN in destination: 0/10000028, TLI: 1
INFO: Removing redundant files in destination directory
WARNING: Renaming pg_control file: Cannot rename file
"/var/lib/pgpro/ent-16-prod/global/pg_control" to
"/var/lib/pgpro/ent-16-prod/global/pg_control.pbk.bak": No such file or directory
INFO: Start transferring data files
INFO: wait for pg_stop_backup()
INFO: pg_stop_backup() successfully executed
INFO: stop_stream_lsn 0/16920000 currentpos 0/16920000
INFO: Databases synchronized. Transferred datafiles size: 10232kB, transferred wal size:
16MB, time elapsed: 1s
INFO: Catchup incremental ratio (less is better): 3% (10232kB/377MB)
INFO: Syncing copied files to disk
INFO: Files are synced, time elapsed: 18s

Обратите внимание на строку «Catchup incremental ratio (less is better)» — она показывает отношение объема переданных для синхронизации страниц к их общему объему. Чем меньше значение, тем эффективнее работает синхронизация. Запустим реплику.

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-prod \
  -l /var/lib/pgpro/ent-16-prod/pgpro-ent.log \
  start

Page 32: Page 32

waiting for server to start..................... done
server started
student$ psql -p 5434 pgpro_probackup_2

Проверим данные на реплике:

=> SELECT count(*) FROM t1 WHERE msg ~ '!$';
 count
-------
  1000
(1 row)

Все измененные строки синхронизированы.

Page 33: Итоги

Утилита pg\_probackup поддерживает инкрементальное копирование и инкрементальное восстановление измененных страниц

  • При наличии архива WAL поддерживается восстановление на момент в прошлом
  • Возможно восстановление части данных кластера и эффективная синхронизация двух кластеров

Page 34: Практика

  1. Подготовьте экземпляр к работе с архивированием WAL, создайте базу данных, выполните полное копирование со сжатием. 2. Измените какие-либо данные в базе и выполните инкрементальное страничное копирование со сжатием. 3. Убедитесь, что полученная цепочка копий пригодна для восстановления экземпляра. 4. Имитируйте аварию сервера с физической порчей данных. 5. Восстановите испорченные данные, не затрагивая при этом другие данные кластера.

Комментарии преподавателя

  1. Аварийно завершите основной процесс экземпляра. Измените файл основного слоя таблицы средствами ОС, например:
dd if=/dev/urandom of=файл conv=fdatasync bs=8K count=10
  1. Выполните инкрементальное восстановление в режиме CHECKSUM.

Page 35: 1. Подготовка каталога резервных копий и регистрация экземпляра. Режим WAL

Создадим и подготовим каталог копий.

student$ sudo mkdir /var/probackup
student$ sudo chown student: /var/probackup

Инициализируем каталог.

student$ pg_probackup init -B /var/probackup
INFO: Backup catalog '/var/probackup' successfully initialized

Регистрируем экземпляр.

student$ pg_probackup add-instance \
  -B /var/probackup --instance ent-16 \
  -D /var/lib/pgpro/ent-16 \
  --remote-host=localhost \
  --remote-user=postgres
INFO: Instance 'ent-16' successfully initialized

Создадим базу данных.

=> CREATE DATABASE pgpro_probackup_2 OWNER backup TEMPLATE templateprobackup;
CREATE DATABASE
student$ psql
=> \c pgpro_probackup_2
You are now connected to database "pgpro_probackup_2" as user "student".

Запомним в настройках конфигурации атрибуты подключения.

student$ pg_probackup set-config \
  -B /var/probackup --instance ent-16 \
  -d pgpro_probackup_2 -U backup \
  --remote-host=localhost \
  --remote-user=postgres \
  --log-level-console=ERROR

Подготовимся к работе в режиме архивирования WAL.

=> ALTER SYSTEM SET archive_mode = on;
ALTER SYSTEM
=> ALTER SYSTEM SET archive_command =
' /opt/pgpro/ent-16/bin/pg_probackup archive-push'
' -B /var/probackup --instance=ent-16'
' --wal-file-path=%p'
' --wal-file-name=%f'
' --remote-host=localhost'
' --remote-user=student'
' --compress';
ALTER SYSTEM

Перезапустим сервер:

student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql pgpro_probackup_2

Проверим работу архивирования WAL.

=> SELECT pg_switch_wal();
 pg_switch_wal
---------------
 0/BF4DC8C
(1 row)
=> CREATE TABLE t AS SELECT 'Тест архивирования WAL.' msg;
SELECT 1

Page 36: Page 36

Проверим, попал ли сегмент WAL в архив.

student$ pg_probackup show \
  -B /var/probackup \
  --archive
ARCHIVE INSTANCE 'ent-16'
==========================================================================================
=======================================
 TLI  Parent TLI  Switchpoint  Min Segno                 Max Segno                 N
segments  Size    Zratio  N backups  Status
==========================================================================================
=======================================
 1    0           0/0          00000001000000000000000B  00000001000000000000000B  1
     5329kB  3,07    0          OK

Получим список сжатых сегментов WAL, помещенных в архив:

student$ ls -R /var/probackup/wal/ent-16
/var/probackup/wal/ent-16:
00000001000000000000000B.zst

По умолчанию сжатие производится методом zstd. Теперь сделаем полную резервную копию со сжатием.

student$ pg_probackup backup \
  -B /var/probackup --instance ent-16 \
  --compress \
  -b FULL

2. Инкрементальная копия

Добавим строку в таблицу.

=> INSERT INTO t VALUES('Еще одна строка.');
INSERT 0 1

Сформируем инкрементальную копию, тоже со сжатием.

student$ pg_probackup backup \
  -B /var/probackup --instance ent-16 \
  --compress \
  -b PAGE

Содержимое каталога копий:

student$ pg_probackup show \
  -B /var/probackup --instance ent-16
==========================================================================================
======================================================
 Instance  Version  ID      Recovery Time                  Mode  WAL Mode  TLI  Time
Data   WAL  Zalg  Zratio  Start LSN   Stop LSN    Status
==========================================================================================
======================================================
 ent-16    16       TIUR8G  2026-07-28 00:19:30.503253+03  PAGE  ARCHIVE   1/1    3s
170kB  16MB  zstd    1,20  0/10000028  0/110000C0  OK
 ent-16    16       TIUR7F  2026-07-28 00:18:57.387743+03  FULL  ARCHIVE   1/0   35s
83MB  16MB  zstd    3,93  0/D000028   0/E0016F8   OK

3. Восстановление из сжатой резервной копии

Остановим экземпляр.

student$ sudo systemctl stop postgrespro-ent-16.service

Удалим содержимое PGDATA.

student$ sudo rm -rf /var/lib/pgpro/ent-16/*

Восстановим экземпляр из цепочки резервных копий.

student$ pg_probackup restore \
  -B /var/probackup --instance ent-16 \
  -i TIUR8G \
  --archive-host=localhost \
  --archive-user=student

Page 37: Page 37

Запустим экземпляр.

student$ sudo systemctl start postgrespro-ent-16.service
student$ psql pgpro_probackup_2
=> SELECT * FROM t;
           msg
-------------------------
 Тест архивирования WAL.
 Еще одна строка.
(2 rows)

Данные восстановлены.

4. Авария сервера

Определим физический путь к файлу, содержащему страницы таблицы t.

=> SELECT pg_relation_filepath('t');
 pg_relation_filepath
----------------------
 base/16541/16542
(1 row)

На сервере происходит авария.

student$ sudo kill -QUIT $(sudo head -n 1 /var/lib/pgpro/ent-16/postmaster.pid)

Сымитируем физическую порчу файла данных, записав в него 10 страниц (80 килобайт) случайных данных:

postgres$ dd if=/dev/urandom of=/var/lib/pgpro/ent-16/base/16541/16542 conv=fdatasync bs=8K count=10
10+0 records in
10+0 records out
81920 bytes (82 kB, 80 KiB) copied, 0,132144 s, 620 kB/s

Запустим сервер.

student$ sudo systemctl start postgrespro-ent-16.service
student$ psql pgpro_probackup_2

Попытаемся обратиться к таблице.

=> SELECT * FROM t;
WARNING:  page verification failed, calculated checksum 6184 but expected 30746
ERROR:  invalid page in block 1 of relation base/16541/16542

5. Инкрементальное восстановление таблицы

Мы хотим восстановить испорченные страницы, не копируя при этом остальные данные кластера. Остановим сервер.

student$ sudo systemctl stop postgrespro-ent-16.service

Выполним инкрементальное восстановление в режиме CHECKSUM.

student$ pg_probackup restore \
  -B /var/probackup --instance ent-16 \
  --log-level-console=INFO \
  -I CHECKSUM \
  --remote-user=postgres \
  --remote-host=localhost \
  --archive-host=localhost \
  --archive-user=student
INFO: Running incremental restore into nonempty directory: "/var/lib/pgpro/ent-16"
INFO: Validating parents for backup TIUR8G
INFO: Validating backup TIUR7F
INFO: validate_tablespace_map_content start
INFO: Backup TIUR7F data files are valid
INFO: Validating backup TIUR8G
INFO: validate_tablespace_map_content start
INFO: Backup TIUR8G data files are valid
INFO: Backup TIUR8G WAL segments are valid

Page 38: Page 38

INFO: Backup TIUR8G is valid.
INFO: Restoring the database from backup TIUR8G on localhost
INFO: Extracting the content of destination directory for incremental restore
INFO: Destination directory content extracted, time elapsed: 0
INFO: Removing redundant files in destination directory
INFO: Redundant files are removed, time elapsed: 0
INFO: Start restoring backup files. PGDATA size: 327MB
INFO: Backup files are restored. Transferred bytes: 1357kB, time elapsed: 4s
INFO: Restore incremental ratio (less is better): 0% (1357kB/327MB)
INFO: Syncing restored files to disk
INFO: Restored backup files are synced, time elapsed: 16s
INFO: Restore of backup TIUR8G completed.

Был скопирован примерно 1 мегабайт данных при общем объеме кластера около 300 мегабайт. Запустим сервер.

student$ sudo systemctl start postgrespro-ent-16.service
student$ psql pgpro_probackup_2

Таблица восстановлена:

=> SELECT * FROM t;
           msg
-------------------------
 Тест архивирования WAL.
 Еще одна строка.
(2 rows)

Уборка

Удалим созданную базу данных и каталог копий, сбросим настройки параметров конфигурации.

=> \c student
You are now connected to database "student" as user "student".
=> DROP DATABASE pgpro_probackup_2 WITH (FORCE);
DROP DATABASE
student$ sudo rm -rf /var/probackup
=> ALTER SYSTEM RESET ALL;
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service

20 Занятие 20: Резервное копирование / Другие возможности pg_probackup

Исходная страница в нативном Markdown: Резервное копирование / Другие возможности pg_probackup
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_20_probackup_3.html Archive: PGPRO-handouts-16.zip Страниц PDF: 21

Page 1: Резервное копирование / Другие возможности pg_probackup

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Хранилище S3
  • Политики хранения
  • Журнал

Page 3: Хранилище S3

Source slide page 3: Хранилище S3{#source-figure-003 caption=“Source slide page 3: Хранилище S3”}

Каталог копий можно размещать в хранилище S3

  • MinIO
  • Amazon S3
  • VK Cloud

Комментарии преподавателя

Утилита pg_probackup способна работать с каталогом копий, который размещен в объектном хранилище, предоставляющем интерфейс S3 (Simple Storage Service).

В настоящее время поддерживаются: MinIO, Amazon S3 и VK Cloud. MinIO — программа с открытым кодом, реализующая API, совместимый с S3, и важнейшие возможности, аналогичные Amazon Web Services. В демонстрационных материалах курса используется именно MinIO.

https://postgrespro.ru/docs/enterprise/16/app-pgprobackup#PBK-SETUP-S3

Page 4: Хранилище S3

В виртуальной машине установлено и настроено программное обеспечение MinIO, реализующее API S3. Запустим сервис MinIO.

student$ sudo systemctl enable --now --quiet minio

Объектное хранилище MinIO использует корзины — аналоги каталогов в обычных файловых системах. Создадим корзину bkp:

student$ /usr/local/bin/mc --quiet mb local/bkp
Bucket created successfully `local/bkp`.

С помощью текстового редактора создадим файл, описывающий подключение к хранилищу S3:

/home/student/s3.config
access-key = minioadmin
secret-key = minioadmin
s3-host = localhost
s3-port = 9000
s3-bucket = bkp
s3-secure = off

access_key и secret-key задают имя учетной записи и пароль в хранилище; s3-host и s3-port — узел и порт службы S3; s3-bucket — имя корзины S3; s3-secure — использование протокола https. Создадим базу данных, к которой будет подключаться роль backup.

=> CREATE DATABASE pgpro_probackup_3 OWNER backup TEMPLATE templateprobackup;
CREATE DATABASE
student$ psql

Инициализируем каталог копий в хранилище S3: провайдер указывается в параметре –s3; расположение файла с настройками — в параметре –s3-config-file.

student$ pg_probackup init \
  -B /probackup \
  --s3=minio \
  --s3-config-file=/home/student/s3.config
INFO: Backup catalog '/probackup' successfully initialized

Если параметр –s3-config-file опущен, pg_probackup ищет настройки подключения к S3 сначала в /etc/pg_probackup/s3.config, а затем в ~postgres/.pg_probackup/s3.config. Также параметры подключения к S3 можно задать с помощью переменных окружения. Добавим в локальный каталог копий экземпляр БД, работающий на удаленном сервере. Каталог копий будет создан в хранилище S3.

student$ pg_probackup add-instance \
  -B /probackup --instance ent-16 \
  -D /var/lib/pgpro/ent-16 \
  --s3=minio \
  --s3-config-file=/home/student/s3.config
INFO: Instance 'ent-16' successfully initialized

Чтобы сократить командную строку, сохраним параметры удаленного доступа в конфигурации.

student$ pg_probackup set-config \
  -B /probackup --instance ent-16 \
  -d pgpro_probackup_3 \
  -U backup \
  --log-level-console=ERROR \
  --s3=minio \
  --s3-config-file=/home/student/s3.config

Page 5: Page 5

Обратите внимание, что параметры –s3=minio и –s3-config-file=/home/student/s3.config задают подключение к S3, они в конфигурацию не записываются и должны задаваться в командной строке явно. Выполним полное резервное копирование в облако.

student$ pg_probackup backup \
  -B /probackup --instance ent-16 \
  -b FULL \
  --stream \
  --s3=minio \
  --s3-config-file=/home/student/s3.config

В каталоге теперь имеется полная потоковая копия:

student$ pg_probackup show \
  -B /probackup --instance ent-16 \
  --s3=minio \
  --s3-config-file=/home/student/s3.config
==========================================================================================
====================================================
 Instance  Version  ID      Recovery Time                  Mode  WAL Mode  TLI  Time
Data   WAL  Zalg  Zratio  Start LSN  Stop LSN   Status
==========================================================================================
====================================================
 ent-16    16       TIUQ8D  2026-07-27 23:58:05.547695+03  FULL  STREAM    1/0   17s
328MB  16MB  none    1,00  0/C000028  0/C001820  OK

Page 6: Политики хранения

Удаление лишних копий

  • Закрепление копий
  • Политика хранения WAL

Page 7: Удаление лишних копий

Source slide page 7: Удаление лишних копий{#source-figure-007 caption=“Source slide page 7: Удаление лишних копий”}

По умолчанию в каталоге хранятся все резервные копии

  • Политика хранения определяет правила удаления копий
    • количество хранимых копий
    • окно удержания в днях

Комментарии преподавателя

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

Например, если окно удержания установлено в значение 7 дней, то в каталоге должна сохраняться минимум одна копия старше 7 дней с файлами WAL и цепочкой инкрементальных копий.

Политика хранения задается параметрами:

● –retention-redundancy — количество полных резервных копий, которое должно сохраняться в каталоге;

● –retention-window — количество дней от текущего момента до самого раннего момента времени в прошлом, на который может быть выполнено восстановление.

Параметры можно указать в командной строке pg_probackup для команд backup или delete, или сохранить в конфигурации командой set-config.

https://postgrespro.ru/docs/enterprise/16/app-pgprobackup#PBK-RETENTI ON-POLICY

Page 8: Закрепление копий

Source slide page 8: Закрепление копий{#source-figure-008 caption=“Source slide page 8: Закрепление копий”}

На закрепленные копии политика хранения не действует

  • Параметры закрепления (команды backup и set-backup)
    • -ttl — время закрепления
    • -expire-time — срок хранения резервной копии

Комментарии преподавателя

Если нужно хранить копию дольше, чем задано политикой хранения, можно закрепить ее на определенное время.

Закрепление можно задать при выполнении команды backup или сохранить в конфигурации командой set-backup.

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

Другой вариант — явно задать время истечения срока хранения копии, воспользовавшись параметром –expire-time.

Определить, закреплена ли копия, можно с помощью команды show утилиты pg_probackup. Закрепленная копия имеет атрибут expire-time, содержащий время окончания срока ее хранения.

https://postgrespro.ru/docs/enterprise/16/app-pgprobackup#PBK-BACKUP-PINNING

Page 9: Политика хранения WAL

Source slide page 9: Политика хранения WAL{#source-figure-009 caption=“Source slide page 9: Политика хранения WAL”}

Политика хранения архива WAL ограничивает глубину архива для экономии места в хранилище

  • Удаление устаревших копий с –delete-wal сопровождается удалением лишних сегментов WAL

Комментарии преподавателя

При работе в режиме непрерывного архивирования сегменты WAL накапливаются в хранилище. Но его емкость ограничена.

Можно удалять устаревшие резервные копии с параметром –delete-wal. При этом удаляются только те сегменты WAL, которые не относятся ни к одной из резервных копий в каталоге. Если возможность восстановления на момент времени нужна только для самых недавних резервных копий, можно, настроив политику хранения архива WAL, ограничить число таких копий — глубину архива — и сэкономить занимаемое место. Из копии, оставшейся без архива WAL, можно восстановиться, только если она является автономной (то есть содержит необходимые журнальные файлы).

Политику хранения архива WAL настраивают параметром –wal-depth. Установленная политика действует для всех линий времени, поэтому можно выполнять PITR для одинакового количества копий на каждой линии времени. Закрепленные копии в этом числе не учитываются.

Можно вручную удалить сегменты, не удовлетворяющие заданной политике хранения архива WAL, выполнив команду pg_probackup delete или pg_probackup backup –delete-wal.

https://postgrespro.ru/docs/enterprise/16/app-pgprobackup#PBK-WAL-ARC HIVE-RETENTION-POLICY

Page 10: Политики хранения копий

Выполним еще одно полное резервное копирование.

student$ pg_probackup backup \
  -B /probackup --instance ent-16 \
  -b FULL \
  --stream \
  --s3=minio \
  --s3-config-file=/home/student/s3.config

Кроме последней, актуальной копии, в каталоге есть первая, ранее созданная.

student$ pg_probackup show \
  -B /probackup \
  --s3=minio \
  --s3-config-file=/home/student/s3.config
BACKUP INSTANCE 'ent-16'
==========================================================================================
====================================================
 Instance  Version  ID      Recovery Time                  Mode  WAL Mode  TLI  Time
Data   WAL  Zalg  Zratio  Start LSN  Stop LSN   Status
==========================================================================================
====================================================
 ent-16    16       TIUQ8X  2026-07-27 23:58:21.255063+03  FULL  STREAM    1/0   13s
328MB  16MB  none    1,00  0/E000028  0/E0001A0  OK
 ent-16    16       TIUQ8D  2026-07-27 23:58:05.547695+03  FULL  STREAM    1/0   17s
328MB  16MB  none    1,00  0/C000028  0/C001820  OK

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

student$ pg_probackup delete \
  -B /probackup --instance=ent-16 \
  --delete-expired \
  --retention-redundancy=1 \
  --s3=minio \
  --s3-config-file=/home/student/s3.config

Проверим состояние каталога резервных копий:

student$ pg_probackup show \
  -B /probackup \
  --s3=minio \
  --s3-config-file=/home/student/s3.config
BACKUP INSTANCE 'ent-16'
==========================================================================================
====================================================
 Instance  Version  ID      Recovery Time                  Mode  WAL Mode  TLI  Time
Data   WAL  Zalg  Zratio  Start LSN  Stop LSN   Status
==========================================================================================
====================================================
 ent-16    16       TIUQ8X  2026-07-27 23:58:21.255063+03  FULL  STREAM    1/0   13s
328MB  16MB  none    1,00  0/E000028  0/E0001A0  OK

Осталась единственная полная резервная копия. Зафиксируем политику удержания единственной полной копии в конфигурации.

student$ pg_probackup set-config \
  -B /probackup --instance ent-16 \
  --retention-redundancy=1 \
  --s3=minio \
  --s3-config-file=/home/student/s3.config

Сделаем полную копию со сжатием. Укажем, что должны быть удалены устаревшие в соответствии с политикой хранения копии и сегменты WAL.

student$ pg_probackup backup \
  -b FULL \
  -B /probackup --instance ent-16 \

Page 11: Page 11

  --stream \
  --compress \
  --delete-expired \
  --delete-wal \
  --s3=minio \
  --s3-config-file=/home/student/s3.config

Теперь в каталоге осталась последняя полная резервная копия.

student$ pg_probackup show \
  -B /probackup \
  --s3=minio \
  --s3-config-file=/home/student/s3.config
BACKUP INSTANCE 'ent-16'
==========================================================================================
=====================================================
 Instance  Version  ID      Recovery Time                  Mode  WAL Mode  TLI  Time
Data   WAL  Zalg  Zratio  Start LSN   Stop LSN    Status
==========================================================================================
=====================================================
 ent-16    16       TIUQ9F  2026-07-27 23:58:38.548337+03  FULL  STREAM    1/0   12s
83MB  16MB  zstd    3,93  0/10000028  0/100001E8  OK

Page 12: Журнал

Журнал сообщений

  • Ротация журнала

Page 13: Журнал сообщений

По умолчанию pg\_probackup выводит сообщения в stderr

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

Комментарии преподавателя

В pg_probackup определены следующие уровни важности сообщений: verbose, log, info, warning, error и off. Каждый уровень включает все последующие, и с каждым следующим уровнем объем сообщений уменьшается. При работе pg_probackup сообщения с уровнем важности info и выше выводятся по умолчанию на консоль в стандартный поток вывода ошибок stderr.

Уровень сообщений настраивается отдельно для консоли (–log-level-console) и для файла журнала (–log-level-file).

Для записи сообщений в журнал следует задать имя файла с помощью параметра –log-filename. По умолчанию файл располагается в подкаталоге log каталога резервных копий, параметром –log-directory расположение можно переопределить.

Сообщения как на консоль, так и в журнал, можно выводить в обычном текстовом виде или в формате JSON.

Настройкой –error-log-filename можно указать шаблон имен файлов для сообщений об ошибках.

Чтобы журналы сообщений не разрастались бесконтрольно, нужно настроить ротацию. Она может происходить по достижении указанного размера файла журнала (–log-rotation-size), либо по достижении максимального времени записи в файл (–log-rotation-age).

https://postgrespro.ru/docs/enterprise/16/app-pgprobackup#PBK-LOGGING-OPTS

Page 14: Настройка журнала сообщений

Настроим параметры для журнала сообщений: –log-level-file — уровень сообщений, которые будут выводиться в файл журнала; –log-level-console — уровень сообщений, которые будут выводиться на консоль; –log-filename — имя файла журнала (по умолчанию в подкаталоге log, расположение можно изменить с помощью – log-directory); –log-rotation-size — размер файла, при превышении которого выполняется ротация. Нам потребуется каталог для хранения файлов журналов:

student$ mkdir /home/student/log
student$ pg_probackup set-config \
  -B /probackup \
  --instance ent-16 \
  --log-directory=/home/student/log \
  --log-filename=probackup.log \
  --log-rotation-size=500kB \
  --log-level-file=info \
  --log-level-console=WARNING \
  --s3=minio \
  --s3-config-file=/home/student/s3.config

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

student$ pg_probackup backup \
  -b FULL \
  -B /probackup \
  --instance ent-16 \
  --stream \
  --compress \
  --delete-expired \
  --delete-wal \
  --s3=minio \
  --s3-config-file=/home/student/s3.config

Заглянем в каталог с журналом:

student$ ls -lh /home/student/log
total 8,0K
-rw-rw-r-- 1 student student 2,7K июл 27 23:59 probackup.log
-rw-rw-r-- 1 student student   10 июл 27 23:58 probackup.log.rotation

Еще раз выполним резервное копирование, включив подробный вывод сообщений в журнал.

student$ pg_probackup backup \
  -b FULL \
  -B /probackup \
  --instance ent-16 \
  --stream \
  --compress \
  --delete-expired \
  --delete-wal \
  --log-level-file=VERBOSE \
  --s3=minio \
  --s3-config-file=/home/student/s3.config

В журнал записалось большое количество сообщений.

student$ ls -lh /home/student/log
total 852K
-rw-rw-r-- 1 student student 848K июл 27 23:59 probackup.log
-rw-rw-r-- 1 student student   10 июл 27 23:59 probackup.log.rotation

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

student$ pg_probackup backup \
  -b FULL \
  -B /probackup \
  --instance ent-16 \
  --stream \
  --compress \
  --delete-expired \

Page 15: Page 15

  --delete-wal \
  --s3=minio \
  --s3-config-file=/home/student/s3.config

В результате ротации журнала старые сообщения были стерты и размер файла уменьшился.

student$ ls -lh /home/student/log
total 8,0K
-rw-rw-r-- 1 student student 2,4K июл 27 23:59 probackup.log
-rw-rw-r-- 1 student student   10 июл 27 23:59 probackup.log.rotation

Корзина bkp нам больше не понадобится, удалим ее:

student$ /usr/local/bin/mc --quiet rb --force local/bkp
Removed `local/bkp` successfully.

Page 16: Итоги

Каталог резервных копий может размещаться в облаке S3

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

Page 17: Практика

  1. Инициализируйте каталог резервных копий и добавьте в него экземпляр, подготовив его для локальной работы. 2. Настройте вывод сообщений в журнал и политику хранения единственной полной копии. 3. Проверьте удаление лишних копий, несколько раз выполнив полное копирование. Найдите в журнале сообщений соответствующие строки.

Комментарии преподавателя

  1. Инициализируйте каталог с помощью pg_probackup init, а затем добавьте в него экземпляр командой pg_probackup add-instance. Зарегистрировав роль backup, создайте базу данных для подключения из шаблона templateprobackup.

  2. При выполнении полного резервного копирования используйте параметр –stream.

Page 18: 1. Подготовка каталога резервных копий

Подготовим каталог в файловой системе.

student$ sudo mkdir /var/probackup
student$ sudo chown student: /var/probackup

Инициализируем каталог копий.

student$ pg_probackup init -B /var/probackup
INFO: Backup catalog '/var/probackup' successfully initialized

Зарегистрируем экземпляр.

student$ pg_probackup add-instance \
  -B /var/probackup --instance ent-16 \
  -D /var/lib/pgpro/ent-16
INFO: Instance 'ent-16' successfully initialized

Создадим базу данных для подключения.

student$ psql
=> CREATE DATABASE pgpro_probackup_3 OWNER backup TEMPLATE templateprobackup;
CREATE DATABASE
=> \c pgpro_probackup_3
You are now connected to database "pgpro_probackup_3" as user "student".

2. Настройка журнала сообщений

Установим политику хранения единственной полной копии и вывод сообщений в журнал.

student$ pg_probackup set-config \
  -B /var/probackup --instance ent-16 \
  -d pgpro_probackup_3 \
  -U backup \
  --retention-redundancy=1 \
  --log-filename=probackup.log \
  --log-level-file=info \
  --log-level-console=WARNING

3. Полная копия

Сформируем полную копию:

student$ pg_probackup backup \
  -B /var/probackup --instance ent-16 \
  -b FULL \
  --stream

Проверим каталог копий.

student$ pg_probackup show \
  -B /var/probackup --instance ent-16
==========================================================================================
====================================================
 Instance  Version  ID      Recovery Time                  Mode  WAL Mode  TLI  Time
Data   WAL  Zalg  Zratio  Start LSN  Stop LSN   Status
==========================================================================================
====================================================
 ent-16    16       TIURC3  2026-07-28 00:21:44.611950+03  FULL  STREAM    1/0   30s
328MB  16MB  none    1,00  0/C000028  0/C0017D8  OK

Еще одна полная копия.

student$ pg_probackup backup \
  -B /var/probackup --instance ent-16 \
  -b FULL \
  --stream \

Page 19: Page 19

  --delete-expired

Проверим каталог копий — предыдущая копия удалена:

student$ pg_probackup show \
  -B /var/probackup --instance ent-16
==========================================================================================
====================================================
 Instance  Version  ID      Recovery Time                  Mode  WAL Mode  TLI  Time
Data   WAL  Zalg  Zratio  Start LSN  Stop LSN   Status
==========================================================================================
====================================================
 ent-16    16       TIURCX  2026-07-28 00:22:10.953566+03  FULL  STREAM    1/0   22s
328MB  16MB  none    1,00  0/E000028  0/E0001E8  OK

Выведем последние строки журнала сообщений:

student$ tail -n 7 /var/probackup/log/probackup.log
2026-07-28 00:22:31 MSK [901065]: INFO: Evaluate backups by retention
2026-07-28 00:22:31 MSK [901065]: INFO: Backup TIURCX, mode: FULL, status: OK.
Redundancy: 1/1, Time Window: 0d/0d. Active
2026-07-28 00:22:31 MSK [901065]: INFO: Backup TIURC3, mode: FULL, status: OK.
Redundancy: 2/1, Time Window: 0d/0d. Expired
2026-07-28 00:22:31 MSK [901065]: INFO: Delete: TIURC3 2026-07-28 00:21:44+03
2026-07-28 00:22:31 MSK [901065]: INFO: There are no backups to merge by retention policy
2026-07-28 00:22:31 MSK [901065]: INFO: Purging finished
2026-07-28 00:22:31 MSK [901065]: INFO: There is no WAL to purge by retention policy

Уборка

Удалим созданную базу данных и каталог копий.

=> \c student
You are now connected to database "student" as user "student".
=> DROP DATABASE pgpro_probackup_3 WITH (FORCE);
DROP DATABASE
student$ sudo rm -rf /var/probackup

Page 20: Практика PPEM

  1. Создайте S3-хранилище резервных копий для экземпляра ent-16 и назовите его ent-16_minio, а каталог копий — /backup_s3. Определите политику хранения только одной полной копии с удалением просроченных. 2. Сделайте автономную резервную копию основного экземпляра ent-16 в созданное хранилище. 3. В базе данных student основного экземпляра создайте новую таблицу в схеме public с одним целочисленным столбцом (первичным ключом). 4. Сделайте разностную резервную копию основного экземпляра в S3-хранилище. 5. Убедитесь, что установленная в п. 1 политика хранения действует.

Комментарии преподавателя

  1. Используйте ПО MinIO, установленное в виртуальной машине курса. Для доступа к хранилищу нужно запустить соответствующую службу: sudo systemctl start minio

Настройки доступа к хранилищу возьмите из демонстрации.

В параметрах хранения укажите число полных копий — 1, просроченные копии — удалять.

  1. При отсутствии архивации WAL нужно оставить включенным параметр «создать автономную копию».

Page 21: 1. Создание S3-хранилища копий

В навигационной панели выберите вкладку «Резервное копирование», в ней — «Хранилища РК» и нажмите «Добавить хранилище». Выберите тип «S3 хранилище», заполните поля открывшейся формы, уделив особое внимание разделу «Параметры хранения» и нажмите «Добавить».

2. Автономная резервная копия

Выберите вкладку «Резервные копии» и нажмите кнопку «Создать резервную копию». Далее в последовательно открывающихся формах заполните необходимые поля и нажмите «Выполнить РК».

3. Изменения в данных экземпляра (создание таблицы)

Во вкладке «Базы данных» выберите базу student, а в ней схему public. Нажмите «Создать таблицу», заполните поля формы и нажмите «Сохранить».

4. Разностная резервная копия

Из вкладки «Резервные копии» выполните создание разностной копии, выбрав режим копирования delta.

5. Действие политики хранения

Выполните еще одно полное резервное копирование экземпляра и проверьте список резервных копий.

21 Занятие 21: Отказоустойчивые решения / Кластер multimaster

Исходная страница в нативном Markdown: Отказоустойчивые решения / Кластер multimaster
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_21_multimaster.html Archive: PGPRO-handouts-16.zip Страниц PDF: 27

Page 1: Отказоустойчивые решения / Кластер multimaster

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Возможности и ограничения
  • Архитектура и принцип работы
  • Распределенные транзакции
  • Поколения узлов
  • Проверка состояния узлов
  • Отказ узла
  • Режим 2+1

Page 3: Возможности

Синхронный симметричный кластер

  • на всех узлах одинаковые данные, все узлы ведущие
  • Распределенные транзакции
    • все узлы работают как единая СУБД с ACID-гарантиями
  • Отказоустойчивость и высокая доступность
  • Масштабируемость чтения

Комментарии преподавателя

Расширение multimaster позволяет преобразовать сервер в синхронный симметричный кластер без разделения ресурсов — данные будут одинаковы на всех узлах кластера, клиенты могут читать и писать данные на любой из узлов. В кластере поддерживаются распределенные транзакции, дающие обычные гарантии ACID, поэтому приложения могут работать с кластером как с единой СУБД.

Multimaster обеспечивает отказоустойчивость за счет автоматического восстановления данных на вернувшихся в строй узлах; кластер имеет высокую доступность — у него нулевое время простоя. Менять конфигурацию (в том числе и обновлять в рамках одной и той же основной версии) узлов можно без остановки обслуживания. Перенаправляя на разные узлы читающие транзакции, можно повысить пропускную способность таких операций.

https://postgrespro.ru/docs/enterprise/16/multimaster

Page 4: Ограничения

Не поддерживаются

  • ОС Windows и решения 1С
  • репликация нескольких баз данных
  • большие объекты (Large Objects)
  • операции неблокирующего построения и перестроения индексов
  • и работы с табличными пространствами
  • Особенности работы с данными
    • транзакции могут занимать длительное время
    • на уровне Read Committed могут происходить сбои сериализации
    • меняется поведение последовательностей
    • OID могут отличаться на разных узлах
  • Некоторые другие особенности и ограничения

Комментарии преподавателя

У расширения есть ряд ограничений.

Не поддерживается ОС Windows и продукты 1С.

Можно реплицировать только одну базу данных (но можно сделать несколько кластеров для разных баз данных).

Не поддерживаются большие объекты (Large Objects).

Не поддерживаются операции CREATE INDEX и REINDEX с предложением CONCURRENTLY, CREATE и DROP TABLESPACE.

Так как каждая транзакция подтверждается несколькими узлами, это занимает некоторое время. Чем больше узлов в кластере, тем дольше будет происходить фиксация изменений.

На уровне изоляции Read Committed транзакции могут завершаться ошибкой сериализации.

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

Идентификаторы OID могут быть различными на разных узлах.

Существуют и некоторые другие ограничения использования мультимастера.

https://postgrespro.ru/docs/enterprise/16/multimaster#MULTIMASTER-LIMIT ATIONS

Page 5: Подготовка сервера

Остановим основной сервер.

student$ sudo systemctl stop postgrespro-ent-16.service

В этой демонстрации часть команд выполняется от имени пользователя ОС postgres. Обращайте внимание на приглашения перед командами. Для текущей демонстрации инициализированы и настроены три экземпляра Postgres Pro Enterprise. Каталоги данных и порты: /var/lib/pgpro/ent-16-MM-1, порт 5001 /var/lib/pgpro/ent-16-MM-2, порт 5002 /var/lib/pgpro/ent-16-MM-3, порт 5003 Для самостоятельной настройки кластеров обратитесь к решению практики. Дополнительные параметры первого экземпляра:

/var/lib/pgpro/ent-16-MM-1/conf.d/multimaster.conf
cluster_name = 'node1'
port = 5001
shared_preload_libraries = 'multimaster'
wal_level = logical
max_connections = 100           # значение по умолчанию
max_prepared_transactions = 300
max_wal_senders = 10            # значение по умолчанию
max_replication_slots = 10      # значение по умолчанию
wal_sender_timeout = 0
max_worker_processes = 320

Остальные экземпляры настроены аналогично.

Инициализация кластера multimaster

Узлы кластера уже подготовлены, теперь необходимо инициализировать весь кластер multimaster из трех узлов:

student$ psql -p 5001 -U postgres
student$ psql -p 5002 -U postgres
student$ psql -p 5003 -U postgres

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

=> CREATE USER multimaster_user WITH SUPERUSER;
CREATE ROLE
=> CREATE DATABASE pgpro_multimaster OWNER multimaster_user;
CREATE DATABASE
=> \c pgpro_multimaster multimaster_user
You are now connected to database "pgpro_multimaster" as user "multimaster_user".

Для второго узла:

=> CREATE USER multimaster_user WITH SUPERUSER;
CREATE ROLE
=> CREATE DATABASE pgpro_multimaster OWNER multimaster_user;
CREATE DATABASE
=> \c pgpro_multimaster multimaster_user
You are now connected to database "pgpro_multimaster" as user "multimaster_user".

И для третьего узла:

=> CREATE USER multimaster_user WITH SUPERUSER;

Page 6: Page 6

CREATE ROLE
=> CREATE DATABASE pgpro_multimaster OWNER multimaster_user;
CREATE DATABASE
=> \c pgpro_multimaster multimaster_user
You are now connected to database "pgpro_multimaster" as user "multimaster_user".

Теперь объединим узлы в кластер. Для этого на любом из узлов добавим расширение multimaster в БД pgpro_multimaster…

=> CREATE EXTENSION multimaster;
CREATE EXTENSION
  • … и запустим функцию инициализации кластера. Первый параметр — это строка подключения к текущему узлу, второй — массив строк подключения к остальным узлам:
=> SELECT mtm.init_cluster(
     'dbname=pgpro_multimaster user=multimaster_user port=5001',
   '{"dbname=pgpro_multimaster user=multimaster_user port=5002",
     "dbname=pgpro_multimaster user=multimaster_user port=5003"}');
 init_cluster
--------------
(1 row)

Функция возвращает пустой ответ в случае успеха, либо ошибку инициализации.

Page 7: Архитектура

Source slide page 7: Архитектура{#source-figure-007 caption=“Source slide page 7: Архитектура”}

Комментарии преподавателя

Расширение multimaster состоит из патчей ядра СУБД (входят в Postgres Pro) и непосредственно кода расширения.

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

Для обеспечения согласованности данных подтверждение распределенных транзакций выполняется с помощью улучшенного трехфазного протокола фиксации (E3PC). Для этого в кластере изначально должно быть не менее трех узлов.

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

https://postgrespro.ru/docs/enterprise/16/multimaster#MULTIMASTER-ARC HITECTURE

Page 8: Процессы

Source slide page 8: Процессы{#source-figure-008 caption=“Source slide page 8: Процессы”}

Комментарии преподавателя

За разные задачи в мультимастере отвечают разные процессы:

● Процесс mtm-monitor запускается на каждом узле и управляет остальными процессами кластера.

● Процесс mtm-logrep-receiver получает поток логической репликации от walsender. Изменения применяются пулом процессов mtm-logrep-receiver-dynworker.

● Процесс mtm-dmq-sender посылает mtm-dmq-receiver служебную информацию, необходимую для реализации распределенных транзакций, а также выполняет периодический опрос доступности узлов (DMQ — distributed message queue, распределенная очередь сообщений).

● Процессы mtm-resolver и mtm-campaigner служат для согласованного исключения узлов из кластера и возвращения узлов в кластер; им отвечает процесс mtm-replier.

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

https://postgrespro.ru/docs/enterprise/16/multimaster#MULTIMASTER-BGW ORKERS

Page 9: Процессы

Посмотрим на процессы, которые запускаются на каждом узле кластера (postmaster.pid находится в первой строке файла postmaster.pid, расположенного в каталоге PGDATA):

student$ ps -o pid,command --ppid 852417
    PID COMMAND
 852421 postgres: node1: checkpointer
 852422 postgres: node1: background writer
 852424 postgres: node1: walwriter
 852425 postgres: node1: autovacuum launcher
 852426 postgres: node1: cfs-worker-0
 852428 postgres: node1: mtm-dmq-sender
 852429 postgres: node1: logical replication launcher
 853698 postgres: node1: multimaster_user pgpro_multimaster [local] idle
 853972 postgres: node1: mtm-monitor
 854152 postgres: node1: mtm-replier
 854153 postgres: node1: mtm-campaigner
 854154 postgres: node1: mtm-resolver
 854155 postgres: node1: mtm-dmq-receiver-1-3
 854157 postgres: node1: mtm-dmq-receiver-1-2
 854377 postgres: node1: walsender multimaster_user pgpro_multimaster [local]
START_REPLICATION
 854462 postgres: node1: mtm-logrep-receiver-1-2
 854463 postgres: node1: walsender multimaster_user pgpro_multimaster [local]
START_REPLICATION
 854493 postgres: node1: mtm-logrep-receiver-1-3

Количество процессов довольно велико, следует обеспечить их достаточными ресурсами.

Page 10: Фиксация изменений

Source slide page 10: Фиксация изменений{#source-figure-010 caption=“Source slide page 10: Фиксация изменений”}

Комментарии преподавателя

Протокол трехфазной фиксации расширяет стандартный протокол двухфазной фиксации, реализованный в PostgreSQL, и работает следующим образом.

  1. Prepare. Когда на узле (который считается координатором транзакции) выполняется оператор COMMIT, транзакция подготавливается (PREPARE TRANSACTION) и передается на остальные доступные узлы. Эти узлы отвечают подтверждением или отказом.

Если какой-либо узел ответил отказом, транзакция обрывается.

  1. Precommit. Узел-координатор рассылает остальным узлам команду Precommit, которая выражает намерение зафиксировать транзакцию.

Для продолжения достаточно, чтобы подтверждением ответило большинство узлов (кворум). Если достаточного количества подтверждений не набирается, транзакция обрывается (ROLLBACK PREPARED).

  1. Commit. Транзакция фиксируется на всех узлах (COMMIT PREPARED).

В отличие от двухфазной фиксации, в трехфазную добавлен этап Precommit. Если после него произойдет сбой одного из узлов и третий этап Commit не будет выполнен на сбойном узле, остальные узлы, не дождавшись подтверждения, самостоятельно зафиксируют транзакцию.

https://www.sciencedirect.com/science/article/pii/S0022000098915665

Page 11: Поколения узлов

Source slide page 11: Поколения узлов{#source-figure-011 caption=“Source slide page 11: Поколения узлов”}

Комментарии преподавателя

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

Узел всегда входит в какое-либо поколение. Если узел узнает, что существует поколение с бо́льшим номером, он старается перейти в него.

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

Транзакцию можно зафиксировать только после того, как она будет подготовлена на всех представителях поколения.

На слайде показано поколение 1 в кластере из трех узлов.

Page 12: Проверка состояния кластера

Проверим статус узла кластера:

=> SELECT * FROM mtm.status() \gx
-[ RECORD 1 ]------+--------
my_node_id         | 1
status             | online
connected          | {1,2,3}
gen_num            | 1
gen_members        | {1,2,3}
gen_members_online | {1,2,3}
gen_configured     | {1,2,3}

На любом из узлов кластера можно получить список всех узлов:

=> SELECT id, conninfo, enabled, connected FROM mtm.nodes() \gx
-[ RECORD 1 ]-------------------------------------------------------
id        | 1
conninfo  | dbname=pgpro_multimaster user=multimaster_user port=5001
enabled   | t
connected | t
-[ RECORD 2 ]-------------------------------------------------------
id        | 2
conninfo  | dbname=pgpro_multimaster user=multimaster_user port=5002
enabled   | t
connected | t
-[ RECORD 3 ]-------------------------------------------------------
id        | 3
conninfo  | dbname=pgpro_multimaster user=multimaster_user port=5003
enabled   | t
connected | t

Проверим работу кластера — создадим таблицу на первом узле. Рекомендуется использовать таблицы с явно определенным первичным ключом — мы так и поступим:

=> CREATE TABLE multimaster_tbl (id integer PRIMARY KEY);
CREATE TABLE

Добавим в таблицу несколько строк:

=> INSERT INTO multimaster_tbl VALUES (1),(2),(3);
INSERT 0 3

Проверим доступность таблицы multimaster_tbl на втором узле:

=> SELECT * FROM multimaster_tbl;
 id
----
  1
  2
  3
(3 rows)

Таблица и строки доступны на втором узле — кластер работает.

Page 13: Отказ узла

Source slide page 13: Отказ узла{#source-figure-013 caption=“Source slide page 13: Отказ узла”}

Комментарии преподавателя

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

https://postgrespro.ru/docs/enterprise/16/multimaster#MULTIMASTER-FAIL URE-DETECTION-AND-RECOVERY

Page 14: Отказ узла

Для демонстрации отказа узла остановим экземпляр Postgres Pro на третьем сервере:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-MM-3 \
  stop
waiting for server to shut down........ done
server stopped

Проверим доступные узлы кластера:

=> SELECT * FROM mtm.status() \gx
-[ RECORD 1 ]------+--------
my_node_id         | 1
status             | online
connected          | {1,2}
gen_num            | 2
gen_members        | {1,2}
gen_members_online | {1,2}
gen_configured     | {1,2,3}

Номер поколения увеличился. Запишем еще три строки в таблицу, теперь на втором узле:

=> INSERT INTO multimaster_tbl VALUES (11),(22),(33);
INSERT 0 3

Проверим корректную работу кластера — прочитаем все строки на первом узле:

=> SELECT * FROM multimaster_tbl;
 id
----
  1
  2
  3
 11
 22
 33
(6 rows)

Добавленные строки видны — кластер продолжает работать даже при отсутствии одного из узлов.

Page 15: Восстановление узла

Source slide page 15: Восстановление узла{#source-figure-015 caption=“Source slide page 15: Восстановление узла”}

Комментарии преподавателя

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

После окончания восстановления третий узел предлагает включить его в кластер, формируя новое поколение 3.

Page 16: Восстановление узла

Текущее состояние узлов кластера:

=> SELECT * FROM mtm.status() \gx
-[ RECORD 1 ]------+--------
my_node_id         | 1
status             | online
connected          | {1,2}
gen_num            | 2
gen_members        | {1,2}
gen_members_online | {1,2}
gen_configured     | {1,2,3}

Вернем третий узел в состав кластера. Для этого запустим экземпляр Postgres Pro на третьем узле:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-MM-3 \
  -l /var/lib/pgpro/ent-16-MM-3/pgpro-ent.log \
  start
waiting for server to start.... done
server started

Сразу после подключения видим, что третий узел пока что не готов к работе:

student$ psql -p 5003 -U postgres
=> SELECT * FROM mtm.status() \gx
-[ RECORD 1 ]------+--------
my_node_id         | 1
status             | online
connected          | {1,2,3}
gen_num            | 2
gen_members        | {1,2}
gen_members_online | {1,2}
gen_configured     | {1,2,3}

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

=> SELECT * FROM mtm.status() \gx
-[ RECORD 1 ]------+--------
my_node_id         | 1
status             | online
connected          | {1,2,3}
gen_num            | 3
gen_members        | {1,2,3}
gen_members_online | {1,2,3}
gen_configured     | {1,2,3}

Все добавленные ранее строки видны на третьем узле.

=> \c pgpro_multimaster multimaster_user
You are now connected to database "pgpro_multimaster" as user "multimaster_user".
=> SELECT * FROM multimaster_tbl;
 id
----
  1
  2
  3
 11
 22
 33
(6 rows)

Page 17: Page 17

Page 18: Режим 2+1

Source slide page 18: Режим 2+1{#source-figure-018 caption=“Source slide page 18: Режим 2+1”}

Комментарии преподавателя

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

Помимо этого, существует режим работы кластера мультимастера 2+1 (его еще называют режимом работы с рефери). В этом случае в кластер входят два полноценных узла, а третий узел работает только как голосующий — он принимает решения о подтверждении и отмене транзакций.

На узле-рефери достаточно установить Postgres Pro Enterprise и добавить расширение referee. Узел-рефери не хранит никакие данные кластера, поэтому нагрузка на узел невелика и он не требователен к ресурсам.

https://postgrespro.ru/docs/enterprise/16/multimaster#SETTING-UP-A-REF EREE

Page 19: Режим 2+1

Source slide page 19: Режим 2+1{#source-figure-019 caption=“Source slide page 19: Режим 2+1”}

Для перехода в режим работы 2+1 удалим из кластера один из узлов (третий):

=> SELECT mtm.drop_node(3);
 drop_node
-----------
(1 row)

На удаленном узле добавим расширение referee:

=> CREATE EXTENSION referee;
CREATE EXTENSION

Остановим остальные два узла:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-MM-1 \
  stop
waiting for server to shut down........ done
server stopped
postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-MM-2 \
  stop
waiting for server to shut down........ done
server stopped

На каждом из двух узлов в файл конфигурации необходимо добавить строку подключения к узлу-рефери:

/var/lib/pgpro/ent-16-MM-1/conf.d/referee.conf
multimaster.referee_connstring = 'dbname=pgpro_multimaster user=multimaster_user
port=5003'
/var/lib/pgpro/ent-16-MM-2/conf.d/referee.conf
multimaster.referee_connstring = 'dbname=pgpro_multimaster user=multimaster_user
port=5003'

Запускаем любой из основных узлов. Например второй:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-MM-2 \
  -l /var/lib/pgpro/ent-16-MM-2/pgpro-ent.log \
  start
waiting for server to start.... done
server started
student$ psql -p 5002 -U multimaster_user -d pgpro_multimaster

Запускаем другой узел:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-MM-1 \
  -l /var/lib/pgpro/ent-16-MM-1/pgpro-ent.log \
  start
waiting for server to start.... done
server started
student$ psql -p 5001 -U multimaster_user -d pgpro_multimaster

Проверим статус кластера:

=> SELECT * FROM mtm.status() \gx

Page 20: Page 20

-[ RECORD 1 ]------+-------
my_node_id         | 2
status             | online
connected          | {1,2}
gen_num            | 6
gen_members        | {1,2}
gen_members_online | {1,2}
gen_configured     | {1,2}

Проверим также, что ранее созданная таблица на месте:

=> SELECT * FROM multimaster_tbl;
 id
----
  1
  2
  3
 11
 22
 33
(6 rows)

Снова добавим строки в таблицу multimaster_tbl:

=> INSERT INTO multimaster_tbl VALUES (111),(222),(333);
INSERT 0 3
=> SELECT * FROM multimaster_tbl;
 id
-----
   1
   2
   3
  11
  22
  33
 111
 222
 333
(9 rows)

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

student$ sudo grep referee /var/lib/pgpro/ent-16-MM-2/pgpro-ent.log
2026-07-28 00:01:13.730 MSK [856270] LOG:  [MTM] [mtm-campaigner] got referee grant in
generation num=5
2026-07-28 00:01:13.912 MSK [856272] LOG:  [MTM] [mtm-resolver] resolving as referee
winner

Первый узел сообщает, что не может получить выигрышный голос (grant) при голосовании, так как такой голос уже выдан второму узлу:

student$ sudo grep referee /var/lib/pgpro/ent-16-MM-1/pgpro-ent.log
2026-07-28 00:01:14.458 MSK [857526] WARNING:  [MTM] [mtm-campaigner] 'select
referee.request_grant(1, 5)' failed: ERROR:  grant was already issued to node 2 in
generation 5
CONTEXT:  PL/pgSQL function referee.request_grant(integer,bigint) line 19 at RAISE
2026-07-28 00:01:14.541 MSK [857526] WARNING:  [MTM] [mtm-campaigner] 'select
referee.request_grant(1, 5)' failed: ERROR:  grant was already issued to node 2 in
generation 5
CONTEXT:  PL/pgSQL function referee.request_grant(integer,bigint) line 19 at RAISE

Page 21: Итоги

Postgres Pro Multimaster — это кластерное решение со всеми ведущими узлами, обеспечивающее автоматическое восстановление узлов после сбоев

  • Не требует установки дополнительного ПО
  • Сфера применения — системы высокой доступности

Page 22: Практика

  1. Настройте кластер для режима работы 2+1. 2. Имитируйте отказ одного из двух узлов и проверьте работоспособность кластера. 3. Верните отключенный узел в строй.

Комментарии преподавателя

  1. Подготовьте три дополнительных экземпляра Postgres Pro Enterprise: для каждого из них инициализируйте каталог данных, настройте аутентификацию и задайте параметры. Подробности можно посмотреть в решении практики.

Page 23: 1. Настройка кластера для режима работы 2+1

Остановим основной сервер.

student$ sudo systemctl stop postgrespro-ent-16.service

Инициализируем кластер из двух узлов и третий узел для рефери. Сначала подготовим три дополнительных экземпляра сервера Postgres Pro. Создаем каталог PGDATA (если экземпляр работает, его предварительно надо остановить):

student$ sudo rm -rf /var/lib/pgpro/ent-16-MM-1
student$ sudo mkdir /var/lib/pgpro/ent-16-MM-1
student$ sudo chown postgres: -R /var/lib/pgpro/ent-16-MM-1

Инициализируем каталог данных:

postgres$ /opt/pgpro/ent-16/bin/initdb -U postgres -k -D /var/lib/pgpro/ent-16-MM-1
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.
initdb: --icu-locale not specified, using lc_collate
Using language tag "en-US" for ICU locale "en_US.UTF-8".
The database cluster will be initialized with this locale configuration:
  provider:    icu
  ICU locale:  en-US
  LC_COLLATE:  en_US.UTF-8
  LC_CTYPE:    en_US.UTF-8
  LC_MESSAGES: en_US.UTF-8
  LC_MONETARY: ru_RU.UTF-8
  LC_NUMERIC:  ru_RU.UTF-8
  LC_TIME:     ru_RU.UTF-8
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".
Data page checksums are enabled.
fixing permissions on existing directory /var/lib/pgpro/ent-16-MM-1 ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting default time zone ... Europe/Moscow
creating configuration files ... ok
performing automatic tuning ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok
initdb: warning: enabling "trust" authentication for local connections
initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or
--auth-local and --auth-host, the next time you run initdb.
Success. You can now start the database server using:
    /opt/pgpro/ent-16/bin/pg_ctl -D /var/lib/pgpro/ent-16-MM-1 -l logfile start

Роль multimaster_user должна иметь право использовать протокол репликации и подключаться к базе pgpro_multimaster, настройки аутентификации это разрешают:

postgres$ grep ^local /var/lib/pgpro/ent-16-MM-1/pg_hba.conf
local   all             all                                     trust
local   replication     all                                     trust

Конфигурационный файл:

postgres$ echo include_dir \'conf.d\' >> /var/lib/pgpro/ent-16-MM-1/postgresql.conf
postgres$ mkdir /var/lib/pgpro/ent-16-MM-1/conf.d
/var/lib/pgpro/ent-16-MM-1/conf.d/multimaster.conf
cluster_name = 'node1'
shared_preload_libraries = 'multimaster'

Page 24: Page 24

wal_level = logical
max_connections = 100           # значение по умолчанию
max_prepared_transactions = 300
max_wal_senders = 10            # значение по умолчанию
max_replication_slots = 10      # значение по умолчанию
wal_sender_timeout = 0
max_worker_processes = 320
port = 5001

Запускаем экземпляр:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-MM-1 \
  -l /var/lib/pgpro/ent-16-MM-1/pgpro-ent.log \
  start
waiting for server to start.... done
server started

Создаем роль и базу данных для мультимастера:

student$ psql -p 5001 -U postgres
=> CREATE USER multimaster_user WITH SUPERUSER;
CREATE ROLE
=> CREATE DATABASE pgpro_multimaster OWNER multimaster_user;
CREATE DATABASE
=> \c pgpro_multimaster multimaster_user
You are now connected to database "pgpro_multimaster" as user "multimaster_user".

Аналогично нужно подготовить еще два экземпляра на портах 5002 и 5003. На третьем узле добавим расширение referee:

student$ psql -p 5003 -U multimaster_user -d pgpro_multimaster
=> CREATE EXTENSION referee;
CREATE EXTENSION

На каждом из двух основных узлов добавим в файл конфигурации строку подключения к узлу-рефери:

/var/lib/pgpro/ent-16-MM-1/conf.d/referee.conf
multimaster.referee_connstring = 'dbname=pgpro_multimaster user=multimaster_user
port=5003'
/var/lib/pgpro/ent-16-MM-2/conf.d/referee.conf
multimaster.referee_connstring = 'dbname=pgpro_multimaster user=multimaster_user
port=5003'

Перезапустим первый и второй узлы, чтобы применить настройки:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-MM-1 \
  stop
waiting for server to shut down........ done
server stopped
postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-MM-2 \
  stop
waiting for server to shut down....... done
server stopped
postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-MM-1 \
  -l /var/lib/pgpro/ent-16-MM-1/pgpro-ent.log \
  start

Page 25: Page 25

waiting for server to start.... done
server started
student$ psql -p 5001 -U multimaster_user -d pgpro_multimaster
postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-MM-2 \
  -l /var/lib/pgpro/ent-16-MM-2/pgpro-ent.log \
  start
waiting for server to start.... done
server started
student$ psql -p 5002 -U multimaster_user -d pgpro_multimaster

Инициализируем кластер мультимастера.

=> CREATE EXTENSION multimaster;
CREATE EXTENSION

Теперь запустим функцию инициализации кластера. Первый параметр — это строка подключения к текущему узлу, второй — подключение ко второму узлу:

=> SELECT mtm.init_cluster(
     'dbname=pgpro_multimaster user=multimaster_user port=5001',
   '{"dbname=pgpro_multimaster user=multimaster_user port=5002"}');
 init_cluster
--------------
(1 row)
=> SELECT * FROM mtm.status() \gx
-[ RECORD 1 ]------+-------
my_node_id         | 1
status             | online
connected          | {1,2}
gen_num            | 3
gen_members        | {1,2}
gen_members_online | {1,2}
gen_configured     | {1,2}

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

=> CREATE TABLE multimaster_tbl (id integer PRIMARY KEY);
CREATE TABLE
=> INSERT INTO multimaster_tbl VALUES (1),(2),(3);
INSERT 0 3

Проверим доступность таблицы multimaster_tbl на втором узле:

=> SELECT * FROM multimaster_tbl;
 id
----
  1
  2
  3
(3 rows)

2. Имитация сбоя и проверка работоспособности

Имитируем отказ узла. Для этого отключим один из узлов, например, второй:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-MM-2 \
  stop
waiting for server to shut down..... done
server stopped
=> SELECT * FROM mtm.status() \gx

Page 26: Page 26

-[ RECORD 1 ]------+-------
my_node_id         | 1
status             | online
connected          | {1}
gen_num            | 4
gen_members        | {1}
gen_members_online | {1}
gen_configured     | {1,2}

Добавим еще несколько строк в таблицу:

=> INSERT INTO multimaster_tbl VALUES (11),(22),(33);
INSERT 0 3

Оставшийся узел сохранил работоспособность.

3. Возвращение узла в строй

Вернем в строй второй узел:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-MM-2 \
  -l /var/lib/pgpro/ent-16-MM-2/pgpro-ent.log \
  start
waiting for server to start.... done
server started
student$ psql -p 5002 -U multimaster_user -d pgpro_multimaster

На втором узле проверим, что таблица доступна:

=> SELECT * FROM multimaster_tbl;
 id
----
  1
  2
  3
 11
 22
 33
(6 rows)

Кластер мультимастера продолжает работать в режиме 2+1.

Уборка

Остановим и удалим экземпляры multimaster и запустим основной.

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-MM-1 \
  stop
waiting for server to shut down..... done
server stopped
postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-MM-2 \
  stop
waiting for server to shut down.... done
server stopped
postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-MM-3 \
  stop
waiting for server to shut down....... done
server stopped
student$ sudo rm -rf /var/lib/pgpro/ent-16-MM-1 /var/lib/pgpro/ent-16-MM-2 /var/lib/pgpro/ent-16-MM-3
student$ sudo systemctl start postgrespro-ent-16.service

Page 27: Page 27

22 Занятие 22: Отказоустойчивые решения / Кластер BiHA

Исходная страница в нативном Markdown: Отказоустойчивые решения / Кластер BiHA
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_22_biha.html Archive: PGPRO-handouts-16.zip Страниц PDF: 35

Page 1: Отказоустойчивые решения / Кластер BiHA

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

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

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Кластер BiHA
  • Архитектура и принцип работы
  • Конфигурация
  • Создание кластера
  • Режимы репликации
  • Отработка отказов
  • Выборы
  • Режим 2+1

Page 3: Кластер BiHA

Кластер физической репликации

  • синхронная и асинхронная репликация
  • масштабируемость по операциям чтения
  • Встроенная высокая доступность
    • автоматическое обнаружение и отработка отказа узла
    • кворум на базе алгоритма консенсуса Raft
    • не требует внешнего управления кластером
  • Работает на Linux-платформах

Комментарии преподавателя

Расширение BiHA (Built-in High Availability) позволяет организовать отказоустойчивый кластер физической репликации, включающий в себя выделенный узел-лидер и несколько узлов-последователей. Лидер передает потоки репликации на узлы-последователи, доступные только для чтения. Репликация может быть синхронной или асинхронной. Балансировка запросов на чтение между несколькими узлами позволяет увеличить пропускную способность системы.

Клиентские соединения могут направляться на определенные узлы внешним балансировщиком или пулером. В версии Postgres Pro Enterprise 17 для этого используют расширение Proxima.

https://postgrespro.ru/docs/enterprise/17/proxima#PROXIMA

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

BiHA не использует внешнее программное обеспечения для управления кластером. https://postgrespro.ru/docs/enterprise/16/biha

В настоящее время BiHA не работает в среде MS Windows.

Page 4: Архитектура

Source slide page 4: Архитектура{#source-figure-004 caption=“Source slide page 4: Архитектура”}

Комментарии преподавателя

Расширение BiHA включает в себя доработанное ядро СУБД, SQL-интерфейс управления и сам код расширения.

На каждом из узлов запускаются служебные процессы BiHA worker, координирующие работу кластера. Эти процессы обмениваются служебной информацией, используя BiHA Control Protocol (BCP). Сами данные с узла-лидера передаются на последователи с помощью штатного механизма физической репликации (поток WAL-записей).

Для инициализации кластера и управления им используется утилита bihactl. Она может создавать узел-лидер, добавлять узлы-последователи, преобразовывать уже существующий экземпляр в узел-лидер или узел-последователь в кластере, а также проверять статус узлов кластера.

https://postgrespro.ru/docs/enterprise/16/biha-architecture#BIHA-ARCHITE CTURE

Page 5: Параметры конфигурации

Source slide page 5: Параметры конфигурации{#source-figure-005 caption=“Source slide page 5: Параметры конфигурации”}

biha.nquorum = 3 biha.minnodes = 3

Комментарии преподавателя

При создании узлов кластера с помощью утилиты bihactl, кроме очевидных параметров (адрес, порт, каталог данных) нужно указать параметры, относящиеся ко всему кластеру. Самые важные из них:

● biha.nquorum — минимальное число узлов, при котором проводятся выборы лидера;

● biha.minnodes — минимальное число узлов, при котором лидер изменяет данные.

Если biha.nquorum или более связанных узлов остались без лидера, они проводят выборы нового лидера. Поведение текущего лидера от этого параметра не зависит.

Если лидер находится в группе из не менее чем biha.minnodes узлов, он обрабатывает запросы на чтение и запись, а при меньшем числе узлов — только на чтение.

По умолчанию для biha.minnodes берется значение biha.nquorum. Чтобы избежать ситуации split-brain, оба параметра рекомендуется задать как (общее число узлов + 1) / 2, округленное в большую сторону. Тогда при разделении сети лидер, оказавшись в меньшинстве, перестанет принимать запросы на запись, а большинство узлов выберет нового лидера. Однако при четном числе узлов имеет смысл задать biha.minnodes как ровно половину от общего числа узлов, чтобы разделение на две равные части не повлияло на работу лидера.

https://postgrespro.ru/docs/enterprise/16/biha-architecture#BIHA-CLUSTER-VARIANTS

Page 6: Инициализация кластера

Source slide page 6: Инициализация кластера{#source-figure-006 caption=“Source slide page 6: Инициализация кластера”}

Комментарии преподавателя

Кластер BiHA можно получить из существующей системы, использующей физическую репликацию, но мы создадим новый трехузловой кластер с нуля.

Первый узел будущего кластера необходимо инициализировать командой bihactl init. Будет создан экземпляр со служебной базой данных biha_db и схемой biha, для него будут установлены необходимые конфигурационные параметры, а также создан слот репликации.

Поскольку мы рассчитываем на три узла, параметр biha.nquorum установим равным двум (параметр biha.minnodes получит то же значение).

https://postgrespro.ru/docs/enterprise/16/biha-architecture#BIHA-POSTGRE S-CONFIGURATION

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

Далее мы будем встречаться с понятием номера поколения кластера (term). Текущее состояние соответствует нулевому поколению, в него входит единственный узел. Номер поколения автоматически увеличивается при изменении состояния кластера, например, при удалении или добавлении узла, при смене лидера.

Page 7: Подготовка BiHA-кластера

В этой демонстрации основной экземпляр PostgresPro Enterprise не используется, остановим его.

student$ sudo systemctl stop postgrespro-ent-16.service

Все узлы будут локальными экземплярами. Подготовим каталог для данных узла-лидера:

postgres$ mkdir /var/lib/pgpro/ent-16-biha-1

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

postgres$ echo '*:*:*:biha_replication_user:biha' >> ~/.pgpass
postgres$ chmod 600 ~/.pgpass

С помощью команды init утилиты bihactl инициализируем кластер и создадим в нем узел-лидер. Будет запущена утилита initdb, а также сформирована конфигурация:

postgres$ bihactl init \
  --biha-node-id=1 \
  --host=host1 \
  --port=7001 --pgdata=/var/lib/pgpro/ent-16-biha-1 \
  --biha-port=7101 \
  --nquorum=2 \
  > /var/lib/postgresql/magic_str
bihactl: password for biha_replication_user was taken from pgpass
initdb: --icu-locale not specified, using lc_collate
initdb: warning: enabling "trust" authentication for local connections
initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or
--auth-local and --auth-host, the next time you run initdb.

С помощью параметров утилиты заданы: –biha-node-id — уникальный идентификатор узла кластера; –host, –port и –pgdata — узел, порт и путь к каталогу данных создаваемого экземпляра; –biha-port — порт для обмена служебной информацией между узлами; –nquorum — значение параметра biha.nquorum для всех узлов. Команда init возвращает «магическую» строку, которая содержит данные для подключения последователей к лидеру. Строку удобно использовать в скриптах подготовки узлов-последователей, чтобы не вводить данные вручную, поэтому мы записали ее в файл /var/lib/postgresql/magic_str:

/var/lib/postgresql/magic_str
dmVyc2lvbj0xIGhvc3Q9aG9zdDEgcG9ydD03MDAxIGJpaGEtcG9ydD03MTAx

Фактически, в этой строке зашифрован набор параметров:

postgres$ cat /var/lib/postgresql/magic_str | base64 -d
version=1 host=host1 port=7001 biha-port=7101

Запустим узел.

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-1 \
  -l /var/lib/pgpro/ent-16-biha-1/pgpro-ent.log \
  start
waiting for server to start.... done
server started
student$ psql -p 7001 -h host1 -U postgres -d biha_db

Конфигурационные параметры при инициализации были записаны в дополнительные файлы, подключаемые к файлам postgresql.conf и pg_hba.conf с помощью директив include. Параметры настройки узла размещаются в подключенном файле postgresql.biha.conf:

=> SELECT name, setting
FROM pg_file_settings
WHERE sourcefile ~ 'postgresql.biha.conf';

Page 8: Page 8

           name            |   setting
---------------------------+-------------
 synchronous_standby_names |
 wal_level                 | replica
 hot_standby               | on
 max_wal_senders           | 10
 max_replication_slots     | 11
 max_slot_wal_keep_size    | 5GB
 wal_keep_size             | 1GB
 biha.id                   | 1
 application_name          | biha_node_1
 biha.port                 | 7101
 biha.host                 | host1
 port                      | 7001
 biha.nquorum              | 2
 biha.minnodes             | 2
 listen_addresses          | *
 biha.use_ssl              | 0
 primary_slot_name         | biha_node_1
(17 rows)

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

=> SELECT type, database, user_name, address, auth_method
FROM pg_hba_file_rules
WHERE file_name ~ 'pg_hba.biha.conf';
 type |   database    |        user_name        | address |  auth_method
------+---------------+-------------------------+---------+---------------
 host | {postgres}    | {biha_replication_user} | all     | scram-sha-256
 host | {biha_db}     | {biha_replication_user} | all     | scram-sha-256
 host | {replication} | {biha_replication_user} | all     | scram-sha-256
(3 rows)

Ряд параметров, критически важных для работы расширения, хранится в отдельном файле pg_biha/biha.conf, и их нельзя изменить с помощью команды ALTER SYSTEM:

/var/lib/pgpro/ent-16-biha-1/pg_biha/biha.conf
primary_conninfo=
synchronous_standby_names=
node_count=1
1:host1:7101:7001:regular
crc=2740808754

Кроме этого, в параметре shared_preload_libraries основного файла конфигурации указывается загружаемая библиотека расширения:

=> SELECT name, setting
FROM pg_file_settings
WHERE name = 'shared_preload_libraries' AND sourcefile ~ 'postgresql.conf';
           name           | setting
--------------------------+---------
 shared_preload_libraries | biha
(1 row)

Состояние узла сохраняется в файле biha.state:

/var/lib/pgpro/ent-16-biha-1/pg_biha/biha.state
node_id=1
term=0
leader_id=1
rewind_state=0
waltrim_state=0
waltrim_common_tli=0
waltrim_start_lsn=0
waltrim_end_lsn=0
node_error=0
next_tli=0
next_tle_begin=0
next_tle_end=0
cur_tli=0

Page 9: Page 9

cur_tle_begin=0
cur_tle_end=0
crc=1733808887

А состояние всего кластера можно увидеть с помощью команды bihactl status:

postgres$ bihactl status --host=host1 --port=7001
 id | leader_id | term | online |   state   | last_known_state | since_last_hb
----+-----------+------+--------+-----------+------------------+---------------
  1 |         1 |    0 | t      | LEADER_RO | LEADER_RO        |
(1 row)

Состояние LEADER_RO означает, что узел находится в режиме только для чтения. При значении biha.minnodes=1 кластер состоял бы из единственного узла со статусом RW. Ту же информацию можно получить, обратившись к представлению biha.status_v.

Page 10: Добавление узла

Source slide page 10: Добавление узла{#source-figure-010 caption=“Source slide page 10: Добавление узла”}

Комментарии преподавателя

Новый узел добавляется в кластер командой bihactl add, выполняемой на подключаемом узле; при этом в кластере будет сформировано новое поколение. Когда число узлов станет равным значению параметра biha.minnodes, кластер станет полностью работоспособным: его лидер сможет работать на запись.

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

Как говорилось, мы планируем развернуть трехузловой кластер — именно такая конфигурация обычно и эксплуатируется. Но что, если в нашем распоряжении только два узла? Какие значения параметров можно выбрать?

При biha.nquorum = 2 и biha.minnodes = 2 потеря любого узла приведет к недоступности кластера на запись.

Если установить biha.nquorum = 1 и biha.minnodes = 1, при разделении сети между двумя работоспособными узлами последователь станет лидером — возникнет ситуация split-brain с двумя работающими лидерами. Это самая плохая ситуация.

Если же установить biha.nquorum = 2 и biha.minnodes = 1, то отказ последователя не нарушит работу лидера, и при разделении сети split-brain не возникнет. Но при отказе лидера последователь не станет новым лидером.

Page 11: Два узла

Добавим в кластер узел-последователь. Используем магическую строку, полученную при инициализации кластера. Будет создана физическая резервная копия узла-лидера (по умолчанию с помощью pg_basebackup):

postgres$ bihactl add \
  --biha-node-id=2 \
  --host=host2 \
  --port=7002 --pgdata=/var/lib/pgpro/ent-16-biha-2 \
  --biha-port=7102 \
  --magic-string=dmVyc2lvbj0xIGhvc3Q9aG9zdDEgcG9ydD03MDAxIGJpaGEtcG9ydD03MTAx

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

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-2 \
  -l /var/lib/pgpro/ent-16-biha-2/pgpro-ent.log \
  start
waiting for server to start............. done
server started
=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |   state   | last_known_state |  since_last_hb
----+-----------+------+--------+-----------+------------------+-----------------
  1 |         1 |    1 | t      | LEADER_RW | LEADER_RW        |
  2 |         1 |    1 | t      | FOLLOWER  | FOLLOWER         | 00:00:01.280459
(2 rows)

Теперь кластер состоит из двух узлов. Первый из них является лидером и может теперь принимать запросы на изменение данных (LEADER_RW), а второй является последователем (FOLLOWER). Посмотрим на процессы экземпляров. Среди них есть BiHA worker, координирующие работу кластера, и обычные для физической репликации процессы.

student$ ps -o pid,command --ppid 860058  # лидер
    PID COMMAND
 860059 postgres: BiHA worker: node 1
 860061 postgres: checkpointer
 860062 postgres: background writer
 860063 postgres: cfs-worker-0
 860106 postgres: postgres biha_db 127.0.0.1(59700) idle
 861275 postgres: walwriter
 861276 postgres: autovacuum launcher
 861277 postgres: logical replication launcher
 861306 postgres: walsender biha_replication_user 127.0.0.1(59068) streaming 0/16A22E8
student$ ps -o pid,command --ppid 860879  # последователь
    PID COMMAND
 860880 postgres: BiHA worker: node 2
 860881 postgres: startup recovering 000000020000000000000001
 860882 postgres: checkpointer
 860883 postgres: background writer
 860885 postgres: cfs-worker-0
 861305 postgres: walreceiver streaming 0/16A22E8

Page 12: Три узла

Source slide page 12: Три узла{#source-figure-012 caption=“Source slide page 12: Три узла”}

Комментарии преподавателя

Добавляя дополнительные узлы в кластер, мы избавляемся от недостатка двухузловой конфигурации: в ней невозможно продолжить запись после потери сетевой связности без риска split-brain. Трех узлов уже достаточно для того, чтобы кластер оставался полностью работоспособным после одного любого сбоя.

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

Page 13: Три узла в кластере

Добавим в кластер еще один узел-последователь:

postgres$ bihactl add \
  --biha-node-id=3 \
  --host=host3 \
  --port=7003 --pgdata=/var/lib/pgpro/ent-16-biha-3 \
  --biha-port=7103 \
  --magic-string=dmVyc2lvbj0xIGhvc3Q9aG9zdDEgcG9ydD03MDAxIGJpaGEtcG9ydD03MTAx

Запустим третий узел и проверим статус кластера:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-3 \
  -l /var/lib/pgpro/ent-16-biha-3/pgpro-ent.log \
  start
waiting for server to start............. done
server started
=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |   state   | last_known_state |  since_last_hb
----+-----------+------+--------+-----------+------------------+-----------------
  1 |         1 |    2 | t      | LEADER_RW | LEADER_RW        |
  2 |         1 |    2 | t      | FOLLOWER  | FOLLOWER         | 00:00:01.008276
  3 |         1 |    2 | t      | FOLLOWER  | FOLLOWER         | 00:00:01.008276
(3 rows)

Появился еще один последователь, номер поколения увеличился. Состояние подключения узлов в кластере можно получить из представления nodes_v:

=> SELECT * FROM biha.nodes_v;
 id | host  | port | state  | since_conn_start | conn_count
----+-------+------+--------+------------------+------------
  1 | host1 | 7101 |        |                  |
  2 | host2 | 7102 | ACTIVE | 00:00:55.127388  |          1
  3 | host3 | 7103 | ACTIVE | 00:00:17.127388  |          1
(3 rows)

Проверим репликацию. Создадим базу на лидере, а в ней — таблицу:

=> CREATE DATABASE pgpro_biha;
CREATE DATABASE
=> \c pgpro_biha
You are now connected to database "pgpro_biha" as user "postgres".
=> CREATE TABLE test AS SELECT 'Проверка репликации' msg;
SELECT 1

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

student$ psql -p 7002 -h host2 -U postgres
=> \c pgpro_biha
You are now connected to database "pgpro_biha" as user "postgres".

Данные появились на втором узле:

=> SELECT * FROM test;
         msg
---------------------
 Проверка репликации
(1 row)

И на третьем:

student$ psql -p 7003 -h host3 -U postgres

Page 14: Page 14

=> \c pgpro_biha
You are now connected to database "pgpro_biha" as user "postgres".
=> SELECT * FROM test;
         msg
---------------------
 Проверка репликации
(1 row)

Page 15: Режимы репликации

Source slide page 15: Режимы репликации{#source-figure-015 caption=“Source slide page 15: Режимы репликации”}

synchronous_standby_names = ANY N MIN M (biha_node1, biha_node2, …)

Комментарии преподавателя

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

Синхронизацией управляет параметр synchronous_standby_names: при фиксации транзакции лидер будет ожидать подтверждения от любых N (ANY N) узлов из списка синхронных. Синхронный режим включается функцией biha.set_sync_standbys, которая и устанавливает число N.

Если временно доступно менее N синхронных узлов, фиксация транзакций будет приостановлена. Чтобы продолжить работу в случае временной недоступности синхронных узлов, можно включить нестрогую кворумную синхронную репликацию. Тогда лидер будет продолжать подтверждать транзакции, пока к нему подключено как минимум M синхронных узлов (MIN M). Значение устанавливается функцией biha.set_sync_standbys_min, а значение −1 возвращает кластер к строгой синхронной репликации.

После включения синхронной репликации перейти на асинхронную уже нельзя, но можно изменить список синхронных узлов функциями biha.set_ssn, biha.remove_from_ssn и biha.add_to_ssn. Узлы, не входящие в список, будут работать асинхронно.

https://postgrespro.ru/docs/enterprise/16/biha-administration#BIHA-ENABLI NG-SYNC-REPLICATION

https://postgrespro.ru/docs/enterprise/16/runtime-config-replication#GUC-SY NCHRONOUS-STANDBY-NAMES

Page 16: Синхронная репликация

Включим в кластере синхронную кворумную репликацию с подтверждением от двух узлов:

=> \c biha_db
You are now connected to database "biha_db" as user "postgres".
=> SELECT biha.set_sync_standbys(2);
 set_sync_standbys
-------------------
 t
(1 row)

Для просмотра текущего значения параметра synchronous_standby_names используем функцию biha.get_ssn:

=> SELECT * FROM biha.get_ssn();
                   get_ssn
----------------------------------------------
 ANY 2 (biha_node_1,biha_node_2,biha_node_3 )
(1 row)

Page 17: Отказ последователя

Source slide page 17: Отказ последователя{#source-figure-017 caption=“Source slide page 17: Отказ последователя”}

Комментарии преподавателя

Если последователь был настроен как асинхронный, то отказ одного узла никак не отразится на лидере, кластер продолжит свою работу.

Если же используется синхронная репликация, то при настройке synchronous_standby_names = ANY 2 отказ узла приведет к невозможности завершения транзакции: лидер должен дождаться подтверждения от двух узлов-последователей, а работает только один.

Чтобы продолжить работу в такой ситуации, можно включить нестрогую синхронную репликацию ANY 2 MIN 1.

Page 18: Отказ синхронного последователя

Нарушим кворум синхронизации, остановив второй узел:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-2 \
  stop
waiting for server to shut down........ done
server stopped

Кластер пока считает узел работающим. Подождем, пока будет обнаружен отказ…

=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |   state   | last_known_state |  since_last_hb
----+-----------+------+--------+-----------+------------------+-----------------
  1 |         1 |    3 | t      | LEADER_RW | LEADER_RW        |
  2 |         1 |    2 | f      | UNKNOWN   | FOLLOWER         | 00:00:21.846679
  3 |         1 |    3 | t      | FOLLOWER  | FOLLOWER         | 00:00:00.846679
(3 rows)

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

=> CREATE DATABASE new_db;

Смягчим ограничение, включив нестрогую кворумную синхронную репликацию:

postgres$ psql -p 7001 -d biha_db -c 'SELECT biha.set_sync_standbys_min(1)'
 set_sync_standbys_min
-----------------------
 t
(1 row)

Теперь транзакция завершена:

CREATE DATABASE

В параметре synchronous_standby_names появилось указание MIN со значением 1:

=> SELECT * FROM biha.get_ssn();
                      get_ssn
----------------------------------------------------
 ANY 2 MIN 1 (biha_node_1,biha_node_2,biha_node_3 )
(1 row)

Page 19: Отказ последователей

Source slide page 19: Отказ последователей{#source-figure-019 caption=“Source slide page 19: Отказ последователей”}

Комментарии преподавателя

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

Page 20: Отказ последователей

Сейчас один из последователей остановлен, но кластер в рабочем состоянии:

=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |   state   | last_known_state |  since_last_hb
----+-----------+------+--------+-----------+------------------+-----------------
  1 |         1 |    4 | t      | LEADER_RW | LEADER_RW        |
  2 |         1 |    2 | f      | UNKNOWN   | FOLLOWER         | 00:00:22.742462
  3 |         1 |    3 | t      | FOLLOWER  | FOLLOWER         | 00:00:01.742462
(3 rows)

Остановим оставшегося последователя:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-3 \
  stop
waiting for server to shut down.... done
server stopped

И немного подождем, чтобы кластер отработал отказ узла…

=> \c biha_db
You are now connected to database "biha_db" as user "postgres".

В строю остался лидер, но доступен он лишь для чтения:

=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |   state   | last_known_state | since_last_hb
----+-----------+------+--------+-----------+------------------+---------------
  1 |         1 |    4 | t      | LEADER_RO | LEADER_RO        |
  2 |         1 |    4 | f      | UNKNOWN   | UNKNOWN          |
  3 |         1 |    4 | f      | UNKNOWN   | UNKNOWN          |
(3 rows)

Снова запустим второй узел и дождемся его возвращения в строй.

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-2 \
  -l /var/lib/pgpro/ent-16-biha-2/pgpro-ent.log \
  start
waiting for server to start.... done
server started
student$ psql -p 7002 -h host2 -U postgres -d biha_db

Статус лидера сменился на LEADER_RW, кластер обрабатывает запросы на изменение данных:

=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |   state   | last_known_state |  since_last_hb
----+-----------+------+--------+-----------+------------------+-----------------
  1 |         1 |    4 | t      | LEADER_RW | LEADER_RW        |
  2 |         1 |    4 | t      | FOLLOWER  | FOLLOWER         | 00:00:01.187268
  3 |         1 |    4 | f      | UNKNOWN   | UNKNOWN          |
(3 rows)

Page 21: Отказ лидера

Source slide page 21: Отказ лидера{#source-figure-021 caption=“Source slide page 21: Отказ лидера”}

Комментарии преподавателя

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

Применяемый в выборах алгоритм консенсуса RAFT гарантирует однозначное определение нового лидера. Выборы проводятся в группах узлов, составляющих кворум; чтобы предотвратить ситуацию split-brain, значение параметра biha.nquorum должно превышать половину узлов.

В процессе выборов узлы выдвигают себя в качестве кандидатов на роль лидера и запрашивают голоса других узлов. Фактор победы в голосовании — наличие на узле наиболее актуальных данных, то есть новым лидером кластера становится последователь с наибольшим числом записей в WAL. В синхронном кластере можно задать для узлов приоритет, определяющий порядок выдвижения на роль лидера: для этого в параметре biha.node_priority узла указывается тайм-аут, по истечении которого узел предложит себя в качестве кандидата.

При необходимости можно вообще запретить узлу становиться лидером или участвовать в голосовании — для этого существуют параметры biha.can_be_leader и biha.can_vote.

https://postgrespro.ru/docs/enterprise/16/biha-architecture#BIHA-ELECTION S

Page 22: Новый лидер

Source slide page 22: Новый лидер{#source-figure-022 caption=“Source slide page 22: Новый лидер”}

Комментарии преподавателя

Помимо автоматического переключения узлов, BiHA поддерживает ручное переключение. Вручную назначить нового лидера можно с помощью функции biha.set_leader; ее рекомендуется вызывать на том узле, который планируется сделать лидером.

https://postgrespro.ru/docs/enterprise/16/biha-administration#BIHA-SWITCH OVER

При изменении состояния кластера есть возможность вызывать на узлах заранее зарегистрированные пользовательские функции — обработчики событий. Это функции для обратного вызова (callback-функции), которые могут выполнить дополнительные действия, например, отправить оповещение администратору, уведомить об изменениях внешнее ПО, управляющее подключением клиентов (различные прокси и балансировщики), или обеспечить ограждение (fencing) старого лидера.

https://postgrespro.ru/docs/enterprise/16/biha-administration#BIHA-CALLBA CKS

Page 23: Возвращение узла

Source slide page 23: Возвращение узла{#source-figure-023 caption=“Source slide page 23: Возвращение узла”}

Комментарии преподавателя

После переключения ролей узлов BiHA выполняет синхронизацию данных кластера в изменившейся конфигурации. Чаще всего на последователях требуется применение записей WAL, полученных с нового лидера. Однако в случае, когда в состав кластера возвращается старый лидер, может потребоваться обратное действие — его «откат» до состояния нового, который выполняется утилитой pg_rewind.

Автоматическое выполнение отката определяется параметром biha.autorewind и по умолчанию выключено. Так сделано потому, что существует риск потери пользовательских транзакций в журнале предзаписи на синхронизируемом узле. Кроме этого, при неудачной автоматической синхронизации узел может перейти в состояние ошибки, и для возвращения его в строй потребуется вмешательство администратора.

https://postgrespro.ru/docs/enterprise/16/biha-administration#BIHA-AUTOM ATIC-CLUSTER-SYNCHRONIZATION-AFTER-FAILOVER

Page 24: Отказ лидера

Сейчас в кластере два узла:

=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |   state   | last_known_state |  since_last_hb
----+-----------+------+--------+-----------+------------------+-----------------
  1 |         1 |    4 | t      | LEADER_RW | LEADER_RW        |
  2 |         1 |    4 | t      | FOLLOWER  | FOLLOWER         | 00:00:01.322084
  3 |         1 |    4 | f      | UNKNOWN   | UNKNOWN          |
(3 rows)

Теперь остановим лидера.

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-1 \
  stop
waiting for server to shut down.......... done
server stopped
=> \c biha_db
You are now connected to database "biha_db" as user "postgres".

После небольшого ожидания посмотрим на втором узле, в каком состоянии находится кластер…

=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |  state   | last_known_state |  since_last_hb
----+-----------+------+--------+----------+------------------+-----------------
  1 |         1 |    4 | f      | UNKNOWN  | LEADER_RO        | 00:00:11.846925
  2 |         1 |    4 | t      | FOLLOWER | FOLLOWER         |
  3 |         1 |    3 | f      | UNKNOWN  | UNKNOWN          |
(3 rows)

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

=> SELECT biha.set_leader(2);
 set_leader
------------
 t
(1 row)

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

=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |   state   | last_known_state |  since_last_hb
----+-----------+------+--------+-----------+------------------+-----------------
  1 |         1 |    4 | f      | UNKNOWN   | UNKNOWN          | 00:00:12.790722
  2 |         2 |    5 | t      | LEADER_RO | LEADER_RO        |
  3 |         1 |    3 | f      | UNKNOWN   | UNKNOWN          |
(3 rows)

Снова включим первый узел.

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-1 \
  -l /var/lib/pgpro/ent-16-biha-1/pgpro-ent.log \
  start
waiting for server to start.... done
server started
student$ psql -p 7001 -h host1 -U postgres -d biha_db

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

Page 25: Page 25

Таким образом, узлы поменялись ролями.

=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |   state   | last_known_state |  since_last_hb
----+-----------+------+--------+-----------+------------------+-----------------
  1 |         2 |    5 | t      | FOLLOWER  | FOLLOWER         | 00:00:00.573122
  2 |         2 |    5 | t      | LEADER_RW | LEADER_RW        |
  3 |         1 |    3 | f      | UNKNOWN   | UNKNOWN          |
(3 rows)

Вернем в строй и третий узел, запретив ему становиться лидером:

/var/lib/pgpro/ent-16-biha-3/postgresql.auto.conf
biha.can_be_leader = false
postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-3 \
  -l /var/lib/pgpro/ent-16-biha-3/pgpro-ent.log \
  start
waiting for server to start.... done
server started

В кластере снова три узла. Лидером по-прежнему выступает второй узел, все узлы находятся в одном новом поколении:

=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |   state   | last_known_state |  since_last_hb
----+-----------+------+--------+-----------+------------------+-----------------
  1 |         2 |    5 | t      | FOLLOWER  | FOLLOWER         | 00:00:00.374409
  2 |         2 |    5 | t      | LEADER_RW | LEADER_RW        |
  3 |         2 |    5 | t      | FOLLOWER  | FOLLOWER         | 00:00:01.374409
(3 rows)

Если теперь остановить второй узел (лидера), то оставшиеся узлы автоматически выберут нового лидера из своего числа:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-2 \
  stop
waiting for server to shut down.......... done
server stopped
student$ psql -p 7003 -h host3 -U postgres -d biha_db

Немного подождем…

=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |   state   | last_known_state |  since_last_hb
----+-----------+------+--------+-----------+------------------+-----------------
  1 |         1 |    4 | t      | LEADER_RO | LEADER_RO        | 00:00:00.692224
  2 |         2 |    5 | f      | UNKNOWN   | UNKNOWN          | 00:00:22.692224
  3 |         1 |    6 | t      | FOLLOWER  | FOLLOWER         |
(3 rows)

Лидером снова стал первый узел; третий мы запретили выбирать.

Page 26: Режим 2+1

Source slide page 26: Режим 2+1{#source-figure-026 caption=“Source slide page 26: Режим 2+1”}

Комментарии преподавателя

В кластере можно создать узел-рефери, который будет участвовать в выборах узла-лидера и поможет избежать проблемы split-brain, которая возможна, если лидер и единственный последователь перестают видеть друг друга. При инициализации узла-рефери на нем создаются лишь служебная база biha_db и системные таблицы; пользовательские данные на узел не копируются. Поэтому для рефери требуется меньше дискового пространства, ресурсов процессора и памяти.

Режим работы узла-рефери определяется параметром –mode команды bihactl add. Поддерживаются два режима:

● referee. Узел только принимает участие в выборах лидера, в синхронной репликации не участвует.

● referee_with_wal. Узел принимает участие в выборах лидера и участвует в репликации данных — получает все журналы с лидера. Если в момент начала выборов больше всего записей будет на узле-рефери, то узлы-последователи будут пытаться получать с него недостающие журнальные файлы.

В обоих режимах узел-рефери отправляет и получает сообщения о контроле состояния по каналу управления и поддерживает функции мониторинга кластера.

Рефери — это конечное состояние узла: он не может стать ни лидером, ни последователем.

https://postgrespro.ru/docs/enterprise/16/biha-architecture#BIHA-REFEREE

Page 27: Режим 2+1. Узел-рефери

Взамен остановленного нами второго узла создадим узел-рефери. Но сначала удалим второй узел из кластера. Сделать это можно с помощью функции biha.remove_node, вызывать которую нужно на лидере:

student$ psql -p 7001 -h host1 -U postgres -d biha_db
=> SELECT biha.remove_node(2);
 remove_node
-------------
 t
(1 row)

В кластере осталось два узла:

=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |   state   | last_known_state |  since_last_hb
----+-----------+------+--------+-----------+------------------+-----------------
  1 |         1 |    7 | t      | LEADER_RW | LEADER_RW        |
  3 |         1 |    7 | t      | FOLLOWER  | FOLLOWER         | 00:00:00.245768
(2 rows)

При добавлении узла-рефери (параметр –mode=referee) утилита pg_basebackup создаст частичную резервную копию лидера кластера. Данные нового экземпляра будут размещены в каталоге данных остановленного узла, поэтому этот каталог следует заранее очистить:

postgres$ rm -rf /var/lib/pgpro/ent-16-biha-2
postgres$ bihactl add \
  --biha-node-id=2 \
  --host=host2 \
  --port=7002 --pgdata=/var/lib/pgpro/ent-16-biha-2 \
  --biha-port=7102 \
  --mode=referee \
  --use-leader "host=host1 port=7001 biha-port=7101"

Стартуем вновь созданный экземпляр:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-2 \
  -l /var/lib/pgpro/ent-16-biha-2/pgpro-ent.log \
  start
waiting for server to start........... done
server started

На новый экземпляр была скопирована только база biha_db и системные таблицы. Пользовательские данные других баз (даже postgres) не переносятся, чтобы уменьшить потребление ресурсов. Если все-таки необходимо, чтобы база postgres попала на узел-рефери (она может понадобится некоторым утилитам или системам мониторинга), нужно в команде выше дополнительно указать параметр –referee-with-postgres-db.

student$ psql -p 7002 -h host2 -U postgres -d biha_db
=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |   state   | last_known_state |  since_last_hb
----+-----------+------+--------+-----------+------------------+-----------------
  1 |         1 |    8 | t      | LEADER_RW | LEADER_RW        | 00:00:00.500849
  2 |         1 |    8 | t      | REFEREE   | REFEREE          |
  3 |         1 |    8 | t      | FOLLOWER  | FOLLOWER         | 00:00:01.500849
(3 rows)

Остановим лидера.

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-1 \
  stop
waiting for server to shut down.... done
server stopped

Оставшиеся узлы должны автоматически выбрать нового. Узел-рефери участвует в голосовании, но сам стать лидером

Page 28: Page 28

Source slide page 28: Page 28{#source-figure-028 caption=“Source slide page 28: Page 28”}

никогда не сможет.

=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |  state   | last_known_state | since_last_hb
----+-----------+------+--------+----------+------------------+----------------
  1 |         1 |    8 | f      | UNKNOWN  | LEADER_RW        | 00:00:11.19196
  2 |         1 |    8 | t      | REFEREE  | REFEREE          |
  3 |         1 |    8 | t      | FOLLOWER | FOLLOWER         | 00:00:00.19196
(3 rows)

Почему нет нового лидера? Мы запретили третьему узлу выбираться. Исправим это:

=> ALTER SYSTEM RESET biha.can_be_leader;
ALTER SYSTEM
=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)
=> \dconfig biha.can_be_leader
List of configuration parameters
     Parameter      | Value
--------------------+-------
 biha.can_be_leader | on
(1 row)

Теперь третий узел стал лидером:

=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |   state   | last_known_state |  since_last_hb
----+-----------+------+--------+-----------+------------------+-----------------
  1 |         1 |    8 | f      | UNKNOWN   | UNKNOWN          | 00:00:23.938969
  2 |         3 |    9 | t      | REFEREE   | REFEREE          | 00:00:00.938969
  3 |         3 |    9 | t      | LEADER_RW | LEADER_RW        |
(3 rows)

Page 29: Итоги

Postgres Pro BiHA — кластерное решение для высокой доступности на основе физической репликации

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

Page 30: Практика

  1. Настройте синхронную физическую репликацию между двумя серверами. 2. Преобразуйте ведущий узел в лидера синхронного трехузлового BiHA-кластера. Проверьте его статус. 3. Преобразуйте ведомый узел (реплику) в узел-последователь и проверьте статус кластера. 4. Добавьте в кластер еще один узел-последователь, работающий как асинхронная реплика. Проверьте статус кластера и работу репликации. Остановите этот узел и убедитесь в том, что это не мешает фиксации транзакций.

Комментарии преподавателя

  1. Создайте два дополнительных экземпляра Postgres Pro Enterprise: для ведущего выполните инициализацию с помощью initdb, а ведомый создайте из копии ведущего, созданной pg_basebackup.

  2. Используйте команду bihactl init c параметрами –convert и –sync-standbys=1.

  3. Для добавления последователя используйте команду bihactl add c параметрами –convert-standby и –use-leader.

  4. Для добавления последователя используйте команду bihactl add c параметрами –convert-standby и –use-leader, а затем переведите его в асинхронный режим функцией biha.remove_from_ssn.

Page 31: 1. Синхронная репликация

Подготовим экземпляр Postgres Pro, который станет ведущим. Создаем каталог PGDATA:

postgres$ mkdir /var/lib/pgpro/ent-16-biha-1

Инициализируем экземпляр:

postgres$ /opt/pgpro/ent-16/bin/initdb \
  -U postgres \
  -k \
  -D /var/lib/pgpro/ent-16-biha-1
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.
initdb: --icu-locale not specified, using lc_collate
Using language tag "en-US" for ICU locale "en_US.UTF-8".
The database cluster will be initialized with this locale configuration:
  provider:    icu
  ICU locale:  en-US
  LC_COLLATE:  en_US.UTF-8
  LC_CTYPE:    en_US.UTF-8
  LC_MESSAGES: en_US.UTF-8
  LC_MONETARY: ru_RU.UTF-8
  LC_NUMERIC:  ru_RU.UTF-8
  LC_TIME:     ru_RU.UTF-8
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".
Data page checksums are enabled.
fixing permissions on existing directory /var/lib/pgpro/ent-16-biha-1 ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting default time zone ... Europe/Moscow
creating configuration files ... ok
performing automatic tuning ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok
initdb: warning: enabling "trust" authentication for local connections
initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or
--auth-local and --auth-host, the next time you run initdb.
Success. You can now start the database server using:
    /opt/pgpro/ent-16/bin/pg_ctl -D /var/lib/pgpro/ent-16-biha-1 -l logfile start

Дополним конфигурацию узла 1:

postgres$ echo include_dir \'conf.d\' >> /var/lib/pgpro/ent-16-biha-1/postgresql.conf
postgres$ mkdir /var/lib/pgpro/ent-16-biha-1/conf.d
/var/lib/pgpro/ent-16-biha-1/conf.d/syncro.conf
cluster_name = 'syncro_primary'
port = 7001

Запускаем:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-1 \
  -l /var/lib/pgpro/ent-16-biha-1/pgpro-ent.log \
  start
waiting for server to start.... done
server started

Разворачиваем реплику. Сделаем копию ведущего узла, разместив ее в каталоге /var/lib/pgpro/ent-16-biha-2

Page 32: Page 32

postgres$ /opt/pgpro/ent-16/bin/pg_basebackup \
  -p 7001 --pgdata=/var/lib/pgpro/ent-16-biha-2 \
  -U postgres \
  -R \
  --checkpoint=fast

Дополним конфигурацию узла 2:

/var/lib/pgpro/ent-16-biha-2/conf.d/syncro.conf
cluster_name = 'syncro_standby'
port = 7002

Запускаем:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-2 \
  -l /var/lib/pgpro/ent-16-biha-2/pgpro-ent.log \
  start
waiting for server to start........... done
server started

Проверим статус репликации:

student$ psql -p 7001 -h host1 -U postgres
=> SELECT state, application_name, sync_state FROM pg_stat_replication;
   state   | application_name | sync_state
-----------+------------------+------------
 streaming | syncro_standby   | async
(1 row)

Настроим синхронную репликацию. Для этого установим на ведущем сервере значение параметра synchronous_standby_names:

=> ALTER SYSTEM SET synchronous_standby_names = 'syncro_standby';
ALTER SYSTEM
=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Репликация стала синхронной:

=> SELECT state, application_name, sync_state FROM pg_stat_replication;
   state   | application_name | sync_state
-----------+------------------+------------
 streaming | syncro_standby   | sync
(1 row)

2. Преобразование ведущего в узел-лидер BiHA

Остановим ведущий сервер.

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-1 \
  stop
waiting for server to shut down.... done
server stopped

Как и в демонстрации, зададим пароль для роли biha_replication_user, записав его в файл:

postgres$ echo '*:*:*:biha_replication_user:biha' >> ~/.pgpass
postgres$ chmod 600 ~/.pgpass

Команда bihactl init с параметром –convert преобразует узел в лидера BiHA-кластера. Мы хотим продолжить работу в синхронном режиме, поэтому в параметре –sync-standbys зададим число синхронных узлов-последователей для подключения к лидеру:

Page 33: Page 33

postgres$ bihactl init \
  --biha-node-id=1 \
  --host=host1 \
  --port=7001 --pgdata=/var/lib/pgpro/ent-16-biha-1 \
  --biha-port=7101 \
  --convert \
  --sync-standbys=1 \
  --nquorum=2
bihactl: password for biha_replication_user was taken from pgpass
bihactl: leader started with 'pg_proaudit, biha'
dmVyc2lvbj0xIGhvc3Q9aG9zdDEgcG9ydD03MDAxIGJpaGEtcG9ydD03MTAx

Запускаем лидера, проверяем статус кластера:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-1 \
  -l /var/lib/pgpro/ent-16-biha-1/pgpro-ent.log \
  start
waiting for server to start.... done
server started
student$ psql -p 7001 -h host1 -U postgres -d biha_db
=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |   state   | last_known_state | since_last_hb
----+-----------+------+--------+-----------+------------------+---------------
  1 |         1 |    0 | t      | LEADER_RO | LEADER_RO        |
(1 row)

По умолчанию значение biha.minnodes устанавливается равным biha.nquorum (у нас это 2), поэтому новый лидер не может пока выполнять изменение данных.

3. Преобразование ведомого сервера в последователя BiHA

Остановим ведомый сервер.

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-2 \
  stop
waiting for server to shut down........ done
server stopped

Нужно, чтобы пароль роли biha_replication_user в файле паролей совпадал с паролем этой роли на лидере. В нашем случае это так, и команда bihactl init с параметром –convert-standby преобразует узел в последователя BiHA-кластера:

postgres$ bihactl add \
  --biha-node-id=2 \
  --host=host2 \
  --port=7002 --pgdata=/var/lib/pgpro/ent-16-biha-2 \
  --biha-port=7102 \
  --convert-standby \
  --use-leader 'host=host1 port=7001 biha-port=7101'

Запускаем последователя, с лидера проверяем статус кластера:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-2 \
  -l /var/lib/pgpro/ent-16-biha-2/pgpro-ent.log \
  start
waiting for server to start.... done
server started
=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |   state   | last_known_state |  since_last_hb
----+-----------+------+--------+-----------+------------------+-----------------
  1 |         1 |    1 | t      | LEADER_RW | LEADER_RW        |
  2 |         1 |    1 | t      | FOLLOWER  | FOLLOWER         | 00:00:00.710236
(2 rows)

4. Асинхронный последователь

Page 34: Page 34

Добавим второго последователя:

postgres$ bihactl add \
  --biha-node-id=3 \
  --host=host3 \
  --port=7003 --pgdata=/var/lib/pgpro/ent-16-biha-3 \
  --biha-port=7103 \
  --use-leader 'host=host1 port=7001 biha-port=7101'
postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-3 \
  -l /var/lib/pgpro/ent-16-biha-3/pgpro-ent.log \
  start
waiting for server to start............. done
server started

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

=> SELECT * FROM biha.remove_from_ssn(3);
 remove_from_ssn
-----------------
 t
(1 row)
=> SELECT * FROM biha.get_ssn();
             get_ssn
---------------------------------
 ANY 1 (biha_node_1,biha_node_2)
(1 row)

Проверим статус кластера:

=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |   state   | last_known_state |  since_last_hb
----+-----------+------+--------+-----------+------------------+-----------------
  1 |         1 |    3 | t      | LEADER_RW | LEADER_RW        |
  2 |         1 |    3 | t      | FOLLOWER  | FOLLOWER         | 00:00:00.910271
  3 |         1 |    3 | t      | FOLLOWER  | FOLLOWER         | 00:00:00.910271
(3 rows)

Проверим работу репликации:

=> CREATE DATABASE pgpro_biha;
CREATE DATABASE
student$ psql -p 7002 -h host2 -U postgres
=> \c pgpro_biha
You are now connected to database "pgpro_biha" as user "postgres".
student$ psql -p 7003 -h host3 -U postgres
=> \c pgpro_biha
You are now connected to database "pgpro_biha" as user "postgres".

Репликация работает в разных режимах:

=> SELECT state, application_name, sync_state FROM pg_stat_replication;
   state   | application_name | sync_state
-----------+------------------+------------
 streaming | biha_node_2      | quorum
 streaming | biha_node_3      | async
(2 rows)

Теперь отключим третий узел:

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-3 \
  stop
waiting for server to shut down.... done

Page 35: Page 35

server stopped
=> SELECT * FROM biha.status_v;
 id | leader_id | term | online |   state   | last_known_state |  since_last_hb
----+-----------+------+--------+-----------+------------------+-----------------
  1 |         1 |    3 | t      | LEADER_RW | LEADER_RW        |
  2 |         1 |    3 | t      | FOLLOWER  | FOLLOWER         | 00:00:00.888897
  3 |         1 |    3 | f      | UNKNOWN   | FOLLOWER         | 00:00:21.888897
(3 rows)

Поскольку узел использовал асинхронную репликацию, вывод его из строя не мешает фиксации транзакций:

=> CREATE TABLE t ();
CREATE TABLE

Уборка

Остановим и удалим экземпляры кластера BiHA.

postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-1 \
  stop
waiting for server to shut down........ done
server stopped
postgres$ /opt/pgpro/ent-16/bin/pg_ctl \
  -w \
  -D /var/lib/pgpro/ent-16-biha-2 \
  stop
waiting for server to shut down........ done
server stopped
student$ sudo rm -rf /var/lib/pgpro/ent-16-biha-1 /var/lib/pgpro/ent-16-biha-2 /var/lib/pgpro/ent-16-
biha-3