Адаптивный пересказ статьи Reliable OpenTelemetry ingestion at scale из блога ClickHouse. Текст мой, конфиги — как в оригинале. Цифры относятся к инсталляции ClickHouse Cloud.
Вопрос, на который приходится отвечать любому, кто собирает телеметрию: что происходит с данными, пока база недоступна? Для маленьких объёмов ответ «ретраи в памяти» работает. На 50 миллионах событий в секунду и 10 ГБ/с сжатого трафика — нет.
Ниже — три итерации, через которые прошла команда ClickHouse, и почему первые две не выдержали.
Итерация 1: агенты и шлюзы
Классическая двухуровневая схема: DaemonSet-агенты на каждой ноде собирают данные и с минимальной буферизацией отправляют на общие шлюзы (gateway), а те копят батчи побольше и пишут в ClickHouse.
Схема работает ровно до первого сбоя базы. Дальше данные некуда девать, они копятся в памяти шлюзов, и те уходят в OOM за считаные минуты после начала недоступности ClickHouse. Причём падение шлюзов роняет и приём от агентов — авария расширяется.
Это ключевой урок: буфер в памяти — не защита от backpressure, а способ превратить недоступность базы в отказ всего конвейера.
Итерация 2: журнал на диске
Логичный следующий шаг — вынести буфер на диск. У OTel-коллектора есть расширение file_storage, позволяющее складывать очередь в PVC.
Данные перестали теряться, но появились три новые проблемы:
- Коллектор становится stateful. Нужен StatefulSet, а спека пода у него неизменяемая — обновление конфигурации усложняется.
- Разбор завала мучительно долгий. Слив 1 ТиБ накопленного бэклога занимал около 4 часов.
- FIFO блокирует свежие данные. Очередь разбирается по порядку, поэтому актуальная телеметрия ждёт, пока проедет весь накопленный хвост. Во время инцидента это ровно то, что нужно меньше всего.
На практике это приводило к обходному манёвру: обрезать журнал, то есть выбросить накопленное, чтобы вернуть свежие данные. Что возвращает нас к потере данных, от которой уходили.
Итерация 3: перелив в S3
Итоговая схема исходит из другого принципа: не буферизовать у себя, а сливать в объектное хранилище и разбирать завал отдельным процессом.
Ограничения, которые команда себе поставила: без Kafka для сырой телеметрии, дёшево на своих объёмах, устойчиво к любому backpressure, на открытых компонентах.
flowchart LR
A["Агенты<br/>(DaemonSet)"] --> G["Gateway-коллектор<br/>failover-коннектор"]
G -->|"приоритет 1<br/>норма"| CH[("ClickHouse")]
G -->|"приоритет 2<br/>при отказе"| S3[("S3")]
S3 --> SQS["SQS<br/>уведомления"]
SQS --> C["Catchup-коллектор<br/>масштабируется до 0"]
C --> CH
Три компонента:
Failover-коннектор. Направляет данные по приоритетным конвейерам и сам определяет здоровье пути по результату отправки. Критичная деталь: у ClickHouse-экспортёра нужно отключить очередь отправки, иначе ошибки «съедаются» очередью и коннектор их не увидит — переключения не произойдёт.
Перелив в S3. Работает как аварийный клапан, а не как постоянный путь. Экономика здесь решает: на 50 млн событий/с стоимость одних только PUT-операций перекрывает стоимость хранения (оценка — от 100 до 500 тысяч долларов в год). Поэтому в S3 пишут строго во время реальных сбоев. Плюс батчи размазывают по широкому пространству ключей, чтобы при частичной деградации хранилища не упереться в троттлинг одного префикса.
Catchup-коллектор. Отдельный stateless-деплоймент, который читает уведомления из SQS и заливает накопленное в ClickHouse. Он масштабируется до нуля, когда очередь пуста, использует синхронные вставки, а неудачные сообщения просто остаются в очереди на повтор.
Конфигурация
Шлюз с приоритетной маршрутизацией:
connectors:
failover/logs:
priority_levels:
- [logs/clickhouse]
- [logs/s3]
retry_interval: 30s
sending_queue:
enabled: true
queue_size: 10000
exporters:
clickhouse:
sending_queue:
enabled: false # иначе ошибки не дойдут до коннектора
retry_on_failure:
enabled: true
awss3/logs:
sending_queue:
enabled: true
queue_size: 10000Catchup-коллектор для разбора завала:
receivers:
awss3/logs:
s3downloader:
s3_bucket: ""
s3_prefix: logs
sqs:
queue_url: ""
exporters:
clickhouse:
connection_params:
async_insert: "1"
wait_for_async_insert: "1"
retry_on_failure:
enabled: trueОбратите внимание на async_insert: 1 вместе с wait_for_async_insert: 1 — ровно та комбинация, которую мы разбирали в части 6: серверный батчинг с подтверждением реальной записи.
Что это даёт и чем платить
Разделение путей решает главную проблему: временные ошибки гасятся ретраями внутри экспортёра и не вызывают переключения, а настоящий backpressure автоматически уводит поток в S3. Когда база возвращается, живой трафик сразу идёт напрямую, а завал разбирается независимо и не задерживает свежие данные — то, чего не умела схема с FIFO-журналом.
Проверка была прямой: часовое учебное отключение целиком, без OOM и без потерянных записей.
Цена решения честная:
- больше инфраструктуры в каждом регионе: бакеты, очереди, уведомления;
- конфигурация коллекторов заметно сложнее, чем «агент → шлюз»;
- во время разбора завала непонятен статус отдельных записей — не видно, чья именно телеметрия ещё не долилась.
Что забрать себе
Даже если у вас не 50 млн событий в секунду, три идеи переносятся на любой масштаб:
- Буфер в памяти — не план на случай сбоя. Он превращает недоступность базы в падение конвейера.
- Разделяйте живой поток и разбор завала. Один процесс не должен делать оба дела, иначе свежие данные будут ждать старые.
- Отключайте очередь там, где нужно видеть ошибки. Проглоченная ошибка ломает любую логику переключения.
Дальше: Часть 12. Wide events: чем они лучше логов и метрик.