Monitoring
Monitoring
Мониторинг PostgreSQL
Москва
2024
УДК 004.65
ББК 32.972.134
Л50
Лесовский А. B.
ISBN 978-5-907754-42-3
УДК 004.65
ББК 32.972.134
Предисловие . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Об этой книге . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Глава 1. Обзор статистики . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Глава 2. Статистика активности . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
Глава 3. Выполнение запросов и функций . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
Глава 4. Базы данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
Глава 5. Область общей памяти и ввод-вывод . . . . . . . . . . . . . . . . . . . . . . . . . . 129
Глава 6. Журнал упреждающей записи . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
Глава 7. Репликация . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
Глава 8. Очистка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
Глава 9. Ход выполнения операций . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
Приложение. Тестовое окружение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
Предметный указатель . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239
Содержание
Предисловие 7
Об этой книге 9
За время работы в этой компании мне лишь изредка приходилось погружаться в тонкости
функционирования СУБД. Моей первой серьезной задачей было обновление СУБД с версии
9.0 на 9.2 под нагрузкой и без остановки приложений. В то время я часто писал о своем техни-
ческом опыте в блогах и по результатам задачи также был написан пост.
Через какое-то время я стал работать в другой компании, где серверы баз данных отдельно
поддерживались компанией-подрядчиком. Однако, уже имея за плечами хороший опыт рабо-
ты с PostgreSQL, я брал инициативу на себя и самостоятельно выполнял часть задач. В результа-
те компания-подрядчик пригласила меня к себе, и вместо системного администратора я стал
администратором баз данных. Теперь каждый мой рабочий день был связан с PostgreSQL.
Другой моей сильной стороной было хорошее знание ОС Linux. Эти знания стали очень полез-
ными при дальнейшем погружении и изучении PostgreSQL. Тема мониторинга всегда вызы-
вала у меня живой интерес, и мне всегда нравилось наблюдать за тем, как работают системы.
Когда я стал администратором баз данных, мой интерес к наблюдениям сместился в сторо-
ну PostgreSQL, и я стал разбираться с тем, как отслеживать характеристики работы СУБД. Ре-
зультатом этого стало создание различных инструментов, начиная от простых SQL-скриптов
и заканчивая плагинами к системам мониторинга и консольными утилитами. И, конечно же,
бесчисленное количество постов. Как правило, они не были связаны между собой и объединя-
лись лишь общей темой мониторинга. Понимая недолговечность постов и их разрозненность
в интернете, мне захотелось соединить их в один большой материал. Так пришла идея напи-
сать книгу.
За те полтора года, что я пишу книгу, я проанализировал свой опыт, провел множество
экспериментов, обобщил и собрал в одном месте большое количество материала. Однако
и PostgreSQL не стоит на месте, продолжает развиваться. В СУБД появляются новые средства
для мониторинга, так, например, после очередного релиза мне пришлось дополнять уже на-
писанные главы.
В сети доступно огромное количество информации для тех, у кого есть достаточно времени
и желания искать, фильтровать и понимать ее. Весьма приятно читать новые статьи и узна-
вать какие-то ранее неизвестные детали. Особенно приятно знакомиться с изменениями
на [Link] и читать коммит-сообщения, еще до основного релиза одним
из первых узнавать о новшествах, которые появятся в СУБД. Однако тема мониторинга очень
обширна, и только объем и глубина книжного формата дают мне возможность полностью объ-
яснить мониторинг PostgreSQL, затрагивая связанные с ним темы, а также дать ссылки для
дальнейшего изучения, если что-то вызовет интерес. Надеюсь, что в этой книге я достиг своей
цели.
Об этой книге
Я написал эту книгу для того, чтобы доходчиво и на реальных примерах объяснить, как
на практике устроен мониторинг PostgreSQL. Обычно официальная документация PostgreSQL
по встроенным средствам наблюдения и мониторинга изложена довольно сухо, поэтому цель
этой книги — объяснить все детали на примерах, которые будут понятны каждому админист-
ратору СУБД.
Эта книга создана для администраторов баз данных, системных администраторов, специалис-
тов по надежности и тех, кто просто интересуется администрированием PostgreSQL. Книга
призвана осветить все тонкости темы мониторинга. В блогах можно найти множество пуб-
ликаций, но большинство из них детально рассматривают лишь отдельные аспекты. Эта книга
всесторонне охватывает мониторинг PostgreSQL, подготавливая читателя и помогая ему разо-
браться в большом разнообразии средств, встроенных в СУБД. Цель практически любого мони-
торинга — это выявление аномалий, предупреждение аварийных ситуаций и прогнозирование
поведения системы в будущем. Используя мониторинг, можно лучше понять, как работает си-
стема, и дальше с помощью корректировки конфигурации и оптимизации рабочей нагрузки
добиться увеличения производительности. Поэтому каждый, кто интересуется оптимизацией
производительности, обязательно извлечет из этой книги полезную информацию и не толь-
ко сделает выводы для себя, но и, возможно, наметит план изменений в администрируемых
системах и получит полезный результат.
• В главе 1 дается общее представление о том, что такое статистика активности СУБД,
почему она так важна и является основой мониторинга PostgreSQL. Это вводная глава с те-
оретическими основами.
• В главе 2 разбирается статистика, которая описывает процессы и события внутри СУБД,
вызванные выполнением рабочей нагрузки и обслуживанием клиентов.
• В главе 3 подробно рассматриваются клиентские сеансы и проводится анализ рабочей на-
грузки, которая создается приложениями.
10 Об этой книге
• базовые сведения об общем устройстве СУБД, например, в общих чертах представляет, что
означает каждая из букв в аббревиатуре ACID, и знает, чем транзакция отличается от за-
проса;
• основы использования языка SQL и умение без подглядывания в документацию составить
простой SELECT-запрос с условиями, соединениями или подзапросами; умение читать
и понимать «чужие» запросы;
• базовые знания конкретно о PostgreSQL и общее представление о таких основных компо-
нентах этой СУБД, как буферный кеш (shared buffers), журнал упреждающей записи, ре-
пликация и т. п.; превосходно, если читатель знаком с книгой The Internals of PostgreSQL,
доступной по адресу [Link]/pg/[Link];
• некоторое представление о системе мониторинга Prometheus1 , о том, что такое метрики
и метки2 ;
1
[Link]/docs/introduction/overview
2
[Link]/docs/concepts/data_model
Примеры кода 11
Примеры кода
Остались вопросы?
1
[Link]/docs/prometheus/latest/querying/basics
12 Об этой книге
Отзывы и пожелания
Я буду рад отзывам читателей. Расскажите, что вы думаете об этой книге, — что понравилось
или, может быть, не понравилось. Отзывы важны для меня и будут полезны при подготов-
ке обновлений этой книги. Вы можете написать отзыв или пожелание по адресу [Link]/
lesovsky/postgresql-monitoring-book/issues. То же самое относится и к найденным опечаткам,
запросам на исправление неточностей и т. п.
Танюша, эта книга посвящается тебе, ты больше, чем жена, ты лучше всех! Мы с тобой отличная
команда ;)
Роман Алексеевич, помни: задача детей — стать лучше, чем их родители. Быстрее, выше, сильнее.
Ты красавчик, я горжусь тобой!
Маруся, ты моя жемчужинка, спасибо тебе за улыбки, задорный смех и хорошее настроение. Ты
всегда в моем сердце.
Андрей Фефелов, ты для меня пример стойкости и крепости духа, большая честь быть твоим
другом.
Посвящаю книгу всем родным и близким, семье и родителям — за поддержку и веру. Спасибо
друзьям и коллегам по сообществу. Спасибо всем, кто ходил на мои доклады, смотрел меня
на YouTube и задавал каверзные вопросы в кулуарах и комментариях. Спасибо организаторам
конференций за возможность живого выступления в зале — это незабываемый опыт. Спасибо
тем, кто читал мои блог-посты и просил писать больше и чаще. Я написал больше, чем просто
пост. Спасибо тебе, читатель, это все для тебя.
Отдельная благодарность Егору Рогову за бесценную помощь в подготовке книги. Вначале бы-
ло сложно, и в процессе согласования я испытал всю гамму чувств — от отрицания до принятия,
но в итоге остался очень доволен результатом. Надеюсь, нам еще удастся поработать вместе.
Глава 1
Обзор статистики
В этой главе дается общее представление о статистике активности и о том, почему эта статис-
тика так важна. На примере внутреннего устройства PostgreSQL объясняется необходимость
статистики для понимания работы внутренних процессов в СУБД и эффективного наблюдения
за ними. Статистика, которую мы будем рассматривать на протяжении всей книги, является
фундаментом для построения большинства инструментов мониторинга PostgreSQL.
Первую главу я хотел бы начать так же, как обычно начинаю свои доклады про мониторинг
PostgreSQL. СУБД PostgreSQL — это продукт с долгой историей. За все время существова-
ния в нее было добавлено множество функций, и, конечно, это отразилось на ее внутреннем
устройстве, которое схематично изображено в моем вольном представлении на рис. 1.1. Из
схемы видно, что СУБД состоит из множества компонентов. Мало того, эти компоненты свя-
заны между собой и постоянно взаимодействуют друг с другом в процессе обработки данных.
Если все сильно упростить, то СУБД можно рассматривать как сервис, который предоставляет
две основные услуги:
1. Надежное хранение данных. Пользователи и сервисы загружают в СУБД свои данные и за-
дача СУБД — обеспечить их прием и сохранность.
Client Backends
Postmaster
Query Planning
Shared Buffers Background Workers
Query Execution
Autovacuum Launcher
Indexes Usage Tables Usage
Write-Ahead Log
Network Storage
Для реализации этих и многих других услуг СУБД опирается на различные подсистемы и ме-
ханизмы.
В своем обычном режиме СУБД работает как служба (программа-сервер) и ожидает подключе-
ний со стороны клиентов. Клиентом может быть как приложение, так и пользователь, подклю-
чающийся через клиентскую программу (psql, pgAdmin, DataGrip и др.). Для каждого клиента
сервером СУБД создаются отдельные процессы операционной системы, которые в привычной
для администраторов терминологии называются бэкендами (backend). В каждом таком про-
цессе между клиентом и сервером СУБД устанавливается сеанс — двухсторонняя связь, поз-
воляющая клиенту взаимодействовать с СУБД. В процессе создания сеанса СУБД выполняет
аутентификацию клиента согласно указаниям в pg_hba.conf, инициализирует внутренние ра-
бочие структуры и применяет различные настройки, влияющие на дальнейшую работу. Когда
сеанс готов, клиент может отправлять серверу команды, включающие в себя как команды SQL,
так и системные управляющие команды1 .
Получив команду от клиента, сервер проверяет ее корректность. Чаще всего командой явля-
ется SQL-запрос; в таком случае СУБД начинает его планирование. Планирование заключается
в составлении оптимального плана для доступа к данным. Доступ и промежуточная обработ-
ка данных могут стоить по-разному (в смысле использования системных ресурсов), и задача
1
[Link]/docs/postgresql/current/sql-commands
18 Глава 1. Обзор статистики
планирования сводится к составлению и выбору наиболее дешевого плана. План обычно при-
нято представлять в виде графа, где каждый узел является вполне конкретной операцией над
данными.
Когда план выбран, сервер приступает к выполнению запроса согласно этому плану. Каждый
узел плана — это конкретная операция над данными (например, над строками из таблиц).
Когда запрос выполнен, результат возвращается клиенту. В качестве результата может высту-
пать набор строк или тег команды (command tag), сообщающий об успешности выполнения.
В случае запросов на изменение данных результат запроса должен быть записан в журнале
транзакций (Write-Ahead Log, WAL). В зависимости от настроек СУБД это может происходить
в синхронном или асинхронном режиме. В любом случае после выполнения запроса СУБД пе-
редает управление клиенту, и он может отправлять следующий запрос.
Локальные области памяти — это сегменты, которые выделяются индивидуально для каждого
процесса (в рамках сеанса), при этом процессы не имеют доступа к локальным сегментам друг
друга. Среди таких сегментов можно выделить следующие характерные области:
• рабочая память процессов (см. параметр work_mem) — выделяется при необходимости для
оперативного размещения данных при выполнении некоторых промежуточных операций
в запросе (сортировка, исключение дубликатов (DISTINCT), соединение таблиц по алгорит-
мам merge join и hash join и др.). Если для выполнения операции рабочей памяти ста-
новится недостаточно, на диске создаются временные файлы, которые удаляются после
завершения операции;
• временные буферы (см. параметр temp_buffers) — используются для работы с данными
временных таблиц (temporary tables), которые существуют в рамках сеанса или вообще
транзакции. Такие таблицы являются нежурналируемыми и часто применяются для со-
хранения промежуточных результатов;
• рабочая память для операций обслуживания (см. параметр maintenance_work_mem) —
выделяется для таких операций, как VACUUM, CREATE INDEX, REINDEX и др. Фоновые про-
цессы автоочистки используют собственную отдельную рабочую память (см. параметр
autovacuum_work_mem).
1.2. Внутреннее устройство PostgreSQL 19
В этой и следующих главах часто будут встречаться параметры конфигурации. Полный спи-
сок всех параметров доступен в документации по адресу: [Link]/docs/postgresql/
current/runtime-config.
Общая память (shared memory) — это один и, как правило, достаточно большой сегмент па-
мяти, который выделяется один раз при запуске СУБД. Доступ к общей памяти имеют все
процессы СУБД. В области общей памяти размещаются:
Буферный кеш
Здесь и далее под термином «буферный кеш» мы будем называть именно общий буферный
кеш, известный как shared buffers. В контексте повествования, где упоминаются локальные
буферные кеши, это отмечено отдельно.
Кроме основной общей памяти, также могут создаваться и небольшие сегменты общей па-
мяти для взаимодействия вспомогательных процессов в случае параллельного выполнения
некоторых операций. Существование таких сегментов ограничено временем жизни вспомо-
гательных процессов, а размер зависит от объемов передаваемых данных.
1
[Link]/docs/postgrespro/current/sql-savepoint
2
[Link]/docs/postgrespro/current/sql-prepare-transaction
20 Глава 1. Обзор статистики
есть, это считается успешной попыткой доступа (hit). Если данных в кеше не нашлось, то для
продолжения работы их требуется загрузить с диска в кеш; это считается промахом кеша (miss,
или read). Если к данным обращались ранее, они могут оказаться в страничном кеше опера-
ционной системы (page cache), откуда взять их будет быстрее, чем прочитать из основного
хранилища. В случае обновления (INSERT, UPDATE, DELETE) страницы изменяются в кеше; такие
страницы считаются грязными (dirtied), что указывает на необходимость их синхронизации
с файлом данных в основном хранилище. Обычно синхронизацией страниц занимаются два
фоновых процесса — checkpointer и background writer, но в некоторых случаях это может делать
и клиентский процесс. Так бывает, когда процессу требуются страницы, которых нет в кеше,
и, чтобы прочитать их с диска, процессу нужны свободные буферы, которых тоже нет. Что-
бы освободить буфер под целевую страницу, процесс начинает поиск и вытеснение страницы,
к которой давно не было обращений. Найденная страница может оказаться грязной, и тогда
процесс сначала синхронизирует ее (written) и только потом освобождает буфер. В общем, по-
лучается не самая дешевая операция, особенно если приходится делать это часто.
Описанная выше модель работы одинакова как для общего, так и для локального кеша. При ра-
боте с обычными и временными таблицами страницы в кеше всегда ассоциированы с файлом
таблицы на диске. В случае использования рабочей памяти такого файла нет ровно до тех пор,
пока не будет превышено ее ограничение (параметр work_mem). В этом случае создается вре-
менный файл, который удаляется после завершения операции. При нехватке рабочей памяти
для операций обслуживания (автоочистка, создание индексов) СУБД повторно использует уже
выделенную память без создания дополнительных файлов.
С точки зрения производительности очень хорошо, когда бóльшая часть данных находится
в памяти и на любое обращение страницу можно найти в кеше, и хуже, когда приходится
регулярно читать данные из основного хранилища или же вытеснять грязные страницы для
освобождения буферов под новые страницы.
Перед тем как вернуть результат запроса, направленного на изменение данных, СУБД запи-
сывает эти изменения в журнал предзаписи (Write-Ahead Log). Еще можно встретить термин
1.2. Внутреннее устройство PostgreSQL 21
журнал транзакций, однако чаще всего просто используется аббревиатура WAL. Запись в жур-
нал требуется для обеспечения надежности и сохранения порядка всех изменений над данны-
ми и возможности восстановления после аварийного завершения СУБД. В таком случае при
последующем запуске СУБД, используя журнал, воспроизведет последовательность измене-
ний над теми данными, которые не были сброшены из буферного кеша в основное хранилище.
Производительность СУБД зависит в том числе и от скорости работы с WAL-журналом, которую
можно узнать с помощью статистики.
После того как команда завершилась, клиенту передается результат выполнения, будь то набор
строк, тег или вообще ошибка. Здесь может выполняться протоколирование, то есть сохране-
ние служебной информации о выполнении команды. Эта информация может включать в себя
данные о клиенте, текст и параметры запроса и т. п. Протоколирование дополняет статистику
и также является важным источником информации о работе СУБД. Далее сервер готов к полу-
чению и выполнению следующей команды.
Репликация изменений
Помимо основной работы с клиентами, СУБД выполняет ряд служебных задач. Для этого суще-
ствуют отдельные процессы, которые работают в фоновом режиме. Одной из таких задач явля-
ется физическая репликация, которая построена на основе журнала упреждающей записи. Все
изменения данных, которые попадают в журнал, читаются процессом walsender и по протоко-
лу репликации передаются на реплики. На репликах процессы walreceiver принимают данные
журнала и сохраняют содержимое на диск. Другой фоновый процесс — startup — читает полу-
ченные данные журнала и воспроизводит последовательность изменений на локальной копии
данных. После этого изменения, пришедшие с основного сервера, становятся видимыми для
запросов, выполняющихся на реплике.
Есть и другие фоновые задачи. Одна из них — это синхронизация измененных данных из бу-
ферного кеша с основным хранилищем. Для этого задействованы два процесса: background
writer и checkpointer. Первый в непрерывном цикле ищет грязные страницы и записывает их
на диск. Второй, checkpointer, выполняет контрольные точки. Контрольная точка — это отмет-
ка в WAL-журнале, которая указывает на то, что все изменения до этой отметки уже записаны
в надежное хранилище. При выполнении контрольной точки процесс checkpointer записывает
все грязные данные в отличие от background writer, который делает это выборочно. Другими
словами, checkpointer, так же как и background writer, синхронизирует изменения из буферного
кеша с диском, но дополнительно ставит особую отметку в WAL-журнале о том, что синхрони-
зированы все грязные данные.
Поскольку журнал предзаписи — это история всех изменений, возникает вопрос: как долго
следует хранить эту историю? Ответ дает процесс checkpointer: установка контрольной точ-
ки означает, что все предыдущие изменения надежно записаны в основное хранилище и все
сегменты журнала, предшествующие этой контрольной точке, могут быть удалены. Таким об-
разом, объем журнала предзаписи сохраняется более или менее постоянным и WAL-сегменты
не накапливаются.
Автоочистка
Еще одной регулярной фоновой задачей является очистка таблиц и индексов от устаревших
версий строк, так называемая автоочистка (autovacuum). Необходимость автоочистки являет-
ся следствием реализации операций обновления данных и конкурентного доступа к ним. При
изменении данных и конкурентной работе нескольких клиентов в таблицах могут возникать
несколько версий одних и тех же строк. Со временем самые старые версии строки становятся
неактуальными и могут быть удалены. Их удалением и занимается процесс автоочистки. Фо-
новый процесс autovacuum launcher с определенной периодичностью (см. autovacuum_naptime)
запускает рабочие процессы autovacuum worker, которые выполняют очистку. Дополнительно
рабочие процессы могут собирать статистику для планировщика о качественных и количест-
венных характеристиках данных в таблицах. Эта статистика нужна для построения планов
запросов. Неэффективная работа автоочистки в перспективе негативно влияет на производи-
тельность, и с помощью статистики активности можно наблюдать за работой не только очист-
ки, но и других фоновых процессов.
Подводя итог, можно сказать, что в PostgreSQL есть много сложных процессов, которые влияют
друг на друга и связаны между собой. При возникновении проблем администратору БД нужна
информация о том, как работают те или иные процессы. Для отслеживания различных событий
СУБД имеет внутренние инструменты, которые ведут учет и хранят статистику в специальной
1.3. Интерфейс статистики 23
Вся статистика представлена в виде служебных данных СУБД, и для доступа к ней не нуж-
но предпринимать дополнительных действий. Получить статистику можно с помощью слу-
жебных функций. Однако пользоваться ими не всегда удобно, поэтому получение статистики
из функций организовано через системные представления (view), к которым можно выпол-
нять SQL-запросы. Такие представления аналогичны таблицам, но не имеют физического слоя
хранения данных, и при работе с ними пользователю доступны почти все (кроме записи) воз-
можности языка SQL: соединения, агрегации, оконные функции, подзапросы и т. д.
1
[Link]/dataegret/pg-utils/tree/master/sql
2
[Link]/wiki/Monitoring
1.6. Тестовое окружение 25
Резюме
Я люблю говорить, что СУБД — это сервис. СУБД часто воспринимается как нечто большое
и сложно устроенное внутри, но можно представить СУБД как небольшой и легко разверты-
ваемый микросервис (столь привычный веб-разработчикам). С этой точки зрения главная за-
дача СУБД — принять и обработать запрос от клиента. Внутреннее взаимодействие сложных
28 Глава 2. Статистика активности
компонентов можно считать второстепенным, так как оно не предполагает прямых действий
со стороны клиента. В таком упрощенном случае есть лишь клиент и сервер. В качестве клиента
выступает программа: это может быть приложение на Go, Python или Ruby, задание от Airflow
или Celery или любимая IDE разработчика. В общем, что угодно, что может подключаться
к СУБД и общаться с ней по ее протоколу. Сервером выступает СУБД, выполняющая коман-
ды клиента. Активность, создаваемая клиентом, формирует рабочую нагрузку. Объем рабочей
нагрузки, с которой может справиться СУБД, определяет пропускную способность и, как след-
ствие, общую производительность. Активность в СУБД можно измерить и проанализировать
и в результате выявить аномалии, устранив которые можно увеличить производительность
СУБД и приложений.
Вопросы к тому, что происходит в СУБД, могут быть самыми разными, в зависимости от задач
администратора, его осведомленности и гипотез, выдвинутых в процессе поиска и устране-
ния проблем. Вопросы могут затрагивать не только обработку запросов клиентов, но и работу
фоновых служб. Ответы, полученные с помощью статистики активности, позволяют устранить
проблемы или оптимизировать работу приложений для достижения более надежной работы
и большей производительности.
Для более полного понимания того, что представляет собой активность, давайте рассмотрим,
как взаимодействуют между собой клиенты и СУБД. Взаимодействие строится по классичес-
кой схеме «клиент — сервер». Сервер работает постоянно в фоновом режиме и ожидает под-
ключений от клиентов. Клиент, будь то приложение или пользователь, подключается к серверу
и после успешного подключения, следуя внутренней логике или желанию пользователя, фор-
мирует и отправляет команды серверу, ожидает их выполнения, получает ответ и обрабатыва-
ет его.
баз данных. При подключении клиента postmaster создает новый, дочерний процесс, внутри
которого выполняются необходимая настройка и подготовка к сеансу. Когда сеанс готов, кли-
ент может начинать отправку запросов.
Режимы работы
Ниже показаны процессы сервера СУБД, выведенные утилитой ps, с точки зрения операцион-
ной системы.
$ ps f -u postgres -o pid,cmd
PID CMD
4150200 /usr/lib/postgresql/15/bin/postgres -c config_file=/etc/postgresql/15/main/[Link]
4150206 \_ postgres: 15/main: logger
3979107 \_ postgres: 15/main: checkpointer
3979108 \_ postgres: 15/main: background writer
3979109 \_ postgres: 15/main: walwriter
3979110 \_ postgres: 15/main: autovacuum launcher
3979111 \_ postgres: 15/main: archiver last was 000000010000000400000096
3979112 \_ postgres: 15/main: logical replication launcher
1389347 \_ postgres: 15/main: walsender postgres [Link](39540) streaming 4/97BB49B0
1865559 \_ postgres: 15/main: postgres pgbench [local] idle
1865560 \_ postgres: 15/main: postgres pgbench [local] SELECT
1865561 \_ postgres: 15/main: postgres pgbench [local] idle
1865562 \_ postgres: 15/main: postgres pgbench [local] SELECT
1865563 \_ postgres: 15/main: postgres pgbench [local] idle
1865564 \_ postgres: 15/main: postgres pgbench [local] SELECT
1865565 \_ postgres: 15/main: postgres pgbench [local] SELECT
1865566 \_ postgres: 15/main: postgres pgbench [local] SELECT
1865567 \_ postgres: 15/main: postgres pgbench [local] SELECT
1865568 \_ postgres: 15/main: postgres pgbench [local] idle
1865571 \_ postgres: 15/main: postgres pgbench [local] UPDATE
1865572 \_ postgres: 15/main: postgres pgbench [local] COMMIT
Процессы выведены в виде дерева «родитель — потомок». Главным из них является postmaster,
который приходится родителем всем остальным процессам. Среди процессов-потомков есть
процессы фоновых служб и процессы клиентских соединений, так называемые бэкенды
(backend). Для наблюдения за работой СУБД со стороны операционной системы хорошо
подходят такие утилиты, как top, htop и atop. С их помощью можно наблюдать за тем, как
процессы операционной системы используют системные и операционные ресурсы, такие как
1
[Link]/docs/postgresql/current/libpq-async
30 Глава 2. Статистика активности
CPU, память, дисковый и сетевой ввод-вывод, пространство на диске. Стоит обратить внима-
ние и на утилиты vmstat, dstat, nicstat и pidstat, iostat, входящие в состав пакета sysstat, — эти
утилиты также могут быть полезны в оценке использования системных ресурсов.
Несмотря на свойство изоляции, нельзя думать, что транзакции полностью независимы друг
от друга. Возможность конкурентной работы подразумевает вероятность одновременного до-
ступа к одним и тем же данным (строкам в таблице) со стороны нескольких транзакций. В слу-
чае операций чтения все просто, поскольку конкурентное чтение не вызывает конфликтов,
почти2 . С записью все становится чуть сложнее: конкурентная запись может вызывать кон-
фликты, и такие операции должны быть сериализованы, то есть выстроены в строгую после-
довательность. Для сериализации доступа к объектам БД используются блокировки. Механизм
блокировок позволяет ограничивать или запрещать одновременный доступ к ресурсу. Рабо-
та блокировок прозрачна для пользователя и в большинстве случаев не требует от него яв-
ных действий. Однако возможны ситуации, когда одновременно несколько клиентов пыта-
ются установить несовместимую блокировку на один и тот же ресурс. В таком случае только
один клиент сможет установить блокировку, а остальные образуют очередь и будут вынужде-
ны ждать, когда блокировка будет снята.
В качестве промежуточного итога можно сделать вывод, что природа конкурентного доступа,
свойства транзакций, механизм блокировок и некоторое стечение обстоятельств в совокуп-
ности могут приводить к ситуациям с негативными последствиями для СУБД и приложений.
В зависимости от важности эксплуатируемой БД такие ситуации могут расцениваться как ава-
рийные, так как могут привести к снижению производительности или к остановке запросов.
1
[Link]/main/writings/pgsql/[Link]
2
[Link]/docs/postgresql/current/sql-select#SQL-FOR-UPDATE-SHARE
2.3. Источники информации об активности 31
Соглашение об именовании
Здесь и далее в тексте при указании представлений и их полей будет использоваться нота-
ция представление.поле. Например, pg_stat_activity.pid указывает на поле pid в представ-
лении pg_stat_activity.
Представление pg_stat_activity
Весь мой практический опыт поиска и устранения проблем говорит о том, что при возникнове-
нии самых разных подозрений на проблемы в работе СУБД pg_stat_activity — главный источ-
ник первичной информации. Если с точки зрения операционной системы администратор мо-
жет увидеть только процессы, то pg_stat_activity помогает администратору заглянуть внутрь
СУБД и посмотреть на работу этих же процессов, но с точки зрения самой PostgreSQL. В пред-
ставлении содержится информация обо всех процессах СУБД, будь то клиентские процессы
или фоновые службы, и каждая строка описывает отдельный серверный процесс. В ранних
версиях PostgreSQL pg_stat_activity содержало информацию преимущественно о клиентских
процессах. В следующих версиях была добавлена информация о фоновых службах. При этом
клиентские процессы и фоновые службы отличаются характером работы с СУБД и часть ин-
формации, которая присуща клиентским процессам, для фоновых служб попросту отсутствует.
32 Глава 2. Статистика активности
Метакоманды
Клиент psql содержит набор метакоманд, полезных для получения справочной информа-
ции об объектах БД, как пользовательских, так и системных. Все метакоманды начинаются
с символа обратной косой черты \, например \d. Для получения справки и списка всех ме-
такоманд используйте метакоманду \?.
# \d pg_stat_activity
View "pg_catalog.pg_stat_activity"
Column | Type | Collation | Nullable | Default
------------------+--------------------------+-----------+----------+---------
datid | oid | | |
datname | name | | |
pid | integer | | |
leader_pid | integer | | |
usesysid | oid | | |
usename | name | | |
application_name | text | | |
client_addr | inet | | |
client_hostname | text | | |
client_port | integer | | |
backend_start | timestamp with time zone | | |
xact_start | timestamp with time zone | | |
query_start | timestamp with time zone | | |
state_change | timestamp with time zone | | |
wait_event_type | text | | |
wait_event | text | | |
state | text | | |
backend_xid | xid | | |
backend_xmin | xid | | |
query_id | bigint | | |
query | text | | |
backend_type | text | | |
Для лучшей идентификации клиент при подключении может обозначить себя через отдель-
ный идентификатор application_name. Это удобный способ отличать приложения в случаях,
когда они подключены с одинаковыми реквизитами и с одного адреса. Поле backend_type поз-
воляет отличать клиентские процессы от фоновых служб. В ранних версиях СУБД этого поля
не было, и для определения клиентских подключений приходилось прибегать к различным
уловкам — например, указывать в SQL-запросе дополнительное условие datname IS NOT NULL.
# SELECT
row_number() OVER (ORDER BY pid) AS n,
pid, backend_type, client_addr, application_name, usename, datname
FROM pg_stat_activity
LIMIT 15;
n | pid | backend_type | client_addr | application_name | usename | datname
----+--------+------------------------------+--------------+------------------+----------+----------
1 | 65 | checkpointer | | | |
2 | 66 | background writer | | | |
3 | 67 | walwriter | | | |
4 | 68 | autovacuum launcher | | | |
5 | 69 | archiver | | | |
6 | 71 | logical replication launcher | | | postgres |
7 | 117 | walsender | [Link] | walreceiver | replica |
8 | 209017 | client backend | | psql | postgres | postgres
9 | 224802 | client backend | | psql | postgres | postgres
10 | 230568 | client backend | [Link] | pgbench | pgbench | pgbench
11 | 231751 | client backend | [Link] | pgbench | maru | pgbench
12 | 231752 | client backend | [Link] | pgbench | maru | pgbench
13 | 231753 | client backend | [Link] | pgbench | maru | pgbench
14 | 231754 | client backend | [Link] | pgbench | maru | pgbench
15 | 231755 | client backend | [Link] | pgbench | maru | pgbench
Этот пример наглядно показывает различия между фоновыми службами (строки с 1-й по 7-ю)
и клиентскими процессами (строки с 8-й по 15-ю). Во-первых, отличить их можно по значе-
нию поля backend_type. Во-вторых, для фоновых служб могут отсутствовать значения других
полей, например client_addr, application_name, usename и datname, поскольку фоновые процес-
сы могут запускаться локально или не устанавливать соединений к БД.
Состояние сеанса. Сеанс в течение своего жизненного цикла может находиться в разных со-
стояниях. По состоянию можно определить, все ли в порядке с сеансом, и принять меры, ес-
ли с ним что-то не так. На состояние сеанса указывает поле state. Также в поле state_change
доступно время перехода в текущее состояние, по которому можно определить его продол-
жительность. Далее по этим полям мы будем отслеживать потенциально опасную активность
в БД и ее продолжительность.
Идентификатор и текст запроса. В поле query указаны тексты выполняющихся в СУБД запросов.
Поле ограничено размером буфера, который регулируется параметром track_activity_query_size
(по умолчанию 1 КБ). Текст выявленного медленного запроса дает администратору отправную
точку для оптимизации производительности. Запрос можно воспроизвести, получить план
выполнения, проанализировать производительность и в результате наметить действия по оп-
тимизации. Дополнительно для каждого запроса формируется уникальный идентификатор
queryid, который в сочетании с полями usesysid и datid можно использовать для соединения
с представлением pg_stat_statements, в результате чего будут получены дополнительные све-
дения о том, сколько ресурсов было использовано этим запросом и ему подобными.
СУБД умеет распределять выполнение частей запроса между несколькими процессами, уско-
ряя выполнение запросов и увеличивая эффективность использования многоядерных систем.
При наблюдении важно отделять группу процессов, занятых выполнением параллельного за-
проса от всех остальных процессов или групп. Для этой цели служит поле leader_pid, которое
у всех дочерних процессов указывает на pid родительского процесса.
2.3. Источники информации об активности 35
Представление pg_locks
# \d pg_locks
View "pg_catalog.pg_locks"
Column | Type | Collation | Nullable | Default
--------------------+--------------------------+-----------+----------+---------
locktype | text | | |
database | oid | | |
relation | oid | | |
page | integer | | |
tuple | smallint | | |
virtualxid | text | | |
transactionid | xid | | |
classid | oid | | |
objid | oid | | |
objsubid | smallint | | |
virtualtransaction | text | | |
pid | integer | | |
mode | text | | |
granted | boolean | | |
fastpath | boolean | | |
waitstart | timestamp with time zone | | |
36 Глава 2. Статистика активности
Посмотреть полное описание pg_locks можно с помощью метакоманды \d+ pg_locks, а выше
приведено его сокращенное описание. Это представление показывает табличную информа-
цию на основе функции pg_lock_status.
• relation, page, tuple — отношение, номер страницы и номер строки внутри страницы;
• virtualxid, transactionid — виртуальный и фактический номера транзакций;
• classid, objid, objsubid — идентификаторы объекта блокировки внутри системного ката-
лога в случае, когда этот объект нереляционный.
1
[Link]/docs/postgresql/current/monitoring-stats#WAIT-EVENT-LOCK-TABLE
2
[Link]/docs/postgresql/current/explicit-locking#LOCKING-TABLES
2.3. Источники информации об активности 37
которая будет полезна для анализа и исправления проблемы на стороне прикладных прило-
жений (имя пользователя и приложения, текст запроса).
Представление pg_stat_database
поле в представлении, которое показывает текущее значение. Все остальные поля содер-
жат кумулятивную статистику за определенный период;
• xact_commit — количество транзакций, завершившихся фиксацией (COMMIT). Счетчик также
учитывает успешное выполнение одиночных запросов в рамках неявных транзакций;
• xact_rollback — количество транзакций, завершившихся обрывом (ROLLBACK), в том числе
по причине ошибок;
• sessions_abandoned — количество сеансов, принудительно завершенных по причине того,
что соединение оставлено клиентом. Это может указывать на ситуации, когда клиентское
приложение забывает о соединении и не закрывает его как положено (graceful close). Дру-
гой причиной могут быть сетевые проблемы, такие как ошибки передачи и потери паке-
тов, приводящие к нарушению работы TCP-сеансов;
• sessions_fatal — количество сеансов, которые были принудительно завершены по при-
чине возникновения фатальных ошибок и невозможности продолжения работы. Такие
ошибки могут происходить во время выполнения запросов или вызываться исключитель-
ными ситуациями на стороне сервера при взаимодействии между клиентом и сервером.
В таком случае сервер не может продолжить сеанс и завершает его. Примером такого сце-
нария может быть завершение сеанса из-за превышения тайм-аута, настроенного пара-
метром idle_in_transaction_session_timeout;
• sessions_killed — количество сеансов, которые были принудительно завершены адми-
нистративным способом. Такое завершение сеанса может быть инициировано функцией
pg_terminate_backend или самой СУБД, например при выключении или перезапуске сер-
вера;
• sessions — суммарное количество сеансов, установленных с БД. Значение поля указыва-
ет на общее число установленных сеансов независимо от статуса их завершения, то есть
включает в себя все значения и по остальным возможным статусам.
• session_time — суммарное время, проведенное всеми сеансами. Учет времени имеет неко-
торую особенность: время увеличивается только при переходе между состояниями. По-
этому можно ожидать появления больших скачков при наличии сеансов, которые могут
2.4. Подключенные клиенты 39
Обратите внимание: среди перечисленных полей нет статистики о времени, проведенном в со-
стоянии idle. Именно это значение придется считать самостоятельно, вычитая из общего вре-
мени время, проведенное в бездействующих транзакциях, и время выполнения запросов.
Сталкиваясь с новой ситуацией или незнакомым окружением, в первую очередь важно оце-
нить общую картину происходящего, получить общее представление о том окружении, в кото-
ром предстоит найти и устранить возникшую проблему. Для меня в начале знакомства с СУБД
важно понимать то, насколько активно используется система и кто ее пользователи. Обычно
это информация о том, сколько и каких клиентов подключено к системе, из чего можно сде-
лать примерный вывод о том, сколько ресурсов им может потребоваться, какую нагрузку они
могут генерировать.
Есть два способа получить такую информацию. Первый способ — использовать представление
pg_stat_database:
В выводе запроса для каждой базы можно увидеть текущее количество клиентских процес-
сов, подключенных к ней. Однако представление pg_stat_database показывает информацию
о подключенных клиентах только в контексте баз данных, что может быть недостаточно для
более детального анализа. Также обратите внимание на строку с пустым именем в результа-
те запроса: это служебная строка, объединяющая в себе статистику по разделяемым объектам,
которые доступны во всех БД, но при этом не принадлежат ни одной из них. Все такие объекты
принадлежат системному каталогу.
Системный каталог
Второй способ получить информацию о подключенных клиентах, к тому же гораздо более по-
дробную, — использовать представление pg_stat_activity:
Значение NULL для client_addr означает, что подключение выполнено через UNIX-сокет с того
же узла, где запущен сервер СУБД, — скорее всего, это наше подключение через psql. Также
можно видеть NULL-значения в полях usename и datname — обычно такие строки соответствуют
фоновым службам. В полном выводе pg_stat_activity можно увидеть больше подобных строк.
1
[Link]/docs/postgresql/current/catalogs
2.4. Подключенные клиенты 41
Обычно для отсеивания фоновых служб и отображения только клиентских сеансов использу-
ется условие backend_type = 'client backend'.
Поле backend_type появилось в PostgreSQL 10, а в более ранних версиях можно прибегнуть
к условиям вроде datname IS NULL. Как правило, фоновые процессы не подключаются к базам
данных, за исключением процессов автоочистки.
Получение информации с помощью SQL удобно, однако на практике часто приходится ана-
лизировать проблемы после того, как они уже исчезли, и нужны информация в исторической
перспективе и динамика изменений во времени. Эти задачи решаются системами монито-
ринга, которые собирают, хранят и визуализируют статистику. С помощью мониторинга адми-
нистратор имеет возможность рассматривать изменение статистики во времени с помощью
графиков.
# sum(postgres_connected_clients_total{service_id="primary"})
График, который можно построить на основе этого запроса, продемонстрирован на рис. 2.1.
График будет выглядеть так, как показано на рис. 2.2. На нем видно, что бóльшая часть сеансов
установлены с адресов [Link] и [Link]. В окружениях с большим количеством эк-
земпляров приложений такой график позволяет определять адреса, которые используют ано-
мально много сеансов.
На графике видно, что большая часть сеансов выполнена от двух прикладных пользователей.
Если присмотреться еще, то можно отметить, что выделяется пользователь stats. В тестовом
окружении под этим пользователем подключается экспортер метрик для снятия статистики.
Для экспортера, да и для любого другого агента мониторинга, такое количество соединений
слишком велико, что является поводом для оптимизации кода.
44 Глава 2. Статистика активности
Теперь взглянем на то, к каким базам данных выполнены подключения (рис. 2.4).
Транзакционная активность
Запросы — это базовая единица рабочей нагрузки. Их можно объединять в транзакции, и тран-
закция выполняется как единое целое: ошибка даже в одном запросе воспринимается как
ошибка всей транзакции целиком. Механизм транзакций устроен так, что даже один запрос,
не обернутый явно в команды управления транзакциями (BEGIN и END), также является тран-
закцией (состоящей из одной команды).
В тестовом окружении всего две активно используемые БД: pgbench и postgres. Также есть осо-
бая строка с отсутствующим именем базы данных, которая содержит статистику общих объек-
тов системного каталога. Следующим запросом мы можем получить данные из мониторинга:
# rate(postgres_database_xact_commits_total{service_id="primary"} +
postgres_database_xact_rollbacks_total{service_id="primary"}[1m])
Запрос считает количество выполненных транзакций в секунду, на его основе можно получить
график (рис. 2.5), который показывает эту картину в динамике. На нем наглядно видно, что
объем выполняемых транзакций в БД pgbench в несколько раз больше, чем в БД postgres.
Нормальная работа сеанса предполагает, что клиент устанавливает соединение с СУБД, от-
правляет запросы и, когда необходимость в подключении исчезает, закрывает соединение
и отключается. Однако возможно и аномальное поведение, при котором сеанс будет прерван:
# sum by (database) (
increase(postgres_database_sessions_total{
service_id="primary", database=~"(postgres|pgbench)"
}[1m]))
На графике (рис. 2.6) видно, что довольно большое количество сеансов — примерно 90 в ми-
нуту — устанавливается с БД postgres. Это объясняется рабочей нагрузкой, характерной для
экспортера метрик. Экспортер устанавливает несколько сеансов на время сбора статистики,
после чего завершает их. Такое поведение не очень эффективно в плане использования ре-
сурсов, поскольку для каждого сеанса СУБД создает отдельный процесс и выполняет дорого-
стоящую инициализацию. С этой точки зрения выгодно установить и использовать несколько
сеансов на постоянной основе, что является хорошей отправной точкой для оптимизации ко-
да экспортера. Кроме экспортера в тестовом окружении работают прикладные приложения,
которые перезапускаются каждые несколько минут с новыми параметрами нагрузки, и это за-
метно по резким пикам установки сеансов с БД pgbench. На практике реальные приложения
могут перезапускаться редко, a установленные ими соединения могут продолжать работать
в течение многих десятков минут и даже часов.
Для подтверждения постоянного характера проблемы я построил график (рис. 2.7) на основе
следующего запроса:
Клиентский процесс в своем жизненном цикле может находиться в разных состояниях, ко-
торые характеризуют происходящее в сеансе. Состояние процесса можно воспринимать как
условный маркер, который помогает различать сеансы с нормальным и нежелательным по-
ведением. Состояние отображается в pg_stat_activity.state, но при этом его можно увидеть
только для клиентских соединений, а для фоновых процессов значение поля всегда отсутству-
ет. Вероятно, это связано с тем, что «состояние» характеризует именно сеанс, а фоновые про-
цессы не устанавливают сеансов, следовательно, и отображать нечего. Вполне возможно, в бу-
дущих версиях PostgreSQL ситуация изменится и появится какая-то информация о состоянии
фоновых процессов. Давайте рассмотрим все состояния, в которых может находиться сеанс:
• idle указывает на состояние простоя. Если клиент не выполняет отправку команд, а сер-
вер не занят выполнением запроса, то сеанс находится в холостом режиме и сервер ждет
команды от клиента. В этом состоянии сеанс может находиться бóльшую часть своего вре-
мени.
• active указывает на активное состояние. Приняв команду от клиента, процесс начинает ее
выполнение и переходит в активное состояние. Здесь важно отметить, что такое состояние
включает в себя и состояние ожидания, когда процесс приостанавливает работу и ждет
определенного события.
• idle in transaction указывает на состояние открытой транзакции, в которой ничего
не происходит и процесс ждет от клиента команду на выполнение. Открыв транзакцию,
приложение должно выполнить в ней набор команд и закрыть ее, однако по каким-то
внутренним причинам между командами возникает пауза. Такое состояние в зависимости
от характера выполняемых команд потенциально может привести к блокировкам и раз-
дуванию таблиц и индексов из-за отложенной автоочистки. Следует отслеживать такие
транзакции и устранять причины, приводящие к такому состоянию. Более подробно о без-
действующих транзакциях можно узнать на с. 56.
• idle in transaction (aborted) указывает на состояние незавершенной транзакции, внут-
ри которой произошла ошибка. Хорошим тоном считается, когда приложение полностью
контролирует ход выполнения транзакции. Если выполнение любой из команд в транзак-
ции привело к ошибке, приложение должно откатить транзакцию. При отсутствии управ-
ления транзакциями в таких ситуациях возникает риск утечки ресурсов (в виде слотов пула
транзакций на стороне драйвера БД в приложении). Для самой СУБД пребывание сеанса
1
[Link]/lesovsky/pgscv/commit/d098923caef2d1a839df76ad9f441893640faed5
50 Глава 2. Статистика активности
в таком состоянии является менее вредными, так как после ошибки все ресурсы и блоки-
ровки освобождаются, а вот для приложения это может быть чувствительно и способно
привести к исчерпанию соединений во внутреннем пуле или появлению ошибок. Более
подробно о бездействующих транзакциях можно узнать в подразделе на с. 56.
• fastpath function call указывает на выполнение функции через интерфейс fast-path1 .
Этот способ выполнения считается устаревшим, и использовать его не рекомендуется.
На практике мне не приходилось встречаться с подобным состоянием, но допускаю, что
давно разработанные и оставшиеся без поддержки приложения могут использовать эту
функциональность.
• disabled не определяет конкретное состояние процесса, а лишь указывает на то, что от-
слеживание состояний сеансов отключено в конфигурации СУБД. Отслеживание обычно
включено по умолчанию, и не рекомендуется выключать его, так как это снижает наблю-
даемость и возможности мониторинга и отладки.
Существует еще одно важное состояние, которое при этом не отражается в поле state. Это
ожидание. Это состояние неявно включено в состояние active, хотя, на мой взгляд, эти два
состояния должны быть разделены и их следует рассматривать отдельно друг от друга. Актив-
ное состояние подразумевает действие и использование ресурсов, в то время как ожидание
подразумевает бездействие и удерживание ресурсов от использования. Из этого следует, что
активное состояние — это нормальное рабочее состояние системы, а ожидание — ненормаль-
ное, и администратор должен предпринимать действия, которые уменьшали бы ожидание.
Отслеживание состояний
В этом примере с синтетической нагрузкой видно, что бо́льшая часть соединений простаивает.
В момент обращения к pg_stat_activity всего три процесса выполняют запросы, а один клиент
открыл транзакцию и ожидает получения команды от клиента. Однако напоминаю, что состо-
яние active неявно включает в себя и возможное ожидание, поэтому из результата непонятно
1
[Link]/docs/postgresql/current/libpq-fastpath
2.5. Состояния сеансов 51
Ожидания и блокировки
• wait_event_type — общий тип ожидания, или, иными словами, обозначение группы, к ко-
торой относится ожидание;
• wait_event — конкретное событие или ресурс, в ожидании которых находится процесс.
1
[Link]/docs/postgresql/current/monitoring-stats#MONITORING-PG-STAT-ACTIVITY-VIEW
52 Глава 2. Статистика активности
Из этого небольшого примера видно, что варианты ожиданий могут быть самыми разны-
ми и, более того, их влияние на синтетическую нагрузку тоже может быть разным. Например,
в зависимости от производительности дисков (HDD, SSD, NVMe) скорость записи и синхрони-
зации WAL будет разной, и это отразится на времени ожидания. Ожидание соседних транзак-
ций зависит от характера рабочей нагрузки, количества конкурентных транзакций и продол-
жительности как самих транзакций, так и составляющих их команд. А вот ожидание команды
от клиента никак не сказывается на нагрузке, сеанс находится в холостом режиме и может
быть использован приложением немедленно, без задержек, как только потребуется. Таким
образом, маркеры ожидания подсказывают администратору, куда тратится время, и дают от-
правную точку для возможных оптимизаций системы.
Однако маркеров ожидания довольно много, и не все из них следует воспринимать как кри-
тичное состояние, требующее немедленной реакции. Как я уже отметил, [Link] —
это вполне безобидное ожидание, которое не нарушает работу системы.
2.5. Состояния сеансов 53
За годы практики я и некоторые мои коллеги пришли к тому, что среди всех типов событий
ожидания есть особая группа, которую можно выделить среди остальных и рассматривать как
отдельное состояние, равноценное значениям из pg_stat_activity.state. Речь о группе собы-
тий с типом Lock: ожидания этой группы указывают на то, что выполнение команды останов-
лено и ожидает получения тяжелой блокировки. Группа включает в себя несколько ожиданий,
которые более точно определяют причину ожидания. Одним из примеров может служить ожи-
дание transactionid, указывающее на ожидание завершения соседней конкурентной транзак-
ции. События группы Lock прямо указывают, что как процесс, так и клиентское приложение
вместо выполнения полезной работы находятся в ожидании, и это само по себе негативно вли-
яет на производительность приложения и СУБД.
Для учета состояния ожиданий блокировок есть два способа. Первый и предпочтительный за-
ключается в использовании событий ожидания. С помощью условия wait_event_type = 'Lock'
процессы можно идентифицировать как находящиеся в ожидании блокировок:
# SELECT
CASE WHEN wait_event_type = 'Lock'
THEN 'waiting' ELSE state
END AS state,
count(*)
FROM pg_stat_activity WHERE backend_type = 'client backend'
GROUP BY 1 ORDER BY 2 DESC;
state | count
---------------------+-------
idle | 31
waiting | 3
active | 3
idle in transaction | 1
Второй способ чуть сложнее и представляет, скорее, академический интерес. Способ заключа-
ется в соединении с pg_locks и использовании флага granted. Однако следует учесть, что для
одного процесса в pg_locks будет несколько блокировок (несколько строк) и учитывать нужно
только те, которые не захвачены:
# SELECT
CASE WHEN NOT [Link]
THEN 'waiting' ELSE [Link]
END AS state,
count(*)
FROM pg_stat_activity a
LEFT JOIN pg_locks l ON [Link] = [Link] AND NOT [Link]
WHERE a.backend_type = 'client backend'
GROUP BY 1 ORDER BY 2 DESC;
54 Глава 2. Статистика активности
state | count
---------------------+-------
idle | 24
active | 8
waiting | 3
idle in transaction | 1
Результаты двух вариантов не совсем совпадают, потому что выполнены в разное время.
Но первый вариант проще, поскольку позволяет обойтись без соединения.
Есть как минимум два способа реализовать метрики, показывающие состояние клиентов для
мониторинга:
# sum by (state)
(postgres_activity_connections_in_flight{service_id="primary"})
На графике есть процессы почти во всех возможных состояниях, в том числе и в ожидании
блокировок. Объединяя возможности полей state и wait_event_type, можно собрать воедино
информацию о состоянии клиентских сеансов.
Взаимоблокировки
Бездействующие транзакции
Это неполный список, и причины, по которым могут появляться простои в транзакциях, мо-
гут быть самыми разными и не всегда очевидными. В большинстве случаев ответственность
за образование бездействующих транзакций возлагается на приложение. Хороший пример —
обработка ошибок. В случае ошибки правильным действием со стороны приложения являет-
ся завершение транзакции откатом. В худшем случае может возникать утечка транзакций —
ситуация, когда приложение теряет контроль над транзакцией и она остается незавершенной
в течение неопределенно долгого времени. Возможны и другие сценарии. Например, пользо-
ватель подключается к БД и выполняет какие-то команды. Пользователю может потребоваться
начать транзакцию, выполнить в ней команду и затем сделать откат. Или IDE, в которой рабо-
тает пользователь, при выполнении команд может неявно открывать транзакцию. Пользова-
тель отвлекается на другую задачу, и ранее открытая транзакция остается незавершенной.
Пребывание клиентских сеансов в состоянии idle in transaction имеет свои негативные эф-
фекты, и некоторые из них в зависимости от условий эксплуатации СУБД могут привести
к аварии:
пытаться открыть новые соединения, что может привести к росту очереди ожидающих
и достижению ограничения на одновременно разрешенные подключения. При достиже-
нии этого ограничения приложение не сможет подключиться к БД, что приведет к невоз-
можности работы и приложения, и СУБД, и других приложений, которые также хотели бы
установить соединение с СУБД.
Таким образом, состояние idle in transaction является потенциально опасным, и долгое пре-
бывание процессов в этом состоянии может привести к негативным последствиям. Такие
транзакции нужно отслеживать и устранять. В тактическом плане такие транзакции можно
завершать автоматически через включение тайм-аута idle_in_transaction_session_timeout в на-
стройках СУБД. Эту настройку можно сделать как глобально, на уровне общей конфигурации
СУБД, так и индивидуально, на уровне отдельных пользователей или баз данных. Другим вари-
антом может быть периодический запуск скриптов на основе функции pg_terminate_backend
средствами утилиты cron. Такое решение может быть предпочтительным в случае, когда воз-
можностей idle_in_transaction_session_timeout недостаточно и требуется более тонкая настройка
тайм-аутов. Пример использования связки pg_terminate_backend и pg_stat_activity:
# SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE usename = 'pgbench' AND application_name = 'pgbench'
AND clock_timestamp() - coalesce(xact_start, query_start) > '00:01:00'::interval
AND state ~ 'idle in transaction';
Такой запрос достаточно вызвать из psql. При необходимости список полей в SELECT или усло-
вия можно переопределить, а вывод результата сохранять, например, для анализа.
clock_timestamp vs now
Рабочая нагрузка состоит из постоянного потока запросов и транзакций, и чем быстрее они
выполняются, тем больше пропускная способность системы. Наблюдая за рабочей нагрузкой
2.6. Время выполнения запросов и транзакций 59
• xact_start — время начала транзакции. Значение может отсутствовать (NULL), если в дан-
ный момент нет активной транзакции или в транзакции произошла ошибка и при этом
транзакция не была закрыта, — состояние idle in transaction (aborted);
• query_start — время начала текущего запроса (состояние active) или последнего выпол-
ненного запроса в сеансе (все прочие состояния).
# SELECT pid,
CASE WHEN wait_event_type = 'Lock' THEN 'waiting' ELSE state END AS state,
(clock_timestamp() - xact_start) AS xact_age,
(clock_timestamp() - query_start) AS query_age
FROM pg_stat_activity
WHERE (clock_timestamp() - xact_start > '00:00:00'::interval)
OR (clock_timestamp() - query_start > '00:00:00'::interval
AND state = 'idle in transaction (aborted)')
ORDER BY coalesce(xact_start, query_start);
pid | state | xact_age | query_age
--------+-------------------------------+-----------------+-----------------
3878 | idle in transaction (aborted) | | 00:00:04.264773
242533 | active | 00:00:00.176216 | 00:00:00.174478
241520 | active | 00:00:00.150633 | 00:00:00.149281
242541 | waiting | 00:00:00.141747 | 00:00:00.139989
241506 | waiting | 00:00:00.123768 | 00:00:00.122107
241523 | active | 00:00:00.102864 | 00:00:00.101012
60 Глава 2. Статистика активности
На рис. 2.9 изображен пример графика на основе этого запроса. Из графика видно, что бóль-
шая часть запросов укладывается в интервал до 500 миллисекунд. Отдельно отметился процесс
автоочистки, который выполнялся на тот момент около четырех секунд. Важно отметить, что
эта метрика и график на ее основе показывают не абсолютно всю активность, происходящую
в СУБД, а лишь ту, что была в момент опроса pg_stat_activity.
2.7. Отслеживание времени ожидания блокировок 61
Во время ожидания процесс не может выполнять полезную работу до тех пор, пока не исчезнет
причина ожидания. Продолжительное ожидание само по себе неэффективно с точки зрения
использования ресурсов и времени. Выявляя и устраняя участки, в которых происходит ожи-
дание, можно ускорить выполнение рабочей нагрузки и общую производительность системы.
Далее речь пойдет о времени, проведенном на блокировках, поскольку нет гарантированно
точного способа определить время ожидания конкретного события.
Использование pg_locks.waitstart
Для оценки времени ожидания понадобится поле pg_locks.waitstart. Если процесс не нахо-
дится в ожидании, то значение этого поля будет отсутствовать. Если процесс не смог взять
блокировку, он переходит в ожидание, и это поле показывает время перехода в ожидание.
Здесь есть неявная связь с другим полем этого же представления: на ожидание блокиров-
ки указывает не только waitstart, но и флаг granted. Однако они не полностью согласованы,
и waitstart может в течение короткого периода содержать NULL, когда поле granted уже уста-
новлено в false.
При оценке времени ожидания можно использовать как минимум два подхода к расчету
(на практике можно встретить и больше). Время ожидания всех процессов на текущий момент
можно получить следующим запросом:
62 Глава 2. Статистика активности
# SELECT
[Link], [Link], [Link],
a.wait_event ||'.'|| a.wait_event_type AS wait,
clock_timestamp() - [Link] AS wait_age
FROM pg_stat_activity a, pg_locks l
WHERE [Link] = [Link]
AND NOT [Link];
pid | state | granted | wait | wait_age
---------+--------+---------+--------------------+-----------------
2405179 | active | f | [Link] | 00:00:00.284314
2406471 | active | f | [Link] | 00:00:00.277662
2405394 | active | f | [Link] | 00:00:00.018215
2406467 | active | f | [Link] | 00:00:00.060994
2406472 | active | f | [Link] | 00:00:00.240659
2406466 | active | f | [Link] | 00:00:00.125218
Первый вариант подсчета сводится к суммированию всех ожиданий всех процессов. В этом
случае получается общее время ожидания, и чем оно больше, тем хуже ситуация, особенно
в системах с большой конкурентностью. Вариант подходит для общей оценки того, сколько
времени СУБД тратит на ожидание.
Второй вариант — это учет лишь максимального времени ожидания среди всех процессов.
В этом случае получится картина только по одному процессу, который ждет дольше всех
остальных. Вариант подходит для оперативного мониторинга, чтобы понимать, что в конкрет-
ный момент есть (или был) конкретный процесс, который находился в ожидании конкретный
интервал времени.
Убрав в исходном запросе лишние поля и обернув его в CTE, можно получить оба значения
и заодно посчитать количество ждущих процессов:
# WITH q AS (
SELECT clock_timestamp() - [Link] AS wait_age
FROM pg_stat_activity a, pg_locks l
WHERE [Link] = [Link] AND NOT [Link]
) SELECT count(*),
coalesce(max(wait_age), '0'::interval) AS max,
coalesce(sum(wait_age), '0'::interval) AS sum
FROM q;
count | max | sum
-------+-----------------+----------------
6 | 00:00:00.284314 | 00:00:01.007062
В этом выводе видно, что в момент опроса было шесть процессов в ожидании, самый долгий
ждал около 284 миллисекунд, а суммарно все клиенты СУБД прождали примерно одну секунду.
В контексте синтетической нагрузки тестового окружения такие цифры могут казаться незна-
чительными, однако в реальных окружениях значения могут быть совсем другого порядка.
Своевременное обнаружение и сокращение времени ожидания позволит увеличить произво-
дительность приложений и СУБД.
2.7. Отслеживание времени ожидания блокировок 63
# postgres_activity_max_seconds{service_id="primary",state="waiting"}
Использование pg_stat_activity.state_change
Для демонстрации потребуется небольшая таблица, например та, что создается стандартным
сценарием pgbench. В эксперименте эта таблица содержит 50 000 строк. Понадобится открыть
64 Глава 2. Статистика активности
три сеанса к СУБД: первый и второй сеансы — для воспроизведения тестового сценария с бло-
кировкой запроса, третий — для отслеживания статистики. В первом сеансе нужно открыть
транзакцию и обновить последнюю строку в тестовой таблице. Транзакцию при этом следует
оставить открытой:
# BEGIN;
# UPDATE pgbench_accounts SET abalance = abalance + 1 WHERE aid = 50000;
Во втором сеансе понадобится предварительно узнать pid процесса, после чего запустить пол-
ное обновление всей таблицы.
# SELECT pg_backend_pid();
2291758
# UPDATE pgbench_accounts SET abalance = abalance + 1;
# SELECT
[Link], [Link], [Link], a.wait_event ||'.'|| a.wait_event_type AS wait,
(clock_timestamp() - a.state_change)::interval(0) AS state_age,
(clock_timestamp() - [Link])::interval(0) AS wait_age
FROM pg_stat_activity a, pg_locks l
WHERE [Link] = [Link] AND [Link] = 2291758;
В выводе запроса нас интересуют поля state_age и wait_age, которые и будут демонстриро-
вать отличие способов в подсчете времени ожидания процесса. Полное обновление таблицы
будет выполняться до тех пор, пока не будет достигнута последняя строка, которая ранее была
обновлена во все еще незакрытой транзакции.
Здесь видно, что у процесса состояние active и время смены состояния state_age увеличива-
ется. Значение granted = true указывает на то, что ожидания блокировок нет и запрос работает.
Через какое-то время выполнение запроса остановится, и можно будет увидеть примерно сле-
дующее:
2.8. Дерево блокировок 65
Появилась строка для блокировки, которую не удалось взять, с состоянием granted = false
и отсчетом, начавшимся в wait_age. При этом значение state осталось прежним, state_age
не сбросился и продолжает отсчитываться. В этом примере получается, что запрос выполнял
полезную работу примерно девять секунд до начала ожидания блокировки. Фактически ожи-
дание началось после девяти секунд работы запроса. Это поведение и демонстрирует отличие
способов учета с помощью waitstart и state_change. Если вы решите повторить эксперимент,
не забудьте в конце отменить или закрыть транзакцию, это позволит запросу во втором сеансе
завершиться.
Используя учет времени на основе state_change, следует помнить о таком поведении. На-
пример, в OLTP-нагрузке, преимущественно состоящей из коротких и быстрых запросов, оно
не сильно исказит статистику, и этот способ подсчета может показаться приемлемым. А вот
в случае более долгих OLAP-запросов (как в эксперименте выше) статистика может оказаться
неточной, время ожидания будет несправедливо включать в себя время выполнения полезной
работы, и способ подсчета может оказаться сомнительным.
На практике часто бывает так, что в определенные моменты довольно много процессов нахо-
дятся в ожидании блокировок, причем часть процессов оказываются заблокированы другими
процессами. Если в такой ситуации найти источник блокировки и устранить его, то с высо-
кой вероятностью остальные процессы смогут нормально продолжить работу. Однако, не имея
подготовленных средств, в такой ситуации довольно сложно точно идентифицировать про-
цесс, который стал причиной всех блокировок. Можно анализировать непосредственно вывод
pg_stat_activity и pg_locks, но это неудобно и может занять много времени. Для опреде-
ления источников блокировок нужно выявить зависимости между процессами и построить
дерево зависимостей между заблокированными и блокирующими процессами. Такая визу-
ализация позволит быстро определить источник блокировки и устранить его. Подход к ре-
шению таких проблем не является новым, и на просторах интернета можно найти разные
реализации, которые позволяют выводить дерево блокировок. Такие запросы основываются
на pg_stat_activity и pg_locks и, как правило, занимают несколько страниц, поэтому я не бу-
ду приводить их здесь. Такие запросы для удобства использования лучше всего оборачивать
в представления.
66 Глава 2. Статистика активности
В качестве отправной точки я взял этот запрос1 и немного модифицировал его. Итоговый за-
прос можно найти в репозитории книги, в файле playground/scripts/locktree.sql2 . Ключевой
особенностью запроса является использование функции pg_blocking_pids. Функция прини-
мает идентификатор процесса и возвращает список процессов, которые его блокируют. Это
довольно удобно и позволяет избежать использования pg_locks, однако, согласно документа-
ции, частый вызов этой функции может негативно сказываться на производительности СУБД,
так как функция получает кратковременный исключительный доступ к общему состоянию ме-
неджера блокировок. Так или иначе, функция удобная, и использование ее в редких случаях
поиска не должно быть проблемой. Не следует применять ее регулярно для нужд мониторин-
га, например для снятия метрик. В определенных условиях эксплуатации (высокая нагрузка,
требования к задержкам), когда эта особенность даже для целей отладки оказывается непри-
емлемой, вместо pg_blocking_pids можно использовать pg_locks3 .
pid | blocked_by | state | wait | wait_age | tx_age | usename | datname | blkd | query
---------+-------------------+----------+---------------------+----------+-----------+---------+---------+------+---------------------------------------------------------------------------------------
2072376 | {} | active | [Link] | | 00:00:00 | classic | pgbench | 1 | [2072376] END;
2072427 | {2072376} | waiting | [Link] | 00:00:00 | 00:00:00 | maru | pgbench | 0 | [2072427] . UPDATE pgbench_branches SET bbalance = bbalance + 3135 WHERE bid = 17;
2072386 | {} | active | [Link] | | 00:00:01 | classic | pgbench | 1 | [2072386] END;
2072382 | {2072386} | waiting | [Link] | 00:00:01 | 00:00:01 | classic | pgbench | 0 | [2072382] . UPDATE pgbench_branches SET bbalance = bbalance + 232 WHERE bid = 15;
2072387 | {} | active | [Link] | | 00:00:01 | classic | pgbench | 1 | [2072387] END;
2072388 | {2072387} | waiting | [Link] | 00:00:00 | 00:00:00 | classic | pgbench | 0 | [2072388] . UPDATE pgbench_branches SET bbalance = bbalance + 3169 WHERE bid = 13;
2072394 | {} | active | [Link] | | 00:00:01 | classic | pgbench | 1 | [2072394] END;
2072391 | {2072394} | waiting | [Link] | 00:00:00 | 00:00:00 | classic | pgbench | 0 | [2072391] . UPDATE pgbench_branches SET bbalance = bbalance + -2825 WHERE bid = 6;
2072399 | {} | active | [Link] | | 00:00:01 | classic | pgbench | 1 | [2072399] END;
2072390 | {2072399} | waiting | [Link] | 00:00:01 | 00:00:01 | classic | pgbench | 0 | [2072390] . UPDATE pgbench_branches SET bbalance = bbalance + -122 WHERE bid = 7;
2072415 | {} | active | [Link] | | 00:00:01 | maru | pgbench | 1 | [2072415] END;
2072418 | {2072415} | waiting | [Link] | 00:00:00 | 00:00:00 | maru | pgbench | 0 | [2072418] . UPDATE pgbench_branches SET bbalance = bbalance + -3319 WHERE bid = 12;
2072423 | {} | active | [Link] | | 00:00:01 | maru | pgbench | 2 | [2072423] END;
2072410 | {2072423} | waiting | [Link] | 00:00:01 | 00:00:01 | serral | pgbench | 0 | [2072410] . UPDATE pgbench_tellers SET tbalance = tbalance + 1404 WHERE tid = 18;
2072416 | {2072423} | waiting | [Link] | 00:00:00 | 00:00:00 | maru | pgbench | 0 | [2072416] . UPDATE pgbench_branches SET bbalance = bbalance + 1468 WHERE bid = 11;
2072424 | {} | active | [Link] | | 00:00:01 | maru | pgbench | 4 | [2072424] END;
2072408 | {2072424} | waiting | [Link] | 00:00:01 | 00:00:01 | serral | pgbench | 3 | [2072408] . UPDATE pgbench_branches SET bbalance = bbalance + -1605 WHERE bid = 8;
2072392 | {2072408} | waiting | [Link] | 00:00:00 | 00:00:00 | classic | pgbench | 2 | [2072392] .. UPDATE pgbench_branches SET bbalance = bbalance + -3889 WHERE bid = 8;
2072409 | {2072408,2072392} | waiting | [Link] | 00:00:00 | 00:00:00 | serral | pgbench | 1 | [2072409] ... UPDATE pgbench_branches SET bbalance = bbalance + 4620 WHERE bid = 8;
2072419 | {2072409} | waiting | [Link] | 00:00:00 | 00:00:00 | maru | pgbench | 0 | [2072419] .... UPDATE pgbench_tellers SET tbalance = tbalance + 2315 WHERE tid = 180;
Все процессы можно условно разделить на группы, в которых есть основной источник бло-
кировки, который никем не блокируется, и есть заблокированные процессы, которые могут
также блокировать других участников группы. Давайте более внимательно рассмотрим по-
следнюю группу, где главным процессом, который заблокировал остальных участников, явля-
ется процесс с идентификатором 2072424. По тексту запроса видно, что это фиксация транзак-
ции (END, или COMMIT), сам процесс находится в активном состоянии и его маркер ожидания —
[Link]. Это указывает на то, что происходит запись в WAL-журнал, и пока она не за-
вершится, клиент не получит возможность отправлять другие команды или открывать тран-
закции. Такое поведение характерно в режиме синхронной фиксации (см. synchronous_commit),
который используется по умолчанию. По полю blkd можно увидеть, что фиксация транзакции
блокирует еще четыре процесса, которые описаны в строках ниже. По полям pid и blocked_by
можно проследить взаимосвязи процессов и кто кого блокирует. Также глубину блокировки
удобно отслеживать по символу точки в поле query. По тексту запроса в этом же поле видно, что
три процесса пытаются обновить одну и ту же строку по условию WHERE bid = 8. Вероятней всего,
эта строка была обновлена в транзакции, которая фиксируется в данный момент, чем и вызва-
на очередь ожидания. Поля state и wait указывают на то, что все четыре процесса находятся
в ожидании блокировок. Судя по полю wait_age, время ожидания составляет меньше одной
секунды — это вполне приемлемо для тестовой рабочей нагрузки, а вот в производственной
нагрузке даже такие короткие ожидания могут стать причиной задержек в приложениях. Поля
usename и datname указывают, от имени каких пользователей и в какой базе данных возникли
блокировки.
Рассмотрим еще один пример дерева блокировок, который может возникнуть в тестовом окру-
жении.
pid | blocked_by | state | wait | wait_age | tx_age | usename | datname | blkd | query
------+------------+---------+--------------------+----------+----------+----------+---------+------+-----------------------------------------------------------------------------------------------...
161 | {} | active | [Link] | | 00:00:00 | postgres | pgbench | 20 | [161] TRUNCATE pgbench_history ;
1923 | {161} | waiting | Lock. relation | 00:00:00 | 00:00:00 | serral | pgbench | 0 | [1923] . INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (15, 13, 1518573,...
1924 | {161} | waiting | Lock. relation | 00:00:00 | 00:00:00 | serral | pgbench | 2 | [1924] . INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (46, 6, 589135, -...
1925 | {161} | waiting | Lock. relation | 00:00:00 | 00:00:01 | serral | pgbench | 1 | [1925] . INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (106, 12, 100334,...
1929 | {161} | waiting | Lock. relation | 00:00:00 | 00:00:00 | serral | pgbench | 0 | [1929] . INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (93, 18, 1330287,...
1936 | {161} | waiting | Lock. relation | 00:00:00 | 00:00:00 | maru | pgbench | 0 | [1936] . INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (99, 7, 914556, 1...
1942 | {161} | waiting | Lock. relation | 00:00:00 | 00:00:00 | maru | pgbench | 3 | [1942] . INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (61, 20, 669763, ...
1957 | {161} | waiting | Lock. relation | 00:00:00 | 00:00:00 | classic | pgbench | 0 | [1957] . INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (104, 4, 880250, ...
1963 | {161} | waiting | Lock. relation | 00:00:00 | 00:00:00 | classic | pgbench | 0 | [1963] . INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (162, 8, 1371725,...
1964 | {161} | waiting | Lock. relation | 00:00:00 | 00:00:00 | classic | pgbench | 0 | [1964] . INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (69, 9, 71583, 18...
1971 | {161} | waiting | Lock. relation | 00:00:00 | 00:00:01 | classic | pgbench | 1 | [1971] . INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (44, 14, 1331579,...
1976 | {161} | waiting | Lock. relation | 00:00:00 | 00:00:00 | classic | pgbench | 2 | [1976] . INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (60, 17, 1238351,...
955 | {1942} | waiting | [Link] | 00:00:00 | 00:00:00 | pgbench | pgbench | 1 | [955] .. UPDATE pgbench_branches SET bbalance = bbalance + -2769 WHERE bid = 20;
1926 | {1976} | waiting | [Link] | 00:00:00 | 00:00:00 | serral | pgbench | 0 | [1926] .. UPDATE pgbench_branches SET bbalance = bbalance + -410 WHERE bid = 17;
1927 | {1971} | waiting | [Link] | 00:00:00 | 00:00:00 | serral | pgbench | 0 | [1927] .. UPDATE pgbench_branches SET bbalance = bbalance + -4047 WHERE bid = 14;
1930 | {1924} | waiting | [Link] | 00:00:00 | 00:00:00 | serral | pgbench | 1 | [1930] .. UPDATE pgbench_branches SET bbalance = bbalance + -3100 WHERE bid = 6;
1938 | {1925} | waiting | [Link] | 00:00:00 | 00:00:00 | maru | pgbench | 0 | [1938] .. UPDATE pgbench_branches SET bbalance = bbalance + 608 WHERE bid = 12;
1955 | {1976} | waiting | [Link] | 00:00:00 | 00:00:00 | classic | pgbench | 0 | [1955] .. UPDATE pgbench_branches SET bbalance = bbalance + 1745 WHERE bid = 17;
1966 | {1942} | waiting | [Link] | 00:00:00 | 00:00:00 | classic | pgbench | 0 | [1966] .. UPDATE pgbench_branches SET bbalance = bbalance + -4835 WHERE bid = 20;
1931 | {955} | waiting | [Link] | 00:00:00 | 00:00:00 | serral | pgbench | 0 | [1931] ... UPDATE pgbench_tellers SET tbalance = tbalance + -4006 WHERE tid = 182;
1972 | {1930} | waiting | [Link] | 00:00:00 | 00:00:00 | classic | pgbench | 0 | [1972] ... UPDATE pgbench_branches SET bbalance = bbalance + -3771 WHERE bid = 6;
68 Глава 2. Статистика активности
Оба рассмотренных случая относятся к рабочей нагрузке в тестовом окружении, которое ха-
рактеризуется короткими транзакциями и такими же короткими блокировками. На практике
все может быть намного сложнее, особенно если источником блокировок выступают бездей-
ствующие транзакции, которые могут удерживать блокировки непредсказуемо долго и потен-
циально приводить к аварийным ситуациям. Порядок действия в таких ситуациях, как прави-
ло, сводится к принудительному завершению процессов. В критической ситуации важно быст-
ро сориентироваться и найти тот корневой процесс, завершение которого разрешит ситуацию.
При недостатке информации часто приходится без разбора завершать множество процессов.
В случае же использования запросов, подобных рассмотренному, можно с большей точностью
определить и устранить источник блокировки, позволив остальным процессам продолжить
работу.
Резюме
После установки соединения и настройки сеанса СУБД готова выполнять команды клиента.
Посредством команд клиент может запрашивать и изменять данные (DML-команды), управ-
лять объектами схемы (DDL-команды), выполнять служебные операции и т. п. Полный список
команд можно найти в официальной документации1 . Базовой единицей выполнения в ра-
бочей нагрузке можно считать запросы (queries) — это, как правило, команды на извлечение
и изменение данных: SELECT, INSERT, UPDATE, DELETE.
1
[Link]/docs/postgresql/current/[Link]
72 Глава 3. Выполнение запросов и функций
Помимо этих стандартных средств, есть и другие инструменты, которые разрабатываются со-
обществом на основе pg_stat_statements:
2. После перезапуска СУБД расширение нужно установить в конкретной базе. Для этого
подходит любая база: можно использовать базу данных postgres, которая всегда созда-
ется по умолчанию. Несмотря на то что расширение устанавливается в конкретную базу,
сбор статистики осуществляется глобально для всех запросов независимо от того, в ка-
ких базах они работают. Установка выполняется с помощью команды CREATE EXTENSION
pg_stat_statements;.
На данный момент уже накоплена статистика по 113 типам запросов. Запросы, даже очень по-
хожие друг на друга, могут отличаться значениями параметров. Хранить в статистике запросы
со всеми значениями параметров избыточно — запросы могут содержать сотни и тысячи пара-
метров и уникальных значений. Поэтому в процессе сбора для каждого запроса выполняется
74 Глава 3. Выполнение запросов и функций
При наличии хранимых процедур и функций имеет смысл устанавливать значение all,
что позволит иметь более детальную статистику о выполняемой рабочей нагрузке.
Можно заметить, что разная статистика появляется в разных версиях расширения. При обнов-
лениях основной версии СУБД следует также обновлять и расширения.
Текст запроса и его метаданные позволяют отличать одни запросы от других и дают отправ-
ную точку поиска приложения, отправившего запрос. Запрос и его метаданные представлены
несколькими полями:
• toplevel — уровень вызова запроса: true для вызовов верхнего уровня и false, если вызов
был вложен в процедуру или функцию. Если pg_stat_statements.track установлен в значение
top, поле всегда равно true. При создании отчетов производительности этот флаг позво-
ляет отфильтровать вложенные вызовы и не учитывать их статистику, поскольку она уже
учтена в статистике запросов верхнего уровня. Без использования этого флага статистика
может быть посчитана несколько раз, и отчет получится неверным.
Наибольший интерес в этой статистике вызывают время планирования и факты его значи-
тельных изменений, особенно в бóльшую сторону, когда запрос стал планироваться дольше
при более или менее постоянном количестве планирований. Такое случается на практике, но,
к счастью, это большая редкость.
• время планирования отдельных типов запросов может быть полезно для отслеживания
аномалий планирования в случаях, когда объем выполняемых запросов не изменяется;
• величина стандартного отклонения показывает, насколько сильно время планирования
«гуляет» относительно среднего времени планирования.
Давайте рассмотрим в качестве примера первый вариант. Для этого нам потребуется следу-
ющий запрос:
# topk_avg(5,
sum by (queryid,query) (
rate(postgres_statements_time_seconds_total{
service_id="primary",mode="planning"
}[1m]) + on(database,user,queryid) group_left(query)
0 * postgres_statements_query_info{service_id="primary"}
), "other")
Подобные запросы будут и дальше использоваться в этой главе, поэтому его стоит детально
разобрать:
На основе запроса можно построить так называемый график top-K (рис. 3.1), который будет
довольно часто появляться на протяжении всей книги. Удобство этого графика заключается
в том, что он позволяет вывести и наибольшие величины, и сумму оставшихся.
Каждая строка легенды содержит идентификатор и текст запроса, а также последнее и макси-
мальное значение метрики. Для каждого типа запросов график показывает, сколько машинного
времени СУБД тратит на планирование в одну секунду реального времени. В машинном време-
ни суммируется продолжительность работы всех ядер, поэтому в нагруженных системах с мно-
гоядерными процессорами машинное время за одну реальную секунду легко может секунду
превышать. График отображает изменение частоты именно суммарного времени планирова-
ния всех запросов конкретного типа, и эту особенность очень важно понимать. На графике
отмечена конкретная точка — 18:30:00, и в эту секунду реального времени на планирование
всех запросов всеми ядрами в сумме было затрачено около 16 миллисекунд (более подробная
детализация приведена во всплывающей подсказке).
Стадия исполнения — это основной этап жизни запроса, в котором СУБД, используя системные
ресурсы, выполняет его согласно плану и возвращает результат пользователю. Результатом мо-
жет быть набор строк или тег (command tag). От скорости выполнения напрямую зависят теку-
щая производительность и время отклика, ведь чем быстрее выполнится запрос и чем больше
их можно выполнить за единицу времени, тем больше и общая производительность СУБД. Ста-
тистика исполнения запросов является наиболее полезной и востребованной, так как позво-
ляет найти аномалии при выполнении запросов и сразу же начать оптимизацию (рефакторинг
запроса, добавление индекса и т. п.), не погружаясь при этом глубоко в детали использования
ресурсов.
• calls — количество успешно исполненных запросов. По смыслу это поле близко к plans,
однако оба поля являются независимыми, считаются отдельно друг от друга и могут содер-
жать разные значения. Успешное планирование не гарантирует, что исполнение не завер-
шится ошибкой. И наоборот, из успешного исполнения не следует, что перед ним выпол-
нялось планирование: в случае подготовленных запросов (prepared statements) на этапе
подготовки план выполнения может кешироваться;
• rows — общее количество строк, которое было возвращено (SELECT) или затронуто (INSERT,
UPDATE, DELETE) в результате выполнения всех запросов данного типа;
Время планирования
Запросы с наибольшим количеством вызовов. Количество вызовов можно получить в поле calls
и с его помощью определить текущую производительность. Для ее вычисления нужно сумми-
ровать значения calls всех запросов:
# sum(rate(postgres_statements_calls_total{service_id="primary"})[1m])
Такой график дает поверхностное представление о текущей рабочей нагрузке, и при ее из-
менении можно увидеть и изменение в производительности, особенно в случае значительных
колебаний. Исключив общее суммирование, можно получить подробную детализацию и опре-
делить долю отдельных типов запросов в общей производительности. Это дает понимание
того, какие запросы выполняются чаще остальных и на каких запросах стоит сосредоточиться
при оптимизации.
3.5. Исполнение запроса 81
# SELECT
sum(calls) AS total_calls,
left(query, 64) AS query_trunc
FROM pg_stat_statements
GROUP BY query
ORDER BY sum(calls) DESC
LIMIT 10;
total_calls | query_trunc
-------------+------------------------------------------------------------------
24525219 | SELECT abalance FROM pgbench_accounts WHERE aid = $1
24525219 | BEGIN
24525219 | UPDATE pgbench_accounts SET abalance = abalance + $1 WHERE aid =
24525219 | UPDATE pgbench_tellers SET tbalance = tbalance + $1 WHERE tid =
24525217 | UPDATE pgbench_branches SET bbalance = bbalance + $1 WHERE bid =
24525213 | INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES
24525213 | END
270642 | SELECT setting FROM pg_settings WHERE name = $1
225535 | SELECT datname FROM pg_database WHERE NOT datistemplate AND data
135321 | SELECT extnamespace::regnamespace FROM pg_extension WHERE extnam
Результат запроса показывает десять наиболее часто выполняемых запросов. Однако здесь мы
снова видим «большие» цифры, которые указывают на общую статистику, накопленную за весь
период сбора. Это может быть полезно для построения отчетов, но в оперативной ситуации
нагрузка непостоянна, и важно видеть изменение частоты выполнения запросов.
С помощью графиков можно увидеть эту картину в динамике, и для начала можем воспользо-
ваться следующим запросом:
82 Глава 3. Выполнение запросов и функций
# topk_avg(5,
sum by (queryid,query) (
rate(postgres_statements_calls_total{service_id="primary"}[1m])
+ on(database,user,queryid) group_left(query)
0 * postgres_statements_query_info{service_id="primary"}
), "other")
# SELECT
sum(rows) AS total_rows,
left(query, 64) AS query_trunc
FROM pg_stat_statements
GROUP BY query
ORDER BY sum(rows) DESC
LIMIT 10;
total_rows | query_trunc
------------+------------------------------------------------------------------
27968942 | UPDATE pgbench_tellers SET tbalance = tbalance + $1 WHERE tid =
27968942 | UPDATE pgbench_accounts SET abalance = abalance + $1 WHERE aid =
27968942 | SELECT abalance FROM pgbench_accounts WHERE aid = $1
27968940 | UPDATE pgbench_branches SET bbalance = bbalance + $1 WHERE bid =
27968936 | INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES
17857314 | SELECT name, setting, unit, vartype FROM pg_show_all_settings()
5415236 | SELECT [Link] AS database, pg_get_userbyid([Link]) AS user,
2768140 | SELECT coalesce(usename, backend_type) AS user, datname AS datab
2000000 | update pgbench_accounts set abalance = abalance + $1
514620 | SELECT datname FROM pg_database WHERE NOT datistemplate AND data
# topk_avg(5,
sum by (queryid,query) (
rate(postgres_statements_rows_total{service_id="primary"}[1m])
+ on(database,user,queryid) group_left(query)
0 * postgres_statements_query_info{service_id="primary"}
), "other")
При стабильной рабочей нагрузке без сильных колебаний этот график может выглядеть более
или менее ровно, при этом практически любой аномальный запрос выделяется довольно хо-
рошо. В данном примере это можно наблюдать по простому запросу, который был запущен
в цикле с помощью команды \watch 1 в psql:
Этот запрос возвращает примерно 490 строк в секунду, в то время как его ближайшие сосе-
ди — в среднем по 35 строк, и это хорошо заметно на графике. На практике так обычно выгля-
дят запросы с забытым ключевым словом LIMIT. В запросе, например, может использоваться
сортировка, и для выполнения условий запроса и последующей сортировки СУБД проскани-
рует большое количество строк и, более того, отправит их клиенту, что вызовет избыточный
расход ресурсов не только на стороне СУБД, но и на стороне приложения. Если это какой-то
регулярно выполняемый запрос, а не разовая задача, то нужно проанализировать запрос и его
соответствие техническому заданию. Если запрос написан именно так, как и задумано, стоит
проверить бизнес-требования функции, для которой используется этот запрос, — насколько
оправданно обращение к такому большому количеству строк (даже для разбиения на страни-
цы есть способы читать ровно столько, сколько нужно).
# SELECT
to_char(
interval '1 millisecond' * sum(total_exec_time), 'HH24:MI:SS'
) AS exec_time,
left(query, 64) AS query_trunc
FROM pg_stat_statements
GROUP BY query ORDER BY sum(total_exec_time) DESC LIMIT 10;
exec_time | query_trunc
-----------+------------------------------------------------------------------
105:41:06 | UPDATE pgbench_branches SET bbalance = bbalance + $1 WHERE bid =
14:14:38 | UPDATE pgbench_tellers SET tbalance = tbalance + $1 WHERE tid =
04:00:12 | UPDATE pgbench_accounts SET abalance = abalance + $1 WHERE aid =
00:53:14 | SELECT current_database() AS database, schemaname AS schema, fun
00:45:37 | SELECT current_database() AS database, [Link] AS schema,
00:43:06 | SELECT current_database() AS database, schemaname AS schema, rel
00:26:14 | SELECT coalesce(datname, $1) AS database, xact_commit, xact_roll
00:25:45 | SELECT checkpoints_timed, checkpoints_req, checkpoint_write_time
00:25:44 | SELECT pg_is_in_recovery()::int AS recovery, wal_records, wal_fp
00:25:17 | SELECT archived_count, failed_count, extract($1 from now() - las
3.5. Исполнение запроса 85
Напомню, что поле total_exec_time хранит время в миллисекундах, и в этом запросе оно для
удобства преобразовано в привычный формат представления времени. Также для учета можно
дополнительно включить и время планирования total_plan_time, получив суммарное время
выполнения запроса, включая планирование.
В результате можно заметить существенно выделяющийся запрос: его суммарное время вы-
полнения составило примерно 105 часов с отрывом от второго места в семь раз. Выполнение
этого запроса и занимает большую часть рабочей нагрузки. Однако важным нюансом, кото-
рый остался за кадром, является ответ на вопрос: а за какой интервал времени представлена
эта статистика? Ведь 105 машинных часов в рамках одного дня или одного месяца — это со-
вершенно разная рабочая нагрузка. Для получения ответа на этот вопрос можно обратиться
к pg_stat_statements_info.stats_reset:
В данном случае статистика накоплена за 17 дней; получается, что интересующий нас запрос
в течение суток выполнялся примерно шесть часов. В принципе, это немного, учитывая, что
остается 18 часов, а если умножить часы на количество доступных процессорных ядер, то и еще
больше.
# topk_avg(5,
sum by (queryid,query) (
rate(postgres_statements_time_seconds_total{
service_id="primary",mode="executing"
}[1m]) + on(database,user,queryid) group_left(query)
0 * postgres_statements_query_info{service_id="primary"}
), "other")
Учет статистики ведется только при включенном параметре track_io_timing. По опыту, всегда
рекомендуется его включать (однако накладные расходы могут быть на уровне 1 %). За счет
этой статистики появляется возможность выявить те запросы, которым регулярно требуется
доступ к диску, и запланировать оптимизацию или увеличение ресурсов.
3.5. Исполнение запроса 87
Можно вычислять как отдельные статистики по чтению и записи, так и одну общую. Обычно
на практике используется второй вариант:
# SELECT
to_char(
interval '1 millisecond' * sum(
blk_read_time + blk_write_time + temp_blk_read_time + temp_blk_write_time
), 'HH24:MI:[Link]'
) AS io_time,
left(query, 64) AS query_trunc
FROM pg_stat_statements
WHERE blk_read_time + blk_write_time + temp_blk_read_time + temp_blk_write_time > 0
GROUP BY query
ORDER BY sum(blk_read_time + blk_write_time + temp_blk_read_time + temp_blk_write_time) DESC
LIMIT 5;
io_time | query_trunc
--------------+------------------------------------------------------------------
01:25:08.755 | UPDATE pgbench_accounts SET abalance = abalance + $1 WHERE aid =
00:00:02.125 | INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES
00:00:00.121 | truncate pgbench_history
00:00:00.050 | UPDATE pgbench_branches SET bbalance = bbalance + $1 WHERE bid =
00:00:00.003 | SELECT current_database() AS database, schemaname AS schema, fun
Бóльшую часть времени ввод-вывод выполняется на одном типе запросов. Это другой запрос,
не тот, что встретился в предыдущей части и на выполнение которого тратилось больше всего
времени. Рассматривая подобные запросы, нужно учитывать частоту их выполнения. Напри-
мер, обращение к диску может быть приемлемо для запросов, которые выполняются редко
и затрагивают холодные (архивные) данные. К регулярно выполняющимся запросам, напо-
добие запроса, обнаруженного в тестовом окружении, такое постоянное обращение к диску
может считаться накладным и увеличивать время выполнения запросов. Устранив обраще-
ния к диску или хотя бы уменьшив время обращения, можно ускорить выполнение запросов.
В зависимости от характера обращений есть различные варианты: увеличить буферный кеш,
так, чтобы все необходимые горячие данные полностью помещались в него; модернизировать
системные ресурсы (увеличить память, использовать более производительный накопитель,
виртуальную машину и т. п.). Самый лучший вариант, конечно же, провести анализ запроса
и оптимизацию.
# topk_avg(5,
sum by (queryid,query) (
rate(postgres_statements_time_seconds_total{
service_id="primary",mode=~"ioread|iowrite"
}[1m]) + on(database,user,queryid) group_left(query)
0 * postgres_statements_query_info{service_id="primary"}
), "other")
88 Глава 3. Выполнение запросов и функций
Обратите внимание, что в запросе для метки mode используется перечисление значений ioread
и iowrite; при необходимости можно получить время только чтения или только записи.
Через пару минут, когда данные попадут в мониторинг, можно увидеть, как выросло время
ввода-вывода до 1,16 секунды в пике (рис. 3.7). По мере продолжения рабочей нагрузки и на-
полнения буферного и страничного кешей время ввода-вывода будет постепенно уменьшать-
ся и в какой-то момент окончательно придет к значению, которое было до сброса страничного
кеша.
Итак, для получения статистики следует из общего времени total_exec_time вычесть время,
потраченное на блочный ввод-вывод:
# SELECT
to_char(
interval '1 millisecond' * sum(total_exec_time - (
blk_read_time + blk_write_time + temp_blk_read_time + temp_blk_write_time)),
'HH24:MI:SS') AS cpu_time,
left(query, 64) AS query_trunc
FROM pg_stat_statements
GROUP BY query
ORDER BY sum(total_exec_time - (
blk_read_time + blk_write_time + temp_blk_read_time + temp_blk_write_time)
) DESC LIMIT 10;
cpu_time | query_trunc
-----------+------------------------------------------------------------------
105:41:06 | UPDATE pgbench_branches SET bbalance = bbalance + $1 WHERE bid =
14:14:38 | UPDATE pgbench_tellers SET tbalance = tbalance + $1 WHERE tid =
02:35:03 | UPDATE pgbench_accounts SET abalance = abalance + $1 WHERE aid =
00:53:14 | SELECT current_database() AS database, schemaname AS schema, fun
00:45:37 | SELECT current_database() AS database, [Link] AS schema,
00:43:06 | SELECT current_database() AS database, schemaname AS schema, rel
00:26:14 | SELECT coalesce(datname, $1) AS database, xact_commit, xact_roll
00:25:45 | SELECT checkpoints_timed, checkpoints_req, checkpoint_write_time
00:25:44 | SELECT pg_is_in_recovery()::int AS recovery, wal_records, wal_fp
00:25:17 | SELECT archived_count, failed_count, extract($1 from now() - las
Здесь процессорное время практически совпадает с общим. Это значит, что СУБД тратит
на блочный ввод-вывод относительно немного времени, всего около 1 %:
90 Глава 3. Выполнение запросов и функций
# SELECT
sum(blk_read_time + blk_write_time + temp_blk_read_time + temp_blk_write_time
) / sum(total_exec_time) AS io_percent,
sum(total_exec_time -
(blk_read_time + blk_write_time + temp_blk_read_time + temp_blk_write_time)
) / sum(total_exec_time) AS cpu_percent
FROM pg_stat_statements;
io_percent | cpu_percent
---------------------+--------------------
0.01101825786144295 | 0.9889817421385566
При использовании SQL для оценки времени выполнения запроса всегда полезно выводить
обе статистики вместе, чтобы видеть, на что именно тратится время.
# SELECT
to_char(
interval '1 millisecond' * sum(total_exec_time),
'HH24:MI:SS'
) AS exec_time,
(100 * sum(
blk_read_time + blk_write_time +
temp_blk_read_time + temp_blk_write_time
) / sum(total_exec_time))::numeric(5,2)::text || ' / ' ||
(100 * sum(total_exec_time - (
blk_read_time + blk_write_time +
temp_blk_read_time + temp_blk_write_time)
) / sum(total_exec_time))::numeric(5,2) AS "io / cpu, %",
left(query, 48) AS query_trunc
FROM pg_stat_statements
GROUP BY query ORDER BY sum(total_exec_time) DESC LIMIT 10;
exec_time | io / cpu, % | query_trunc
-----------+---------------+--------------------------------------------------
105:50:01 | 0.00 / 100.00 | UPDATE pgbench_branches SET bbalance = bbalance
14:15:46 | 0.00 / 100.00 | UPDATE pgbench_tellers SET tbalance = tbalance +
04:00:25 | 35.47 / 64.53 | UPDATE pgbench_accounts SET abalance = abalance
00:53:21 | 0.00 / 100.00 | SELECT current_database() AS database, schemanam
00:45:44 | 0.00 / 100.00 | SELECT current_database() AS database, [Link]
00:43:12 | 0.00 / 100.00 | SELECT current_database() AS database, schemanam
00:26:18 | 0.00 / 100.00 | SELECT coalesce(datname, $1) AS database, xact_c
00:25:49 | 0.00 / 100.00 | SELECT checkpoints_timed, checkpoints_req, check
00:25:48 | 0.00 / 100.00 | SELECT pg_is_in_recovery()::int AS recovery, wal
00:25:21 | 0.00 / 100.00 | SELECT archived_count, failed_count, extract($1
Здесь сразу видно отношение времени, затраченного на ввод-вывод и CPU относительно друг
друга, и запрос, в котором на ввод-вывод тратится примерно треть от общего времени вы-
полнения, довольно хорошо бросается в глаза. Если этому типу запроса дать возможность
находить нужные данные в буферном кеше, то из четырех часов суммарного выполнения
высвободится примерно 80 минут. Это время можно потратить на выполнение других запро-
сов и увеличить производительность.
3.6. Сквозная идентификация с queryid 91
Однако долгое время эти источники не были связаны друг c другом. Чтобы получить це-
лостную картину о конкретном типе запроса, необходимо обработать данные из всех источ-
ников. Но в pg_stat_activity и журналах сообщений указываются полные тексты запросов,
а в pg_stat_statements сохраняются нормализованные запросы, и сопоставить их было не-
простой задачей. Расширение pg_stat_statements умеет рассчитывать идентификатор запроса
queryid, и начиная с версии 14 этот идентификатор стал доступен в остальных источниках —
теперь выполнять идентификацию запроса по всем возможным источникам стало гораздо
проще.
Для оценки эксплуатации системы за длительные периоды могут быть полезны сводные или
суммарные отчеты. Как правило, эти отчеты в разных проекциях показывают, насколько эф-
фективно система справляется с рабочей нагрузкой. С их помощью можно не только быстро
оценить состояние системы, но и отследить динамику относительно предыдущих периодов.
Такие отчеты можно строить на основе статистики по запросам из pg_stat_statements, учиты-
вая ее накопительный характер.
92 Глава 3. Выполнение запросов и функций
Концепция такого отчета довольно проста: нужно определить суммарное использование ре-
сурсов по каждой проекции в pg_stat_statements и затем для каждого отдельного запроса
определить его вклад в эту сумму. Полученные доли использования ресурсов следует отсор-
тировать по какому-либо критерию, например, по суммарному времени выполнения запроса.
Примеры скриптов для создания подобных отчетов можно найти в репозитории DataEgret1 .
Отчет содержит список запросов, выполнение которых заняло больше всего времени, обога-
щенный подробной информацией из pg_stat_statements. Вот пример такого отчета по запро-
сам в тестовом окружении:
# \i /var/lib/postgresql/scripts/query_stat_total.sql
Output format is unaligned.
?column?
total time: 11:50:59 (IO: 1.29%)
total queries: 25,411,617 (unique: 62)
report for all databases, version 0.9.5 @ PostgreSQL 15beta1
tracking all 5000 queries, utilities on, logging 500ms+ queries
=============================================================================================================
pos:1 total time: 05:12:05 (43.9%, CPU: 44.5%, IO: 0.0%) calls: 1,800,650 (7.09%) avg_time: 10.40ms (IO: 0.0%)
user: classic db: pgbench rows: 1,800,650 (8.37%) query:
UPDATE pgbench_branches SET bbalance = bbalance + $1 WHERE bid = $2
=============================================================================================================
pos:2 total time: 02:53:43 (24.4%, CPU: 24.8%, IO: 0.0%) calls: 996,873 (3.92%) avg_time: 10.46ms (IO: 0.0%)
user: maru db: pgbench rows: 996,873 (4.64%) query:
UPDATE pgbench_branches SET bbalance = bbalance + $1 WHERE bid = $2
=============================================================================================================
pos:3 total time: 00:50:08 (7.1%, CPU: 7.1%, IO: 0.0%) calls: 496,689 (1.95%) avg_time: 6.06ms (IO: 0.0%)
user: pgbench db: pgbench rows: 496,689 (2.31%) query:
UPDATE pgbench_branches SET bbalance = bbalance + $1 WHERE bid = $2
1
[Link]/dataegret/pg-utils/blob/master/sql/global_reports/query_stat_total_13.sql
3.7. Построение отчетов на основе pg_stat_statements 93
Полученной информации достаточно, чтобы оценить доли запросов в рабочей нагрузке и при
необходимости перейти к анализу производительности и рефакторингу. При желании скрипт
можно модифицировать и добавить другую необходимую информацию, например период,
за который взята статистика, идентификаторы запросов, использование ресурсов, объем сге-
нерированных журнальных записей и т. д.
В качестве другого примера можно взять вывод отчета из pgcenter, где для любого интересу-
ющего запроса из pg_stat_statements можно получить сводную информацию:
summary:
total queries: 25,421,767
total rows: 21,512,607
total WAL: 25 GB
total_time: 11:51:10, 100%
total_plan_time: 00:26:34, 3.74%
total_cpu_time: 11:15:27, 94.98%
total_io_time: 00:09:08, 1.29%
query info:
queryid: 2368592290184574017
username: classic,
database: pgbench,
calls (relative to total): 1,801,358, 7.09%,
rows (relative to total): 1,801,358, 8.37%,
WAL usage (relative to total):
records: 2,011,320, 10.88%
full-page images: 849, 0.03%
bytes: 144 MB, 0.56%
total times (relative to total): 05:12:10.007, 43.89%
planning: 00:01:40.461, 6.30%
cpu: 05:10:29.546, 45.97%
io: 00:00:00.000, 0.00%
average times (in-query distribution): 10.40ms, 100%
planning: 0.06ms, 0.54%
cpu: 10.34ms, 99.46%
io: 0.00ms, 0.00%
query text:
UPDATE pgbench_branches SET bbalance = bbalance + $1 WHERE bid = $2
94 Глава 3. Выполнение запросов и функций
В фокусе отчета pgcenter находится всего лишь один выбранный запрос. В первой, верхней
части отчета приведена сводная информация, а во второй части показаны статистика по вы-
бранному запросу и его вклад в общую рабочую нагрузку:
Сравнивая оба отчета, можно заметить, что они различаются предназначением. Отчет Data-
Egret предлагает более полную картину о том, что выполняется в СУБД, и больше подходит для
роли сводного отчета. Отчет pgcenter предлагает оценку использования ресурсов конкретным
запросом и больше ориентирован на оперативный анализ и выявление деталей по подозри-
тельным запросам.
При построении подобных отчетов и расчете суммарного использования ресурсов важно пом-
нить, что расширение pg_stat_statements может собирать статистику выполнения не толь-
ко верхнеуровневых, но и вложенных запросов, и это поведение регулируется параметром
pg_stat_statements.track. В случае более детального отслеживания со значением all есть риск
посчитать часть статистики дважды, например, при наличии запросов с функциями, внутри
которых выполняются другие запросы или функции. Подсчет общей статистики будет вклю-
чать статистику как самой функции, так и всех вложенных в нее запросов, что может сильно
исказить результат. Для исключения этой проблемы следует учитывать флаг toplevel, кото-
рый указывает на уровень выполнения и помогает исключить вложенные запросы (toplevel =
false). К сожалению, флаг toplevel появился в версии 14, поэтому в более ранних версиях для
отчетов рекомендуется установить pg_stat_statements.track = top.
• dealloc — количество раз, когда расширение было вынуждено частично удалить статис-
тику из-за достижения ограничения pg_stat_statements.max. При этом возникает риск по-
тери некоторой статистики, что может искажать данные отчетов. В качестве обходного
1
[Link]/docs/postgrespro/current/runtime-config-wal#GUC-FULL-PAGE-WRITES
3.9. Выполнение процедур и функций 95
пути источником данных может быть система мониторинга. Если ограничение еще не до-
стигнуто, проблему можно решить на уровне конфигурации СУБД, увеличив значение
pg_stat_statements.max, но это потребует перезапустить сервер.
• stats_reset — отметка времени последнего сброса статистики. Позволяет четко представ-
лять, за какой интервал времени накоплена статистика по запросам.
Пример вывода:
Для реализации сложной логики при обработке данных можно использовать функции. Такие
функции могут быть написаны на SQL и могут объединять в себе несколько запросов. Кро-
ме SQL поддерживаются и такие процедурные языки (procedural language, PL), как PL/pgSQL,
PL/Tcl, PL/Perl и PL/Python. Когда в рабочей нагрузке присутствуют запросы с вызовами функ-
ций, перед администратором возникает задача поиска медленных функций и их оптими-
зации. Для простейшего анализа производительности функций СУБД есть представление
pg_stat_user_functions, которое содержит необходимый минимум статистики по выполнению
функций. Для более детального профилирования функций придется использовать сторонние
инструменты.
Стоит отметить, что SQL-функции, которые встраиваются (inline) в вызываемый запрос, не от-
слеживаются при любом значении параметра.
• funcname — имя функции, которое может быть неуникальным внутри схемы, поскольку до-
пускаются перегруженные функции, отличающиеся набором входных параметров;
• self_time — общее время выполнения самой функции (в миллисекундах) без учета време-
ни выполнения вложенных функций.
Как я уже отмечал, основной сценарий использования этой статистики — это определение наи-
более часто вызываемых функций и функций, выполнение которых занимает больше всего
времени. В тестовой нагрузке на основе стандартного сценария pgbench отсутствуют вызовы
функций, поэтому статистики будет немного и она будет включать в себя только вызовы функ-
ций из расширений:
В этом запросе нет дополнительных условий или сортировок, так как я заранее знаю, что в ста-
тистике немного данных. Есть статистика только по трем функциям, принадлежащим двум
расширениям: pg_buffercache и pg_stat_statements. Две из них запрашиваются агентом мо-
ниторинга, и в поле calls отражается количество вызовов. Время total_time и self_time для
обеих функций совпадает, поскольку в них не используются вложенные вызовы других пользо-
вательских функций. Суммарное время выполнения функций составило примерно 122 и 14 се-
кунд. Функция pg_buffercache_pages вызывается в два раза чаще, однако ее общее время вы-
полнения больше примерно в 8,5 раза, то есть ее выполнение обходится СУБД дороже.
Используя такой подход и сравнивая время выполнения функций, можно находить наиболее
долгие. Следующим запросом можно получить статистику из мониторинга:
# increase(postgres_function_total_time_seconds_total{service_id="primary"}[1m])
Резюме 97
В запросе используется increase вместо rate. Поскольку эти функции вызываются только аген-
том мониторинга, то очевидно, что вызовы происходят относительно редко, несколько раз
в минуту, поэтому с этой точки зрения удобнее видеть, сколько времени тратится на выполне-
ние функций за одну минуту (вместо одной секунды). В случае же производственных нагрузок,
где выполнение функций может происходить чаще и конкурентно в нескольких сеансах, ис-
пользование rate может быть более предпочтительным.
На рис. 3.8 отражается то, что можно было видеть в выводе запроса к pg_stat_user_functions, —
выполнение функции pg_buffercache_pages занимает бóльшую часть времени:
Резюме
В предыдущей главе мы рассмотрели запросы как основу любой рабочей нагрузки, а также
связанные с ними статистику и мониторинг активности. Другой стороной рабочей нагрузки
являются пользовательские данные. В запросах указываются конкретные таблицы, из которых
читаются и в которые сохраняются данные. В этой главе мы рассмотрим статистику актив-
ности с точки зрения обращений к объектам СУБД и проследим события, которые происходят
в контексте именно этих объектов — баз данных, таблиц и индексов.
Одной из главных задач СУБД является надежное хранение данных и обеспечение доступа
к этим данным. На уровне СУБД средствами организации и хранения данных являются таб-
лицы и другие типы объектов, каждый со своим предназначением. Все эти средства являются
логическим слоем организации данных. Однако СУБД не является вещью самой в себе и вы-
нуждена опираться на средства операционной системы, на ее собственные абстракции и ре-
сурсы. С точки зрения операционной системы данные СУБД размещаются в файловой системе
и используют блочные устройства. На физическом уровне все таблицы, индексы и многие дру-
гие абстракции СУБД — это всего лишь файлы и каталоги на диске.
Дальше мы рассмотрим, как в СУБД организовано хранение данных. Это поможет лучше по-
нять качественный состав рабочей нагрузки (на какие объекты СУБД приходится нагрузка)
100 Глава 4. Базы данных
и более осознанно подойти к созданию мониторинга рабочей нагрузки с точки зрения исполь-
зования объектов и данных. Опытные администраторы, хорошо представляющие внутреннюю
структуру хранения, могут сразу перейти к разделу 4.2.
Первое, что мы рассмотрим, — это концепция кластера баз данных (database cluster). Кластер
представляет собой единый и неделимый набор баз данных и общий для них набор глобаль-
ных объектов. Свойство единости и неделимости означает, что весь набор существует вместе
в рамках одного экземпляра СУБД (instance) — группы процессов, запущенных в пространстве
операционной системы и имеющих область общей памяти. На одном сервере могут работать
несколько экземпляров СУБД, если их TCP-порты не конфликтуют. Кластер баз данных не
следует путать с кластером репликации. Кластер репликации обычно состоит из нескольких
экземпляров СУБД, запущенных в независимых средах (отдельные физические, виртуальные
серверы или контейнеры).
С точки зрения операционной системы кластер баз данных представляет собой набор ката-
логов и файлов. Инициализация кластера осуществляется командой initdb с указанием целе-
вого каталога. Полученный каталог принято называть каталогом данных (data directory), он
содержит в себе другие каталоги и файлы кластера. Исключением могут быть каталоги таблич-
ных пространств и WAL-журнала, которые могут быть вынесены за пределы каталога данных
и связаны с основным каталогом через символические ссылки. Таким образом, каталог дан-
ных и все возможные внешние каталоги (табличные пространства и WAL) образуют хранилище
данных кластера.
Ниже приведен пример каталога данных из тестового окружения. Каждый подкаталог играет
свою значимую роль в функционировании кластера. Администратору важно знать назначение
каждого подкаталога и его роль в работе СУБД.
# ls -l /var/lib/postgresql/data/
total 132
-rw------- 1 postgres postgres 3 Jul 31 08:52 PG_VERSION
drwx------ 7 postgres postgres 4096 Jul 31 08:53 base
-rw------- 1 postgres postgres 46 Aug 2 00:00 current_logfiles
drwx------ 2 postgres postgres 4096 Jul 31 08:55 global
drwx------ 2 postgres postgres 4096 Jul 31 08:52 pg_commit_ts
drwx------ 2 postgres postgres 4096 Jul 31 08:52 pg_dynshmem
-rw------- 1 postgres postgres 4991 Jul 31 08:52 pg_hba.conf
-rw------- 1 postgres postgres 1636 Jul 31 08:52 pg_ident.conf
drwx------ 4 postgres postgres 4096 Aug 2 04:13 pg_logical
drwx------ 4 postgres postgres 4096 Jul 31 08:52 pg_multixact
drwx------ 2 postgres postgres 4096 Jul 31 08:52 pg_notify
drwx------ 3 postgres postgres 4096 Jul 31 08:53 pg_replslot
4.1. Иерархия объектов СУБД 101
pg_wal и pg_xlog
Зачем же нужно знать внутреннюю структуру каталога данных? Хорошим примером явля-
ется история подкаталога WAL-журнала. Этот каталог критически важен для функциони-
рования СУБД. При некоторых обстоятельствах его содержимое может неконтролируемо
расти и занимать все больше и больше места на диске, но удаление каталога может при-
вести к непредсказуемым последствиям. До версии 9.6 включительно каталог журнала WAL
назывался pg_xlog, и неопытные администраторы нередко удаляли его, считая, что там хра-
нятся какие-то «обычные текстовые логи», которыми можно пожертвовать, чтобы освобо-
дить место. В итоге удаление каталога приводило к печальным последствиям1 .
Табличные пространства
и template1, которые используются как шаблоны для создания новых баз (см. команду
CREATE DATABASE1 );
• pg_global — служебное пространство, которое также размещается внутри основного ката-
лога кластера. Здесь размещаются общие для всего кластера объекты системного каталога.
Получить список всех табличных пространств можно с помощью метакоманды \db+ или из таб-
лицы pg_tablespace:
# \db+
List of tablespaces
Name | Owner | Location | Access privileges | Options | Size | Description
------------+----------+----------+-------------------+---------+--------+-------------
pg_default | postgres | | | | 372 MB |
pg_global | postgres | | | | 556 kB |
В выводе команды интерес представляет поле Location, которое содержит путь до каталога
табличного пространства. Для пространств по умолчанию это поле содержит NULL (поскольку
оба находятся в основном каталоге данных). Все остальные подключаемые пространства (как
правило) находятся вне основного каталога данных. Они связаны с основным каталогом по-
средством символических ссылок, определенных в подкаталоге pg_tblspc.
Табличные пространства могут использоваться для размещения как баз данных, так и отдель-
ных таблиц и индексов. Отдельно стоит отметить базы данных: размещение в конкретном
табличном пространстве фактически означает, что в нем размещаются объекты системного
каталога этой базы, а остальное содержимое может размещаться и в других табличных про-
странствах. Например, вполне допустимо существование базы в пространстве pg_default, в ко-
торой может находиться большая секционированная таблица; при этом секции и индексы этой
таблицы за последние три месяца могут находиться в пространстве, размещенном в быстром
SSD-хранилище, а более старые, архивные секции и индексы — в медленном HDD-хранилище.
Таким образом, база размещена в одном пространстве, а часть ее данных — в двух других про-
странствах.
1
[Link]/docs/postgresql/current/sql-createdatabase
4.1. Иерархия объектов СУБД 103
Базы данных
Базы данных являются своего рода контейнерами для данных и обеспечивают их изоляцию.
Таблицы и индексы одной базы недоступны в других базах (для обхода этого ограничения мож-
но использовать такие средства, как postgres_fdw и dblink). Отдельные базы используются для
логического разделения данных внутри одного кластера — данные нескольких не взаимосвя-
занных между собой приложений можно разделить и хранить в отдельных базах, но в рамках
одного кластера баз данных.
# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | ICU Locale | Locale Provider | Access privileges
-----------+----------+----------+------------+------------+------------+-----------------+-----------------------
pgbench | pgbench | UTF8 | en_US.utf8 | en_US.utf8 | | libc |
postgres | postgres | UTF8 | en_US.utf8 | en_US.utf8 | | libc |
template0 | postgres | UTF8 | en_US.utf8 | en_US.utf8 | | libc | =c/postgres +
| | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | en_US.utf8 | en_US.utf8 | | libc | =c/postgres +
| | | | | | | postgres=CTc/postgres
При инициализации кластера создаются три базы данных. Две базы — template0 и template1 —
используются самой СУБД в качестве шаблонов при создании новых баз, а третья база —
postgres — является обычной базой, пригодной для создания в ней таблиц. Как правило, БД
postgres не используется для прикладных бизнес-задач, администраторы обычно устанавли-
вают в нее вспомогательные скрипты и инструменты. Для размещения пользовательских дан-
ных рекомендуется создавать отдельные базы и давать им имена, которые ясно указывали бы
на назначение БД или на суть хранимых там данных.
Все базы данных внутри основного каталога размещены в подкаталоге base. Каждая база раз-
мещена в каталоге, имя которого соответствует идентификатору в pg_database.oid.
Схемы
Внутри отдельной базы можно дополнительно разделить данные по схемам (schema). Схемы,
по сути, являются пространствами имен (namespace) и позволяют задействовать дополнитель-
ный уровень изоляции и хранения объектов, но не предусматривают вложенности. Например,
с помощью схем можно организовать хранение таблиц согласно разным предметным облас-
тям, версионирование функций и представлений. Массу интересных вариантов использова-
ния схем можно найти в презентации Б. Момджяна, посвященной микросервисам1 .
Любая база создается со схемой public, которая является схемой по умолчанию для всех созда-
ваемых таблиц и индексов. Часто на практике достаточно одной этой схемы.
1
[Link]/main/writings/pgsql/[Link]
104 Глава 4. Базы данных
# \dn+
List of schemas
Name | Owner | Access privileges | Description
--------+-------------------+----------------------------------------+------------------------
public | pg_database_owner | pg_database_owner=UC/pg_database_owner+| standard public schema
| | =U/pg_database_owner |
Схемы являются внутренней абстракцией СУБД для логической группировки объектов. Они
не имеют ассоциированных с ними файлов или каталогов в файловой системе.
Таблицы и индексы
Таблицы являются основными, можно даже сказать, главными объектами для размещения
пользовательских данных, представленных в виде строк. Индексы являются дополнительной
к таблице структурой и позволяют ускорить поиск отдельных строк таблицы.
Получить список таблиц можно с помощью метакоманды \dt+ или представления pg_tables:
# \dt+
List of relations
Schema | Name | Type | Owner | Persistence | Access method | Size | Description
--------+------------------+-------+---------+-------------+---------------+---------+-------------
public | pgbench_accounts | table | pgbench | permanent | heap | 256 MB |
public | pgbench_branches | table | pgbench | permanent | heap | 40 kB |
public | pgbench_history | table | pgbench | permanent | heap | 0 bytes |
public | pgbench_tellers | table | pgbench | permanent | heap | 48 kB |
В выводе представлены только пользовательские таблицы. Для просмотра всех таблиц, вклю-
чая системные, следует указать модификатор S: команда примет вид \dtS+. Для индексов есть
аналогичная метакоманда \di и представление pg_indexes (на основе таблицы pg_index):
# \di+
List of relations
Schema | Name | Type | Owner | Table | Persistence | Access method | Size | Description
--------+-----------------------+-------+---------+------------------+-------------+---------------+-------+-------------
public | pgbench_accounts_pkey | index | pgbench | pgbench_accounts | permanent | btree | 43 MB |
public | pgbench_branches_pkey | index | pgbench | pgbench_branches | permanent | btree | 16 kB |
public | pgbench_tellers_pkey | index | pgbench | pgbench_tellers | permanent | btree | 16 kB |
Отдельно можно отметить таблицу pg_class, которая содержит список всех объектов в текущей
базе и часто используется как вспомогательная при получении статистики по объектам базы.
Далее в некоторых запросах мы будем обращаться к этой таблице.
4.1. Иерархия объектов СУБД 105
Отношения и кортежи
Отношение (relation) — это обобщающий термин для любых объектов в базе данных, кото-
рые имеют имя и список атрибутов. Таблицы и индексы, внешние таблицы, представления
(обычные и материализованные), последовательности (sequence), составные типы — все это
можно назвать отношениями. Также в PostgreSQL существует термин класс — это устарев-
ший синоним отношения, который остался со времен увлечения объектной ориентирован-
ностью. В самых ранних версиях PostgreSQL существовала таблица pg_relation, которую
быстро переименовали в pg_class, а вот префикс rel у атрибутов так и остался.
В более общем виде отношение — это множество кортежей (tuple); например, результат
запроса также является отношением. Кортеж представляет собой упорядоченный набор ат-
рибутов. Порядок атрибутов задается при определении таблицы (или другого отношения),
которая будет содержать кортежи. В этом случае кортеж часто называют строкой табли-
цы. Он также может определяться структурой результирующего множества; такие кортежи
иногда называют записями. Часто этот термин принимает смысл «версия строки», так как
относится скорее к физическому представлению данных внутри страницы, чем к логичес-
кому понятию строки.
Каждая таблица или индекс представлены несколькими файлами, так называемыми слоями
(fork):
• Основной файл таблицы (main fork) содержит пользовательские данные в виде строк.
• Карта видимости (visibility map) предназначена для отслеживания страниц, строки в кото-
рых видны всем активным транзакциям. Карты видимости существуют только для таблиц.
Файлы карт видимости имеют суффикс _vm. Карты видимости обновляются при очистке
и обычно занимают немного места. Более подробное описание можно найти в докумен-
тации1 .
• Карта свободного пространства (free space map) предназначена для отслеживания свобод-
ного места в таблице (или индексе) и тех страниц, что доступны для вставки новых строк.
Так же как и карты видимости, карты свободного пространства обновляются при очистке
и занимают немного места. Файлы этих карт имеют суффикс _fsm. Более подробное опи-
сание можно найти в документации2 .
• Файл инициализации (initialization fork) используется только для нежурналируемых таб-
лиц (unlogged table) и индексов, изменения в которых не фиксируются в WAL-журнале.
Такие таблицы не восстанавливаются после сбоя и пересоздаются путем копирования
1
[Link]/docs/postgresql/current/storage-vm
2
[Link]/docs/postgresql/current/storage-fsm
106 Глава 4. Базы данных
файла инициализации поверх основного файла (если таблица была большой и содержала
несколько сегментов, они удаляются). В документации не так много информации по файлу
инициализации1 .
На более низком уровне таблицы и индексы имеют страничную организацию и состоят из стра-
ниц (блоков) по 8 КБ. Каждая такая страница имеет служебные поля и место, зарезервирован-
ное под пользовательские данные, — строки, или кортежи. Индексы также имеют страничную
организацию, но в зависимости от типа индекса (btree, hash, gin и пр.) могут по-разному струк-
турировать место внутри страниц.
TOAST
Одним из ограничений страничной организации является то, что строки не могут выходить за
пределы страниц, то есть в странице нельзя хранить строки длиной, превышающей ее размер.
Для преодоления этого ограничения используется методика хранения сверхбольших атрибу-
тов TOAST (The Oversized-Attribute Storage Technique), которая заключается в сжатии и/или
разбиении длинных строк. Это происходит автоматически и прозрачно для конечного пользо-
вателя, хотя отмечу, что возможности для тонкой настройки TOAST имеются. Методика TOAST
поддерживается только для тех типов данных, которые могут быть представлены атрибутами
переменной длины (varlena, variable-length attribute), так как она не имеет смысла для типов
фиксированного размера.
Если таблица имеет столбцы с типами переменной длины, с таблицей также будет связана
дополнительная TOAST-таблица, в которой и будут размещаться данные, не вмещающиеся
в обычные страницы. Как и обычные таблицы, TOAST-таблицы также используют странич-
ную организацию с фиксированным размером страницы, но за счет дополнительного сжа-
тия и разбиения значений на фрагменты методика позволяет распределять большие значения
по нескольким страницам. Более подробное описание методики и то, как устроено размеще-
ние данных на диске и в памяти, можно прочитать в официальной документации2 .
1
[Link]/docs/postgresql/current/storage-init
2
[Link]/docs/postgresql/current/storage-toast
4.2. События в кластере баз данных 107
Еще одной статистикой, которая также характеризует рабочую нагрузку, является статистика
по использованию таблиц и индексов. Эта статистика доступна в двух вариантах: на уровне
строк и на уровне блоков. Первый вариант представляет собой информацию о том, сколько
строк затронуто в результате выполнения запросов. Статистика по строкам является логи-
ческим отражением рабочей нагрузки, поскольку указывает на использование логических аб-
стракций СУБД; статистика по блокам, выраженная в страницах или байтах, является более
близкой к физическому слою хранения данных и хорошо подходит для анализа ввода-вывода.
Для получения нужных данных потребуется несколько представлений. Получить общую кар-
тину в рамках кластера или отдельных БД можно в представлении pg_stat_database, в котором
содержится набор необходимых полей:
Эта статистика также подходит для поверхностной оценки колебаний рабочей нагрузки.
Из мониторинга ее можно получить с помощью следующих метрик:
108 Глава 4. Базы данных
• postgres_database_tuples_returned_total;
• postgres_database_tuples_fetched_total;
• postgres_database_tuples_inserted_total;
• postgres_database_tuples_updated_total;
• postgres_database_tuples_deleted_total.
Каждая метрика возвращает соответствующее значение затронутых строк для отдельной БД.
Для наблюдения в динамике следует ориентироваться на частоту изменения величины, и для
оценки общей картины метрики отдельных баз следует просуммировать.
Для графика (рис. 4.1) потребуется два запроса: каждая из метрик описывает отдельное дей-
ствие, связанное со строкой.
# sum(rate(postgres_database_tuples_fetched_total{service_id="primary"}[1m]))
# sum(rate(postgres_database_tuples_returned_total{service_id="primary"}[1m]))
Аналогичную картину можно получить для строк, затронутых при операциях изменения. Для
построения графика (рис. 4.2) потребуются три запроса:
# sum(rate(postgres_database_tuples_inserted_total{service_id="primary"}[1m]))
# sum(rate(postgres_database_tuples_updated_total{service_id="primary"}[1m]))
# sum(rate(postgres_database_tuples_deleted_total{service_id="primary"}[1m]))
Оба графика показывают ровную рабочую нагрузку без сильных колебаний. Это говорит о том,
что со стороны приложений количество клиентов и объем запросов более или менее посто-
янны, и со стороны СУБД количество затрагиваемых строк при выполнении рабочей нагрузки
также постоянно. Причиной сильных колебаний может быть как изменение количества экзем-
пляров приложений и объема отправляемых запросов, так и внутренние причины, такие как
4.2. События в кластере баз данных 109
Для более детального анализа и понимания того, какие именно таблицы затронуты в нагруз-
ке, следует использовать представления pg_stat_user_tables и pg_stat_user_indexes. В этих
представлениях содержится больше информации об использовании именно таблиц и индек-
сов. В pg_stat_user_tables интерес представляют следующие поля:
1
[Link]/en/hot-updates-in-postgresql-for-better-performance
2
[Link]/pg/[Link]
4.2. События в кластере баз данных 111
Для получения списка таблиц, которые читаются последовательно, можно использовать сле-
дующий запрос (подключение должно быть выполнено к БД pgbench):
Получение этой и подобной статистики с помощью запросов удобно для построения отчетов
за периоды, однако для оперативного анализа и поиска проблем важно видеть динамику из-
менений. Следующим запросом можно получить первые K таблиц по количеству строк, про-
читанных при последовательном доступе:
# topk_avg(5,
rate(postgres_table_seq_tup_read_total{service_id="primary"}[1m]),
"other"
)
Если на основе этого запроса построить график, на нем будет показана всего одна таблица, что
полностью соответствует результату SQL-запроса.
В качестве небольшого эксперимента можно выполнить разовый запрос с чтением всех строк
другой таблицы:
# SELECT
relname,
seq_tup_read AS seq_read,
idx_tup_fetch AS idx_fetch,
n_tup_ins AS inserted,
n_tup_upd AS updated,
n_tup_del AS deleted
FROM pg_stat_user_tables
ORDER by relname;
relname | seq_read | idx_fetch | inserted | updated | deleted
------------------+-----------+-----------+----------+---------+---------
pgbench_accounts | 2000000 | 11506518 | 2000000 | 5753260 | 0
pgbench_branches | 115105760 | 0 | 20 | 5753260 | 0
pgbench_history | 486693 | | 5753260 | 0 | 0
pgbench_tellers | 40336000 | 5551581 | 200 | 5753260 | 0
• postgres_table_seq_tup_read_total;
• postgres_table_idx_tup_fetch_total;
• postgres_table_tuples_inserted_total;
• postgres_table_tuples_updated_total;
• postgres_table_tuples_deleted_total;
• postgres_table_tuples_hot_updated_total.
# topk_avg(5,
rate(postgres_table_tuples_updated_total{service_id="primary"}[1m]),
"other"
)
График (рис. 4.4) более наглядно выводит информацию о том, на какие именно таблицы при-
ходится больше всего обновлений.
114 Глава 4. Базы данных
Меняя метрику в запросе, можно получить соответствующие графики для других показателей,
например для вставленных строк (рис. 4.5). Используя такие графики на практике, можно опе-
ративно отслеживать колебания в рабочей нагрузке, особенно в случае каких-то значительных
изменений на стороне приложений.
расценивать как возникновение ошибки (за исключением случаев явного вызова коман-
ды ROLLBACK). Следовательно, большое число откатов может сигнализировать об ошибках
при выполнении как отдельных запросов, так и транзакций, и xact_rollback можно ис-
пользовать как счетчик таких ошибок. Типичными примерами могут служить наруше-
ние уникальности при вставке строк, ошибки синтаксиса, неверное указание аргументов
функции или обращение к несуществующим объектам. Однако кроме таких ошибок воз-
можны и другие, которые могут возникать по причинам, не зависящим от приложения,
но эту информацию можно получить только из журнала СУБД.
• conflicts — количество запросов, отмененных из-за конфликтов между выполнением за-
проса и воспроизведением WAL-журнала на узлах горячего резерва (при использовании
репликации). Это отдельный класс ошибок, который более подробно будет рассмотрен
в главе 7, посвященной репликации. В любом случае возникновение таких конфликтов
также можно расценивать как ошибку.
• deadlocks — взаимоблокировки, их мы также рассматривали ранее в главе 2. Для разреше-
ния взаимоблокировки какая-то из транзакций завершается принудительно, и это также
можно расценивать как ошибку.
• checksum_failures и checksum_last_failure показывают количество ошибок при провер-
ке контрольных сумм (data page checksums) и время последней зафиксированной ошиб-
ки. Контрольные суммы рассчитываются на основе содержимого страницы данных, пере-
считываются при последующих изменениях и используются для выявления повреждения
данных при обращениях к странице. Это один из наиболее неприятных типов ошибок; их
появление (особенно в производственном окружении) следует расценивать очень серьез-
но и предпринимать все необходимые меры по исключению таких ошибок в будущем.
• sessions_abandoned, sessions_fatal, sessions_killed указывают на количество сеансов
с ненормальным завершением. Эти поля мы также обсуждали в главе 2. Они указывают
на то, что сеанс завершился ненормально, и это также можно рассматривать как ошибку.
Как вы могли заметить, все рассмотренные события по сути являются ошибками, и при нор-
мальной и правильной эксплуатации СУБД и приложений их вообще не должно возникать.
Хорошей практикой является использование графика, где все метрики по этим ошибкам со-
браны вместе:
• postgres_database_xact_rollbacks_total;
• postgres_database_conflicts_total;
• postgres_database_deadlocks_total;
• postgres_database_checksum_failures_total;
• postgres_database_sessions_total{reason=~"abandoned|fatal|killed"}.
В самом идеальном случае такой график должен быть «пустым», как на рис. 4.6. Любые выбро-
сы должны привлекать внимание администратора БД и служить поводом разобраться с при-
чинами.
116 Глава 4. Базы данных
Довольно часто вместе с основной статистикой по объектам БД хочется видеть и размеры этих
объектов, чтобы понимать их масштаб относительно друг друга. Часто размер объекта опреде-
ляет приоритет работ и сам подход к работе — большие объекты требуют большей аккуратнос-
ти (нельзя допускать долгого удерживания блокировок, следует избегать побочной нагрузки),
и конечная выгода в результате проведенных работ может быть больше. Размеры объектов
важны и сами по себе, они являются одним из ключевых факторов при планировании емкости
дискового пространства. При продолжительной эксплуатации СУБД данные будут добавлять-
ся, объекты будут увеличиваться и занимать все больше и больше места на диске.
4.3. Функции для работы с объектами СУБД 117
Отслеживать размеры объектов можно как со стороны операционной системы, так и со сторо-
ны СУБД. Как известно, почти все объекты СУБД — это файлы и каталоги, и можно получить
информацию об объеме этих объектов в файловой системе. С другой стороны, СУБД опериру-
ет собственными абстракциями — табличными пространствами, базами данных, таблицами,
индексами и т. д. В обоих случаях для получения размеров нам понадобятся так называемые
функции системного администрирования1 . Это большой набор вспомогательных функций для
получения самой разной информации, которая может потребоваться администратору БД. По-
ка из всего набора функций нас интересуют две группы:
1. Функции управления объектами баз данных2 . Большинство функций в этой группе ис-
пользуются для подсчета места, занимаемого различными объектами СУБД.
Если не указывать второй аргумент, значение main предполагается по умолчанию. Для ин-
декса размер карты видимости всегда будет нулевым.
• pg_size_pretty — вспомогательная функция, переводящая значение в байтах в текстовый
вид с единицами измерения (bytes, kB, MB, GB, TB, PB).
• pg_size_bytes — вспомогательная функция, обратная pg_size_pretty: она позволяет пере-
считать текстовое значение в байты.
Некоторые из этих функций существуют в двух вариантах и в качестве объекта могут прини-
мать либо его числовой идентификатор OID, либо текстовое имя.
В запросах, выводящих статистику по объектам БД, эти функции удобно использовать для ука-
зания размеров объектов. Размеры таких составных объектов, как табличные пространства
или базы данных, необходимы для оценки используемого места в хранилище и для планиро-
вания емкости. Размеры прочих объектов, таких как таблицы и индексы, обычно нужны при
оценке использования места внутри базы данных или для оценки эффекта раздувания. Дальше
мы коротко рассмотрим примеры использования функций.
Запросы метакоманд
Чтобы получить текст запроса, который используется метакомандой, можно запустить psql
c аргументом -E (--echo-hidden), который указывает psql включить отображение запро-
сов при использовании встроенных метакоманд. В открытом сеансе отображение запросов
можно включить с помощью команды \set ECHO_HIDDEN on.
Базы данных. Для получения размеров баз данных используется функция pg_database_size.
В качестве аргумента функция может принимать числовой идентификатор OID или имя базы
данных. Пример запроса для получения размеров:
• Вместо pg_stat_user_tables используется pg_class, так как в этой таблице содержится пол-
ный список объектов БД.
• Для получения нужных объектов используется фильтр [Link] IN ('p', 'r', 'm'), кото-
рый оставляет только обычные (r) и секционированные (p) таблицы, а также материали-
зованные представления (m), таким образом исключая индексы, TOAST-таблицы, обычные
представления, последовательности и прочие объекты.
• Исключение таблиц, заблокированных в исключительном режиме AccessExclusiveLock.
Важным нюансом использования административных функций является уровень уста-
навливаемых блокировок. Для подсчета размеров объекта функциям требуется блоки-
ровка уровня ExclusiveLock1 . При регулярном использовании таких функций, напри-
мер в случае мониторинга, появляется риск конфликта с самой строгой исключительной
(AccessExclusiveLock) блокировкой и вероятность встать в очередь. Для устранения такого
риска заблокированные таблицы не выводятся; их размер можно будет получить в другой
раз, когда блокировка будет снята.
• Выбираются только таблицы, размер которых превышает 500 КБ. Подобным условием
удобно отсекать небольшие таблицы. Впрочем, в тестовом окружении вообще немного
таблиц, а больших — и того меньше.
• Отдельным подзапросом к pg_index подсчитывается общее количество индексов для каж-
дой таблицы.
• Для полноты картины добавлен подсчет размера init-слоев, хотя это актуально только при
использовании нежурналируемых таблиц, которых, впрочем, нет в тестовом окружении.
Логическим улучшением такого запроса может быть представление служебных слоев в ви-
де процентных отношений относительно общего размера таблицы: так будет удобнее видеть
аномалии и перекосы (хотя я не припомню, чтобы мне встречались служебные слои больших
размеров). Также можно вывести табличные пространства, которым принадлежат таблицы,
однако для этого потребуется выполнить соединение с pg_tablespace.
1
[Link]/docs/postgresql/current/explicit-locking
4.3. Функции для работы с объектами СУБД 121
Для удобства работы с индексами можно использовать представление pg_indexes, которое ос-
новано на pg_class и pg_index, однако в нем нет поля с уникальным идентификатором, что
делает невозможным соединение с другими представлениями по OID. Для простоты запрос
выполняется только к pg_indexes без соединения с pg_locks, однако риск конфликта с исклю-
чительной блокировкой остается, как и в случае с таблицами.
• postgres_database_size_bytes;
• postgres_table_size_bytes;
• postgres_index_size_bytes.
На основе этих метрик можно сделать два типа графиков. Первый и самый простой вариант —
показывать объекты с наибольшим размером. Это простая оценка того, как используется место
самыми большими объектами БД, однако такой график не покажет аномального роста каких-
то других таблиц — это будет заметно, только когда таблица увеличится в размерах настолько,
что попадет в список самых больших. Рост может длиться долгое время, за которое проблема
аномального увеличения могла бы быть уже исправлена. Вторым, дополнительным вариантом
может быть использование графика с объектами, у которых происходит наибольшее измене-
ние размера в интервале времени.
На примере таблиц давайте рассмотрим несколько случаев. Следующим запросом можно по-
лучить самые большие таблицы:
Пример запроса для второго варианта с таблицами, у которых происходит наибольшее изме-
нение размера:
# delta(postgres_table_size_bytes{service_id="primary"}[1h])
В этом запросе используется функция delta, которая считает разницу в значениях за интервал
в один час. Следует аккуратно подходить к выбору интервала: даже один час может оказаться
слишком длинным интервалом и сглаживать резкие изменения в размерах за короткие пери-
оды. График, построенный по этой функции, показан на рис. 4.8:
В этом примере видно, что таблица очищается и затем стабильно растет примерно на 5 МБ
в час. Для большей наглядности на график можно наложить размер самой таблицы (рис. 4.9):
Помимо объектов, занятых в хранении пользовательских данных, есть и другие объекты СУБД,
которые также хранятся на диске и при некоторых обстоятельствах могут занимать значитель-
ный объем. Для их просмотра и анализа также есть набор функций:
# SELECT pg_ls_dir('/');
pg_ls_dir
----------------------------
lib
opt
bin
run
etc
var
tmp
srv
mnt
media
usr
dev
sbin
proc
root
home
sys
docker-entrypoint-initdb.d
.dockerenv
Следующая не менее интересная функция — это pg_read_file. Функция читает указанный тек-
стовый файл и возвращает его содержимое. В примере ниже мы просто выводим результат,
но его, конечно, можно передать другой функции для дальнейшей обработки. В качестве до-
полнительных параметров можно задавать смещение и длину для более точного указания диа-
пазона чтения.
# SELECT pg_read_file('/etc/os-release');
pg_read_file
------------------------------------------------------------------------
NAME="Alpine Linux" +
ID=alpine +
VERSION_ID=3.16.0 +
PRETTY_NAME="Alpine Linux v3.16" +
HOME_URL="[Link] +
BUG_REPORT_URL="[Link]
• размер в байтах;
• время последнего обращения (atime, access time);
• время последнего изменения (mtime, modification time);
• время последнего изменения состояния (ctime, change time), только в Unix-системах;
• время создания (creation time), только в Windows;
• флаг, указывающий, что объект является каталогом.
Суммируем все сказанное выше: имея доступ к перечисленным функциям, можно прочитать
содержимое любого каталога и файла в пределах прав пользователя, от имени которого запу-
щена СУБД. По умолчанию доступ к функциям ограничен; при необходимости выдачи прав
на эти функции риски безопасности следует серьезно взвесить и учесть.
пишет журнал сообщений, — задача не совсем тривиальная, и ответ к тому же зависит от вер-
сии системы.
Функция выводит имя, размер и время последнего изменения (mtime) всех файлов (за ис-
ключением скрытых и специальных) в каталоге журналов сообщений СУБД. С точки зрения
мониторинга можно получить полный объем, занимаемый журналами на диске:
Функция может вывести ошибку, если подсистема сбора журналов выключена (logging_collector
= off) и каталога, указанного в log_directory, не существует. С некоторыми настройками журна-
лы сообщений могут занимать много места, и, более того, они могут располагаться в каталоге
кластера баз данных. Если журналы заполнят все свободное место в файловой системе, они
станут причиной аварийной остановки СУБД.
Прочие функции для вывода содержимого каталогов. Следующие функции относятся к выводу
содержимого каталогов, задействованных в репликации и логическом декодировании (кото-
рое используется в логической репликации). Все следующие функции выводят имя, размер
и время последнего изменения всех обычных файлов в целевых каталогах:
Резюме
Область общей памяти используется для оперативного размещения данных таблиц, индексов
и большого количества различных служебных структур. Общей она называется потому, что
все процессы СУБД имеют к ней равный доступ и изменения, внесенные одним процессом,
становятся доступными всем остальным. Как правило, самой большой частью общей памяти
является буферный, или, как его еще называют, общий кеш, который используется для разме-
щения пользовательских данных.
С буферным кешем почти никогда не возникает проблем ровно до тех пор, пока он способен
вместить себя все данные из основного хранилища (основной каталог данных и табличные
пространства). Однако, когда объем хранимых данных превышает объем общего кеша, появ-
ляется задача эффективного использования отведенного объема: как держать в нем нужные
данные и замещать ненужные в случае нехватки места. Могут возникнуть вопросы: какие си-
стемные структуры находятся в общей памяти и сколько они занимают места? Какие объекты
(таблицы, индексы) находятся в буферном кеше? Каковы размеры и доля этих объектов от-
носительно друг друга? Насколько эффективно используется буферный кеш? Какой процент
обращений заканчивается успехом? Как часто данные не удается найти и приходится обра-
щаться к основному хранилищу? Для ответа на эти вопросы СУБД предлагает несколько ин-
струментов, которые можно использовать как для разового анализа, так и для регулярного
мониторинга:
Представление pg_buffercache
1
[Link]/docs/postgresql/current/pgbuffercache
2
[Link]/docs/postgresql/current/view-pg-shmem-allocations
5.1. Анализ общей памяти 131
Для использования расширения достаточно установить его с помощью команды CREATE EX-
TENSION. После установки появятся соответствующие функция и представление. В тестовом
окружении расширение уже установлено и готово к использованию.
Каждая строка описывает отдельный буфер; соответственно, чем больше размер буферного
кеша, тем больше будет строк в представлении. На основе практического опыта отмечу, что
не помню случаев, когда бы могла понадобиться информация по отдельно взятому буферу:
обычно нужна статистика, сгруппированная по объектам, базам и таблицам. При составлении
запросов к pg_buffercache важно учитывать, что расширение устанавливается в конкретную
БД, но статистика описывает все буферы, в том числе и те, что ассоциированы с объектами
в других БД. Для преобразования числовых идентификаторов в имена таблиц и других отноше-
ний можно выполнить соединение с pg_class, но такое преобразование будет работать только
для объектов из данной БД. В таких случаях рекомендуется добавить условие, чтобы выводить
буферы только для текущей БД, а не всех вообще.
• isdirty — флаг, указывающий на то, что буфер является грязным и содержимое находяще-
гося в нем блока отличается от содержимого блока в основном хранилище;
• usagecount — счетчик обращений к буферу, который используется алгоритмом Clock Sweep
для вытеснения неиспользуемых страниц из общего кеша1 ;
• pinning_backends — текущее количество закреплений буфера клиентскими процессами.
Закрепление буферов
Пока процесс читает страницу, буфер необходимо блокировать от попыток изменения дру-
гими процессами, но долгая блокировка буфера негативно скажется на конкурентной рабо-
те. Благодаря правилам видимости строк блокировка буфера нужна только на время чтения
оглавления страницы; однако по-прежнему важно, чтобы страница не была вытеснена из
буфера и содержимое страницы не изменилось кардинально. Чтобы ограничить спектр воз-
можных действий с буфером, не блокируя его, используется так называемое закрепление
(pin) буфера, которое позволяет другим процессам читать и изменять данные, но не позво-
ляет вытеснять страницу из буфера или очищать ее.
Также можно принять во внимание значение usagecount, которое показывает текущую интен-
сивность обращений к буферу. Этот счетчик увеличивается на единицу при каждом доступе
(максимальное значение — 5) и уменьшается на единицу при каждом проходе алгоритма вы-
теснения. Блок вытесняется и заменяется другим блоком, если к моменту уменьшения счет-
чика его значение уже равно нулю. Таким образом, по значению usagecount (от 0 до 5) можно
определить, насколько активно используются буферы.
Состояние занятого буфера можно детализировать. Так, например, к буферу могут обращаться
клиентские процессы, читая или изменяя данные блока, на что указывает ненулевое значение
pinning_backends. Если содержимое буфера было изменено, он становится грязным, и эти из-
менения должны быть перенесены в ассоциированный с буфером блок в основном хранилище.
На грязное состояние указывает флаг isdirty.
1
[Link]/pg/[Link]#_8.4.4.
5.1. Анализ общей памяти 133
Таким образом, использование буферного кеша можно рассматривать как минимум в двух
проекциях:
Влияние на производительность
Давайте вернемся чуть назад, к SQL-запросу к pg_buffercache. Практически весь его вывод
представлен числовыми идентификаторами; для получения более понятной информации по-
требуется соединить результат с дополнительными источниками информации и проделать
некоторые преобразования.
# SELECT
[Link],
[Link],
count(*) AS buffers,
pg_size_pretty(count(*) * 8192) AS bytes
FROM pg_buffercache b
JOIN pg_class c
ON [Link] = pg_relation_filenode([Link]) AND
[Link] IN (
0,
(SELECT oid FROM pg_database WHERE datname = current_database())
)
JOIN pg_namespace n ON [Link] = [Link]
GROUP BY [Link], [Link]
ORDER BY 3 DESC
LIMIT 10;
134 Глава 5. Область общей памяти и ввод-вывод
Полученный результат стал более понятен: вместо числовых идентификаторов объектов те-
перь выводятся имена, статистика сгруппирована по объектам, а блоки просуммированы
и дополнительно выражены в байтах. Теперь видно, что бóльшую часть в буферном кеше
(в тестовом окружении значение shared_buffers установлено в 128 МБ) занимают таблица
pgbench_accounts и ее индекс.
Преобразование в байты
В тестовом окружении мне заранее известен размер блока, и в примерах запросов использу-
ется константа 8192. Однако на практике размер блока может быть переопределен на этапе
компиляции из исходных кодов, и в таких случаях преобразование будет неверным. Вместо
константы можно использовать более универсальное решение и получать актуальный раз-
мер блока из конфигурации СУБД вызовом функции current_setting('block_size'). Такой
способ предпочтителен, когда нужно выразить размер блоков в байтах, но при этом точный
размер блока неизвестен.
# SELECT
pg_size_pretty(count(*) FILTER (
WHERE reldatabase IS NULL) * 8192) AS free,
pg_size_pretty(count(*) FILTER (
WHERE pinning_backends = 0 AND isdirty = 'f') * 8192) AS clean,
pg_size_pretty(count(*) FILTER (
WHERE pinning_backends > 0 AND isdirty = 'f') * 8192) AS "clean/pinned",
pg_size_pretty(count(*) FILTER (
WHERE pinning_backends = 0 AND isdirty = 't') * 8192) AS dirty,
pg_size_pretty(count(*) FILTER (
WHERE pinning_backends > 0 AND isdirty = 't') * 8192) AS "dirty/pinned"
FROM pg_buffercache;
5.1. Анализ общей памяти 135
В выводе запроса видно, что бóльшая часть буферов является грязными. Это говорит о том,
что вносится довольно много изменений, и это действительно так: в главе, посвященной за-
просам, мы выяснили, что в тестовой рабочей нагрузке преобладают запросы на обновление
записей в pgbench_accounts. В тестовом окружении агент мониторинга использует подобный
запрос для получения метрики postgres_shared_buffers_all_usage_bytes, на основе которой
можно построить следующий график (рис. 5.1):
Представление pg_shmem_allocations
# SELECT *
FROM pg_shmem_allocations
ORDER BY size DESC LIMIT 10;
name | off | size | allocated_size
----------------------+-----------+-----------+----------------
Buffer Blocks | 6843520 | 134217728 | 134217728
<anonymous> | | 6710784 | 6710784
XLOG Ctl | 54144 | 4208200 | 4208256
| 149586304 | 1900160 | 1900160
Buffer Descriptors | 5794944 | 1048576 | 1048576
CommitTs | 4792192 | 533920 | 534016
Xact | 4263040 | 529152 | 529152
Checkpointer Data | 146862208 | 393280 | 393344
Checkpoint BufferIds | 141323392 | 327680 | 327680
Subtrans | 5326336 | 267008 | 267008
Каждая строка описывает отдельный участок памяти, выделенный под конкретную служебную
структуру. Самая большая структура в списке — область Buffer Blocks. Именно в ней разме-
щается буферный кеш с данными пользовательских объектов, который можно детально ис-
следовать с помощью pg_buffercache. Если посмотреть на полный вывод представления, то
можно заметить, что в общей памяти размещается большое количество структур. Например,
для версии 15 их около шестидесяти, а итоговое число может варьироваться в зависимости
от конфигурации, используемых модулей и т. п. В самом представлении не так много полей:
• name — имя сегмента, по которому можно примерно понять его назначение. Сегмент, у ко-
торого отсутствует имя (NULL), представляет неиспользуемую память; сегменты с именем
<anonymous> являются анонимными и могут использоваться для хранения информации
о блокировках;
• off — смещение в области общей памяти, с которого начинается сегмент. У анонимных
сегментов значение смещения отсутствует (NULL);
• size — размер сегмента в байтах;
• allocated_size — размер сегмента с учетом выравнивания (padding). Для свободной памя-
ти и анонимных сегментов значения size и allocated_size всегда равны.
Еще два инструмента, которые также могут пригодиться для отладки и поиска проблем, — это
представление pg_backend_memory_contexts и функция pg_log_backend_memory_contexts. Оба
5.3. Оценка использования SLRU-кешей 137
Применение этих инструментов может пригодиться скорее для отладки, поиска и устране-
ния проблем, чем для регулярного мониторинга. Однако на данный момент (версия 15) это
единственные инструменты, позволяющие детально проанализировать состав и использова-
ние памяти процессов СУБД.
Осталось рассмотреть еще одну группу кешей, SLRU-кеши (Simple Least-Recently-Used). Это
отдельные кеши, призванные ускорять доступ к некоторым служебным структурам, которые
размещаются на диске в основном каталоге данных. Примерами таких служебных структур
являются:
Статистика использования SLRU-кешей пока может применяться только для мониторинга, по-
скольку конфигурация СУБД не предусматривает параметров, позволяющих регулировать их
размеры (однако не исключено, что такие параметры могут появиться в будущем). Основной
сценарий использования статистики — это оценка эффективности кешей.
только о тех объектах, что находятся в кеше в данный момент, и из поля зрения может пропасть
множество других объектов БД, доступ к которым осуществлялся между снимками статистики.
Для более полного учета ввода-вывода нужна статистика накопительного характера по всем
объектам БД. Эта информация располагается в нескольких представлениях, с одним из кото-
рых нам уже приходилось сталкиваться:
Базы данных
Учет времени доступен только при включенном параметре track_io_timing. Для полноты кар-
тины не хватает только статистики по записанным и грязным блокам. На основе этих данных
можно получить общее представление об эффективности использования кеша, однако важ-
но помнить, что статистика содержит данные как по общему, так и по локальным кешам, что
имеет особое значение при широком использовании временных таблиц.
их в кеш, при этом вытесняя из кеша другие данные. При постоянной низкой эффективности
кеша много времени тратится на дисковый ввод-вывод, и это негативно сказывается на общей
производительности. К сожалению, здесь нет простого или универсального решения, и повы-
сить эффективность кеша можно разными способами. Самый очевидный — это увеличение
общего кеша, однако такой способ не всегда приводит к успеху. Более правильным спосо-
бом является выявление тех запросов, которые осуществляют бóльшую часть ввода-вывода
или тратят на это значительное время, и попытка их оптимизации (с помощью рефакторинга
или добавления индексов). За счет такой оптимизации можно в десятки и сотни раз улучшить
производительность запросов и увеличить эффективность использования кеша, не прибегая
к изменению конфигурации.
Такой график хорошо подходит для общей оценки состояния СУБД и отражает моменты, когда
СУБД была вынуждена тратить время на чтение и запись, в результате чего могли возникать
просадки производительности при выполнении запросов.
Отдельного внимания заслуживает статистика попаданий в кеш (поля с суффиксом _hit). Ко-
гда блок найден в буферном кеше, ввода-вывода не происходит: блок уже находится в памяти,
принадлежащей СУБД, так что достаточно прочитать в буфере нужную строку или даже ее
часть. Важно понимать что статистика попаданий в кеш показывает объем найденных в ке-
ше данных, но при этом реальный объем потребовавшихся данных может быть меньшим. При
организации мониторинга нужно стараться избегать попыток прямого сравнения попаданий
в кеш с объемом чтения и особенно представления обоих значений в байтах. Для оценки эф-
фективности кеша лучше оценивать отношение попаданий в кеш к общему количеству обра-
щений к данным (попаданиям в кеш и чтениям из хранилища). То же самое справедливо и в от-
ношении статистики загрязнения буферов и записи страниц (поля blks_dirtied и blks_written
в представлении pg_stat_statements): загрязнение блока при вставке, обновлении или удале-
нии отдельных строк является лишь частичным изменением буфера и не приводит к вводу-
выводу в отличие от записи блока непосредственно в основное хранилище. Этот нюанс важно
учитывать при сведении метрик в один график или вычислении объемов ввода-вывода при со-
ставлении отчетов производительности и при выборе общих единиц измерения (блоков или
байтов).
Другое важное замечание относится к индивидуальности этой статистики — базы данных яв-
ляются своего рода изолированными контейнерами для таблиц, индексов и прочих объектов.
Следовательно, представления содержат статистику только по тем объектам, которые принад-
лежат этой базе данных. Для сбора статистики по всем объектам в кластере баз данных необ-
ходимо подключаться по очереди к каждой отдельной БД.
142 Глава 5. Область общей памяти и ввод-вывод
Следующим запросом можно получить объем чтения, который приходится на таблицы базы
(рис. 5.3 и 5.4):
# sum by (table)
(rate(postgres_table_io_blocks_total{service_id="primary",access="read"}[1m]))
Объем ввода-вывода на первом графике (рис. 5.3) более или менее стабилен, но с точки зре-
ния производственной эксплуатации все-таки могут возникнуть вопросы относительно пери-
одических пиков, связанных с таблицей pgbench_accounts, и увеличения объема чтения табли-
цы pgbench_history, который менее заметен, чем пики, однако проявляется, если выключить
остальные метрики (рис. 5.4).
На графике видно, что большая часть блоков читается из кеша и относительно небольшая часть
читается из основного хранилища (или страничного кеша ОС), что в общем хорошо для произ-
водительности. И, конечно, важно отметить, что в качестве единиц измерения используются
блоки без какого-либо преобразования.
Также возможны и другие сценарии использования в каких-то особых случаях поиска и устра-
нения проблем производительности.
Статистика СУБД предлагает несколько источников для отслеживания временных файлов, ко-
торые могут пригодиться в разных сценариях:
Запрос выполняется несколько секунд и создает временный файл размером около 60 МБ. По-
вторив запрос к pg_stat_database, можно обнаружить, что в БД postgres зафиксировано ис-
пользование временного файла:
Обычно такой график нужен для поверхностного отслеживания временных файлов: как толь-
ко замечены критические уровни использования временных файлов, можно перейти к более
детальному анализу с помощью pg_stat_statements или журналов сообщений.
Отсутствие счетчиков _hits и _dirtied является нормальным, поскольку весь ввод-вывод осу-
ществляется напрямую с файлом без кеширования. Более того, для работы с временным фай-
лом используется техника, отличная от страничной, которая применяется при работе с буфер-
ными кешами (общим или локальными): ввод-вывод в этом случае работает с плотно упако-
ванными строками без заголовков и только с нужными полями. В представлении также есть
поле dbid, группировка по которому позволяет получить статистику по отдельным базам. Это
может навести на мысли о сходстве с pg_stat_database, однако в нем ведется учет количества
и размеров временных файлов, а в pg_stat_statements — объема ввода-вывода. Прямое срав-
нение значений из pg_stat_database и pg_stat_statements довольно бессмысленно.
С точки зрения мониторинга запросов полезны также графики top-K, позволяющие быстро вы-
явить запросы, которые больше остальных используют временные файлы. Однако в тестовом
окружении нет подходящих запросов и такой график не покажет ничего интересного.
Следите за журналом
Начнем с устройства файловой системы. Для размещения временных файлов1 СУБД руковод-
ствуется параметром temp_tablespaces, в котором указывается список табличных пространств:
одно из них выбирается из этого списка случайным образом, и временный файл создается
1
[Link]/docs/postgresql/current/storage-file-layout
5.7. Ввод-вывод фоновых процессов 149
Для эксперимента откроем два сеанса и в первом запустим уже известный запрос, создающий
временные файлы, однако модифицируем его, добавив еще одно соединение для увеличения
объема выполняемой работы:
# SELECT a.*, b.*, c.* FROM pg_class a, pg_class b, pg_class c ORDER BY random();
Запрос, выводящий нужную статистику, занимает несколько десятков строк, поэтому я не буду
его приводить, однако его можно найти в каталоге scripts в тестовом окружении1 . Во втором
сеансе будем периодически запускать SQL-скрипт с запросом, который покажет не только уве-
личение размеров временного файла, но и появление новых сегментов:
# \i /var/lib/postgresql/scripts/active_temp_files.sql
pid | query_age | filename | size | last_modification |
-------+-----------------+---------------------------------+---------+-------------------+--------------------------...
68403 | 00:02:06.101641 | base/pgsql_tmp/pgsql_tmp68403.0 | 1024 MB | 00:01:01.995852 | SELECT a.*, b.*, c.* FROM...
68403 | 00:02:06.101641 | base/pgsql_tmp/pgsql_tmp68403.1 | 1024 MB | 00:00:10.995852 | SELECT a.*, b.*, c.* FROM...
68403 | 00:02:06.101641 | base/pgsql_tmp/pgsql_tmp68403.2 | 455 MB | 00:00:01.004148 | SELECT a.*, b.*, c.* FROM...
Дополнительно можно открыть еще один сеанс и понаблюдать за тем, в какой момент обнов-
ляется статистика в представлениях pg_stat_database и pg_stat_statements.
Среди процессов СУБД помимо клиентских процессов есть еще и фоновые службы, которые
также осуществляют ввод-вывод. В этой главе рассмотрим процесс фоновой записи background
writer и процесс checkpointer, выполняющий контрольные точки. Оба процесса берут на себя
задачу записи изменений из буферного кеша в основное хранилище. Поскольку объем данных
и количество необходимых операций ввода-вывода может быть довольно большим, процессы
записывают данные не в момент внесения изменений, а асинхронно в фоновом режиме.
1
[Link]/lesovsky/postgresql-monitoring-book/blob/main/playground/scripts/active_temp_files.sql
150 Глава 5. Область общей памяти и ввод-вывод
Задача процесса фоновой записи заключается в записи грязных буферов из буферного кеша
в основное хранилище. Процесс выполняет свою работу, чередуя ее с паузами. Перед началом
очередной итерации выполняется оценка количества обращений к буферам, которое произо-
шло за предыдущую итерацию, — чем больше обращений, тем больше буферов будет списано
в этой итерации (см. bgwriter_lru_multiplier), но не более, чем указано в bgwriter_lru_maxpages1 .
Среди грязных буферов выбираются именно те, что будут вытеснены в ближайшем будущем, —
для этого фактически повторяется алгоритм вытеснения, но без уменьшения счетчика исполь-
зования. То есть процесс фоновой записи работает на упреждение и предотвращает попытки
вытеснения грязных страниц со стороны клиентских процессов.
# TABLE pg_stat_bgwriter;
-[ RECORD 1 ]---------+------------------------------
checkpoints_timed | 316
checkpoints_req | 2
checkpoint_write_time | 84919068
checkpoint_sync_time | 58509
buffers_checkpoint | 1254815
buffers_clean | 1801423
maxwritten_clean | 0
buffers_backend | 443469
buffers_backend_fsync | 0
buffers_alloc | 2646195
stats_reset | 2022-11-04 05:01:49.427562+00
1
[Link]/docs/postgresql/current/runtime-config-resource#RUNTIME-CONFIG-RESOURCE-BACKGROUND-WRITER
2
[Link]/docs/postgresql/current/wal-configuration
3
[Link]/pg/[Link]#_9.7.
5.7. Ввод-вывод фоновых процессов 151
Причины наступления контрольных точек. Первые два поля описывают количество выполнен-
ных контрольных точек:
Как видно, контрольные точки могут запускаться либо по расписанию, через интервал вре-
мени, указанный в параметре checkpoint_timeout, либо по необходимости, что означает вне-
плановый запуск при превышении объемом записи ограничения, установленного в параметре
max_wal_size. Также к последней категории относятся контрольные точки, запущенные адми-
нистратором с помощью команды CHECKPOINT, и контрольные точки, выполняемые при выклю-
чении сервера СУБД для синхронизации буферного кеша с основным хранилищем.
При настройке контрольных точек есть две стратегии. Первая стратегия полагается на выпол-
нение контрольных точек по расписанию, когда выбирается относительно длинный интервал
времени checkpoint_timeout и выполнение контрольной точки растягивается на этот интервал.
Эта стратегия применима на оборудовании с низкой производительностью, и ее смысл состоит
в распределение нагрузки от контрольных точек таким образом, чтобы ее выполнение меньше
влияло на общую производительность. При этом появление контрольных точек по необходи-
мости указывает на то, что нагрузка увеличилась и в WAL-журнал стало записываться больше
данных еще до истечения настроенного интервала. У этой стратегии есть недостаток: если уве-
личивать интервал checkpoint_timeout и предел max_wal_size, то из-за больших объемов записи
в WAL-журнал и редких контрольных точек восстановление после потенциальной аварии мо-
жет занять много времени (так как при восстановлении после сбоя все изменения, записанные
в WAL, нужно воспроизвести).
Особенностью записи в WAL-журнал является то, что после выполнения контрольной точки
первое изменение любого буфера в кеше приводит к записи всей страницы (full page write,
FPW) из этого буфера в журнал (см. full_page_writes). Все последующие изменения в этом бу-
фере будут журналироваться отдельно. Таким образом, при частом выполнении контрольных
точек (неважно, по интервалу или по необходимости) будет возникать дополнительная нагруз-
ка от записи полных образов страниц, и объем этой нагрузки зависит от размера буферного
кеша и объема происходящих в нем изменений (операции INSERT, UPDATE, DELETE).
Можно отметить, что на практике большее распространение получила первая стратегия на-
стройки, именно потому, что контрольная точка воспринимается как тяжелая операция и же-
лательно проводить ее аккуратно с наименьшим влиянием на производительность остальных
152 Глава 5. Область общей памяти и ввод-вывод
В мониторинге можно использовать график на основе этих метрик (рис. 5.7) и отслеживать
пики появления «нежелательных» контрольных точек (в зависимости от выбранной стратегии
настройки).
График показывает, что бóльшая часть контрольных точек выполняются по расписанию, зна-
чит, объем записи в WAL-журнал укладывается в предел max_wal_size. Есть и контрольные точ-
ки, выполненные по необходимости: если бы это была производственная среда, это мог бы
быть пик записи в WAL, но в тестовом окружении нагрузка стабильная, и, как следствие, объем
записи в WAL тоже ровный, без резких всплесков. Для демонстрации эти контрольные точки
были запущены с помощью команды CHECKPOINT.
1
[Link]/wiki/Fsync_Errors
5.7. Ввод-вывод фоновых процессов 153
блоков, после чего запись возобновляется. Следующие поля как раз показывают время, затра-
ченное на этих этапах (оба значения — в миллисекундах):
Запись страниц на первом этапе занимает бóльшую часть времени контрольной точки, а син-
хронизация является завершающим этапом и, как правило, выполняется существенно быст-
рее. Однако скорость синхронизации зависит от производительности дисковой подсистемы:
на медленных носителях на этапе синхронизации могут образоваться очереди запросов ввода-
вывода, что приведет к увеличению задержек и снижению производительности. Следователь-
но, медленная синхронизация — признак недостаточной производительности дисковой под-
системы. По значениям этих полей мы можем отслеживать время, требуемое на синхрони-
зацию, и, если синхронизация становится долгой, стоит оценить влияние контрольных точек
на производительность запросов (изменение времени выполнения запросов в момент синхро-
низации на контрольных точках).
На этом графике видно, что время выполнения контрольных точек ровное, но в интервале есть
две контрольные точки, где синхронизация длится около 10 секунд. Это выделяется на фоне
других контрольных точек и является достаточным поводом проявить интерес и разобраться,
почему в редких случаях синхронизация выполняется так долго.
Запись буферов. Следующие поля позволяют анализировать объем данных, записанных как
фоновыми, так и клиентскими процессами:
Процесс фоновой записи можно рассматривать как помощника процесса контрольной точки.
Постоянно записывая грязные страницы, он уменьшает объем работы, необходимый для вы-
полнения контрольной точки. Первые две метрики показывают объем работы, проделанный
двумя этими фоновыми процессами. Третья метрика показывает количество буферов, запи-
санных клиентскими процессами, включая и рабочие процессы автоочистки. Напомню, что,
если процесс не смог найти нужный блок с данными в кеше, он вынужден прочитать этот
блок с диска. Для этого обычно требуется найти буфер (buffers_alloc) и вытеснить из него
имеющийся блок. Если найденный буфер окажется грязным, то дополнительно придется запи-
сать содержимое буфера на диск. Именно эту работу и проделывает заранее процесс фоновой
записи.
Перечисленные метрики можно вывести в график (рис. 5.9). На нем видно, что процессы фоно-
вой записи и контрольной точки создают более или менее стабильную и ровную нагрузку, а вот
от клиентских процессов есть два пика запросов на запись. Стоит разобраться, справляется ли
процесс фоновой записи (см. maxwritten_clean) или это был спонтанный всплеск в рабочей
нагрузке.
1
[Link]/docs/postgresql/current/runtime-config-resource#RUNTIME-CONFIG-RESOURCE-BACKGROUND-WRITER
Резюме 155
Завершая главу, стоит отметить, что некоторые другие представления, такие как pg_stat_wal
или pg_stat_replication_slots, также содержат статистику ввода-вывода. Они в меньшей сте-
пени относятся к вводу-выводу, связанному с пользовательскими данными, однако могут слу-
жить источником информации о вводе-выводе на уровне экземпляра СУБД. Эти и другие пред-
ставления будут рассмотрены в следующих главах, посвященных WAL-журналу и репликации.
Резюме
Для гарантий надежности все изменения данных в СУБД должны быть записаны в надежное
хранилище (обычно на диск). Нельзя допускать искажений, повреждений и тем более потери
данных даже в случае сбоев в системе. Для реализации этих требований практически все СУБД
используют журнал предзаписи. В PostgreSQL журнал представляет собой историю изменений
данных, и СУБД в случае аварии использует журнал для воспроизведения этих изменений и до-
стижения последней согласованной точки до момента аварии.
Однако СУБД может эксплуатироваться очень долго, и хранить абсолютно всю историю изме-
нений невозможно (либо это требует значительных или даже неадекватных экономических
вложений). Для повторного использования файлов журнала и поддержания его в разумных
объемах применяются контрольные точки: все изменения, предшествующие такой точке, га-
рантированно записаны в основное хранилище данных, которое считается надежным. Отмет-
ки об успешном завершении контрольных точек также записываются в журнал, после чего
часть журнала до контрольной точки может быть удалена. В случае аварии остается воспро-
извести только те изменения, которые были записаны в журнал после контрольной точки,
и прийти к состоянию до момента аварии.
На практике журнал представляет собой набор файлов (сегментов) внутри подкаталога pg_wal
в основном каталоге данных:
# ls -l /var/lib/postgresql/data/pg_wal/
total 196616
-rw------- 1 postgres postgres 341 Nov 4 05:03 [Link]
-rw------- 1 postgres postgres 16777216 Nov 18 05:26 000000010000004B000000C2
-rw------- 1 postgres postgres 16777216 Nov 18 05:27 000000010000004B000000C3
-rw------- 1 postgres postgres 16777216 Nov 18 05:28 000000010000004B000000C4
-rw------- 1 postgres postgres 16777216 Nov 18 05:28 000000010000004B000000C5
-rw------- 1 postgres postgres 16777216 Nov 18 05:29 000000010000004B000000C6
-rw------- 1 postgres postgres 16777216 Nov 18 05:30 000000010000004B000000C7
-rw------- 1 postgres postgres 16777216 Nov 18 05:31 000000010000004B000000C8
-rw------- 1 postgres postgres 16777216 Nov 18 05:32 000000010000004B000000C9
-rw------- 1 postgres postgres 16777216 Nov 18 05:24 000000010000004B000000CA
-rw------- 1 postgres postgres 16777216 Nov 18 05:20 000000010000004B000000CB
-rw------- 1 postgres postgres 16777216 Nov 18 05:25 000000010000004B000000CC
-rw------- 1 postgres postgres 16777216 Nov 18 05:23 000000010000004B000000CD
drwx------ 2 postgres postgres 4096 Nov 18 05:31 archive_status
Все сегменты журнала имеют имя из 24 цифр в шестнадцатеричном формате без расширения.
Имя состоит из трех октетов (на примере сегмента 000000010000004B000000CD):
6.1. Write-Ahead Log — журнал упреждающей записи 159
1. 00000001 — идентификатор линии времени (timeline id). Линии времени используется ме-
ханизмом восстановления на точку во времени (Point-in-Time Recovery, PITR)1, 2 .
Каждая отдельная запись имеет уникальный идентификатор LSN (log sequence number). Иден-
тификатор можно использовать как позицию в WAL-журнале, по которой можно определить
местоположение записи в журнале. На примере записи 4B/DF0026B0:
# SELECT pg_walfile_name('4B/DF008B78');
pg_walfile_name
--------------------------
000000010000004B000000DF
При активной эксплуатации СУБД и постоянном изменении данных в журнал вставляются но-
вые записи и текущая позиция (выраженная в LSN) постоянно смещается вперед. После встав-
ки добавленные записи следует надежно записать и в основное хранилище. Получить текущую
позицию записи журнала можно с помощью нескольких функций:
# ls -l /var/lib/postgresql/data/pg_wal/
total 196616
-rw------- 1 postgres postgres 341 Nov 4 05:03 [Link]
-rw------- 1 postgres postgres 16777216 Nov 18 06:21 000000010000004B000000F5
-rw------- 1 postgres postgres 16777216 Nov 18 06:22 000000010000004B000000F6
-rw------- 1 postgres postgres 16777216 Nov 18 06:23 000000010000004B000000F7
-rw------- 1 postgres postgres 16777216 Nov 18 06:24 000000010000004B000000F8
-rw------- 1 postgres postgres 16777216 Nov 18 06:25 000000010000004B000000F9
-rw------- 1 postgres postgres 16777216 Nov 18 06:26 000000010000004B000000FA
-rw------- 1 postgres postgres 16777216 Nov 18 06:27 000000010000004B000000FB
-rw------- 1 postgres postgres 16777216 Nov 18 06:28 000000010000004B000000FC <<< активный сегмент
-rw------- 1 postgres postgres 16777216 Nov 18 06:17 000000010000004B000000FD
-rw------- 1 postgres postgres 16777216 Nov 18 06:16 000000010000004B000000FE
6.2. Отслеживание активности в журнале 161
Запись в журнал может осуществляться как клиентом, так и фоновым процессом walwriter.
По умолчанию все изменения в журнал пишутся клиентским процессом. Это может проис-
ходить при подтверждении транзакции командой COMMIT или при заполнении WAL-буфера
(см. wal_buffers). Такая запись называется синхронной, и, перед тем как отправить следующую
команду или запрос, клиент вынужден дождаться завершения записи в журнал. Настрой-
ки СУБД позволяют переопределить поведение и использовать асинхронное подтверждение
транзакций (см. synchronous_commit). В таком режиме запись в журнал делегируется процессу
walwriter, который работает в фоновом режиме. Перекладывая задачу записи в журнал на фо-
новый процесс, клиентские процессы могут не дожидаться завершения записи и сразу от-
правлять серверу следующую команду. Однако следует помнить, что в таком режиме в слу-
чае сбоев есть риск потери последних транзакций, которые еще не были записаны процессом
walwriter. Настройка подтверждения транзакций имеет еще несколько режимов, действующих
при использовании репликации. Более подробно с ними можно ознакомиться в документа-
ции1 . Можно найти и исчерпывающую информацию о работе WAL-журнала2, 3 .
1
[Link]/docs/postgresql/current/wal-async-commit
2
[Link]/docs/postgresql/current/wal-configuration
3
[Link]/pg/[Link]
162 Глава 6. Журнал упреждающей записи
Представление pg_stat_wal
# TABLE pg_stat_wal;
-[ RECORD 1 ]----+------------------------------
wal_records | 322508636
wal_fpi | 46090986
wal_bytes | 393680012729
wal_buffers_full | 15545
wal_write | 48958771
wal_sync | 48846599
wal_write_time | 5697276.907
wal_sync_time | 486414839.104
stats_reset | 2022-11-04 05:01:49.427562+00
На рис. 6.2 изображен график объема записи в журнал в байтах, и здесь нагрузка также уме-
ренно ровная. Но, если присмотреться, этот график, так же как и предыдущий, напоминает
пилу с пятиминутными интервалами. Подобный график можно использовать для визуализа-
ции объема записи в журнал.
График на рис. 6.3 показывает количество операций записи данных из буфера в журнал
и последующих синхронизаций сегментов. В левой части графика количество операций за-
писи совпадает с количеством синхронизаций. Это связано с тем, что запись и последую-
щая синхронизация происходят при завершении каждой транзакции. Рядом показано ко-
личество подтвержденных транзакций (синяя линия commits), практически совпадающее
с остальными метриками. Дальше, в правой части графика, при неизменном потоке тран-
закций количество синхронизаций сегментов сильно уменьшилось и затем через какое-то
время вернулось на прежний уровень. Количество операций записи тоже уменьшилось, хотя
и не так сильно. В этот период был включен режим асинхронного подтверждения транзакций
(synchronous_commit = off). В этом режиме задачи по записи в журнал и синхронизации сегмен-
тов перекладываются на процесс walwriter, который делает это согласно своим настройкам.
Рис. 6.3. Количество операций записи из WAL-буфера, синхронизаций сегментов и подтвержденных транзакций
На таком графике важно отслеживать пики и выяснять причины всплесков, когда на запись
в журнал СУБД тратит больше времени, чем обычно.
Представление pg_stat_statements
В перечисленных полях содержится статистика записи для каждого конкретного типа запро-
сов. С ее помощью можно проанализировать состав WAL-журнала и ответить на вопрос о том,
какие запросы пишут в журнал больше остальных:
# SELECT
pg_size_pretty(sum(wal_bytes)) AS wal_volume,
left(query, 64) AS query_trunc
FROM pg_stat_statements
GROUP BY query
ORDER BY sum(wal_bytes) DESC
LIMIT 10;
wal_volume | query_trunc
------------+------------------------------------------------------------------
678 GB | UPDATE pgbench_accounts SET abalance = abalance + $1 WHERE aid =
10196 MB | INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES
8219 MB | UPDATE pgbench_tellers SET tbalance = tbalance + $1 WHERE tid =
7805 MB | UPDATE pgbench_branches SET bbalance = bbalance + $1 WHERE bid =
9313 kB | SELECT abalance FROM pgbench_accounts WHERE aid = $1
1394 kB | truncate pgbench_history
536 kB | vacuum pgbench_tellers
375 kB | select count(*) from pgbench_branches
250 kB | vacuum pgbench_branches
129 kB | SELECT current_database() AS database, schemaname AS schema, fun
166 Глава 6. Журнал упреждающей записи
Результат показывает, что наибольший объем записи в журнал генерирует запрос, связанный
с обновлением строк в pgbench_accounts (678 ГБ), с большим отрывом опережающий запрос,
занявший второе место. Получить данные из мониторинга можно следующим запросом:
# topk_avg(5,
sum by (queryid,query) (
rate(postgres_statements_wal_bytes_all_total{
service_id="primary"
}[1m]) + on(database,user,queryid) group_left(query)
0 * postgres_statements_query_info{service_id="primary"}
), "other")
Представление pg_stat_archiver
# TABLE pg_stat_archiver;
-[ RECORD 1 ]------+------------------------------
archived_count | 45711
last_archived_wal | 00000001000000B20000009D
last_archived_time | 2022-12-07 06:43:05.091157+00
failed_count | 21
last_failed_wal | 000000010000004600000073
last_failed_time | 2022-11-17 05:52:27.826036+00
stats_reset | 2022-11-04 05:01:49.427562+00
1
[Link]/docs/postgresql/current/continuous-archiving
168 Глава 6. Журнал упреждающей записи
Чтобы понять, как лучше использовать эту статистику, важно понимать классы проблем, кото-
рые могут возникнуть при архивировании журнала:
• ошибки передачи данных или отказ в обслуживании на стороне архива — в этом случае
процесс архивирования не может передать сегмент в хранилище;
• ошибка при выполнении команды архивирования, например из-за ошибки в скрипте или
его отсутствия;
• зависание процесса (по каким-то неизвестным причинам), ответственного за непосред-
ственную отправку в архив;
• зависание процесса archiver — это маловероятный сценарий, однако его нельзя исключать
полностью.
С проблемами вроде зависаний дело обстоит сложнее: в этой ситуации команда запустилась,
но не может завершиться, а процесс архивирования ожидает ее завершения. В таком случае
archived_count, failed_count и last_failed_time остаются без изменений, но при этом воз-
раст now() - last_archived_time начинает расти, что указывает на остановку архивирования.
На рис. 6.7 изображен график, демонстрирующий ту же самую остановку архивирования, что
и на рис. 6.6. В среднем архивирование сегментов выполняется каждые 54 секунды, но был
период, когда время увеличилось до 11 минут.
Очередь архивирования
Внутри каталога pg_wal есть подкаталог archive_status с набором файлов, описывающих статус
архивирования конкретных сегментов.
170 Глава 6. Журнал упреждающей записи
# ls -l /var/lib/postgresql/data/pg_wal/archive_status/
-rw------- 1 postgres postgres 0 Nov 4 05:03 [Link]
-rw------- 1 postgres postgres 0 Dec 8 06:00 [Link]
-rw------- 1 postgres postgres 0 Dec 8 06:01 [Link]
-rw------- 1 postgres postgres 0 Dec 8 06:02 [Link]
-rw------- 1 postgres postgres 0 Dec 8 06:04 [Link]
-rw------- 1 postgres postgres 0 Dec 8 06:05 [Link]
-rw------- 1 postgres postgres 0 Dec 8 06:06 [Link]
-rw------- 1 postgres postgres 0 Dec 8 06:06 [Link]
-rw------- 1 postgres postgres 0 Dec 8 06:08 [Link]
В этом каталоге нас интересуют файлы с расширениями ready и done. Статус ready указыва-
ет на то, что соответствующий сегмент готов для архивирования, а статус done — на то, что
сегмент был успешно отправлен в архив. Таким образом, чтобы получить число сегментов
в очереди на отправку, нам нужно лишь подсчитать количество файлов с расширением ready.
Размер очереди можно перевести в байты (умножить на размер сегмента, по умолчанию 16 МБ)
и получить представление о том, сколько места на диске занимают сегменты, требующие ар-
хивирования. Для подсчета потребуется функция pg_ls_archive_statusdir либо ее более уни-
версальный аналог pg_ls_dir:
# SELECT count(*)
FROM pg_ls_archive_statusdir()
WHERE name ~'.ready';
count
-------
0
# SELECT *
FROM pg_ls_archive_statusdir() ORDER BY modification;
name | size | modification
-----------------------------------------------+------+------------------------
[Link] | 0 | 2022-11-04 05:03:21+00
[Link] | 0 | 2022-12-08 06:10:14+00
[Link] | 0 | 2022-12-08 06:11:08+00
[Link] | 0 | 2022-12-08 06:12:06+00
[Link] | 0 | 2022-12-08 06:13:06+00
[Link] | 0 | 2022-12-08 06:14:15+00
[Link] | 0 | 2022-12-08 06:15:17+00
[Link] | 0 | 2022-12-08 06:16:19+00
[Link] | 0 | 2022-12-08 06:17:29+00
# SELECT count(*)
FROM pg_ls_archive_statusdir() WHERE name ~'.ready';
count
-------
4
172 Глава 6. Журнал упреждающей записи
Резюме
• Журнал упреждающей записи — важный компонент любой СУБД, который хранит исто-
рию всех изменений.
• Журнал представляет собой непрерывную последовательность файлов-сегментов.
• Для детального анализа содержимого журнала используются утилита pg_waldump и рас-
ширение pg_walinspect.
• Для отслеживания общей активности, связанной с журналом, используется представление
pg_stat_wal.
• pg_stat_statements содержит статистику записи в журнал для конкретных запросов.
• Журнал также используется для резервного копирования и репликации.
• Журнал можно архивировать для задач резервного копирования и восстановления.
• Для отслеживания архивирования используется представление pg_stat_archiver.
• Отслеживание очереди архивирования — наиболее универсальный способ выявления
проблем.
Глава 7
Репликация
• репликацию и ее устройство;
• инструменты отслеживания репликации;
• представления pg_stat_replication и pg_stat_wal_receiver;
• отставание репликации;
• слоты репликации;
• представление pg_replication_slots;
• публикации и подписки;
• представление pg_stat_replication_slots;
• представления pg_stat_subscription и pg_stat_subscription_stats;
• конфликты восстановления и представление pg_stat_database_conflicts.
Репликация — это процесс синхронизации данных между двумя и более узлами. Обычно раз-
личают два вида репликации: физическую и логическую. Физическая репликация предпола-
гает передачу физических изменений блоков данных без анализа содержимого передавае-
мых изменений. Логическая репликация является более сложной и включает в себя анализ,
174 Глава 7. Репликация
Оба типа репликации в PostgreSQL устроены схожим образом. Сначала выполняется начальная
синхронизация данных с основного узла на реплику. Дальше реплика подключается к основно-
му узлу, и все изменения, попадающие в WAL-журнал основного узла, передаются на реплику
по протоколу репликации. Реплики, в свою очередь, также могут передавать изменения на дру-
гие узлы; таким образом можно выстраивать каскадные конфигурации.
При физической репликации могут использоваться так называемые слоты репликации, а в слу-
чае логической репликации они являются обязательными. Слоты позволяют удерживать необ-
ходимый объем WAL-сегментов, исключая возможность их переработки в ходе выполнения
контрольных точек. Слоты являются важной частью механизма репликации и требуют внима-
ния и контроля со стороны администратора: для мониторинга логической репликации нужно
отслеживать не только передачу изменений, но и состояние слотов репликации.
1
[Link]/docs/postgresql/current/sql-createpublication
2
[Link]/docs/postgresql/current/sql-createsubscription
176 Глава 7. Репликация
При эксплуатации кластеров репликации могут возникнуть разные проблемы, способные по-
влиять как на работоспособность кластера, так и на производительность отдельных узлов:
• аварийное завершение работы СУБД из-за нехватки места на диске в связи с накоплением
сегментов журнала.
Представление pg_stat_replication
Основной узел является источником всех изменений и может реплицировать данные больше
чем на один узел. Скорость отправки изменений и скорость их применения на репликах мо-
гут различаться, что выражается в величине отставания репликации и степени актуальности
данных. Для определения отставания репликации и других характеристик передачи журна-
ла можно использовать представление pg_stat_replication. В каждой его строке содержится
статистика работы отдельного процесса walsender. Представление pg_stat_replication пока-
зывает текущие данные аналогично тому, как работает представление pg_stat_activity:
# TABLE pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid | 39
usesysid | 16431
usename | replica
application_name | walreceiver
client_addr | [Link]
client_hostname |
client_port | 46666
backend_start | 2022-12-14 03:54:32.228266+00
backend_xmin |
state | streaming
sent_lsn | D8/EF27AFC0
write_lsn | D8/EF27AFC0
flush_lsn | D8/EF27AFC0
replay_lsn | D8/EF27AFC0
write_lag | 00:00:00.000159
flush_lag | 00:00:00.002637
replay_lag | 00:00:00.002807
sync_priority | 0
sync_state | async
reply_time | 2022-12-16 04:36:36.495495+00
— streaming — это основной режим работы: walsender передает поток изменений на реп-
лику, после того как реплика успешно «догнала» основной узел;
Как уже упоминалось, изменения на репликах могут появляться с задержкой. Величина за-
держки (отставание репликации) в идеале должна быть близка к нулю, но это не всегда воз-
можно. Отставание еще можно расценивать как меру работы, которую необходимо проделать.
Однако основной узел не стоит на месте, и в его журнал записываются новые и новые из-
менения. Объем появляющихся изменений может варьироваться; соответственно, отставание
реплики тоже будет непостоянным. Увеличение отставания может выражаться в том, что ре-
зультаты выполнения запросов к основному узлу и реплике будут расходиться, и это следует
учитывать при проектировании приложений, которые получают данные с реплик.
Чтобы «догнать» основной узел, реплике требуется: а) получить все изменения с него, б) запи-
сать их в локальное хранилище, в) синхронизировать и г) воспроизвести над локальной копией
данных. Используя вышеперечисленные поля с позициями журнала, можно вычислить рас-
стояние в байтах между позициями и получить представление о том, в каком месте образова-
лась наибольшая задержка. Для этого потребуются уже известная функция pg_current_wal_lsn
и оператор вычитания, который поддерживается типом данных pg_lsn:
Следующая группа полей позволяет сразу отслеживать отставание в секундах, показывая при-
близительное время, которое потребуется реплике, чтобы «догнать» основной узел:
Остаются еще два поля: sync_priority и sync_state. При асинхронной репликации sync_state
всегда равно async. В случае синхронной репликации узлы могут находиться в разных состоя-
ниях и выполнять в кластере разные роли. Значение sync означает, что в данный момент реп-
лика работает как синхронная. Если используется синхронная репликация, основанная на при-
оритетах, значение potential говорит о том, что реплика работает в асинхронном режиме,
но является кандидатом на переход в синхронный режим (в этом случае sync_priority пока-
зывает приоритет данного узла). Если же используется синхронная репликация, основанная
на кворуме, значение quorum означает, что реплика входит в минимально необходимое число
узлов, подтвердивших получение изменений. Больше информации о настройке синхронной
репликации можно получить из документации1 .
1
[Link]/docs/postgrespro/15/runtime-config-replication#GUC-SYNCHRONOUS-STANDBY-NAMES
180 Глава 7. Репликация
С точки зрения поиска и устранения проблем интересными являются поля с адресом, позици-
ями обработки журнала и задержками, которые позволяют точно идентифицировать реплики
(особенно если их несколько) и определить, на каких этапах обработки журнала возникают
задержки. Поля с позициями журнала можно сразу преобразовать в величину задержек:
# SELECT
client_addr,
state,
pg_current_wal_lsn() - replay_lsn AS total_lag_bytes,
pg_current_wal_lsn() - sent_lsn AS pending_bytes,
sent_lsn - write_lsn AS write_lag_bytes,
write_lsn - flush_lsn AS flush_lag_bytes,
flush_lsn - replay_lsn AS replay_lag_bytes,
write_lag,
flush_lag,
replay_lag
FROM pg_stat_replication;
-[ RECORD 1 ]----+----------------
client_addr | [Link]
state | streaming
total_lag_bytes | 17152
pending_bytes | 0
write_lag_bytes | 0
flush_lag_bytes | 17152
replay_lag_bytes | 0
write_lag | 00:00:00.000151
flush_lag | 00:00:00.000151
replay_lag | 00:00:00.000151
Представление позволяет оценивать величину задержки как в байтах, так и в секундах, по-
казывая, какой объем данных нужно обработать реплике, чтобы «догнать» основной узел,
и сколько времени это займет. В выводе запроса видно, что отставание реплики незначительно
и составляет несколько микросекунд, в байтах задержка составляет всего около 17 КБ, при этом
данные уже переданы и записаны на реплику, их осталось только синхронизировать и воспро-
извести.
На основе величин задержек можно собирать метрики и строить графики, которые будут отоб-
ражать отставание реплик во времени (рис. 7.1 и 7.2).
Обратите внимание на легенды обоих графиков. Отставание в байтах представлено более по-
дробно и содержит информацию об объеме журнала, который еще не был отправлен на репли-
ку. Второй любопытный факт связан с тем, что в выбранный момент оба графика показывают
разные максимумы отставания. Например, большая часть отставания в байтах находится на
этапе записи изменений, в то время как отставание в секундах — уже на этапах синхронизации
и воспроизведения. Особенно хорошо это заметно на небольших значениях. Общая картина
обоих графиков показывает, что отставание часто появляется на этапах синхронизации и вос-
произведения. Это говорит о том, что реплика чуть хуже справляется со своей частью работы.
7.2. Инструменты отслеживания репликации 181
Представление pg_stat_wal_receiver
недоступности или из-за катастрофы) эти функции можно использовать для определения уз-
ла, который наилучшим образом подходит на роль основного: сравнивая позиции журнала,
следует выбрать узел, который успел принять больше журнальных записей или воспроизвести
бо́льшую их часть.
При нормальной работе репликации основной узел отправляет журналы на реплику, а реплика
воспроизводит изменения на локальной копии данных. Однако могут возникнуть обстоятель-
ства, при которых реплика не будет успевать воспроизводить изменения, или даже не будет
успевать получать и записывать журнал. Например, из-за увеличенной нагрузки на основном
узле генерируется большой объем журнала, пропускной способности сети становится недоста-
точно, сегменты журнала начинают скапливаться на основном узле и отставание реплики все
увеличивается. Или передача журналов прерывается из-за нарушения сетевой связности меж-
ду основным узлом и репликой, и репликация останавливается. В ранних версиях PostgreSQL
основной узел никак не отслеживал необходимость реплики в тех или иных сегментах жур-
нала — если реплика отключилась, сервер продолжает работу и при выполнении контрольной
точки может удалить те журналы, которые еще не были переданы на реплику. Если репли-
ка вновь подключится и запросит журнал с известной ей позиции, а на основном узле таких
сегментов журнала уже нет, то репликация не сможет продолжиться. Решением этой пробле-
мы являлась либо загрузка необходимых сегментов из архива, либо повторная инициализация
реплики. Оба варианта имеют свои минусы: содержать архив сегментов может быть накладно
по ресурсам, а инициализация может занимать много времени. Для минимизации пробле-
мы можно использовать параметр wal_keep_size (ранее назывался wal_keep_segments), который
указывает СУБД удерживать от переработки дополнительный объем журнала как раз на слу-
чай подобных отказов реплик. Другой, современный способ решить проблему — использовать
слоты репликации.
# TABLE pg_replication_slots;
-[ RECORD 1 ]-------+------------
slot_name | standby
plugin |
slot_type | physical
datoid |
database |
temporary | f
active | t
active_pid | 39
xmin |
catalog_xmin |
restart_lsn | D3/94ADE4C8
confirmed_flush_lsn |
wal_status | reserved
safe_wal_size |
two_phase | f
Обычно этого представления достаточно для оценки состояния слотов и определения факта
существования проблем:
• slot_name, slot_type, plugin — поля, идентифицирующие слот: его имя, тип и плагин для
декодирования потока WAL-записей (указываются только для логических слотов);
• datoid, database — идентификатор и имя базы данных, с которой ассоциирован слот (толь-
ко для логических слотов;
• temporary — флаг, указывающий на то, что слот является временным. Состояние времен-
ных слотов не сохраняется на диске, в случае ошибки или завершения сеанса они автома-
тически удаляются;
• active — флаг, указывающий на то, что слот является активным и используется в данный
момент. Это один из главных атрибутов слота, по которому можно определить, что потре-
битель слота жив;
• safe_wal_size — объем изменений в байтах, при записи которого в журнал слот окажется
в состоянии lost (при отсутствии потребителя). Значение может быть не указано (NULL),
если слот уже потерян или ограничение max_slot_wal_keep_size не установлено;
• two_phase — флаг, указывающий на то, что слот используется для декодирования подготов-
ленных транзакций (только для логических слотов).
# cd playground
# docker-compose stop standby
186 Глава 7. Репликация
# SELECT
slot_name, active,
pg_current_wal_lsn() - restart_lsn AS backlog,
wal_status, safe_wal_size
FROM pg_replication_slots;
slot_name | active | backlog | wal_status | safe_wal_size
-----------+--------+-----------+------------+---------------
standby | f | 353427008 | reserved | 722212616
Из-за выключения резервного узла через слот перестали передаваться изменения, и слот стал
неактивным (active = f). При этом рабочая нагрузка никуда не делась, и новые изменения
продолжают записываться в WAL-журнал, в результате чего растет отставание потребите-
ля (поле backlog). Вместе с ростом отставания начинает уменьшаться запас объема журнала
safe_wal_size, при этом состояние журнала пока reserved, что указывает на то, что требуемый
объем журнала укладывается в max_wal_size.
Можно запустить метакоманду \watch 60 и понаблюдать за тем, как отставание будет увеличи-
ваться со временем и в какой-то момент safe_wal_size станет отрицательным.
Если не включить вовремя реплику, слот будет потерян, что и произошло в этом эксперименте.
В журнале сообщений можно найти запись о том, что слот стал недействительным в процессе
выполнения контрольной точки:
# docker-compose rm standby
# docker volume rm playground_standby_data
# docker-compose up -d
После пересоздания контейнера статус слота покажет, что слот снова начал использоваться,
как и прежде:
# SELECT
slot_name, active,
pg_current_wal_lsn() - restart_lsn AS backlog,
wal_status, safe_wal_size
FROM pg_replication_slots;
slot_name | active | backlog | wal_status | safe_wal_size
-----------+--------+-----------+------------+---------------
standby | f | 0 | reserved | 1090234536
Публикации и подписки
1
[Link]/docs/postgresql/current/logical-replication
7.2. Инструменты отслеживания репликации 189
запись может вступить в конфликт уже со следующим запросом, из-за чего отставание может
накапливаться.
В случае принудительной отмены запроса приложение получит ошибку, которая будет также
зафиксирована в журнале сообщений:
Как поступать с такими ошибками, зависит от бизнес-требований: важно определить, что яв-
ляется наименьшим из двух зол — отставание от основного узла или прерывание запросов.
Если важно выполнение запросов без ошибок, то имеет смысл увеличить величину задержки
max_standby_streaming_delay, что потенциально может приводить к еще большему отставанию
реплики. Если же, наоборот, отставание реплики недопустимо (например, реплика является
основным кандидатом для аварийного переключения), то ошибки можно игнорировать или
обрабатывать их на стороне приложения, повторяя сбойные запросы на основном узле. Дру-
гим частым решением является использование отдельной реплики с очень большой величи-
ной допустимой задержки и потенциально большим отставанием. Такая реплика используется
преимущественно для выполнения продолжительных запросов и не участвует в процедурах
аварийного переключения.
Причинами конфликтов могут быть разные события, возникающие на основном узле. На прак-
тике чаще всего встречается отмена запросов из-за очистки: изменения, вызванные очист-
кой устаревших версий строк, попадают на реплику, в то время как на ней выполняется за-
прос, которому все еще нужны эти версии (счетчик confl_snapshot). Как правило, это про-
должительные аналитические запросы, обрабатывающие большие объемы данных, из-за чего
они могут выполняться непредсказуемо долго и требовать большого объема ресурсов. Отме-
на и повторение таких запросов может обходиться дорого как по ресурсам, так и по време-
ни ожидания результата. Для уменьшения вероятности отмены запросов есть две меры, ко-
торые можно принимать как по отдельности, так и вместе. Первая — это увеличение вели-
чины допустимой задержи max_standby_streaming_delay. Вторая — включение обратной связи
hot_standby_feedback. С помощью обратной связи процесс walsender будет получать от реп-
лики необходимый ей горизонт и учитывать его при удалении устаревших версий строк
(этот горизонт виден в pg_stat_replication.backend_xmin, если слот не используется, или
в pg_replication_slots.xmin, если используется). В этом случае конфликтующих изменений
не возникнет (подробно о том, что такое транзакционный горизонт, можно узнать в главе 8,
посвященной очистке). Однако ни один из этих способов не дает 100 % гарантии, что запрос
будет выполнен, и оказывают определенное негативное влияние. Увеличение задержки мо-
жет приводить к росту отставания, но, если время выполнения запроса превысит задержку,
запрос все равно будет отменен (в крайнем случае можно заставить реплику ждать бесконечно,
установив значение параметра в −1). Обратная связь гарантирует защиту только от очистки,
но за счет откладывания очистки таблицы будут раздуваться; кроме того, остаются и другие
причины, по которым могут произойти конфликт и последующая отмена запроса. Поэтому
идеального решения проблемы не существует и следует отталкиваться от бизнес-требований
о допустимости отмены запросов и возможной величине отставания. В качестве компромисс-
ного решения для аналитических запросов можно сделать отдельную реплику с большим зна-
чением допустимой задержки, но не использовать ее в выборах основного узла. Тогда большое
отставание будет допустимо; при этом можно отказаться от использования обратной связи
и минимизировать эффекты раздувания.
Резюме
В предыдущих главах рассмотрена часть фоновых процессов СУБД, в этой продолжено их рас-
смотрение. Глава посвящена процессу очистки, более известной как очистка (vacuum). Далее
в тексте я буду использовать термин «очистка», подразумевая оба его варианта, как ручной, так
и автоматический; в контексте автоматической очистки будет использоваться термин «авто-
очистка».
Очистка представляет собой реализацию сборщика мусора (garbage collector) и является важ-
ной частью СУБД. Ее эффективная работа напрямую влияет на производительность. В этой
главе в необходимом объеме будет рассмотрен механизм очистки, аспекты его работы, требу-
ющие отслеживания и, конечно же, необходимые для мониторинга инструменты СУБД.
При выполнении запросов и транзакций СУБД оперирует снимками (snapshot). Снимок опре-
деляет видимое для транзакции состояние данных и включает только зафиксированные на мо-
мент создания снимка изменения, а момент создания выбирается исходя из установленного
уровня изоляции (isolation level) транзакций. Конкурентное выполнение транзакций (и запро-
сов) предполагает существование нескольких снимков, в рамках которых может осуществлять-
ся не только чтение, но и изменение данных.
В модели MVCC PostgreSQL строки не изменяются на месте (in-place); вместо этого различные
операции выполняют действия с версиями строк:
Таким образом, для одной и той же строки может существовать несколько версий — одна ак-
туальная, она же живая (live), и, возможно, несколько мертвых (dead). Строки, помеченные как
удаленные, продолжают некоторое время храниться в тех же страницах и файлах данных, по-
скольку все еще могут потребоваться другим активным транзакциям (то есть входят в снимки,
используемые этими транзакциями). В случае высокой активности на запись может появлять-
ся все больше и больше версий строк, помеченных удаленными, что сказывается на размерах
таблиц и индексов. С постепенным завершением транзакций мертвые строки в конечном сче-
те становятся не нужными ни одной из транзакций, и их можно безопасно удалить, освободив
место для новых строк. Более подробно о работе механизма MVCC можно прочитать в соответ-
ствующей главе книги The Internals of PostgreSQL1 .
Первый процесс автоочистки autovacuum launcher поддерживает список баз данных и при-
нимает решение, когда нужно запустить очистку в конкретной базе. Когда в этом возникает
необходимость, autovacuum launcher отправляет сигнал головному процессу postmaster, и тот,
1
[Link]/pg/[Link]
2
[Link]/docs/postgrespro/15/sql-vacuum
8.2. Особенности очистки на практике 197
в свою очередь, запускает рабочий процесс autovacuum worker. Рабочий процесс очистки узна-
ет имя целевой базы, подключается к ней и строит список таблиц, подлежащих обработке.
Обрабатывая по очереди таблицы из списка, рабочий процесс сканирует их на предмет уста-
ревших версий строк, не нужных ни одной из активных транзакций, и освобождает место как
в самих таблицах, так и в индексах. Освобожденное пространство может использоваться для
вставки новых строк. Если в ходе очистки свободное пространство образовалось в конце файла
таблицы, то очистка может отсечь хвостовые пустые страницы (truncate heap) и таким образом
уменьшить файл таблицы.
При некоторых видах рабочих нагрузок возникают случаи, когда в таблицах и индексах содер-
жится относительно небольшое количество живых строк, а все остальное пространство осво-
бождено и при этом не используется. Если при этом участки пустого пространства физически
находятся в разных частях файла, то очистка не может высвободить эти участки и уменьшить
сам файл. Процесс образования таких участков и их постепенное увеличение называют разду-
ванием (bloat). В самом безобидном случае следствием эффекта раздувания является впустую
занимаемое пространство, а в худшем — снижение производительности ввода-вывода из-за
избыточных операций, нехватки буферного кеша и снижения эффективности индексного до-
ступа из-за образования лишних уровней у индексов. Поэтому при эксплуатации СУБД необ-
ходимо отслеживать излишнее раздувание таблиц и индексов и сокращать его.
Подводя некоторый итог, можно сказать, что с точки зрения эксплуатации администратор БД
должен отслеживать эффективность работы автоочистки, создаваемый ею уровень нагрузки
и долю мертвых строк в таблицах, и при необходимости корректировать настройки очистки
и уменьшать образовавшееся раздувание.
Чтобы получить представление о том, что именно следует отслеживать в работе очистки,
давайте рассмотрим практические особенности ее работы. Очистка запускается только при
наступлении определенных событий, и в идеале не должна запаздывать. Но в случае запазды-
вания важно представлять себе объем накопившейся работы.
Автоочистка обрабатывает таблицу, когда объем мертвых строк в ней превышает некоторое
допустимое количество. Устаревшие версии строк появляются из-за обновлений и удалений;
198 Глава 8. Очистка
Теперь, зная условие срабатывания автоочистки, можно написать запрос для определения таб-
лиц, у которых количество мертвых строк превышает допустимое значение. Запрос выводит
список пользовательских таблиц с некоторой статистикой:
• relation — имя таблицы. При желании можно вывести также имя схемы;
• reltuples — приблизительное количество строк в таблице. Это значение обновляется каж-
дый раз после выполнения автоочистки, команд VACUUM, ANALYZE и некоторых DDL-команд
вроде CREATE INDEX;
• live_tup — количество живых строк в таблице;
• dead_tup — количество устаревших, мертвых строк в таблице;
• boundary — допустимое количество мертвых версий строк. Когда количество мертвых вер-
сий превысит это значение, таблица должна обработаться автоочисткой;
• av_cnt — общее количество выполненных операций автоочистки на таблице;
• since_last_av — время, прошедшее с момента выполнения последней автоочистки;
1
[Link]/docs/postgresql/current/sql-createtable#SQL-CREATETABLE-STORAGE-PARAMETERS
8.2. Особенности очистки на практике 199
• av_need — признак того, что количество мертвых версий превышает допустимое значение
и таблице требуется обработка;
• dead_ratio — процентное отношение количества мертвых версий к общему числу строк
в таблице.
# SELECT
*,
av.dead_tup > [Link] AS av_need,
CASE WHEN reltuples > 0
THEN round(100.0 * av.dead_tup / reltuples)
ELSE 0
END AS n_dead_ratio
FROM
(SELECT
[Link] AS relation,
[Link] AS reltuples,
pg_stat_get_live_tuples([Link]) AS live_tup,
pg_stat_get_dead_tuples([Link]) AS dead_tup,
round(current_setting('autovacuum_vacuum_threshold')::integer +
current_setting('autovacuum_vacuum_scale_factor')::numeric * [Link]
) AS boundary,
pg_stat_get_autovacuum_count([Link]) AS av_cnt,
now() - pg_stat_get_last_autovacuum_time([Link]) AS since_last_av
FROM pg_class c
LEFT JOIN pg_namespace n ON ([Link] = [Link])
WHERE [Link] = 'r'
AND [Link] NOT IN ('pg_catalog', 'information_schema')
) AS av
ORDER BY av_need, dead_tup DESC;
relation | reltuples | live_tup | dead_tup | boundary | av_cnt | since_last_av | av_need | dead_ratio
------------------+--------------+----------+----------+----------+--------+-----------------+---------+------------
pgbench_accounts | 1.998889e+06 | 1998889 | 161911 | 399828 | 0 | | f | 8
pgbench_history | 957729 | 973640 | 0 | 191596 | 114 | 01:02:37.853934 | f | 0
pgbench_tellers | 200 | 200 | 1361 | 90 | 2849 | 00:00:36.65028 | t | 680
pgbench_branches | 20 | 20 | 1160 | 54 | 2849 | 00:00:36.660004 | t | 5800
1
[Link]/docs/postgresql/current/runtime-config-autovacuum#GUC-AUTOVACUUM-NAPTIME
200 Глава 8. Очистка
Можно сделать вывод: в тестовом окружении не так много таблиц, и автоочистка справляется
со своей работой.
Набор похожих полей есть и для операций сбора статистики планировщика. Сбор статисти-
ки может выполняться либо с помощью вызова команды ANALYZE, либо как дополнительная
операция при вызове команды VACUUM (с указанием параметра ANALYZE), либо рабочим про-
цессом автоочистки.
1
[Link]/docs/postgresql/current/sql-createtable#SQL-CREATETABLE-STORAGE-PARAMETERS
8.2. Особенности очистки на практике 201
# SELECT
schemaname ||'.'|| relname AS relation,
vacuum_count AS vacuum,
now() - last_vacuum AS since_last_vacuum,
autovacuum_count AS autovacuum,
now() - last_autovacuum AS since_last_autovacuum
FROM pg_stat_user_tables;
relation | vacuum | since_last_vacuum | autovacuum | since_last_autovacuum
-------------------------+--------+-------------------+------------+-----------------------
public.pgbench_tellers | 5 | 07:54:41.804464 | 2869 | 00:00:29.026952
public.pgbench_branches | 5 | 07:54:41.805567 | 2869 | 00:00:29.035908
public.pgbench_history | 0 | | 114 | 01:22:30.721539
public.pgbench_accounts | 0 | | 0 |
Собрав статистику выполнения операций очистки со всех БД, можно получить общую картину
по всему экземпляру и вывести ее в мониторинг (рис. 8.1):
График на рис. 8.1 показывает количество операций очистки и анализа, выполненных в те-
чение пяти минут. За показанные сутки количество операций держится на одном уровне без
больших колебаний, однако можно заметить некоторую цикличность с интервалом 10 часов.
В случае корректировок настроек автоочистки такой график помогает понять, как сделанные
изменения отразились на ее работе. Значительные изменения в рабочей нагрузке, особенно
связанные с объемом записи, также могут повлиять на частоту выполнения служебных опера-
ций, и последствия этих изменений также будут заметны на графике.
202 Глава 8. Очистка
У каждой транзакции, будь это даже одиночная команда, есть уникальный идентификатор. Это
может быть виртуальный идентификатор транзакции (virtualxid), а если транзакция вносит
изменения, ей присваивается постоянный 32-битный идентификатор (transaction id, xid, или
txid). При выполнении команд на изменение данных (вставка, обновление, удаление) в верси-
ях строк в служебных полях xmin и xmax1 записываются идентификаторы транзакций: в xmin —
создавшей строку, а в xmax — удалившей строку. Правила видимости строк опираются на эту
информацию. Например, версия строки, созданная только что зафиксированной транзакцией,
не попадет в уже начавшуюся выборку благодаря тому, что xmin этой версии будет превышать
максимально видимый в выборке номер2 .
При постоянной нагрузке СУБД выделяет все новые и новые идентификаторы транзакций. Их
диапазон ограничен 32 битами и соответствует значениям от 0 до 232−1 (около 4,2 миллиарда).
Получается, что в какой-то момент диапазон номеров может израсходоваться и счетчик тран-
закций должен будет начать отсчет с нуля. Но в таком случае все строки прошлых транзакций
оказались бы в будущем — ведь их идентификаторы превысили бы текущее значение счет-
чика. Это привело бы к внезапной потере всех данных, которые, физически оставаясь в стра-
ницах, перестали бы удовлетворять правилам видимости и стали бы недоступными. Чтобы
исключить такое поведение, диапазон идентификаторов зациклен и образует круг, разделен-
ный на две части: для любого идентификатора следующие два миллиарда идентификаторов
считаются «старше» его и находятся в будущем, а предыдущие два миллиарда — «младше»
и находятся в прошлом. Строка, созданная в какой-либо транзакции, для последующих двух
миллиардов транзакций находится в прошлом, но, если ничего не предпринять, для следую-
щих транзакций окажется в будущем. Поэтому еще до того, как будет достигнута граница в два
миллиарда, СУБД находит такие старые строки и ставит на них признак заморозки. По пра-
вилам видимости замороженные (frozen) строки считаются настолько старыми, что уже нет
необходимости проверять их значение xmin. Такие строки являются безусловно видимыми —
до тех пор, пока строка не будет обновлена или удалена, что приведет к установке значения
xmax.
FrozenTransactionId
В версиях PostgreSQL до 9.4 заморозка строк реализована заменой xmin строки на служеб-
ный идентификатор FrozenTransactionId (равный двум). В последующих версиях признак
заморозки устанавливается как битовый флаг в другое служебное поле, а xmin строки сохра-
няется.
1
[Link]/docs/postgresql/current/ddl-system-columns
2
[Link]/education/books/internals
8.3. Счетчик транзакций и предотвращение ошибок, связанных с его зацикливанием 203
тых в системе после данной, до текущего момента. Выполнять заморозку строк с малым воз-
растом может быть невыгодно, поскольку строка может измениться и выполненная работа
пропадет даром. Откладывать заморозку надолго тоже нельзя: при интенсивной рабочей на-
грузке может скопиться большое количество незамороженных строк и их обработка потребует
значительных ресурсов и времени. Заморозка обычно выполняется автоматически рабочими
процессами очистки (но может быть выполнена и в ручном режиме командой VACUUM FREEZE),
и, следовательно, для своевременной заморозки автоочистка также должна запускаться свое-
временно и без задержек. В зависимости от конфигурации СУБД или рабочей нагрузки могут
возникать ситуации, которые откладывают работу автоочистки или даже мешают ей. Игнори-
рование таких ситуаций в течение продолжительного времени может привести к снижению
производительности, а в худшем случае — к аварийной остановке СУБД. В качестве примеров
того, что может откладывать очистку, можно привести:
При нормальной эксплуатации автоочистка всегда должна быть включена, а в рабочей нагруз-
ке не должно возникать продолжительных или бездействующих операций.
Помимо relfrozenxid у таблиц есть схожий атрибут relminmxid, который указывает на са-
мый старый задействованный идентификатор мультитранзакции в этой таблице. Муль-
титранзакции представляют собой группы обычных транзакций, которым потребовалась
одновременная разделяемая блокировка одной строки. Такие транзакции также имеют
32-битные идентификаторы, которые записываются в поле xmax версий строк, и для них
также требуются аналог заморозки и продвижение relminmxid. Ниже при упоминании
relfrozenxid соответствующий контекст можно распространить и на мультитранзакции,
хоть это и не будет упоминаться явно.
При очистке СУБД оценивает горизонт заморозки и выбирает подходящий режим работы.
В обычном режиме очистка замораживает версии строк, возраст xmin которых превышает
значение vacuum_freeze_min_age (по умолчанию 50 млн). Если горизонт заморозки таблицы
превышает значение vacuum_freeze_table_age (по умолчанию 150 млн), очистка выполняется
в агрессивном режиме с полным сканированием всех страниц, чтобы заморозить версии строк
и на тех страницах, которые в обычном режиме пропускаются из-за карты видимости. Ес-
ли же возраст горизонта заморозки таблицы превышает значение autovacuum_freeze_max_age
(по умолчанию 200 млн), то очистка с заморозкой запускается принудительно, даже когда таб-
лица не требует очистки или автоочистка выключена (глобально параметром autovacuum или
параметрами хранения для отдельных таблиц). Это в том числе гарантирует, что версии строк
будут заморожены в таблицах, строки в которых не меняются, например в архивных секциях.
Как отмечено выше, в нашем примере есть одна таблица, pgbench_accounts, которая ни ра-
зу не очищалась и горизонт заморозки которой больше остальных. Для эксперимента мож-
но выполнить очистку с принудительной заморозкой (VACUUM FREEZE), после которой значение
relfrozenxid продвинется вперед, а горизонт уменьшится.
8.3. Счетчик транзакций и предотвращение ошибок, связанных с его зацикливанием 205
Если рассматривать все таблицы в отдельной базе данных, то их значения relfrozenxid, как
правило, различаются; среди всех таблиц найдется таблица с самым большим горизонтом (са-
мым старым значением relfrozenxid). Горизонт заморозки этой таблицы определяет горизонт
заморозки базы данных и хранится в pg_database.datfrozenxid — все версии строк с более ста-
рыми идентификаторами в любой таблице этой базы данных гарантированно заморожены.
Самое старое значение pg_database.datfrozenxid в кластере баз данных определяет горизонт
заморозки всего кластера. Продвижение вперед этого горизонта позволяет высвободить иден-
тификаторы транзакций для их повторного использования.
Значение datfrozenxid продвинуто только у базы pgbench (для нее недавно была выполнена
очистка с заморозкой). В остальных базах значение сохранилось еще со времен инициали-
зации кластера: в них нет никакой активности, нет необходимости выполнять очистку, сле-
довательно, и заморозка в них не происходит. Но, как только горизонты заморозки этих баз
превысят значение autovacuum_freeze_max_age (по умолчанию 200 млн), в них будет запущена
принудительная очистка с заморозкой. Осталось ждать не так долго. Очистка скоро выполнит-
ся, и запрос покажет продвижение datfrozenxid и уменьшение горизонта:
Для базы pgbench все осталось как прежде, поскольку в ней заморозка была выполнена недавно
и повторная пока не требуется.
206 Глава 8. Очистка
Первые два поля указывают на активные транзакции, выполняющиеся на текущем узле. Ес-
ли используются реплики и включена обратная связь (hot_standby_feedback = on), оставшееся
поле определяет горизонт, удерживаемый транзакциями на реплике с помощью слота ре-
пликации. Если репликация не использует слот, эта же информация будет доступна в по-
ле pg_stat_activity.backend_xid соответствующего процесса walsender. С помощью обратной
связи администратор может отслеживать долгую активность на репликах со стороны основно-
го узла, но при этом обратная связь откладывает работу очистки, приводя ко всем сопутству-
ющим негативным последствиям. Поэтому решение об использовании обратной связи должно
приниматься взвешенно.
# SELECT
max_horizon,
2147483647 - max_horizon AS available_xids
FROM
(
SELECT greatest(
max(age(datfrozenxid)),
max(mxid_age(datminmxid))
) AS max_horizon
FROM pg_database
) AS d;
max_horizon | available_xids
-------------+----------------
3547151 | 2143936496
В приведенном выводе горизонт достиг 3,5 млн и имеется очень большой запас идентифика-
торов (поле available_xids), превышающий два миллиарда.
В нормальной ситуации вывод запроса будет пустой, однако если использовать \watch с ко-
ротким интервалом (например, одна десятая секунды), то можно будет увидеть, как быстро
появляются и быстро исчезают строки с очень коротким горизонтом — например, как в выводе
выше. В данном примере горизонт равен единице: в тестовом окружении нет долгих транзак-
ций, которые бы надолго удерживали горизонт видимости.
208 Глава 8. Очистка
Однако можно открыть еще один сеанс, обновить в новой транзакции произвольную строку
в pgbench_accounts и некоторое время просто не завершать начатую транзакцию:
# BEGIN;
# UPDATE pgbench_accounts SET abalance = abalance + 1 WHERE aid = 1;
Теперь, если повторить запрос еще несколько раз, будет видно, что картина изменилась:
Из-за активности в базе данных счетчик транзакций увеличивается, а из-за открытой транзак-
ции горизонт начал расти (идентификатор, определяющий снимок открытой транзакции, ухо-
дит в прошлое относительно номера текущей транзакции, в которой выполняется наш запрос).
Количество доступных идентификаторов тоже начало уменьшаться, хотя это не так заметно.
Самое главное — чем дольше транзакция во втором сеансе будет находиться в открытом состо-
янии, тем больше будет увеличиваться горизонт видимости, в пределах которого автоочистка
не сможет удалять устаревшие версии строк (что будет приводить к раздуванию) и заморажи-
вать старые версии строк (что со временем может привести к реальному уменьшению коли-
чества доступных идентификаторов). После фиксации или отката транзакции горизонт снова
уменьшится до нуля.
в журнале сообщений начнут появляться уведомления о том, что самый старый используемый
идентификатор транзакции находится далеко в прошлом:
В качестве подсказки СУБД предлагает закрыть открытые транзакции, подтвердить или от-
катить старые подготовленные транзакции или удалить неактивные слоты репликации. При
дальнейшем ухудшении ситуации очистка начинает выполняться в особом режиме (failsafe),
при котором для ускорения заморозки пропускается обработка индексов. Этот режим вклю-
чается при превышении возрастом relfrozenxid значения vacuum_failsafe_age:
Если продолжать игнорировать сообщения, СУБД будет более прямолинейна и начнет сооб-
щать о том, что вполне конкретные базы данных должны быть обработаны очисткой:
Такая ситуация расценивается как опасная и переход СУБД в аварийный режим исключает
возможность продолжить работу и повредить данные. Для восстановления и продолжения
нормальной работы администратору придется перезапустить СУБД в особом, однопользова-
тельском режиме (single-user mode), выполнить очистку в тех БД, где это необходимо, после
чего перезапустить СУБД в обычном режиме.
Устаревшие версии строк появляются в результате операций обновления или удаления. При
нормальной работе занимаемое такими версиями место оперативно освобождается очисткой
и используется для вставки новых версий. Однако если устаревших версий накопилось слиш-
ком много и автоочистка долгое время не имела возможности вычищать их, то после очистки
в файлах таблицы и индексов образуются большие свободные участки, которые, возможно,
не израсходуются в дальнейшем и будут просто занимать место на диске. В результате диско-
вое пространство используется неэффективно. Стоит отдельно отметить, что это относится
к «пустотам», находящимся в начале или середине файла: если очистка освобождает место
в конце файла, она может попытаться сделать усечение файла.
При продолжительной эксплуатации СУБД таких пустот в таблицах и индексах может образо-
вываться все больше и больше, и происходит так называемое раздувание. Если сложить все
эти пустоты вместе, может получиться внушительная цифра. С точки зрения эксплуатации
необходимо регулярно проверять таблицы и индексы на предмет раздувания, выполнять их
обслуживание и высвобождать неиспользуемое пространство.
Для поиска раздутых объектов наиболее эффективным и точным средством оценки являет-
ся расширение pgstattuple1 . Однако ценой точности являются большие накладные расходы:
расширение читает каждую страницу проверяемого объекта, что требует соответствующе-
го ввода-вывода и нагружает дисковую подсистему. Учитывая, что СУБД не имеет встроен-
ных механизмов ограничения пропускной способности, такая «проверка» может значительно
1
[Link]/docs/postgresql/current/pgstattuple
8.4. Раздувание таблиц и индексов 211
Время выполнения запроса зависит от размера таблицы, так как необходимо прочи-
тать каждый ее блок, поэтому оценка pgbench_accounts выполнялась дольше, чем оценка
pgbench_branches. В результате анализа таблиц доступна следующая информация, по которой
можно оценить раздувание:
1
[Link]/wiki/Show_database_bloat
212 Глава 8. Очистка
В продолжение нашего примера выполним команду полной очистки VACUUM FULL для таблицы
pgbench_branches. Это хоть и блокирующая операция, но выполнится она мгновенно, поскольку
целевая таблица небольшая и содержит всего 20 строк. После операции полной очистки снова
посмотрим статистику pgstattuple:
Высвободилось практически все свободное место, а таблица стала умещаться в одну страницу
размером 8 КБ. Однако это всего лишь небольшая таблица в тестовом окружении. В настоящих
производственных окружениях значения и результаты будут совершенно иными.
Представление pg_stat_activity
1
[Link]/reorg/pg_repack
2
[Link]/dataegret/pgcompacttable
3
[Link]/cybertec-postgresql/pg_squeeze
214 Глава 8. Очистка
• query — текст запроса является определяющим и позволяет взять только те строки, кото-
рые относятся к процессам, выполняющим очистку, неважно, автоматическую или запу-
щенную администратором в отдельном сеансе.
При необходимости можно добавить и другие поля. Используя фильтры на основе перечис-
ленных полей, можно исключить все прочие процессы и выбрать только те, что выполняют
очистку. С точки зрения постоянного мониторинга нас могут интересовать ответы на следу-
ющие вопросы:
• Сколько времени длится очистка? В идеале, если позволяют ресурсы, очистка должна вы-
полняться быстро. Наличие свободных рабочих процессов в распоряжении СУБД позволя-
ет избегать появления очередей таблиц на обработку. Особенно опасной может считаться
ситуация, характерная для «больших» баз данных со смешанной нагрузкой (HTAP, Hybrid
Transactional/Analytical Processing). В таких базах могут встречаться как таблицы больших
размеров, так и таблицы с большим объемом записи. Первые таблицы могут требовать за-
морозки, при которой таблица должна быть прочитана полностью, что может занять мно-
го времени, а изменения вторых будут постоянно сдвигать счетчик транзакций вперед.
8.5. Отслеживание активных процессов очистки 215
# SELECT
pid,
datname,
now() - xact_start as duration,
state,
wait_event_type,
wait_event,
query
FROM pg_stat_activity
WHERE query ~'^autovacuum' OR query ~*'^vacuum';
-[ RECORD 1 ]------+----------------------------
pid | 2364758
datname | pgbench
duration | 00:00:10.04677
state | active
wait_event_type | Timeout
wait_event | VacuumDelay
query | autovacuum: VACUUM ANALYZE public.pgbench_accounts
В запросе используется условие на текст запроса, который должен начинаться с шаблонов, ха-
рактерных для рабочих процессов автоочистки и очистки, запущенной в отдельном сеансе.
Такой способ более удобен, чем условие на поле backend_type, не позволяющее выделить очист-
ку в обычных сеансах. Одна строка в выводе запроса указывает на то, что запущен всего один
рабочий процесс. Подсчитав количество строк функцией count, можно узнать, сколько рабо-
чих процессов очистки запущено в конкретный момент. Это позволяет ответить на первый
вопрос — о количестве выполняющихся процессов. Поле duration отображает продолжитель-
ность выполнения очистки, что дает ответ на второй вопрос — о длительности. Приведенная
в запросе информация о состоянии и событиях ожиданий в мониторинг не собирается и чаще
всего нужна только в случаях поиска и устранения проблем.
216 Глава 8. Очистка
Полученные ответы можно отобразить в виде графиков. На рис. 8.3 показан график количества
процессов очистки. Возникает резонный вопрос: откуда такая разница между рисунком и ре-
альным количеством очисток, выполняемых в таблицах pgbench_branches и pgbench_tellers?
Ответ заключается в специфике представления pg_stat_activity, которое показывает теку-
щий снимок и не носит накопительного характера: все то, что происходит между снимками,
остается неизвестным. Для понимания количества очисток предпочтителен график на рис. 8.1.
График на рис. 8.4 показывает те моменты, когда удалось поймать процессы очистки
в pg_stat_activity и зафиксировать их продолжительность. Значений снова не так уж и мно-
го, но, к сожалению, в СУБД нет других представлений, откуда можно было бы извлечь такую
информацию. Более полную и точную информацию можно получить из журналов активности.
При установке параметра log_autovacuum_min_duration статистика работы каждого процесса ав-
тоочистки, занявшего более указанного времени, будет записана в журнал сообщений.
Представление pg_stat_progress_vacuum
# SELECT
[Link],
now() - a.xact_start AS duration,
wait_event_type ||'.'|| wait_event AS wait_event,
CASE
WHEN [Link] ~ '^autovacuum.*to prevent wraparound' THEN 'wraparound'
WHEN [Link] ~ '^vacuum' THEN 'user'
ELSE 'regular'
END AS mode,
[Link] AS database,
[Link]::regclass AS table,
[Link],
pg_size_pretty(p.heap_blks_total * current_setting('block_size')::int) AS table_size,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size,
pg_size_pretty(p.heap_blks_scanned * current_setting('block_size')::int) AS scanned,
round(100.0 * p.heap_blks_scanned / p.heap_blks_total, 1) AS scanned_pct,
pg_size_pretty(p.heap_blks_vacuumed * current_setting('block_size')::int) AS vacuumed,
round(100.0 * p.heap_blks_vacuumed / p.heap_blks_total, 1) AS vacuumed_pct,
p.index_vacuum_count,
round(100.0 * p.num_dead_tuples / p.max_dead_tuples,1) AS work_mem_usage
FROM pg_stat_progress_vacuum p
JOIN pg_stat_activity a ON [Link] = [Link]
ORDER BY now() - a.xact_start DESC;
-[ RECORD 1 ]------+---------------------------------------------------
pid | 2688928
duration | 00:00:52.050535
wait_event | [Link]
mode | regular
database | pgbench
table | pgbench_accounts
phase | vacuuming heap
table_size | 522 MB
total_size | 608 MB
scanned | 522 MB
scanned_pct | 100.0
vacuumed | 239 MB
vacuumed_pct | 45.7
index_vacuum_count | 1
work_mem_usage | 17.2
• mode = regular указывает на то, что это обычная автоочистка (не связанная с обслуживани-
ем счетчика транзакций и не вызванная пользователем);
• размер таблицы 522 МБ, вместе с индексами — 608 МБ;
• значение scanned_pct = 100.0 % говорит о том, что таблица полностью просканирована
на предмет мертвых строк и выполняется непосредственно очистка (phase = vacuuming
heap);
• vacuumed_pct = 45.7 % указывает на то, что очистка выполнена почти наполовину;
• рабочая память заполнена на 17.2 % (work_mem_usage) и уже был выполнен один цикл об-
работки индексов (index_vacuum_count = 1).
Если снова провести полное обновление таблицы pgbench_accounts, а в соседнем сеансе запус-
тить запрос с помощью \watch 1, можно будет наглядно наблюдать за тем, как выполняется
очистка. Однако такой запрос и выводимая статистика больше подходят для текущей оценки
происходящего, собирать такую информацию в мониторинг не имеет особого смысла.
Резюме
• Механизм конкурентного доступа допускает существование нескольких версий одной
и той же строки.
• Жизненный цикл строк подразумевает существование живых и мертвых версий.
• Мертвые версии строки необходимо регулярно вычищать.
• Очистка выполняется как для таблиц, так и для индексов.
• Автоочистка выполняется, когда количество мертвых версий строк превышает определен-
ный порог.
• Очистка создает дополнительный ввод-вывод и может влиять на производительность.
• Администратору важно отслеживать работу очистки в СУБД.
• Автоочистка должна запускаться без задержек.
• Автоочистка обслуживает счетчик транзакций и обеспечивает постоянный запас свобод-
ных идентификаторов транзакций.
• Администратору важно отслеживать запас доступных идентификаторов транзакций.
• При неблагоприятном стечении обстоятельств запас идентификаторов может оказаться
исчерпанным, что приведет к остановке нормальной работы СУБД.
• Неэффективная работа очистки может приводить к эффекту раздувания.
• Следствием раздувания являются неэффективное использование дискового пространства
и снижение производительности.
• Для оценки степени раздувания используется расширение pgstattuple.
• Для отслеживания активных процессов очистки могут использоваться представления
pg_stat_activity и pg_stat_progress_vacuum.
Глава 9
Ход выполнения операций
При исполнении запроса СУБД следует наиболее оптимальному с точки зрения использова-
ния ресурсов плану выполнения, выбранному планировщиком из множества возможных пла-
нов. Для оценки планов используется статистика, описывающая данные, хранящиеся в таб-
лицах, и от ее точности зависит качество выбранного плана. Собираемая статистика отража-
ется в системном представлении pg_stats1 , основанном на таблице pg_statistic2 . В процессе
эксплуатации СУБД количественные и качественные характеристики данных могут и будут
меняться, и статистика планировщика в pg_statistics будет неизбежно устаревать. В услови-
ях изменяющихся данных очень важно регулярно обновлять статистику: в противном случае
выбираемые на основе устаревшей информации планы будут неэффективными, что приведет
к избыточному использованию ресурсов и увеличению времени выполнения.
• после обновления версии СУБД с помощью утилиты pg_upgrade6 , поскольку при такой
процедуре статистика планировщика не переносится.
Возможны и другие сценарии, но в любом случае процесс сбора статистики может занимать
продолжительное время и администратору следует иметь представление о том, как долго он
1
[Link]/docs/current/[Link]
2
[Link]/docs/current/[Link]
3
[Link]/docs/current/[Link]
4
[Link]/docs/current/[Link]#GUC-DEFAULT-STATISTICS-TARGET
5
[Link]/docs/current/[Link]
6
[Link]/docs/current/[Link]
9.1. Представление pg_stat_progress_analyze 223
Исходные данные из представления могут быть не очень понятны, поэтому для большей ин-
формативности имеет смысл провести некоторую обработку: транслировать идентификаторы
в имена, перевести блоки в байты и взять дополнительную информацию из pg_stat_activity,
соединив представления по полю pid.
В тестовом окружении довольно сложно поймать автоматически сбор статистики, поэтому за-
прос можно выполнить с помощью \watch 1 и в соседнем сеансе запустить команду ANALYZE.
В первом сеансе появится подобный вывод:
224 Глава 9. Ход выполнения операций
# SELECT
[Link], now() - a.xact_start AS xact_age,
[Link], [Link]::regclass AS relation,
[Link], a.wait_event_type ||'.'|| a.wait_event AS wait_event,
[Link],
pg_size_pretty(p.sample_blks_total * (
SELECT current_setting('block_size')::int )) AS sample_size,
round(100 * p.sample_blks_scanned /
greatest(p.sample_blks_total,1), 2) AS "scanned,%",
p.ext_stats_total ||'/'|| p.ext_stats_computed AS "ext_total/done",
p.child_tables_total ||'/'|| p.child_tables_done AS "child_total/done",
current_child_table_relid::regclass AS child_in_progress,
[Link]
FROM pg_stat_progress_analyze p
INNER JOIN pg_stat_activity a ON [Link] = [Link]
ORDER BY now() - a.xact_start DESC;
-[ RECORD 1 ]-----+----------------------
pid | 536666
xact_age | 00:00:07.660563
datname | pgbench
relation | pgbench_accounts
state | active
wait_event | [Link]
phase | acquiring sample rows
sample_size | 522 MB
scanned,% | 85.00
ext_total/done | 0/0
child_total/done | 0/0
child_in_progress | -
query | ANALYZE;
Команда ANALYZE выполняется семь секунд, для подсчета статистики нужно получить выбор-
ку размером 522 МБ, и на данный момент просканировано уже 85 %. В данном примере через
пару секунд сканирование будет закончено и начнется фаза подсчета статистики. Для более
сложных случаев с секционированными таблицами или расширенной статистикой есть допол-
нительные поля с уточняющей информацией.
Чтобы увидеть какие-либо результаты, стоит выполнить приведенный ниже запрос с помощью
\watch 1, запустив в соседнем терминале резервное копирование. Поскольку полученная ре-
зервная копия нам не понадобится, ее запись можно направить в /dev/null:
# SELECT
[Link],
host(a.client_addr) AS started_from,
to_char(backend_start, 'YYYY-MM-DD HH24:MI:SS') AS started_at,
now() - backend_start AS duration,
[Link], a.wait_event_type ||'.'|| a.wait_event AS waiting,
[Link],
pg_size_pretty(p.backup_total) AS size_total,
pg_size_pretty(p.backup_streamed) AS sent,
round(100 * p.backup_streamed / greatest(p.backup_total,1), 2) AS "sent,%",
p.tablespaces_total ||'/'|| p.tablespaces_streamed AS "ts_total/streamed"
FROM pg_stat_progress_basebackup p
INNER JOIN pg_stat_activity a ON [Link] = [Link]
ORDER BY now() - backend_start DESC;
-[ RECORD 1 ]--------------+-------------------------
pid | 545355
started_from | [Link]
started_at | 2023-03-06 04:43:41
duration | 00:00:36.700719
state | active
waiting | [Link]
phase | streaming database files
size_total | 713 MB
sent | 623 MB
sent,% | 87.00
ts_total/streamed | 1/0
Из приведенного фрагмента видно, что резервное копирование длится чуть больше 30 секунд,
клиенту отправлена бóльшая часть содержимого БД, копирование выполнено на 87 % и уже
подходит к завершению. Из представления pg_stat_activity взяты поле state и маркер ожи-
дания, который в большинстве случаев будет показывать ожидания ввода-вывода, поскольку
процесс копирует файлы данных и не использует блокировки.
Команды CLUSTER1 и VACUUM FULL2 используются для пересоздания таблиц. Они потребляют мно-
го ресурсов и устанавливают исключительную блокировку, запрещающую доступ к обрабаты-
ваемым таблицам. При неаккуратном использовании такие команды могут легко заблокиро-
вать работу приложений вплоть до своего завершения, поэтому администратору требуется ин-
струмент, позволяющий отслеживать ход их выполнения. Для этого в СУБД есть представление
pg_stat_progress_cluster, где каждая строка содержит информацию об отдельной операции
со следующим набором полей:
1
[Link]/docs/current/[Link]
2
[Link]/docs/current/[Link]
9.3. Представление pg_stat_progress_cluster 227
На основе перечисленных полей легко понять, на каком этапе находится выполнение коман-
ды, и оценить ход выполнения. Для большей информативности также имеет смысл соединить
представление с pg_stat_activity и провести некоторую обработку.
# SELECT
[Link],
now() - a.xact_start AS xact_age,
[Link],
[Link]::regclass AS relation,
p.cluster_index_relid::regclass AS index,
[Link],
(SELECT count(distinct [Link]) FROM pg_locks l
WHERE [Link] = [Link] AND NOT [Link]) AS blocked_total,
a.wait_event_type ||'.'|| a.wait_event AS wait_event,
[Link],
pg_size_pretty(p.heap_blks_total * (
SELECT current_setting('block_size')::int)) AS size_total,
round(100 * p.heap_blks_scanned /
greatest(p.heap_blks_total,1), 2) AS "scanned,%",
coalesce(p.heap_tuples_scanned, 0) AS tuples_scanned,
coalesce(p.heap_tuples_written, 0) AS tuples_written,
[Link]
FROM pg_stat_progress_cluster p
INNER JOIN pg_stat_activity a ON [Link] = [Link]
ORDER BY now() - a.xact_start DESC;
-[ RECORD 1 ]--+-------------------------------
pid | 550142
xact_age | 00:00:06.867676
datname | pgbench
relation | pgbench_accounts
index | -
state | active
blocked_total | 33
wait_event | [Link]
phase | seq scanning heap
size_total | 257 MB
scanned,% | 70.00
tuples_scanned | 1404122
tuples_written | 1404122
query | VACUUM FULL pgbench_accounts;
В приведенном примере команда VACUUM FULL длится уже шесть секунд, она выполнена на 70 %,
однако ее завершения ждут еще 33 сеанса, в которых выполняются запросы к перестраиваемой
таблице. Соответственно, приложения, которые выполняют эти запросы, также заблокирова-
ны и вынуждены ждать завершения команды.
При активной разработке приложений и при появлении в приложении новых типов запросов
создание индексов может быть довольно частой операцией. Создание индексов, особенно для
9.4. Представление pg_stat_progress_create_index 229
• command — выполняемая команда: CREATE INDEX, CREATE INDEX CONCURRENTLY, REINDEX или
REINDEX CONCURRENTLY;
— waiting for writers before build — команды CREATE INDEX CONCURRENTLY или REINDEX
CONCURRENTLY ожидают завершения транзакций, которые удерживают блокировки
на запись и могут читать таблицу. Фаза пропускается при выполнении операции в бло-
кирующем режиме. Детали выполнения фазы можно отслеживать в lockers_total,
lockers_done и current_locker_pid;
— waiting for writers before validation — команды CREATE INDEX CONCURRENTLY или
REINDEX CONCURRENTLY ожидают завершения транзакций, которые удерживают блоки-
ровки на запись и могут записывать в таблицу. Эта фаза пропускается при выполне-
нии операции в блокирующем режиме. Детали выполнения фазы можно отслеживать
в lockers_total, lockers_done и current_locker_pid;
— index validation: scanning index — команда CREATE INDEX CONCURRENTLY сканирует ин-
декс на предмет строк, требующих проверки. Фаза пропускается при выполнении
операции в блокирующем режиме. Детали выполнения фазы отражаются в полях
blocks_total и blocks_done;
— index validation: sorting tuples — команда CREATE INDEX CONCURRENTLY сортирует стро-
ки, найденные в фазе сканирования;
— index validation: scanning table — команда CREATE INDEX CONCURRENTLY сканирует таб-
лицу, чтобы проверить строки индекса, собранные в предыдущих двух фазах. Фаза
пропускается при выполнении операции в блокирующем режиме. Детали выполне-
ние фазы отражаются в blocks_total и blocks_done;
230 Глава 9. Ход выполнения операций
— waiting for old snapshots — команды CREATE INDEX CONCURRENTLY или REINDEX
CONCURRENTLY ожидают освобождения снимков теми транзакциями, которые могут
видеть содержимое таблицы. Фаза пропускается при выполнении операции в бло-
кирующем режиме. Детали выполнения отражаются в lockers_total, lockers_done
и current_locker_pid;
— waiting for readers before marking dead — перед тем как пометить старый индекс как
нерабочий, команда REINDEX CONCURRENTLY ожидает завершения транзакций, удержи-
вающих блокировки чтения. Фаза пропускается при выполнении операции в бло-
кирующем режиме. Детали выполнения отражаются в lockers_total, lockers_done
и current_locker_pid;
— waiting for readers before dropping — прежде чем удалить старый индекс, команда
REINDEX CONCURRENTLY ожидает завершения транзакций, которые удерживают блоки-
ровки чтения. Фаза пропускается при выполнении операции в блокирующем режиме.
Детали выполнения отражаются в lockers_total, lockers_done и current_locker_pid;
• partitions_total — общее число секций, для которых должны быть созданы индексы
(в случае работы с секционированной таблицей);
• partitions_done — общее число секций, для которых уже выполнено создание индексов
(в случае работы с секционированной таблицей).
Представление довольно подробно показывает ход построения индекса. Особенно стоит отме-
тить, что значения, указывающие на блоки и строки, относятся к отдельным фазам, а не ко все-
му процессу целиком.
Для получения результатов запроса в соседнем сеансе нужно запустить перестроение индекса:
# SELECT
[Link],
now() - a.xact_start AS xact_age,
[Link],
[Link]::regclass AS relation,
p.index_relid::regclass AS index,
[Link], a.wait_event_type ||'.'|| a.wait_event AS wait_event,
[Link],
p.current_locker_pid AS locker_pid,
p.lockers_total ||'/'|| p.lockers_done AS lockers,
pg_size_pretty(p.blocks_total * (
SELECT current_setting('block_size')::int)) AS size_total,
round(100 * p.blocks_done /
greatest(p.blocks_total, 1), 2) AS "size_done,%",
p.tuples_total,
round(100 * p.tuples_done /
greatest(p.tuples_total, 1), 2) AS "tuples_done,%",
p.partitions_total ||'/'|| round(100 * p.partitions_done /
greatest(p.partitions_total, 1), 2) AS "parts_total/done,%",
[Link]
FROM pg_stat_progress_create_index p
INNER JOIN pg_stat_activity a ON [Link] = [Link]
ORDER BY now() - a.xact_start DESC;
-[ RECORD 1 ]------+--------------------------------------------------
pid | 550142
xact_age | 00:00:02.523236
datname | pgbench
relation | pgbench_accounts
index | pgbench_accounts_pkey_ccnew
state | active
wait_event | [Link]
phase | building index: scanning table
locker_pid | 0
lockers | 0/0
size_total | 261 MB
size_done,% | 75.00
tuples_total | 0
tuples_done,% | 0.00
parts_total/done,% | 0/0.00
query | REINDEX INDEX CONCURRENTLY pgbench_accounts_pkey;
На больших объемах данных и при недостаточных ресурсах команда может выполняться про-
должительное время и влиять на производительность конкурентных запросов.
• type — метод ввод-вывода, который используется при работе с данными: FILE, PROGRAM,
PIPE (для COPY FROM STDIN и COPY TO STDOUT) или CALLBACK в случае начальной синхронизации
таблицы при логической репликации;
• bytes_total — размер исходного файла в байтах в случае выполнения COPY FROM. Значение
может быть равно нулю, когда размер определить нельзя;
# SELECT
[Link],
[Link], a.wait_event_type ||'.'|| a.wait_event AS wait_event,
now() - a.xact_start AS xact_age,
[Link], [Link]::regclass AS relation,
pg_size_pretty(pg_relation_size([Link])) AS table_size_total,
[Link]::bigint AS table_tuples_total,
p.tuples_processed,
p.tuples_excluded,
pg_size_pretty(p.bytes_total) AS source_file_total,
pg_size_pretty(p.bytes_processed) AS processed,
CASE WHEN [Link] = 'COPY FROM'
THEN round(100 * p.bytes_processed / greatest(p.bytes_total, 1), 2)
ELSE round(100 * p.tuples_processed / greatest([Link]::bigint, p.tuples_processed), 2)
END AS "done,%",
[Link] || ' ' || [Link] AS command,
[Link]
FROM pg_stat_progress_copy p
LEFT JOIN pg_class c ON [Link] = [Link]
INNER JOIN pg_stat_activity a ON [Link] = [Link]
ORDER BY now() - a.xact_start DESC;
9.5. Представление pg_stat_progress_copy 233
Команда COPY имеет два режима работы — загрузка (COPY FROM) и выгрузка (COPY TO); в выводе
для удобства режим объединен с указанием источника данных. Оценка выполненной работы
(поле done,%) также учитывает оба режима.
Для оценки загрузки можно обойтись полями самого представления: bytes_total показывает
размер исходного файла, из которого выполняется загрузка, а bytes_processed — количество
байтов, прочитанных командой COPY. Следовательно, процент выполненной работы опреде-
ляется отношением прочитанного объема к общему.
С оценкой выгрузки дело обстоит чуть сложнее: представление данных в СУБД отличается
от формата выгрузки (не говоря уже о том, что и в СУБД данные могут по-разному сжиматься,
и форматов выгрузки существует несколько), поэтому итоговый файл будет иметь совершенно
другой размер, чем исходная таблица. В показанном запросе оценка вычисляется как отно-
шение количества обработанных строк tuples_processed к общему количеству строк таблицы
из поля pg_class.reltuples. Однако в этом способе тоже есть недостаток: pg_class.reltuples
обновляется после очистки, и, если она выполнялась достаточно давно, значение не будет со-
ответствовать действительности и оценка окажется неточной. Но опыт показывает, что такая
оценка все равно получается более точной, чем оценка на основе bytes_processed и размера
таблицы.
-[ RECORD 1 ]------+---------------------------------------------------------------------
pid | 223457
state | active
wait_event | [Link]
xact_age | 00:00:18.393571
datname | pgbench
relation | pgbench_accounts
table_size_total | 277 MB
table_tuples_total | 1999223
tuples_processed | 1755368
tuples_excluded | 0
source_file_total | 0 bytes
processed | 169 MB
done,% | 87.00
command | COPY TO PIPE
query | COPY public.pgbench_accounts (aid, bid, abalance, filler) TO stdout;
Резюме
Общая информация
• Кластер потоковой репликации PostgreSQL из двух узлов. Кластер необходим для демон-
страции средств и способов мониторинга репликации.
• Экспортер метрик pgSCV. Это агент мониторинга, собирающий статистику с узлов Post-
greSQL и предоставляющий ее в формате Prometheus. Агент мониторинга является экспе-
риментальным продуктом и не рекомендуется для производственного применения.
• Система мониторинга на основе Victoriametrics и Vmagent. Система мониторинга собира-
ет метрики с агента pgSCV и хранит их в течение двух дней.
• Приложения на основе утилиты pgbench, создающие рабочую нагрузку. В установке запу-
щено несколько экземпляров pgbench, задача которых — создавать постоянную динами-
ческую нагрузку на кластер PostgreSQL.
• Система визуализации Grafana. Используется как фронтенд для системы мониторинга
и позволяет создавать самые разные графики с богатой настраиваемостью.
Зависимости
Убедитесь, что в системе, где будет запускаться тестовое окружение, установлены следующие
инструменты:
Запуск окружения
Выполните следующие команды, чтобы перейти в каталог тестового окружения и затем по-
смотреть справку по доступным целям make:
Цели make предоставляют короткий способ запуска и остановки тестового окружения и под-
ключения к службам в контейнерах. Это будет необходимо для дальнейшей работы. При вы-
полнении целей в самой первой строке будет выводиться команда, которую можно выполнить
без использования Makefile.
$ make up
При первом запуске выполняются сборка контейнеров, инициализация служб и загрузка дан-
ных в тестовую базу (~300 МБ). Процедура может занять некоторое время, оно зависит от про-
изводительности устройства.
Общая проверка работоспособности 237
После инициализации следует проверить готовность окружения к работе. Первым делом необ-
ходимо выяснить общее состояние контейнеров и сервисов:
$ make ps
docker-compose ps
NAME COMMAND SERVICE STATUS PORTS
playground-app1-1 "docker-entrypoint.s…" app1 running 5432/tcp
playground-app2-1 "docker-entrypoint.s…" app2 running 5432/tcp
playground-app3-1 "docker-entrypoint.s…" app3 running 5432/tcp
playground-app4-1 "docker-entrypoint.s…" app4 running 5432/tcp
playground-grafana-1 "/[Link]" grafana running [Link]:3000->3000/tcp
playground-pgscv-1 "pgscv" pgscv running [Link]:9890->9890/tcp
playground-primary-1 "docker-entrypoint.s…" primary running 5432/tcp
playground-standby-1 "/docker-entrypoint.…" standby running 5432/tcp
playground-victoriametrics-1 "/victoria-metrics-p…" victoriametrics running 8428/tcp
playground-vmagent-1 "/vmagent-prod -prom…" vmagent running 8429/tcp
Все контейнеры должны быть в состоянии running (поле STATUS). Обе службы БД должны по-
казывать готовность принимать клиентские подключения. Репликация между узлами также
должна работать. Проверить журнал сообщений основного узла можно так:
$ make primary/logs
...
primary_1 | 2022-03-03 08:57:44 UTC [1] LOG: listening on IPv4 address "[Link]", port 5432
primary_1 | 2022-03-03 08:57:44 UTC [1] LOG: listening on IPv6 address "::", port 5432
primary_1 | 2022-03-03 08:57:44 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.[Link].5432"
primary_1 | 2022-03-03 08:57:44 UTC [63] LOG: database system was shut down at 2022-03-03 08:57:44 UTC
primary_1 | 2022-03-03 08:57:44 UTC [1] LOG: database system is ready to accept connections
$ make standby/logs
...
standby_1 | 2022-03-03 08:57:50 UTC [24] LOG: consistent recovery state reached at 0/11000100
standby_1 | 2022-03-03 08:57:50 UTC [1] LOG: database system is ready to accept read-only connections
standby_1 | 2022-03-03 08:57:50 UTC [28] LOG: started streaming WAL from primary at 0/12000000 on timeline 1
$ make primary/psql
psql (15.0)
Type "help" for help.
postgres=#
238 Приложение. Тестовое окружение
В книге все запросы выполняются на основном узле, если это специально не оговорено.
Grafana
Фронтенд Grafana доступен по адресу [Link]:3000. Логин и пароль для входа не требуются.
Для построения графиков можно использовать режим Explore или создать отдельный набор
панелей и добавлять панели с графиками в него.
Остановка и удаление
$ make down
Если тестовое окружение больше не нужно, можно остановить его и удалить все связанные
с ним компоненты:
$ make destroy
Предметный указатель
A current_setting 134
address 42
D
ANALYZE 198, 200, 222–224
database 42, 60, 77
application_name 33, 44
DataGrip 17, 23
archive_command 167, 171
DBeaver 23
archiver 21, 167–168
dblink 103
archive_timeout 168
deadlock_timeout 55, 68
atop 29
default_statistics_target 222
autovacuum 203–204
DELETE 107, 109, 196
autovacuum launcher 22, 196, 200
delta 122
autovacuum worker 22, 197
docker 236
autovacuum_freeze_max_age 204–205, 208
docker-compose 236
autovacuum_max_workers 214
dstat 30
autovacuum_naptime 22, 199–200, 215
autovacuum_vacuum_scale_factor 198 E
autovacuum_vacuum_threshold 198 END 30, 44, 67
autovacuum_work_mem 18, 217–218 EXPLAIN 91, 161, 222
B F
background writer 20, 22, 149 false 170
BEGIN 30, 44 FPI 151, 162
bgwriter_lru_maxpages 150, 154 fsync 152
bgwriter_lru_multiplier 150 full_page_writes 94, 151
buffers_backend 154 G
bytea 125 getrusage 72, 89
C Grafana 235, 238
CHECKPOINT 151–152 H
checkpointer 20, 22, 149 hot_standby_feedback 178, 192, 206
checkpoint_timeout 151, 163 HTAP 214
clock_timestamp 58 htop 29
CLUSTER 221, 226–227
I
COMMIT 30, 38, 67, 161
idle_in_transaction_session_timeout 38, 58
compute_query_id 91
increase 47, 97
convert_from 125
initdb 100–101
COPY 203, 221–222, 231–233
INSERT 107, 109, 196
count 215
iostat 30
CREATE INDEX 18, 198, 221, 229
CREATE INDEX CONCURRENTLY 229–230 K
cron 58, 196 Kubernetes 46, 56
240 Предметный указатель
pg_ls_logdir 124–126 state 34, 49, 51, 53–55, 60, 65, 214, 226
pg_ls_logicalmapdir 124, 127 state_change 34, 63, 65
pg_ls_logicalsnapdir 124, 127 usename 33, 40
pg_lsn 178 usesysid 34
pg_ls_replslotdir 124, 128 wait_event 34, 51, 66, 214
pg_ls_tmpdir 124, 127 wait_event_type 34, 51, 53, 55, 60, 66,
pg_ls_waldir 124, 126–127 214
pg_monitor 126–128 waiting 51
pg_namespace 104 xact_start 34, 59–60, 214
pg_prepared_xacts 206 pg_stat_all_indexes 110
pg_read_binary_file 125 pg_stat_all_tables 110, 198, 200
pg_read_file 117, 125 pg_stat_archiver 167, 170
pg_read_server_files 117 pg_stat_bgwriter 150
pg_relation 105 pg_stat_database 31, 37, 39–40, 44–47, 107,
pg_relation_filenode 123 110, 112, 114, 139, 143–149
pg_relation_filepath 123, 132 active_time 39
pg_relation_size 117, 119–120, 232 blk_read_time 139–140
pg_repack 213 blks_hit 139
pg_replication_slots 176, 183–186 blks_read 139
xmin 184–185, 192, 206 blk_write_time 139–140
pg_restore 231 checksum_failures 115
pgSCV 235 checksum_last_failure 115
pg_shmem_allocations 130, 135–136 conflicts 115, 191–192
pg_size_bytes 118 deadlocks 55, 115
pg_size_pretty 118 idle_in_transaction_time 39
pg_squeeze 213 numbackends 37
pg_stat_activity 31–32, 34–40, 50–51, 58–60, sessions 38, 48
64–66, 91, 133, 177, 213, 215–217, sessions_abandoned 38, 48, 115
219, 221, 223, 226–227, 232 sessions_fatal 38, 115
application_name 33 sessions_killed 38, 115
backend_start 33, 214 session_time 38
backend_type 33, 41, 214–215 state 50
backend_xid 35, 206 stats_reset 39, 145
backend_xmin 35 temp_bytes 145–146
client_addr 33, 40 temp_files 145
client_port 33 tup_deleted 107
datid 34 tup_fetched 107
datname 33, 40, 214 tup_inserted 107
leader_pid 34 tup_returned 107
pid 34, 213 tup_updated 107
query 34, 214 xact_commit 38, 44
queryid 34, 91 xact_rollback 38, 44, 114–115
query_start 34, 59–60 pg_stat_database_conflicts 173, 176, 191
242 Предметный указатель
postgres_database_conflicts_total 115
S
postgres_database_deadlocks_total 115 shared_buffers 134
postgres_database_sessions_total 115 shared_preload_libraries 73–74
postgres_database_size_bytes 121 startup 21, 175, 178–179, 182
postgres_database_temp_bytes_total 146 state 54, 60
postgres_database_tuples_deleted_total 108 statement_timeout 145
stats 43
postgres_database_tuples_fetched_total 108
stats collector 23
postgres_database_tuples_inserted_total 108
synchronous_commit 67, 161, 164, 174, 179
postgres_database_tuples_returned_total 108
postgres_database_tuples_updated_total 108 T
postgres_database_xact_rollbacks_total 115 temp_buffers 18, 144
template0 101, 103
postgres_fdw 103
template1 102–103
postgres_index_size_bytes 121
temp_tablespaces 148–149
postgres_service_settings_info 42 text 125
postgres_shared_buffers_all_usage_bytes 135 TOAST 106
postgres_statements_calls_total 80 top 29
postgres_statements_query_info 77 Top-K 78, 112
postgres_statements_time_seconds_total 77 topk_avg 78
track_activity_query_size 34
postgres_table_idx_tup_fetch_total 113
track_commit_timestamp 137
postgres_table_seq_tup_read_total 113
track_functions 95
postgres_table_size_bytes 121 track_io_timing 80, 86, 139
postgres_table_tuples_deleted_total 113 track_utility 46
postgres_table_tuples_hot_updated_total 113 track_wal_io_timing 162
244 Предметный указатель
true 167, 170 Блокировка 30, 34–35, 51, 68, 213, 226
TRUNCATE 68 дерево 65
type 60 избежание 120
Буферный кеш 19, 130
U вытеснение 132, 150
UPDATE 107, 109, 196
грязный буфер 132, 150, 154
user 42, 60, 77
закрепление 132
V эффективность 139
VACUUM 18, 196, 198, 200–201, 214, 222 Бэкенд 17, 29, 33, 41
VACUUM FREEZE 203–204
В
VACUUM FULL 212–213, 221, 226–228
Ввод-вывод 86, 129, 139
vacuum_failsafe_age 209
синхронизация 152, 164
vacuum_freeze_min_age 204
Версия строки 105, 196
vacuum_freeze_table_age 204
Взаимоблокировка 55
Victoriametrics 235
Vmagent 235
Г
vmstat 30
Горизонт
W видимости 35, 178, 185, 207
wal_block_size 159 заморозки 203, 205–206
wal_buffers 161–162
Ж
wal_keep_segments 183
Журнал сообщений 21, 56, 125, 147, 187,
wal_keep_size 183, 185
216
walreceiver 21, 174–176, 178–179, 181–182
Журнал транзакций 18, 20, 126, 150, 157,
wal_segment_size 159
174
walsender 21, 175–179, 187–188, 192, 206,
архивирование 21, 166–172
224–225
воспроизведение 175
wal_sync_method 68, 162
walwriter 161, 164–165
З
work_mem 18, 20, 144–145
Закрепление буфера 132
А Заморозка 202
Автоочистка 22, 196–220, 222 Запрос 17, 30, 33–34, 71
длительность 214 PromQL 77
откладывание 203, 206 ввод-вывод 86, 143
срабатывание 197 длительность 59, 79, 84
фазы 217 журналирование 165
Активность 27 исполнение 79
Архивирование журнала 21, 166–172 метаданные 75
нормализация 74
Б планирование 17, 76, 78
База данных 37, 44, 103, 107, 119
ввод-вывод 139 И
Блок, см. Страница Избыточный доступ 110
Предметный указатель 245
Мониторинг PostgreSQL
ООО «Бумба»
108811, г. Москва, вн.тер.г. поселение Московский,
22-й км Киевского ш., двлд. 4, стр. 5
Тел.: +7 (977) 586-38-56
email: info@[Link]
[Link]