Learning Platform
Глоссарий Troubleshooting
Урок 17.01 · 40 мин
Продвинутый
NimbleMetaVeloxFlatBuffersStripeBlock EncodingStream EncodingWide SchemaAI/ML TablesEncoding Pipeline

Nimble Architecture

Parquet проектировался в 2013 году для аналитических workload’ов: десятки колонок, SQL-запросы, Hadoop MapReduce. Внутри Meta масштабы другие: AI/ML training таблицы с 10 000+ колонок (features, embeddings, метрики), где Parquet’s Thrift-based metadata становится узким горлом, а фиксированный набор кодировок ограничивает эффективность для новых типов данных.

Nimble — это C++ формат хранения, разработанный Meta (facebookincubator/nimble) как ответ на эти ограничения. Открыт в 2024 году (до этого — внутренний проект «Alpha»). Тесно интегрирован с Velox — execution engine Meta, используемым в Presto, Spark и внутренних ML pipeline’ах.

WARNING

Nimble находится на ранней стадии. Нет Python-биндингов, нет standalone-библиотеки — требуется сборка с Velox (gtest, glog, folly, abseil). Это не формат для самостоятельного использования, а компонент экосистемы Meta. Ценность для курса — архитектурные решения и философия дизайна.

NOTE

В Модуле 15 мы разобрали Lance и Vortex — два Rust-based формата с Arrow-native архитектурой. Nimble идёт другим путём: C++, привязка к Velox, философия «одна библиотека — один формат». В уроке 05 мы сравним все четыре подхода.

Мотивация: широкие таблицы Meta

Meta обрабатывает таблицы, структура которых радикально отличается от типичных OLAP-датасетов:

Структура таблиц: OLAP vs Meta AI/ML

OLAP-таблица

Типичная аналитическая таблица: 10-100 колонок, строковые/числовые типы, SQL-запросы с GROUP BY и WHERE. Metadata — десятки записей. Parquet справляется идеально.
Характеристики10-100 колонок: user_id, timestamp, region, revenue. Thrift metadata (Parquet): ~100 entries, парсинг <1ms. Кодировки: PLAIN, DICTIONARY, RLE, DELTA — достаточно для стандартных типов.

Meta AI/ML таблица

AI/ML training таблица Meta: 10 000+ колонок — feature vectors, embeddings, модельные предсказания, AB-test метрики. Каждый эксперимент добавляет колонки, а старые не удаляются.
Характеристики10 000+ колонок: feature_001...feature_9999, embedding_768_dims, prediction_scores. Thrift metadata (Parquet): 10K+ entries → парсинг 50-200ms на stripe → bottleneck при scan. Нужны кастомные кодировки для novel data types.

Конкретные проблемы Parquet для таблиц Meta:

  1. Metadata bottleneck. Parquet использует Apache Thrift для сериализации метаданных. Thrift требует полного парсинга всего сообщения — нельзя прочитать metadata для одной колонки из 10 000 без десериализации всего блока. При 10K+ колонок это 50-200ms на row group только на metadata.

  2. Фиксированные кодировки. Parquet поддерживает ~6 кодировок (PLAIN, DICTIONARY, RLE, DELTA_BINARY_PACKED, BYTE_STREAM_SPLIT, DELTA_LENGTH_BYTE_ARRAY). Добавление новой кодировки требует обновления спецификации, всех реализаций (Java, C++, Rust), совместимости. Meta хочет добавлять кодировки быстро.

  3. Непредсказуемое потребление памяти. Parquet декодирует данные потоково (stream) — объём памяти зависит от размера page и паттерна кодирования. Для scheduler’а ML-pipeline, который должен разместить N декодеров на GPU/CPU cores, непредсказуемость — проблема.

Структура файла Nimble

Nimble организует данные в stripes (аналог row groups в Parquet), но с принципиально другой организацией внутри:

Структура файла Nimble
Nimble FileФайл Nimble: последовательность stripes + footer. Footer содержит метаданные всех stripes для навигации. Stripe = единица параллельного декодирования — один stripe может обрабатываться одним ядром независимо.

Footer размещён в конце файла — это позволяет writer’у записывать данные последовательно и финализировать метаданные одним write. Reader сначала читает footer (seek to end → read size → read footer), затем навигирует к нужным stripes.

Stripes, Streams и Blocks

Внутри каждого stripe данные организованы в streams. Каждая колонка представлена одним или несколькими streams (для nested типов — дополнительные streams для definition/repetition levels):

Внутренняя структура Stripe

Stripe

Stripe — единица параллельного декодирования. Содержит подмножество строк для всех колонок. Stripe footer хранит offset'ы streams для O(1) навигации к конкретной колонке.
Stream: column_AStream — последовательность данных одной колонки внутри stripe. Разбит на blocks фиксированного размера. Каждый block декодируется атомарно — reader знает точный объём памяти до начала декодирования.
Stream: column_BКаждая колонка — отдельный stream. Streams независимы: reader может декодировать только нужные колонки, пропуская остальные. Для широких таблиц (10K колонок) это критично — читать 50 из 10 000.

Stripe Footer (FlatBuffers)

Stripe Footer: FlatBuffers metadata для данного stripe. Содержит offset и размер каждого stream, encoding информацию, statistics (min/max/null count) для каждой колонки. Позволяет skip ненужных streams без чтения.

Ключевое отличие от Parquet: блочная организация вместо страничной. В Parquet данные в page — это stream (произвольной длины), декодирование которого может потребовать непредсказуемый объём памяти. В Nimble block — это атомарная единица с известным размером decoded output.

FlatBuffers vs Thrift: доступ к метаданным

Выбор формата метаданных — одно из ключевых архитектурных решений Nimble:

Парсинг метаданных: Thrift (Parquet) vs FlatBuffers (Nimble)

Thrift (Parquet)

Apache Thrift: формат сериализации Meta (изначально разработан в Facebook). Используется в Parquet для file metadata, row group metadata, page headers. Основной недостаток: требует полной десериализации.
Парсинг 10K колонокДля чтения metadata одной колонки из 10K: десериализовать весь Thrift message → построить in-memory структуру → найти нужную колонку. Объём: ~10K × (name + type + encoding + statistics) = мегабайты Thrift данных.

FlatBuffers (Nimble)

Google FlatBuffers: zero-copy сериализация. Данные читаются прямо из буфера без десериализации — доступ по offset'ам. Размер overhead: ~4 bytes per field (vtable offset). Используется в Nimble для всех метаданных.
Парсинг 10K колонокДля чтения metadata одной колонки: прыгнуть по offset в FlatBuffer → прочитать поля нужной колонки. Не нужно десериализовать остальные 9999 колонок. O(1) random access к metadata любой колонки.
NOTE

FlatBuffers изобретены в Google для игровой индустрии (низкая latency), но идеально подходят для wide-schema метаданных. Nimble — не единственный формат с FlatBuffers: Vortex (Модуль 15) и F3 (урок 03) тоже используют FlatBuffers. Это тренд нового поколения форматов — уход от Thrift/Protobuf к zero-copy сериализации.

Block Encoding vs Stream Encoding

Главное архитектурное решение Nimble — блочное кодирование вместо потокового. Это напрямую влияет на предсказуемость потребления памяти:

Block Encoding (Nimble) vs Stream Encoding (Parquet)

Stream Encoding (Parquet)

Stream Encoding (Parquet): данные в page кодируются как непрерывный поток. Декодер читает byte за byte, пока не восстановит все значения. Объём decoded output зависит от данных — dictionary size, run lengths, delta magnitudes.
ДекодированиеПроцесс: начать читать поток → декодировать значения → объём памяти растёт по мере чтения → завершить когда page закончилась. Проблема: scheduler не знает заранее, сколько памяти потребуется для одного page.

Непредсказуемый memory footprint

Результат: scheduler'у ML-pipeline приходится перевыделять память (overallocate) или рисковать OOM. Для GPU kernels, где memory budget фиксирован, непредсказуемость критична.

Block Encoding (Nimble)

Block Encoding (Nimble): данные разбиты на blocks фиксированного размера. Каждый block header содержит: encoding type, encoded size, decoded size, value count. Декодер знает объём памяти ДО начала декодирования.
ДекодированиеПроцесс: прочитать block header → выделить точный объём памяти → декодировать. Один block = одна предсказуемая аллокация. Scheduler может точно спланировать: 'этот block = 64KB decoded → поместится в L2 cache'.

Предсказуемый memory footprint

Результат: scheduler точно знает memory budget каждого block. Можно распределить N blocks на M cores с гарантией отсутствия OOM. Идеально для GPU kernel scheduling.

Предсказуемость памяти — не теоретическое преимущество. В инфраструктуре Meta, где тысячи ML-training jobs конкурируют за GPU/CPU ресурсы, scheduler должен точно знать memory requirements каждого task. Block encoding делает decoding планируемым — как CPU instruction с известным latency.

Encoding Pipeline

Nimble использует рекурсивный, композируемый encoding pipeline. В отличие от Parquet, где кодировка выбирается на уровне page и применяется целиком, Nimble составляет цепочки кодировок:

Encoding Pipeline Nimble

Raw Column Data

Raw данные: column values перед кодированием. Pipeline анализирует данные и строит дерево кодировок — от высокоуровневых (structural) к низкоуровневым (compression).
Level 1: Structural EncodingПервый уровень: структурные кодировки, определяющие как данные организованы. Nullable → split на values + null bitmap. Dictionary → replace values с indices + dictionary. Constant → один value + count.
Level 2: Value EncodingВторой уровень: кодирование значений. RLE → runs of identical values. Delta → differences between consecutive values. Varint → variable-length integer encoding. Каждый sub-stream может иметь свою кодировку.
Level 3: CompressionТретий уровень: byte-level compression. ZSTD, LZ4, Snappy на закодированных данных. Применяется поблочно — каждый block сжимается независимо.

Encoded Block

Encoded Block: результат трёхуровневого pipeline. Metadata блока хранит полное дерево кодировок для decode: 'Nullable → Dictionary → RLE → ZSTD'. Декодирование — обратный обход дерева.

Рекурсивность означает, что каждый уровень может создавать sub-streams, которые проходят следующие уровни. Пример: Nullable(Dictionary(Delta(ZSTD(data)))) — nullable колонка с dictionary encoding, где indices закодированы delta + сжаты ZSTD.

TIP

Сравните с подходами других форматов: Parquet — фиксированный pipeline (encoding → compression, один уровень каждого). Vortex (Модуль 15) — cascading encodings с BtrBlocks-style selection. Nimble — рекурсивное дерево с произвольной глубиной. F3 (урок 03) — embedded Wasm decoders.

Интеграция с Velox

Nimble не существует как standalone библиотека — он тесно интегрирован с Velox, execution engine Meta:

Nimble в экосистеме Velox

Presto

Presto: distributed SQL engine Meta. Использует Velox как execution backend. Nimble — один из форматов чтения/записи через Velox connectors.

Spark (Gluten)

Spark: Apache Spark с Velox plugin (Gluten). Nimble файлы читаются через тот же Velox connector.

ML Pipelines

ML Pipelines: внутренние Meta training pipelines. Nimble оптимизирован для wide-table reads при feature extraction.

Velox Engine

Velox: C++ vectorized execution engine Meta. Columnar in-memory format (Velox Vectors). Connectors для разных storage formats: Parquet, ORC, Nimble. Nimble — нативный формат Velox, без conversion overhead.
ConnectorVelox connector для Nimble: direct read в Velox Vectors. Нет промежуточной конвертации (Nimble → Arrow → Velox). Блоки Nimble → Velox column batches. Zero-copy где возможно.
ConnectorVelox connectors для Parquet и ORC: требуют конвертацию из формата в Velox Vectors. Дополнительный overhead при декодировании.

HDFS / S3 / Local FS

Storage: HDFS, S3, локальная файловая система. Nimble файлы — обычные файлы, без специальных требований к storage layer.

Tight coupling с Velox даёт преимущество в performance (нет conversion layer), но создаёт серьёзное ограничение: без Velox нельзя читать Nimble файлы. Нет standalone decoder, нет Python bindings, нет Java/Rust реализации.

Сравнение с Parquet: структура метаданных

Metadata Access: Parquet vs Nimble (10K колонок)

Stripe Layout: потоки данных

Каждый stripe содержит параллельные streams для каждой колонки. Для nested типов (struct, list, map) создаётся иерархия streams:

Streams внутри Stripe для nested типов
Root: struct<user_id: int64, features: list<float32>, metadata: map<string, string>>Корневой тип: struct с полями разных типов. Каждое поле — отдельный stream. Вложенные структуры создают под-streams. Leaf streams содержат actual data blocks.
Stream 0: user_idLeaf stream: int64 values. Block encoding: Delta + Varint + ZSTD. Декодирование: ZSTD → Varint → Delta → raw int64 values.
Stream 1: features (list)Composite stream для list type. Два sub-streams: offsets (длина каждого list) и values (все элементы подряд). Offsets stream: lengths encoding (run-length если lists одинаковой длины).
Stream 2: metadata (map)Composite stream для map type. Три sub-streams: offsets (количество пар в каждом map), keys (string stream), values (string stream).

Иерархия streams зеркалирует иерархию типов — в отличие от Parquet’s Dremel encoding, где nested types «размазываются» в плоские колонки с repetition/definition levels. Подход Nimble более прямолинейный: каждый уровень вложенности = отдельный stream. Это упрощает декодирование и делает его параллелизуемым.

Параллельное декодирование

Блочная архитектура Nimble обеспечивает предсказуемый параллелизм:

Параллельное декодирование Nimble

Velox Scheduler

Scheduler (Velox): получает запрос на N колонок из stripe. Читает stripe footer (FlatBuffers) → знает offset, size, decoded size каждого block в каждом stream. Может точно спланировать параллельное декодирование.
ПланированиеШаг 1: прочитать stripe footer → для каждого запрошенного stream: список blocks с known decoded size. Шаг 2: распределить blocks по cores. Шаг 3: pre-allocate буферы точного размера. Нет guessing, нет overallocation.

Core 0: stream A, blocks 0-3

Core 0: декодирует blocks 0-3 stream column_A. Memory budget: sum(decoded_size[0..3]). Известен заранее из stripe footer.

Core 1: stream B, blocks 0-3

Core 1: декодирует blocks 0-3 stream column_B параллельно. Нет зависимостей от Core 0. Полная изоляция.

Core 2: stream A, blocks 4-7

Core 2: декодирует blocks 4-7 stream column_A. Один stream может быть разбит на несколько cores — blocks независимы.

Velox Vectors (decoded)

Velox Vectors: decoded колонки в in-memory columnar формате Velox. Готовы для vectorized execution (filter, aggregate, join). Нет дополнительной конвертации.
TIP

Ключевое отличие от Parquet: в Parquet scheduler не может точно узнать decoded size page до начала декодирования (зависит от dictionary size, RLE runs, null count). В Nimble block header содержит decoded_size — scheduler знает memory budget до первого байта декодирования.

Сравнение с форматами курса

Nimble в контексте форматов нового поколения

Итоги

Nimble — это целенаправленный ответ на конкретные проблемы Meta:

  1. FlatBuffers metadata решает bottleneck Thrift при 10K+ колонках: O(1) random access вместо O(N) десериализации.

  2. Block encoding обеспечивает предсказуемое потребление памяти: decoded size известен до начала декодирования. Scheduler точно планирует параллельное декодирование на N cores.

  3. Рекурсивный encoding pipeline позволяет комбинировать кодировки произвольной глубины без изменения спецификации.

  4. Tight coupling с Velox даёт performance (нет conversion layer), но ограничивает экосистему.

В следующем уроке мы разберём философию дизайна Nimble — подход «библиотека как спецификация», почему Meta осознанно отказывается от multi-implementation модели Parquet, и какие уроки из фрагментации Parquet-экосистемы привели к такому решению.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Meta-инженер работает с AI/ML training таблицей: 12 000 колонок (features, embeddings, метрики). Запрос: SELECT feature_42, feature_7891, embedding_768 FROM table WHERE metric_100 > threshold. При чтении из Parquet — 150ms уходит только на парсинг metadata row group. Почему Nimble решает эту проблему?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5