Сервис развивается: тестируем формат, собираем идеи, улучшаем сервис. Есть идеи? Написать
Войти
Дайджесты новостей
Серверная дисковая подсистема VictoriaLogs с суточными разделами, колоночными файлами и фильтрами Блума в пиксель-арт стиле

Внутреннее устройство VictoriaLogs: от приема записей до байтов на диске и колоночного сжатия

Детальный разбор архитектуры хранения логов в VictoriaLogs: почему колоночный формат и суточные разделы обходятся без тяжелых полнотекстовых индексов, как фильтры Блума отсекают блоки данных без чтения с диска и какие правила формирования потоков защищают систему от деградации памяти.

Внутреннее устройство 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). Это решает две задачи:

  1. Сужение области поиска: запросы за последние часы или дни отсекают ненужные сутки до чтения файлов данных.
  2. Нулевая стоимость ротации (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" система выполняет каскадный отбор:

  1. Фильтр по метаиндексу в памяти: metaindex.bin отсекает сегменты и блоки, чье время или потоки не пересекаются с запросом.
  2. Чтение релевантных заголовков: из index.bin считываются заголовки только прошедших блоков.
  3. Локализация колонок: через columns_header_index.bin определяются смещения полей _msg и app.
  4. Проверка фильтра Блума: из message_bloom.bin считывается хеш-блок и проверяется токен payment_failed. Если слова в блоке нет, блок отбрасывается без чтения значений.
  5. Точечное чтение значений: только при подтверждении от фильтра Блума считываются байты из message_values.bin для окончательного сопоставления.

Благодаря каскаду до 90–99% дискового объема отсекается до выполнения распаковки.

Колоночное хранение и алгоритмы сжатия

В традиционных построчных хранилищах для извлечения одного поля приходится читать всю строку со всеми атрибутами.

В VictoriaLogs реализована столбцовая модель (columnar storage): каждое поле хранится как отдельный массив значений. Если запрос агрегирует коды ответов status по времени, система считывает только колонки status и _time, игнорируя тяжелые стектрейсы из других полей.

Столбцовая раскладка позволяет применять разные алгоритмы компрессии:

  • Gorilla compression: для монотонно возрастающих временных меток (_time) и числовых параметров, устраняя избыточность через дельты соседних значений.
  • Словарное кодирование (Dictionary encoding): для текстовых полей с частыми повторами (уровни логов INFO, ERROR, имена хостов). Длинные строки заменяются целочисленными индексами.
  • Алгоритм ZSTD: для неструктурированного текста (_msg) и сложных JSON, обеспечивая высокую степень сжатия при быстрой распаковке.

Сопоставление архитектур хранения логов

КритерийElasticsearch / OpenSearchGrafana LokiVictoriaLogs
Базовая модельИнвертированные индексы по всем полямИндексация только меток (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): следите за единообразием типов полей между сервисами, чтобы поддерживать максимальную плотность сжатия в блоках.