У хранилища сессий одна задача: позволить любому процессу, который обслуживает пользователя, быстро найти его сессию и забыть её, когда она истечёт. Как только вы запускаете второй процесс приложения, второй контейнер или deploy, заменяющий первый, сессии внутри процесса перестают справляться с этой задачей - пользователь входит на одном worker и оказывается анонимом на следующем. Valkey решает это почти без кода: у каждого фреймворка из этой статьи есть бэкенд сессий, говорящий на протоколе Redis, а Valkey говорит на нём без изменений.
Конфигурация - короткая часть. Будет ли всё работать в продакшене, решают четыре настройки, которые обычно пропускают: префикс ключей, чтобы два приложения не делили сессии, TTL, совпадающий со сроком жизни cookie, политика вытеснения, которая не выбросит вошедших пользователей, и сохранение данных, чтобы перезапуск не разлогинил всех. В этой статье - рабочая настройка для Express, Django, Laravel, чистого PHP и ASP.NET Core, а затем эти четыре настройки.
Почему сессиям место в общем хранилище#
Сессия может жить в трёх местах, и каждое где-то уместно.
| Где | Работает между процессами | Переживает перезапуск | Подходит для |
|---|---|---|---|
| Память процесса | Нет | Нет | Один процесс, разработка |
| Подписанная или зашифрованная cookie | Да | Да | Маленькие сессии: id пользователя и пара флагов |
| Таблица в базе данных | Да | Да | Небольшой трафик, сессии, которые нужно аудировать |
| Valkey | Да | Да, если включено сохранение | Большинство веб-приложений с несколькими процессами |
Вариант с cookie недооценён. Если вся сессия - это id пользователя и CSRF-токен, подписанной cookie вообще не нужно состояние на сервере. Идея перестаёт быть хорошей, когда сессия растёт: размер cookie ограничен примерно 4 КБ, она передаётся с каждым запросом, включая запросы картинок, и её нельзя отозвать на стороне сервера. «Выйти на всех устройствах» с чистыми cookie-сессиями невозможно, потому что на сервере нечего удалять.
Таблица в базе данных работает, и при нескольких сотнях активных пользователей это нормально. Цена - запись при каждом запросе, который трогает сессию, и задача очистки просроченных строк. Valkey делает то же самое со встроенным сроком жизни: каждая сессия - один ключ с TTL, и сервер удаляет его, когда TTL истекает. Никакой задачи сборки мусора, никакой таблицы, разрастающейся в фоне.
Взамен вы получаете зависимость. Если Valkey недоступен, никто не может войти. Для большинства приложений это правильный обмен, потому что Valkey - едва ли не самая простая служба на свете, но знать об этом стоит, и именно поэтому важны таймауты подключения, о которых пойдёт речь дальше.
Параметры подключения, нужные каждому фреймворку#
Каждому примеру ниже нужны одни и те же четыре значения: хост, порт, пароль и, при желании, номер базы. Большинство библиотек принимают их одной строкой URL:
redis://:a-long-generated-password@203.0.113.20:6380/0redis://default:a-long-generated-password@203.0.113.20:6380/0Схема остаётся redis:// - клиентские библиотеки не знают и не интересуются тем, что сервер - это Valkey. Пустое имя пользователя перед двоеточием означает «пользователь по умолчанию»: именно так ожидает аутентификации сервер, защищённый одним только requirepass. Явно написанный default делает то же самое в клиентах, поддерживающих имена пользователей ACL. Число в конце выбирает логическую базу; 0 - значение по умолчанию.
Положите этот URL в переменную окружения, но никогда не в репозиторий. Где ему место на каждом типе хостинга, описано в статье переменные окружения и секреты. Прежде чем подключать к нему фреймворк, убедитесь, что учётные данные работают с той машины, которая будет ими пользоваться:
$ valkey-cli -h 203.0.113.20 -p 6380 --askpassPlease input password: ****203.0.113.20:6380> PINGPONGredis-cli работает точно так же, если в вашей системе есть именно он. Если PING здесь не проходит, никакая настройка фреймворка этого не исправит - в статье начало работы с Valkey разобраны ошибки подключения и что означает каждая.
В RE:NODE тариф Valkey выдаётся с хостом, портом и паролем, сгенерированным для этого сервера, и сохраняет данные на диск через AOF и снимки, так что перезапуск его не опустошает. На тарифах баз данных нет слота прокси: ваше приложение подключается напрямую к хосту и порту, а это именно то, что нужно библиотеке сессий.
Express и Node.js#
express-session занимается cookie и объектом сессии; connect-redis - это хранилище. Текущие версии connect-redis экспортируют именованный класс RedisStore и ожидают подключённый клиент из пакета redis (node-redis).
$ npm install express-session connect-redis redisimport session from "express-session";import { RedisStore } from "connect-redis";import { createClient } from "redis";const client = createClient({ url: process.env.VALKEY_URL });client.on("error", (err) => console.error("valkey", err));await client.connect();export const sessions = session({ store: new RedisStore({ client, prefix: "shop:sess:" }), secret: process.env.SESSION_SECRET, name: "sid", resave: false, saveUninitialized: false, cookie: { httpOnly: true, secure: true, sameSite: "lax", maxAge: 1000 * 60 * 60 * 24 * 7 },});Каждая строка делает что-то конкретное:
- `prefix` выделяет ключам пространство имён. Без него каждое приложение, направленное на один и тот же Valkey, использует
sess:, и они читают сессии друг друга. Дайте каждому приложению свой. - `resave: false` отменяет запись на каждом запросе, если ничего не изменилось. Вместо этого TTL обновляет метод хранилища
touch. - `saveUninitialized: false` означает, что посетитель, который так и не вошёл, не создаёт ключа. Оставьте
true- и каждый краулер, бот и health check создаст сессию; именно так хранилище сессий доходит до миллиона ключей на сайте с сорока пользователями. - `maxAge` задаёт срок жизни cookie, и
connect-redisиспользует его как TTL ключа, так что эти два значения не могут разойтись. - `secure: true` отправляет cookie только по HTTPS. За обратным прокси, который завершает TLS, Express видит обычный HTTP и отказывается ставить secure-cookie, пока вы не добавите
app.set("trust proxy", 1). Эта одна пропущенная строка - самая частая ошибка вида «в продакшене сессии не держатся». Остальная настройка прокси описана в статье deploy Express API.
В старой кодовой базе вы увидите const RedisStore = require("connect-redis")(session). Эта форма с фабрикой относится к старым мажорным версиям; подбирайте стиль импорта под версию в вашем package.json.
Django#
В Django 4.0 и новее есть встроенный бэкенд кэша Redis, так что кроме клиентской библиотеки redis сторонние пакеты не нужны. Сессии затем используют кэш.
$ pip install "redis>=5" hiredisimport osCACHES = { "default": { "BACKEND": "django.core.cache.backends.redis.RedisCache", "LOCATION": os.environ["VALKEY_URL"], "KEY_PREFIX": "shop", "TIMEOUT": 300, }}SESSION_ENGINE = "django.contrib.sessions.backends.cache"SESSION_CACHE_ALIAS = "default"SESSION_COOKIE_AGE = 60 * 60 * 24 * 14SESSION_COOKIE_SECURE = TrueCSRF_COOKIE_SECURE = TrueDjango предлагает два движка на основе кэша, и выбор между ними важен:
- `backends.cache` хранит сессию только в кэше. Быстрее всего, и сессия пропадает, если ключ вытеснен или сервер его потерял.
- `backends.cached_db` пишет насквозь в базу данных и читает из кэша. Каждая запись сессии стоит записи в базу, зато потерянный ключ кэша восстанавливается из таблицы, а не разлогинивает пользователя.
При Valkey с сохранением данных и разумной политикой вытеснения обычно выбирают backends.cache. Если ваши сессии несут что-то, что дорого потерять, - скажем, наполовину оформленный заказ, - осторожный выбор это cached_db. TTL сессии берётся из SESSION_COOKIE_AGE, а не из TIMEOUT кэша, так что короткий таймаут кэша не обрезает сессии.
За прокси задавайте SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") только если прокси действительно ставит этот заголовок; полный список - в статье deploy Django в продакшен.
Laravel#
Laravel общается с Valkey через свой драйвер redis, по умолчанию используя расширение phpredis или пакет Predis на чистом PHP, если расширения нет. Всё задаётся в .env:
SESSION_DRIVER=redisSESSION_CONNECTION=defaultSESSION_LIFETIME=120REDIS_CLIENT=phpredisREDIS_HOST=203.0.113.20REDIS_PORT=6380REDIS_PASSWORD=a-long-generated-passwordREDIS_DB=0REDIS_CACHE_DB=1Сначала проверьте php -m | grep redis. Если расширение не загружено, либо установите его, либо выполните composer require predis/predis и задайте REDIS_CLIENT=predis. Predis медленнее, но не требует ничего компилировать.
SESSION_LIFETIME задаётся в минутах. Laravel добавляет к каждому ключу префикс из REDIS_PREFIX, который по умолчанию строится из APP_NAME, - так что два приложения Laravel с одинаковым APP_NAME на одном Valkey будут делить ключи сессий. Задайте REDIS_PREFIX явно или дайте каждому приложению свой APP_NAME.
Разделение REDIS_DB и REDIS_CACHE_DB - самая важная строка. Laravel кладёт кэш и сессии в разные логические базы, чтобы php artisan cache:clear, очищающий базу кэша, не разлогинивал всех. Если свести их к одному номеру, рутинная очистка кэша превращается в массовый выход пользователей. После изменения .env выполните php artisan config:clear или php artisan config:cache, если в продакшене конфигурация кэшируется, - почему на этом шаге многие спотыкаются, объясняет статья deploy Laravel в продакшен.
Чистый PHP с phpredis#
Без фреймворка собственную обработку сессий PHP можно направить в Valkey двумя строками php.ini, если установлено расширение phpredis:
session.save_handler = redissession.save_path = "tcp://203.0.113.20:6380?auth=a-long-generated-password&database=2&prefix=shop_sess:"session.gc_maxlifetime = 7200redis.session.locking_enabled = 1redis.session.lock_expire = 30redis.session.lock_wait_time = 50000redis.session.lock_retries = 100После этого session_start() и $_SESSION работают точно как раньше. session.gc_maxlifetime становится TTL ключа и обновляется при каждом запросе, который пишет сессию. Если править php.ini нельзя, те же две настройки можно применить через ini_set() до вызова session_start().
Блокировки заслуживают отдельного абзаца, потому что это единственное настоящее отличие от файловых сессий. Файловый обработчик PHP блокирует файл сессии на всё время запроса, поэтому два одновременных запроса одного пользователя выполняются друг за другом. Обработчик phpredis по умолчанию не блокирует: два параллельных AJAX-вызова оба читают сессию, оба её меняют, и вторая запись молча затирает первую. redis.session.locking_enabled = 1 возвращает поведение файлового обработчика. Цена в том, что медленный запрос блокирует остальные запросы того же пользователя, - но так было и с файловыми сессиями, а полезная привычка - вызывать session_write_close(), как только скрипт закончил писать в сессию.
ASP.NET Core#
Middleware сессий в ASP.NET Core хранит данные в том IDistributedCache, который зарегистрирован. Реализация для Redis находится в Microsoft.Extensions.Caching.StackExchangeRedis:
$ dotnet add package Microsoft.Extensions.Caching.StackExchangeRedisbuilder.Services.AddStackExchangeRedisCache(options =>{ options.Configuration = builder.Configuration["Valkey:Configuration"]; options.InstanceName = "shop:";});builder.Services.AddSession(options =>{ options.IdleTimeout = TimeSpan.FromMinutes(30); options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; options.Cookie.SecurePolicy = CookieSecurePolicy.Always;});var app = builder.Build();app.UseSession();Строка конфигурации записывается в синтаксисе StackExchange.Redis, а не как URL: 203.0.113.20:6380,password=...,abortConnect=false. abortConnect=false позволяет приложению запуститься и продолжать попытки подключения, если Valkey ненадолго недоступен, вместо того чтобы упасть при старте. InstanceName - это префикс ключей.
Здесь .NET-разработчики спотыкаются о две вещи. Во-первых, сессия ASP.NET Core - это не аутентификация: cookie входа, которую выдаёт cookie-аутентификация или Identity, самодостаточна и зашифрована и в сессии вообще не живёт. Во-вторых, это шифрование использует ключи Data Protection, которые по умолчанию хранятся на локальном диске каждого экземпляра. Запустите два экземпляра или пересоздайте контейнер - и пользователи окажутся разлогинены, потому что новый процесс не может расшифровать старую cookie. Решение - хранить связку ключей централизованно, и Valkey для этого естественное место: Microsoft.AspNetCore.DataProtection.StackExchangeRedis и PersistKeysToStackExchangeRedis(connection, "DataProtection-Keys"). Подробнее об этом - в статье основы аутентификации в ASP.NET Core.
TTL, вытеснение и сохранение: настройки, от которых зависит, останутся ли пользователи в системе#
Каждая библиотека выше ставит TTL на каждый ключ сессии. Именно это делает Valkey хорошим хранилищем сессий, и именно поэтому две серверные настройки важнее любого кода.
Политика вытеснения. Когда Valkey достигает лимита памяти, что делать, решает maxmemory-policy. С noeviction записи падают с ошибкой нехватки памяти: новые входы громко ломаются, существующие сессии выживают. С allkeys-lru удаляются ключи, к которым дольше всего не обращались: всё продолжает работать, а пользователи, бездействовавшие дольше всех, молча выходят из системы. С volatile-lru или volatile-ttl кандидатами становятся только ключи с TTL - а поскольку он есть у каждой сессии, вытесняются именно сессии.
Для Valkey, где хранятся только сессии, честный выбор - noeviction с достаточным объёмом памяти: о том, что нужен тариф побольше, вы узнаёте из ошибки, а не из жалоб пользователей на постоянные разлогинивания. Если тот же экземпляр держит ещё и кэш, либо разделите их (второй маленький экземпляр стоит недорого), либо смиритесь с тем, что сессии конкурируют за память с записями кэша. Полная таблица - в статье память и политики вытеснения Valkey.
Проверить, что у вас сейчас, можно так:
$ valkey-cli -h 203.0.113.20 -p 6380 --askpass CONFIG GET maxmemory-policy$ valkey-cli -h 203.0.113.20 -p 6380 --askpass INFO memoryЕсли на управляемом экземпляре CONFIG запрещён, INFO memory всё равно показывает maxmemory и maxmemory_policy.
Сохранение данных. Хранилище сессий, теряющее содержимое при перезапуске, разлогинивает всех пользователей при каждом перезапуске. С журналом только для добавления и appendfsync everysec жёсткая остановка теряет не больше секунды записей сессий, чего на практике никто не замечает. Оба механизма и цена каждого объяснены в статье сохранение данных в Valkey: AOF и RDB.
Расчёт размера. Типичная сессия - id пользователя, CSRF-токен, flash-сообщение и немного служебных данных фреймворка - занимает от нескольких сотен байт до пары килобайт плюс накладные расходы на ключ. Десять тысяч активных сессий по 2 КБ - это около 20 МБ. Тариф на 256 МБ вмещает намного больше сессий, чем когда-либо будет у большинства сайтов; на деле хранилище сессий заполняют либо анонимные сессии в духе saveUninitialized, либо приложения, которые запихивают в сессию целые объекты - корзину с данными товаров, запись пользователя с правами - вместо id. Держите сессии маленькими, а остальное ищите по id.
Настоящие цифры можно увидеть, а не угадывать:
$ valkey-cli --askpass -h 203.0.113.20 -p 6380 --scan --pattern 'shop:sess:*' | head -3$ valkey-cli --askpass -h 203.0.113.20 -p 6380 MEMORY USAGE shop:sess:Xk2v...$ valkey-cli --askpass -h 203.0.113.20 -p 6380 TTL shop:sess:Xk2v...На всём, что обслуживает трафик, используйте --scan, но никогда не KEYS: KEYS обходит всё пространство ключей одним блокирующим вызовом.
Устранение неполадок#
Пользователей разлогинивает после каждого deploy. Либо хранилище внутри процесса (конфигурация так и не применилась - проверьте, что приложение действительно загрузило хранилище Valkey), либо сменился секрет сессий (secret в Express, APP_KEY в Laravel, SECRET_KEY в Django или ключи Data Protection в .NET), либо Valkey перезапустился без сохранения данных.
В продакшене сессии вообще не держатся. Secure-cookie по соединению, которое приложение считает HTTP. Настройте в фреймворке доверие к прокси, чтобы он читал X-Forwarded-Proto. Откройте инструменты разработчика в браузере и проверьте, приходит ли заголовок Set-Cookie и сохраняется ли cookie.
Случайные выходы из системы под нагрузкой. Вытеснение. Посмотрите evicted_keys в INFO stats; если число растёт, сессии удаляются, чтобы освободить место. Добавьте памяти, вынесите кэш в другое место или переключитесь на noeviction.
`NOAUTH Authentication required` или `WRONGPASS`. Пароль в URL отсутствует или неверен. Специальные символы пароля внутри URL нужно кодировать через проценты - @ или / в пароле ломают разбор.
Запросы одного пользователя медленные и идут друг за другом. Блокировка сессии. Освобождайте сессию пораньше через session_write_close() в PHP или выносите долгую работу из запросов, которые держат сессию.
Два приложения видят сессии друг друга. Одинаковый префикс, одна и та же база. Дайте каждому свой префикс.
FAQ#
Можно ли использовать один экземпляр Valkey для сессий и кэша?
Да, если разделить их префиксом или логической базой и смириться с тем, что память у них общая. Риск в том, что кэш разрастётся настолько, что вытеснение начнёт удалять сессии. Для всего крупнее маленького сайта два небольших экземпляра чище, чем один большой с политикой, которая должна подходить обеим задачам.
Что происходит с сессиями, если Valkey упал?
Новые запросы не могут загрузить сессии, поэтому пользователи выглядят разлогиненными или получают ошибки - в зависимости от того, как библиотека обрабатывает неудачное подключение. Когда Valkey возвращается с включённым сохранением, сохранённые им сессии на месте. Задайте разумные таймауты подключения, чтобы отсутствующий Valkey приводил к быстрой ошибке, а не к зависанию каждого запроса.
Безопасно ли хранить персональные данные в сессии?
Храните как можно меньше: id, по которому вы найдёте запись, а не саму запись. Ни одна из этих библиотек не шифрует содержимое сессий при хранении в Valkey, и любой, у кого есть пароль, может его прочитать. Относитесь к паролю Valkey так же, как к паролю базы данных.
Сколько должна длиться сессия?
Так мало, как готовы терпеть ваши пользователи. Два часа бездействия для админки, две недели для потребительского сайта с галочкой «запомнить меня». TTL ключа должен равняться сроку жизни cookie; каждая библиотека выше связывает их, если вы задаёте возраст cookie и не трогаете собственную настройку TTL хранилища.
Нужны ли sticky sessions на балансировщике, если я использую Valkey?
Нет. В этом и смысл общего хранилища: любой процесс может обслужить любой запрос. Sticky sessions - обходной путь для состояния сессий внутри процесса, и они становятся не нужны, как только сессии живут в Valkey.




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