Добрый день, читатели habr!


Prometheus и его экосистема экспортеров (агентов), является хорошим инструментом для любого администратора и разработчика. Простота доставки, простота (относительная) настройки, возможность использования сервиса автоматического обноружения.
Но речь пойдет не столько о Prometheus, сколько об одном из примечательных агентов, а именно о postgres_exporter. Он позволяет собирать метрики с PostgreSQL. Но если бы всё было так просто...


К сожалению, документация у postgres_exporter достаточно аскетична и затрагивает только общие случаи. Но что делать, если вам достался экземпляр кластера СУБД с несколькими базами и/или вы хотите собирать метрики по нескольким экземплярам кластера сразу.


Цель


Собственно, о цели статьи или скорее заметки. Сразу отмечу, что здесь я не буду описывать процессы сборки или настройки Prometheus и postgres_exporter, их взаимодействия. Всё это доступно описано в документации и во многих других источниках.


Мне хотелось бы, затронуть некоторые частные случаи использования postgres_exporter, для решения конкретных задач, а именно сбор метрик одним агентом с:


  1. нескольких баз данных в одном экземпляре;
  2. нескольких экземпляров;
  3. нескольких баз данных на разных экземплярах.

postgres_exporter


Субъективно, плюсы и минусы.


Из плюсов:


  1. Первое и немаловажное преимущество, простота доставки и настройки агента. Агент — представляет собой исполняемый файл (опционально, yaml-файл с набором пользовательских метрик). Это самодостаточное приложение, скомпилированное под необходимый дистрибутив и архитектуру, и не требующее установки дополнительных пакетов на сервер. Агент можно устанавливать как на одном узле с экземпляром кластера, так и на отдельном узле;
  2. Агент подключается к СУБД как обычный sql-клиент. Возможно подключение как через inet, так и через unix-сокет;
  3. Возможность получать метрики одним агентом, с нескольких экземпляров экземпляров и/или с нескольких баз данных одного экземпляра;
  4. Сбор метрики производится так часто, как их запрашивает Prometheus или другой коллектор;
  5. Возможность получать метрики простым HTTP-запросом;
  6. Автоматическое получение, агентом, списка баз данных на одном экземпляре PostgreSQL, с версии postgres_exporter 0.5.0+ появилась опция --auto-discover-databases.

Из минусов:


  1. Отсутствие авторизации;
  2. Передача данных, только, по протоколу HTTP. Все метрики будут передаваться открытым текстом. А это плохо, так как злоумышленник, при перехвате, может получить достоверный список баз данных и ролей;
  3. Не кэширует метрики. Таким образом, например при сетевой недоступности агента данные за период недоступности, в Prometheus, не поступят;
  4. При использовании опции --auto-discover-databases, нет возможности исключить определённые базы данных из списка. Это скорее временное, так как в следующем релизе такая возможность уже должна появиться (опция --exclude-databases).

Несколько баз данных в одном экземпляре


Ну что же, перейдем к практике. Предположим, что у нас имеется экземпляр PostgreSQL с несколькими базами данных и нужно организовать сбор метрик экземпляра и всех баз данных.
Почему я разделил сбор метрик баз данных и экземпляра кластера, всё очень просто, сценарий работы postgres_exporter с несколькими базами данных на одном кластере, подразумевает выполнение одного и того же набора sql-запросов в разных базах данных. И как следствие, при попытке получить метрики из представлений pg_stat_replication, pg_stat_activity, pg_stat_statement и т.п. являющимися общими для кластера, мы всегда получим, в понимании postgres_exporter, один и те же набор метрик который будет приводить к возникновению дублей ключей и значений, что приведет к ошибке.
Посмотрим как это выглядитна практике.


Мы имеем тестовый экземпляр с набором баз данных:


                                 List of databases
   Name    |  Owner   | Encoding |  Collate   |   Ctype    |   Access privileges   
-----------+----------+----------+------------+------------+-----------------------
 dbtest1   | postgres | UTF8     | en_US.utf8 | en_US.utf8 | 
 dbtest2   | postgres | UTF8     | en_US.utf8 | en_US.utf8 | 
 dbtest3   | postgres | UTF8     | en_US.utf8 | en_US.utf8 | 
 postgres  | postgres | UTF8     | en_US.utf8 | en_US.utf8 | 

Запускаем postgres_exporter, с опцией --auto-discover-databases(если в строке подключения не указано имя базы данных, то подключение будет производится к базе данных с именем пользователя):


$ DATA_SOURCE_NAME="postgresql://postgres@127.0.0.1:5432/?sslmode=disable" ./postgres_exporter --auto-discover-databases --log.format=logger:stdout

  • DATA_SOURCE_NAME — переменная среды окружения, содержащая параметры подключения к экземпляру PostgreSQL

В выводе агента, наблюдаем идиллическую картину, он запущен и смог подключиться ко всем базам данных в кластере (хоть он и не пишет к каким именно, но будем надеяться это исправят):


INFO[0000] Established new database connection to "127.0.0.1:5432". source="postgres_exporter.go:788"
INFO[0000] Established new database connection to "127.0.0.1:5432". source="postgres_exporter.go:788"
INFO[0000] Semantic Version Changed on "127.0.0.1:5432": 0.0.0 -> 11.5.0 source="postgres_exporter.go:1251"
INFO[0000] Semantic Version Changed on "127.0.0.1:5432": 0.0.0 -> 11.5.0 source="postgres_exporter.go:1251"
INFO[0000] Established new database connection to "127.0.0.1:5432". source="postgres_exporter.go:788"
INFO[0000] Semantic Version Changed on "127.0.0.1:5432": 0.0.0 -> 11.5.0 source="postgres_exporter.go:1251"
INFO[0000] Established new database connection to "127.0.0.1:5432". source="postgres_exporter.go:788"
INFO[0000] Semantic Version Changed on "127.0.0.1:5432": 0.0.0 -> 11.5.0 source="postgres_exporter.go:1251"
INFO[0000] Established new database connection to "127.0.0.1:5432". source="postgres_exporter.go:788"
INFO[0000] Semantic Version Changed on "127.0.0.1:5432": 0.0.0 -> 11.5.0 source="postgres_exporter.go:1251"
INFO[0000] Starting Server: :9187 source="postgres_exporter.go:1490"

Думаю внимательный читатель заметит, что баз у нас в кластере четыре (postgres, dbtest1, dbtest2 и dbtest3, template0 и template1 — игнорируются), а подключений установлено пять. В нашем случае, postgres_exporter создает два подключения к базе данных postgres. И с этой особенностью нужно быть очень внимательным. Почему? Об этом узнаем чуть дальше.


Ну что же, продолжим и по пытаемся получить метрики:


$ curl http://localhost:9178/metrics

В результате, в выводе получаем предупреждения о дубликатах "was collected before with the same name and label values" (а вот в логе postgres_exporter мы предупреждений не увидим):


...
* collected metric pg_stat_activity_max_tx_duration label:<name:"datname" value:"dbtest1" > label:<name:"server" value:"127.0.0.1:5432" > label:<name:"state" value:"fastpath function call" > gauge:<value:0 >  was collected before with the same name and label values
* collected metric pg_stat_bgwriter_checkpoints_timed label:<name:"server" value:"127.0.0.1:5432" > counter:<value:1 >  was collected before with the same name and label values
...

Единственный способ избавится от ошибок, это отключить сбор метрик по-умолчанию. Это можно сделать двумя способами, первый установить переменные окружения PG_EXPORTER_DISABLE_DEFAULT_METRICS и PG_EXPORTER_DISABLE_SETTINGS_METRICS в true или использовать опции --disable-default-metrics и --disable-settings-metrics


Перезапускаем postgres_exporter с дополнительными опциями:


$ DATA_SOURCE_NAME="postgresql://postgres@127.0.0.1:5432/?sslmode=disable" ./postgres_exporter --auto-discover-databases --log.format=logger:stdout --disable-default-metrics --disable-settings-metrics

Пытаемся получить метрики:


$ curl http://localhost:9178/metrics

И вот, все прошло по плану, но в выводе нет ни одной метрики связанной с PostgreSQL:


# HELP go_gc_duration_seconds A summary of the GC invocation durations.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 0
go_gc_duration_seconds{quantile="0.25"} 0                                   
go_gc_duration_seconds{quantile="0.5"} 0                                    
go_gc_duration_seconds{quantile="0.75"} 0                                   
go_gc_duration_seconds{quantile="1"} 0                                      
go_gc_duration_seconds_sum 0                                                
go_gc_duration_seconds_count 0 
...
# HELP process_virtual_memory_bytes Virtual memory size in bytes.
# TYPE process_virtual_memory_bytes gauge
process_virtual_memory_bytes 1.3832192e+07

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


Для теста, будем собирать метрики из отношения pg_statio_user_tables. Для этого создадим файл queries.yaml, со следующим содержимым:


pg_statio_user_tables:
  query: "SELECT current_database() as datname, schemaname, relname, heap_blks_read, heap_blks_hit FROM pg_statio_user_tables"
  metrics:
    - datname:
        usage: "LABEL"
        description: "Name of database"
    - schemaname:
        usage: "LABEL"
        description: "Name of the schema that this table is in"
    - relname:
        usage: "LABEL"
        description: "Name of this table"
    - heap_blks_read:
        usage: "COUNTER"
        description: "Number of disk blocks read from this table"
    - heap_blks_hit:
        usage: "COUNTER"
        description: "Number of buffer hits in this table"

Думаю тут необходимо пояснить один момент, а именно добавление имени базы данных, в которой выполняется запрос. Это обязательное требование, и на это есть как минимум две причины:


  1. В базах данных могут быть таблицы с одинаковыми именами, что приведет к ошибке из за дублирования метрик;
  2. Без этого вы не сможете идентифицировать, к какой базе данных относится метрика, что превратит собранные данные в мусор.

И так, запускаем нашего агента с опцией --extend.query-path (здесь указывается путь к yaml-файлу с описанием метрик):


DATA_SOURCE_NAME="postgresql://postgres@127.0.0.1:5432?sslmode=disable" ./postgres_exporter --log.format=logger:stdout --auto-discover-databases --disable-default-metrics --disable-settings-metrics --extend.query-path=./queries.yaml

Пытаемся получить метрики (для наглядности, возьмём только pg_statio_user_tables_heap_blks_hit):


curl -s http://localhost:9187/metrics | grep pg_statio_user_tables_heap_blks_hit

В результате, получаем однозначно интерпретируемый набор метрик:


# HELP pg_statio_user_tables_heap_blks_hit Number of buffer hits in this table
# TYPE pg_statio_user_tables_heap_blks_hit counter
pg_statio_user_tables_heap_blks_hit{datname="dbtest1",relname="t1",schemaname="public",server="127.0.0.1:5432"} 0
pg_statio_user_tables_heap_blks_hit{datname="dbtest1",relname="t2",schemaname="public",server="127.0.0.1:5432"} 0
pg_statio_user_tables_heap_blks_hit{datname="dbtest2",relname="t1",schemaname="public",server="127.0.0.1:5432"} 0
pg_statio_user_tables_heap_blks_hit{datname="dbtest2",relname="t2",schemaname="public",server="127.0.0.1:5432"} 0
pg_statio_user_tables_heap_blks_hit{datname="dbtest3",relname="t1",schemaname="public",server="127.0.0.1:5432"} 0
pg_statio_user_tables_heap_blks_hit{datname="dbtest3",relname="t2",schemaname="public",server="127.0.0.1:5432"} 0

В итоге, мы получили возможность, используя опцию --auto-discover-databases, производить сбор метрик со всех баз данных одного экземпляра кластера. Приятным бонусом, может послужить то, что при добавлении новой базы данных не потребуется перезапускать агента.
Но при всем этом, мы остались без метрик экземпляра. Выход, на текущий момент, только один — использовать разных агентов для сбора метрик баз данных и экземпляра.
Выглядит конечно не очень, но данную неприятность возможно нивелировать группируя агентов для сбора метрик с нескольких экземпляров. Эту, еще одну, достаточно интересную возможность рассмотрим ниже.


Ответ на загадку о 'лишнем' подключении

Помните, в начале, мы обратили внимание на "лишнее" подключение.Так вот, это особенность работы postgres_exporter с опцией --auto-discover-databases.
Но почему это может доставить много неприятностей? На самом деле все просто и уже описано выше, а именно проблема в том, что postgres_exporter будет собирать метрики с базы данных postgres дважды и начнет дублировать метрики. Помочь в нашем случае, сможет только появление опции --exclude-databases (так что ждем, с нетерпением, следующий релиз).
И да, если у вас в базе данных postgres будут пользовательские таблицы, то пример приведенный выше окажется нерабочим.


Несколько экземпляров


Ну что же, двигаемся дальше. Мы разобрались как получать метрики с нескольких баз данных, теперь рассмотрим вариант, как производить мониторинг, одним агентом нескольких экземпляров. Это очень просто, для этого достаточно перечислить их в переменной окружения DATA_SOURCE_NAME разделяя запятой:


$ DATA_SOURCE_NAME="postgresql://postgres@127.0.0.1:5432/postgres?sslmode=disable,postgresql://postgres@127.0.0.1:5434/postgres?sslmode=disable" ./postgres_exporter --log.format=logger:stdout

Здесь мы подключаемся к двум разным экземплярам кластера, запущенным, в нашем случае, на локальном узле. Вот как это выглядит в логах:


INFO[0000] Established new database connection to "127.0.0.1:5432".  source="postgres_exporter.go:788"
INFO[0000] Semantic Version Changed on "127.0.0.1:5432": 0.0.0 -> 11.5.0  source="postgres_exporter.go:1251"
INFO[0000] Established new database connection to "127.0.0.1:5434".  source="postgres_exporter.go:788"
INFO[0000] Semantic Version Changed on "127.0.0.1:5434": 0.0.0 -> 11.5.0  source="postgres_exporter.go:1251"
INFO[0000] Starting Server: :9187                        source="postgres_exporter.go:1490"

Далее, пытаемся получить метрики (для наглядности ограничимся метрикой pg_stat_database_blk_read_time):


curl -s http://localhost:9187/metrics | grep pg_stat_database_blk_read_time

В результате, с одного агента, получаем метрики по обоим экземплярам:


# HELP pg_stat_database_blk_read_time Time spent reading data file blocks by backends in this database, in milliseconds
# TYPE pg_stat_database_blk_read_time counter
pg_stat_database_blk_read_time{datid="1",datname="template1",server="127.0.0.1:5432"} 0
pg_stat_database_blk_read_time{datid="1",datname="template1",server="127.0.0.1:5434"} 0
pg_stat_database_blk_read_time{datid="13116",datname="template0",server="127.0.0.1:5432"} 0
pg_stat_database_blk_read_time{datid="13116",datname="template0",server="127.0.0.1:5434"} 0
pg_stat_database_blk_read_time{datid="13117",datname="postgres",server="127.0.0.1:5432"} 0
pg_stat_database_blk_read_time{datid="13117",datname="postgres",server="127.0.0.1:5434"} 0
pg_stat_database_blk_read_time{datid="16384",datname="dbtest1",server="127.0.0.1:5432"} 0
pg_stat_database_blk_read_time{datid="16385",datname="dbtest2",server="127.0.0.1:5432"} 0
pg_stat_database_blk_read_time{datid="16386",datname="dbtest3",server="127.0.0.1:5432"} 0

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


Резюме


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


В итоге, что же мы имеем в сухом остатке, postgres_exporter, на мой взгляд, достаточно интересный и перспективный инструмент администратора для мониторинга экземпляров кластера PostgreSQL и развернутых на них баз данных. Но в силу своего возраста, не лишенный недостатков, которые вполне можно понять и простить.


Источники


  • Prometheus [1] — это приложение с открытым исходным кодом, используемое для мониторинга событий и оповещения о них. Он записывает метрики в реальном времени в базу данных временных рядов, построенную с использованием модели HTTP-запроса, с гибкими запросами и оповещениями в режиме реального времени.
  • postgres_exporter — это экспортер метрик PostgreSQL для Prometheus.

Версия, на момент написания заметки, v 0.5.1. Поддерживаемые версии PostgreSQL 9.4+ (в исходном коде замечено ограничение версии 9.1+).

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


  1. Sleuthhound
    23.09.2019 19:35

    Есть же замечательная штука PMM — Percona Monitoring and Management, который под капотом содержит тот же Prometheus + еще кучку всего и из коробки умеет мониторить MySQL, Pg и MongoDB


    1. sfalkongm Автор
      23.09.2019 20:11

      Возможно. Ещё есть mamonsu для zabbix, а так же это можно делать скриптами и вероятно есть ещё много разных инструментов… Но к заметке выше это не имеет ни какого отношения.
      Я не сравнивал решения, а разобрал вполне конкретную ситуацию и не более того.


    1. mcleod095
      23.09.2019 20:40

      То есть если у меня уже стоит prometheus и даже если поднят тот же consul, то предполагается что я буду ставить дополнительно еще одну систему мониторинга отдельно для БД в виде PMM?
      хотя PMM это тот же prometheus + consul + клиент для регистрации и куча хороших шаблонов.
      Кстати шаблоны percona не скрывает и они лежат в открытом доступе. Для себя я взял их за основу, раскатил exporter и донастроил шаблоны под себя.


  1. Vitaly2606
    23.09.2019 20:28

    Для информации: есть еще pgwatch2, я остановился на этом решении.
    github.com/cybertec-postgresql/pgwatch2

    Главный плюс (по моему мнению) нет необходимости ставить каких либо агентов/экспортёров на хосты PG.
    Остальные возможности, указаны в описании репозитория.


    1. sfalkongm Автор
      23.09.2019 21:50

      Может показаться, что я «топлю» за использование postgres_exporter… Но все таки отмечу, что он так же не требует установки на одном узле с PG. И в какой то мере, безопаснее ставить экспортер ближе к prometheus так как postgres_exporter не умеет шифровать трафик до prometheus, но может общаться с PG по ssl


  1. fessmage
    24.09.2019 10:32

    Насчет отсутствия авторизации и TLS — это общее место для всех prometheus exporter и это осознанная позиция их разработчиков. Они считают что экспортер должен заниматься только непосредственное собственной задачей, обеспечение защищенности канала предоставляется на усмотрение пользователя. И нельзя сказать что они не правы — ведь в ином случае код каждого из сотен экспортеров должен был включать работу с авторизацией и TLS.
    По-умолчанию считается, что на хосте который мы мониторим должна быть private network и экспортер должен слушать интерфейс внутренней сети — в этом случае необходимость доп. защиты отсутствует.
    Если же возможности такой нет, а у нас доступен только публичный адрес, то можно сделать две вещи:


    • поставить экспортер на локальный интерфейсе, а на публичный адрес поставить nginx, который будет по определенному location проксировать запрос на экспортер, плюс обеспечивать tls и авторизацию
    • поставить экспортер на локальный интерфейс, а на публичный адрес поставить telegraf (который умеет и в авторизацию и в tls, а также может собой заменить и некоторые экспортеры), а уже telegraf заставить собирать данные с prometheus экспортеров хоста и отдавать их же в prometheus формате.
      На текущий момент я использую последний вариант — на инстанс ставится telegraf, все prometheus экспортеры подключаются к нему локально, а prometheus сервер стучится только на один target хоста — в telegraf.


    1. Sleuthhound
      24.09.2019 13:48

      Позиция может и верная, но в результате чтобы мониторить пяток сервисов нужно поставить на сервер 5 разных экспортеров, да еще потом воткнуть и настроить тот же nginx (к примеру) и получается гора софта и гора настроек — усложнение на ровном месте.