Я хочу поделиться своим опытом использования Zabbix для анализа проблем с производительностью PostgreSQL, используя расширение pg_stat_statements.

pg_stat_statements

Что такое Zabbix, объяснять, я думаю, не нужно, раз вы уже зашли в эту статью. Пару слов про pg_stat_statements. Это расширение (extension) PostgreSQL, которое поставляется в пакете contrib (например, postgresql14-contrib) вместе с другими официальными пакетами из репозитория PostgreSQL. Для того, чтобы оно работало, необходимо прописать загрузочный модуль в shared_preload_libraries, после чего этот модуль начинает собирать статистику (метрики), сгруппированную по SQL-запросам (query). Статистика разнообразна: длительность выполнения запроса; использование запросом различных ресурсов, например, различных буферов в памяти. Такие запросы как SELECT, UPDATE, INSERT и DELETE он пытается преобразовывать: удаляет содержащиеся в них данные, потом ищет похожие и объединяет их статистику.

Всё это позволяет выявлять различные проблемы, связанные с производительностью. Например, можно использовать это расширение для поиска наиболее долгоиграющих запросов (поиск по max_exec_time); запросов, создающих наибольшую нагрузку на сервер (поиск по total_exec_time); а потом можно будет использовать другие инструменты (EXPLAIN ANALYZE или auto_explain) для анализа этих запросов. Что касается буферов памяти, то тут можно смотреть на метрики shared_blks_*, они соответствуют конфигурационной опции shared_buffers и позволяют выяснить, когда она недостаточная. Метрики local_blks_* соответствуют конфигурационной опции temp_buffers, а метрики temp_blks_* — опции work_mem. Есть ещё конфигурационная опция, тоже отвечающая за размер буферов памяти, maintenance_work_mem, но я пока не смог выяснить, можно ли её нехватку заметить по метрикам pg_stat_statements, и если да, то по каким. Если кто знает, пишите в комментариях.

Метрики по нехватке shared_buffers (кэш ОЗУ) можно посмотреть и другим способом, в pg_stat_database. В pg_statio_user_tables, pg_statio_user_indexes и pg_statio_user_sequences отдельно метрики по использованию кэша для таблиц, индексов и последовательностей. Эту статистику уже собирает официальный PostgreSQL плагин для второго агента Zabbix. Pg_stat_statements может быть интереснее тем, что помимо информации по использованию shared_buffers даёт информацию по использованию temp_buffers и work_mem. А ещё его можно настроить таким образом, чтобы в эту статистику попадала только работа приложения, а не служебных запросов, например от Zabbix.

Для того, чтобы получить доступ к этим метрикам, нужно в любой базе данных создать расширение pg_stat_statements (не нужно создавать в каждой), тогда там появится view pg_stat_statements со всеми запросами. Обычный пользователь может видеть только свои запросы, а чтобы видеть все, нужен либо суперпользователь, либо, начиная с 10-го PostgreSQL, нужно быть в роли pg_read_all_stats или pg_monitor.

И тут появляются две проблемы, ради решения которых и пришлось прикручивать Zabbix. Первая: оптимизацией запросов к базам данных в современном мире сейчас занимаются веб-программисты (раньше для этого была специальная должность DBA). А у веб-программистов нет прямого доступа к SQL-серверу прода. Использование Zabbix в качестве промежуточного звена позволит дать им ограниченный доступ к метрикам запросов в эксплуатационной системе, без прямого SQL-доступа.

Вторая проблема: если установить pg_stat_statements «на всякий случай» и потом обратиться к нему, когда в эксплуатации случился какой‑то «ахтунг», то от этого будет мало пользы. Там окажется статистика по всем запросам, допустим, за полгода. И найти среди них то, что создаёт проблему прямо сейчас или на этой неделе, будет весьма затруднительно. Для того, чтобы метрики были актуальны и соответствовали действительности, необходимо регулярно сбрасывать статистику, например раз в сутки в полночь. Сохраняя при этом информацию за предыдущие сутки, скажем, о наиболее проблемных запросах. Тогда можно будет посмотреть, например, как менялось распределение проблемных запросов в течение недели. А также, если понадобится срочно решать внезапно появившуюся проблему, можно будет быть уверенным, что вся информация, которая в текущий момент находится в pg_stat_statements, содержится только за текущий день. И тут тоже на помощь приходит Zabbix: он будет в полночь сохранять метрики и после этого сбрасывать (reset) статистику.

Проблемы совместимости pg_stat_statements

Предлагаемое мной решение сможет работать в PostgreSQL версий 10-16 (впрочем, на 16 ещё не тестировал), а также в версиях 9.5 и 9.6 c оговорками. Конечно, в такой современной компании, как наша, такие древние версии давно уже не используются, но они могут использоваться в каких-то других, «плохих» компаниях.

Первая проблема совместимости, на которую стоит обратить внимание, возникла в 13-й версии PostgreSQL. Если раньше (до 13-й) статистика собиралась по полной длительности выполнения запроса, учитывая время, затраченное и планирование, и на выполнение, и записывалось в поле, например, max_time, то начиная с 13-й версии считается уже отдельно метрика длительности планирования max_plan_time и отдельно — длительности исполнения max_exec_time. Причём по умолчанию длительность планирования не собирается (всегда 0). Чтобы её собирать, нужно указать специальную конфигурационную опцию pg_stat_statements.track_planning. Для моей задачи это означает, что придётся писать разные SQL-запросы для 13-й (и старше) и для 12-й (и младше) версий.

Вторую проблему не назвать проблемой совместимости, но важно её учесть. Статистика сбрасывается вызовом функции pg_stat_statements_reset. Начиная с 12-й версии PostgreSQL можно указывать для этой функции базу данных, для которой следует сбросить статистику, в то время как до 12-й версии эта функция сбрасывает статистику для всех баз данных сразу. Это не проблема совместимости, потому что начиная с 13-й можно продолжать по-старинке вызывать pg_stat_statements_reset() без параметров и сбрасывать всю статистику. Но для Zabbix эта функциональность имеет важное значение. Разумеется, правильным было бы помещать статистику pg_state_statements в элементы данных (item) Zabbix, которые привязаны к конкретной базе данных. И потом, например, давать к ним доступ конкретным веб-программистам, которые отвечают за этот сервис. Начиная с 12-го PostgreSQL сделать это в Zabbix легко: единым скриптом собираешь метрики для нужной базы и тут же сбрасываешь статистику. А до 12-го всё сложнее: приходится собирать все метрики по всем базам на сервере сразу и сбрасывать статистику для всего сервера. А потом эти метрики надо разложить так, чтобы они оказались в элементах данных, которые относятся к конкретным базам данных.

Что с 9.5 и 9.6? Там нет такой роли как pg_monitor или pg_read_all_stats. Поэтому придётся запускать Zabbix под суперпользователем: либо выдавая суперпользователя для zbx_monitor, либо для этих запросов подключая Zabbix под другим пользователем, у которого эти права есть, например postgres.

В современном Zabbix используется второй агент. И под него существует PostgreSQL плагин, который позволяет оптимизировать доступ к базе данных. Он использует для всех запросов одно и то же подключение (нет необходимости подключаться для каждого запроса), а запросы оформляет в prepared statement, что уменьшает накладные расходы. Это особенно важно, если запросы планируется выполнять часто. Но, к сожалению, этот плагин написали веб-программисты на Go, и поскольку люди они самостийные и творческие, они использовали не стандартную библиотеку libpq, а что-то своё. И поэтому возникло две проблемы.

Первая: этот плагин попросту не работает с PostgreSQL <10. Возможно, мог бы частично работать для каких-то запросов, но они проверяют версию сервера в самом начале и разрывают соединение, если версия меньше 10. В текущей реализации я использую как соединения через psql (библиотека libpq), так и через плагин, там, где это оправданно. При текущей реализации сбор статистики за день сможет работать, а сбор статистики на текущий момент времени, точнее метрики скоростей изменения статистики, в версиях 9.5-9.6 работать не смогут, потому что они реализованы через плагин. Если потребуется, чтобы всё работало с базами 9.5-9.6, то надо будет переписать реализацию через psql. Для этого можно использовать всё тот же второй агент, переходить на первый не нужно.

Вторая проблема: если подключаться через файловые сокеты, то URI для таких соединений для плагина на Go принципиально отличается от аналогичного для библиотеки libpq (например, приложения psql). В libpq для соединения через файловые сокеты достаточно не указать в URI имя хоста, а для Go надо указывать путь в файловой системе к этому файловому сокету. Вопрос «Что лучше?», быть может, спорный и выходит за рамки этой статьи. Но такую разницу точно надо учитывать. И хранить различные URI: один для доступа через psql, другой для доступа через плагин от Zabbix.

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

Подготовка PostgreSQL

Чтобы всё работало, серверы PostgreSQL должны быть определённым образом подготовлены. Для начала, обычные рутины для Zabbix-мониторинга: должен быть заведён пользователь для подключения Zabbix, обычно это zbx_monitor, и ему должны быть выданы нужные права, обычно это роль pg_monitor.

GRANT pg_monitor TO zbx_monitor;

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

CREATE EXTENSION pg_stat_statements;

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

GRANT EXECUTE ON FUNCTION pg_stat_statements_reset TO zbx_monitor;

Как аутентифицировать Zabbix в PostgreSQL? Самый естественный вариант, если агент Zabbix запущен на том же самом сервере, где и база данных, так обычно и делают. В этом случае можно было бы указать, что Linux-пользователь zabbix может заходить в PostgreSQL под SQL-пользователем zbx_monitor без пароля. Аутентификация при этом выполняется самой операционной системой. Для этого надо в pg_hba.conf добавить что-то вроде:

local all zbx_monitor peer map=zbx_monitor

И в файл pg_ident.conf:

zbx_monitor zabbix zbx_monitor

После таких правок не обязательно делать restart, достаточно reload. В некоторых «плохих» компаниях так не заморачиваются и попросту пишут другой способ аутентификации «trust» для локальных пользователей. Когда я впервые стал работать с PostgreSQL в 1999 году, уже тогда это считалось недопустимым и плохим тоном. В нашей компании, разумеется, так никто не делает.

Низкоуровневое обнаружение (Low Level Discovery)

Речь тут пойдёт о таком методе Zabbix как низкоуровневое обнаружение (LLD, Low Level Discovery). Суть его в следующем. В Zabbix создаётся специальное правило LLD, которое тестирует, например, Linux и ищет в нём определенного типа ресурсы. Для найденных ресурсов с помощью прототипов элементов данных (prototype item) создаются элементы данных (item), которые хранят ту или иную метрику ресурса. Так обнаруживаются, например, смонитрованные файловые системы или сетевые интерфейсы. Есть такое правило LLD (discovery rule) и у PostgreSQL плагина Zabbix.

Но это правило LLD у плагина не умеет корректно работать, если на Linux-сервере много серверов PostgreSQL. Оно может работать только с одним сервером (instance). Ему в качестве параметра надо передать URI для подключения к этому серверу, потом он туда заходит, находит все базы данных и для каждой из них создаёт набор элементов данных для хранения метрик. Всё это особенно странно, учитывая, что PostgreSQL всегда умел запускать несколько серверов на одном Linux-сервере. А поддержка этого в пакетах дистрибутивов, я помню, впервые появилась в Debian, очень давно. Не скажу точно, когда, я тогда ещё молодым был.

В нашей фирме это обычная практика, иметь по несколько PostgreSQL серверов на одном Linux сервере. В девелоперовском контуре их может быть несколько десятков на одном сервере, по одному на каждую команду. И почему профессионалы Zabbix это не учли, для меня большая загадка. Впрочем, они и system load делят на количество ядер у процессора, об этом как‑нибудь ниже. Один из способов, как тут можно выкрутится, это создавать в Zabbix «фейковые хосты» на каждый сервер PostgreSQL, и там указывать плагину PostgreSQL в качестве параметра URI к этому серверу. Выглядит нелепо, так делают в каких-то других, «плохих» компаниях, в нашей так никогда никто не делает.

Учитывая всё вышесказанное, появилось желание создать собственное правило LLD для серверов PostgreSQL, лишённое таких недостатков. И оказалось его очень просто сделать. Такие правила LLD прописываются в шаблоне (template), который можно подключить к нужному хосту в Zabbix прямо или опосредованно (через наследование шаблонов). Он будет создавать элементы данных для всех серверов и всех баз данных PostgreSQL, которые он там найдёт.

В конце статьи я приложу шаблон и все файлы. Правило LLD сделано через такую функциональность агента Zabbix как UserParameter. К сожалению, в Zabbix нельзя использовать переносы строк в этой синтаксической конструкции, выглядит это совершенно нечитаемо, особенно на Хабре, где отсутствует перенос длинных строк во вставленном коде. Поэтому читателей я отсылаю к файлу в конце статьи, который нужно будет положить в /etc/zabbix/zabbix_agent2.d/odsamoylov.conf.

Правило LLD Pg_srv

Правило LLD Pg_srv основано на UserParameter с ключом pg.servers (в файле odsamoylov.conf). Это, по сути, shell-скрипт, который ищет файловые сокеты PostgreSQL в директории /var/run/postgresql/, и для тех серверов PostgreSQL, что он находит, заполняет макросы LLD, которые потом будут использоваться в прототипах для создания элементов данных для этого сервера. В этом правиле будут создаваться те элементы данных, которые соответствуют серверу, а не каждой базе данных. Например, это может быть версия сервера или размер WAL директории. Так уж исторически сложилось, что дистрибутивы на основе RedHat и Debian раскладывают файлы PostgreSQL по разным директориям. А здесь редкое исключение: и в RedHat, и в Debian файловые сокеты лежат в одной директории, так что работать будет и там, и там.

Список определяемых макросов LLD и что они значат:

  • {#POSTGRESQL_URI}. Обычный URI для PostgreSQL, который принимают приложения на основе libpq, например psql. Вид: postgresql://hostname:5432. Может использоваться, например, в прототипе триггера, чтобы таким образом указать базу данных, где произошла проблема. И потом к ней будет легко подключиться, просто скопировав этот URI к себе в командную строку для запуска psql. Может использоваться и для проверки сетевых подключений к этой базе данных типа пинга, если, конечно, база будет заранее открыта для таких проверок.

  • {#URI}. Сначала URI (который выше) я использовал и для именования элементов данных внутри Zabbix. Но потом столкнулся с тем, что у Zabbix под именование элемента данных выделено довольно мало места, а написать там хочется много. Поэтому это «укороченный» URI для таких случаев. Будет выглядеть как :5432/. Ни на что не влияет, можно заменить в соответствии со своим чувством прекрасного.

  • {#PG.URI}. Этот URI для Zabbix плагина postgresql. По сути это путь к файловому сокету.

  • {#PG.SERVER_VERSION_NUM}. Для того, чтобы уже на уровне правила LLD можно было бы решать, какие прототипы использовать в зависимости от версии PostgreSQL, сюда я заношу версию сервера в виде show server_version_num;.

  • {#HOSTNAME}. Просто содержимое bash-переменной $HOSTNAME, имя сервера, как определено в Linux. Есть аналогичная переменная, определённая в Zabbix, только там она будет такой, как она определена в конфиге Zabbix. Для моих целей Linux hostname был более информативным. Использовалась в прототипе триггера, который я, правда, удалил... Скажу по-другому: вы сможет использоваться в прототипе триггера, если его напишете.

  • {#PG.PORT}. Порт, на котором находится PostgreSQL-сервер. Используется в том числе для подключения к серверу Zabbix через файловый сокет с помощью UserParameter и psql.

Правило LLD Pg_db

Теперь про то, что актуально для каждой базы данных, например про её размер. В шаблоне правило LLD называется Pg_db, основано на UserParameter с ключом pg.databases. К сожалению, в Zabbix нельзя сделать так. чтобы одно правило LLD определяло другое правило LLD. То есть чтобы правило для обнаружения серверов настраивало бы правило для обнаружения баз данных внутри серверов. Все правила LLD друг от друга независимы. Поэтому этот скрипт повторяет то же самое, что и предыдущий. Сначала анализирует файловые сокеты, определяет, какие серверы в наличии, а потом подключается к каждому из них и запрашивает, какие базы данных в этом сервере существуют. В списке по одной строчке на каждую базу данных. {#POSTGRESQL_URI}, {#URI}, {#PG.URI}, {#PG.PORT}, {#PG.SERVER_VERSION_NUM} такие же, как и в предыдущем скрипте, только к URI добавлено имя базы данных. И отдельно задаётся {#PG.DATABASE} с именем базы данных.

Прототипы элементов данных

Макросы LLD поступают в прототипы элементов данных, из чего получаются элементы данных, которые и будут хранить метрики. Как я уже говорил, есть сложность с тем, что pg_stat_statements у разных версий PostgreSQL заметно отличаются. Поддержку 9.5 и 9.6 я не сделал, да их и разработчики PostgreSQL уже не поддерживают. Хоть добавить их и не сложно, но будет гораздо больше писанины и копипастов. А версии с 10 по 16 можно разделить на две группы по тому, как их можно обрабатывать:

  • Последние версии (с 13 и старше) характерны тем что там статистика по времени разделена на время планирования и время выполнения. Столбцы называются по-другому, и, как следствие, текст SQL-запроса будет другим. Для этих версий можно сбрасывать статистику отдельно для каждой базы данных, что значительно упрощает алгоритм. Если бы все базы были современными, то этим можно было бы и ограничиться.

  • А в группе с 10 по 12 версию общая длительность и планирования, и выполнения, столбы называются по-другому. И статистику сбрасывать приходится сразу для всех баз данных. То есть сначала нужно получить всю статистику на сервер, всю сбросить, а потом разложить её по полочкам, отдельно на каждую базу данных.

Имя каждого прототипа элементов данных будет, например,
{#URI}->pgstmts.day.rows.v13. Сначала идёт {#URI} — это тот самый макрос LLD, который был определён правилом LLD, по сути идентификатор базы данных. После -> идёт аббревиатура pgstmts, это всего лишь сокращение от pg_stat_statements, я сильно экономлю место. Следующее — либо day, либо sec. Day означает, что эта статистика ежедневная, а sec — ежесекундная, по сути секундная скорость, усреднённая на минуту. Такое разделение я поясню позже. Дальше идёт идентификатор метрики, например rows. А затем идёт пояснение, к какой версии этот прототип относится: v13 — для версий PostgreSQL 13 и старше, v12 — для версий 10-12. Такой прототип превратится в название элемента данных, например
:5432/database->pgstmts.day.rows.v13 (где database — это название базы данных).

Для того, чтобы определить, какую версию использовать, применяется такой механизм Zabbix как замещения (overrides) внутри правила LLD. По умолчанию настроено так, что у прототипов с v13 выставлен флаг Обнаружение (Discover), то есть эти прототипы будут использоваться для низкоуровневого обнаружения. А у прототипов с v12 флаг Обнаружения (Discover) по умолчанию убран, они работать не будут. Замещение с названием pgstmts.*.v13 отключает флаг Обнаружение у прототипов v13, если версия PostgreSQL меньше 13. Сделано не через численное сравнение, а через regexp, но смысл тот же. А Замещение с названием pgstmts.*.v12 включает Обнаружение для прототипов с v12, если версия PostgreSQL будет 10-12. Сделано так, что для каждого сервера сработают прототипы либо v12, либо v13, и создадут нужные элементы данных.

Ежедневная статистика (day)

Для версий 13 и выше. Раз в сутки (время выставлено в 00:00) срабатывает элемент данных, полученный из прототипа из правила LLD Pg_db (для каждой базы данных) с именем {#URI}->pgstmts.day.json.v13 (тут в URI входит название базы данных). Конечно, нельзя ожидать, что время выполнения у всех будет ровно 00:00, потому что количество одновременно выполняемых процессов ограничено, но это не важно, нам же не нужна высокая точность в этом вопросе. Этот элемент получает данные из скрипта UserParameter с ключом pgstmts.day.json.v13[port,database]. А скрипт получает информацию из pg_stat_statements по тем метрикам, которые интересны, и после этого, второй командой эту статистику сбрасывает. Сделать это через плагин для postgresql от Zabbix через функциональность CustomQueries невозможно, потому что CustomQueries подразумевает один запрос, который можно упаковать в PREPARED STATEMENT. Два запроса туда не упакуешь. Все нужные данные получаются единым запросом, упаковываются в JSON и записываются в Zabbix как текстовое поле. Но содержимое этого поля не хранится в истории, вместо этого JSON разбирается на части теми элементами данных, которые оформлены как зависимые.

Для версий 10-12 раз в сутки, также в 00:00, срабатывает элемент данных из правила LLD Pg_srv (один на каждый сервер) с именем {#URI}->pgstmts.day.json.v12, вот только в URI имени базы данных нет. Пример такого имени :5432/->pgstmts.day.json.v12. Туда заносится информация, полученная из скрипта UserParameter pgstmts.day.json.v12[port]. Это упакованная в JSON вся нужная информация по всем базам данных сразу, после чего статистика сбрасывается. У Zabbix есть ограничение на размер возвращаемых таким образом данных. И если данные из pg_stat_statements содержат очень длинные запросы (такие обычно генерируют ORM), да ещё по всем базам данных, то случается переполнение буфера (так было) и никакая информация не возвращается. Поэтому предпочтительнее метод, когда статистика собирается отдельно для каждой БД (для версий 13 и выше). Этот JSON с информацией по всем базам данных хранится в виде текстового поля в истории Zabbix 4 часа.

В два часа ночи запускается элементы данных уже из Pg_db, то есть для каждой базы данных. Имя прототипа, {#URI}->pgstmts.day.json.v12, вроде бы такое же, но здесь в URI название базы данных уже входит, например :5432/database->pgstmts.day.json.v12. Эти элементы данных вычисляемого типа (calculated). Они забирают JSON, полученный в предыдущем абзаце, с информацией по всем базам данных, и с помощью механизма Предобработки (Preprocessing) берут оттуда раздел, относящийся к своей базе данных. В результате получается ровно такой же элемент данных JSON с информацией по одной базе данных, который раньше был описан для версии 13, он также не хранится в истории Zabbix и нужен только для того, чтобы его разобрали на части зависящие от него элементы данных.

Ремарка про кибербезопасность. Как я уже говорил, pg_stat_statements убирает данные из запросов типа SELECT, UPDATE, INSERT, DELETE. Но не убирает данные из запросов типа ALTER USER username WITH PASSWORD 'password'. В самом начале тестирования я с удивлением обнаружил, что в Zabbix засветился пароль от пользователя, который никому видеть бы не следовало. Единственное, что я придумал, это отфильтровывать в pg_stat_statements запросы от пользователя postgres. Другой нежелательный пользователь — zbx_monitor: запросы от Zabbix одни из самых медленных, а это не то, что нужно веб-программистам для работы. Да и остальные запросы от служебных пользователей веб-разработчикам видеть ни к чему. Поэтому в UserParameter в скриптах, в которых я забираю данные из pg_stat_statements, я отфильтровываю запросы от нежелательных пользователей (postgres, zbx_monitor, pgbouncer).

Я думал о том, чтобы список таких пользователей оформить в виде параметра. Технически это возможно. Но есть опасность, потому что тогда пользователь, обладающий правами редактировать конфигурацию Zabbix, сможет настроить так, что начнёт видеть запросы от суперпользователя. Поэтому список нежелательных пользователей прописан в коде в конфигурационном файле в UserParameter, и изменить его может только сисадмин с правами редактирования этого файла (обычно root) на этом сервере.

Итак, на этом этапе есть текстовый элемент данных (item) типа JSON, в котором находится вся нужная информация для конкретной базы данных, имя и ключ которого заканчивается либо на v13, либо на v12, но содержимое полностью одинаково. Если бы Zabbix был объектно-ориентированным языком, то можно было бы легко весь последующий код, объекты (items, графики, триггеры и т. д.) сделать универсальным. Но, к сожалению, Zabbix это не объектно-ориентированный язык программирования. Нельзя сделать зависимые объекты так, чтобы они настраивались на JSON-элемент данных v12 или v13, в зависимости от того, который есть. Поэтому пришлось полностью дублировать код, создавать все объекты для v13 и потом их клонировать и переделывать на v12. Если кто придумает, как этого можно избежать и всё сделать более элегантно, напишите в комментариях. При условии, что сами всё сделаете, протестируете и убедитесь, что работает. Я перепробовал разные варианты, и решение для Zabbix v6.0 не нашёл.

Сейчас я распишу, какие есть элементы данных, для v12 и v13 они совпадают.

  • top_max_time — текстовое поле топ-5 запросов по критерию максимальная длительность выполнения.

  • top_total_time — текстовое поле топ-5 запросов по критерию суммарная длительность выполнения. Эти запросы создают максимальную нагрузку на сервер.

  • max_time — максимальная длительность выполнения запроса, выбранная из всех запросов за период с момента сброса статистики. У меня это сутки.

  • mean_time — средняя длительность всех запросов. Толку от этого мало, но может помочь оценивать производительность сервера в каких‑то ситуациях.

  • min_time — минимальная длительность выполнения, выбранная из всех запросов.

  • calls — так это поле называется в pg_stat_statements, тут будет количество запросов в сутки. Резкое увеличение этого параметра может сигнализировать, что что‑то пошло не так.

  • rows — суммарное количество строк, которые запросы возвращают или обрабатывают в сутки. Необычное изменение значения тоже может навести на какие‑то умные мысли, что с базой не так.

  • shared_blks_miss — количество промахов мимо кэша shared_buffers, в процентах.

  • local_blks_miss — количество промахов мимо кэша temp_buffers, в процентах.

  • temp_blks_read — тут количество промахов в процентах не посчитаешь, поэтому абсолютное значение. Говорит о том, что параметр work_mem недостаточен и поэтому PostgreSQL приходится сбрасывать данные на винчестер. Конкретно это значение — количество блоков, прочитанных с винчестера. Если памяти хватает, то всё содержится в памяти, с винчестера чтений не будет.

Немаловажное замечание. Если так получится, что к базе не было вообще никаких запросов, которые бы попали в JSON, то большинство этих метрик будут отсутствовать и рядом с ними будет гореть предупреждение, что при их получении произошла ошибка. Конечно, можно было бы сделать так, чтобы в случае отсутствия записей в эти метрики вставлялись пустые значения (для текстовых полей) или 0 для цифровых. Но тогда база данных у Zabbix будет забиваться ненужными и бессмысленными нулями. А так, когда возникают ошибки по причине того, что вставлять на самом деле нечего, и место в базе Zabbix заниматься не будет.

Посекундная статистика (sec)

В Zabbix есть термины история и динамика (history и trends). В истории хранится история как она есть, которая собирается, например каждую минуту. В динамике хранится усреднённая статистика за большие периоды времени, например по суткам, и хранится дольше. История хороша для того, чтобы быстро решать внезапно появившиеся проблемы. Динамика хороша для оценки тенденций, анализа и прогнозов потребления ресурсов при нормальной работе базы данных.

Та статистика, которую я назвал «ежедневная» и которая собирается раз в сутки, это как бы «динамика» в терминах Zabbix. Но для быстрого решения внезапных проблем полезно иметь графики, которые показывали бы текущую ситуацию и позволяли бы быстро её оценивать. Не все метрики из вышеперечисленных можно так показывать, для многих необходимо сбрасывать статистику. Чтобы показывать раз в минуту top_total_time, max_total_time (топ списки запросов), mean_time (среднее время) и т. д., надо будет и статистику сбрасывать раз в минуту. Большие издержки, толку мало. Но в то же время есть такие метрики, для которых характерно увеличение значения каждую минуту. Например, total_time — суммарное машинное время, потраченное на запросы. Zabbix позволяет у такого параметра получить разницу между двумя соседними измерениями и разделить на количество секунд между этими измерениями. Получится скорость или первая производная для этой метрики, усреднённая за минуту (если измерения каждую минуту). Для таких параметров можно строить графики скорости хоть каждую минуту без сброса pg_stat_statements. И они могут быть полезны для быстрой диагностики проблем.

Почему нужны оба способа сбора метрик? Ежедневная статистика полезна тем, что в ней есть метрики, которые иначе не получишь. Она была написана в первую очередь для того, чтобы ежедневно получать топ пять самых долгоиграющих запросов. Секундная статистика полезна тогда, когда что-то идёт не так и надо быстро продиагностировать проблему. Ещё она полезна, когда надо оценить графики в зависимости от времени суток. Например, если проблемы возникают в каком-то узком периоде времени и нужно понять, что там происходит. В общем, одна другую не заменит.

Поскольку секундная статистика снимается часто (раз в минуту) и не требует сброса, то её можно и нужно делать через такую функциональность PostgreSQL плагина от Zabbix как CustomQueries. Это своеобразный аналог UserParameter, только тут вместо shell-скрипта прописывается непосредственно SQL-запрос, который выполняется оптимальным образом, без постоянного запуска psql, переподключений к БД и т. д. Поддерживается постоянное подключение, и в нём все запросы выполняются в виде PREPARED STATEMENT. Нужные запросы лежат в файлах pgstmts.sec.json.v12.sql и pgstmts.sec.json.v13.sql. Вызываются они в прототипах элемента данных {#URI}->pgstmts.sec.json.v12 и {#URI}->pgstmts.sec.json.v13, в правиле LLD Pg_db, раз в минуту. Потом этот JSON точно так же разбирается на части, причём у этих метрик вычисляется скорость изменения в секунду описанным выше способом. Получаются следующие элементы данных:

  • calls — количество запросов в секунду;

  • load — аналог system load, об этом ниже;

  • row — количество обработанных строк в секунду;

  • local_blks_hit — количество попаданий в кэш temp_buffers;

  • local_blks_read — количество чтений мимо кэша temp_buffers;

  • shared_blks_hit — количество попаданий в кэш shared_buffers;

  • shared_blks_read — количество чтений мимо кэша shared_buffers;

  • temp_blks_read — количество чтений мимо work_mem.

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

  • local_blks_miss — доля промахов для temp_buffers;

  • shared_blks_miss — доля промахов для shared_buffers.

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

Load

Маленькая ремарка об истории этой метрики. Load, она же загрузка, это, наверное, самая первая метрика оценки загруженности Linux сервера с незапамятных времен. У неё очень простой смысл: это количество активных процессов на текущий момент времени. В load попадают процессы, которые активно работают, потребляя процессор, или находятся в ожидании поступления информации с винчестера. Возможно, потому, что раньше микросхем «прямого доступа к памяти» не было и заносить данные с винчестера в ОЗУ приходилось тому же самому многострадальному процессору. А вот процессы, ожидающие поступления информации по сети, в load не попадают. Так исторически сложилось. Возможно, я где-то тут смог и напутать, но только слегка, если кто более сведущ, пишите в комментариях.

Сама по себе load в чистом виде никогда не использовалась, применялись только её усреднённые значения за одну, пять и пятнадцать минут. Что позволяло, сравнивая три числа, понять, является ли высокая загрузка пиковой или постоянной. Её можно было посмотреть в выводе команды uptime, top или в файле /proc/loadavg. Там она фигурирует как load average, но так её никто не называл, потому что average, очевидно, не является частью имени, а всего лишь характеристика того, что load усреднена. Говорили просто «какой лоад?» или «какой был лоад?» Поскольку никаких других метрик load на тот момент не было, то пояснений и не требовалось.

Потом появилась вторая знаковая метрика, у которой было нормальное собственное имя — CPU Load. Вычислялась как загрузка каждого ядра процессора в процентах. И сразу стало понятно, что между старой доброй load и CPU Load есть какая-то связь. Если load равна, например 3, то CPU Load покажет какую-то нагрузку на примерно трёх ядрах процессора. В идеале, если бы быстродействие процессов зависело только от CPU, то при load 3 можно было бы ожидать, что CPU Load покажет 100 % нагрузку на ровно трёх ядрах процессора. Но такое редко бывает. Например, когда купили новый сервер (на предыдущей работе), его нужно было протестировать, и я по знакомству запустил на нём задачку из теорфизики, что-то посчитать про металлический водород. Тогда да, все ядра процессора были на 100 % и load был ровно столько, сколько ядер у процессора.

Но в обычной жизни, например при работе с базой данных, быстродействие лимитирует не процессор, а память или винчестер. CPU Load вполне может показывать низкую нагрузку на каждое ядро, при том что load будет немаленьким. Это не означает, что проблем нет и "процессор справляется", ровно наоборот: проблемы есть, и либо винчестер, либо ОЗУ не справляется, например, сервер свопит свою память, или данные не помещаются в кэш и читаются с винчестера. И поэтому делить load на количество ядер процессора — несусветная глупость, которую можно увидеть и в Zabbix. Если у load есть совершенно чёткий физический смысл (написал выше), то у поделенного на число ядер процессора никакого смысла нет. Даже если лимитирует только процессор, то есть load 1 и одно из ядер загруженно на 100 %, какой смысл будет у поделенного load, равного 0,041666666666667?

После того, как появился CPU Load, мне стало интересно, а нет ли у load (также называемого load average или LA) какого-то реального названия? Повторюсь, average частью названия быть не может, CPU Load может тоже как-то усреднённо вычисляться, не называть же его CPU Load average? И где-то на просторах интернета я нашёл подходящее название: System Load, или загрузка системы. Возможно, оно так и называлось, но поскольку конкурентов долгое время не было, то слово system из названия выпало, для краткости.

System Load — полезная метрика, которая очень хорошо, гораздо лучше, чем CPU Load, показывает порог, когда сервер теряет производительность, начинает подвисать, лагать, а порой и вовсе отказываться работать. Хотя такой порог у каждого сервера и нагрузки свой, современные машины с отключённым свопом и на твердотельных винчестерах гораздо лучше переносят высокие значения System Load, подвисать начинают гораздо позже. И я захотел найти аналог этой метрики, но для баз данных. В идеале, чтобы можно было видеть, какой вклад каждая из баз данных привносит в System Load.

Так появилась метрика {#URI}->pgstmts.sec.load.v13, которая, по сути, является разницей суммарного времени, потраченного на запросы в секундах, поделенного на количество секунд между двумя измерениями. Этакий аналог System Load average 1 min для базы данных, основанная на pg_stat_statements. Не без недостатков: вся информация о потраченном времени приходит только в момент завершения запроса. Поэтому на коротких запросах, у которых длительность выполнения значительно меньше минуты, работает хорошо. Если же запросы превышают минуту, то длительность их загрузки не распределяется по длительности реального выполнения, а создаёт высокие пики в момент завершения запроса. Это, конечно, недостаток, хотя и польза тоже есть: очень хорошо визуально заметны запросы, выполняющиеся дольше одной минуты. Поэтому я и не стал обрезать.

Примеры реального использования

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

Load в девелоперовском контуре.
Load в девелоперовском контуре.

Видно, что не смотря на большое количество БД, реальную нагрузку на сервер создают только три базы. С одной стороны, хочется похвалить разработчиков этих сервисов, они большие молодцы, раз тестируют под нагрузкой. Но если поскрести (использовать другие метрики, например, показывающих основные проблемные запросы), то оказалось что не только в тестовой нагрузке было дело. Эти базы содержали реальные проблемы. Например, у той, что на графике изображена тёмно-синей, была очень коварная «несовместимость» перехода на 12-й PostgreSQL, настолько коварная, что она даже официально не объявлена как несовместимость. Про неё подробно рассказывал Олег Бартунов в выступлении про CTE в 12-м PostgreSQL.

Эту проблему удалось своевременно выявить и исправить до того, как она попала в эксплуатацию. Теперь про базу, которая отмечена на графике бледно-голубым. Вот её график до и после оптимизации:

До и после оптимизации.
До и после оптимизации.

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

Ближайшие аналоги и конкуренты

Какую-то похожую информацию может выдать плагин PostgreSQL by Zabbix agent 2. Ключ:

pgsql.queries["{$PG.CONNSTRING}","{$PG.USER}","{$PG.PASSWORD}","{$PG.DATABASE}","{$PG.QUERY_ETIME.MAX.WARN}"]

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

Реальный случай. Сотрудницы одного из наших подразделений жаловались на задержки в работе из-за базы данных. Когда сисадмины смотрели на график максимальной длительности запросов, полученных как раз из этого плагина Zabbix, то было видно, что за ближайшие полгода максимум времени, в течение которого выполнялся бы запрос, был несколько десятков миллисекунд. И сисадмины отвечали одним из двух популярных среди сисадминов ответов всех времён и народов: «Не вижу проблем». Второй популярный ответ всех времён и народов у сисадминов: «У меня всё работает». Но когда я поставил на эту базу pg_stat_statements и пока ещё вручную стал проверять работу, то выяснилось, что вполне бывают запросы, которые выполняются и по 40 минут. А то, что показывает плагин Zabbix, это, конечно, красивые графики, которые и выглядят правдоподобно, и начальству нравятся, и создают иллюзию работы, и иллюзию контроля над проблемами. Вот только не имеют никакой связи с действительностью.

Именно после этого случая я и решил придумать систему мониторинга запросов в Zabbix на основе pg_stat_statements.

Файлы

pgstmts.sec.json.v12.sql

WITH su AS (
        SELECT usesysid FROM pg_user
        WHERE usename IN ('postgres', 'zbx_monitor', 'pgbouncer'))
SELECT jsonb_build_object(
                'temp_blks_read', sum(temp_blks_read),
                'shared_blks_read', sum(shared_blks_read),
                'shared_blks_hit', sum(shared_blks_hit),
                'local_blks_read', sum(local_blks_read),
                'local_blks_hit', sum(local_blks_hit),
                'total_time', sum(total_time),
                'calls', sum(calls),
                'rows', sum(rows)
        ) AS "json"
FROM pg_stat_statements(false)
WHERE (SELECT pgd.oid FROM pg_database AS pgd WHERE datname=$1) = dbid
        AND userid NOT IN (TABLE su);

pgstmts.sec.json.v13.sql

WITH su AS (
        SELECT usesysid FROM pg_user
        WHERE usename IN ('postgres', 'zbx_monitor', 'pgbouncer'))
SELECT jsonb_build_object(
                'temp_blks_read', sum(temp_blks_read),
                'shared_blks_read', sum(shared_blks_read),
                'shared_blks_hit', sum(shared_blks_hit),
                'local_blks_read', sum(local_blks_read),
                'local_blks_hit', sum(local_blks_hit),
                'total_time', sum(total_exec_time + total_plan_time),
                'calls', sum(calls),
                'rows', sum(rows)
        ) AS "json"
FROM pg_stat_statements(false)
WHERE (SELECT pgd.oid FROM pg_database AS pgd WHERE datname=$1) = dbid
        AND userid NOT IN (TABLE su);

odsamoylov.conf

# discover PostgreSQL servers accessible by file sockets
UserParameter=pg.servers, echo '['; delimiter=''; for f in /var/run/postgresql/.s.PGSQL.*; do if [ -S "$f" ]; then p="${f#/var/run/postgresql/.s.PGSQL.}"; svn="$(psql --no-psqlrc --set=ON_ERROR_STOP=on --no-password --no-readline --tuples-only --no-align --user='zbx_monitor' --port="$p" --dbname='postgres' --command="show server_version_num;")"; echo "${delimiter}{ \"{#URI}\":\":$p/\", \"{#POSTGRESQL_URI}\":\"postgresql://$HOSTNAME:$p\", \"{#PG.URI}\":\"$f\", \"{#PG.SERVER_VERSION_NUM}\":$svn, \"{#HOSTNAME}\":\"$HOSTNAME\", \"{#PG.PORT}\":$p }"; delimiter=', '; fi; done; echo ']'

# discover PostgreSQL databases accessible by file sockets
UserParameter=pg.databases,echo '['; delimiter='';for f in /var/run/postgresql/.s.PGSQL.*; do if [ -S "$f" ]; then p="${f#/var/run/postgresql/.s.PGSQL.}"; svn="$(psql --no-psqlrc --set=ON_ERROR_STOP=on --no-password --no-readline --tuples-only --no-align --user='zbx_monitor' --port="$p" --dbname='postgres' --command="show server_version_num;")"; for d in $(psql --no-psqlrc --set=ON_ERROR_STOP=on --no-password --no-readline --tuples-only --no-align --user='zbx_monitor' --port="$p" --dbname='postgres' --command="SELECT datname FROM pg_database WHERE NOT datistemplate AND datallowconn;"); do echo "${delimiter}{ \"{#URI}\":\":$p/$d\", \"{#POSTGRESQL_URI}\":\"postgresql://$HOSTNAME:$p/$d\", \"{#PG.URI}\":\"$f\", \"{#PG.PORT}\":$p, \"{#PG.DATABASE}\":\"$d\", \"{#PG.SERVER_VERSION_NUM}\":$svn }"; delimiter=', '; done; fi; done; echo ']'

# get pg_stat_statements per databases
# в данном запросе, если zbx_moniotor дать grant на pg_stat_statements_reset() и роль pg_read_all_stats или pg_monitor,
# то можно будет запускать под zbx_monitor
# расшифровка ключа
# pgstmts данные из pg_stat_statements
# day суточные данные (бывают еще посекундные)
# все гребу в единый json, потом разгребаю
# v13 -> postgresql >=13
# pgstmts.day.json.v13[port,database]
UserParameter=pgstmts.day.json.v13[*], psql --no-psqlrc --set=ON_ERROR_STOP=on --no-password --no-readline --tuples-only --quiet --no-align --user='zbx_monitor' --port='$1' --dbname='postgres' --command="WITH su AS (SELECT usesysid FROM pg_user WHERE usename IN ('postgres', 'zbx_monitor', 'pgbouncer')) SELECT jsonb_build_object( 'top_total_time', (SELECT jsonb_agg(stat.*) FROM (SELECT * FROM pg_stat_statements(true) WHERE dbid=pgd.oid AND userid NOT IN (TABLE su) ORDER BY total_exec_time + total_plan_time DESC LIMIT 5) AS stat), 'top_max_time', (SELECT jsonb_agg(stat.*) FROM (SELECT * FROM pg_stat_statements(true) WHERE dbid=pgd.oid AND userid NOT IN (TABLE su) ORDER BY max_exec_time + max_plan_time DESC LIMIT 5) AS stat)) || (SELECT jsonb_build_object( 'temp_blks_read', sum(temp_blks_read), 'shared_blks_miss', CASE WHEN sum(shared_blks_read)=0 THEN 0 ELSE 100.*sum(shared_blks_read)/sum(shared_blks_hit+shared_blks_read) END, 'local_blks_miss', CASE WHEN sum(local_blks_read)=0 THEN 0 ELSE 100.*sum(local_blks_read)/sum(local_blks_hit+local_blks_read) END, 'max_time', max(max_exec_time + max_plan_time), 'min_time', min(min_exec_time + min_plan_time), 'mean_time', avg(mean_exec_time + mean_plan_time), 'total_time', sum(total_exec_time + total_plan_time), 'calls', sum(calls), 'rows', sum(rows) ) FROM pg_stat_statements(false) WHERE dbid=pgd.oid AND userid NOT IN (TABLE su) ) FROM pg_database AS pgd WHERE datname='$2';" --command "DO \$DO\$ BEGIN PERFORM pg_stat_statements_reset(0,pgd.oid,0) FROM pg_database AS pgd WHERE datname='$2'; END \$DO\$;"

# get pg_stat_statements per instance
# расшифровка ключа
# pgstmts данные из pg_stat_statements
# day суточные данные (бывают еще посекундные)
# все гребу в единый json, потом разгребаю
# v12 -> 10<= postgresql <=12
# pgstmts.day.json.v12[port]
UserParameter=pgstmts.day.json.v12[*], psql --no-psqlrc --set=ON_ERROR_STOP=on --no-password --no-readline --tuples-only --quiet --no-align --user='zbx_monitor' --port='$1' --dbname='postgres' --command="WITH su AS (SELECT usesysid FROM pg_user WHERE usename IN ('postgres', 'zbx_monitor', 'pgbouncer')) SELECT jsonb_object_agg( pgd.datname, jsonb_build_object( 'top_total_time', (SELECT jsonb_agg(stat.*) FROM (SELECT * FROM pg_stat_statements(true) WHERE dbid=pgd.oid AND userid NOT IN (TABLE su) ORDER BY total_time DESC LIMIT 5) AS stat), 'top_max_time', (SELECT jsonb_agg(stat.*) FROM (SELECT * FROM pg_stat_statements(true) WHERE dbid=pgd.oid AND userid NOT IN (TABLE su) ORDER BY max_time DESC LIMIT 5) AS stat)) || (SELECT jsonb_build_object( 'temp_blks_read', sum(temp_blks_read), 'shared_blks_miss', CASE WHEN sum(shared_blks_read)=0 THEN 0 ELSE 100.*sum(shared_blks_read)/sum(shared_blks_hit+shared_blks_read) END, 'local_blks_miss', CASE WHEN sum(local_blks_read)=0 THEN 0 ELSE 100.*sum(local_blks_read)/sum(local_blks_hit+local_blks_read) END, 'max_time', max(max_time), 'min_time', min(min_time), 'mean_time', avg(mean_time), 'total_time', sum(total_time), 'calls', sum(calls), 'rows', sum(rows) ) FROM pg_stat_statements(false) WHERE dbid=pgd.oid AND userid NOT IN (TABLE su)) ) FROM pg_database AS pgd WHERE NOT datistemplate AND datallowconn;" --command="DO \$DO\$ BEGIN PERFORM pg_stat_statements_reset(); END \$DO\$;"

Файл odsamoylov.conf должен лежать там, куда указывается опция Include=/etc/zabbix/zabbix_agent2.d/*.conf в файле /etc/zabbix/zabbix_agent2.conf. В данном случае в директории /etc/zabbix/zabbix_agent2.d/. А файлы pgstmts.sec.json.v*.sql должны лежать в директории, куда указывает опция Plugins.PostgreSQL.CustomQueriesPath в файле /etc/zabbix/zabbix_agent2.d/plugins.d/postgresql.conf. Обычно она закомментирована (значит, опция отключена), надо расскоментировать и прописать нужный путь, например /etc/zabbix/postgresql.

zbx_export_templates.yaml

А этот файл добавить в Zabbix через Импорт, раздел Шаблоны, кнопка в правом верхнем углу.

zabbix_export:
  version: '6.0'
  date: '2023-11-03T09:21:35Z'
  groups:
    - uuid: 748ad4d098d447d492bb935c907f652f
      name: Templates/Databases
  templates:
    - uuid: 11887f960b8945fd8405569f6d5cc4ab
      template: odsamoylov
      name: 'ODSamoylov PostgresQL discovery'
      description: 'Discover working PostgreSQL servers from existing socket files.'
      groups:
        - name: Templates/Databases
      items:
        - uuid: 9843942f673540f8b61b7983761cdbd7
          name: 'System Load (avg 1 min)'
          key: 'system.cpu.load[all, avg1]'
          value_type: FLOAT
          description: 'System load, 1min average.'
      discovery_rules:
        - uuid: 2d573742e4e64329a9d02d4165902df7
          name: Pg_db
          key: pg.databases
          delay: 1d
          lifetime: 1d
          item_prototypes:
            - uuid: deccbd857fba47b7be99646694c4032b
              name: '{#URI}->pgstmts.sec.json.v12'
              key: 'pgsql.custom.query["{#PG.URI}",''postgres'',,''postgres'',"pgstmts.sec.json.v12", "{#PG.DATABASE}"]'
              history: '0'
              trends: '0'
              discover: NO_DISCOVER
              value_type: TEXT
              description: |
                Текущие значения из pg_stat_statements, без reset.
                Для postgresql <13
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$[0]["json"]'
            - uuid: 60ab8bb11ea84732b2e28470e9e729ef
              name: '{#URI}->pgstmts.sec.json.v13'
              key: 'pgsql.custom.query["{#PG.URI}",''postgres'',,''postgres'',"pgstmts.sec.json.v13", "{#PG.DATABASE}"]'
              history: '0'
              trends: '0'
              value_type: TEXT
              description: |
                Текущие значения из pg_stat_statements, без reset.
                Для postgresql >=13
                Если для zbx_monitor выдать правильные пермишенсы, можно будет запускать от его имени.
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$[0]["json"]'
            - uuid: d51ce3c027194c03ac9b9fad60954f62
              name: '{#URI}->pgstmts.day.calls.v12'
              type: DEPENDENT
              key: 'pgstmts.day.calls.v12["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              discover: NO_DISCOVER
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["calls"]'
              master_item:
                key: 'pgstmts.day.json.v12["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: 9a5c104842d543f3bbcbe03108a1dbfa
              name: '{#URI}->pgstmts.day.calls.v13'
              type: DEPENDENT
              key: 'pgstmts.day.calls.v13["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["calls"]'
              master_item:
                key: 'pgstmts.day.json.v13["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: 1817cd7081194d8cb91a584d3d497b13
              name: '{#URI}->pgstmts.day.json.v12'
              type: CALCULATED
              key: 'pgstmts.day.json.v12["{#PG.PORT}","{#PG.DATABASE}"]'
              delay: 0;h2
              history: '0'
              trends: '0'
              discover: NO_DISCOVER
              value_type: TEXT
              params: 'first(//pgstmts.day.json.v12["{#PG.PORT}"], 4h)'
              description: 'Parse databases stats. 9.5 <= postgresql <=12.'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["{#PG.DATABASE}"]'
            - uuid: 7c5e2872b3124158b315baf92547e292
              name: '{#URI}->pgstmts.day.json.v13'
              key: 'pgstmts.day.json.v13["{#PG.PORT}","{#PG.DATABASE}"]'
              delay: 0;h0
              history: '0'
              trends: '0'
              value_type: TEXT
              description: 'Get pg_stat_statements and reset statistics, need postgresql >=13'
            - uuid: 6e9f27eb89704487b57e4fed2000d7db
              name: '{#URI}->pgstmts.day.local_blks_miss.v12'
              type: DEPENDENT
              key: 'pgstmts.day.local_blks_miss.v12["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              discover: NO_DISCOVER
              value_type: FLOAT
              units: '%'
              description: 'Need to increase temp_buffers PostgresQL config option.'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["local_blks_miss"]'
              master_item:
                key: 'pgstmts.day.json.v12["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: ce749edb579b4d498fabb5879b812bc9
              name: '{#URI}->pgstmts.day.local_blks_miss.v13'
              type: DEPENDENT
              key: 'pgstmts.day.local_blks_miss.v13["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              value_type: FLOAT
              units: '%'
              description: 'Need to increase temp_buffers PostgresQL config option.'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["local_blks_miss"]'
              master_item:
                key: 'pgstmts.day.json.v13["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: 19569e8199f94a5f98531a996b062028
              name: '{#URI}->pgstmts.day.max_time.v12'
              type: DEPENDENT
              key: 'pgstmts.day.max_time.v12["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              discover: NO_DISCOVER
              value_type: FLOAT
              units: ms
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["max_time"]'
              master_item:
                key: 'pgstmts.day.json.v12["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: c989cf9537d14064b88302f92bc6cbc3
              name: '{#URI}->pgstmts.day.max_time.v13'
              type: DEPENDENT
              key: 'pgstmts.day.max_time.v13["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              value_type: FLOAT
              units: ms
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["max_time"]'
              master_item:
                key: 'pgstmts.day.json.v13["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: 76b7a5b9f6604920babcf5cac5f54b97
              name: '{#URI}->pgstmts.day.mean_time.v12'
              type: DEPENDENT
              key: 'pgstmts.day.mean_time.v12["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              discover: NO_DISCOVER
              value_type: FLOAT
              units: ms
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["mean_time"]'
              master_item:
                key: 'pgstmts.day.json.v12["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: 56c7a1339c8f45fb86a37fbf7ffdfe3c
              name: '{#URI}->pgstmts.day.mean_time.v13'
              type: DEPENDENT
              key: 'pgstmts.day.mean_time.v13["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              value_type: FLOAT
              units: ms
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["mean_time"]'
              master_item:
                key: 'pgstmts.day.json.v13["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: 690c0ddf1d864d29a9482d59b46e7db3
              name: '{#URI}->pgstmts.day.min_time.v12'
              type: DEPENDENT
              key: 'pgstmts.day.min_time.v12["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              discover: NO_DISCOVER
              value_type: FLOAT
              units: ms
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["min_time"]'
              master_item:
                key: 'pgstmts.day.json.v12["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: 55d279ff6943492d96cc6f23ace8aa86
              name: '{#URI}->pgstmts.day.min_time.v13'
              type: DEPENDENT
              key: 'pgstmts.day.min_time.v13["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              value_type: FLOAT
              units: ms
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["min_time"]'
              master_item:
                key: 'pgstmts.day.json.v13["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: df66165ed81142129de2cbdf5e8b59d7
              name: '{#URI}->pgstmts.day.rows.v12'
              type: DEPENDENT
              key: 'pgstmts.day.rows.v12["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              discover: NO_DISCOVER
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["rows"]'
              master_item:
                key: 'pgstmts.day.json.v12["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: 4d2dbd01aea2455ab58dccfdf13dd7bf
              name: '{#URI}->pgstmts.day.rows.v13'
              type: DEPENDENT
              key: 'pgstmts.day.rows.v13["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["rows"]'
              master_item:
                key: 'pgstmts.day.json.v13["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: a2ffb06cc87a4d19bda568497ec92743
              name: '{#URI}->pgstmts.day.shared_blks_miss.v12'
              type: DEPENDENT
              key: 'pgstmts.day.shared_blks_miss.v12["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              discover: NO_DISCOVER
              value_type: FLOAT
              units: '%'
              description: 'Need to increase shared_buffers PostgresQL config option.'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["shared_blks_miss"]'
              master_item:
                key: 'pgstmts.day.json.v12["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: 6e41b880cdd243d89f5556c94d31c569
              name: '{#URI}->pgstmts.day.shared_blks_miss.v13'
              type: DEPENDENT
              key: 'pgstmts.day.shared_blks_miss.v13["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              value_type: FLOAT
              units: '%'
              description: 'Need to increase shared_buffers PostgresQL config option.'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["shared_blks_miss"]'
              master_item:
                key: 'pgstmts.day.json.v13["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: e29562671e1a4e88b29637b5a018f4a3
              name: '{#URI}->pgstmts.day.temp_blks_read.v12'
              type: DEPENDENT
              key: 'pgstmts.day.temp_blks_read.v12["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              discover: NO_DISCOVER
              description: 'Need to increase work_mem PostgresQL config option.'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["temp_blks_read"]'
              master_item:
                key: 'pgstmts.day.json.v12["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: f26de5990a8d434a8d2ef3c401d66c44
              name: '{#URI}->pgstmts.day.temp_blks_read.v13'
              type: DEPENDENT
              key: 'pgstmts.day.temp_blks_read.v13["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              description: 'Need to increase work_mem PostgresQL config option.'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["temp_blks_read"]'
              master_item:
                key: 'pgstmts.day.json.v13["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: 243a966177c944adbbeddb07af730b06
              name: '{#URI}->pgstmts.day.top_max_time.v12'
              type: DEPENDENT
              key: 'pgstmts.day.top_max_time.v12["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              trends: '0'
              discover: NO_DISCOVER
              value_type: TEXT
              description: 'Top queries for max_time.'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["top_max_time"]'
                - type: JAVASCRIPT
                  parameters:
                    - 'return JSON.stringify(JSON.parse(value),null," ")'
                - type: STR_REPLACE
                  parameters:
                    - \\n
                    - \n
              master_item:
                key: 'pgstmts.day.json.v12["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: 88daedcf4efb4645aa418e555b83e83b
              name: '{#URI}->pgstmts.day.top_max_time.v13'
              type: DEPENDENT
              key: 'pgstmts.day.top_max_time.v13["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              trends: '0'
              value_type: TEXT
              description: 'Top queries for max_exec_time'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["top_max_time"]'
                - type: JAVASCRIPT
                  parameters:
                    - 'return JSON.stringify(JSON.parse(value),null," ")'
                - type: STR_REPLACE
                  parameters:
                    - \\n
                    - \n
              master_item:
                key: 'pgstmts.day.json.v13["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: ae1e10422cd0469faec4550bd9da3da0
              name: '{#URI}->pgstmts.day.top_total_time.v12'
              type: DEPENDENT
              key: 'pgstmts.day.top_total_time.v12["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              trends: '0'
              discover: NO_DISCOVER
              value_type: TEXT
              description: 'Top queries for total_time.'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["top_total_time"]'
                - type: JAVASCRIPT
                  parameters:
                    - 'return JSON.stringify(JSON.parse(value),null," ")'
                - type: STR_REPLACE
                  parameters:
                    - \\n
                    - \n
              master_item:
                key: 'pgstmts.day.json.v12["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: 8a195b984ac24a6c8ff7a9da0ca673ce
              name: '{#URI}->pgstmts.day.top_total_time.v13'
              type: DEPENDENT
              key: 'pgstmts.day.top_total_time.v13["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              trends: '0'
              value_type: TEXT
              description: 'Top queries for total_exec_time.'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["top_total_time"]'
                - type: JAVASCRIPT
                  parameters:
                    - 'return JSON.stringify(JSON.parse(value),null," ")'
                - type: STR_REPLACE
                  parameters:
                    - \\n
                    - \n
              master_item:
                key: 'pgstmts.day.json.v13["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: 2e9b82cb015a41e6ac34dc7abc50da13
              name: '{#URI}->pgstmts.day.total_time.v12'
              type: DEPENDENT
              key: 'pgstmts.day.total_time.v12["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              discover: NO_DISCOVER
              value_type: FLOAT
              units: ms
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["total_time"]'
              master_item:
                key: 'pgstmts.day.json.v12["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: 501c9852f74147228960eabc2137b9c3
              name: '{#URI}->pgstmts.day.total_time.v13'
              type: DEPENDENT
              key: 'pgstmts.day.total_time.v13["{#PG.URI}","{#PG.DATABASE}"]'
              delay: '0'
              history: 1w
              value_type: FLOAT
              units: ms
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["total_time"]'
              master_item:
                key: 'pgstmts.day.json.v13["{#PG.PORT}","{#PG.DATABASE}"]'
            - uuid: a79a9d0c0e944d5792f1a8e061dc7ea4
              name: '{#URI}->pgstmts.sec.calls.v12'
              type: DEPENDENT
              key: 'pgstmts.sec.calls.v12["{#PG.URI}",,,"{#PG.DATABASE}"]'
              delay: '0'
              trends: '0'
              discover: NO_DISCOVER
              description: 'Queries calls per second.'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["calls"]'
                - type: CHANGE_PER_SECOND
                  parameters:
                    - ''
              master_item:
                key: 'pgsql.custom.query["{#PG.URI}",''postgres'',,''postgres'',"pgstmts.sec.json.v12", "{#PG.DATABASE}"]'
            - uuid: 6a0e03a545644a82b0240f51ffd803d3
              name: '{#URI}->pgstmts.sec.calls.v13'
              type: DEPENDENT
              key: 'pgstmts.sec.calls.v13["{#PG.URI}",,,"{#PG.DATABASE}"]'
              delay: '0'
              trends: '0'
              description: 'Queries calls per second.'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["calls"]'
                - type: CHANGE_PER_SECOND
                  parameters:
                    - ''
              master_item:
                key: 'pgsql.custom.query["{#PG.URI}",''postgres'',,''postgres'',"pgstmts.sec.json.v13", "{#PG.DATABASE}"]'
            - uuid: eb5775d274ae4b45a400b8eb5b676c93
              name: '{#URI}->pgstmts.sec.load.v12'
              type: DEPENDENT
              key: 'pgstmts.sec.load.v12["{#PG.URI}",,,"{#PG.DATABASE}"]'
              delay: '0'
              trends: '0'
              discover: NO_DISCOVER
              value_type: FLOAT
              description: 'Аналог system load, вычисляется на базу данных из pg_stat_statements. Поскольку данные поступают с завершением запросов, запросы больше минуты будут вносить неточность.'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["total_time"]'
                - type: MULTIPLIER
                  parameters:
                    - '0.001'
                - type: CHANGE_PER_SECOND
                  parameters:
                    - ''
              master_item:
                key: 'pgsql.custom.query["{#PG.URI}",''postgres'',,''postgres'',"pgstmts.sec.json.v12", "{#PG.DATABASE}"]'
            - uuid: 3ab68c00d288440b8fe576905b1787a4
              name: '{#URI}->pgstmts.sec.load.v13'
              type: DEPENDENT
              key: 'pgstmts.sec.load.v13["{#PG.URI}",,,"{#PG.DATABASE}"]'
              delay: '0'
              trends: '0'
              value_type: FLOAT
              description: 'Аналог system load, вычисляется на базу данных из pg_stat_statements. Поскольку данные поступают с завершением запросов, запросы больше минуты будут вносить неточность.'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["total_time"]'
                - type: MULTIPLIER
                  parameters:
                    - '0.001'
                - type: CHANGE_PER_SECOND
                  parameters:
                    - ''
              master_item:
                key: 'pgsql.custom.query["{#PG.URI}",''postgres'',,''postgres'',"pgstmts.sec.json.v13", "{#PG.DATABASE}"]'
            - uuid: 9062ad5dad914883a02db6c42e0841f3
              name: '{#URI}->pgstmts.sec.local_blks_hit.v12'
              type: DEPENDENT
              key: 'pgstmts.sec.local_blks_hit.v12["{#PG.URI}",,,"{#PG.DATABASE}"]'
              delay: '0'
              trends: '0'
              discover: NO_DISCOVER
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["local_blks_hit"]'
                - type: CHANGE_PER_SECOND
                  parameters:
                    - ''
              master_item:
                key: 'pgsql.custom.query["{#PG.URI}",''postgres'',,''postgres'',"pgstmts.sec.json.v12", "{#PG.DATABASE}"]'
            - uuid: a2e6e98edcbb4ff5a87be6539b10ebd9
              name: '{#URI}->pgstmts.sec.local_blks_hit.v13'
              type: DEPENDENT
              key: 'pgstmts.sec.local_blks_hit.v13["{#PG.URI}",,,"{#PG.DATABASE}"]'
              delay: '0'
              trends: '0'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["local_blks_hit"]'
                - type: CHANGE_PER_SECOND
                  parameters:
                    - ''
              master_item:
                key: 'pgsql.custom.query["{#PG.URI}",''postgres'',,''postgres'',"pgstmts.sec.json.v13", "{#PG.DATABASE}"]'
            - uuid: f1c46395b722427eb3abe1952bd64b70
              name: '{#URI}->pgstmts.sec.local_blks_miss.v12'
              type: CALCULATED
              key: 'pgstmts.sec.local_blks_miss.v12["{#PG.URI}",,,"{#PG.DATABASE}"]'
              trends: '0'
              discover: NO_DISCOVER
              value_type: FLOAT
              units: '%'
              params: '100*last(//pgstmts.sec.local_blks_read.v12["{#PG.URI}",,,"{#PG.DATABASE}"])/(last(//pgstmts.sec.local_blks_read.v12["{#PG.URI}",,,"{#PG.DATABASE}"])+last(//pgstmts.sec.local_blks_hit.v12["{#PG.URI}",,,"{#PG.DATABASE}"])+0.00001)'
            - uuid: 5ac87f478a1349f4b4b1fd6200d94763
              name: '{#URI}->pgstmts.sec.local_blks_miss.v13'
              type: CALCULATED
              key: 'pgstmts.sec.local_blks_miss.v13["{#PG.URI}",,,"{#PG.DATABASE}"]'
              trends: '0'
              value_type: FLOAT
              units: '%'
              params: '100*last(//pgstmts.sec.local_blks_read.v13["{#PG.URI}",,,"{#PG.DATABASE}"])/(last(//pgstmts.sec.local_blks_read.v13["{#PG.URI}",,,"{#PG.DATABASE}"])+last(//pgstmts.sec.local_blks_hit.v13["{#PG.URI}",,,"{#PG.DATABASE}"])+0.00001)'
            - uuid: 51baf10158164b0cb5470bb4743ddc53
              name: '{#URI}->pgstmts.sec.local_blks_read.v12'
              type: DEPENDENT
              key: 'pgstmts.sec.local_blks_read.v12["{#PG.URI}",,,"{#PG.DATABASE}"]'
              delay: '0'
              trends: '0'
              discover: NO_DISCOVER
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["local_blks_read"]'
                - type: CHANGE_PER_SECOND
                  parameters:
                    - ''
              master_item:
                key: 'pgsql.custom.query["{#PG.URI}",''postgres'',,''postgres'',"pgstmts.sec.json.v12", "{#PG.DATABASE}"]'
            - uuid: c1eb2c53db3d48f698650822662d2c67
              name: '{#URI}->pgstmts.sec.local_blks_read.v13'
              type: DEPENDENT
              key: 'pgstmts.sec.local_blks_read.v13["{#PG.URI}",,,"{#PG.DATABASE}"]'
              delay: '0'
              trends: '0'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["local_blks_read"]'
                - type: CHANGE_PER_SECOND
                  parameters:
                    - ''
              master_item:
                key: 'pgsql.custom.query["{#PG.URI}",''postgres'',,''postgres'',"pgstmts.sec.json.v13", "{#PG.DATABASE}"]'
            - uuid: d8687f228a724f3a96036d520adc0d7c
              name: '{#URI}->pgstmts.sec.rows.v12'
              type: DEPENDENT
              key: 'pgstmts.sec.rows.v12["{#PG.URI}",,,"{#PG.DATABASE}"]'
              delay: '0'
              trends: '0'
              discover: NO_DISCOVER
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["rows"]'
                - type: CHANGE_PER_SECOND
                  parameters:
                    - ''
              master_item:
                key: 'pgsql.custom.query["{#PG.URI}",''postgres'',,''postgres'',"pgstmts.sec.json.v12", "{#PG.DATABASE}"]'
            - uuid: 46c7fff000d944cc81792141877e30b3
              name: '{#URI}->pgstmts.sec.rows.v13'
              type: DEPENDENT
              key: 'pgstmts.sec.rows.v13["{#PG.URI}",,,"{#PG.DATABASE}"]'
              delay: '0'
              trends: '0'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["rows"]'
                - type: CHANGE_PER_SECOND
                  parameters:
                    - ''
              master_item:
                key: 'pgsql.custom.query["{#PG.URI}",''postgres'',,''postgres'',"pgstmts.sec.json.v13", "{#PG.DATABASE}"]'
            - uuid: 645e9467a9a04e86b4f7b29edca134b9
              name: '{#URI}->pgstmts.sec.shared_blks_hit.v12'
              type: DEPENDENT
              key: 'pgstmts.sec.shared_blks_hit.v12["{#PG.URI}",,,"{#PG.DATABASE}"]'
              delay: '0'
              trends: '0'
              discover: NO_DISCOVER
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["shared_blks_hit"]'
                - type: CHANGE_PER_SECOND
                  parameters:
                    - ''
              master_item:
                key: 'pgsql.custom.query["{#PG.URI}",''postgres'',,''postgres'',"pgstmts.sec.json.v12", "{#PG.DATABASE}"]'
            - uuid: 3697b2fc1c3e40a18ede6998ec6915cf
              name: '{#URI}->pgstmts.sec.shared_blks_hit.v13'
              type: DEPENDENT
              key: 'pgstmts.sec.shared_blks_hit.v13["{#PG.URI}",,,"{#PG.DATABASE}"]'
              delay: '0'
              trends: '0'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["shared_blks_hit"]'
                - type: CHANGE_PER_SECOND
                  parameters:
                    - ''
              master_item:
                key: 'pgsql.custom.query["{#PG.URI}",''postgres'',,''postgres'',"pgstmts.sec.json.v13", "{#PG.DATABASE}"]'
            - uuid: b8a03880ab72460f84108b56215fafd9
              name: '{#URI}->pgstmts.sec.shared_blks_miss.v12'
              type: CALCULATED
              key: 'pgstmts.sec.shared_blks_miss.v12["{#PG.URI}",,,"{#PG.DATABASE}"]'
              trends: '0'
              discover: NO_DISCOVER
              value_type: FLOAT
              units: '%'
              params: '100*last(//pgstmts.sec.shared_blks_read.v12["{#PG.URI}",,,"{#PG.DATABASE}"])/(last(//pgstmts.sec.shared_blks_read.v12["{#PG.URI}",,,"{#PG.DATABASE}"])+last(//pgstmts.sec.shared_blks_hit.v12["{#PG.URI}",,,"{#PG.DATABASE}"])+0.00001)'
            - uuid: 969c9423e22344d0beb86aed847e2c9f
              name: '{#URI}->pgstmts.sec.shared_blks_miss.v13'
              type: CALCULATED
              key: 'pgstmts.sec.shared_blks_miss.v13["{#PG.URI}",,,"{#PG.DATABASE}"]'
              trends: '0'
              value_type: FLOAT
              units: '%'
              params: '100*last(//pgstmts.sec.shared_blks_read.v13["{#PG.URI}",,,"{#PG.DATABASE}"])/(last(//pgstmts.sec.shared_blks_read.v13["{#PG.URI}",,,"{#PG.DATABASE}"])+last(//pgstmts.sec.shared_blks_hit.v13["{#PG.URI}",,,"{#PG.DATABASE}"])+0.00001)'
            - uuid: 325fc8eacfeb4719b3634409a5151025
              name: '{#URI}->pgstmts.sec.shared_blks_read.v12'
              type: DEPENDENT
              key: 'pgstmts.sec.shared_blks_read.v12["{#PG.URI}",,,"{#PG.DATABASE}"]'
              delay: '0'
              trends: '0'
              discover: NO_DISCOVER
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["shared_blks_read"]'
                - type: CHANGE_PER_SECOND
                  parameters:
                    - ''
              master_item:
                key: 'pgsql.custom.query["{#PG.URI}",''postgres'',,''postgres'',"pgstmts.sec.json.v12", "{#PG.DATABASE}"]'
            - uuid: aa15364ef029496fa5b9ab4cd24dad0e
              name: '{#URI}->pgstmts.sec.shared_blks_read.v13'
              type: DEPENDENT
              key: 'pgstmts.sec.shared_blks_read.v13["{#PG.URI}",,,"{#PG.DATABASE}"]'
              delay: '0'
              trends: '0'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["shared_blks_read"]'
                - type: CHANGE_PER_SECOND
                  parameters:
                    - ''
              master_item:
                key: 'pgsql.custom.query["{#PG.URI}",''postgres'',,''postgres'',"pgstmts.sec.json.v13", "{#PG.DATABASE}"]'
            - uuid: 10020da9cc0e4363bb68d4f8b4d1eb41
              name: '{#URI}->pgstmts.sec.temp_blks_read.v12'
              type: DEPENDENT
              key: 'pgstmts.sec.temp_blks_read.v12["{#PG.URI}",,,"{#PG.DATABASE}"]'
              delay: '0'
              trends: '0'
              discover: NO_DISCOVER
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["temp_blks_read"]'
                - type: CHANGE_PER_SECOND
                  parameters:
                    - ''
              master_item:
                key: 'pgsql.custom.query["{#PG.URI}",''postgres'',,''postgres'',"pgstmts.sec.json.v12", "{#PG.DATABASE}"]'
            - uuid: d7585acf54144ba9b9e2427c4aaaf3fc
              name: '{#URI}->pgstmts.sec.temp_blks_read.v13'
              type: DEPENDENT
              key: 'pgstmts.sec.temp_blks_read.v13["{#PG.URI}",,,"{#PG.DATABASE}"]'
              delay: '0'
              trends: '0'
              preprocessing:
                - type: JSONPATH
                  parameters:
                    - '$["temp_blks_read"]'
                - type: CHANGE_PER_SECOND
                  parameters:
                    - ''
              master_item:
                key: 'pgsql.custom.query["{#PG.URI}",''postgres'',,''postgres'',"pgstmts.sec.json.v13", "{#PG.DATABASE}"]'
          overrides:
            - name: 'pgstmts.*.v13'
              step: '1'
              filter:
                conditions:
                  - macro: '{#PG.SERVER_VERSION_NUM}'
                    value: '^(1[0-2]....|.....)$'
                    formulaid: A
              operations:
                - operationobject: ITEM_PROTOTYPE
                  operator: REGEXP
                  value: '->pgstmts\..*\.v13$'
                  discover: NO_DISCOVER
            - name: 'pgstmts.*.v12'
              step: '2'
              filter:
                conditions:
                  - macro: '{#PG.SERVER_VERSION_NUM}'
                    value: '^(1[0-2]....)$'
                    formulaid: A
              operations:
                - operationobject: ITEM_PROTOTYPE
                  operator: REGEXP
                  value: '->pgstmts\..*\.v12$'
                  discover: DISCOVER
        - uuid: f56274e9f87e42bb8122559c13c7ea5d
          name: Pg_srv
          key: pg.servers
          delay: 1d
          lifetime: 1d
          description: 'Discover PostgreSQL servers by looking for unix sockets.'
          item_prototypes:
            - uuid: cb751a1fe02f463da409a158375570b1
              name: '{#URI}->pgstmts.day.json.v12'
              key: 'pgstmts.day.json.v12["{#PG.PORT}"]'
              delay: 0;h0
              history: 4h
              trends: '0'
              discover: NO_DISCOVER
              value_type: TEXT
              description: |
                Get pg_stat_statements and reset statistics, need 9.5 <= postgresql <=12.
                Works per instances,
          overrides:
            - name: 'pgstmts.*.v12'
              step: '1'
              filter:
                conditions:
                  - macro: '{#PG.SERVER_VERSION_NUM}'
                    value: '^(1[0-2]....)$'
                    formulaid: A
              operations:
                - operationobject: ITEM_PROTOTYPE
                  operator: REGEXP
                  value: '/->pgstmts\..*\.v12$'
                  discover: DISCOVER

Комментарии (5)


  1. dbax
    23.11.2023 09:54

    "это обычная практика, иметь по несколько PostgreSQL серверов на одном Linux сервере. В девелоперовском контуре их может быть несколько десятков на одном сервере, по одному на каждую команду. И почему профессионалы Zabbix это не учли, для меня большая загадка"

    Как раз не загадка, ни разу. В современном мире с легкодоступными средствами виртуализации и контейнеризации я нигде не видел более одного экземпляра БД на машине. Даже на самых "совковых" проектах...


    1. splarv Автор
      23.11.2023 09:54

      Типичная манипуляция общественным сознанием, использование слово "совковый", хотя оно тут совершенно неприемлемо. Современные средства виртуализации и конвейнеризации настолько глючные, что большинство поломок приходится именно на них. Сам по себе PostgreSQL очень стабилен и с самим PostgreSQL как с софтом никогда не бывает проблем, если только он не размещен на средствах виртуализации и конвейнеризации.
      Более того. Отказоустойчивость веб сервисов основана на том, что в самих контейнерах не должно быть никакой информации относящейся к состоянию системы. Прочитайте, это в любом учебнике написано. Поэтому в случае выхода одного контейнера из строя или узла, легко можно поднять запасной. Это достигается как раз тем, что вся информация о состоянии системы находится в базах данных, например на базе PostgreSQL, а вот они находятся как раз не в контейнерах и их отказоустойчивость достигается другими методами.
      Плюс потери производительности при виртуализации. Про потерю надежности уже говорил. Ну и, конечно, вопрос денег. Если молятся на модную микросервисную архитектуру это означает что баз данных очень много, но они небольшие и по сложности и по объему. У нас в компании несколько сотен баз данных. Арендовать под каждую виртуалку на точке обмена трафиком это еще и накладно, как по деньгам, так и по потерям производительности. Базы на железных машинах.


      1. dbax
        23.11.2023 09:54

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

        Даже когда я работал с Ораклом был один экземпляр и разные схемы для разрабов.

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

        И, как видите, мое мнение поддерживают, как минимум разработчики заббикса...

        Еще могу сказать что разработка в контейнере != микросервисная архитектура. Но если каждому из десятков разработчиков нужна полноценная база на терабайты "для тестов" - это ясно просчет в организации разработки.


        1. splarv Автор
          23.11.2023 09:54

          А так вы сейчас пишите про девелоперовский контур. Тут я вас поддерживаю, кстати. Я считаю что у каждого разработчика должна быть своя БД для отлаживания разработки. И я себе всегда такую делал. Впрочем и от общих баз данных для разработки тоже есть польза, в них объединяется труд разных разработчиков и окончательно проверяется перед выкаткой в продакшин. И с них я делал себе копию, если разрабатывал что-то своё.
          А основная идея этой стати была в том, что бы такой мониторинг повесить именно что на продакшин с целью чтобы у разработчиков был доступ к информации о проблемах производительности и проблемных запросах на продакшине.


      1. khajiit
        23.11.2023 09:54

        Типичная манипуляция общественным сознанием

        Типичный бессознательный бред

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

        Поверить, конечно, мы должны на слово? )

        Сам по себе PostgreSQL очень стабилен и с самим PostgreSQL как с софтом никогда не бывает проблем

        Буквально неделю назад 15.4 остановил все записи, нахапал коннекшнов до лимита, и его пришлось убивать ручками.

        в самих контейнерах не должно быть никакой информации относящейся к состоянию системы

        Docker головного мозга detected

        вся информация о состоянии системы находится в базах данных, например на базе PostgreSQL, а вот они находятся как раз не в контейнерах

        С чего бы им находиться не в контейнерах?

        потери производительности при виртуализации

        Показать, где там виртуализция в ядерном lxc — сможете?

        Про потерю надежности уже говорил

        Теперь ссылка на свое же, ранее ничем не подкрепленное, высказывание в качестве пруфа? )
        На поржать сгодится, но ржать над таким — стыдно по испански…