Меня зовут Дима Синявский, я SRE-инженер в Ви.Tech — это IT-дочка ВсеИнструменты.ру. Мы в своей компании давно используем vector.dev для обработки логов, о чем я рассказывал в предыдущих статьях (раз, два). Мне попалась интересная статья на английском языке о vector.dev и как можно сломать по-тихому незаметно для себя конвейер "правильными" данными и настройками. Представляю свое виденье этого материала.

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

Эта статья не рассказывает об основах использования Vector (документация проекта с этим справляется отлично!). Вместо этого мы разберём один интересный сценарий сбоя: как медленный поток «отравленных» событий может полностью заблокировать работу всего конвейера.

Статья будет полезна тем, кто уже эксплуатирует Vector, но даже если вы просто разделяете идею "всё — это очередь", вы сможете извлечь из неё полезные идеи.

Управление потоком событий в Vector

События поступают в Vector через sources (источ��ики), могут проходить трансформы, фильтрацию, хоть «на солнце», или переправлены в sinks (приёмники). Vector может доставлять события во всевозможные sinks — это могут быть HTTP-сервисы, S3, очереди сообщений, облачные системы мониторинга, /dev/null и другие. За исключением приемника "чёрная дыра" (blackhole sink), большинство приемников не 100% надежны. Vector предлагает несколько механизмов для работы с временными или постоянными сбоями доставки, о которых ниже.

1. Буферы

Каждый sink имеет настраиваемый буфер (по умолчанию — 500 событий). Событие остаётся в буфере, пока не будет успешно доставлено или окончательно отброшено. Если буфер заполняется из-за медленной или неудачной доставки, он может:

  • Блокировать новые события (включая "backpressure" обратное давление вверх по конвейеру — блокирование приема/вычитки из source),

  • Отбрасывать новые события, освобождая место

По умолчанию задано — блокировать.

Буферы у источников и трансформов тоже есть, но они меньше (обычно 100 событий) и их нельзя настроить.

2. Повторные попытки (Retries)

Повтор отправки события выполняется конечное число раз, оно остается в буфере до тех пор, пока не будет выполнена успешная доставка. После того как количество повторных попыток отправки события исчерпано, оно отбрасывается. Но по умолчанию в Vector лимит попыток настолько велик (примерно 9×10¹⁸), что его можно считать бесконечным.

3. Подтверждение доставки (Acknowledgements)

Некоторые приемники (source) поддерживают сквозные подтверждения для клиентов (end-to-end acknowledgements), которые помещают данные в Vector. Например, HTTP-источник будет ждать подтверждения успешной доставки в приемник, прежде чем вернет клиенту HTTP код ответа 200, или, в качестве альтернативы, ответит HTTP 5xx, если доставка завершится неудачей. При этом:

  • Если событие отфильтровано — оно считается «успешно обработанным».

  • Если доставка не удалась из-за временного сбоя, но попытки повторной доставки не исчерпаны — клиент не получает ответа. Соединение может зависнуть до таймаута.

    Подтверждения срабатывают только при успешной доставке или при постоянном сбое.

Временный сбой, до момента пока повторные попытки еще не были исчерпаны – не является ни тем, ни другим!

При отключенных подтверждениях (acknowledgements) HTTP-источник немедленно отвечает кодом 200 при успешном получении события, не дожидаясь выяснения, доставил/сохранил ли его приемник, а дошло ли оно вообще до приемника.

Это может показаться проблемой, но есть способы получше, чтобы все испортить.

Отравленные события — ломаем все по тихому

Представьте конфигурацию с:

  • Бесконечными повторами

  • Блокирующим буфером

  • Включёнными подтверждениями доставки

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

Подразумевается следующее: S3 в конце концов заработает снова, и если отправители событий достаточно умны, то они понимают, что таймауты/сбои соединений указывают на необходимость повторной отправки своих данных в Vector. Как только S3 станет доступен, буфер сможет очиститься, и поток событий вернётся в норму.

Но что, если можно создать ситуацию, при которой буфер никогда не сможет очиститься?

Если ваш sink, например, Datadog, а Vector настроен использовать API-ключ из самого события (API-ключ является частью самого события, хотя и не содержимого тела лога), то всё будет работать… пока кто-то не пришлёт событие с недействительным или несуществующим ключом.

Это событие никогда не удастся доставить, сколько бы раз вы его ни повторяли. Оно будет вечно висеть в буфере. Даже небольшой поток таких «отравленных» событий со временем заполнит буфер полностью. И тогда — весь конвейер остановится.

Обратное давление (backpressure) распространится вверх: трансформы заблокируются, источники перестанут принимать новые данные. Если включены acknowledgements — клиенты зависнут в ожидании ответа.

Почему это так? Архитектурная особенность

Эта проблема не уникальна для Vector (у Fluentbit схожая модель, Opentelemetry Collector тоже имеет backpressure), но интересна она тем, что как только событие достигает приёмника, ничего в нем уже нельзя изменить. До попадания в приёмник (включая сопутствующие секреты, такие как API-ключ) оно может быть изменено, нормализовано, отфильтровано, разобрано (и собрано заново), трансформировано по пути к приёмнику. Но как только событие попало в приемник — его можно только отправить, отбросить или оставить в буфере.

Ещё хуже, если у вас разветвлённая топология:

source → transform → filter → File sink
                           ├→ HTTP sink
                           └→ S3 sink

Даже если большинство приёмников тщательно настроены с конечным числом повторов или отбрасыванием событий при заполнении буфера, достаточно одного приёмника с настройками по умолчанию (блокирующий буфер на 500 событий), чтобы он заполнился и заблокировал вышестоящие компоненты. Рассмотрим, например, заполненный файловый приёмник на схеме выше: компонент filter не может отправить события в File sink, поэтому filter заполняется; компонент transform не может отправить события в filter, поэтому и он заполняется; source не может отправить события в transform, и сам становится заполненным; в результате source перестаёт принимать запросы.

Достаточно одного приемника (sink) с блокирующим буфером и бесконечными повторами (а это настройки по умолчанию!), чтобы вся цепочка остановилась — даже если остальные приемники работают идеально.

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

Обходные решения — как с этим бороться?

1. Ограничьте число повторов

Установите разумный лимит попыток (например, 3–5). Это гарантирует, что «ядовитые» события рано или поздно будут отброшены, и конвейер разблокируется.

2. Включите сквозное подтверждение

При включенном сквозном подтверждении (end-to-end acknowledgements) отправитель узнает о неудаче (HTTP 5xx) и сможет повторить отправку — возможно, уже с исправленными данными.

Важно: Vector пока не поддерживает DLQ (Dead Letter Queue). Поэтому единственный способ «переиграть» проблемные события — это, чтобы их источник отправил их заново. Но есть некоторые обходные решения.

3. Если ищете способ переусложнить архитектуру — создайте промежуточный DLQ-сервис

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

Начните с простого: конечное число повторов + acknowledgements (по возможности) — это уже огромный шаг к устранению риска тихо все сломать.

Заключение

Vector.dev — мощный инструмент, но его сила требует понимания тонкостей управления потоком. Отравленные события — редкий, но потенциально разрушительный фактор. Правильная настройка буферов, повторов отправки и acknowledgements — ключ к надёжности вашей observability-инфраструктуры.

P.S. О других граблях и уходе за Vector читайте в статье Vector: руководство по уходу за граблями, а если у вас есть вопросы, то присоединяйтесь к Телеграм-сообществу https://t.me/vectordev_ru

P.P.S. Пока писал статью, возникло несколько открытых вопросов — хотел бы услышать ваш опыт:

  1. Как вы на практике ловите события, которые «застревают навсегда»?
    Используете ли вы костыльные DLQ (например, отдельный file или Kafka-приёмник для проблемных событий), или полагаетесь на источники с повторной отправкой, что сам сам словит таймаут и переотправит?

  2. Насколько вообще распространено — и допустимо — держать credentials (типа API-ключа) прямо в теле события?
    Это антипаттерн, или в multi-tenant системах без этого не обойтись?

  3. Какие метрики Vector вы мониторите в первую очередь, чтобы поймать backpressure до того, как конвейер встанет?
    Например, заполнение буфера vector_buffer_events/vector_buffer_max_events, component_errors_total — или есть что-то более тонкое?

  4. Можно ли честно изолировать ветки конвейера, чтобы один «сломанный» sink не заблокировал всё остальное?
    Теоретически — да, через route + отдельные transforms. Но на практике, если upstream-компонент один, обратное давление (backpressure) всё равно поползёт наверх. Кто пробовал — получилось ли? Или архитектура Vector всё равно создаёт неявную связь?

  5. Если сравнивать Vector с OpenTelemetry Collector или Fluent Bit — насколько эта проблема проявляется в них?
    У меня нет опыта работы с ними двумя, потому интересно услышать ваш опыт.