Logo Craft Homelab Docs Контакты Telegram
Как ломается платформа приёма телеметрии под нагрузкой ретраев Трендовые github проекты в нашем телеграм канале. Подпишись →
2 сентября 2026 г.

Три оси, по которым стоит ломать систему приёма метрик и логов

Классический нагрузочный тест проверяет систему в состоянии «всё работает, просто данных много». Платформа приёма телеметрии — метрики, логи, трейсы — ломается в другом состоянии: один компонент отказал, и все клиенты одновременно решили, что запрос надо повторить. Нагрузку в этот момент создаёт не пользователь, а собственный код ответа сервера. Ниже — три оси, по которым такую систему нужно ломать отдельно, и что на каждой из них отказывает раньше, чем прогон через k6 успевает что-то показать.

Почему у observability другая нагрузка

Веб-приложение нагружают люди. Человек ждёт ответа, злится, обновляет страницу и уходит. Пиковая нагрузка ограничена сверху числом людей.

Платформу приёма телеметрии нагружают агенты: vmagent, Vector, fluent-bit, vector-подобные сборщики. Агент не уходит и не устаёт. Если ему ответили ошибкой — он повторит запрос с тем же телом и будет повторять сутками. Его пиковая нагрузка ограничена сверху только вашим кодом ответа.

Отсюда три независимых оси:

  • Поток записи — сколько сэмплов и строк лога в секунду принимает система. Единственная ось, которую обычно и тестируют.
  • Кардинальность — сколько уникальных временных рядов лежит в базе. Растёт от лейблов клиента, а не от трафика, и именно она определяет память хранилища.
  • Поведение клиента при отказе — что делает агент, когда сервер ответил ошибкой. Ось видна только в момент отказа и на графиках нагрузки не проявляется.

Ось первая: запас памяти под cgroup-лимитом

VictoriaMetrics, VictoriaLogs и VictoriaTraces не имеют фиксированного потолка памяти. Флаги -memory.allowedPercent и -memory.allowedBytes управляют размером внутренних кэшей и буферов, а не всем потреблением процесса. Дополнительная память на обработку входящих запросов в этот лимит не входит.

Дальше механика простая. Компонент занимает кэшами свою долю от лимита, и на этом фоне любой всплеск — тяжёлый запрос из Grafana, скачок кардинальности, шторм ретраев — выталкивает RSS за границу cgroup. Ядро убивает процесс по memcg-OOM. Средний расход памяти при этом выглядит спокойно.

Как это выглядит на практике. Лимиты в compose, посчитанные под тестовый сервер с 2 ГБ, уезжают на прод с 8 ГБ и остаются маленькими:

MEM_VICTORIALOGS  160m
MEM_VMSTORAGE     224m
MEM_VMSELECT      160m
MEM_VICTORIATRACES 96m

Счётчик RestartCount контейнеров при таких лимитах показывает тысячи перезапусков: хранилище рестартует каждые несколько минут месяцами, anon-rss держится у самой границы (227 МБ против лимита 224m). Снаружи это выглядит как разрывы в графиках на 5–10 секунд, которые легко списать на сеть.

После подъёма лимитов возникает вторая, менее очевидная часть. Хранилище садится на 84% нового потолка, потому что кэши заполняют заданную долю независимо от того, сколько памяти выдали. Запас до OOM — один тяжёлый запрос. Поэтому долю кэшей приходится опускать, например с 60% до 50%: кэшам 192 МБ из 384, потребление около половины, запас удваивается.

Правило: под cgroup-лимитом тюнится запас, а не потребление. Метрика, за которой надо следить, — сколько остаётся до лимита в момент пика. RestartCount контейнеров здесь метрика первого класса: она молча копит месяцами то, чего не видно в дашбордах.

Ось вторая: кардинальность вместо rps

Нагрузка на TSDB измеряется не в запросах в секунду. Сто хостов, отправляющих данные раз в 15 секунд, дают тривиальный rps и при этом кладут хранилище по памяти, потому что важно число активных серий — рядов, получивших хотя бы один сэмпл за последний час.

Обычный Linux-хост с node_exporter — это порядка 900–1000 серий. А один узел демо-кластера Kubernetes с полутора десятками подов:

control-plane (apiserver/etcd/workqueue)   39 576 серий
node_exporter (все хосты аккаунта)           4 247
cAdvisor/kubelet, container_*                3 135
kube-state-metrics                           1 103
kubelet_*                                      742
-----------------------------------------------------
активных серий у аккаунта                  ~49 000

Один узел кластера весит как полсотни обычных серверов, и 80% этого веса — гистограммы control-plane (apiserver_request_duration_seconds_bucket и родственные). Эта часть не растёт от числа узлов: она растёт от активности API-сервера, то есть на боевом кластере клиента будет не десять тысяч рядов, а сотни тысяч с одного клиента. От числа узлов растёт другая часть: около 900 серий node_exporter плюс 2–3 тысячи cAdvisor/kubelet на узел.

Лечится отсевом на стороне сбора — metric_relabel_configs с action: drop по суффиксу _bucket:

metric_relabel_configs:
  # Гистограммы control-plane: 80% кардинальности кластера.
  # Дропаем только _bucket — _sum и _count остаются,
  # средние и rate() считаются, теряются лишь квантили.
  - source_labels: [__name__]
    regex: '(apiserver|etcd|workqueue)_.*_bucket'
    action: drop

На демо это убрало 39 тысяч серий из 49 тысяч. Держите такое правило отдельным блоком с комментарием, чтобы вернуть квантили одной строкой, когда они понадобятся.

Как тестировать эту ось: у VictoriaMetrics есть prometheus-benchmark для write-нагрузки реальными метриками node_exporter, а для высокой кардинальности — генератор avalanche. Полезный прогон: зафиксировать rps и линейно поднимать число уникальных серий, наблюдая за памятью vmstorage и RestartCount. Точка, где хранилище начинает перезапускаться, и есть реальная ёмкость, измеренная в сериях. Отдельно проверьте churn: если в лейблы протекает что-то уникальное на запуск (pod, uuid, queryid, таймстемп), старые ряды постоянно заменяются новыми, активных серий немного, а памяти уходит как на порядок большее число.

Ось третья: отказ, который не входит в план тестирования

Самый дорогой инцидент часто не связан с производительностью вообще. Типичная цепочка из четырёх звеньев, каждое из которых по отдельности корректно:

  1. У тестового тенанта истекла подписка, его агент продолжает писать на прод.
  2. Выкатилась блокировка приёма по подписке: эндпоинт проверки токена стал отвечать 402 Payment Required для истёкших. Код семантичный, ревью прошёл.
  3. Этот эндпоинт вызывается из nginx через auth_request, а auth_request пробрасывает клиенту только 401 и 403. Любой другой код подзапроса считается ошибкой и превращается в 500.
  4. Для vmagent и Vector ответ 5xx означает «сервер моргнул, повторяй». Оба уходят в бесконечный ретрай полными телами метрик и логов.

Результат: круглосуточно около 700 ответов 500 в час, несколько суток подряд, отдано наружу меньше 10 МБ — весь объём входящий. Побочный урон — OOM у VictoriaLogs: маленькие кэши не держат шторм ретраев, и в моменты рестартов задержки логов получают все остальные тенанты. Один истёкший клиент деградирует сервис всем.

Нагрузку здесь создал собственный код ответа. Ни один сценарий k6 этого не находит: нагрузка не в rps, а в контуре обратной связи «код ответа приложения → поведение nginx → политика ретраев клиента».

Матрица кодов отказа

Разница в поведении клиентов на разные коды — то, что превращает код ответа в оружие против себя:

Клиент4xx5xx
vmagent409 — дроп блока; 400/415 — репаковка, затем дроп; 403, 429 — ретрай с экспоненциальным backoff, бесконечно, с уважением Retry-Afterретрай бесконечно, буфер на диске в -remoteWrite.tmpDataPath, при переполнении дропаются самые старые данные
Vector (HTTP sink)ретраит только 408 и 429; прочие 4xx не ретраит — без disk-buffer батч теряетсяретраит всё ≥ 500, кроме 501

Отсюда рабочий инвариант: любой отказ на путях приёма обязан быть 401, 403 или 429. Всё остальное клиентский агент увидит как 500 и будет долбить полными телами. После замены 402 на 403 на всех путях приёма (метрики, логи, трейсы) поток на приём падает примерно в десять раз: Vector сбрасывает батчи сразу, vmagent продолжает стучаться, но мелкими телами.

Ещё одна деталь про 429. Для отсечения слишком болтливых агентов удобна зона limit_req в nginx, но limit_req_status по умолчанию 503 — код, который агент воспримет как «сервер сломался». Переключается одной строкой:

map $http_authorization $agent_token {
    default $http_authorization;
}
limit_req_zone $agent_token zone=agents:10m rate=1200r/m;
limit_req_status 429;   # по умолчанию 503 — агент прочитает его как «сервер моргнул»

Чек-лист прогонов

  • Посмотрите RestartCount всех контейнеров прямо сейчас, без нагрузки. Тридцать секунд и самый вероятный источник неприятного открытия.
  • Замерьте запас до cgroup-лимита в пике, а не средний расход. Компонент на 84% лимита уже одноразовый.
  • Прогон по кардинальности при фиксированном rps: ищется ёмкость в сериях. Инструменты — prometheus-benchmark, avalanche.
  • Матрица кодов отказа: на каждый способ отказать клиенту проверьте, что реально видит агент после прохода через nginx, а не что вернуло приложение.
  • «Злой клиент»: агент, которому отвечают ошибкой, оставленный на час. Смотреть на трафик и на соседних тенантов — деградация соседей это отдельный класс дефектов.
  • Soak на сутки вместо получасового прогона: утечки, рост churn и переполнение дисковой очереди на коротком тесте не видны.
  • Открытая модель нагрузки. Если генератор ждёт ответа перед следующей итерацией, при деградации он сам снижает rps и не измерит те медленные ответы, ради которых тест затевался. В k6 это constant-arrival-rate и ramping-arrival-rate.
  • Тест на чтение, а не только на запись: тяжёлый запрос из Grafana по широкому диапазону — легальный способ выбить хранилище за лимит памяти, потому что запросы не учитываются в -memory.allowedPercent.

Что забрать в CI

Ценнее всего — превратить разбор в проверку, которая упадёт при повторении. Минимальный набор для барьера перед выкаткой:

  • истёкший тенант на путях /insert, /logs/jsonline и трейсах получает 403; любой 5xx роняет тест с явным текстом про бесконечный ретрай;
  • после продления подписки приём возвращается — без этой проверки набор остаётся зелёным даже при полностью мёртвом приёме;
  • хранилище логов живо, RestartCount не растёт по ходу прогона.

Если в проде стоят vmagent, Vector или fluent-bit, потратьте пять минут и проверьте, какой код они получают на каждом способе отказать. Это дешевле, чем трое суток самообстрела.