vmsingle: один бинарник VictoriaMetrics вместо всего стека Prometheus
Published: 2026-06-09
Этот сайт живёт на одноузловом k0s-кластере на небольшом VPS, и на том же VPS крутятся сами сайты, почтовый сервер и полдюжины прокси. kube-prometheus-stack хочет больше памяти, чем всё это вместе взятое. Замена — vmsingle, single-node VictoriaMetrics: скрейпит, хранит и отвечает на запросы одним бинарником с лимитом 512 МБ. Без Prometheus, без оператора, без CRD.
Почему не kube-prometheus-stack
На рабочем многоузловом кластере kube-prometheus-stack — очевидный выбор: оператор, ServiceMonitor'ы, HA-пары. На VPS с двумя ядрами картина другая:
- один только Prometheus в покое ест 400–700 МБ даже со скромным списком таргетов
- оператор, admission-вебхуки и CRD добавляют поды, которые на одном узле не делают ничего полезного
- абстракция ServiceMonitor не нужна, когда все таргеты можно перечислить руками
У vmsingle есть встроенный скрейпер, который принимает обычные Prometheus scrape_configs как есть. Один под заменяет Prometheus + оператор, говорит на PromQL (плюс расширения MetricsQL) и приносит с собой VMUI для ad-hoc запросов.
Helm-релиз
Чарт — vm/victoria-metrics-single, разворачивается из values-файла:
yamlserver:
fullnameOverride: "vmsingle-server"
retentionPeriod: "90d"
persistentVolume:
enabled: true
existingClaim: vmsingle-data
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 50m
memory: 128Mi
extraArgs:
enableTCP6: "true"
envflag.enable: "true"
loggerFormat: json
vmalert.proxyURL: http://vmalert.monitoring.svc.cluster.local:8080
memory.allowedPercent: "20"
Два флага здесь важны:
memory.allowedPercent: 20— VictoriaMetrics рассчитывает размер внутренних кэшей как процент от доступной памяти. Значение по умолчанию (60%) берётся от cgroup-лимита, а когда на узле живёт ещё куча всего, кэши безопаснее держать маленькими. Скорость запросов на датасете такого размера не страдает.vmalert.proxyURL— VMUI проксирует вкладки алертинга в vmalert, и сработавшие правила видны в том же UI.
Срок хранения 90 дней для домашнего кластера — с запасом, и всё равно укладывается в считанные гигабайты: VictoriaMetrics сжимает медленно меняющиеся ряды сильно меньше байта на сэмпл.
Хранилище: hostPath PV с Retain
Storage class в кластере нет, поэтому PV объявлен руками и прибит к каталогу на узле:
yamlapiVersion: v1
kind: PersistentVolume
metadata:
name: vmsingle-data
spec:
capacity:
storage: 20Gi
accessModes: [ReadWriteOnce]
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /var/lib/victoria-metrics
type: DirectoryOrCreate
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: vmsingle-data
namespace: monitoring
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 20Gi
volumeName: vmsingle-data
storageClassName: ""
storageClassName: "" — несущая конструкция: пустая строка отключает динамический провижнинг, иначе PVC повиснет в Pending в ожидании провижионера по умолчанию, которого нет. Retain означает, что 90 дней метрик переживут helm uninstall.
Scrape-конфиг
scrape.enabled: true включает встроенный скрейпер. Конфиг — обычный синтаксис Prometheus:
yamlscrape:
enabled: true
config:
global:
scrape_interval: 30s
scrape_timeout: 10s
scrape_configs:
- job_name: node
static_configs:
- labels:
instance: k0s-node
targets:
- node-exporter-prometheus-node-exporter.monitoring.svc.cluster.local:9100
- job_name: kube-state-metrics
static_configs:
- targets:
- kube-state-metrics.monitoring.svc.cluster.local:8080
- job_name: traefik
static_configs:
- labels:
instance: traefik
targets:
- traefik.traefik.svc.cluster.local:9101
metric_relabel_configs:
- source_labels: [service]
regex: '(.+)-[0-9a-f]{16}@kubernetescrd'
replacement: '$1'
target_label: service
Везде static_configs — на одном узле со стабильными именами Service'ов service discovery не добавляет ничего, кроме движущихся частей. Полный список таргетов: node-exporter, kube-state-metrics, сам vmsingle, vmalert, Traefik, два экспортёра прокси, три джоба blackbox-exporter (HTTP-аптайм девяти сайтов, TCP-проверки портов прокси, внешняя DNS-связность), kubelet cAdvisor и textfile-коллектор для пиров WireGuard.
Relabel-правило для Traefik заслуживает отдельного абзаца: сервисы из IngressRoute CRD Traefik называет как namespace-name-<16 hex символов>@kubernetescrd. Хеш меняется при изменении роута, и любой дашборд с группировкой по service разваливается. Relabel срезает хеш прямо на этапе скрейпа.
Скрейп cAdvisor через kubelet
Метрики CPU и памяти по контейнерам отдаёт встроенный в kubelet cAdvisor — ему нужны TLS и токен:
yaml- job_name: cadvisor
scheme: https
tls_config:
insecure_skip_verify: true
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
metrics_path: /metrics/cadvisor
honor_labels: true
static_configs:
- targets:
- 91.184.248.13:10250
metric_relabel_configs:
- source_labels: [container]
regex: ".+"
action: keep
- source_labels: [container]
regex: "POD"
action: drop
Токен ServiceAccount работает, только если этому ServiceAccount разрешено ходить в API kubelet'а. Это маленький ClusterRole:
yamlapiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: vmsingle-kubelet-metrics
rules:
- apiGroups: [""]
resources: ["nodes", "nodes/metrics"]
verbs: ["get", "list", "watch"]
- nonResourceURLs: ["/metrics", "/metrics/cadvisor", "/metrics/resource"]
verbs: ["get"]
привязанный к ServiceAccount vmsingle-server. Два metric_relabel_configs выкидывают ряды pause-контейнеров (container="POD") и агрегаты по cgroup с пустым лейблом container — без них каждый под порождает два-три фантомных дубля, и дашборды считают память дважды.
Стабильное имя сервиса
Одно неочевидное дополнение — обычный ClusterIP Service перед vmsingle:
yamlapiVersion: v1
kind: Service
metadata:
name: vmsingle-stable
namespace: monitoring
spec:
selector:
app.kubernetes.io/instance: vmsingle
app.kubernetes.io/name: victoria-metrics-single
ports:
- port: 8428
targetPort: 8428
type: ClusterIP
Чарт создаёт headless Service, а headless DNS резолвится прямо в IP пода. Во время рестартов CoreDNS (на одном узле это каждая перезагрузка ноды) клиенты с закэшированным старым IP пода ловят connection refused. Grafana и vmalert смотрят на vmsingle-stable — ClusterIP переживает пересоздание пода.
Что может пойти не так
OOMKilled под нагрузкой запросов. Cgroup-лимит 512 МБ, и тяжёлые запросы по длинным диапазонам дают всплески. memory.allowedPercent: 20 держит кэши маленькими; если OOM всё равно случается — сначала поднимайте лимит, а не трогайте флаг: кэши меньше ~100 МБ замедляют вообще всё.
PVC висит в Pending. Почти всегда — отсутствующая пара storageClassName: "" / volumeName на самодельных PV+PVC. Смотрите kubectl -n monitoring describe pvc vmsingle-data: «waiting for first consumer» — нормально, «no persistent volumes available» — биндинг настроен неверно.
Скрейп cAdvisor возвращает 401. ClusterRoleBinding должен указывать на то имя ServiceAccount, которое чарт реально сгенерировал — fullnameOverride его меняет. Проверьте kubectl -n monitoring get sa и subject в биндинге.
Дашборды считают память контейнеров дважды. Прорвались ряды container="POD" и с пустым container. Убедитесь, что metric_relabel_configs есть в работающем конфиге: curl -s localhost:8428/config | grep -A5 cadvisor после port-forward.
Итого
- Один под
vmsingleзаменяет Prometheus, оператор и все его CRD — 128 МБ request, потолок 512 МБ - Встроенный скрейпер принимает обычные Prometheus
scrape_configs; на одном узле статические таргеты лучше service discovery - Для cAdvisor нужны RBAC на API kubelet'а и relabel-правила против дублей pause-контейнеров
- hostPath PV с
Retainи явным биндингом черезvolumeName— без storage class - Отдельный ClusterIP Service (
vmsingle-stable) спасает от протухшего headless DNS после рестартов CoreDNS - Срок хранения 90 дней на этом масштабе стоит единицы гигабайт