ClickHouse под трейсы: почему шесть бэкендов Jaeger оказались выбором из двух


Cloud Tracing — сервис распределённой трассировки в VK Cloud. Приложения отправляют в него спаны по протоколу OpenTelemetry, сервис хранит их и отдаёт через Jaeger-совместимое API. Поэтому трейсы можно открывать в привычных Jaeger UI и Grafana без доработок.
За этой совместимостью стоит выбор хранилища, который для публичного облака оказывается сложнее, чем кажется. Сервис принимает поток спанов от сотен несвязанных клиентов, хранит десятки терабайт телеметрии и должен надёжно изолировать данные проектов друг от друга. По умолчанию один проект может отправлять до 1 МБ/с, а доступ к данным контролируется токенами IAM. При таких ограничениях недостаточно просто выбрать базу, которая быстро записывает спаны или умеет находить трейс по trace ID.
На странице развёртывания Jaeger на момент проектирования сервиса были перечислены шесть вариантов: Cassandra, Elasticsearch, Kafka, gRPC-плагин, Badger и memory. На первый взгляд это полноценный список кандидатов. Но локальные хранилища отпадают для прода, Kafka оказывается буфером перед настоящим хранилищем, а gRPC-плагин служит способом подключить систему, которой в списке нет. В результате сравнивать приходится Cassandra и Elasticsearch, а через плагин появляется третий вариант — ClickHouse.
Я Леонид Левин, архитектор PaaS в VK Cloud. Этот разбор мы подготовили вместе с технологическим евангелистом Стасом Погоржельским и инженерами сервиса. Мы разберём, почему Cassandra и Elasticsearch по-разному подходят для хранения трейсов, что дали чужие продовые кейсы и наши замеры до 362 тысяч спанов в секунду на четырёх ядрах коллектора, а также как схема хранения на ClickHouse закрыла точечное чтение и поиск по атрибутам.
Однако бенчмарки и стоимость хранения определили только часть решения. Главным ограничением стала мультитенантность: Jaeger изначально рассчитан на одну организацию и не даёт готовой изоляции сотен независимых проектов. Именно это требование привело к собственному приёмнику на записи, авторизующему прокси на чтении и схеме, в которой ClickHouse стал хранилищем за Jaeger-совместимым API.
Требования к хранилищу трейсов
Трейс состоит из спанов — записей об отдельных операциях. Спан содержит идентификатор трейса, имя сервиса и операции, время начала, длительность, теги и статус. Приложения отправляют спаны непрерывным потоком, а после записи их не обновляют и не удаляют по одному.
Такой профиль нагрузки характерен для append-only-хранилищ с удалением данных по возрасту. Поэтому хранилище должно недорого принимать непрерывный поток записей.
Трейсы читают двумя способами. По известному trace ID получают все спаны одного трейса и строят водопад. Поисковые запросы находят трейсы сервиса X с операцией Y, тегом Z и длительностью больше секунды за последние два часа.
Запрос по trace ID хорошо ложится на key-value-хранилище. Поиск по сервису, операции, тегам и длительности требует фильтровать и агрегировать большой объём данных. Хранилищу нужно справляться с обоими сценариями, и на поиске различия между бэкендами становятся особенно заметны.
На практике поисковые запросы и агрегации могут преобладать. У Uber более 80% запросов к платформе телеметрии приходилось на гистограммы, перцентили и группировки. В Elasticsearch дашборд по часовому окну логов открывался медленно, а запросы за шесть часов уже не укладывались в таймаут.
Следующий фактор — объём данных. Мы рассчитали его для одного тенанта. При лимите приёма 1 МБ/с проект генерирует около 86 ГБ сырых данных в сутки. После сжатия остаётся примерно 17 ГБ, за 30 дней — около 510 ГБ. Сто активных проектов — наша модельная точка для расчётов — дают чуть больше 50 ТБ за месяц. В расчёте используется средний размер записи 1–2 КБ, без запаса на всплески.
Наконец, сервис изначально строился как мультитенантный. В публичном облаке в одно хранилище пишут сотни независимых клиентов, и каждый должен видеть только свои трейсы. Изоляцию нужно обеспечить на всех слоях сервиса. Именно это требование в итоге сильнее всего повлияло на выбор.
Похожая задача стояла не только перед нами. SigNoz, Uptrace и qryn, сейчас gigapipe построили свои observability-платформы поверх ClickHouse. По опросу Grafana за 2025 год OpenTelemetry-трейсы собирает уже половина организаций. Хранилище для трейсов стало массовой задачей.
Выбор ограничивало продуктовое требование. Команды внутри облака уже использовали Jaeger с Elasticsearch или Cassandra и смотрели трейсы в Jaeger UI и Grafana. Новый сервис должен был сохранить Jaeger-совместимое API, чтобы не пришлось менять эту обвязку.
Поэтому в первую очередь мы рассматривали бэкенды Jaeger. Готовые observability-платформы на ClickHouse с собственными API и моделями данных не подходили, как и Grafana Tempo, где мультитенантность строится на заголовке X-Scope-OrgID. Чтобы сохранить совместимость с Jaeger, поверх таких систем пришлось бы ставить отдельный прокси и адаптировать его к чужой модели данных. При этом команда уже эксплуатировала ClickHouse в соседних сервисах телеметрии.
Шесть бэкендов: два кандидата и одна дверь
На странице Jaeger перечислены шесть вариантов, но к выбору хранилища относятся не все.
memory хранит данные в памяти процесса all-in-one-сборки и нужен для демо. Badger — embedded key-value-база с данными на локальном диске одного экземпляра. Репликации и горизонтального масштабирования у неё нет. Для прода с десятками терабайт оба варианта отпадают сразу.
Kafka тоже не хранит трейсы постоянно. В штатной схеме jaeger-collector записывает спаны в топик, а jaeger-ingester читает их и передаёт в Cassandra или Elasticsearch. Очередь сглаживает пики и помогает пережить недоступность базы, но хранилище всё равно нужно выбирать отдельно. К связке collector → Kafka → ingester вернёмся в разделе о мультитенантности.
gRPC-плагин — это интерфейс для внешнего хранилища. Jaeger передаёт данные отдельному процессу по gRPC, а тот уже записывает их в выбранную систему. Через него к Jaeger подключается ClickHouse: этот интерфейс реализует community-плагин jaeger-clickhouse.
В итоге из официального списка остаются Cassandra и Elasticsearch. ClickHouse идёт отдельным путём — через gRPC-плагин.
Cassandra против Elasticsearch
В FAQ команда Jaeger рекомендует выбирать OpenSearch или Elasticsearch вместо Cassandra. Причина в том, как эти базы отвечают на типовые запросы к трейсингу.
Cassandra хорошо подходит для чтения трейса по trace ID. Но поиск по сервису, операции, тегам или длительности в её модель данных не укладывается. Jaeger собирает такие запросы сам, выполняя несколько key-value-операций на стороне клиента. В FAQ прямо предупреждают, что такой поиск ограничен и может выдавать непоследовательные результаты.
Проблема описана в issue #166, который открыт с 2017 года. При поиске по сервису, операции и тегам Jaeger пересекает наборы trace ID без гарантированного порядка. Затем LIMIT отбрасывает часть подходящих результатов. В обсуждении предлагают сначала фильтровать по тегам с низкой кардинальностью, но этот обход не меняет сам принцип работы поиска.
Одиночные записи Cassandra обрабатывает быстрее Elasticsearch. Однако для поиска Jaeger создаёт несколько записей на каждый спан: отдельно для имени сервиса, операции и каждого тега. Так возникает write amplification. В FAQ отмечают, что из-за этих дополнительных записей итоговая пропускная способность оказывается близка к Elasticsearch, где спан записывается одной операцией, а индексы строит сама база.
Elasticsearch изначально рассчитан на поиск. Jaeger передаёт спан одной записью, а база индексирует его и позволяет искать по тегам без дополнительной логики на стороне клиента. Поэтому команда Jaeger и рекомендует Elasticsearch или OpenSearch.
За это приходится платить ресурсами. Elasticsearch по умолчанию строит инвертированные индексы по всем полям, записывает данные в translog, периодически выполняет флеши и фоновые слияния сегментов. В разборе Uber около 5% проиндексированных полей использовались в запросах, а остальная индексация тратила ресурсы без практической пользы.
В Cassandra срок хранения задаётся нативным TTL: устаревшие записи удаляются автоматически. В Elasticsearch для этого нужно настроить и обслуживать ротацию индексов.
Cassandra подходит, когда трейсы в основном получают по известному ID. Elasticsearch удобнее для поиска по атрибутам, но требует ресурсов на индексацию. При этом значительная часть запросов к телеметрии — это агрегации, а не поиск отдельных трейсов. По данным Uber, ни Cassandra, ни Elasticsearch не оптимальны для такого профиля чтения.
ClickHouse через gRPC-плагин
К моменту выбора у нас уже были примеры компаний, которые хранили телеметрию в ClickHouse на существенно больших объёмах.
В 2021 году Uber рассказал о переносе лог-платформы с ELK на ClickHouse. До миграции в каждом регионе работало больше 20 кластеров Elasticsearch и 50 пайплайнов Logstash. После миграции один узел ClickHouse принимал 300 тысяч записей в секунду — примерно в десять раз больше, чем один узел Elasticsearch. При растущем трафике расходы на оборудование сократились более чем вдвое. Средний коэффициент сжатия составил около 3x, а для некоторых данных доходил до 30x. В логах нет чтения трейса по trace ID, но непрерывная запись и агрегатные запросы создают похожую нагрузку.
Alibaba Cloud сравнила ClickHouse и Elasticsearch на одинаковых кластерах: по четыре узла с 8 vCPU и 32 ГБ памяти. В одном из наборов было 570 миллионов записей трейсов с traceId, spanId и тегами. ClickHouse оказался быстрее в аналитических запросах и импорте данных, а также требовал меньше ресурсов на хранение. Elasticsearch лучше справлялся с точечным поиском, если в кластере было достаточно памяти.
В 2019 году Contentsquare перенесла аналитику с Elasticsearch на ClickHouse. Компания хранила около 500 ТБ данных с ретеншеном 13 месяцев. Миграция заняла полтора года. По итогам ClickHouse оказался в 11 раз дешевле, среднее время выполнения запросов сократилось в четыре раза, а 99-й перцентиль — в десять. Одной из главных причин перехода стала нестабильность кластера Elasticsearch под этой нагрузкой.
Отдельный замер для трейсов опубликовала ClickHouse Inc. в 2023 году. В исследовании 200 миллионов OpenTelemetry-спанов с примерно 133 ГиБ несжатых колонок заняли на диске 13,7 ГиБ. Суммарный коэффициент сжатия составил 9,72x, а колонки с атрибутами ресурсов сжались в 26 раз. Поиск по trace ID на этом объёме занял 0,197 секунды, с ограничением по времени — 0,110 секунды.
Эти цифры нельзя напрямую переносить на другую систему: результат зависит от структуры данных, объёма памяти, запросов и схемы хранения. Но кейсы сходятся в одном: ClickHouse хорошо подходит для телеметрии, которую записывают непрерывным потоком и затем читают преимущественно агрегатными запросами. В таком профиле он обычно быстрее поискового движка и требует меньше ресурсов на хранение.
Наш замер записи
Приёмная часть Cloud Tracing — trace-collector, сервис на Go. Он принимает OpenTelemetry-протокол, конвертирует спаны в модель Jaeger и записывает их в ClickHouse батчами.
Стенд для замера: 4 CPU и 4 ГБ памяти на коллектор, столько же — на ClickHouse. Нагрузку создавал наш тестовый клиент с настраиваемым числом параллельных клиентов и количеством спанов в батче.
Воркеры | Спанов в батче | Запросов/с | Спанов/с | МБ/с |
2 | 100 | 169 | 16 907 | 1,9 |
2 | 1000 | 125 | 125 130 | 14 |
8 | 100 | 691 | 69 101 | 7,9 |
8 | 1000 | 336 | 335 768 | 39 |
32 | 100 | 2074 | 207 351 | 24 |
32 | 1000 | 362 | 361 990 | 42 |
Зависимость оказалась нелинейной по обоим параметрам. Увеличение числа воркеров в 16 раз при батче 100 подняло поток с 16,9 до 207 тысяч спанов в секунду. Простое увеличение батча со 100 до 1000 при двух воркерах дало 125 тысяч спанов в секунду.
Предел стенда — 362 тысячи спанов в секунду и 42 МБ/с на четырёх ядрах — дала комбинация обоих параметров. На большом пуле вклад размера батча оказался заметно выше. Для ClickHouse это ожидаемо: редкие крупные вставки для него — штатный режим.
Создатель Jaeger Юрий Шкуро в анонсе Jaeger v2 отдельно отмечал, что OpenTelemetry Collector передаёт данные батчами. Для таких хранилищ, как ClickHouse, это особенно важно: пакетные вставки работают заметно быстрее одиночных.
Профилирование того же стенда показало, на что уходит основная часть ресурсов при конвертации протоколов. Ниже — верхние строки heap-профиля; пути сокращены до имени пакета:
Flat | Flat % | Sum % | Cum % | Функция |
|---|---|---|---|---|
16.57GB | 59.82% | 59.82% | 60.35% |
|
5.20GB | 18.77% | 78.58% | 84.28% |
|
1.42GB | 5.14% | 83.72% | 5.14% |
|
1.09GB | 3.95% | 87.68% | 3.95% | |
0.69GB | 2.50% | 90.17% | 2.50% |
|
0.53GB | 1.93% | 92.10% | 1.93% |
|
Почти 60% памяти в куче занимала функция, которая собирает теги при конвертации OTLP в модель Jaeger. В CPU-профиле 28,5% времени приходилось на стандартный protobuf.
Строка ch-go/proto.(*ColStr).Append в том же профиле вывела на драйвер. Запись строковых колонок в Go-драйверах ClickHouse тогда работала не лучшим образом. Проблему разбирали в issue для clickhouse-go, который закрыли только в начале 2025 года, и в pull request для ch-go.
Профиль подсказал, что можно оптимизировать в приёмнике, но это тема для отдельного материала. Производительность из таблицы ограничивал процессор коллектора: ClickHouse на таком же стенде оставался недогружен. Больше всего времени уходило на конвертацию протоколов.
Схема хранения в двух таблицах
За основу мы взяли схему из jaeger-clickhouse. В одной таблице храним спан целиком и читаем трейсы по trace ID. Во второй лежат поля, по которым обычно ищут: сервис, операция, длительность и теги. Эта таблица рассчитана на фильтрацию и поиск.
Ниже — сокращённая схема плагина:
-- Трейс по ID: тело спана как сериализованная модель
CREATE TABLE jaeger_spans (
timestamp DateTime CODEC (Delta, ZSTD(1)),
traceID String CODEC (ZSTD(1)),
model String CODEC (ZSTD(3))
) ENGINE = MergeTree()
PARTITION BY toDate(timestamp)
ORDER BY traceID;
-- Поиск: атрибуты спана под фильтры
CREATE TABLE jaeger_index (
timestamp DateTime CODEC (Delta, ZSTD(1)),
traceID String CODEC (ZSTD(1)),
service LowCardinality(String) CODEC (ZSTD(1)),
operation LowCardinality(String) CODEC (ZSTD(1)),
durationUs UInt64 CODEC (ZSTD(1)),
tags Nested (
key LowCardinality(String),
value String
) CODEC (ZSTD(1)),
INDEX idx_tag_keys tags.key TYPE bloom_filter(0.01) GRANULARITY 64,
INDEX idx_duration durationUs TYPE minmax GRANULARITY 1
) ENGINE = MergeTree()
PARTITION BY toDate(timestamp)
ORDER BY (service, -toUnixTimestamp(timestamp));В этой схеме важны четыре детали:
Поисковая таблица отсортирована по сервису и времени в обратном порядке. Так обычно и запрашивают данные: нужны последние трейсы конкретного сервиса с заданными фильтрами.
Теги хранятся в
Nested-структуре, а по их ключам построен bloom-фильтр. При запросеerror=trueClickHouse может пропустить гранулы, где тегаerrorнет, и не читать их с диска.Список операций для выпадающего списка в Jaeger собирает материализованное представление на
SummingMergeTreeповерх поисковой таблицы.Данные разбиты на дневные партиции. Когда заканчивается срок хранения, ClickHouse удаляет целую партицию — то есть каталог с её данными. Ротировать индексы, как в Elasticsearch, не нужно.
Поиск по тегам в такой схеме выглядит как обычный SQL:
SELECT DISTINCT traceID
FROM jaeger_index
WHERE service = 'checkout'
AND operation = 'submit'
AND durationUs > 1000000
AND tags.value[indexOf(tags.key, 'error')] = 'true'
AND timestamp > now() - INTERVAL 2 HOUR
ORDER BY timestamp DESC
LIMIT 20;В Elasticsearch этот же запрос превращается в дерево из bool, must и nested:
{
"query": {
"bool": {
"must": [
{
"term": {
"process.serviceName": "checkout"
}
},
{
"term": {
"operationName": "submit"
}
},
{
"range": {
"duration": {
"gt": 1000000
}
}
},
{
"range": {
"startTimeMillis": {
"gte": "now-2h"
}
}
},
{
"nested": {
"path": "tags",
"query": {
"bool": {
"must": [
{
"term": {
"tags.key": "error"
}
},
{
"term": {
"tags.value": "true"
}
}
]
}
}
}
}
]
}
},
"size": 20,
"sort": [
{
"startTimeMillis": {
"order": "desc"
}
}
]
}Оба запроса решают одну задачу. Разница становится заметна на десятках терабайт данных: ClickHouse выполняет фильтрацию как SQL-запрос к колоночным данным, Elasticsearch — через поисковый индекс. От этого зависят требования к процессору, памяти и диску.
Кодеки из DDL выше на нашей модели данных дали сжатие от 5x до 18x. Этот диапазон мы учитывали в расчётах сайзинга из начала статьи.
Чужие кейсы и наш замер записи показывают одну картину: ClickHouse быстро принимает поток спанов, а узкое место у нас оказалось в конвертации протоколов на стороне коллектора. Схема из двух таблиц покрывает чтение трейса по trace ID и поиск по атрибутам. Но производительность и стоимость хранения решили только часть задачи. Дальше пришлось разбираться с мультитенантностью.

Lakehouse-платформа для аналитики и ML
Объединяйте данные из разных систем и снижайте расходы на хранение в 7–10 раз
Мультитенантность, которая перевесила бенчмарки
Сервису трассировки в публичном облаке нужна изоляция в трёх местах:
На приёме: спан должен быть привязан к конкретному клиенту, и эту привязку нельзя подделать.
В хранении: запрос одного клиента не должен физически захватывать данные другого.
На чтении: API и UI должны проверять, кто делает запрос, и отдавать только его данные.
Jaeger спроектирован для одной организации, поэтому все три задачи ложатся на команду сервиса. Их сложность различается, но закрывать приходится каждую.
Что умеют плагин и ingester
У jaeger-clickhouse мультитенантность формально есть. Гайд предлагает два режима:
Общие таблицы с колонкой tenant: каждая инсталляция плагина настраивается на собственное имя тенанта.
Отдельная база ClickHouse для каждого тенанта: изоляция жёстче, но сущностей больше.
В обоих случаях конфигурация содержит одно значение на инсталляцию. В гайде это сформулировано так: «Configure the per-tenant jaeger-clickhouse clients».
# Экземпляр плагина тенанта 1 # Экземпляр плагина тенанта 2
database: shared # database: shared
tenant: tenant_1 # tenant: tenant_2То есть для каждого тенанта нужно запускать отдельный экземпляр плагина и отдельный Jaeger. Режим мультитенантности выбирают при создании схемы: «Режим мультитенантности нужно включить при первом развёртывании, позже изменить эту настройку нельзя».
Для внутренней платформы с пятью командами это рабочая схема. У нас арифметика другая: старт с полусотни активных проектов, модельная сотня в расчёте сайзинга и целевой горизонт на два порядка выше. Тысячи экземпляров Jaeger с плагинами негде и незачем разворачивать.
Похожая проблема возникает и в Elasticsearch-пути, где изоляцию обычно строят на индексе для каждого тенанта. Каждый новый клиент приносит набор сущностей, которые нужно создавать, настраивать и обслуживать.
У jaeger-ingester, второго звена рекомендованной цепочки, мультитенантности нет вовсе. В его конфигурации нет ни колонки тенанта, ни отдельного пути записи. Это сразу исключило связку collector → Kafka → ingester. Даже если бы мы согласились на отдельную схему в читающей части, запись в мультитенантные таблицы всё равно пришлось бы реализовывать собственным кодом.
Бенчмарки Cassandra, Elasticsearch и ClickHouse отвечают на вопрос, что быстрее. Для нас решающим оказался другой вопрос: что вообще можно собрать в мультитенантную схему. Ответ пришлось собирать самостоятельно.
Что мы построили вместо этого
Раз штатный конвейер записи не подходил, мы написали свой приёмник — trace-collector из раздела с бенчмарком. Промежуточный вариант с OpenTelemetry Collector и clickhouseexporter позволил бы убрать часть конвертации. Но аутентификацию по токенам, привязку тенанта и лимиты на проект всё равно пришлось бы добавлять своим кодом. Кроме того, экспортёр и сейчас имеет статус beta.
Приёмник получил аутентификацию отправителя и привязку каждого спана к тенанту. Цепочка обошлась без jaeger-collector, Kafka и ingester. Тенантом стал проект облака. В платформе уже работала IAM-система на Keystone с токенами, ролями и сервисными аккаунтами. Трассировка встроилась в неё как ещё один ресурс с правами на чтение и запись.
Отказ от Kafka был отдельным решением архитектурного комитета. Очередь в пайплайне телеметрии считается хорошей практикой, её рекомендуют и разработчики Jaeger. Но в нашем случае были четыре аргумента:
Приёмник и так буферизует данные: перед записью он накапливает батчи в памяти.
Схема без очереди уже работала в двух соседних сервисах — логах и мониторинге, а значит, у команды был опыт эксплуатации.
У ClickHouse нет длительных окон обслуживания, а новые шарды добавляются без остановки кластера.
При временной недоступности части шардов роль буфера берут на себя
Distributed-таблицы: они умеют локально накапливать вставки и отправлять их после восстановления шарда.
Отказ приёмника приводит к потере содержимого его батч-буфера в памяти — то есть батчей за последние мгновения, а не часов записи. Цена решения — более слабая гарантия доставки, и она зафиксирована в требованиях. Сервис не обещает стопроцентную запись трейсов. Для телеметрии это приемлемый компромисс, для бизнес-данных — нет.
Distributed-таблицы решают и шардирование. В качестве ключа используется хеш trace ID, поэтому все спаны одного трейса попадают на один шард. Чтение водопада остаётся локальной операцией:
CREATE TABLE jaeger_spans AS jaeger_spans_local
ENGINE = Distributed(
'{cluster}',
jaeger,
jaeger_spans_local,
cityHash64(traceID)
);Первый прототип мультитенантности был предельно прост: тенант передавался в обычном заголовке, который можно было подменить. Именно на эту проблему указал архитектурный комитет.
В продовой схеме заголовок заменили полноценной проверкой токенов через IAM. Чтобы IAM не стал точкой отказа на пути каждого батча, приёмник кеширует результаты проверки.
Читающая часть построена зеркально. Jaeger-совместимый API поверх того же ClickHouse отдаёт трейсы, а перед ним работает авторизующий прокси. Он проверяет токен, определяет проект и добавляет фильтр по тенанту в каждый запрос. Снаружи это выглядит как обычный Jaeger: Grafana подключается штатным датасорсом, Jaeger UI работает без доработок, а контроль доступа остаётся в прокси.
Изоляцию ресурсов дополняет лимит приёма — 1 МБ/с на проект по умолчанию. Он не позволяет одному шумному тенанту занять полосу остальных.
Итоговая схема выглядит так:

От шести бэкендов из документации в этой схеме остались ClickHouse за gRPC-интерфейсом и модель данных Jaeger.
Мультитенантность в Jaeger и обвязке вокруг него есть только на уровне схемы хранения плагина, причём она требует отдельного экземпляра на каждого тенанта. Ingester не поддерживает её вовсе. Для сервиса на сотни и тысячи тенантов это означает собственный приёмник на записи и авторизующий прокси на чтении. Jaeger остаётся моделью данных и API-совместимостью.
Плагин, который закрыл апстрим
Путь через jaeger-clickhouse оказался временным решением. В README плагина есть предупреждение: «Этот модуль реализует только API grpc-plugin, который в Jaeger уже признан устаревшим». Sidecar-режим не поддерживается с версии 1.58, а сам плагин остался экспериментальным community-проектом. Jaeger v1, согласно плану команды, достиг конца жизненного цикла в конце 2025 года.
В ноябре 2024 года вышел Jaeger v2, построенный на базе OpenTelemetry Collector. В README плагина тогда обещали: «Jaeger v2 будет нативно поддерживать ClickHouse».
Обещание выполнили. Issue #5058, открытый в конце 2023 года для разработки ClickHouse как основного бэкенда, закрыли 30 мая 2026 года вместе со всеми двенадцатью подзадачами: от записи спанов до sampling store.
В актуальной документации ClickHouse уже указан как экспериментальный бэкенд за feature gate. Для крупных продовых инсталляций Jaeger пока рекомендует OpenSearch, но нативная поддержка ClickHouse теперь есть в апстриме.
Мультитенантность остаётся открытым вопросом. В документации Jaeger v2 по-прежнему нет изоляции тенантов в пути чтения. Поэтому при переходе на нативный ClickHouse-бэкенд наша обвязка сохранится.
Где Elasticsearch выигрывает
У Elasticsearch остаётся своё место. В замере Alibaba он быстрее находил отдельные записи при достаточном объёме памяти.
Если трейсы редко читают по trace ID, логи рядом требуют полнотекстового поиска, а весь рабочий объём помещается в памяти кластера, можно использовать Elasticsearch-бэкенд Jaeger без дополнительной обвязки. Не придётся писать собственный приёмник или подключать плагин.
В нашем случае решающими стали агрегатные запросы, объём данных и мультитенантность. Для другого профиля нагрузки Elasticsearch может быть более подходящим выбором.
Сводная таблица
Бэкенд | Распределённый | Поиск по тегам | Агрегации | Устаревание | Мультитенантность | Статус в Jaeger, 2026 |
| нет | да | нет | рестарт | нет | для демо |
Badger | нет | да | нет | TTL | нет | embedded |
Kafka | да | роль буфера | роль буфера | ретеншен топика | нет, только через собственные заголовки сообщений | буфер перед хранилищем |
Cassandra | да | ограничен, на стороне Jaeger | нет | нативный TTL | нет | основной |
Elasticsearch / OpenSearch | да | да, инвертированный индекс | слабые на объёме | ротация индексов | нет | основной, рекомендованный |
ClickHouse | да | да, SQL и skip-индексы | сильные | удаление партиций | колонка | v1: плагин; v2: нативный, experimental |
Оговорки и ссылки
Колонка МБ/с в таблице показывает входящий сетевой трафик синтетических спанов. Они заметно меньше реальных, поэтому эти значения нельзя напрямую сравнивать с оценкой в 1–2 КБ на запись, которую мы использовали для сайзинга.
Расчёт сайзинга основан на нашей модели данных. В другой системе итоговый объём и коэффициент сжатия будут зависеть прежде всего от состава тегов.
Что читать по теме:
jaeger-clickhouse — гайды по мультитенантности и шардированию.
clickhouseexporter для OpenTelemetry Collector. Для трейсов и логов экспортёр имеет статус beta.
Разбор схемы хранения спанов от ClickHouse.
Issue #5058 с дизайном нативного ClickHouse-бэкенда.
У нас остаются два открытых вопроса.
Tail-based-сэмплирование. Сейчас решение о сэмплировании принимает SDK на стороне клиента. Мы хотим принимать его после завершения трейса, учитывая ошибки и задержки. Для этого проектируем двухуровневую схему коллекторов.
Сравнение ClickHouse и OpenSearch. Следующим материалом станет собственный прогон ClickHouse и OpenSearch на нашем наборе трейсов и с нашим профилем запросов. Чужие результаты полезны как ориентир, но для выбора важнее получить сопоставимые данные на своей нагрузке.
Вместо вывода
Решение использовать ClickHouse мы приняли в 2023 году. Тогда апстрим только открыл задачу на нативный бэкенд, а подключить ClickHouse к Jaeger можно было лишь через community-плагин. За следующие два с половиной года путь, который приходилось прокладывать самостоятельно, дошёл до штатной поддержки в Jaeger v2.
Однако выбор определили не бенчмарки. Они подтвердили, что ClickHouse хорошо принимает поток спанов, сжимает телеметрию и справляется с поисковыми и агрегатными запросами. Но в публичном облаке этого недостаточно. Сервис должен понимать, от какого проекта пришёл каждый спан, не позволять подменить эту привязку и гарантировать, что запрос одного клиента не затронет данные другого. Для этого пришлось строить собственный путь записи с проверкой IAM-токенов и отдельный слой авторизации перед Jaeger-совместимым API.
Поэтому при выборе хранилища для трейсов стоит начинать не со списка поддерживаемых бэкендов и не с числа спанов в секунду. Сначала нужно описать реальные запросы, объём данных, срок хранения и границы между тенантами. После этого становится понятнее, достаточно ли штатного решения или вокруг него потребуется своя обвязка.
Описанная схема работает в Cloud Tracing в VK Cloud. Приложения отправляют данные по OpenTelemetry из любого SDK, трейсы доступны через Jaeger UI и Grafana из маркетплейса облака, а изоляция и лимиты действуют на уровне проектов.
Если вы строили мультитенантную трассировку иначе — через Tempo, отдельные инсталляции на тенантов или колонку tenant в собственной схеме, — расскажите в комментариях, какое ограничение проявилось первым.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.