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%+. Данные постоянно вытеснялись из shared_buffers, потому что 280 ресторанов с меню из 50–200 позиций генерировали огромный объём читающих запросов.

Обоснование выбора 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 обеспечивает высокую доступность Redis. Если master падает, Sentinel автоматически промоутит одну из реплик в master за несколько секунд. Мы развернули три Sentinel-процесса — по одному на каждом Redis-сервере — для обеспечения кворума.

Конфигурация 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 секунд — на уровне Sentinel. Клиентская библиотека на 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 секунд каждый ключ получает TTL от 240 до 360 секунд.

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

Когда ресторан обновляет меню через панель, данные в кеше должны обновиться немедленно. Мы использовали 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 курьеров, начисление бонусов — всё это выполнялось через cron каждые 30 секунд. Это приводило к задержкам до 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, вместо единого пароля можно создавать отдельных пользователей с разными правами:

# Подключаемся к 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:

# Генерируем сертификаты (используем внутренний 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 — стандарт де-факто для мониторинга Redis через Prometheus:

# Устанавливаем 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 рабочих дней. Результаты замеров через 2 недели после запуска:

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

МетрикаДо 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 обеспечивает высокую доступность (HA) для схемы master-replica: мониторит master и автоматически промоутит реплику при его падении. Все данные помещаются на одном узле. Redis Cluster — это шардирование данных по нескольким узлам, когда данные не помещаются в память одного сервера. Sentinel подходит для объёмов до 50–100 ГБ, Cluster — для больших объёмов и горизонтального масштабирования.

Для большинства кеширующих сценариев подходит allkeys-lfu — она вытесняет наименее часто используемые ключи. Если все ключи имеют TTL, можно использовать volatile-lfu. Политика allkeys-lru (Least Recently Used) подходит, когда все ключи запрашиваются примерно с одинаковой частотой. Для очередей (Streams) никогда не используйте volatile-*, так как потеря сообщений критична — выделяйте отдельный инстанс Redis.

Redis Streams подходят для потоков до 50–100 тысяч сообщений в секунду, когда уже используется Redis для кеширования — не нужен отдельный брокер. RabbitMQ лучше для сложных маршрутизаций (exchanges, routing keys, dead-letter queues). Kafka — для петабайтных потоков с длительным хранением и 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 replicas + Sentinel, аналогичной описанной в статье, потребуются 3 сервера с 8–16 ГБ RAM каждый (от 5 000 руб/мес за облачные VPS). Работа инженеров АйТи Фреш по аудиту, проектированию, внедрению и миграции — от 150 000 руб. В эту сумму входит настройка мониторинга, документация и передача знаний команде клиента.

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

Специалисты АйТи Фреш помогут с внедрением и настройкой — 15+ лет опыта, обслуживание от 15 000 ₽/мес

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