API отвечал 3 секунды: Redis-кластер для сервиса доставки с 10К заказов в день

Задача клиента: API доставки, который не справляется

В феврале 2026 года к нам в АйТи Фреш обратился сервис доставки еды «ЕдаБлиц» из Казани — агрегатор с партнёрской сетью из 280 ресторанов. Через мобильное приложение ежедневно проходило около 10 000 заказов, и цифра росла на 15–20% каждый месяц. Казалось бы, хорошая история про рост. Но параллельно нарастала другая история — API тормозил так, что пользователи просто уходили.

Среднее время ответа на главных эндпоинтах (/menu, /restaurants, /order/status) держалось на уровне 2–3 секунды. В вечерний пик — с 18:00 до 21:00 — цифры доходили до 5–7 секунд. Люди открывали приложение, ждали меню и закрывали его. Аналитика была безжалостной: 23% пользователей уходили, если страница не загружалась за 2 секунды. Это живые деньги, которые просто утекали.

«Мы росли, но рост нас убивал. Каждый новый ресторан добавлял нагрузку на базу, и мы физически не могли ответить пользователю быстрее трёх секунд. Наши курьеры простаивали, потому что заказы оформлялись слишком медленно» — CTO «ЕдаБлиц».

Аудит текущей архитектуры

Наши инженеры подключились к серверам и провели детальный аудит. Архитектура включала:

  • 2 сервера приложений на Go (API) за nginx-балансировщиком
  • 1 сервер PostgreSQL 15 — основное хранилище (меню, заказы, пользователи)
  • Без кеширования — каждый запрос шёл напрямую в PostgreSQL
  • Очереди на cron — отправка уведомлений и расчёт ETA курьеров выполнялись через cron-задачи каждые 30 секунд

Вот что показал анализ нагрузки на PostgreSQL:

# Подключаемся к PostgreSQL и смотрим активные запросы
sudo -u postgres psql -c "
SELECT pid, now() - pg_stat_activity.query_start AS duration,
       query, state
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY duration DESC
LIMIT 10;"

# Результат: 47 активных запросов, самый длинный — 12 секунд
# Запрос к меню ресторана с JOIN на 6 таблиц

# Статистика попаданий в кеш PostgreSQL
SELECT
  sum(heap_blks_read) as disk_reads,
  sum(heap_blks_hit) as cache_hits,
  round(sum(heap_blks_hit) * 100.0 /
    (sum(heap_blks_hit) + sum(heap_blks_read)), 2) as ratio
FROM pg_statio_user_tables;

# disk_reads: 2,847,291 | cache_hits: 18,492,003 | ratio: 86.65%

Cache hit ratio — 86%. Для OLTP-систем норма — 99%+. По сути, база постоянно читала данные с диска: 280 ресторанов, у каждого меню от 50 до 200 позиций, и всё это запрашивалось снова и снова. Данные не успевали оседать в shared_buffers — их тут же вытесняли новые запросы.

Обоснование выбора Redis

Мы рассмотрели три варианта:

РешениеПлюсыМинусы
MemcachedПростой, быстрыйНет persistence, нет структур данных, нет кластеризации
RedisСтруктуры данных, Streams, Sentinel, persistenceЧуть сложнее в настройке
Apache KafkaМощные очередиOverkill для текущих задач, требует ZooKeeper/KRaft

Redis победил по совокупности факторов. Одним инструментом закрывались сразу две задачи — кеширование и очереди, без лишних зависимостей в стеке. В итоге утвердили архитектуру из 3 узлов Redis с Sentinel для HA и Streams для очередей.

Установка и базовая конфигурация Redis

Под Redis выделили три отдельных сервера (Ubuntu 22.04 LTS) и ещё один — для Sentinel-нод. Все машины стояли в одном ЦОД, но в разных стойках. Это не паранойя — физическое разнесение по стойкам реально спасает при питании или коммутаторе, упавшем в одной из них.

Установка Redis 7.2 из официального репозитория

На каждом из трёх серверов выполняем установку:

# Добавляем официальный репозиторий Redis
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list

sudo apt update
sudo apt install -y redis-server redis-tools

# Проверяем версию
redis-server --version
# Redis server v=7.2.4 sha=00000000:0 malloc=jemalloc-5.3.0 bits=64 build=...

# Останавливаем сервис, будем конфигурировать
sudo systemctl stop redis-server

Перед конфигурированием подготовим систему — оптимизируем ядро под Redis:

# Отключаем transparent huge pages (критично для Redis)
echo 'never' | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
echo 'never' | sudo tee /sys/kernel/mm/transparent_hugepage/defrag

# Делаем постоянным через systemd
sudo bash -c 'cat > /etc/systemd/system/disable-thp.service << EOF
[Unit]
Description=Disable Transparent Huge Pages
Before=redis-server.service

[Service]
Type=oneshot
ExecStart=/bin/sh -c "echo never > /sys/kernel/mm/transparent_hugepage/enabled && echo never > /sys/kernel/mm/transparent_hugepage/defrag"

[Install]
WantedBy=multi-user.target
EOF'
sudo systemctl enable disable-thp.service

# Настройки sysctl
sudo bash -c 'cat >> /etc/sysctl.conf << EOF
vm.overcommit_memory = 1
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
EOF'
sudo sysctl -p

Конфигурация master-узла

Master-узел (redis-01, IP 10.0.2.11) принимает все операции записи. Вот конфигурация /etc/redis/redis.conf:

# /etc/redis/redis.conf — Master node (redis-01)

# Сетевые настройки
bind 10.0.2.11 127.0.0.1
port 6379
protected-mode yes
tcp-backlog 511
timeout 300
tcp-keepalive 60

# Общие настройки
daemonize no  # systemd управляет процессом
supervised systemd
loglevel notice
logfile /var/log/redis/redis-server.log
databases 16

# Аутентификация (Redis 6+ ACL)
requirepass S3cur3R3d1s!Pr0d2026
masterauth S3cur3R3d1s!Pr0d2026

# Persistence — RDB snapshots
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /var/lib/redis

# Persistence — AOF (Append Only File)
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-use-rdb-preamble yes

# Управление памятью
maxmemory 6gb
maxmemory-policy allkeys-lfu
maxmemory-samples 10

# Лимиты клиентов
maxclients 10000

# Slow log для отладки
slowlog-log-slower-than 10000
slowlog-max-len 128

# Latency monitor
latency-monitor-threshold 100

Ключевые решения по конфигурации:

  • maxmemory-policy allkeys-lfu — выбрали LFU (Least Frequently Used) вместо привычного LRU. Для сервиса доставки это принципиально: топовые рестораны запрашиваются в 100 раз чаще, чем хвост из малопопулярных заведений. LFU держит горячие данные в кеше, а не вытесняет их только потому, что они «давно не запрашивались».
  • AOF + RDB preamble — гибридная persistence: AOF для надёжности (потеря максимум 1 секунды данных), RDB-preamble для быстрого старта.
  • appendfsync everysec — баланс между производительностью и надёжностью. Для кеша приемлема потеря 1 секунды данных при аварии.

Конфигурация replica-узлов

Два replica-узла (redis-02, redis-03) реплицируют данные с master и обслуживают запросы чтения:

# /etc/redis/redis.conf — Replica node (redis-02, IP 10.0.2.12)
# Аналогичная конфигурация, отличия:

bind 10.0.2.12 127.0.0.1

# Указываем master
replicaof 10.0.2.11 6379
masterauth S3cur3R3d1s!Pr0d2026
requirepass S3cur3R3d1s!Pr0d2026

# Replica доступна только для чтения
replica-read-only yes

# Реплика продолжает отвечать на запросы во время синхронизации
replica-serve-stale-data yes

# Не используем diskless-sync для стабильности
repl-diskless-sync no
repl-diskless-sync-delay 5

# Backlog для частичной ресинхронизации
repl-backlog-size 256mb
repl-backlog-ttl 3600

Запускаем все узлы и проверяем репликацию:

# На master
sudo systemctl start redis-server
sudo systemctl enable redis-server

# На каждой реплике
sudo systemctl start redis-server
sudo systemctl enable redis-server

# Проверяем статус репликации на master
redis-cli -a 'S3cur3R3d1s!Pr0d2026' info replication

# role:master
# connected_slaves:2
# slave0:ip=10.0.2.12,port=6379,state=online,offset=1547,lag=0
# slave1:ip=10.0.2.13,port=6379,state=online,offset=1547,lag=0
# master_replid:a3f4b7c2d9e1f08a3b5c7d9e1f2a4b6c8d0e2f4a
# master_repl_offset:1547

Redis Sentinel: автоматический failover

Sentinel — это страховка на случай падения master. Он мониторит узлы и, если master перестаёт отвечать, автоматически промоутит одну из реплик. Весь процесс занимает несколько секунд. Мы развернули три Sentinel-процесса — по одному на каждом Redis-сервере — чтобы обеспечить кворум из трёх голосов: без этого Sentinel не примет решение о failover.

Конфигурация Sentinel

На каждом из трёх серверов создаём конфигурацию /etc/redis/sentinel.conf:

# /etc/redis/sentinel.conf

port 26379
bind 10.0.2.11 127.0.0.1
protected-mode no
daemonize no
supervised systemd

# Мониторим master. Кворум = 2 (из 3 sentinel должны согласиться)
sentinel monitor edablitz-master 10.0.2.11 6379 2

# Пароль для подключения к master/replicas
sentinel auth-pass edablitz-master S3cur3R3d1s!Pr0d2026

# Если master не отвечает 5 секунд — считаем его down (SDOWN)
sentinel down-after-milliseconds edablitz-master 5000

# Таймаут failover — 60 секунд
sentinel failover-timeout edablitz-master 60000

# Сколько реплик одновременно синхронизируются с новым master
# 1 = минимальное влияние на доступность чтения при failover
sentinel parallel-syncs edablitz-master 1

# Оповещения при failover
sentinel notification-script edablitz-master /opt/redis/notify-failover.sh
sentinel client-reconfig-script edablitz-master /opt/redis/update-dns.sh

logfile /var/log/redis/sentinel.log

Создаём скрипт уведомления для Telegram:

#!/bin/bash
# /opt/redis/notify-failover.sh
BOT_TOKEN="7012345678:AAHxxxxxxxxxxxxxxxxxxxxx"
CHAT_ID="-100123456789"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
MESSAGE="🔴 Redis Sentinel Alert\n\nEvent: $1\nMaster: $2\nDetails: $3 $4 $5 $6\nTime: $TIMESTAMP"
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
  -d chat_id="${CHAT_ID}" \
  -d text="${MESSAGE}" \
  -d parse_mode="HTML" > /dev/null 2>&1

Запуск и тестирование failover

Запускаем Sentinel на всех трёх узлах:

# Создаём systemd-unit для Sentinel
sudo bash -c 'cat > /etc/systemd/system/redis-sentinel.service << EOF
[Unit]
Description=Redis Sentinel
After=network.target redis-server.service

[Service]
Type=notify
ExecStart=/usr/bin/redis-sentinel /etc/redis/sentinel.conf
Restart=always
RestartSec=5
User=redis
Group=redis
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target
EOF'

sudo systemctl daemon-reload
sudo systemctl start redis-sentinel
sudo systemctl enable redis-sentinel

# Проверяем статус Sentinel
redis-cli -p 26379 sentinel masters
# 1) "name" "edablitz-master"
#    "ip" "10.0.2.11"
#    "port" "6379"
#    "num-slaves" "2"
#    "num-other-sentinels" "2"
#    "quorum" "2"

Тестируем failover — имитируем падение master:

# На master (redis-01) останавливаем Redis
sudo systemctl stop redis-server

# Через 5-10 секунд проверяем Sentinel
redis-cli -h 10.0.2.12 -p 26379 sentinel get-master-addr-by-name edablitz-master
# 1) "10.0.2.12"   <-- новый master!
# 2) "6379"

# В логе Sentinel:
# +sdown master edablitz-master 10.0.2.11 6379
# +odown master edablitz-master 10.0.2.11 6379 #quorum 2/2
# +switch-master edablitz-master 10.0.2.11 6379 10.0.2.12 6379

# Запускаем redis-01 обратно — он автоматически станет репликой
sudo systemctl start redis-server

# Проверяем — redis-01 теперь replica нового master
redis-cli -h 10.0.2.11 -a 'S3cur3R3d1s!Pr0d2026' info replication
# role:slave
# master_host:10.0.2.12

Failover занял 7 секунд. Клиентская библиотека на Go (go-redis) переподключилась к новому master автоматически — через Sentinel discovery, без каких-либо правок в коде приложения.

Redis как кеш: TTL-стратегии и паттерны

Главная задача Redis в этом проекте — разгрузить PostgreSQL. Не просто «добавить кеш», а выстроить осмысленную стратегию: что кешируем, на сколько, что инвалидируем немедленно. Мы разработали многоуровневый подход с разными TTL для разных типов данных.

Архитектура кеширования

Мы выделили три уровня кеша по критичности и частоте обновления:

# Уровень 1: Горячий кеш — меню ресторанов (TTL: 5 минут)
# Самые частые запросы, данные меняются редко
# Паттерн: Cache-Aside с фоновым обновлением
SET menu:restaurant:142 '{"items":[...]}' EX 300

# Уровень 2: Тёплый кеш — рейтинги, отзывы (TTL: 15 минут)
# Умеренная частота, обновляются при новом отзыве
SET ratings:restaurant:142 '{"avg":4.7,"count":1284}' EX 900

# Уровень 3: Сессионный кеш — корзины, геолокация (TTL: 30 минут)
# Привязаны к пользователю, теряют актуальность при закрытии приложения
SET cart:user:98765 '{"items":[{"dish_id":42,"qty":2}]}' EX 1800

# Геолокация курьеров — TTL 10 секунд (почти real-time)
GEOADD couriers:active 49.1234 55.7891 "courier:501"
SET courier:501:meta '{"name":"Ильдар","eta":12}' EX 10

Реализация Cache-Aside с защитой от cache stampede

Есть классическая ловушка — cache stampede, она же thundering herd. TTL популярного ключа истёк, и сотни запросов одновременно ломятся в PostgreSQL за одними и теми же данными. На нагруженном сервисе это моментально роняет базу. Мы закрыли проблему паттерном с мьютексом:

# Псевдокод на Go (пакет go-redis v9)

func GetRestaurantMenu(ctx context.Context, restaurantID int) (*Menu, error) {
    key := fmt.Sprintf("menu:restaurant:%d", restaurantID)

    // 1. Пытаемся получить из кеша
    cached, err := rdb.Get(ctx, key).Result()
    if err == nil {
        var menu Menu
        json.Unmarshal([]byte(cached), &menu)
        return &menu, nil
    }

    // 2. Кеша нет — пробуем взять мьютекс
    lockKey := fmt.Sprintf("lock:%s", key)
    locked, err := rdb.SetNX(ctx, lockKey, 1, 10*time.Second).Result()

    if locked {
        // 3. Мы получили лок — грузим из PostgreSQL
        defer rdb.Del(ctx, lockKey)
        menu, err := db.LoadMenu(ctx, restaurantID)
        if err != nil {
            return nil, err
        }
        data, _ := json.Marshal(menu)

        // 4. Сохраняем в Redis с TTL + jitter (240-360 сек)
        jitter := time.Duration(rand.Intn(120)) * time.Second
        rdb.Set(ctx, key, data, 240*time.Second+jitter)

        return menu, nil
    }

    // 5. Другой горутине не удалось взять лок — ждём и повторяем
    time.Sleep(100 * time.Millisecond)
    return GetRestaurantMenu(ctx, restaurantID)
}

Jitter — случайный разброс TTL — не даёт всем ключам истекать одновременно. Вместо фиксированных 300 секунд каждый ключ живёт от 240 до 360 секунд. Мелкая деталь, которая на практике снимает целый класс проблем с пиковой нагрузкой.

Инвалидация кеша при обновлении данных

Когда ресторан обновляет меню через панель управления, ждать истечения TTL нельзя — пользователи увидят устаревшие данные. Мы решили это через Pub/Sub + инвалидация: обновление в панели тут же публикует событие, подписчики на всех серверах приложений немедленно сбрасывают соответствующие ключи.

# При обновлении меню ресторатором:
# 1. Обновляем PostgreSQL
# 2. Удаляем кеш
DEL menu:restaurant:142

# 3. Публикуем событие для всех инстансов API
PUBLISH cache:invalidate '{"type":"menu","restaurant_id":142}'

# На каждом инстансе API подписчик слушает канал:
# SUBSCRIBE cache:invalidate
# При получении — удаляет локальный in-memory кеш (если есть)

# Для массовой инвалидации (например, изменение цен поставщика)
# используем Lua-скрипт для атомарного удаления по паттерну:
redis-cli -a 'S3cur3R3d1s!Pr0d2026' --eval /opt/redis/scripts/invalidate.lua

-- /opt/redis/scripts/invalidate.lua
local cursor = "0"
local deleted = 0
repeat
    local result = redis.call('SCAN', cursor, 'MATCH', KEYS[1], 'COUNT', 100)
    cursor = result[1]
    local keys = result[2]
    if #keys > 0 then
        deleted = deleted + redis.call('UNLINK', unpack(keys))
    end
until cursor == "0"
return deleted

-- Вызов: redis-cli EVAL "$(cat invalidate.lua)" 1 "menu:restaurant:*"

Redis Streams: очереди вместо cron

Вторая боль — cron-задачи. Уведомления о заказах (push, SMS, email), расчёт ETA курьеров, начисление бонусов — всё это дёргалось по расписанию раз в 30 секунд. Пользователь оформил заказ, а уведомление ресторану ушло только через полминуты. Плюс PostgreSQL получал бессмысленные поллинги на каждом цикле — база работала вхолостую, просто чтобы проверить: «а нет ли чего нового?»

Создание и настройка Streams

Redis Streams — персистентная структура данных, по духу напоминающая Apache Kafka, но без необходимости разворачивать отдельный кластер. Она встроена прямо в Redis. Мы разбили потоки по типам задач:

# Создаём consumer groups для каждого потока
# Группа гарантирует, что каждое сообщение обработается ровно одним воркером

# Поток уведомлений о заказах
XGROUP CREATE orders:notifications $ MKSTREAM

# Поток расчёта ETA
XGROUP CREATE courier:eta-calc $ MKSTREAM

# Поток начисления бонусов
XGROUP CREATE loyalty:bonuses $ MKSTREAM

# Добавляем событие при создании заказа (в Go-коде API):
XADD orders:notifications * \
  order_id 78542 \
  user_id 98765 \
  restaurant_id 142 \
  type new_order \
  user_phone "+79171234567" \
  restaurant_name "Пекарня Волга"

# Проверяем содержимое потока
XLEN orders:notifications
# (integer) 1

XRANGE orders:notifications - +
# 1) 1) "1709312847123-0"
#    2) 1) "order_id" 2) "78542"
#       3) "user_id" 4) "98765"
#       5) "type" 6) "new_order"
#       ...

Consumer workers на Go

Каждый воркер — отдельный процесс. Он читает события из потока через consumer group и обрабатывает их независимо. Если воркер упал — ничего не теряется: необработанные сообщения автоматически переназначаются живым участникам группы:

// notification_worker.go
func (w *NotificationWorker) Run(ctx context.Context) error {
    consumerName := fmt.Sprintf("worker-%s", hostname())

    for {
        // Читаем до 10 сообщений с блокировкой 5 секунд
        streams, err := w.rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
            Group:    "notification-workers",
            Consumer: consumerName,
            Streams:  []string{"orders:notifications", ">"},
            Count:    10,
            Block:    5 * time.Second,
        }).Result()

        if err == redis.Nil {
            continue // Нет новых сообщений
        }

        for _, msg := range streams[0].Messages {
            orderID := msg.Values["order_id"].(string)
            msgType := msg.Values["type"].(string)

            switch msgType {
            case "new_order":
                w.sendPush(msg.Values)
                w.sendSMS(msg.Values)
            case "status_change":
                w.sendPush(msg.Values)
            case "delivered":
                w.sendPush(msg.Values)
                w.requestRating(msg.Values)
            }

            // Подтверждаем обработку
            w.rdb.XAck(ctx, "orders:notifications",
                "notification-workers", msg.ID)
        }
    }
}

// Обработка зависших сообщений (claimed but not acked)
// Запускается отдельной горутиной каждые 60 секунд
func (w *NotificationWorker) ReclaimStale(ctx context.Context) {
    pending, _ := w.rdb.XPendingExt(ctx, &redis.XPendingExtArgs{
        Stream: "orders:notifications",
        Group:  "notification-workers",
        Start:  "-",
        End:    "+",
        Count:  100,
    }).Result()

    for _, p := range pending {
        if p.Idle > 2*time.Minute {
            // Перехватываем зависшее сообщение
            w.rdb.XClaim(ctx, &redis.XClaimArgs{
                Stream:   "orders:notifications",
                Group:    "notification-workers",
                Consumer: hostname(),
                MinIdle:  2 * time.Minute,
                Messages: []string{p.ID},
            })
        }
    }
}

Безопасность: ACL, TLS и сетевая изоляция

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

Redis ACL: гранулярный контроль доступа

Начиная с Redis 6, единый пароль через requirepass — прошлый век. Теперь можно создавать отдельных пользователей с гранулированными правами, как в нормальной СУБД:

# Подключаемся к Redis и настраиваем ACL
redis-cli -a 'S3cur3R3d1s!Pr0d2026'

# Пользователь для API-серверов — полный доступ к кешу и Streams
ACL SETUSER api-service on >ApiStr0ngP@ss! ~menu:* ~ratings:* ~cart:* ~orders:* ~courier:* ~lock:* +@all

# Пользователь для мониторинга — только чтение и INFO
ACL SETUSER monitoring on >M0n1t0r!ng ~* +info +ping +client|getname +slowlog +latency +dbsize +get +llen +xlen +scan -@write -@admin -@dangerous

# Пользователь для Sentinel
ACL SETUSER sentinel on >S3nt1n3lP@ss ~* +multi +slaveof +ping +exec +subscribe +config|rewrite +role +publish +info +client|setname +client|kill +script|kill +psync +replconf

# Отключаем default пользователя
ACL SETUSER default off

# Сохраняем ACL в файл
ACL SAVE

# Проверяем список пользователей
ACL LIST
# 1) "user api-service on >... ~menu:* ~ratings:* ~cart:* ~orders:* ~courier:* ~lock:* +@all"
# 2) "user monitoring on >... ~* +info +ping ..."
# 3) "user sentinel on >... ~* +multi +slaveof ..."
# 4) "user default off ..."

TLS-шифрование и firewall

Трафик между Redis и приложением шифруется по TLS 1.3. На практике это добавляет 1–2 мс латентности, но для большинства сценариев это абсолютно некритично:

# Генерируем сертификаты (используем внутренний CA)
mkdir -p /etc/redis/tls
cd /etc/redis/tls

# CA сертификат
openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
  -out ca.crt -subj "/CN=EdaBlitz Redis CA"

# Server сертификат
openssl genrsa -out redis.key 2048
openssl req -new -key redis.key -out redis.csr \
  -subj "/CN=redis-01.edablitz.local"
openssl x509 -req -in redis.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out redis.crt -days 365 -sha256

chown redis:redis /etc/redis/tls/*
chmod 600 /etc/redis/tls/redis.key

# Добавляем в redis.conf
tls-port 6380
tls-cert-file /etc/redis/tls/redis.crt
tls-key-file /etc/redis/tls/redis.key
tls-ca-cert-file /etc/redis/tls/ca.crt
tls-auth-clients optional
tls-protocols "TLSv1.2 TLSv1.3"
tls-replication yes
tls-cluster yes

# Firewall — только наши серверы имеют доступ
sudo ufw default deny incoming
sudo ufw allow from 10.0.2.0/24 to any port 6379 comment "Redis plain"
sudo ufw allow from 10.0.2.0/24 to any port 6380 comment "Redis TLS"
sudo ufw allow from 10.0.2.0/24 to any port 26379 comment "Redis Sentinel"
sudo ufw allow from 10.0.1.0/24 to any port 6380 comment "API servers"
sudo ufw enable

Мониторинг Redis: Prometheus, Grafana и алерты

Redis без мониторинга — это как ехать ночью без фар. Всё хорошо ровно до удара. Мы развернули полноценный стек наблюдаемости, чтобы знать о проблемах раньше пользователей.

Redis Exporter для Prometheus

Для сбора метрик используем redis_exporter от Oliver006 — де-факто стандарт в экосистеме Prometheus. Он отдаёт несколько сотен метрик: от hit rate и latency до состояния репликации:

# Устанавливаем redis_exporter на каждом узле
wget https://github.com/oliver006/redis_exporter/releases/download/v1.58.0/redis_exporter-v1.58.0.linux-amd64.tar.gz
tar xzf redis_exporter-v1.58.0.linux-amd64.tar.gz
sudo mv redis_exporter-v1.58.0.linux-amd64/redis_exporter /usr/local/bin/

# Systemd unit
sudo bash -c 'cat > /etc/systemd/system/redis-exporter.service << EOF
[Unit]
Description=Redis Exporter
After=redis-server.service

[Service]
Type=simple
ExecStart=/usr/local/bin/redis_exporter \
  --redis.addr=redis://127.0.0.1:6379 \
  --redis.password=S3cur3R3d1s!Pr0d2026 \
  --web.listen-address=:9121 \
  --include-system-metrics
Restart=always
User=redis

[Install]
WantedBy=multi-user.target
EOF'

sudo systemctl daemon-reload
sudo systemctl start redis-exporter
sudo systemctl enable redis-exporter

# Prometheus scrape config
# /etc/prometheus/prometheus.yml (добавляем в scrape_configs)
scrape_configs:
  - job_name: 'redis'
    static_configs:
      - targets:
          - '10.0.2.11:9121'
          - '10.0.2.12:9121'
          - '10.0.2.13:9121'
        labels:
          cluster: 'edablitz-redis'

Ключевые метрики и алерты

В Prometheus Alertmanager настроили алерты на критические ситуации — те, которые нельзя пропустить даже ночью:

# /etc/prometheus/rules/redis-alerts.yml
groups:
  - name: redis-alerts
    rules:
      # Высокое потребление памяти (>85%)
      - alert: RedisMemoryHigh
        expr: redis_memory_used_bytes / redis_memory_max_bytes * 100 > 85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Redis memory usage >85% on {{ $labels.instance }}"
          description: "Current: {{ $value }}%. Eviction policy will start removing keys."

      # Репликация отстаёт
      - alert: RedisReplicationLag
        expr: redis_connected_slaves < 2
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Redis master lost a replica"

      # Высокий latency
      - alert: RedisSlowCommands
        expr: rate(redis_slowlog_length[5m]) > 5
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Redis slow commands increasing"

      # Streams backlog растёт
      - alert: RedisStreamBacklog
        expr: redis_stream_length{stream="orders:notifications"} > 1000
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Notification stream backlog >1000 messages"

Параллельно используем встроенные инструменты самого Redis — они дают срез прямо здесь и сейчас, без лишних слоёв:

# Мониторинг в реальном времени через redis-cli
redis-cli -a 'S3cur3R3d1s!Pr0d2026' --latency-history -i 5
# min: 0, max: 1, avg: 0.32 (503 samples) -- 5.00 seconds range

# Топ медленных команд
redis-cli -a 'S3cur3R3d1s!Pr0d2026' slowlog get 10

# Статистика по командам
redis-cli -a 'S3cur3R3d1s!Pr0d2026' info commandstats
# cmdstat_get:calls=4827391,usec=2413695,usec_per_call=0.50
# cmdstat_set:calls=847291,usec=508374,usec_per_call=0.60
# cmdstat_xadd:calls=312847,usec=187708,usec_per_call=0.60

# Анализ ключей по памяти
redis-cli -a 'S3cur3R3d1s!Pr0d2026' --bigkeys
# [00.00%] Biggest string found so far '"menu:restaurant:89"' with 147832 bytes
# [00.00%] Biggest stream found so far '"orders:notifications"' with 12847 entries

Результаты внедрения: цифры говорят сами за себя

Redis-кластер мы запустили за 12 рабочих дней — от первого аудита до продакшена. Через две недели после запуска сняли замеры:

Ключевые метрики производительности

МетрикаДо RedisПосле RedisУлучшение
Среднее время ответа API2,8 сек80 мс35x быстрее
P99 latency (пиковые часы)7,2 сек320 мс22x быстрее
Нагрузка на PostgreSQL (CPU)87%24%-63%
Активные соединения PostgreSQL47 постоянно8–12-75%
Задержка уведомленийдо 30 сек (cron)< 1 сек (Streams)30x быстрее
Bounce rate приложения23%8%-65%
Cache hit ratio Redis97.3%
Redis failover время7 сек

Бизнес-результаты

Через месяц CTO «ЕдаБлиц» сам написал нам. Поделился цифрами из аналитики — и они оказались лучше, чем мы ожидали:

  • Среднее число заказов выросло с 10 000 до 13 500 в день (+35%) — люди просто перестали уходить с сайта, пока он тормозил
  • Конверсия из просмотра меню в заказ выросла с 12% до 18% — полтора процентных пункта здесь стоят очень дорого в деньгах
  • Время обработки заказа упало с 45 до 12 секунд — от нажатия «Оформить» до push-уведомления на планшете ресторана
  • Ноль инцидентов с доступностью Redis за первый месяц — Sentinel отработал одну плановую миграцию мастера полностью в фоне, без единой секунды простоя

Инженеры АйТи Фреш продолжают вести инфраструктуру «ЕдаБлиц» на аутсорсе. В периметр входит мониторинг Redis-кластера и подготовка к переходу на Redis Cluster mode — это понадобится при достижении примерно 25 000 заказов в день, когда данные перестанут помещаться на одном узле.

Часто задаваемые вопросы

Вопрос, который клиенты задают чаще всего. Redis Sentinel — это про высокую доступность в рамках одного узла данных: мониторит master, при падении автоматически промоутит реплику. Все данные живут на одной машине. Redis Cluster — про шардирование: данные делятся между несколькими узлами, когда в память одного сервера они уже не лезут. Если объём до 50–100 ГБ — Sentinel закрывает задачу с запасом. Нужно больше или нужен горизонтальный рост — тогда Cluster.

Для большинства кеширующих задач берите allkeys-lfu — она вытесняет ключи, к которым обращаются реже всего. Если у всех ключей стоит TTL, подойдёт volatile-lfu. allkeys-lru имеет смысл, когда частота обращений к ключам примерно одинакова. Отдельный момент про очереди на Streams: никогда не используйте для них политику volatile-*. Потеря сообщения в очереди — это не «кеш протух», это реальная бизнес-проблема. Выносите Streams на отдельный инстанс Redis с политикой noeviction.

Redis Streams комфортно держат до 50–100 тысяч сообщений в секунду. Главный аргумент в их пользу — если Redis уже стоит для кеширования, никакого отдельного брокера поднимать не нужно. RabbitMQ выигрывает там, где нужны сложные маршрутизации: exchanges, routing keys, dead-letter queues. Kafka — это уже другая весовая категория, петабайтные потоки и long-term replay. Для типичного веб-приложения с нагрузкой 1–10K сообщений в секунду Redis Streams — оптимальный выбор по соотношению простоты и возможностей.

Если коротко — вот чеклист, который мы применяем на каждом проекте: 1) Bind только на внутренние интерфейсы, 0.0.0.0 — никогда; 2) ACL — отдельные пользователи с минимальными привилегиями, без общего requirepass; 3) TLS — шифрование трафика между клиентами и серверами; 4) Firewall — порты Redis открыты только для доверенных подсетей; 5) опасные команды отключены через rename-command: FLUSHALL, FLUSHDB, KEYS, CONFIG. Redis в интернете — это одна из самых частых причин компрометации серверов, которые мы видим на аудитах.

Зависит от масштаба. Для схемы master + 2 реплики + Sentinel, как в этой статье, нужны 3 сервера по 8–16 ГБ RAM — облачные VPS обойдутся от 5 000 руб/мес каждый. Работа инженеров АйТи Фреш — аудит, проектирование, внедрение, миграция — от 150 000 руб. В эту стоимость входят настройка мониторинга, документация и нормальная передача знаний команде клиента, чтобы они не звонили нам среди ночи с вопросом «а что это за алерт».

Нужна помощь с настройкой?

Команда АйТи Фреш настроит Redis Cluster под ваши задачи — 15 лет в деле, стоимость обслуживания от 15 000 ₽/мес

📞 Связаться с нами
#Redis кластер настройка#Redis Sentinel HA#Redis кеширование#Redis Streams очереди#maxmemory policy Redis#Redis TTL стратегии#Redis ACL безопасность#Redis мониторинг Prometheus