VictoriaLogs в k0s: база логов, которой хватает 128 МБ

Published: 2026-06-10

Метрики в этом кластере давно живут в VictoriaMetrics, а логи так и оставались на уровне kubectl logs и grep. Хотелось, чтобы access-логи Traefik можно было запрашивать — кто ходит на какой сайт, какие запросы падают — и не арендовать под это VPS побольше. VictoriaLogs оказалась полным ответом: один контейнер с лимитом памяти 128 МБ, hostPath-том на 5 ГБ и встроенный UI для запросов. Этот пост про серверную часть; доставка логов — в следующем.


Почему VictoriaLogs

Кандидаты на роль «логи на VPS с двумя ядрами, где живут и сами сервисы»:

  • Elasticsearch — хочет heap больше, чем у этого VPS оперативки. Отпал за секунды.
  • Loki — спроектирован под object storage и microservices mode; single-binary режим работает, но всё равно ест сотни МБ, а его правила по кардинальности лейблов переносят сложность в конфиг агента.
  • VictoriaLogs — один Go-бинарник, без JVM, без object storage, индексирует все поля, включая высококардинальные (IP, пути), и понимает Loki push protocol — значит, работает с любым существующим агентом.

Кластерную VictoriaLogs + Vector я уже эксплуатирую на работе, так что язык запросов (LogsQL) переносится как есть. Домашняя версия радикально меньше: одна реплика, один Deployment, без Helm-чарта — всё умещается в ~90 строк YAML.

Deployment

yamlapiVersion: apps/v1
kind: Deployment
metadata:
  name: victoria-logs
  namespace: logging
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: victoria-logs
  template:
    metadata:
      labels:
        app: victoria-logs
    spec:
      tolerations:
        - operator: Exists
      containers:
        - name: victoria-logs
          image: victoriametrics/victoria-logs:v1.11.0-victorialogs
          args:
            - --storageDataPath=/data
            - --retentionPeriod=7d
            - --httpListenAddr=:9428
            - --loggerFormat=json
          ports:
            - containerPort: 9428
              name: http
          resources:
            requests:
              cpu: 10m
              memory: 32Mi
            limits:
              cpu: 200m
              memory: 128Mi
          volumeMounts:
            - name: data
              mountPath: /data
          livenessProbe:
            httpGet:
              path: /health
              port: 9428
            initialDelaySeconds: 10
            periodSeconds: 30
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: victoria-logs-data

Три решения стоит проговорить:

strategy: Recreate. Том — ReadWriteOnce hostPath. RollingUpdate, применяемый по умолчанию, попытался бы поднять новый под, пока старый ещё держит каталог с данными, — два писателя в одном каталоге это верный способ испортить LSM-дерево. Recreate сначала убивает старый под. Цена — несколько секунд простоя ингеста на каждый деплой, и это не важно: агенты буферизуют и повторяют отправку.

Блок ресурсов. 32 МБ request, лимит 128 МБ — это реальные цифры с работающего экземпляра, который принимает access-логи Traefik для девяти сайтов. В стабильном состоянии VictoriaLogs здесь сидит на 40–60 МБ.

retentionPeriod=7d. Access-логи — операционные данные, не архив. Недели хватает на вопросы «что случилось»; всё, что должно жить дольше — это метрика, и она уже лежит в VictoriaMetrics со сроком хранения 90 дней.

Хранилище

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

yamlapiVersion: v1
kind: PersistentVolume
metadata:
  name: victoria-logs-data
spec:
  capacity:
    storage: 5Gi
  accessModes: [ReadWriteOnce]
  persistentVolumeReclaimPolicy: Retain
  hostPath:
    path: /var/lib/victoria-logs
    type: DirectoryOrCreate
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: victoria-logs-data
  namespace: logging
spec:
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 5Gi
  volumeName: victoria-logs-data

5 ГБ — намеренный запас. VictoriaLogs жмёт агрессивно: неделя access-логов этих сайтов занимает десятки МБ на диске. Запас нужен, чтобы добавление новых источников логов потом не превращалось в миграцию хранилища.

Ингест: три протокола, ноль плагинов

Service — обычный ClusterIP на 9428. Один и тот же порт принимает:

  • /insert/jsonline — родной формат VictoriaLogs
  • /insert/loki/api/v1/push — протокол Loki, на котором говорят Promtail, Grafana Alloy и loki-sink Vector'а
  • /insert/elasticsearch/_bulk — bulk API Elasticsearch, для Filebeat/Logstash

Это и есть практическая причина, почему VictoriaLogs так легко встраивается в существующую систему: любой агент, который вы уже умеете настраивать, шлёт в неё без модификаций. Мой агент доставки логов шлёт сюда:

http://victoria-logs.logging.svc.cluster.local:9428/insert/loki/api/v1/push

Запросы: VMUI и LogsQL

Встроенный UI живёт на :9428/select/vmui. Port-forward — и вперёд:

bashkubectl -n logging port-forward svc/victoria-logs 9428:9428
# → http://localhost:9428/select/vmui

LogsQL читается как конвейер. Селектор стрима, дальше фильтры:

{job="traefik-access"}                                  # всё из одного стрима
{job="traefik-access"} DownstreamStatus:~"[45][0-9][0-9]"   # только 4xx/5xx
{job="traefik-access"} RequestPath:"/feed" _time:1h      # один путь, последний час
_time:5m error                                           # слово "error" где угодно, 5 минут

В отличие от Loki, фильтры по полям вроде DownstreamStatus:~"[45][0-9][0-9]" не требуют, чтобы поле было лейблом стрима — VictoriaLogs индексирует сами поля лога. Это снимает целое проектное упражнение «какие лейблы безопасны по кардинальности».

Datasource в Grafana

Grafana нужен плагин victoriametrics-logs-datasource — ставится и провижинится из Helm values:

yamlplugins:
  - victoriametrics-logs-datasource

datasources:
  datasources.yaml:
    apiVersion: 1
    datasources:
      - name: VictoriaLogs
        type: victoriametrics-logs-datasource
        url: http://victoria-logs.logging.svc.cluster.local:9428
        access: proxy

После этого logs-панели в дашбордах принимают LogsQL-выражения напрямую.

Что может пойти не так

Liveness-проба убивает под на старте. После нечистого завершения VictoriaLogs проигрывает WAL до того, как начнёт отвечать /health. С коротким initialDelaySeconds kubelet прибивает её посреди реплея — и получается цикл рестартов. Для этого объёма данных хватает 10 секунд; если том больше — масштабируйте задержку вместе с ним (ровно этот класс проблем я уже ловил с другим stateful-подом на этой ноде и пришёл к initialDelaySeconds: 30).

Дашборды из провижининга показывают «datasource not found». Grafana назначает datasource случайный UID, если его не зафиксировать. Дашборд, закоммиченный с "uid": "victorialogs", ломается, когда созданный через провижининг datasource получил UID PD775F2863313E6C7. Либо задавайте uid: явно в блоке провижининга, либо копируйте сгенерированный UID в JSON дашборда. Я выяснил это, разглядывая пустую logs-панель, которая прекрасно работала в Explore.

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

Итого

  • Один Deployment, один Service, один hostPath PV на 5 ГБ — весь лог-бэкенд в ~90 строках YAML
  • 32 МБ request / 128 МБ limit реально хватает для объёма access-логов одного узла
  • strategy: Recreate обязательна с ReadWriteOnce hostPath-томом — RollingUpdate означает двух писателей
  • Loki-совместимый push-эндпоинт: Promtail/Alloy/Vector работают без плагинов
  • LogsQL фильтрует по любому полю без планирования кардинальности — главный бытовой выигрыш перед Loki
  • Фиксируйте UID datasource в Grafana, иначе дашборды из провижининга будут смотреть в никуда