Внутреннее устройство VictoriaLogs: от приема записей до байтов на диске и колоночного сжатия
Сбор и анализ логов в микросервисных архитектурах и кластерах Kubernetes ежедневно генерирует терабайты неструктурированного текста. Традиционные поисковые платформы, такие как Elasticsearch и OpenSearch, опираются на полнотекстовые инвертированные индексы (структуры данных, где для каждого уникального слова хранится список содержащих его документов). Этот подход обеспечивает быстрый произвольный поиск, но требует огромных накладных расходов: размер индексов нередко превышает объем исходных логов, а кластеры требуют сотен гигабайт оперативной памяти.
Альтернативный подход Grafana Loki отказался от индексации текста в пользу индексации только служебных меток (labels), однако при фильтрации по содержимому Loki вынужден линейно сканировать "сырые" блоки данных, создавая высокую нагрузку на процессор и диск.
Система VictoriaLogs от команды VictoriaMetrics предлагает иной инженерный компромисс. Вместо тяжелого инвертированного индекса или сплошного сканирования движок объединяет суточное секционирование, столбцовое хранение данных (columnar storage) и отсев блоков через фильтры Блума.
Концепция потоков: роль низкой кардинальности
Фундаментальной единицей группировки данных в VictoriaLogs выступает лог-поток (stream). Поток объединяет записи с одинаковым набором значений полей, заданных директивой _stream_fields или HTTP-заголовком VL-Stream-Fields при приеме данных (ingestion).
Если в качестве полей потока выбрана связка service, namespace или pod, container, то все сообщения конкретного контейнера физически размещаются на диске рядом. Это дает высокую локальность данных: при расследовании инцидента в микросервисе базе не нужно собирать логи по разрозненным блокам.
Критически важна кардинальность полей (cardinality, число уникальных значений):
- Поля низкой кардинальности: имя сервиса (
app), окружение (env), пространство имен (namespace), имя пода или контейнера. Они меняются редко и формируют стабильные долгоживущие потоки. - Поля высокой кардинальности: идентификаторы пользователей (
user_id), сквозные идентификаторы запросов (trace_id,request_id), IP-адреса клиентов.
Включение полей высокой кардинальности в _stream_fields приводит к фрагментации хранилища: система создает миллионы микропотоков из единичных записей, что расходует память на метаданные и ухудшает сжатие. Идентификаторы запросов должны оставаться обычными полями лога, а не ключами потока.
Физическая структура: от суточных разделов к сегментам
На файловой системе VictoriaLogs сохраняет данные по суточным разделам — партициям (partitions) с именами в формате YYYYMMDD (например, 20260904). Это решает две задачи:
- Сужение области поиска: запросы за последние часы или дни отсекают ненужные сутки до чтения файлов данных.
- Нулевая стоимость ротации (retention): для удаления устаревших логов достаточно удалить каталоги соответствующих суток, без тяжелого построчного удаления и перестроения индексов (vacuum).
Внутри партиции данные упакованы в неизменяемые сегменты (segments) из сжатых блоков. В исследовании реального сегмента 4 240 299 строк логов были сгруппированы в 1 915 блоков: 6,02 ГиБ исходного текста на диске заняли 538 МБ (сжатие более чем в 11 раз). Блок представляет собой единицу чтения от 1 000 до 65 536 строк (до 2–4 МиБ несжатых данных) со своими метаданными: диапазоном времени, потоками и смещениями колонок.
Внутренности сегмента: метаданные, фильтры Блума и шарды
Каталог сегмента содержит бинарные файлы с четким разделением ролей:
metaindex.bin— компактный метаиндекс диапазонов потоков и блоков, постоянно удерживаемый в оперативной памяти (RAM).index.bin— индекс заголовков блоков, связывающий временные рамки и потоки со смещениями данных.columns_header_index.bin— специализированный индекс заголовков колонок, позволяющий быстро находить дескриптор нужного поля без линейного перебора всех атрибутов JSON.values.binNиbloom.binN— файлы шардов данных (гдеN— номер от 0 до 127). В сегменте создается до 128 шардовых пар, между которыми распределяются поля. Файлvalues.binNхранит сжатые значения, аbloom.binN— соответствующие фильтры Блума.message_values.binиmessage_bloom.bin— изолированные файлы для основного текста сообщения (_msg).
Фильтр Блума (Bloom filter) — вероятностная структура данных, проверяющая принадлежность элемента множеству. Главное свойство фильтра — отсутствие ложноотрицательных срабатываний (false negative): если фильтр сообщает, что слова в блоке нет, его там точно нет. Движок полностью пропускает чтение блока с диска. Если фильтр возвращает положительный ответ (возможен небольшой процент ложноположительных срабатываний, false positive), движок считывает точный диапазон байт из values.binN.
Путь поискового запроса: каскадный отсев
При выполнении запроса LogsQL вида _stream:{app="checkout"} AND "payment_failed" система выполняет каскадный отбор:
- Фильтр по метаиндексу в памяти:
metaindex.binотсекает сегменты и блоки, чье время или потоки не пересекаются с запросом. - Чтение релевантных заголовков: из
index.binсчитываются заголовки только прошедших блоков. - Локализация колонок: через
columns_header_index.binопределяются смещения полей_msgиapp. - Проверка фильтра Блума: из
message_bloom.binсчитывается хеш-блок и проверяется токенpayment_failed. Если слова в блоке нет, блок отбрасывается без чтения значений. - Точечное чтение значений: только при подтверждении от фильтра Блума считываются байты из
message_values.binдля окончательного сопоставления.
Благодаря каскаду до 90–99% дискового объема отсекается до выполнения распаковки.
Колоночное хранение и алгоритмы сжатия
В традиционных построчных хранилищах для извлечения одного поля приходится читать всю строку со всеми атрибутами.
В VictoriaLogs реализована столбцовая модель (columnar storage): каждое поле хранится как отдельный массив значений. Если запрос агрегирует коды ответов status по времени, система считывает только колонки status и _time, игнорируя тяжелые стектрейсы из других полей.
Столбцовая раскладка позволяет применять разные алгоритмы компрессии:
- Gorilla compression: для монотонно возрастающих временных меток (
_time) и числовых параметров, устраняя избыточность через дельты соседних значений. - Словарное кодирование (Dictionary encoding): для текстовых полей с частыми повторами (уровни логов
INFO,ERROR, имена хостов). Длинные строки заменяются целочисленными индексами. - Алгоритм ZSTD: для неструктурированного текста (
_msg) и сложных JSON, обеспечивая высокую степень сжатия при быстрой распаковке.
Сопоставление архитектур хранения логов
| Критерий | Elasticsearch / OpenSearch | Grafana Loki | VictoriaLogs |
|---|---|---|---|
| Базовая модель | Инвертированные индексы по всем полям | Индексация только меток (labels) | Метаданные потоков + столбцовые блоки + фильтры Блума |
| Диск | Высокий расход (индексы до 100–150% данных) | Низкий (сжатые блоки без полнотекстовых индексов) | Минимальный (столбцовое сжатие ZSTD и Gorilla) |
| RAM | Очень высокий расход (кэширование индексов) | Умеренный (индексы меток) | Низкий (только компактные метаиндексы в памяти) |
| Узкое место | Рост числа полей и перегрузка маппинга | Высокая кардинальность меток | Высокая кардинальность потоков (_stream_fields) |
| Поиск по тексту | Мгновенный по инвертированному индексу | Медленный (сканирование чанков) | Быстрый отсев через фильтры Блума |
Инженерный чек-лист SRE: проектирование схемы и запросов
Для стабильной и быстрой работы VictoriaLogs соблюдайте базовые правила:
- Стабильные метаданные для потоков: используйте связку
service, env, namespaceдля_stream_fields. Число активных потоков должно измеряться тысячами, а не миллионами. - Изоляция динамических ключей: идентификаторы сессий, пользователей и трассировок передавайте внутри тела документа, не включая их в ключевые поля потока.
- Порядок фильтров в LogsQL: всегда начинайте запрос с ограничения временного диапазона и фильтрации по потоку, а затем добавляйте полнотекстовые условия по
_msg. - Контроль селективности: отслеживайте метрики
vl_blocks_read_totalиvl_rows_read_total. Рост числа прочитанных блоков сигнализирует о неселективных запросах или фрагментации потоков. - Гигиена схемы (schema hygiene): следите за единообразием типов полей между сервисами, чтобы поддерживать максимальную плотность сжатия в блоках.
