Представьте небольшой сервер с одним рабочим приложением: веб-часть, очередь задач, база данных. Рядом с ними поселились три наблюдателя. Один читает файлы журналов и отправляет строки в хранилище логов. Второй раз в несколько секунд обходит эндпоинты и пересылает метрики. Третий принимает трассы от приложения и передаёт их дальше. У каждого свой конфигурационный файл, свой юнит systemd, свои обновления, своя память и свои представления о том, что такое «временная ошибка получателя».

Пока сервер почти пуст, этого не замечаешь. Когда приложение растёт, оказывается, что наблюдатели тоже едят процессор и оперативную память, причём делают это именно в часы пиковой нагрузки: в пик больше запросов, больше строк в журнале, больше спанов в трассах. Система, которая должна объяснять, почему сервис тормозит, сама становится одним из слагаемых этой торможки.

Grafana Alloy предлагает заменить трёх наблюдателей одним процессом. Но этот текст не про то, как красиво выглядит консолидация. Он про то, что именно вы получаете, когда сбор сигналов сходится в одной точке, и где в этой точке придётся поставить ограничители, чтобы она не стала самой уязвимой деталью сервера. Обещать снижение нагрузки заранее нельзя: насколько один процесс окажется легче трёх, зависит от вашего приложения, объёма данных и конфигурации, и это нужно измерять у себя.

Карта входов и выходов: с чего начинается любая конфигурация

Прежде чем писать хоть строчку конфигурации, полезно нарисовать карту, на бумаге или в заметке. Для каждого сигнала приложения ответьте на два вопроса: откуда он приходит и куда должен уйти. Согласно документации Grafana Alloy, агент собирает и обрабатывает телеметрию через компоненты семейств Prometheus, OpenTelemetry, Loki и Pyroscope, а данные идут по настроенным конвейерам (pipelines) к соответствующим backend. Эта формулировка уже подсказывает форму решения: конвейеров будет несколько, и каждый из них оканчивается своим получателем.

Возьмём условное приложение: интернет-магазин на небольшом VPS. Карта могла бы выглядеть так.

Условная карта телеметрии одного приложения
СигналИсточник на сервереКак попадает в агентКуда уходит
ЖурналыФайлы приложения и обратного проксиАгент читает файлыХранилище логов
МетрикиHTTP-эндпоинт метрик приложения, метрики узлаАгент опрашивает эндпоинтыХранилище метрик
ТрассыSDK приложенияПриложение отправляет данные агенту по сети, на локальный адресХранилище трасс

Обратите внимание на асимметрию. Журналы и метрики агент в типичной схеме забирает сам: читает файлы и опрашивает эндпоинты. Трассы, наоборот, приходят к нему: приложение выступает клиентом, а агент слушает порт. Это различие важно по двум причинам. Во-первых, когда агент перегружен, «забирающие» источники можно просто притормозить, а у «приходящих» есть собственный клиент, который должен что-то сделать с отказом. Во-вторых, лишний открытый порт на сервере нужно осознанно ограничить: если приложение и агент живут на одной машине, приёмнику трасс незачем слушать внешние интерфейсы.

Карта нужна ещё и затем, чтобы увидеть, что вы не собираете. Если в журнале приложения две трети строк составляют проверки живости от балансировщика, это видно уже на этапе рисования карты, и вопрос «что отбрасывать» возникает до первого запуска, а не после первой тревожной недели.

Три сигнала, три трубы: как не смешать форматы

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

  • Журналы — события с текстом и небольшим набором меток. Хранилище логов индексирует метки, а содержимое строки обычно хранит как есть. Поэтому решение о том, какие метки навесить на поток, определяет и стоимость хранения, и скорость поиска.
  • Метрики — числовые ряды, где каждая уникальная комбинация меток создаёт отдельный ряд. Здесь опасность другая: лишняя метка с большим числом значений плодит ряды.
  • Трассы — деревья спанов с идентификаторами, временем и атрибутами. Они крупные, их много, и обычно нет смысла хранить каждую.

Компоненты в Alloy сгруппированы по семействам, и это помогает держать потоки раздельно. Всё, что относится к журналам, живёт в компонентах Loki; метрики идут через компоненты Prometheus; приём и обработка трасс опираются на компоненты OpenTelemetry. Конвейер каждого сигнала собирается из этих компонентов, и выход одного подключается ко входу следующего. Практический вывод: пишите конфигурацию тремя отдельными блоками, а не одним слитным полотном. Тогда при чтении видно, где заканчивается один сигнал и начинается другой, а ошибка в правилах для логов не ломает конвейер трасс.

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

Ещё одна полезная привычка: выбирать метки один раз и использовать их одинаково везде. Если в журналах сервис называется shop-api, то и в метриках, и в трассах он должен называться так же. Иначе пользователь в Grafana, перейдя от всплеска на графике к логам этого же сервиса, не найдёт соответствия, и все выгоды общего агента останутся только на схеме. Единообразные имена — это не возможность агента, а ваша договорённость, и агент лишь даёт удобное место, где её можно реализовать: все три конвейера находятся в одном конфигурационном файле, и расхождение видно глазами.

Один агент — не одно хранилище

Тут часто возникает путаница, поэтому скажем прямо: Alloy — это сборщик и маршрутизатор, а не долговременное хранилище телеметрии. Документация описывает его как средство сбора и обработки, после которого данные отправляются на backend. Куда именно, решаете вы.

Практически это значит, что вопросов остаётся три, а не один:

  1. Где хранить журналы? Нужен backend, который их принимает и умеет искать.
  2. Где хранить метрики? Нужен backend метрик, к которому подключится Grafana.
  3. Где хранить трассы? Нужен отдельный backend для трасс.

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

Отсюда и разумная архитектура для небольшого проекта. Агент работает на том же VPS, что и приложение, потому что ему нужен доступ к локальным файлам и портам. Хранилища и Grafana лучше держать отдельно, если нагрузка это оправдывает: тогда сбой приложения не уносит вместе с собой данные о собственном сбое. Если пока рано заводить второй сервер, можно начать с одной машины, но осознанно понимая, что при её потере вы теряете и приложение, и его историю. Про установку самих Grafana и Prometheus на VPS у нас есть отдельный разбор: как установить и настроить Grafana и Prometheus на VPS. Он поможет с той частью, куда агент будет отправлять данные.

Когда получатель недоступен: что происходит с потоком

Хранилище перезагружается, сеть моргнула, получатель не успевает принимать. Для агента это нормальная ситуация, и вопрос только в том, что он делает с данными в это время. Здесь нужно быть аккуратным, потому что поведение зависит от конкретного компонента отправки: у исходящих компонентов метрик, журналов и трасс свои буферы и свои параметры повторных попыток. Единого «правила очереди для всего Alloy» нет, и выводить его из общих соображений нельзя. Сверяйтесь с документацией той версии и тех компонентов, которые вы действительно подключили.

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

  • буфер ограничен, и при переполнении самые старые или самые новые данные отбрасываются;
  • буфер растёт, пока не кончится память;
  • источник замедляется, потому что агент перестал принимать новое.

Третий вариант называют обратным давлением (backpressure). Для источников, которые агент читает сам, например, файла журнала, он безобиден: файл никуда не денется, агент вернётся к нему позже. Для источника, который отправляет данные агенту, всё зависит от клиента: библиотека трассировки в приложении тоже имеет свою очередь, и если агент отвечает ошибкой, клиент либо повторит попытку, либо выбросит пакет. Поэтому конфигурацию агента нельзя проверять отдельно от приложения: оба конца должны вести себя согласованно.

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

Простой способ проверить свою схему, не дожидаясь настоящей аварии: на тестовом стенде остановите получателя на несколько минут при обычном потоке данных и посмотрите на три вещи. Сколько памяти занял агент. Что произошло с данными за это время: часть пропала или все дошли после возвращения получателя. Как отреагировало приложение: не выросла ли задержка ответов. Эти три наблюдения дадут вам больше, чем любые рассуждения о буферах. И записывайте результат как свой собственный замер для своего потока, а не как свойство продукта.

Заранее примите решение и о допустимой потере. Для журналов аудита потеря строк недопустима, и там нужен другой подход, с долговременной записью до отправки. Для обычных трасс потеря нескольких пакетов во время аварии — разумная цена за стабильность приложения. Для метрик пропуск нескольких точек на графике обычно терпим. Агент не знает, какие данные для вас святые, а какие нет. Знаете вы, и это знание должно быть отражено в конфигурации и в договорённостях с командой.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.

АРЕНДОВАТЬ СЕРВЕР

Память: ограничитель на входе конвейера трасс

Теперь о главном ограничителе. В части, относящейся к OpenTelemetry, у Alloy есть компонент otelcol.processor.memory_limiter. По его документации, он использует два порога: мягкий (soft) и жёсткий (hard). Когда использование памяти выше мягкого порога, компонент перестаёт принимать данные и возвращает ошибки тем, кто стоит перед ним. Выше жёсткого порога он дополнительно запускает сборку мусора.

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

Теперь о границах этого механизма, потому что на них чаще всего спотыкаются.

  • Ограничитель работает внутри конвейера OpenTelemetry. Он защищает цепочку компонентов, в которую включён, и не управляет памятью компонентов Loki или Prometheus, подключённых отдельно. Приписывать ему защиту всего процесса нельзя.
  • Это не жёсткий лимит операционной системы. Если вам нужно, чтобы процесс не мог выйти за определённую границу памяти в принципе, это задача уровня systemd, cgroups или контейнера, а не конвейера трасс. Разумно использовать оба уровня: внутренний ограничитель, чтобы агент деликатно сбрасывал нагрузку, и внешний, чтобы аварийно ограничить процесс.
  • Он не гарантирует отсутствия потерь. Отказ от данных выше мягкого порога по определению означает, что часть пакетов не будет принята. Вопрос лишь в том, увидит ли отправитель ошибку и повторит ли попытку.

Как выбирать пороги? Отталкивайтесь от бюджета памяти сервера, а не от желания «чтобы побольше влезло». Условный пример: на VPS с 4 ГБ оперативной памяти ОС и сопутствующие службы занимают порядка полугигабайта, приложение с базой данных — основную часть остального. Если по вашим замерам под пиком остаётся запас в несколько сотен мегабайт, то агенту нужно отвести заведомо меньше этого запаса, а мягкий порог поставить с зазором до жёсткого, чтобы сборка мусора успела отработать. Числа здесь иллюстративные: реальный бюджет вы получите только наблюдением за своим сервером при своей нагрузке. Тарифы VPS с разным объёмом памяти, от 2 ГБ у Ba до 4 ГБ у Uab и 8 ГБ у Heka, дают разный запас, но ресурсы тарифа — не обещание ёмкости, и то, сколько телеметрии поместится рядом с приложением, определяется замером.

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

Отбрасывать лучше у источника

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

Вот места, где отбрасывание обычно даёт наибольший эффект.

Журналы: шум, которого никто не читает

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

Отдельная тема — метки журналов. Если превратить в метку идентификатор пользователя или запроса, число потоков в хранилище логов вырастет до неуправляемого. Держите набор меток небольшим и стабильным: сервис, окружение, уровень. Всё остальное оставляйте внутри строки.

Метрики: ряды, которые никогда не попадут на график

Для метрик опасность в количестве рядов. Приложение или библиотека могут отдавать десятки тысяч комбинаций меток, из которых вы реально используете несколько сотен. Для начала выпишите, какие метрики вам нужны для дашбордов и оповещений, и ограничьте приём этим списком, а остальное отбросьте на этапе, пока данные ещё не ушли получателю. Это сокращает и нагрузку на агента, и объём у получателя. Следите также за интервалом опроса: опрашивать эндпоинт каждые пять секунд ради графика, который вы смотрите раз в день, — расточительство. Интервал нужно выбирать под вопрос, на который метрика отвечает.

Трассы: сэмплирование вместо «всё подряд»

Трассы самые тяжёлые из трёх сигналов, и сохранять каждую при заметной нагрузке обычно нет смысла. Распространённая идея: оставлять долю обычных трасс и приоритетно сохранять ошибочные и медленные. Конкретные компоненты сэмплирования и их параметры берите из документации вашей версии Alloy; здесь важен принцип: решение о выбрасывании должно приниматься как можно раньше. В идеале это делается на стороне SDK приложения, и тогда лишние спаны вообще не покидают процесс. Если же решение принимается в агенте, то оно должно стоять после ограничителя памяти и до пакетирования и отправки.

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

Кто сторожит сторожа

Теперь самая неприятная часть. Когда сбор телеметрии сходится в одной точке, она становится точкой единственного отказа для наблюдаемости. Если агент остановился, то никаких тревог не будет: тишина выглядит ровно так же, как идеальное здоровье. Вы узнаете о проблеме не из оповещения, а когда откроете дашборд, увидите пустой хвост графика и поймёте, что последние несколько часов вы были слепы.

Поэтому о сбое самого агента нужно узнавать иначе, чем о сбое приложения. Несколько слоёв.

  1. Менеджер процессов. Запускайте агента как службу с автоматическим перезапуском при падении. Это не заменяет оповещений, но избавляет от большинства мелких происшествий. Сам факт частых перезапусков тоже полезно заметить: служба, которая «чинит себя» каждые десять минут, — повод для расследования, а не повод для спокойствия.
  2. Собственные метрики агента. У самого агента есть собственная телеметрия о работе его компонентов. Какие именно показатели доступны в вашей версии, смотрите в документации. Разумно отправлять их тем же маршрутом, но с оговоркой: этот маршрут как раз тот, что может сломаться. Поэтому основной сигнал тревоги лучше строить не на самих метриках агента, а на их отсутствии.
  3. Оповещение по отсутствию данных. Заведите правило: если от конкретного сервера не поступало ни метрик, ни логов в течение заданного времени, это тревога. Такое правило живёт на стороне хранилища, а не агента, и поэтому сработает, даже если агент полностью мёртв.
  4. Независимый внешний контроль. Если всё наблюдение стоит на том же VPS, что и приложение, то при потере сервера пропадут и приложение, и агент, и правила оповещений. Простой выход: проверку «жив ли сервер и отвечает ли агент» выполнять с другой машины, пусть даже самым примитивным способом. Это та часть, которую нельзя делать самим агентом ни при каких условиях.

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

Наконец, потренируйтесь. Остановите агента на тестовом сервере и засеките, через какое время и кому придёт уведомление. Если ответ «никому и никогда», то наблюдаемость у вас пока что только на словах.

Что при этом нужно от VPS

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

На VPS MAATRIX доступны тарифы от Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe) до Djed (16 vCPU, 32 ГБ), с промежуточными Uab, Heka и Maat. Диски NVMe, процессоры AMD EPYC и root-доступ, а значит, вы можете сами настроить службу агента, лимиты systemd и правила отправки. Это удобно, если сервер нужно подогнать под свою схему наблюдения. Выбирать тариф лучше по замеренному запасу памяти и процессора под пиком, а не по ощущениям. Если со временем окажется, что собственный стек наблюдения заметно конкурирует с приложением, самый естественный шаг — вынести хранилища и Grafana на отдельный сервер, оставив на рабочей машине только лёгкий агент сбора.

В итоге картина такая. Единый агент убирает дублирование установок и конфигураций и позволяет держать правила всех сигналов рядом. Но он не отменяет решений о хранилищах, не избавляет от очередей, которые нужно уметь переживать, и не освобождает от памяти, которую нужно ограничивать на входе. И самое важное: чем больше наблюдения сходится в одной точке, тем важнее, чтобы эта точка сама была под наблюдением. Сборщик, который молча перестал собирать, хуже, чем отсутствие сборщика: он создаёт иллюзию, что за системой кто-то следит. Пусть исчезновение агента будет такой же заметной тревогой, как падение самого приложения, и тогда единая точка сбора действительно станет опорой, а не слабым звеном.