EnglishРусский Map

Тайная жизнь данных в Valkey

title
Тайная жизнь данных в Valkey
type
summary
summary
Кайл Дэвис о скрытом слое кодирования в Valkey, где превышение порога на один байт увеличивает расход памяти на 77%
tags
valkey, redis, databases, memory, cost-optimization
created
2026-07-29
updated
2026-07-29
lang
ru
source_updated
2026-07-29
translated
2026-09-01
translator
lllm/antigravity/gemini-3.7-flash-medium

Кайл Дэвис в блоге Valkey (21 июля 2026 года) пишет о слое между моделью данных, с которой работает программист, и тем, что реально хранится в RAM. Valkey предоставляет строки, хэши, списки, множества, упорядоченные множества и потоки с отдельным набором команд для каждого типа. Под капотом каждый ключ имеет encoding (кодировку данных), которую выбирает сервер, и этот выбор напрямую влияет на расходы.

Один байт

Три хэша, значения которых отличаются ровно на один символ:

> HSET hash0 field "xxxx…"   # 63-byte value
> HSET hash1 field "xxxx…"   # 64-byte value
> HSET hash2 field "xxxx…"   # 65-byte value

MEMORY USAGE возвращает 104, 120 и 212 байт. Переход от 63 к 64 байтам стоит 15.38%; переход от 64 к 65 байтам стоит 76.67%. OBJECT ENCODING объясняет причину: hash0 и hash1 - это listpack, а hash2 - hashtable.

Listpack хранит элементы последовательно в одном непрерывном блоке памяти, экономя на накладных расходах указателей, которые требуются хэш-таблице. Это не та хэш-таблица, которую можно ожидать по названию команды, и абстракция работает в обе стороны: LPUSH list1 foo создаёт список с кодировкой listpack, тогда как SADD с элементом в 65 байт создаёт множество с кодировкой hashtable. Дэвис формулирует это так: классическая структура данных - это модель, а не способ хранения: "Принято считать, что Valkey сохраняет данные с помощью классических структур, таких как хэш-таблицы, связные списки и множества, в зависимости от используемых команд. Оказывается, нет."

Скачок на 15.38% между 63 и 64 байтами, где encoding не изменился, - это отдельный эффект. Накладные расходы listpack'а растут ступенчато, а не линейно: при том же шаблоне ключа и имени поля значения длиной от 49 до 63 байт занимают 104 байта, а от 64 до 79 - 120 байт. Поэтому значения длиной в 64 и 65 байт в таблицах затрат далее по тексту статьи дают 120 байт, хотя лишь одно из них остаётся listpack'ом.

Пороговые значения

Для строк используется захардкоженная логика. Все остальные типы выбирают кодировку на основе пороговых значений конфигурации: на уровне порога или ниже - одна кодировка, выше - другая. Значения по умолчанию для хэшей в Valkey 9.1:

hash-max-listpack-entries 512
hash-max-listpack-value 64

Хэш остаётся listpack'ом, пока каждое значение не превышает 64 байт, а число пар "поле-значение" меньше 512. Пересечение любой из этих границ приводит к конвертации всего ключа в hashtable. Аналогичные параметры для других типов: list-max-listpack-size, set-max-intset-entries / set-max-listpack-entries / set-max-listpack-value, а также zset-max-listpack-entries / zset-max-listpack-value. У потоков есть кодировки, но нет порогов, поскольку вариант кодировки всего один. Для типов из модулей правила определяет сам модуль.

Арифметика затрат

Именно на это и направлена статья. Если размеры значений в ваших хэшах кучкуются чуть выше 64 байт, вы платите по тарифу hashtable за данные, которые уместились бы в listpack при чуть большем пороге, и экономия масштабируется на весь парк серверов.

Заведомо экстремальный пример Дэвиса: кластер на 100 GB из пяти primary-узлов по 20 GB, где 95 GB ключей превышают порог ровно на один байт и занимают по 212 байт каждый. Увеличение hash-max-listpack-value снижает их размер до 120 байт (56.6% от исходного), и кластер сокращается до 58.8 GB - три primary-узла вместо пяти, плюс экономия на всех replica'х, привязанных к двум убранным узлам.

Более умеренный сценарий практичнее. Когда порог превышают 60% ключей, та же оптимизация уменьшает кластер со 100 GB до 74 GB. Это по-прежнему пять primary-узлов, но каждый держит 14.8 GB вместо 20, что укладывается в 16 GB instance. Суть здесь не в процентах, а в тарифных ступеньках: как в облаке, так и для собственного железа цены привязаны к градациям размеров, и выгода заключается в переходе на ступень ниже той, за которую вы едва переваливали. Если же размер instance'а изменить нельзя, освободившуюся память можно пустить на дополнительное кэширование, снижение частоты вытеснения данных (eviction) или увеличение TTL.

Метод поиска кандидатов лишён лоска и не требует специальных инструментов: взять выборку ключей через OBJECT ENCODING, найти те, что используют дорогую кодировку, выяснить, какой порог заставил их туда перейти, и принять решение. Дэвис делает оговорку: пороговые значения - это не бесплатная оптимизация. Кодировка listpack компактна за счёт последовательного хранения элементов, поэтому бездумное завышение чисел "может привести к субоптимальной производительности или неэффективности". В своём примере он предусмотрительно оговаривает невысокие требования к пропускной способности при значениях в 65 байт.

Контекст

В redis-cost-of-ambition Чарльз Лейфер утверждает, что Valkey победил благодаря ставке на "неприметную работу - многопоточную производительность, эффективность по памяти, надёжность кластера и пропускную способность" вместо погони за новыми пунктами в списке возможностей. Эта статья иллюстрирует тот же тезис: никакого нового типа данных, никакой новой подсистемы, просто разбор существующей ручки настройки и количества instance'ов, которое она позволяет вычеркнуть из счёта. Механизм кодировок здесь унаследован, а не изобретён заново: Valkey - это форк Redis, и src/listpack.c достался ему по наследству. Это делает статью наглядной иллюстрацией главного довода Лейфера: дело не в том, что оригинальный дизайн antirez был ошибочным, а в том, что горстки продуманных примитивов уже было достаточно, и вся оставшаяся работа сводилась к тому, насколько эффективно они работают.

Подобный подход встречается во многих движках хранения: компактное представление для мелких объектов и перегруженное указателями представление при превышении порогового размера, причём сам порог никто не настраивает. В lsm-tree и lsm-trees-nosql описан дисковый вариант того же компромисса, где write amplification и read amplification находятся на противоположных концах ручки настройки, которая тоже поставляется со значением по умолчанию, и мало кто его меняет.