Ситуация знакомая почти каждому, кто дежурил у боевой базы. Вчера с двух до четырёх часов дня всё тормозило, сейчас работает нормально, а объяснить, что это было, нужно сегодня.
Дашборд показывает графики за тот период, и по ним видно, что было плохо. Но главный вопрос он не закрывает: на что база потратила время. Лог медленных запросов ближе к делу, однако он показывает отдельные выбросы — те, что перевалили за порог, — и ничего не говорит про их долю в общей картине. Запрос, который сам по себе быстрый, но выполнялся сто тысяч раз, в этот лог даже не попадёт.
Отвечает на такой вопрос отчёт нагрузки за период. В мире Oracle эту роль играет AWR, и многие знают её именно оттуда. В PostgreSQL то же самое делают расширения pg_profile и pgpro_pwr — второе идёт в сборках Postgres Pro.
Откуда берётся отчёт за период
Все нужные числа PostgreSQL считает сам и без всяких расширений: pg_stat_statements копит статистику по запросам, семейство pg_stat_* — по таблицам, индексам и базам.
Загвоздка в том, что счётчики там накопительные. Они растут с момента запуска сервера, и в них лежит сумма за всё время сразу. Посмотреть на такой счётчик — всё равно что посмотреть на одометр машины: пробег видно, а сколько проехали вчера — нет.
Отсюда и устройство инструмента. Он периодически вызывает take_sample() и сохраняет состояние счётчиков в свою схему — это снимок. Отчёт за интервал строится функцией get_report(start_id, end_id) и представляет собой разницу между двумя снимками: сколько накрутилось между ними. Есть и get_diffreport() — она сравнивает сразу два периода, чем удобно сличать вчерашний пик с обычным днём неделей раньше.
Чтобы отчёт был полным, включают несколько вещей:
pg_stat_statements— обязательно, без него не будет разделов про запросы;pg_wait_sampling— формально необязательно, но именно он даёт разделы про ожидания. Без него будет видно, сколько времени ушло, но не на чём система стояла;pg_stat_kcache— по желанию, добавляет процессорное время и обращения к файловой системе;- в конфигурации
track_io_timing = on, иначе все времена ввода-вывода останутся нулями.
Снимки обычно снимают раз в полчаса или чаще. Частота — компромисс: чем реже, тем сильнее короткий всплеск усредняется внутри интервала и теряется; чем чаще, тем больше места и накладных расходов.
Это дополнение к обычному мониторингу, а не замена ему. Мониторинг отвечает на вопрос «плохо ли прямо сейчас», отчёт — «куда ушло время за те два часа».
В каком порядке читать
Отчёт большой, и читать его подряд бессмысленно — утонешь на второй странице. Работает движение сверху вниз, от масштаба к виновнику.
Сначала масштаб. Раздел со статистикой по базе: сколько было транзакций, сколько прочитано и изменено строк, сколько ушло во временные файлы и в журнал. Здесь вы отвечаете на первый вопрос — период вообще был необычным по нагрузке или нагрузка была штатной, а плохо стало по другой причине. Разница принципиальная: «пришло вдвое больше запросов» и «запросов столько же, но каждый втрое медленнее» — совершенно разные расследования.
Потом ожидания. Разделы про типы ожиданий и топ событий показывают, во что упирается система:
| Что преобладает | О чём это говорит |
|---|---|
IO | Читаем с диска: рабочий набор не помещается в память или не хватает индекса |
Lock | Блокировки: длинные транзакции, конкурирующие обновления одних строк |
LWLock | Внутренняя конкуренция — часто буферы, журнал, распухшие таблицы |
CPU (ожиданий нет) | Упираемся в вычисления: тяжёлые сортировки, обработка большого объёма строк |
Client | База ждёт приложение — обычно долгие транзакции с походами во внешние сервисы |
Затем виновники. Топы SQL: по суммарному времени, по числу выполнений, по чтению с диска, по временным файлам, по объёму журнала. Порядок здесь важен — сначала суммарное время, всё остальное потом.
Дальше объекты. Топ таблиц по последовательному чтению, по изменениям, по числу мёртвых строк, статистика вакуума.
И в конце настройки. Отчёт показывает параметры сервера, а сравнительный отчёт — что между периодами изменилось. Иногда всё расследование заканчивается строчкой «кто-то тронул work_mem».
Главная ловушка: среднее вместо суммарного
Самая частая ошибка чтения — отсортировать запросы по среднему времени и смотреть верхние строки.
Наверху окажется какой-нибудь ночной отчёт: выполняется дважды в сутки, каждый раз по восемь секунд. Выглядит внушительно, а к дневным тормозам отношения не имеет — за сутки он занял шестнадцать секунд.
Смотреть надо на суммарное время — долю от общего времени базы. Запрос, который отрабатывает за четыре миллисекунды, но вызывается два миллиона раз, съедает больше двух часов процессорного времени. Он и делает базе плохо, а по среднему времени стоит в самом низу списка.
Из этого следует простое разделение диагнозов.
Если суммарное время большое и среднее большое — перед вами тяжёлый запрос. Дальше смотрят план через EXPLAIN, ищут недостающий индекс, переписывают запрос.
Если суммарное время большое при крошечном среднем и огромном числе вызовов — проблема не в базе, а в приложении. Скорее всего, запрос выполняется в цикле: достаём заказы списком, а потом для каждого отдельно идём за покупателем. Никакой индекс это не вылечит, запрос и так быстрый. Лечится пакетной выборкой на стороне сервиса.
Второй случай встречается чаще первого и почти всегда приезжает из кода.
От чисел к диагнозу
Дальше — типовые картины, которые складываются из этих разделов.
Много чтения с диска, ожидания в IO, средний запрос вырос. Данные читаются с диска, а не из кэша. Причин обычно три: рабочий набор перерос память; пропал индекс, и база пошла читать таблицу целиком — это видно по топу таблиц; либо таблица распухла, и строк столько же, а страниц втрое больше.
Большой объём временных файлов у нескольких запросов. Сортировки и соединения не поместились в память и ушли на диск. Лечится увеличением work_mem — осторожно, он выделяется на операцию, а не на соединение, — уменьшением выборки или индексом, который сразу отдаёт нужный порядок.
Ожидания Lock и всплеск времени на простых обновлениях. Идёт конкуренция за одни и те же строки либо кто-то держит длинную транзакцию. Смотреть надо не на сам медленный запрос, а на того, кто его держит: обычно это сессия, которая давно ничего не делает, но не закрыла транзакцию. Разбор — в статье про блокировки.
Растёт число мёртвых строк, автовакуум отстаёт. Видно по разделу вакуума: доля мёртвых строк увеличивается, дата последней уборки старая. Дальше по цепочке — распухание, чтение с диска, деградация планов. Что с этим делать, разобрано в статье про vacuum.
Один запрос с огромным объёмом журнала. Обычно это массовое обновление: UPDATE по всей таблице, миграция данных, перестроение. Такое разбивают на порции — и заодно проверяют, не отстала ли реплика, пока это ехало.
Транзакций стало вдвое больше, а время каждой не изменилось. База ни при чём: выросла нагрузка. Дальше вопрос про ёмкость, пул соединений и кэширование, а не про индексы.
Чего отчёт не покажет
У метода есть границы, и знать их важнее, чем помнить названия разделов.
Плана конкретного запроса. Отчёт называет виновника, но не объясняет, почему тот медленный. Следующий шаг всегда один — EXPLAIN (ANALYZE, BUFFERS).
Разных планов одного запроса. Запросы в статистике нормализуются: параметры заменяются плейсхолдерами, и все выполнения слипаются в одну строку. Запрос, который для одного клиента идёт по индексу за миллисекунду, а для другого читает половину таблицы, будет одной строкой со средним по больнице.
Того, что не поместилось. У pg_stat_statements есть предел на число хранимых запросов, и когда их становится больше, редкие вытесняются. Если отчёт предупреждает об этом — часть картины потеряна, и предел стоит поднять.
Событий короче интервала. Тридцатисекундная блокировка внутри часового окна растворится в средних без следа. Для такого нужны либо частые выборки состояния сессий, либо обычный мониторинг.
Как этим пользоваться
Отчёт приносит пользу не в момент аварии, а как привычка.
Держите эталон. Снимите отчёт за нормальный рабочий день и сохраните. Без него любое число не с чем сравнить: 40% времени базы на одном запросе — это авария или так было всегда?
Сравнивайте периоды, а не читайте один отчёт. Сравнительный отчёт между «сейчас» и «неделю назад» отвечает на вопрос быстрее, чем чтение обоих по отдельности.
Смотрите отчёт после крупных изменений. Вышел релиз, проехала миграция, поменяли параметр — через день загляните в топ запросов. Так регрессия находится за неделю до того, как о ней сообщат пользователи.
Не выбрасывайте историю. Снимки занимают немного, а возможность посмотреть, каким запрос был три месяца назад, окупается на первом же разборе.
Если хочется понять, из каких именно запросов складывается каждый раздел, — это разобрано в статье «Отчёт нагрузки своими руками»: там по каждому разделу дан запрос, который его порождает.
Коротко
- Отчёт нагрузки — это разница между двумя снимками накопительной статистики; в Oracle ту же роль играет AWR, в PostgreSQL —
pg_profileиpgpro_pwr. - Нужны
pg_stat_statementsиtrack_io_timing; ожидания появляются только сpg_wait_sampling, процессорное время — сpg_stat_kcache. - Порядок чтения: масштаб нагрузки → ожидания → топы SQL → объекты схемы → настройки и их изменения.
- Сортировать надо по суммарному времени, а не по среднему: тысяча мелких запросов дороже одного тяжёлого отчёта.
- Большое среднее время лечится планом и индексами; огромное число вызовов — изменениями в приложении.
- Отчёт называет виновника, но не показывает план: следующий шаг всегда
EXPLAIN (ANALYZE, BUFFERS). - Одиночный отчёт почти бесполезен без эталонного периода — держите базовый снимок нормального дня.
Что почитать дальше
- Отчёт нагрузки своими руками — какими запросами получается каждый раздел отчёта.
- Мониторинг PostgreSQL — что смотреть в реальном времени и какие метрики держать на дашборде.
- EXPLAIN и планы запросов — следующий шаг после того, как виновник найден.
- VACUUM и распухание — почему таблица растёт, а строк не прибавляется.