Часть 11. Надёжный приём OpenTelemetry: что делать, когда ClickHouse недоступен

Три итерации архитектуры приёма телеметрии в ClickHouse на 50 млн событий/с: почему падает связка agent→gateway, чем плох WAL на диске и как устроен перелив в S3 через failover-коннектор с отдельным catchup-коллектором.
Опубликовано:

Адаптивный пересказ статьи Reliable OpenTelemetry ingestion at scale из блога ClickHouse. Текст мой, конфиги — как в оригинале. Цифры относятся к инсталляции ClickHouse Cloud.

Вопрос, на который приходится отвечать любому, кто собирает телеметрию: что происходит с данными, пока база недоступна? Для маленьких объёмов ответ «ретраи в памяти» работает. На 50 миллионах событий в секунду и 10 ГБ/с сжатого трафика — нет.

Ниже — три итерации, через которые прошла команда ClickHouse, и почему первые две не выдержали.

Итерация 1: агенты и шлюзы

Классическая двухуровневая схема: DaemonSet-агенты на каждой ноде собирают данные и с минимальной буферизацией отправляют на общие шлюзы (gateway), а те копят батчи побольше и пишут в ClickHouse.

Схема работает ровно до первого сбоя базы. Дальше данные некуда девать, они копятся в памяти шлюзов, и те уходят в OOM за считаные минуты после начала недоступности ClickHouse. Причём падение шлюзов роняет и приём от агентов — авария расширяется.

Это ключевой урок: буфер в памяти — не защита от backpressure, а способ превратить недоступность базы в отказ всего конвейера.

Итерация 2: журнал на диске

Логичный следующий шаг — вынести буфер на диск. У OTel-коллектора есть расширение file_storage, позволяющее складывать очередь в PVC.

Данные перестали теряться, но появились три новые проблемы:

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

Итерация 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. Он масштабируется до нуля, когда очередь пуста, использует синхронные вставки, а неудачные сообщения просто остаются в очереди на повтор.

Конфигурация

Шлюз с приоритетной маршрутизацией:

YAML
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: 10000
Нажмите, чтобы развернуть и увидеть больше

Catchup-коллектор для разбора завала:

YAML
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 млн событий в секунду, три идеи переносятся на любой масштаб:

  1. Буфер в памяти — не план на случай сбоя. Он превращает недоступность базы в падение конвейера.
  2. Разделяйте живой поток и разбор завала. Один процесс не должен делать оба дела, иначе свежие данные будут ждать старые.
  3. Отключайте очередь там, где нужно видеть ошибки. Проглоченная ошибка ломает любую логику переключения.

Дальше: Часть 12. Wide events: чем они лучше логов и метрик.

Начать поиск

Введите ключевые слова для поиска статей

↑↓
ESC
⌘K Горячая клавиша