RE:NODE

Базы данных10 мин чтения

Память Valkey: maxmemory, политики вытеснения и большие ключи

Расчёт и настройка памяти Valkey: что учитывает maxmemory, как выбрать политику вытеснения, найти большие ключи, использовать компактные кодировки и читать фрагментацию.

0 прочтений

Всё, что хранит Valkey, живёт в памяти, поэтому память - это тот предел, в который вы на самом деле упрётесь. Что произойдёт, когда вы в него упрётесь, решают две настройки: maxmemory, потолок для набора данных, и maxmemory-policy, что делать на потолке. Для чистого кэша задайте потолок и используйте allkeys-lru или allkeys-lfu, и Valkey будет тихо выбрасывать наименее полезные ключи. Для сессий, очередей и всего, что нельзя терять, используйте noeviction: эта политика отклоняет новые записи с ошибкой OOM, а не удаляет данные, - и позаботьтесь о том, чтобы заметить это раньше, чем оно случится. Политики volatile-* находятся посередине и несут одну ловушку: они вытесняют только ключи с TTL, так что если таких нет, они ведут себя точно как noeviction. После выбора политики дело за измерениями: найти ключи, которые занимают память, удерживать небольшие структуры в компактных кодировках и оставлять достаточный запас для той части памяти, которая не является вашими данными.

maxmemory: что он ограничивает, а что нет#

valkey.conf
maxmemory 400mbmaxmemory-policy allkeys-lrumaxmemory-samples 5

maxmemory ограничивает память, занятую набором данных, - ту, что показывается в used_memory. Когда запись должна вывести её за предел, срабатывает политика вытеснения. По умолчанию значение 0, что на 64-битной системе означает отсутствие ограничения, - Valkey будет расти, пока его не остановит операционная система или контейнер, а это куда худший сбой, чем любая политика вытеснения. Задавайте его всегда.

То, что в него не входит, важно не меньше того, что входит:

  • Буферы репликации и AOF не учитываются в лимите, когда решается, вытеснять ли что-то.
  • Буферы клиентов - буферы запросов и выходные буферы для медленных читателей и подписчиков pub/sub - это реальная память, которая живёт вне ваших ключей.
  • Фрагментация - разница между тем, что держит аллокатор, и тем, что используют данные, - вообще вне used_memory.
  • Форк для снимков и перезаписи AOF во время работы может потребовать много дополнительной памяти через copy-on-write, как описано в статье сохранение данных в Valkey.

Поэтому реальный объём процесса, used_memory_rss, всегда больше maxmemory, иногда намного. Обычная отправная точка - maxmemory на уровне 60-80 процентов доступной памяти и ниже, если включено сохранение и частота записи высока. CONFIG SET maxmemory 400mb меняет значение на ходу, если сервер разрешает изменение конфигурации.

Политики вытеснения#

ПолитикаЧто вытесняетПодходит для
noevictionНичего; записи падают с OOMСессии, очереди, данные, которые нельзя терять (по умолчанию)
allkeys-lruКлюч, к которому дольше всего не обращались, среди всех ключейКэш общего назначения
allkeys-lfuКлюч, которым реже всего пользуются, среди всех ключейКэш со стабильным горячим набором
allkeys-randomСлучайный ключРавномерный доступ, редко лучший выбор
volatile-lruДавно не использовавшийся среди ключей с TTLКэш и долговечные данные на одном сервере
volatile-lfuРеже всего используемый среди ключей с TTLТо же, но по частоте
volatile-randomСлучайный ключ с TTLРедко
volatile-ttlКлюч с ближайшим сроком истеченияКогда TTL отражает важность

Как выбирать:

  1. Всё ли на этом сервере одноразовое? Тогда allkeys-lru. Переходите на allkeys-lfu, если небольшой набор ключей читают постоянно, а сканирование или пакетная задача норовят их вытолкнуть, - LFU помнит, что ключ популярен, а LRU помнит лишь, что к нему недавно обращались.
  2. Есть ли на этом сервере что-то не одноразовое? Тогда noeviction и мониторинг памяти, потому что заполненный сервер отклоняет записи.
  3. Смесь? Честный ответ - два сервера. Политики volatile-* позволяют смешанному серверу вытеснять только ключи кэша (у которых есть TTL) и сохранять долговечные (у которых его нет), но они зависят от того, что каждая запись в кэш задаёт TTL - всегда и во всех ветках кода.

Как на самом деле работает вытеснение#

Valkey не хранит точный список ключей, упорядоченный по последнему использованию; это стоило бы памяти на каждом ключе. Вместо этого, когда нужно вытеснить, он делает выборку из нескольких ключей и вытесняет лучшего кандидата среди них, сохраняя между раундами небольшой пул хороших кандидатов. maxmemory-samples 5 - значение по умолчанию; если поднять его до 10, поведение приблизится к настоящему LRU ценой чуть большего расхода процессора на каждое вытеснение. На практике приближение настолько хорошее, что вы этого не заметите.

LFU использует для каждого ключа небольшой логарифмический счётчик, который растёт с обращениями и затухает со временем. lfu-log-factor (по умолчанию 10) управляет тем, как быстро счётчик насыщается, а lfu-decay-time (по умолчанию 1, в минутах) - тем, как быстро он затухает, пока ключ простаивает. OBJECT FREQ key показывает счётчик ключа, когда активна политика LFU; OBJECT IDLETIME key показывает число секунд с последнего обращения при политиках LRU.

Вытеснение происходит, когда записи нужна память, внутри этой записи. Поэтому большой всплеск записей на заполненный сервер выполняет работу по вытеснению на пути записи, и это проявляется как задержка. lazyfree-lazy-eviction yes передаёт само освобождение вытесненных значений фоновому потоку, что помогает, когда вытесняемые значения велики.

Счётчики, за которыми стоит следить в INFO stats, - evicted_keys, который растёт каждый раз, когда политика что-то удаляет, и expired_keys для ключей, удалённых по TTL. Для кэша некоторое вытеснение нормально. Стабильно растущая скорость означает, что рабочий набор больше не помещается, а коэффициент попаданий падает вместе с ним.

Что на самом деле занимает память#

Каждый ключ стоит больше, чем его имя и значение. Есть запись в хеш-таблице пространства ключей, строка ключа, заголовок объекта для значения, запись о сроке жизни, если есть TTL, и округление аллокатора на каждом выделении памяти. Для крошечных значений накладные расходы могут превышать сами данные - миллион ключей с 4-байтовым счётчиком занимают гораздо больше 4 МБ. Valkey 8.1 заменил основную хеш-таблицу более компактной конструкцией, что заметно сократило накладные расходы на ключ, но мелкие ключи всё равно сравнительно дороги.

Крупную экономию дают компактные кодировки. Небольшие агрегаты хранятся одним упакованным блоком, а не полноценной хеш-таблицей или списком с пропусками:

ТипКомпактная кодировкаОстаётся компактным, пока
HashlistpackНе более hash-max-listpack-entries (128) полей, значения до hash-max-listpack-value (64) байт
Sorted setlistpackНе более zset-max-listpack-entries (128) элементов, до 64 байт каждый
Множество целых чиселintsetНе более set-max-intset-entries (512) элементов
Небольшое множествоlistpackНе более set-max-listpack-entries (128) элементов
Listquicklist из listpackРазмер узла задаётся list-max-listpack-size

OBJECT ENCODING key показывает, в какой кодировке находится ключ. Хеш, выросший за пороги, превращается в полноценную хеш-таблицу и обратно уже не возвращается. Два практических следствия:

  • Группируйте мелкие значения в хеши. Один хеш с сотней мелких полей намного дешевле сотни отдельных ключей, потому что делит накладные расходы одного ключа и лежит в listpack. Хранить профиль пользователя как HSET user:42 name ... plan ... лучше, чем SET user:42:name и SET user:42:plan.
  • По возможности держите поля небольшими. Одно поле длиннее 64 байт выталкивает весь хеш из кодировки listpack. Повышение порогов меняет немного процессорного времени (listpack просматривается линейно) на память; значения до нескольких сотен разумны, если ограничением служит память.

Поиск больших ключей#

Один непомерно большой ключ - список, который никто не обрезает, множество всех пользователей, когда-либо входивших в систему, закэшированный блоб целого каталога - часто в одиночку объясняет проблему с памятью. CLI умеет находить такие ключи, не блокируя сервер, потому что обходит пространство ключей через SCAN:

bash
$ valkey-cli -h db.example.net -p 6380 --askpass --bigkeys$ valkey-cli -h db.example.net -p 6380 --askpass --memkeys$ valkey-cli -h db.example.net -p 6380 --askpass MEMORY USAGE session:9f2c41 SAMPLES 0

--bigkeys сообщает самый большой ключ каждого типа по числу элементов; --memkeys - по памяти. MEMORY USAGE даёт число байт для одного ключа, включая накладные расходы; SAMPLES 0 заставляет его измерить каждый элемент агрегата, а не оценивать по пяти.

Большие ключи вредят сильнее, чем можно судить по их размеру. Чтение такого ключа блокирует сервер, пока ответ строится и отправляется. Удаление через DEL блокирует его, пока освобождается память, - используйте UNLINK, который освобождает её в фоне. И большой ключ невозможно вытеснить частично: при нехватке памяти он либо остаётся целиком, либо уходит целиком. Ограничивайте списки через LTRIM, задавайте множествам идентификаторов TTL или схему ротации, а значения размером с каталог разбивайте на ключи по отдельным позициям.

Фрагментация#

INFO memory показывает и то, сколько используют данные, и то, сколько держит процесс:

bash
$ valkey-cli -h db.example.net -p 6380 --askpass INFO memory \    | grep -E '^(used_memory_human|used_memory_rss_human|mem_fragmentation_ratio|maxmemory_human|maxmemory_policy):'

mem_fragmentation_ratio - это RSS, делённый на used_memory. Значения примерно от 1.0 до 1.5 - здоровые. Заметно выше означает, что аллокатор держит память в страницах, использованных лишь частично, - обычно после того, как было удалено или истекло множество ключей разного размера. Ниже 1.0 означает, что часть процесса ушла в swap, а для базы данных, обещающей ответы на скорости памяти, это проблема похуже. На почти пустом сервере коэффициент ничего не значит - несколько мегабайт базового RSS поверх крошечного набора данных дают большое число, за которым ничего не стоит.

Средства, по порядку: MEMORY DOCTOR даёт диагноз простыми словами; activedefrag yes позволяет серверу в фоне перемещать значения в более заполненные страницы, если он собран с jemalloc (по умолчанию в Linux), в пределах, заданных active-defrag-threshold-lower и active-defrag-ignore-bytes; MEMORY PURGE просит аллокатор освободить свободные страницы. Перезапуск возвращает всё, и при включённом сохранении он дёшев, но это грубый инструмент.

Память, которая не является вашими данными#

Оставляйте над maxmemory запас для того, что он не учитывает:

  • Выходные буферы клиентов. Медленный потребитель или подписчик pub/sub, переставший читать, копит ответы в памяти сервера. client-output-buffer-limit их ограничивает - по умолчанию pubsub 32mb 8mb 60, то есть подписчик отключается при 32 МБ или после 60 секунд выше 8 МБ.
  • Copy-on-write во время сохранения. Каждая страница, записанная во время снимка или перезаписи AOF, дублируется ради дочернего процесса.
  • Соединения. Каждый клиент стоит памяти на свои буферы. Тысячи простаивающих соединений из протекающего пула складываются в заметный объём; их показывают CLIENT LIST и INFO clients.
  • Скрипты Lua и движок скриптов - немного, но не ноль.

Что делает операционная система, когда сумма превышает доступное, - а это никогда не то, чего вы хотите, - описано в статье swap и OOM killer в Linux.

Расчёт размера: разобранный пример#

Оценка арифметикой ненадёжна; оценка выборкой проста. Допустим, приложение будет хранить 200 000 сессий. Загрузите несколько тысяч реалистичных сессий на тестовый сервер и измерьте:

sample_size.py
import statistics, redisr = redis.Redis.from_url("redis://:pass@db.example.net:6380/0")sizes = []for key in r.scan_iter(match="session:*", count=1000):    sizes.append(r.memory_usage(key, samples=0))    if len(sizes) >= 2000:        breakprint(f"keys sampled: {len(sizes)}, mean bytes: {statistics.mean(sizes):.0f}")

Если среднее получилось около 600 байт, 200 000 сессиям нужно примерно 120 МБ под набор данных. Добавьте запас на рост и на память, которая не является вашими данными, - при включённом сохранении разумное правило - ещё половина, - и выйдет около 180 МБ, что помещается на сервер с примерно 200 МБ доступной памяти, но почти без запаса. После запуска измерьте снова через INFO memory, потому что настоящие сессии всегда больше тестовых.

О том, как держать значения сессий небольшими - а это самая дешёвая экономия памяти, какая у вас когда-либо будет, - рассказано в статье сессии в Valkey.

Память на сервере Valkey в RE:NODE#

Тарифы Valkey продаются по объёму памяти - 256 МБ, 512 МБ, 1 ГБ, 2 ГБ и 4 ГБ, - и для каждого указан доступный для данных объём, примерно 80 процентов от этого: около 200 МБ, 400 МБ, 800 МБ, 1,6 ГБ и 3,2 ГБ. Рассчитывайте по доступному объёму. Если сам процесс достигает лимита памяти тарифа, платформа останавливает контейнер и чисто перезапускает его, а не даёт ему уйти в swap; с AOF и снимками данные возвращаются с диска, но подключённые клиенты видят короткий перерыв, и сторож сбоев его учитывает. Когда сервер новый, проверьте maxmemory и maxmemory-policy через INFO memory, чтобы знать, какое поведение вы получите при заполнении, и переходите на тариф выше, когда evicted_keys или расход памяти говорят, что рабочий набор из него вырос.

FAQ#

Какая политика вытеснения стоит по умолчанию?

noeviction. Когда достигнут maxmemory, команды записи падают с ошибкой OOM, и ничего не удаляется. Это безопасно для данных, которые нужно сохранить, и бесполезно для кэша, поэтому на сервере кэша явно задайте allkeys-lru или allkeys-lfu.

LRU или LFU для кэша?

LRU - безопасный вариант по умолчанию. LFU лучше, когда небольшой набор ключей используется постоянно, а редкие массовые чтения - отчёт, краулер - иначе вытолкнули бы горячие ключи. Если не можете определить, используйте LRU и после переключения сравните коэффициенты попаданий.

Почему процесс использует больше памяти, чем maxmemory?

Потому что maxmemory ограничивает набор данных, а не процесс. Буферы клиентов, фрагментация и copy-on-write во время снимков и перезаписи AOF - всё это вне его. Оставляйте запас от пятой части до половины лимита в зависимости от того, насколько интенсивно сервер пишет.

Как найти, какие ключи занимают больше всего памяти?

Запустите valkey-cli --memkeys или --bigkeys, которые сканируют пространство ключей без блокировки, и MEMORY USAGE key SAMPLES 0 для подозреваемых. В первую очередь ищите неограниченные списки и множества; обычно виноваты они.

Вернёт ли Valkey память после удаления ключей?

Частично. Освобождённая память сразу используется для новых данных, но аллокатор не всегда возвращает её операционной системе, поэтому RSS может оставаться высоким, а коэффициент фрагментации растёт. Помогают activedefrag и MEMORY PURGE; перезапуск с включённым сохранением сбрасывает всё полностью.


Комментарии

Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.

0/2000