Привет, Хабр! Меня зовут Станислав Епишин, я DBA в дивизионе поддержки решений в тестовых средах в СберТехе. Эту статью я написал вместе с Дмитрием Корневым, тимлидом и DBA. У Сбера есть целевая СУБД, которую разработали в СберТехе на основе open source версии PostgreSQL, — Platform V Pangolin. Наша команда перешла на Pangolin в числе первых, когда у продукта еще не было инструментов для мониторинга БД. Забегая вперед, позже появились такие решения — графическая консоль Platform V Kintsugi, расширение для сбора статистики — Performance Insights и система мониторинга IT‑инфраструктуры Platform V Monitor. А поначалу мы решили мониторить базы данных связкой Grafana, Prometheus и postgres_exporter. Но, во‑первых, столкнулись, с тем, что нам не хватает гибкости в использовании queries.yaml в postgres exporter. А, во‑вторых, так мы не могли регистрировать события с таймаутом меньше 15 секунд. Поэтому мы тогда сделали свой инструмент для мониторинга — pangolin_exporter.
Надеюсь, что эта статья будет полезна тем, кто мониторит инфраструктуру с помощью postgres_exporter и хочет кастомизировать всё под свои нужды. Покажу детали и код решения.
Наши условия таковы: на сопровождении примерно 1200 баз данных, структура динамическая — их количество меняется, создаются новые, удаляются неактуальные. Нам требовалось реализовать динамическое распространение postgres_exporter (это инструмент PostgreSQL для сбора метрик с экземпляров кластера СУБД в формате, доступном Prometheus) по серверам, желательно с периодом 24 часа. Пробуем использовать Jenkins и сценарии Ansible для автоматизации развёртывания postgres_exporter. Итак, здесь всё хорошо: сценарии написаны, решение реализовано.
Далее нам надо было получать актуальные для нас метрики, в том числе специфические для Platform V Pangolin. Мы хотели наблюдать статистику активности и выполнения запросов и функций, ввода-вывода, WAL, доступности и актуальности снимков performance_insights и pg_profile и так далее. А также нам хотелось получать метрики по Pangolin Manager и Pangolin Pooler — это улучшенные и переработанные версии Patroni и Pgbouncer. Но, как я уже упоминал в начале, мы столкнулись с тем, что в postgres_exporter не можем гибко настроить получение метрик под себя.
Тогда мы решили взять исходный код инструмента и сделать форк на его основе. Создавая решение, мы черпали много информации из книги «Мониторинг PostgreSQL» Алексея Лесовского (спасибо Алексею за дельные и полезные идеи!).
Начинаем делать свой экспортёр
Создаём pangolin_exporter из исходников последней версии postgres_exporter-0.15.0 с GitHub. Для проекта мы использовали Microsoft VS Code, настройка под Go не вызвала каких‑то трудностей. Задаём переменные окружения для сборки и запуска экспортёра. Определяем переменную GOOS. При желании можно собрать версию под Windows, присвоив значение Windows
. В нашем случае мы присваиваем значение Linux
.
"GOOS": "linux"
Определяем переменную DATA_SOURCE_NAME
, содержащую строку подключения к БД, с которой будем собирать метрики. В учебных целях для этого примера определяем учётные данные в открытом виде. Меняем <user>:<pass>@<hostname>
на актуальные данные.
"DATA_SOURCE_NAME": "postgresql://<user>:<pass>@<hostname>:5432/postgres?sslmode=disable"
Для этого добавляем пару файлов в проект:
/<project_path>/cmd/postgres_exporter/.vscode/launch.json
/<project_path>/cmd/postgres_exporter/.vscode/settings.json
settings.json
{
"go.toolsEnvVars": {
"GOOS": "linux"
}
}
launch.json
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}",
"env": {
"DATA_SOURCE_NAME": "postgresql://<user>:<pass>@<hostname>:5432/postgres?sslmode=disable"
}
}
]
}
Собираем бинарник из исходников:
cd /<project_path>/cmd/postgres_exporter
go build -o /<project_path>/cmd/postgres_exporter/build .
Запускаем в Microsoft VS Code, для этого нужно открыть main.go и нажать F5, при необходимости пошагово отлаживаем код, расставляя breakpoints.
Далее добавляем необходимые нам метрики и редактируем существующие. Для редактирования в пакете collector находим нужную метрику. Файлы пакета находятся в каталоге /<project_path>/collector.
Рассмотрим пример добавления метрики для определения версии Pangolin. Планируем SQL-запрос для получения данных:
select product_version from product_version();
Версия Pangolin будет выводиться в label, метрика будет равна 1, если SQL-запрос выполнен. Добавляем новый файл в пакет collector, путь /<project_path>/collector.
package collector
import (
"context"
"database/sql"
"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
)
const InfoPangolinSubsystem = ""
func init() {
registerCollector(InfoPangolinSubsystem, defaultEnabled, NewPGInfoPangolinCollector)
}
type PGInfoPangolinCollector struct {
log log.Logger
}
func NewPGInfoPangolinCollector(config collectorConfig) (Collector, error) {
return &PGInfoPangolinCollector{log: config.logger}, nil
}
var (
InfoPangolinVersion = prometheus.NewDesc(
prometheus.BuildFQName(namespace_pangolin, InfoPangolinSubsystem, "version"),
"Pangolin version.",
[]string{"product_version"},
prometheus.Labels{},
)
pgInfoPangolinQuery = `select product_version from product_version();`
)
func (c PGInfoPangolinCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
db := instance.getDB()
rows, err := db.QueryContext(ctx, pgInfoPangolinQuery)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var product_version sql.NullString
if err := rows.Scan(&product_version); err != nil {
return err
}
product_versionLabel := "unknown"
upMetric := 0.0
if product_version.Valid {
product_versionLabel = product_version.String
upMetric = 1
} else {
upMetric = 0
}
ch <- prometheus.MustNewConstMetric(
InfoPangolinVersion,
prometheus.CounterValue,
upMetric,
product_versionLabel,
)
}
if err := rows.Err(); err != nil {
return err
}
return nil
}
Запускаем экспортёр, при настройках порта по умолчанию вбиваем в веб-браузер http://localhost:9187/metrics и видим нашу новую метрику:
# HELP pangolin_version Pangolin version
# TYPE pangolin_version counter
pangolin_version{product_version="Platform V Pangolin 5.5.0"} 1
Набредаем на баг в Postgres Exporter и другие грабли
Делая своё решение, попутно нашли баг в исходном коде postgres_exporter. Нам нужно было определить рост ожиданий в зависимости от нагрузки, чтобы понять, насколько АС подвержена риску на ПРОМе. Целиком можно было закрыть риск, проведя нагрузочное тестирование и посмотрев на события ожидания от LockManager, — это фоновый процесс в Pangolin, отвечающий за блокировки структур данных. Вариант решения заключался в использовании оригинальной метрики pg_stat_activity_max_tx_duration
из Postgres_exporter.
pg_stat_activity_max_tx_duration
— максимальная продолжительность выполнения активной транзакции в секундах.
Однако после изучения SQL‑запроса этой метрики (см. ниже), выяснилось, что для pg_stat_activity_max_tx_duration
возраст транзакций рассчитывается некорректно. Возраст существующих на конкретный момент транзакций должен определяться следующим образом: текущее время минус время начала транзакции. А на деле вместо текущего времени подставляется время запуска текущей транзакции, now ()
, то есть в нашем случае это время начала другой транзакции, и поэтому получается некорректное значение.
now()
→ timestamp with time zone. Current date and time (start of current transaction);clock_timestamp()
→ timestamp with time zone. Current date and time (changes during statement execution)
Некорректные значения скрывались агрегатной функцией max
.
MAX(EXTRACT(EPOCH FROM now() - xact_start))::float AS max_tx_duration
Для мониторинга необходимо использовать clock_timestamp()
, которая всегда показывает текущую отметку времени. Запрос, использующийся в оригинальной версии postgres_exporter для max_tx_duration
:
SELECT
pg_database.datname,
tmp.state,
tmp2.usename,
tmp2.application_name,
COALESCE(count,0) as count,
COALESCE(max_tx_duration,0) as max_tx_duration
FROM
(
VALUES ('active'),
('idle'),
('idle in transaction'),
('idle in transaction (aborted)'),
('fastpath function call'),
('disabled')
) AS tmp(state) CROSS JOIN pg_database
LEFT JOIN
(
SELECT
datname,
state,
usename,
application_name,
count(*) AS count,
MAX(EXTRACT(EPOCH FROM now() - xact_start))::float AS max_tx_duration
FROM pg_stat_activity GROUP BY datname,state,usename,application_name) AS tmp2
ON tmp.state = tmp2.state AND pg_database.datname = tmp2.datname;
Что с этим делать? Мы решили выводить все ожидания из pg_stat_activity
с группировкой по wait_event
. Нам был интересен кумулятивный рост ожиданий в зависимости от нагрузки. В целом ожидали получить график, где будет виден и ряд ожиданий, и их рост в зависимости от времени работы.
select wait_event,
SUM(EXTRACT (EPOCH FROM clock_timestamp () - xact_start)::float) AS sum_xact_age,
from pg_stat_activity where state not in ('idle') and wait_event is not null
group by wait_event
Внедрили этот запрос в наш форк. Новую метрику назвали pangolin_activity_max_seconds_xact_age
. Вывели её в Grafana. Нас интересовали таймауты от 5 мс и выше.
Здесь проявился главный недостаток связки Grafana+Prometheus и других подобных связок, например Grafana+VictoriaMetrics (их мы тоже использовали). В Prometheus есть параметр scrape_interval
(интервал сбора метрик). Минимальное значение, которое у нас получилось настроить, — 15 секунд. Если интересующее нас событие длится от нескольких миллисекунд до 15 секунд, то Prometheus с некоторой вероятностью не фиксирует его. Это недостатки мониторинга подобными связками. С ними не получится видеть объективную картину по wait_event
.
Что решили с этим? В тот момент в команде Pangolin только появился собственный графический инструмент Platform V Kintsugi. Начали использовать его для получения картины по wait_event
.
Kintsugi — инструмент для оперативного анализа и диагностики СУБД и сопутствующей инфраструктуры. Он дополняет систему мониторинга, становясь рядом, чтобы пользователи понимали, когда их СУБД тормозит и почему это происходит.
Kintsugi позволил нам фиксировать 100% событий, которые нам нужны, но только в разрезе сессий.
На тот момент задачу с регистрацией нужных нам таймаутов он не решал. Можно было попробовать использовать pg_profile
и pg_wait_sampling
.
pg_profile
— расширение Postgres, собирает статистику и создаёт снимки по собранной статистике, отдалённо напоминает AWR‑отчеты в ORACLE.pg_wait_sampling
— расширение Postgres для периодического сбора статистики по событиям ожидания.
Но в Pangolin нельзя просто так добавить сторонние расширения, сначала они проходят тщательный аудит безопасности. В итоге мы по косвенным признакам сделали вывод об отсутствии интересующих нас событий ожиданий с превышением 100 мс и закрыли этот риск в ПРОМе.
Да, ещё к этому моменту у Platform V, цифровой платформы СберТеха, появилась система мониторинга IT‑инфраструктуры Platform V Monitor. Этот продукт мониторит в том числе и Pangolin, но, честно говоря, мы пока не успели протестировать его для наших задач. А в ближайшее время будем пробовать новую функциональность Platform V Kintsugi — за время подготовки статьи там появилась опция создания собственных метрик, теперь сможем создавать различные особые метрики и наблюдать за ними.
Вместо заключения
Наше решение можно взять здесь и использовать. Будет здорово, если инструмент окажется вам полезным.
Если вы пользуетесь Platform V Pangolin или Platform V Kintsugi, то знайте, что у команды есть канал и чат, где можно задавать вопросы про настройку и не только.
Надеюсь, что читать вам было так же интересно, как и нам искать и перебирать решения для сбора метрик :) Будем рады, если наш опыт пригодится тем, кто задумывается о кастомизации postgres_exporter. Если появились вопросы о деталях решения, которые мы могли упустить, или есть желание поделиться своим опытом, приходите в комментарии.