# Дополнительные запросы для postgres_exporter (PG_EXPORTER_EXTEND_QUERY_PATH). # # Здесь только то, отсутствие чего уже стоило времени. Каждый блок — ответ на # конкретный разбор постфактум, а не «полезно иметь». # # cache_seconds стоит у всех: экспортер скрейпится раз в 60 с, а часть запросов # трогает pg_class по всей базе. Кэш держит нагрузку на уровне шума. # ── Раздутие: обновления, идущие МИМО HOT ──────────────────────────────────── # Разбор #2992/#2989: у `listings` 198 апдейтов на строку при доле HOT 0,43 %. # Каждый не-HOT апдейт переписывает строку во все индексы и заново тостит # описание — отсюда TOAST 15 ГБ при ~230 МБ живого содержимого. Копилось 91 день, # потому что смотреть было не на что. pg_table_write_amplification: query: | SELECT schemaname AS schema, relname AS table, n_tup_ins AS tup_ins, n_tup_upd AS tup_upd, n_tup_del AS tup_del, n_tup_hot_upd AS tup_hot_upd, n_live_tup AS live_tup, n_dead_tup AS dead_tup, COALESCE(EXTRACT(EPOCH FROM (now() - last_autovacuum)), -1) AS last_autovacuum_age_s, COALESCE(EXTRACT(EPOCH FROM (now() - last_autoanalyze)), -1) AS last_autoanalyze_age_s FROM pg_stat_user_tables WHERE n_tup_upd > 0 OR n_live_tup > 10000 cache_seconds: 60 metrics: - schema: { usage: "LABEL", description: "Схема" } - table: { usage: "LABEL", description: "Таблица" } - tup_ins: { usage: "COUNTER", description: "Вставлено строк" } - tup_upd: { usage: "COUNTER", description: "Обновлено строк" } - tup_del: { usage: "COUNTER", description: "Удалено строк" } - tup_hot_upd: { usage: "COUNTER", description: "Из них HOT — не трогают индексы" } - live_tup: { usage: "GAUGE", description: "Живых строк" } - dead_tup: { usage: "GAUGE", description: "Мёртвых строк — работа для vacuum" } - last_autovacuum_age_s: { usage: "GAUGE", description: "Секунд с последнего autovacuum, -1 если не было" } - last_autoanalyze_age_s: { usage: "GAUGE", description: "Секунд с последнего autoanalyze, -1 если не было" } # ── Размеры: куда именно уходит диск ───────────────────────────────────────── # Отдельно heap, индексы и TOAST. Суммарный размер таблицы этого не показывает, # а именно разделение объясняло, почему `listings` весила 19 ГБ при 230 МБ данных. pg_table_size_detail: query: | SELECT n.nspname AS schema, c.relname AS table, pg_relation_size(c.oid) AS heap_bytes, pg_indexes_size(c.oid) AS index_bytes, COALESCE(pg_total_relation_size(c.reltoastrelid), 0) AS toast_bytes, pg_total_relation_size(c.oid) AS total_bytes FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'r' AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND pg_total_relation_size(c.oid) > 10485760 cache_seconds: 300 metrics: - schema: { usage: "LABEL", description: "Схема" } - table: { usage: "LABEL", description: "Таблица" } - heap_bytes: { usage: "GAUGE", description: "Сами строки" } - index_bytes: { usage: "GAUGE", description: "Индексы" } - toast_bytes: { usage: "GAUGE", description: "TOAST — вынесенные длинные значения" } - total_bytes: { usage: "GAUGE", description: "Итого с учётом всего" } # ── WAL: сколько журнала генерится ─────────────────────────────────────────── # Замер 20.08: 7,02 ГБ/сутки при примерно четырёх пользовательских расчётах в # сутки. Диспропорция такого масштаба и есть симптом — но заметить её можно # только имея ряд. pg_wal_bytes: query: | SELECT CAST(pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0') AS BIGINT) AS wal_bytes_total, (SELECT count(*) FROM pg_ls_waldir()) AS wal_segments cache_seconds: 60 metrics: - wal_bytes_total: { usage: "COUNTER", description: "Позиция WAL от начала — рост даёт байт/сек" } - wal_segments: { usage: "GAUGE", description: "Сегментов в pg_wal сейчас" } # ── Горизонт vacuum и долгие транзакции ────────────────────────────────────── # Разбор #2607: осиротевшие запросы висели 46 часов и держали горизонт, из-за # чего vacuum не мог убрать мёртвые строки во всей базе. Одна забытая транзакция # отравляет весь кластер, и снаружи это выглядит просто как «база пухнет». pg_activity_horizon: query: | SELECT COALESCE(MAX(EXTRACT(EPOCH FROM (now() - xact_start))), 0) AS oldest_xact_age_s, COALESCE(MAX(EXTRACT(EPOCH FROM (now() - query_start))), 0) AS oldest_query_age_s, count(*) FILTER (WHERE state = 'idle in transaction') AS idle_in_transaction, count(*) FILTER (WHERE wait_event_type = 'Lock') AS waiting_on_lock, count(*) FILTER (WHERE backend_type = 'client backend') AS client_backends FROM pg_stat_activity WHERE backend_type = 'client backend' cache_seconds: 30 metrics: - oldest_xact_age_s: { usage: "GAUGE", description: "Возраст самой старой транзакции, сек" } - oldest_query_age_s: { usage: "GAUGE", description: "Возраст самого старого запроса, сек" } - idle_in_transaction: { usage: "GAUGE", description: "Открыта транзакция и ничего не делает — держит горизонт" } - waiting_on_lock: { usage: "GAUGE", description: "Ждут блокировку" } - client_backends: { usage: "GAUGE", description: "Клиентских соединений" } # ── Размер баз ─────────────────────────────────────────────────────────────── # У GlitchTip нет политики ретенции вообще (проверено: grep по retention даёт # ноль попаданий), база растёт без ограничения. Нужен ряд, чтобы поймать это # до того, как кончится диск. pg_database_size_bytes_detail: query: | SELECT datname AS database, pg_database_size(datname) AS bytes FROM pg_database WHERE datistemplate = false AND datallowconn = true cache_seconds: 300 metrics: - database: { usage: "LABEL", description: "База" } - bytes: { usage: "GAUGE", description: "Размер, байт" }