- PVSM.RU - https://www.pvsm.ru -

Мониторинг managed PostgreSQL в Yandex Cloud: практика с экспортером pgSCV и k8s

Цели

  • Настроить мониторинг managed PostgreSQL Yandex Cloud;

  • Деплой в k8s;

  • Сервис дисковеринг (экспортёр самостоятельно обнаруживает кластера и хосты БД);

  • Минимизировать нагрузку от экспортёра на БД и Victoria Metrics (собирать только нужные метрики с заданной частотой - т.к. не все метрики нужно пересчитывать при каждом скрейпе);

  • Избежать шумного поведения экспортёра (большой packet-rate на ноде).

Деплой pgSCV с двухуровневым кешем

Деплой pgSCV с двухуровневым кешем

Важно отметить, что цель предлагаемого подхода — получить управляемый и предсказуемый мониторинг, который:

  • минимизирует влияние на хосты PostgreSQL;

  • минимизирует трафик и потребление ресурсов внутри кластера Kubernetes;

  • сам актуализирует список хостов;

  • не генерирует шум в системе хранения метрик.

Поэтому в дальнейшем акцент сделан на кеширование, контроль частоты сбора и разделение метрик по «важности».

Предполагается, что у вас в k8s уже установлен victoria-metrics-k8s-stack [1]

Пространство имён k8s

Деплой будем осуществлять в pgscv namespace. Создайте файл namespace.yaml

---
apiVersion: v1
kind: Namespace
metadata:
  name: pgscv
$ kubectl apply -f namespase.yaml 
namespace/pgscv created

Подготовка PostgreSQL

В кластерах создайте пользователя pgscv c доступом к пользовательским БД и правами mdb_monitor. Пароль пользователя должен быть одинаковый на уровне folder в облаке.

В managed PostgreSQL от Yandex Cloud роль mdb_monitor даёт достаточные права для чтения статистики, при этом не позволяет выполнять DDL читать или изменять данные.

Примерное описание ресурса terraform
resource "yandex_mdb_postgresql_user" "pgscv" {
  cluster_id = yandex_mdb_postgresql_cluster.postgres.id
  name       = "pgscv"
  password   = "exporter_password"
  conn_limit = 15
  grants     = ["mdb_monitor"]
  
  settings   =  {
    pool_mode = "transaction"
  }
  
  permission {
    database_name = yandex_mdb_postgresql_cluster.db.name
  }
}

Ресурс приведён для иллюстрации

Пароль от пользователя pgscv отправим в секрет (измените my-password на реальный пароль)

postgresql-password.yaml:

---
apiVersion: v1
kind: Secret
stringData:
  PGPASSWORD: "my-password"
metadata:
  name: pgscv-exporter-postgresql-password
  namespace: pgscv
type: Opaque
kubectl apply -f postgresql-password.yaml 
secret/pgscv-exporter-postgresql-password created

Сервисная учётная запись (SA)

Создайте сервисную учётную запись с правами managed-postgresql.viewer и ключ авторизации.

Использование сервисного аккаунта и API облака позволяет отказаться от статического описания хостов PostgreSQL.

Это особенно важно для managed-сервисов, где:

  • хосты могут пересоздаваться;

  • добавляться или удаляться реплики;

  • меняться topology без участия пользователя.

pgSCV в этом сценарии выполняет роль service discovery, самостоятельно отслеживая актуальный состав кластеров и баз данных.

main.tf

terraform {
  required_providers {
    yandex = {
      source = "yandex-cloud/yandex"
    }
  }
  required_version = ">= 0.13"
}

provider "yandex" {
  zone = "ru-central1-a"
}

locals {
  folder_id = "укажите идентификатор"
}

resource "yandex_iam_service_account" "pgscv_exporter" {
  folder_id = local.folder_id
  name      = "pgscv-exporter"
}

resource "yandex_iam_service_account_key" "pgscv_exporter_sa_auth_key" {
  service_account_id = yandex_iam_service_account.pgscv_exporter.id
  description        = "SA for pgSCV exporter"
  key_algorithm      = "RSA_2048"
}

resource "yandex_resourcemanager_folder_iam_member" "permissions" {
  depends_on = [yandex_iam_service_account.pgscv_exporter]
  folder_id  = local.folder_id
  role       = "managed-postgresql.viewer"
  member     = "serviceAccount:${yandex_iam_service_account.pgscv_exporter.id}"
}

После выполнения terraform init && terraform apply в стейте будет содержаться ключ авторизации.

terraform.tfstate
{
      "mode": "managed",
      "type": "yandex_iam_service_account_key",
      "name": "pgscv_exporter_sa_auth_key",
      "provider": "provider["registry.terraform.io/yandex-cloud/yandex"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "created_at": "2026-01-05T06:57:45Z",
            "description": "SA for pgSCV exporter",
            "encrypted_private_key": null,
            "format": "PEM_FILE",
            "id": "xxxxxyyyyyzzzzz",
            "key_algorithm": "RSA_2048",
            "key_fingerprint": null,
            "output_to_lockbox": [],
            "output_to_lockbox_version_id": null,
            "pgp_key": null,
            "private_key": "PLEASE DO NOT REMOVE THIS LINE! Yandex.Cloud SA Key ID u003IDu003en-----BEGIN PRIVATE KEY-----nn-----END PRIVATE KEY-----n",
            "public_key": "-----BEGIN PUBLIC KEY-----nn-----END PUBLIC KEY-----n",
            "service_account_id": "xxxxxyyyyyzzzzz"
          },
          "sensitive_attributes": [
            [
              {
                "type": "get_attr",
                "value": "private_key"
              }
            ]
          ],
          "identity_schema_version": 0,
          "private": "encoded-private-key==",
          "dependencies": [
            "yandex_iam_service_account.pgscv_exporter"
          ]
        }
      ]
    }

Для формирования секрета в k8s нам понадобиться 6 полей:

  • created_at

  • id

  • key_algorithm

  • private_key

  • public_key

  • service_account_id

Создайте файл pgscv-exporter-cloud-sa.yaml

---
apiVersion: v1
kind: Secret
stringData:
  authorized_key.json: |
    {
      "created_at": "2026-01-05T06:57:45Z",
      "id": "",
      "key_algorithm": "RSA_2048",
      "private_key": "",
      "public_key": "",
      "service_account_id": ""
    }
metadata:
  name: pgscv-exporter-cloud-sa
  namespace: pgscv
type: Opaque
$ kubectl apply -f pgscv-exporter-cloud-sa.yaml
secret/pgscv-exporter-cloud-sa created

Memcached

pgSCV собирает часть метрик через достаточно тяжёлые SQL-запросы к системным каталогам и функциям PostgreSQL.

Если выполнять их при каждом scrape-запросе Prometheus, это приведёт к:

  • росту нагрузки на БД;

  • увеличению времени ответа экспортёра;

  • росту packet-rate внутри кластера.

Использование memcached позволяет:

  • разделить частоту scrape и частоту реального опроса БД;

  • настраивать частоту реального сбора метрик на уровне коллекторов (подсистем мониторинга);

  • получить общий кеш для подов экспортёра;

  • снизить нагрузку как на PostgreSQL, так и на систему мониторинга.

Создадим деплоймент memcached и проверим, что под создался.

memcached-deployment.yaml

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pgscv-cache
  namespace: pgscv
  labels:
    app: pgscv-cache
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pgscv-cache
  template:
    metadata:
      labels:
        app: pgscv-cache
    spec:
      nodeSelector: {}
      affinity: {}
      tolerations: []
      containers:
        - name: memcached
          image: memcached:1.6.39-alpine3.22
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 11211
          args:
            - --conn-limit=1024
            - --memory-limit=64
            - --threads=4
          resources: {}
$ kubectl apply -f memcached-deployment.yaml
deployment.apps/pgscv-cache created
$ deploy kubectl -n pgscv get pods
NAME                           READY   STATUS    RESTARTS   AGE
pgscv-cache-56b7fbb479-l98wj   1/1     Running   0          16s

Под в состоянии READY, отлично, проверим работу memcached.

В первом терминале прокинем порт

$ kubectl -n pgscv port-forward pods/pgscv-cache-56b7fbb479-l98wj 11211:11211
Forwarding from 127.0.0.1:11211 -> 11211
Forwarding from [::1]:11211 -> 11211

а во втором терминале в него постучимся

$ echo stats | netcat localhost 11211
STAT pid 1
STAT uptime 56
STAT time 1767606355
STAT version 1.6.39
STAT libevent 2.1.12-stable
STAT pointer_size 64
....

Создадим сервис

memcached-service.yaml

---
apiVersion: v1
kind: Service
metadata:
  name: pgscv-cache
  namespace: pgscv
  labels:
    app: pgscv-cache
spec:
  type: ClusterIP
  ports:
    - name: cache
      targetPort: 11211
      protocol: TCP
      port: 11211
  selector:
    app: pgscv-cache
$ kubectl apply -f memcached-service.yaml
service/pgscv-cache created

Memcached-exporter

Экспортёр для memcached не является обязательным компонентом схемы, но он позволяет:

  • наблюдать реальное потребление памяти кешем;

  • контролировать hit/miss ratio;

  • корректно подобрать лимиты памяти для memcached.

Это особенно полезно на этапе настройки, когда ещё не понятно, какие TTL и объёмы кеша оптимальны.

memcached-exporter-deployment.yaml

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pgscv-cache-exporter
  namespace: pgscv
  labels:
    app: pgscv-cache-exporter
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pgscv-cache-exporter
  template:
    metadata:
      labels:
        app: pgscv-cache-exporter
    spec:
      nodeSelector: {}
      affinity: {}
      tolerations: []
      containers:
        - name: memcached-exporter
          image: quay.io/prometheus/memcached-exporter:v0.15.3
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 9150
          args:
            - "--memcached.address=pgscv-cache:11211"
          resources: {}

memcached-exporter-service.yaml

---
apiVersion: v1
kind: Service
metadata:
  name: pgscv-cache-exporter
  namespace: pgscv
  labels:
    app: pgscv-cache-exporter
spec:
  type: ClusterIP
  ports:
    - name: metrics
      targetPort: 9150
      protocol: TCP
      port: 9150
  selector:
    app: pgscv-cache-exporter
$ kubectl apply -f memcached-exporter-service.yaml   
service/pgscv-cache-exporter created
$ kubectl apply -f memcached-exporter-service.yaml
service/pgscv-cache-exporter configured

Чтобы стек мониторинга начал скрейпить метрики, создадим service monitor. Можно сократить количество манифестов, если использовать pod monitor.

memcached-exporter-service-monitor.yaml

---
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMServiceScrape
metadata:
  name: pgscv-cache-exporter
  namespace: pgscv
  labels:
    app: pgscv-cache-exporter
spec:
  endpoints:
    - attach_metadata: {}
      port: metrics
      path: "/metrics"
      interval: "30s"
  selector:
    app: pgscv-cache-exporter
  namespaceSelector:
    matchNames:
      - pgscv

Экспортёр pgSCV

Документация на параметры экспортёра можно найти в вики [2] на GitHub.

Конфигурация pgSCV в данном примере построена вокруг нескольких принципов:

  • отключение неиспользуемых коллекторов (в managed решении коллекторы system и postgres/logs бесполезны);

  • ограничение количества соединений с БД (раздел pool);

  • агрессивное кеширование метрик, не требующих высокой актуальности, частый сбор которых нагружает БД;

  • отключено получение полных текстов запросов в лейблы метрик (no_track_mode: true).

  • сбор top-k метрик по таблицам, индексам и запросам (параметры collect_top_XXXX) .

Немного подробностей про top-k метрик:

  • collect_top_query

    Для каждого запроса из pg_stat_statements вычисляются независимые ранги (ROW_NUMBER()) по следующим показателям:

    • количество вызовов (calls);

    • количество обработанных строк (rows);

    • суммарное время выполнения (total_time);

    • время чтения и записи блоков (blk_read_time, blk_write_time);

    • shared buffers:

      • попадания,

      • чтения,

      • модификации,

      • записи;

    • статистика по local buffers;

    • использование временных файлов (temp_blks_read, temp_blks_written).

    Если запрос попадает в Top хотя бы по одному из этих критериев, он помечается как visible и включается в дальнейшую обработку.

  • collect_top_table

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

    Активность индексов и доступа

    • количество сканирований по индексам (idx_scan);

    • количество строк, полученных через индексы (idx_tup_fetch).

    Изменение данных

    • количество вставок (n_tup_ins);

    • количество обновлений (n_tup_upd);

    • количество HOT-обновлений (n_tup_hot_upd);

    • количество удалений (n_tup_del).

    Размер и «живость» данных

    • количество живых строк (n_live_tup);

    • количество мёртвых строк (n_dead_tup);

    • приблизительное количество строк (reltuples);

    • физический размер таблицы (pg_table_size(relid)).

    Статистика и обслуживание

    • количество изменений с последнего ANALYZE (n_mod_since_analyze);

    • количество запусков VACUUM (vacuum_count);

    • количество запусков AUTOVACUUM (autovacuum_count);

    • количество запусков ANALYZE (analyze_count).

    I/O-нагрузка

    • чтения heap-блоков (heap_blks_read);

    • попадания по индексам (idx_blks_hit);

    • чтения TOAST-блоков (toast_blks_read);

    • попадания TOAST-блоков в кеш (toast_blks_hit).

    Если таблица попадает в Top по любому из этих показателей, она помечается как visible и включается в дальнейшую обработку.

  • collect_top_index

    Для каждого пользовательского индекса вычисляются независимые ранги (row_number()) по следующим показателям:

    Использование индекса

    • количество сканирований по индексу (idx_scan);

    • количество строк, прочитанных через индекс (idx_tup_read);

    • количество строк, фактически возвращённых индексом (idx_tup_fetch).

    I/O-нагрузка

    • количество чтений индексных блоков с диска (idx_blks_read);

    • количество попаданий индексных блоков в кеш (idx_blks_hit).

    Размер индекса

    • физический размер индекса (pg_relation_size(indexrelid)).

    Если индекс попадает в Top по любому из этих показателей, он помечается как visible и включается в дальнейшую обработку.

Такой подход позволяет использовать pgSCV даже в окружениях с сотнями кластеров PostgreSQL без заметного влияния на производительность.

Если вы по какой то причине не будете использовать nginx proxy для кеша, то уберите параметр url_prefix

pgscv-configmap.yaml

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: pgscv-configmap
  namespace: pgscv
data:
  pgscv.yaml: |
    listen_address: '0.0.0.0:9890'
    conn_timeout: 10
    no_track_mode: true
    collect_top_query: 15
    collect_top_table: 15
    collect_top_index: 15
    skip_conn_error_mode: true
    disable_collectors:
      - system
      - postgres/logs
    pooler:
      max_conns: 5
      min_conns: 1
      min_idle_conns: 1
    url_prefix: http://nginx-pgscv.pgscv:9890
    cache:
      type: "memcached"
      server: pgscv-cache:11211
      ttl: 30s
      collectors:
        postgres/indexes:
          ttl: 10m
        postgres/tables:
          ttl: 10m
        postgres/statements:
          ttl: 5m
    discovery:
      yandex_mdb:
        type: yandex-mdb
        config:
          - authorized_key: /app/secret/authorized_key.json
            folder_id: "идентификатор папки в облаке"
            password_from_env: "PGPASSWORD"
            user: "pgscv"
            refresh_interval: 1
            clusters:
              - name: ".*"
                db:
                exclude_name:
                exclude_db:

Согласуйте параметры: GOMEMLIMIT должен быть примерно 90% от resources.limits.memory

pgscv-deployment.yaml

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pgscv
  namespace: pgscv
  labels:
    app: pgscv
spec:
  replicas: 2
  selector:
    matchLabels:
      app: pgscv
  template:
    metadata:
      labels:
        app: pgscv
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
      nodeSelector: {}
      affinity: {}
      tolerations: []
      volumes:
        - name: pgscv-config
          configMap:
            name: pgscv-configmap
            items:
              - key: pgscv.yaml
                path: pgscv.yaml
        - name: cloud-service-account-auth-key
          secret:
            secretName: pgscv-exporter-cloud-sa
      containers:
          - name: pgscv
            image: cherts/pgscv:v0.15.1
            imagePullPolicy: IfNotPresent
            securityContext:
              runAsNonRoot: true
              runAsUser: 1000
            resources:
              limits:
                cpu: 2
                memory: 1500Mi
              requests:
                cpu: 1
                memory: 1000Mi
            ports:
              - containerPort: 9890
                name: http
                protocol: TCP
            args:
              - --config-file=/app/conf/pgscv.yaml
            volumeMounts:
              - name: pgscv-config
                mountPath: /app/conf/
              - name: cloud-service-account-auth-key
                mountPath: /app/secret/
            env:
              - name: GOMEMLIMIT
                value: 1350MiB
              - name: LOG_LEVEL
                value: info
            envFrom:
              - secretRef:
                  name: pgscv-exporter-postgresql-password

pgscv-service.yaml

---
apiVersion: v1
kind: Service
metadata:
  name: pgscv
  namespace: pgscv
  labels:
    app: pgscv
spec:
  type: ClusterIP
  ports:
    - name: http
      targetPort: 9890
      protocol: TCP
      appProtocol: http
      port: 9890
  selector:
      app: pgscv

задеплоим, в configmap не забудьте поставить корректный folder_id

$ kubectl apply -f pgscv-configmap.yaml            
configmap/pgscv-configmap created
$ deploy kubectl apply -f pgscv-deployment.yaml
deployment.apps/pgscv created
$ kubectl apply -f pgscv-service.yaml 
service/pgscv created

Проверим, прокинув порт

$ kubectl -n pgscv port-forward services/pgscv 9890:9890 
Forwarding from 127.0.0.1:9890 -> 9890
Forwarding from [::1]:9890 -> 9890

откроем в браузере, убедимся что всё хорошо. Через некоторое время по ручке /targets будут возвращаться все хосты Managed PostgreSQL.

Мониторинг managed PostgreSQL в Yandex Cloud: практика с экспортером pgSCV и k8s - 2

Nginx proxy cache

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

От излишних запросов в PostgreSQL нас уже защищает memcached, но он кеширует только результаты SQL запросов, экспортёру pgSCV всё равно бы пришлось собирать метрики (нагружая CPU подов).

Размер кеша задаётся параметром max_size=1G

Время кеширования указывается последним параметром в директиве proxy_cache_valid

nginx-configmap.yaml

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-configmap
  namespace: pgscv
data:
  nginx.conf: |
    worker_processes 1;
    events {
      worker_connections 1024;
    }
    http {
      proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=metrics_cache:10m max_size=1G inactive=60m use_temp_path=off;
      
      server {
        listen 80;
        
        location / {
          proxy_pass http://pgscv:9890;
          proxy_cache metrics_cache;
          proxy_cache_valid 200 30s;
          proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
          add_header X-Proxy-Cache $upstream_cache_status;
        }
      }
    }

nginx-deployment.yaml

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  namespace: pgscv
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      nodeSelector: {}
      affinity: {}
      tolerations: []
      containers:
      - name: nginx
        image: nginx:1.22
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
        volumeMounts:
        - name: nginx-config
          mountPath: /etc/nginx/nginx.conf
          subPath: nginx.conf
        resources:
          limits:
            cpu: 1
            memory: 150Mi
          requests:
            memory: 100Mi
      volumes:
      - name: nginx-config
        configMap:
          name: nginx-configmap

nginx-service.yaml

---
apiVersion: v1
kind: Service
metadata:
  name: nginx-pgscv
  namespace: pgscv
  labels:
    app: nginx
spec:
  type: ClusterIP
  ports:
    - name: http
      targetPort: 80
      protocol: TCP
      appProtocol: http
      port: 9890
  selector:
    app: nginx

Задеплоим

$ kubectl apply -f nginx-configmap.yaml                                            
configmap/nginx-configmap created
$ kubectl apply -f nginx-deployment.yaml
deployment.apps/nginx created
$ kubectl apply -f nginx-service.yaml   
service/nginx-pgscv created

VictoriaMetrics scrape config

Использование httpSDConfigs позволяет VictoriaMetrics динамически получать ��писок целей от pgSCV, не требуя ручного обновления конфигурации при изменении состава кластеров PostgreSQL.

pgscv-scrape-config.yaml

---
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMScrapeConfig
metadata:
  name: pgscv
  namespace: victoria-metrics
spec:
  httpSDConfigs:
    - url: "http://nginx-pgscv.pgscv:9890/targets"
  metricRelabelConfigs:
    - sourceLabels: [host]
      targetLabel: shorthost
      regex: (.*).mdb.yandexcloud.net
      replacement: "$1"

Выводы

pgSCV в сочетании с кешированием и service discovery позволяет выстроить мониторинг, который:

  • даёт достаточную наблюдаемость;

  • не создаёт дополнительной нагрузки;

  • остаётся управляемым по мере роста инфраструктуры.

Пример утилизации ресурсов подами pgSCV + Memcached + Nginx + memcached-exporter

На графиках утилизация ресурсов k8s для стека, который мониторит 170 трехнодовых кластера PostgreSQL

CPU

CPU
Memory

Memory
network

network

Автор: d_bulashev

Источник [3]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/postgresql/441349

Ссылки в тексте:

[1] victoria-metrics-k8s-stack: https://docs.victoriametrics.com/helm/victoria-metrics-k8s-stack

[2] вики: https://github.com/cherts/pgscv/wiki/Configuration-settings-reference

[3] Источник: https://habr.com/ru/articles/983852/?utm_source=habrahabr&utm_medium=rss&utm_campaign=983852