Vector и OpenTelemetry Collector постоянно оказываются в одной клетке архитектурной схемы, подписанной «слой сбора телеметрии». Из-за этого выбор между ними обсуждают как соревнование: кто быстрее, кто меньше ест, кто моднее. Соревнование это ложное — у инструментов разные сильные стороны, и они спокойно живут в одной системе.
Но и разговор о «разных сильных сторонах» обычно остаётся разговором. Утверждения вроде «Vector эффективнее» или «у Collector трейсы первым классом» кочуют из статьи в статью без единого числа, а те числа, что встречаются, сняты на разных стендах, разной нагрузке и разной трансформации — то есть сравнивать их между собой нельзя.
Поэтому здесь один и тот же поток логов проходит через три конвейера на одной машине: Vector, OpenTelemetry Collector и Fluent Bit третьим для масштаба. У всех одинаковый вход, одинаковая трансформация и одинаковый набор полей на выходе, причём последнее не обещание, а проверяемое условие: если наборы полей разошлись, замеры не публикуются.
Четвёртым на стенде стоит связка Vector → Collector — та самая «поставьте оба», которую советуют чаще всего. Она в равноправном сравнении не участвует: у неё другая задача — проверить, собирается ли стык вообще, и чего это стоит. Ни ресурсы, ни поля на её выходе с остальными тремя не сопоставлялись, и ниже это оговорено отдельно.
Статья не про выбор бэкенда логов — OpenSearch здесь приёмник, а не предмет разговора; выбор между хранилищами будет разобран в статье про Loki, OpenSearch и VictoriaLogsготовится, с 21 октября. Не про то, зачем вообще нужен слой сбора и как его разворачивать — это в статье про OpenTelemetry Collector. И не про устройство самого Vector: сквозной контур на нём с VRL, буферами и брокером между агентом и агрегатором собран в статье про pipeline логов на Vector.
В статье
- Что сравниваем и на чём
- Модель данных: что каждый считает событием
- Трансформация: VRL, OTTL и processors
- Надёжность: что теряется, когда бэкенд лёг
- Ресурсы: сколько это стоит
- Эксплуатация: версии, образы, проверка конфига
- Развилка выбора
- «Оба вместе» на практике
- Когда не нужен ни тот, ни другой
- Итог
- Документация и первоисточники
Что сравниваем и на чём
nginx access + app JSON"] --> V["Vector 0.57"] G --> C["OTel Collector 0.157"] G --> F["Fluent Bit 5.0"] G --> B["Vector → OTLP → Collector"] V --> OS[("OpenSearch 3.5")] C --> OS F --> OS B --> OS
flowchart LR
G["Генератор
nginx access + app JSON"] --> V["Vector 0.57"]
G --> C["OTel Collector 0.157"]
G --> F["Fluent Bit 5.0"]
G --> B["Vector → OTLP → Collector"]
V --> OS[("OpenSearch 3.5")]
C --> OS
F --> OS
B --> OS
Генератор пишет два файла: nginx access-лог в combined-формате, где часть запросов идёт к /health, и app-лог JSON-строками. Задача у трёх сравниваемых конвейеров одна: распарсить обе формы, проставить служебное поле pipeline, отсечь health-запросы и положить результат в OpenSearch. У связки задача другая — дойти до бэкенда вообще, и её результат меряется только счётчиком доставленного.
Стенд — digital-cookbook/observability/vector-vs-collector. Прогоны строго последовательные, по одному конвейеру: одновременный запуск превратил бы замер процессора в конкуренцию за ядра. Версии зафиксированы — Vector 0.57.0, OpenTelemetry Collector contrib 0.157.0, Fluent Bit 5.0.9, OpenSearch 3.5.0.
Одинаковая работа — это проверяемое условие
Сравнивать потребление ресурсов можно только у конвейеров, делающих одно и то же. Формулировка «мы настроили их одинаково» тут не годится: настроить можно с любой добросовестностью, а проверить — только по результату. Всё, что в этом разделе, относится к трём основным конвейерам; связка сюда не входит.
Граница проведена по выходному документу. Скрипт берёт по документу из каждого индекса и сверяет набор прикладных полей — отдельно для nginx-события, отдельно для app-события:
=== прикладные поля документа, service=nginx
logs-vector: agent client pipeline referer request service size status
logs-otelcol: agent client pipeline referer request service size status
logs-fluentbit: agent client pipeline referer request service size status
=== прикладные поля документа, service=api
logs-vector: duration_ms level msg pipeline service ts
logs-otelcol: duration_ms level msg pipeline service ts
logs-fluentbit: duration_ms level msg pipeline service ts
OK: наборы прикладных полей совпадают, замеры сопоставимыСлужебные поля из сверки исключены сознательно, и это не поблажка. У каждого инструмента своя модель записи, и требовать её совпадения — значит требовать, чтобы инструменты перестали быть собой. Именно этой разницей и открывается сравнение.
Одно уточнение о составе задачи: дедупликации в общей трансформации нет. У Fluent Bit прямого аналога dedupe не существует, и включение её сломало бы эквивалентность. Само это отсутствие — уже характеристика инструмента, к которой вернёмся в разделе про трансформации.
Модель данных: что каждый считает событием
Одна и та же строка лога, пройдя три конвейера, оседает в индексе тремя разными документами. Прикладные поля совпадают — иначе замеры были бы несравнимы, — а вот всё остальное расходится, и расхождение это содержательное.
Fluent Bit даёт самый компактный документ: только поля и одно служебное @timestamp.
{
"@timestamp": "2026-07-28T15:51:05.000Z",
"client": "10.0.0.1",
"request": "GET / HTTP/1.1",
"status": 404,
"size": 3545,
"referer": "-",
"agent": "curl/8.5.0",
"service": "nginx",
"pipeline": "fluentbit"
}Vector добавляет четыре плоских служебных поля — file, host, source_type, timestamp. Исходная строка удаляется явным del(.message) в трансформации: если этого не сделать, событие несёт и разобранные поля, и оригинал.
Collector оборачивает всё в структуру OpenTelemetry: прикладные поля лежат внутри attributes, рядом появляются body, severity, instrumentationScope, observedTimestamp и data_stream.
{
"attributes": {
"client": "10.0.0.2",
"request": "GET /api/items/7 HTTP/1.1",
"status": 500,
"service": "nginx",
"pipeline": "otelcol",
"log.file.name": "nginx-access.log",
"data_stream": {"dataset": "default", "namespace": "namespace", "type": "record"}
},
"body": "10.0.0.2 - carol [28/Jul/2026:15:51:06 +0000] \"GET /api/items/7 HTTP/1.1\" 500 1381 \"-\" \"Mozilla/5.0\"",
"instrumentationScope": {},
"observedTimestamp": "2026-07-28T16:05:08.63987961Z",
"severity": {},
"@timestamp": "1970-01-01T00:00:00Z"
}Здесь стоит задержаться на двух вещах.
Первая — body. По спецификации OpenTelemetry это поле необязательное, но в нашем конвейере filelog receiver кладёт в него исходную строку и не убирает её после разбора. Для отладки удобно, для объёма хранения — вдвое дороже: и разобранные поля, и оригинал. Vector и Fluent Bit в тех же конфигурациях исходник отбрасывают, у Vector это явный del(.message).
Вторая — @timestamp, равный эпохе. Collector не выводит время события из строки лога, пока ему не задан отдельный оператор разбора времени. Vector в том же сценарии проставил 2026-07-28T15:51:05Z, потому что parse_nginx_log разбирает время сам, а Fluent Bit — потому что группа времени объявлена как time_key в парсере. У Collector это надо делать руками, и умолчание тихое: документы индексируются, поле есть, значение бессмысленное.
Сигналы: где проходит настоящая граница
Логи — общая территория всех троих. Расходятся они на OTLP.
Collector принимает OTLP в обоих кодированиях, protobuf и JSON, по gRPC и по HTTP. Это его родной протокол, и трейсы для него такой же полноправный сигнал, как логи.
Vector OTLP тоже умеет — утверждение «не умеет» неверно, — но с оговорками, каждую из которых пришлось выяснять живьём.
Первая: OTLP/JSON он не принимает. Тот же самый запрос, который Collector обрабатывает штатно, Vector отвергает:
трейсы: отправлено 100 спанов (31000 байт) → HTTP 500,
ответ: 1Rejection(InvalidHeader { name: "content-type" })Спецификация OTLP/HTTP допускает оба кодирования. Vector принимает только protobuf. Чтобы отделить «не умеет OTLP» от «не умеет JSON», сигнал был пропущен через Collector в роли конвертера — тот принял JSON и отдал дальше protobuf. После этого приём заработал, значит дело именно в кодировке.
Вторая оговорка тоньше и обходится дороже. У источника есть переключатель use_otlp_decoding с тремя сигналами, все по умолчанию false. Название подсказывает, что false — это «не разбирать OTLP», и рука тянется включить. Результат обратный ожидаемому:
use_otlp_decoding |
Что оказалось в выходном файле |
|---|---|
false (умолчание) |
100 спанов и 100 логов, каждый отдельным событием, поля разложены, span_id читаемым hex |
true |
одна запись на весь пакет, структура OTLP как есть, идентификаторы сырыми байтами |
При выключенном декодировании — то есть по умолчанию — запись выглядит так:
{
"attributes": {"pipeline": "loadgen", "service": "api"},
"message": "db query slow",
"severity_text": "ERROR",
"severity_number": 17,
"source_type": "opentelemetry",
"span_id": "af10aa16c45b102d",
"trace_id": "843edf61a58870d3f96fe7dc8c6d0479",
"timestamp": "2026-07-28T12:00:00Z"
}Всё на месте: сигнал разложен по плоской модели Vector, trace_id сохранён у всех ста записей — корреляция лога со спаном не теряется. А вот при use_otlp_decoding: true весь ExportTraceServiceRequest становится одним событием Vector, и ни отфильтровать отдельный спан, ни прочитать его идентификатор в таком виде нельзя: "spanId":"y?[?" вместо hex.
Третья мелочь того же ряда: источник opentelemetry выдаёт три именованных выхода — .logs, .traces, .metrics, — и сослаться на него целиком нельзя. Конфиг с inputs: [otlp] отвергается на старте. При этом встроенный генератор примеров vector generate выдаёт для этого источника ровно такую ссылку, и его вывод валидацию не проходит.
Ни одно из этих ограничений не смертельно. Но вместе они и есть та разница, которая отличает основной сигнал инструмента от второстепенного: на логах Vector не заставляет разбираться в тонкостях, на трейсах — заставляет.
Трансформация: VRL, OTTL и processors
Одна и та же задача — распарсить nginx combined, проставить поле, отсечь /health — тремя языками.
Vector, VRL. Отдельный язык с функциями под конкретные форматы:
transforms:
parse_nginx:
type: remap
inputs: [nginx_file]
source: |
parsed, err = parse_nginx_log(.message, "combined")
if err != null {
.service = "unparsed"
} else {
. = merge(., parsed)
.service = "nginx"
del(.message)
}
.pipeline = "vector"
drop_health:
type: filter
inputs: [parse_nginx]
condition:
type: vrl
source: |
req = to_string(.request) ?? ""
!contains(req, "/health")parse_nginx_log знает про combined-формат и разбирает строку целиком, включая время. Обработка ошибки обязательна: err придётся либо проверить, либо оборвать выполнение через !.
Collector, OTTL. Разбор и преобразование разнесены: regex живёт в операторе ресивера, изменение полей — в процессоре.
receivers:
file_log/nginx:
include: [/logs/nginx-access.log]
operators:
- type: regex_parser
regex: '^(?P<client>\S+) \S+ (?P<user>\S+) \[(?P<ts>[^\]]+)\] "(?P<request>[^"]*)" (?P<status>\d+) (?P<size>\d+) "(?P<referer>[^"]*)" "(?P<agent>[^"]*)"'
processors:
transform/nginx:
log_statements:
- context: log
statements:
- set(attributes["service"], "nginx")
- set(attributes["pipeline"], "otelcol")
- set(attributes["status"], Int(attributes["status"]))Готовой функции под nginx нет — regex пишется руками, типы приводятся отдельными выражениями. Зато OTTL один и тот же для логов, метрик и трейсов, а Vector требует разных подходов к разным сигналам.
Fluent Bit. Парсеры объявляются отдельно и подключаются к входу по имени:
parsers:
- name: nginx_combined
format: regex
regex: '^(?<client>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<request>[^"]*)" (?<status>[^ ]*) (?<size>[^ ]*) "(?<referer>[^"]*)" "(?<agent>[^"]*)"$'
time_key: time
time_format: "%d/%b/%Y:%H:%M:%S %z"
types: "status:integer size:integer"Декларативно и коротко, приведение типов и разбор времени — параметрами парсера. Ценой гибкости: условная логика сложнее «сматчилось или нет» требует Lua, то есть выхода за пределы конфига.
Во что обходится разбор
Тот же поток, 5000 событий в секунду, с трансформацией и без неё:
| Конвейер | passthrough | с трансформацией | отношение |
|---|---|---|---|
| Fluent Bit | 2.1–2.3% | 4.7–5.7% | ×2.4 |
| Vector | 5.7% | 12.0–12.1% | ×2.1 |
| Collector | 33.5–33.6% | 33.2–34.2% | ×1.0 |
Числа требуют осторожного чтения, и вот в какую сторону. Вариант passthrough отправляет в бэкенд на 9% больше документов, потому что не отсекает /health. То есть полный конвейер вдвое дороже, уже делая меньше работы по отправке; чистая цена разбора ещё выше измеренной.
У Vector и Fluent Bit разбор строк — основная статья расхода, он удваивает потребление процессора. У Collector теряется в шуме, но не потому, что OTTL дешевле VRL: базовая стоимость его конвейера втрое выше, и на этом фоне трансформация просто не видна.
Здесь же место сказать про дедупликацию, которую пришлось исключить из общей задачи. У Vector она встроенная — dedupe с настраиваемым кэшем и списком полей. У Collector есть logdedup processor. У Fluent Bit прямого аналога нет: повторы отсеиваются либо на бэкенде, либо через Lua-скрипт. Для потока, где половина событий — повторяющиеся сообщения приложения, это разница не в удобстве, а в объёме хранилища.
Надёжность: что теряется, когда бэкенд лёг
Самый содержательный замер серии — и единственный, где инструменты разошлись принципиально.
Сценарий: поток 5000 событий в секунду, через пятнадцать секунд OpenSearch становится недоступен на минуту, потом возвращается. У каждого конвейера включена дисковая устойчивость своим механизмом.
Недоступность моделируется двумя способами, и это не педантизм:
stop— контейнер останавливается, docker убирает его DNS-запись, клиент получает «no such host». В Kubernetes так выглядит удалённый Service или обращение по DNS-имени отдельного Pod, но не обычная замена пода за Service: там DNS-имя сохраняется, а меняется только EndpointSlice;pause— процессы заморожены, контейнер существует, DNS на месте, соединение упирается в таймаут. Так выглядит зависший или перегруженный бэкенд.
| Конвейер | stop |
pause |
|---|---|---|
| Vector, disk buffer 256 МиБ | 0 потерь | 0 потерь |
| Fluent Bit, filesystem storage | 0 потерь | 0 потерь |
| Collector, очередь по умолчанию | −218 087 (79%) | −126 568 (46%) |
| Collector, очередь 256 МиБ | −218 087 (79%) | 0 потерь |
Vector и Fluent Bit не потеряли ничего. Vector при этом ретраит и отказ резолвинга тоже:
WARN vector::sinks::util::retries: Retrying after error.
error=Failed to make HTTP(S) request: error trying to connect:
dns error: failed to lookup address information: Name or service not known
...
INFO vector::sinks::util::service::health: Endpoint is healthy.У Collector две разные причины потерь, и их важно не смешивать.
Первая — переполнение очереди, и это умолчание, а не свойство. В режиме pause он теряет 46% потока с сообщением sending queue is full. Ёмкость sending_queue по умолчанию не рассчитана на минуту простоя под такой нагрузкой. Стоит задать её явно — и потери исчезают полностью:
exporters:
opensearch:
sending_queue:
enabled: true
storage: file_storage
sizer: bytes
queue_size: 268435456Вторая — классификация ошибки, и вот она размером очереди не лечится. В режиме stop обе конфигурации, с умолчанием и с очередью на 256 МиБ, потеряли ровно одинаково — 218 087 событий. Причина в логе:
error: "not retryable error: Permanent error: flush: dial tcp:
lookup opensearch on 127.0.0.11:53: no such host"
dropped_items: 82Отказ резолвинга считается постоянной ошибкой, и пачка выбрасывается, не дойдя до очереди, — при включённом retry_on_failure.
Практический смысл стоит очертить точно, потому что соблазн обобщить здесь велик. Пока бэкенд просто не отвечает — настроенный Collector надёжен, потери нулевые. Проблема возникает, только когда имя перестаёт резолвиться, а это более узкий случай, чем кажется: в Kubernetes обычная замена пода за Service DNS-имя не трогает — оно остаётся, обновляется лишь EndpointSlice. Имя исчезает при удалении самого Service, при обращении по DNS-имени отдельного Pod и в части headless-сценариев. Вне Kubernetes это, например, снятая запись у внешнего бэкенда или недоступный резолвер. В таких ситуациях телеметрия за время недоступности теряется безвозвратно, и размер очереди не помогает.
Чем платят за устойчивость
Механизмы разные, и это видно по занятому месту (до отказа → во время недоступности → после доставки, КиБ):
| Конвейер | до | во время | после |
|---|---|---|---|
| Vector | 17 900 | 86 128 | 86 128 |
| Collector | 6 508 | 70 004 | 70 088 |
| Fluent Bit | 108 | 38 488 | 3 856 |
Fluent Bit единственный освобождает место после доставки. У Vector и Collector объём остаётся по верхней отметке всплеска: файлы буфера append-only и удаляются не по подтверждению отдельного события, а по мере обработки сегмента целиком. Для планирования диска это существенно — место занято не текущим объёмом неотправленного, а максимальным за время жизни сегмента.
Устроены механизмы тоже по-разному. У Vector это одна секция buffer на синке. У Collector — две части: sending_queue у экспортёра плюс extension file_storage, который эту очередь хранит. У Fluent Bit устойчивость привязана к входу (storage.type: filesystem у tail), а не к получателю.
Отдельная мелочь, на которой легко споткнуться: минимальный размер дискового буфера Vector — 268435488 байт, то есть 256 МиБ плюс 32 байта. Круглое значение отвергается на старте:
Configuration error. error=Sink "opensearch": error occurred when building
buffer: failed to build individual stage 0: parameter 'max_buffer_size' was
invalid: must be greater than or equal to 268435488 bytesПерезапуск: здесь все одинаковы
Поток отработан и доставлен, конвейер перезапускается:
| Конвейер | том состояния сохранён | том удалён |
|---|---|---|
| Vector | +0 | +36 627 (ровное удвоение) |
| Collector | +0 | +36 627 |
| Fluent Bit | +0 | +36 627 |
Ни одного дубля при сохранённом состоянии и ровное удвоение при потерянном — у всех троих. Механизм у каждого свой (data_dir у Vector, extension file_storage у Collector, параметр db у Fluent Bit), следствие общее: том под состояние — условие корректности, а не пожелание. Причём потеря состояния при живом файле логов даёт не потерю данных, а их дублирование, что для счётчиков и алертов зачастую хуже.
Ресурсы: сколько это стоит
Тот же поток, два уровня нагрузки, по два повтора каждого прогона. Проценты — в единицах docker stats, где 100% означает одно полностью занятое ядро.
| Конвейер | 5 000 соб/с | 20 000 соб/с | RAM при 20 000 | RAM в простое |
|---|---|---|---|---|
| Fluent Bit | 4.7–5.7% | 7.6–8.9% | 25–26 МиБ | 20–21 МиБ |
| Vector | 12.0–12.1% | 44.6–45.7% | 470–478 МиБ | 146–155 МиБ |
| Collector | 33.2–34.2% | 93.4–94.7% | 107–112 МиБ | 38–42 МиБ |
Потерь нет ни у кого: на обоих уровнях все три доставили поток полностью.
Читать эту таблицу как рейтинг не стоит — она про размен, а не про победителя.
Fluent Bit дешевле обоих сразу и по процессору, и по памяти, с большим отрывом. При учетверении потока он прибавил всего в полтора раза. Если задача — «читать файлы и складывать в бэкенд», платить за что-то ещё незачем.
Между Vector и Collector размен, а не превосходство. Vector втрое дешевле по процессору и вчетверо дороже по памяти. При 20 000 событий в секунду Collector подошёл вплотную к полному ядру, Vector занял меньше половины — но потребовал почти полгигабайта.
Память Vector растёт с потоком, у двух других — практически нет: 330 → 475 МиБ против 105 → 110 и 25 → 25. Для агента на каждом хосте важнее не пик, а простой: Vector держит 150 МиБ, ничего не делая, против 40 у Collector и 20 у Fluent Bit. Умножьте на число хостов — это и есть настоящая цена присутствия.
Числа привязаны к этой машине, этому потоку и этим конфигам; сравнивать их между инструментами можно только потому, что работа у всех проверенно одинаковая. Разброс повторов: у Vector 0.8–2.4%, у Collector 1.4–2.9%, у Fluent Bit до 15% — последнее на малых абсолютных значениях, где доли процента дают большой относительный разброс.
Эксплуатация: версии, образы, проверка конфига
То, что не видно в бенчмарках, но чувствуется через полгода работы.
Vector всё ещё 0.x, и это не формальность. Версия 0.57 выключила интерполяцию ${VAR} в конфигах по умолчанию и ввела template confinement: шаблон синка без литерального префикса отвергается на старте. Оба изменения ломающие, оба попадают в типовые конфигурации. Версию надо пиновать и читать upgrade guide перед обновлением — подробнее об этом в статье про pipeline на Vector.
У Collector своя цена — сборка. Contrib-образ содержит всё подряд: 107 ресиверов в версии 0.157. Для прода собирают свой дистрибутив через OCB, включая только нужные компоненты, — это дополнительный шаг в CI, которого у двух других нет.
Fluent Bit ведёт две ветки параллельно. На момент замеров свежие релизы 4.2.x и 5.0.x выходили вперемешку. Выбор ветки — отдельное решение, которое надо принять осознанно.
Образы и пользователь по умолчанию:
| Инструмент | Размер образа | Пользователь |
|---|---|---|
| Fluent Bit 5.0.9 | 48 МБ | root |
| Vector 0.57.0-debian | 88 МБ | root |
| Collector contrib 0.157.0 | 108 МБ | 10001:10001 |
Collector — единственный, кто по умолчанию непривилегированный. На стенде это обернулось падением на named volume, созданном docker от root: пришлось готовить том перед стартом, как это делает initContainer в k8s. Понижать его до root ради удобства неправильно — безопасное умолчание тут достоинство, а не помеха.
Образы Collector и Fluent Bit — distroless, внутрь не зайти: docker run --entrypoint sh отвечает executable file not found. Для отладки это ощутимо, для поверхности атаки — наоборот.
Проверка конфига до запуска. Заведомо сломанный конфиг ловят все трое, но по-разному:
| Инструмент | Команда | Код на сломанном конфиге |
|---|---|---|
| Vector | vector validate |
78 |
| Collector | otelcol validate --config |
1 |
| Fluent Bit | отдельной команды нет, только старт | 1 |
Разница существеннее, чем кажется. У Vector и Collector проверка не ограничивается разбором YAML: обе строят компоненты и трогают окружение. Валидация Collector, например, поймала отсутствие каталога для состояния и устаревший формат секции телеметрии — до всякого запуска. У Fluent Bit единственный способ проверить конфиг — запустить процесс, что в CI означает поднимать контейнер и смотреть, не упал ли он.
Три умолчания, которые тихо портят данные
Отдельная категория, о которой в сравнениях не пишут, потому что она не видна ни в документации, ни в бенчмарках. Каждое из этих умолчаний ломает данные, не поднимая никакого сигнала: в логе чисто, доставка идёт, документы в индексе есть.
Collector режет строки активно пишущегося файла. Параметр force_flush_period ресивера file_log по умолчанию 500 мс: если строка не завершилась переводом каретки за этот срок, ресивер отправляет прочитанное как самостоятельную запись. На стенде тринадцать строк nginx и четырнадцать строк app уехали пятьюдесятью четырьмя обрывками — одна строка лога превратилась в два события, ни одно из которых не прошло разбор.
Проверяется это арифметикой: разорванная строка даёт +1 документ и +2 нераспарсенных, значит обрывков должно быть ровно вдвое больше расхождения. Так и вышло — 54 против 2 × 27. С force_flush_period: 0 не остаётся ни расхождения, ни обрывков. Vector и Fluent Bit в тех же условиях конца строки дожидаются.
Fluent Bit теряет чанки при исправной сети. Лимит буфера HTTP-ответа у выхода opensearch — 512 КБ по умолчанию. OpenSearch отвечает на bulk статусом по каждому документу, и на реальных пачках ответ в умолчание не влезает:
[warn] [http_client] cannot increase buffer: current=512000 requested=544768 max=512000
[warn] [output:opensearch:opensearch.0] http_do=-1 URI=/_bulk
[error] [engine] chunk cannot be retried: task_id=0, input=tail.0 > output=opensearch.0Проблема растёт вместе с размером пачки — то есть появляется под нагрузкой и не воспроизводится на маленьких прогонах. Лечится buffer_size: 8M.
Vector на стенде ушёл в формат чужой версии API. У синка elasticsearch параметр api_version по умолчанию auto: Vector опрашивает бэкенд и подстраивает формат bulk. На этом стенде определение иногда не срабатывало, и в метаданные bulk попадал параметр _type, которого в OpenSearch 3.x нет:
400 illegal_argument_exception:
"Action/metadata line [1] contains an unknown parameter [_type]"Vector считает 400 неретраибельной ошибкой и выбрасывает пачку целиком. Воспроизводилось непостоянно: при неспешном старте определение проходило и запись шла, при быстром перезапуске в скрипте замера — нет. Документация описывает более условную логику отката, завязанную в том числе на suppress_type_name, так что универсальным правилом это наблюдение считать не стоит — но api_version: v8 задать явно дешевле, чем разбираться, почему поток исчез.
Общее у всех трёх: заметить их можно только сверкой точного ожидания с фактом. Стенд, который проверяет «доехало примерно столько» или «не меньше N процентов», пропустит все три.
Развилка выбора
Только логи, файлы, ограниченные ресурсы → Fluent Bit. Если задача — читать файлы, разобрать их парсером и сложить в бэкенд, он дешевле остальных в разы и по процессору, и по памяти, и единственный освобождает место на диске после доставки. Платой будет потолок выразительности: условная логика сложнее «сматчилось или нет» требует Lua, дедупликации нет, проверить конфиг можно только запуском.
Логи как основной сигнал и сложная обработка → Vector. VRL — полноценный язык с функциями под конкретные форматы, дедупликация и буферы встроены, ретраи переживают даже исчезновение бэкенда из DNS. Цена — память: 150 МиБ в простое и полгигабайта под нагрузкой на каждом хосте, плюс жизнь на ветке 0.x с ломающими изменениями между минорными версиями.
OTLP, трейсы, инструментированные приложения → Collector. Здесь у него нет конкурентов среди двоих: оба кодирования OTLP, все три сигнала одинаково полноправны, tail sampling, connectors между конвейерами, экосистема OpenTelemetry целиком. Цена — процессор (втрое дороже Vector на том же потоке), обязательная явная настройка ёмкости очереди и потеря данных при исчезновении бэкенда из DNS.
Оговорка, без которой развилка врёт: это выбор для конкретной задачи, а не навсегда. Ниже — про то, что бывает, когда задач две.
«Оба вместе» на практике
Совет ставить оба звучит в каждом сравнении и выглядит взрослым компромиссом: Vector агентом логов на хосте, Collector хабом для OTLP. Логи идут через сильный на логах инструмент, трейсы — через родной для них, стык между ними по OTLP.
Стык собирается, но не так, как подсказывает схема на салфетке.
Наивная сборка — прочитать файлы, разобрать VRL и отдать синком opentelemetry — не работает. Прогон: 40 000 событий, ожидание 36 627, в индексе ничего, индекса нет вовсе. В логе Vector:
ERROR sink{component_id=to_collector component_type=opentelemetry}:
Not retriable; dropping the request. reason="Default retry strategy: Bad Request"
ERROR component_events_dropped: Events dropped intentional=false count=18962
ERROR component_events_dropped: Events dropped intentional=false count=17665Collector при этом не пишет ни строки: с его точки зрения пришёл некорректный запрос, он ответил 400.
Дело не в том, что Collector не понимает JSON — OTLP/HTTP он принимает в обоих кодированиях, и protobuf, и JSON. Дело в том, что codec: json отправляет не OTLP/JSON, а обычный JSON события Vector: без конверта resourceLogs, без scopeLogs, без структуры записи. Для приёмника OTLP это просто посторонний документ.
Нужный кодек существует, и это ключевая деталь: encoding.codec: otlp кодирует события в OTLP-protobuf, то есть ровно в тот формат, который приёмник ждёт. Но сам по себе он ничего не спасает — с ним Vector падает раньше отправки, и ошибка уже своя, а не от Collector:
ERROR codecs::internal_events: Failed serializing frame.
error=Log event does not contain OTLP top-level fields
(resourceLogs or resourceMetrics)
error_code="encoder_serialize" error_type="encoder_failed" stage="sending"
ERROR vector::internal_events::common: Failed to build request.
error=unable to encode event error_type="encoder_failed"Вот в чём суть: кодек не преобразует событие Vector в OTLP — он ожидает событие, которое уже имеет структуру OTLP. Строка лога, прочитанная из файла и разобранная в поля, такой структуры не имеет, и кодек честно отказывается её кодировать.
Значит, укладывать событие в proto-модель OTel приходится самому — трансформацией:
to_otlp:
type: remap
inputs: [drop_health, parse_app]
source: |
svc = to_string(.service) ?? "unknown"
pipe = to_string(.pipeline) ?? "vector-combo"
msg = encode_json(.)
ts = to_string(to_unix_timestamp(now(), unit: "nanoseconds"))
. = {
"resourceLogs": [
{
"resource": {
"attributes": [
{"key": "service.name", "value": {"stringValue": svc}}
]
},
"scopeLogs": [
{
"scope": {"name": "vector"},
"logRecords": [
{
"timeUnixNano": ts,
"observedTimeUnixNano": ts,
"severityNumber": 9,
"severityText": "INFO",
"body": {"stringValue": msg},
"attributes": [
{"key": "pipeline", "value": {"stringValue": pipe}},
{"key": "service", "value": {"stringValue": svc}}
]
}
]
}
]
}
]
}С этой трансформацией связка работает полностью: 36 627 документов при ожидании 36 627, потерь нет, ошибок в логе нет.
Так что вывод не «связка невозможна», а «связка стоит дороже, чем кажется по схеме».
Цена видна прямо в этом фрагменте, и стоит проговорить её честно, потому что показанная конфигурация — рабочая, но не образцовая:
- поля перестают быть полями. Всё событие уложено в
bodyодной JSON-строкой, вattributesвынесены толькоserviceиpipeline. Искать поduration_msв бэкенде после такого не выйдет. Разложить поля поattributesможно, но тогда каждое надо описать парой key/value с типизированным значением, и трансформация вырастает в несколько раз; - время события подменяется временем обработки.
timeUnixNanoберётся изnow(), а не из разобранного лога. Для правильногоtimeUnixNanoнужно достать время из события и перевести в наносекунды — ещё несколько строк, свои для каждого формата; - это код, не имеющий отношения к предметной области. Двадцать пять строк, которые описывают чужую модель данных и которые придётся сопровождать.
Именно поэтому связка на стенде проверялась только на доставку: счётчик сошёлся, потерь нет. Сравнивать её выходные документы с тремя основными конвейерами бессмысленно — они собраны по-другому и по другой схеме полей.
Есть и ограничение транспорта: синк умеет только HTTP, protocol.type: grpc отвергается как unknown variant. Для OTLP это законно — HTTP+protobuf спецификацией предусмотрен, — но выбора нет.
Практические варианты, если связка нужна:
- собрать OTLP-структуру в VRL, как выше: работает, но конвейер обрастает кодом, который к вашей предметной области отношения не имеет;
- поставить Collector первым: он принимает OTLP от приложений и отдаёт Vector, которому остаётся обработка и доставка. Этим путём в статье проверялся приём трейсов Vector, и он не требует ручной сборки вообще;
- стыковать через брокер — Kafka или NATS понимают оба инструмента, и вопрос кодировки снимается;
- держать конвейеры раздельными: логи через Vector, трейсы через Collector, без стыка между ними.
Отдельно о том, чем опасен первый, наивный вариант. Оба конфига проходят валидацию. Оба процесса стартуют без ошибок. Метрики показывают, что события читаются и отправляются. Единственный признак неисправности — пустой индекс на другом конце, и обнаруживается он тогда, когда кто-то пойдёт искать логи. С codec: otlp хотя бы видна внятная ошибка сериализации, указывающая на причину.
Когда не нужен ни тот, ни другой
Слой сбора — не бесплатная абстракция: ещё один процесс на каждом хосте, ещё один конфиг, ещё одно место, где теряются данные. Иногда он не нужен.
Одно приложение, один хост, немного логов. docker logs и journalctl решают задачу полностью. Слой сбора появляется, когда хостов становится больше одного.
Приложение уже пишет напрямую в бэкенд. Библиотеки логирования умеют отправлять в Loki, OpenSearch или ClickHouse сами. Это хуже переживает недоступность бэкенда и связывает приложение с хранилищем, но для внутреннего сервиса бывает достаточно.
Kubernetes с готовым решением. Если в кластере уже развёрнут DaemonSet сбора логов, второй слой рядом обычно создаёт больше проблем, чем решает.
Только трейсы и метрики, логов почти нет. SDK OpenTelemetry умеет отправлять напрямую в бэкенд без Collector. Промежуточный слой понадобится, когда нужны сэмплинг, обогащение или буферизация.
Когда слой всё-таки нужен и зачем — разобрано в статье про OpenTelemetry Collector.
Итог
Вопрос «Vector или OpenTelemetry Collector» поставлен неверно, но не потому, что «оба хороши», а потому, что у них разные основные сигналы, и это видно в поведении, а не в маркетинге.
Что показали замеры на одинаковой работе:
- ресурсы: Fluent Bit дешевле обоих кратно; между Vector и Collector размен — втрое меньше процессора ценой вчетверо большей памяти;
- трансформация: разбор строк удваивает CPU у Vector и Fluent Bit, у Collector теряется на фоне его собственной стоимости;
- надёжность: Vector и Fluent Bit не потеряли ничего в обоих режимах отказа; Collector требует явной настройки очереди, а при исчезновении бэкенда из DNS теряет данные независимо от неё;
- перезапуск: все трое одинаковы — том под состояние обязателен, иначе ровное удвоение;
- OTLP: Vector принимает трейсы и сохраняет корреляцию, но только в protobuf, и настройка декодирования работает обратно своему названию;
- связка по OTLP: в направлении Vector → Collector собирается, но требует ручной укладки события в proto-модель OTel — сам по себе ни один кодек этого не делает.
Практический вывод короче: берите Fluent Bit, если задача простая; Vector, если логи основной сигнал и нужна серьёзная обработка; Collector, если основной сигнал OTLP. А если нужны оба — проще ставить Collector первым: обратный порядок работает, но конвейер приходится обвешивать кодом сборки OTLP-структуры.
И общее наблюдение, которое оказалось важнее любого из чисел. Три из найденных дефектов — разрезанные строки у Collector, потерянные чанки у Fluent Bit, чужой формат API у Vector — портят данные молча. Ни один не поднимает ошибку, не роняет процесс и не меняет метрики. Единственное, что их ловит, — сверка точного числа отправленных событий с числом доехавших. Если в вашем конвейере такой сверки нет, вы не знаете, доезжают ли логи целиком; вы знаете только, что процесс жив.
Как устроен сам Vector изнутри — VRL, буферы, брокер между агентом и агрегатором — в статье про pipeline логов на Vector. Зачем нужен слой сбора и как его разворачивать — в статье про OpenTelemetry Collector. Где хранить логи после сбора — будет в сравнении Loki, OpenSearch и VictoriaLogsготовится, с 21 октября.
Стенд со всеми конфигами, скриптами и сырыми выводами замеров — digital-cookbook/observability/vector-vs-collector.
Документация и первоисточники
- Vector documentation — конфигурация, VRL, буферы
- Vector highlights и upgrade guides — ломающие изменения между версиями
- OpenTelemetry Collector — архитектура, компоненты, OCB
- OTTL — язык трансформаций Collector
- Fluent Bit documentation — входы, парсеры, буферизация
- OTLP specification — протокол, кодирования, транспорты
Комментарии