Компрессия на практике: тюнинг и измерение
В предыдущем уроке мы разобрали внутренности LZ77, Huffman и ANS — теперь понимаем почему Zstd сжимает лучше Snappy. Но знание алгоритмов — только половина. Вторая половина — как настроить компрессию для ваших данных: размер блока, выбор алгоритма per-column, методология измерения, и Zstd dictionary training.
Размер блока: ratio vs random access
Компрессия применяется к блокам (page в Parquet, chunk в ORC). Размер блока — фундаментальный trade-off:
- Больше блок → больше контекст для LZ77 → больше повторов → лучше compression ratio
- Меньше блок → меньше данных для декомпрессии при random access → быстрее point queries
Правило большого пальца: Parquet page size = 1 MB — правильный выбор для 90% случаев. Увеличивать до 4–8 MB имеет смысл только для archival (read-never) данных. Уменьшать до 64–256 KB — только если point queries доминируют (тогда, возможно, Parquet — не лучший формат).
Зависимость от данных
Кривая “block size → ratio” не универсальна. Она зависит от entropy данных после encoding:
Column-level выбор компрессии
Разные колонки одного файла могут использовать разные алгоритмы компрессии. На уровне файла задаётся default, на уровне колонки — override:
Spark и Polars не поддерживают per-column compression — только file-level. Для per-column control используйте PyArrow напрямую: pq.write_table() принимает dict с compression per column name. Это полезно для файлов со смешанными колонками: ZSTD для больших строк, LZ4 для уже хорошо закодированных integers.
Методология бенчмаркинга
Типичная ошибка: измерить compression ratio, объявить “Zstd лучше” и остановиться. Правильный бенчмарк включает три метрики и учитывает пять pitfalls:
Пять pitfalls бенчмаркинга
Шаблон бенчмарка
- Данные: production sample ≥ 1 GB, разные column types
- Алгоритмы: Snappy, LZ4, Zstd-1, Zstd-3, Zstd-9
- 3 метрики × 5 прогонов × drop caches
- Отделить encoding от compression per-column-type
- Результат: best algorithm per column type per workload
Zstd Dictionary Training
Стандартная компрессия работает плохо на маленьких данных (< 4 KB): LZ77 не находит повторов в коротких блоках, ANS таблицы — значительный overhead. Решение: обучить словарь на sample данных и использовать его при компрессии.
Проблема: короткие записи (< 4 KB) → ratio ~1.2x
Проблема: короткие записи (JSON events 200–500 bytes, log lines 100–300 bytes, Kafka messages 50–2000 bytes). Стандартный Zstd: ratio ~1.2x (почти нет сжатия). Причина: LZ77 окно пустое, ANS overhead > savings.Результат: ratio 1.2x → 3–5x на записях < 4 KB
Результат: ratio на коротких записях вырастает с ~1.2x до 3–5x. Compress/decompress speed не меняется (словарь = предзаполненное состояние, не дополнительная работа). Ограничение: один словарь per data type. Нельзя сжимать JSON и CSV одним словарём.Когда dictionary полезен
Dictionary ≠ silver bullet. Он помогает только на данных < 4–16 KB. На больших блоках (Parquet pages, ORC stripes) — бесполезен. И: словарь нужно хранить вместе с данными или в отдельном registry. Если словарь потерян — данные не распакуются. Версионирование словарей — обязательно.
Compression ratio vs query latency: кривая trade-off
Финальный практический вопрос: как compression level влияет на end-to-end query latency? Сжатие экономит I/O (меньше данных с диска), но тратит CPU (decompress). Существует оптимальная точка:
Оптимальный уровень для queries = тот, где дополнительная I/O экономия ≈ дополнительному CPU cost. На типичных SSD/HDD (200–500 MB/s) это Zstd-3 (default). На NVMe (3+ GB/s) — Zstd-1 или LZ4: диск быстрее CPU, компрессия сверху минимальна. На S3 (~100 MB/s): Zstd-9 — медленный network оправдывает сильное сжатие.
Ключевые выводы
- Размер блока = ratio vs random access. Parquet 1 MB — правильный default. Увеличивать только для archival, уменьшать — для point lookups.
- Column-level compression: Spark/Polars — только file-level. PyArrow — per-column dict. Используйте PyArrow для mixed workloads.
- Бенчмаркинг: три метрики (ratio, compress speed, decompress speed) × пять pitfalls (cache, skew, column bias, encoding conflation, throughput ≠ latency).
- Zstd dictionary: обучить словарь на samples → ratio на коротких записях (< 4 KB) вырастает с ~1.1x до 3–5x. Бесполезен для больших блоков (> 16 KB).
- Compression level vs query latency: optimal point = Zstd-3 на SSD, Zstd-1/LZ4 на NVMe, Zstd-9 на S3. После optimal point — diminishing returns (CPU cost > I/O gain).
- Encoding снижает ценность compression: на хорошо закодированных данных разница между алгоритмами минимальна — encoding уже сделал основную работу.