Мониторинг query interner

Query interner дедуплицирует тексты Parse в памяти процесса pg_doorman. Хранилище разделено на две независимые хеш-таблицы — для именованных prepared (NAMED) и анонимных (ANON); каждая работает по своей политике. NAMED чистит пассивный сборщик по Arc::strong_count. ANON вытесняет по бездействию через query_interner_anon_idle_ttl_seconds. Обе половины публикуют метрики Prometheus: текущие значения, счётчики вытеснений и гистограмму длительности уборки. Плюс счётчик синтетических ошибок SQLSTATE 26000 — этот код pg_doorman возвращает клиентам, чей анонимный prepared statement выпал из всех кешей.

Эта страница помогает оператору пользоваться этими метриками: готовый дашборд, правила алертов и приёмы настройки.

Дашборд

Главные панели (на первом экране)

  1. Сводка — общий объём интернера. sum(pg_doorman_query_interner_bytes) в разрезе инстансов. Красный порог 1.5 ГиБ, жёлтый — 500 МиБ. Главный сигнал по памяти.
  2. График — записи по типу. Две линии:
    • pg_doorman_query_interner_entries{kind="named"}
    • pg_doorman_query_interner_entries{kind="anonymous"} Окно шесть часов. Устойчивый рост любой из линий — повод открыть панели детализации.
  3. График — частота синтетических 26000. rate(pg_doorman_query_interner_synthetic_misses_total[5m]). Норма — плоский ноль. Любой всплеск означает: либо TTL вытеснил запись, на которую сослался клиент, либо драйвер рассчитывает на безымянный prepared statement, который остаётся доступным после Sync.

Детализация

  1. Скорость вытеснений с разбивкой по причине: sum by (kind, reason) (rate(pg_doorman_query_interner_evictions_total[5m])).
  2. Тепловая карта длительности уборки: histogram_quantile(0.5, rate(pg_doorman_query_interner_gc_duration_seconds_bucket[5m])), с P99 поверх.
  3. Среднее число байт на запись по типу: pg_doorman_query_interner_bytes / pg_doorman_query_interner_entries.

Корреляции

  1. Скорость вытеснений ANON в сравнении с общим темпом запросов. Линейная корреляция говорит о здоровом трафике; нелинейная — о взрыве динамического SQL от ORM.
  2. Частота синтетических 26000 в сравнении с P99 latency запросов. Корреляция означает, что TTL режет живой трафик; разбираться с медленной веткой.

Переменные дашборда

  • instance — сравнивать реплики.
  • kind — отфильтровать значения и счётчик до одной из половин.

Лейблы pool, user и database к интернеру неприменимы — он один на процесс. На панелях интернера они только введут читателя в заблуждение.

Правила алертов

Готовый блок groups: лежит в monitoring/prometheus-rules/query-interner.yaml. Пять алертов.

  • PgDoormanAnonInternerMemoryHigh (critical) — байты ANON выше 1.5 ГиБ. Уменьшить TTL или проверить ORM на динамический SQL.
  • PgDoormanAnonTTLTooShort (critical) — синтетические 26000 чаще 1/с в течение 10 минут. Сначала определить источник: клиентский LRU, RESET INTERNER, TTL-вытеснение anonymous-записей или поведение драйвера.
  • PgDoormanAnonInternerNotShrinking (warning) — ANON растёт, а вытеснение по TTL не идёт. TTL слишком велик, либо поток уникальных запросов превышает скорость их истечения.
  • PgDoormanInternerGCSlow (warning) — P99 уборки выше 50 мс на 15-минутном окне. Увеличить query_interner_gc_interval_seconds (этот параметр работает только при перезапуске: reload не изменит частоту проходов у работающего процесса) или сделать RESET INTERNER и уменьшить размеры кешей.
  • PgDoormanNamedInternerGrowsUnbounded (warning) — больше 100 000 записей в NAMED при почти нулевом вытеснении. Почти всегда баг: ссылка на Arc<str> удерживается навсегда.

Защита от холодного старта: у всех алертов for: > 5m. Пустой интернер сразу после запуска процесса их не зажигает.

Размеры

Стационарный объём ANON-интернера при условии, что 50% запросов идут через prepared, а средний SQL — 2 КиБ:

RPSTTL = 60 сTTL = 300 с
100~12k записей / ~24 МиБ~60k / ~120 МиБ
1 000~120k / ~240 МиБ~600k / ~1.2 ГиБ
10 000~1.2M / ~2.4 ГиБразмер не рекомендуется

Интернер глобален на процесс, поэтому объём кластера растёт линейно с числом реплик pg_doorman. Берите это как стартовую оценку для query_interner_anon_idle_ttl_seconds и бюджета RAM на хост; фактическое значение даёт pg_doorman_query_interner_bytes.

Реальный TTL

Сборщик ходит в два прохода. На первом помечает запись как кандидата, на втором удаляет — но только если за это время к записи никто не обратился. Проход выполняется каждые gc_interval / 4 секунд. По умолчанию (gc_interval = 60 с, anon_idle_ttl = 60 с) это раз в 15 секунд. Пометка ставится через 60–75 секунд после последнего использования, удаление — на следующем проходе. Итого запись простаивает 75–120 секунд, прежде чем сборщик её выкинет. Снижать anon_idle_ttl ниже 60 секунд смысла мало: чаще, чем раз в gc_interval / 4, сборщик не запускается.

Приёмы настройки

Уменьшить TTL, когда давит память

Когда: горит PgDoormanAnonInternerNotShrinking, байты ANON подходят к лимиту памяти хоста.

Действие: уменьшить query_interner_anon_idle_ttl_seconds в general (например, с 60 до 30). Перечитать конфиг pg_doorman. Скорость вытеснений догонит новый порог.

Разобрать синтетические 26000 перед увеличением TTL

Когда: горит PgDoormanAnonTTLTooShort.

Действие: понять, какой клиент и какой запрос. У synthetic_misses нет меток, поэтому смотреть WARN-лог — он пишется на каждый промах и содержит client, pool, connection_id. Перед изменением конфига проверьте pg_doorman_clients_prepared_anonymous_evictions_total и pg_doorman_query_interner_evictions_total{kind="anonymous"}. Если промахи идут из клиентского Anonymous LRU, увеличьте client_anonymous_prepared_cache_size. Если они идут из TTL anonymous interner или от драйвера, который законно переиспользует безымянный Bind в следующей пачке, поднимите TTL (с 60 до 300, например). Если нет — переключите клиента на именованный prepared.

Сбросить интернер

Когда: разовая диагностика или жёсткое сжатие памяти под инцидентом.

Действие: psql "host=127.0.0.1 port=6432 user=admin dbname=pgdoorman" -c "RESET INTERNER". Возвращает CommandComplete RESET. Активные клиенты заново разберут запрос при следующем использовании. Короткоживущие клиенты эффекта не заметят: их last_anonymous_hash всё ещё помнит хеш, который они зарегистрировали до сброса, и следующий Bind увидит отсутствие записи и один раз получит 26000. Драйвер на это отреагирует повторным Parse.

Правила предвычислений (recording rules)

Кластерные агрегаты, которые имеет смысл предвычислять, чтобы дашборды были дешевле:

groups:
  - name: pg_doorman_query_interner_recording
    interval: 30s
    rules:
      - record: pg_doorman:query_interner_total_bytes:5m
        expr: sum without (instance) (pg_doorman_query_interner_bytes)
      - record: pg_doorman:query_interner_eviction_rate:5m
        expr: |
          sum without (instance) (rate(pg_doorman_query_interner_evictions_total[5m]))

Первое правило позволяет общей сводной панели читать одну серию. Второе рисует темп вытеснений с разбивкой по причине без пересчёта rate() на каждой перерисовке дашборда.