Почти каждому веб-проекту рано или поздно нужен Redis: кэшировать тяжёлые запросы, хранить сессии, ограничивать частоту запросов к API. Поднять его на том же сервере несложно, но потом приходится следить за памятью, обновлениями и тем, чтобы порт 6379 не оказался открыт в интернет. А облачные Redis-сервисы вроде Upstash и Redis Cloud из России не оплатить.

Что такое managed Redis

Managed Redis — это Redis, который обслуживает провайдер: память, обновления, реплики, TLS и сетевой доступ. Приложение получает адрес и пароль, а следить за сервером под кэшем больше не нужно.

На Татнет эту роль выполняет Managed Valkey. Valkey — это форк Redis, который сообщество начало под эгидой Linux Foundation в 2024 году, после того как Redis сменил лицензию. Протокол и команды те же, поэтому redis-py, ioredis, node-redis и go-redis работают с ним без изменений: меняется только адрес подключения.

Что получится в конце:

  • кластер Valkey в приватной сети (VPC), куда пускают только адреса вашей сети;
  • приложение подключается к нему по TLS и проверяет сертификат сервера;
  • готовые примеры кэша, сессий и rate limit для Django, Node.js и Go.

Схема: приложение и кластер managed Redis на Valkey в одной VPC. Приложение ходит к Valkey по rediss:// с проверкой сертификата; из интернета доступа к кэшу нет

Если вы уже читали гайд по Managed PostgreSQL, первые шаги покажутся знакомыми: сеть та же, а принцип подключения похож.

Шаг 0. CLI и сеть

Всё делаем из терминала через CLI Татнет. То же самое доступно в панели управления.

npx tatnet@latest --help        # или npm install -g tatnet
tatnet auth login
export TATNET_PROJECT=my-saas

Регионы, версии Valkey и тарифы:

tatnet valkey regions -o json

Нужна приватная сеть в том же регионе, что и приложение. Если она уже есть (например, вы создавали её для PostgreSQL), используйте её. Если нет:

REGION=<id региона>
tatnet vpc create --name app-net --cluster $REGION --subnet 10.20.0.0/24

Подсеть может быть любой приватной, кроме 10.0.0.0/12: этот диапазон занят инфраструктурой платформы. Приватные сети бесплатны. Зачем они вообще нужны, мы разбирали в статье «Что такое VPC».

Шаг 1. Выбираем тариф по памяти для данных

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

Поэтому тарифы Татнет называются по памяти для данных, а не по объёму RAM:

ТарифПамять узлаДля данных
vk1.1g1,5 ГБ1 ГБ
vk1.2g2,5 ГБ2 ГБ
vk1.4g5 ГБ4 ГБ

Остальные тарифы и актуальные цены — на странице тарифов и в выводе tatnet valkey regions.

Как прикинуть объём: сессия занимает от одного до нескольких килобайт, закэшированный JSON-ответ занимает столько же, сколько сам ответ, а счётчик rate limit — меньше сотни байт. Сто тысяч активных сессий по 2 КБ — это около 200 МБ. Большинству проектов на старте хватает гигабайта, а тариф потом можно поднять без пересоздания кластера (шаг 5).

Шаг 2. Кластер в приватной сети

VPC=<id сети app-net>

tatnet valkey create \
  --name app-cache \
  --region $REGION \
  --plan vk1.1g \
  --topology single \
  --vpc $VPC \
  --allow 10.20.0.0/24

Самый важный флаг здесь — --allow. У PostgreSQL есть ещё одна линия защиты, pg_hba, а у Valkey её нет: список разрешённых адресов — единственный сетевой фильтр перед портом. Поэтому разрешаем только подсеть нашей VPC и никогда не пишем 0.0.0.0/0 «на время отладки».

Если позже понадобится добавить адрес, учтите, что команда allowlist заменяет список целиком. Адреса, которые вы не перечислили, сразу теряют доступ:

tatnet valkey allowlist app-cache 10.20.0.0/24 10.30.0.0/24

Адрес и порт кластера покажет команда:

tatnet valkey get app-cache -o json

Пароль сохраните сразу, как только платформа его покажет. Если он потерялся или утёк, его можно сбросить:

tatnet valkey reset-password app-cache

Новый пароль показывается один раз, а старый перестаёт работать сразу. Поэтому сначала обновите секрет у приложения, а потом перезапустите его.

Шаг 3. Вытеснение и сохранение на диск

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

  • Чистый кэш. Подходит вытеснение любых ключей по давности использования (в терминах Valkey — allkeys-lru). Когда память кончается, Valkey сам удаляет то, к чему дольше всего не обращались, а приложение при промахе просто идёт в базу.
  • Сессии и счётчики rate limit. Их потеря заметна пользователю: его разлогинит или ему сбросится лимит. Для них нужен запас памяти и политика, которая не трогает такие ключи без необходимости.

Самый простой способ не выбирать — завести два кластера: один под кэш с агрессивным вытеснением, второй под сессии и лимиты. Второй кластер на младшем тарифе обойдётся дешевле, чем разбираться, почему пользователей разлогинило в час пик.

Политику и режим сохранения на диск можно задать при создании (--eviction, --persistence) или поменять позже:

tatnet valkey params app-cache --eviction <политика> --persistence <режим>

Параметры применяются к работающему серверу без перезапуска, так что политику можно спокойно поменять, когда станет понятно, как ведёт себя нагрузка.

Для чистого кэша сохранение на диск обычно не нужно: после перезапуска он прогреется заново. Для сессий его лучше включить.

Шаг 4. Подключение по TLS

Valkey на Татнет принимает только зашифрованные соединения. Схема адреса — rediss:// (с двумя s), а сертификат сервера клиент проверяет по корневому сертификату (CA) кластера:

tatnet valkey ca app-cache --out valkey-ca.pem

CA-сертификат не секрет: его можно положить в репозиторий рядом с кодом. Секрет — это пароль, и он хранится в секретах приложения:

tatnet app env set api REDIS_URL "rediss://:<пароль>@<адрес>:<порт>/0" --secret

Приложение должно быть в той же сети. Для существующего приложения это делается командой tatnet app update api --vpc $VPC. При смене сети платформа пересоздаёт реплики приложения, так что возможен короткий перерыв.

Дальше приведены примеры для трёх стеков. Во всех них клиент проверяет и подпись сертификата, и имя сервера, а не просто шифрует трафик.

Django

В Django 4.0+ поддержка Redis встроена, отдельный пакет не нужен. Опции из OPTIONS передаются в redis-py:

# settings.py
import os

VALKEY_TLS = {
    "ssl_cert_reqs": "required",
    "ssl_ca_certs": os.path.join(BASE_DIR, "valkey-ca.pem"),
}

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": os.environ["REDIS_URL"],
        "OPTIONS": VALKEY_TLS,
        "KEY_PREFIX": "cache",
        "TIMEOUT": 300,
    },
    "sessions": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": os.environ.get("SESSIONS_REDIS_URL", os.environ["REDIS_URL"]),
        "OPTIONS": VALKEY_TLS,
        "KEY_PREFIX": "sess",
        "TIMEOUT": None,  # срок жизни сессии задаёт SESSION_COOKIE_AGE
    },
}

# Сессии в Valkey, без запросов к базе на каждый хит
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "sessions"

Если вы завели второй кластер под сессии, передайте его адрес в SESSIONS_REDIS_URL. Если терять сессии нельзя совсем, используйте backends.cached_db: тогда Valkey работает как кэш, а источником правды остаётся база.

Кэш используется как обычно:

from django.core.cache import cache

def dashboard_stats(user_id):
    return cache.get_or_set(f"stats:{user_id}", lambda: compute_stats(user_id), 60)

Rate limit. Пакет django-ratelimit хранит счётчики в кэше Django, поэтому отдельная настройка ему не нужна:

from django_ratelimit.decorators import ratelimit

@ratelimit(key="ip", rate="10/m", method="POST", block=True)
def login_view(request):
    ...

Node.js

Пример на официальном клиенте redis (node-redis v4+) вместе с Express. Схема rediss:// сама включает TLS, а CA мы передаём явно:

import fs from "node:fs";
import express from "express";
import session from "express-session";
import { createClient } from "redis";
import { RedisStore as SessionStore } from "connect-redis";
import { rateLimit } from "express-rate-limit";
import { RedisStore as LimitStore } from "rate-limit-redis";

const valkey = createClient({
  url: process.env.REDIS_URL,
  socket: {
    ca: fs.readFileSync("./valkey-ca.pem"), // проверка сертификата и имени сервера
  },
});
valkey.on("error", (err) => console.error("valkey:", err));
await valkey.connect();

const app = express();

// Сессии
app.use(
  session({
    store: new SessionStore({ client: valkey, prefix: "sess:" }),
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: { secure: true, httpOnly: true, maxAge: 7 * 24 * 3600 * 1000 },
  }),
);

// Rate limit: 100 запросов в минуту на IP для всего API
app.use(
  "/api",
  rateLimit({
    windowMs: 60_000,
    limit: 100,
    standardHeaders: "draft-7",
    legacyHeaders: false,
    store: new LimitStore({
      prefix: "rl:",
      sendCommand: (...args) => valkey.sendCommand(args),
    }),
  }),
);

// Кэш
app.get("/api/products", async (req, res) => {
  const cached = await valkey.get("cache:products");
  if (cached) return res.type("json").send(cached);

  const products = JSON.stringify(await db.products.findAll());
  await valkey.set("cache:products", products, { EX: 300 });
  res.type("json").send(products);
});

Если в проекте уже используется ioredis, менять его не нужно: new Redis(process.env.REDIS_URL, { tls: { ca: fs.readFileSync("./valkey-ca.pem") } }).

Go

go-redis v9 по схеме rediss:// сам включает TLS и проверку имени сервера. Нам остаётся только добавить CA кластера:

package cache

import (
	"crypto/x509"
	"os"

	"github.com/redis/go-redis/v9"
)

func New() (*redis.Client, error) {
	opt, err := redis.ParseURL(os.Getenv("REDIS_URL"))
	if err != nil {
		return nil, err
	}

	pem, err := os.ReadFile("valkey-ca.pem")
	if err != nil {
		return nil, err
	}
	pool := x509.NewCertPool()
	pool.AppendCertsFromPEM(pem)
	opt.TLSConfig.RootCAs = pool // проверяем сертификат кластера, а не системные CA

	return redis.NewClient(opt), nil
}

Кэш с промахом в базу:

func Product(ctx context.Context, rdb *redis.Client, id string) ([]byte, error) {
	key := "cache:product:" + id
	b, err := rdb.Get(ctx, key).Bytes()
	if err == nil {
		return b, nil
	}
	if err != redis.Nil {
		return nil, err
	}

	b, err = loadProductJSON(ctx, id) // запрос в базу
	if err != nil {
		return nil, err
	}
	rdb.Set(ctx, key, b, 5*time.Minute)
	return b, nil
}

Сессии в Go обычно хранят по шаблону «случайный ID в cookie, данные в Valkey с TTL». Это rdb.Set(ctx, "sess:"+id, data, 7*24*time.Hour) при входе и rdb.Get на каждом запросе.

Rate limit — пакет github.com/go-redis/redis_rate/v10. Он реализует алгоритм GCRA одним Lua-скриптом, поэтому лимит считается атомарно, даже если реплик приложения несколько:

limiter := redis_rate.NewLimiter(rdb)

func limit(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		res, err := limiter.Allow(r.Context(), "rl:"+clientIP(r), redis_rate.PerMinute(100))
		if err == nil && res.Allowed == 0 {
			w.Header().Set("Retry-After", strconv.Itoa(int(res.RetryAfter.Seconds())+1))
			http.Error(w, "too many requests", http.StatusTooManyRequests)
			return
		}
		next.ServeHTTP(w, r)
	})
}

Обратите внимание: если Valkey недоступен (err != nil), запрос пропускается. Для большинства API лучше ненадолго пропустить лимит, чем уронить весь сервис. Тот же принцип относится и к кэшу: ошибка Valkey должна превращаться в промах, а не в ответ 500.

Шаг 5. Рост: тариф и HA

Тариф меняется одной командой:

tatnet valkey plan app-cache vk1.2g

Тариф можно только повышать: память, vCPU и диск нового тарифа должны быть не меньше текущих. Узлы перезагружаются по одному, а новый лимит maxmemory начинает действовать, когда все узлы уже работают на новых ресурсах. В топологии single узел один, поэтому на время его перезагрузки кэш недоступен. Именно здесь пригодится правило «ошибка Valkey — это промах».

Отказоустойчивость. Когда кэш или сессии становятся критичными, кластер переводится в HA:

tatnet valkey topology app-cache ha

Переход проходит без простоя: к работающему серверу присоединяется реплика, и у кластера появляется отдельный адрес для чтения. Его можно отдать тяжёлым читающим запросам, а основной адрес оставить для записи. При необходимости кластер можно вернуть в single.

Переезд с Upstash и Redis Cloud

Для кэша переносить данные не нужно. Меняете REDIS_URL, добавляете CA, выкатываете приложение, и кэш прогревается сам за счёт промахов в первые минуты.

Для сессий при простом переключении пользователей разлогинит один раз. Если это критично, переключайтесь в спокойное время или сначала перейдите на cached_db (Django) или аналог: тогда сессии переживут смену хранилища.

Код при переезде менять не нужно: Valkey понимает те же команды, что и Redis.

Сколько это стоит

Как считается стоимость:

  • За узел. У кластера single один узел. В HA узлов больше, и стоимость растёт кратно их числу. Итоговую сумму панель показывает до создания.
  • Почасово. Тестовый кластер, поднятый на вечер, обойдётся в рубли, а не в месячную подписку.
  • Сеть бесплатна. Приватная сеть ничего не стоит на любом тарифе, а публичный адрес в этой схеме не нужен.
  • В рублях, без иностранной карты и пересчёта по курсу.

Начинать стоит с младшего тарифа: тариф поднимается одной командой (шаг 5), а опустить его нельзя. Актуальные цены — на странице тарифов.

Итого

  1. Тариф выбираете по памяти для данных, а не по RAM узла.
  2. Кластер создаёте в VPC, allowlist ограничиваете подсетью сети: это единственный сетевой фильтр.
  3. Политику вытеснения подбираете под задачу. Кэш и сессии лучше держать в разных кластерах.
  4. Подключаетесь по rediss:// с CA кластера, пароль храните в секретах приложения.
  5. Ошибка Valkey в коде должна давать промах, а не 500. Когда простой становится дорогим, переводите кластер в HA.

Полный справочник команд — в документации Managed Valkey. Создать первый кластер можно в панели Татнет.