Redis Sentinel vs Redis Cluster: как мы выбрали архитектуру для 1 миллиона кэш-запросов в секунду

Ситуация: один Redis на всю рекламную платформу

«АдСервер» — платформа для показа рекламы в реальном времени (RTB — Real-Time Bidding). 1 миллион запросов к Redis в секунду: чтение профилей пользователей, частоты показов, таргетинговых правил. Один Redis 7.2 на сервере с 128 GB RAM — 90 GB данных в памяти.

Проблемы:

  • Единая точка отказа — падение Redis = остановка показа рекламы = потеря выручки $15 000/час.
  • Память на пределе — 90 GB из 110 GB (maxmemory). Через 2-3 месяца данные перестанут помещаться.
  • Однопоточность — Redis single-threaded. При 1M ops/sec на одном ядре CPU-bound операции (сложные Lua-скрипты, SORT) блокируют весь сервер.
  • Латентность при persistence — BGSAVE (RDB snapshot) раз в час вызывал fork, который копировал 90 GB page tables, создавая латентность-спайк на 200-500 мс.

Команда «АдСервер» обратилась к нам в itfresh.ru с вопросом: Sentinel или Cluster? Мы разобрали оба варианта.

Redis Sentinel: master-slave + автоматический failover

Redis Sentinel — надстройка над обычной master-replica репликацией. Sentinel-процессы мониторят master и при его недоступности промотируют одну из реплик.

Архитектура Sentinel:

# Топология Redis Sentinel

# Redis master + 2 replicas
redis-master    (10.10.1.1:6379)  — 128 GB RAM
redis-replica1  (10.10.1.2:6379)  — 128 GB RAM
redis-replica2  (10.10.1.3:6379)  — 128 GB RAM

# 3 Sentinel процесса (на тех же серверах)
sentinel1 (10.10.1.1:26379)
sentinel2 (10.10.1.2:26379)
sentinel3 (10.10.1.3:26379)

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

# /etc/redis/sentinel.conf
port 26379
sentinel monitor adserver-master 10.10.1.1 6379 2
sentinel auth-pass adserver-master strong_password_here
sentinel down-after-milliseconds adserver-master 5000
sentinel failover-timeout adserver-master 30000
sentinel parallel-syncs adserver-master 1

# 2 — кворум: сколько Sentinel должны согласиться, что master недоступен
# down-after-milliseconds 5000 — master считается down после 5 сек без ответа
# failover-timeout 30000 — максимальное время на failover
# parallel-syncs 1 — сколько реплик одновременно синхронизируются с новым master

Конфигурация Redis master:

# /etc/redis/redis.conf (master)
bind 0.0.0.0
port 6379
requirepass strong_password_here
masterauth strong_password_here

# Память
maxmemory 110gb
maxmemory-policy allkeys-lfu

# Persistence
save 3600 1
save 300 100
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /var/lib/redis

# AOF (для минимизации потерь при failover)
appendonly yes
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 512mb

# Репликация
min-replicas-to-write 1
min-replicas-max-lag 10
repl-backlog-size 512mb

Плюсы Sentinel:

  • Простая конфигурация — настроить за 30 минут
  • Все данные на одном master — нет проблем с multi-key операциями
  • Клиентские библиотеки отлично поддерживают Sentinel

Минусы для «АдСервер»:

  • Не решает проблему памяти — master по-прежнему хранит все 90 GB
  • Не решает проблему CPU — все writes на одном ядре
  • Replica читают, но при 1M ops/sec одного master мало

Redis Cluster: шардирование + HA

Redis Cluster распределяет данные по нескольким master-нодам через hash slots (16384 слота). Каждая нода владеет частью слотов и имеет свои реплики.

# Создаём кластер из 6 нод: 3 master + 3 replica
# Каждый master — 64 GB RAM, итого 192 GB для данных

redis-cli --cluster create \
  10.10.1.1:6379 10.10.1.2:6379 10.10.1.3:6379 \
  10.10.1.4:6379 10.10.1.5:6379 10.10.1.6:6379 \
  --cluster-replicas 1 \
  -a strong_password_here

# Результат:
# Master[0] 10.10.1.1:6379 slots: 0-5460    (5461 slots)
# Master[1] 10.10.1.2:6379 slots: 5461-10922 (5462 slots)
# Master[2] 10.10.1.3:6379 slots: 10923-16383 (5461 slots)
# Replica[0] 10.10.1.4:6379 -> Master[0]
# Replica[1] 10.10.1.5:6379 -> Master[1]
# Replica[2] 10.10.1.6:6379 -> Master[2]

Конфигурация Redis Cluster ноды:

# /etc/redis/redis.conf (каждая нода кластера)
bind 0.0.0.0
port 6379
requirepass strong_password_here
masterauth strong_password_here

# Cluster
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-require-full-coverage no
cluster-allow-reads-when-down yes

# Память — на каждой ноде по 60 GB (из 64 GB)
maxmemory 60gb
maxmemory-policy allkeys-lfu

# Persistence
save 3600 1
appendonly yes
appendfsync everysec

# Performance
io-threads 4
io-threads-do-reads yes
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes

Как Redis Cluster определяет, на какой ноде хранится ключ:

# Slot = CRC16(key) mod 16384
# Пример:
# CRC16("user:12345") mod 16384 = 8923 → Master[1] (slots 5461-10922)
# CRC16("user:67890") mod 16384 = 3201 → Master[0] (slots 0-5460)

# Если клиент обращается к неправильной ноде:
# -MOVED 8923 10.10.1.2:6379
# Клиент перенаправляется на правильную ноду и кэширует маршрут

# Hash tags — для multi-key операций с одинаковым slot
# {user:123}.profile и {user:123}.sessions попадут на один слот
# потому что hash считается только для части в фигурных скобках

Почему мы выбрали Redis Cluster для АдСервер

Сравнительная таблица для задачи «АдСервер»:

КритерийSentinelCluster
Горизонтальное масштабированиеНет (один master)Да (N masters)
Макс. памятьОграничена одним серверомN × память одной ноды
Write throughput1 ядро CPUN ядер (по одному на master)
Multi-key операцииБез ограниченийТолько в одном slot (hash tags)
Failover время10-30 сек5-15 сек
Клиентская поддержкаОтличнаяХорошая (но сложнее)
Сложность эксплуатацииНизкаяСредняя

Для «АдСервер» Redis Cluster — единственный вариант: нужно масштабирование и по памяти (90 GB → 180 GB), и по CPU (3 master вместо 1).

Основная сложность — рефакторинг multi-key операций. В приложении было 14 мест с MGET/MSET по ключам разных пользователей. Мы переписали их с использованием hash tags:

# До (не работает в Cluster — ключи на разных слотах):
MGET user:123:freq user:456:freq user:789:freq

# После — pipeline с индивидуальными запросами:
import redis
from redis.cluster import RedisCluster

rc = RedisCluster(
    host='10.10.1.1',
    port=6379,
    password='strong_password_here',
    decode_responses=True,
    read_from_replicas=True
)

# Pipeline автоматически группирует команды по слотам
with rc.pipeline() as pipe:
    for user_id in user_ids:
        pipe.get(f"user:{user_id}:freq")
    results = pipe.execute()

# Для данных одного пользователя — hash tags:
# {user:123}:profile, {user:123}:freq, {user:123}:targeting
# Все на одном слоте → MGET работает
rc.mget("{user:123}:profile", "{user:123}:freq", "{user:123}:targeting")

Resharding — добавление нового master без даунтайма:

# Добавляем новую ноду в кластер
redis-cli --cluster add-node 10.10.1.7:6379 10.10.1.1:6379 \
  -a strong_password_here

# Перебалансируем слоты
redis-cli --cluster reshard 10.10.1.1:6379 \
  --cluster-from all \
  --cluster-to  \
  --cluster-slots 4096 \
  --cluster-yes \
  -a strong_password_here

# Добавляем реплику для нового master
redis-cli --cluster add-node 10.10.1.8:6379 10.10.1.7:6379 \
  --cluster-slave --cluster-master-id  \
  -a strong_password_here

Memory optimization и persistence tuning

90 GB данных в Redis — это дорого. Мы оптимизировали потребление памяти:

# Анализ потребления памяти по типам данных
redis-cli -a strong_password_here --bigkeys
# Biggest string:  user:8834521:profile (4.2 KB)
# Biggest hash:    campaign:1247:targeting (12.3 KB)
# Biggest set:     segment:premium_users (890 KB, 145000 members)
# Biggest zset:    leaderboard:daily (2.1 MB, 500000 members)

redis-cli -a strong_password_here MEMORY DOCTOR
# Sam, I have a few reports for you...
# Peak memory: 92.4 GB, current: 89.7 GB
# Keys with TTL: 67%, Keys without TTL: 33%
# Recommendation: review keys without TTL

Оптимизации, которые мы применили:

# 1. maxmemory-policy: allkeys-lfu вместо volatile-lru
# LFU (Least Frequently Used) лучше для кэша — сохраняет популярные ключи
maxmemory-policy allkeys-lfu
lfu-log-factor 10
lfu-decay-time 1

# 2. Сжатие строк — хранение JSON в MessagePack
import msgpack
import json

# JSON: 847 байт
profile_json = json.dumps(user_profile)
# MessagePack: 534 байта (экономия 37%)
profile_mp = msgpack.packb(user_profile)
rc.set(f"user:{uid}:profile", profile_mp, ex=3600)

# 3. Hash ziplist — для маленьких хэшей Redis использует компактный формат
hash-max-ziplist-entries 128
hash-max-ziplist-value 64

# 4. TTL на все ключи — нет бессмертных данных в кэше
rc.set(f"user:{uid}:freq", freq_data, ex=86400)  # 24 часа
rc.set(f"campaign:{cid}:targeting", data, ex=3600)  # 1 час

Persistence tuning — минимизируем влияние на латентность:

# RDB: только раз в час (или отключаем на мастерах)
save 3600 1
stop-writes-on-bgsave-error no
rdb-del-sync-files no

# AOF: everysec — компромисс между durability и performance
appendonly yes
appendfsync everysec
no-appendfsync-on-rewrite yes  # не fsync при BGSAVE
aof-rewrite-incremental-fsync yes

# Lazy free: удаление больших ключей в фоне
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
replica-lazy-flush yes

# I/O threads (Redis 7+) — параллельное чтение/запись
io-threads 4
io-threads-do-reads yes

Результат оптимизации: потребление памяти снизилось с 90 GB до 62 GB (31% экономия), латентность при BGSAVE исчезла (вместо 200-500 мс спайков).

Failover testing и мониторинг

Тестируем failover Redis Cluster:

# Проверяем состояние кластера
redis-cli -a strong_password_here cluster info
# cluster_state:ok
# cluster_slots_assigned:16384
# cluster_slots_ok:16384
# cluster_known_nodes:6
# cluster_size:3

redis-cli -a strong_password_here cluster nodes
# abc123 10.10.1.1:6379@16379 master - 0 1712300000000 1 connected 0-5460
# def456 10.10.1.2:6379@16379 master - 0 1712300000000 2 connected 5461-10922
# ghi789 10.10.1.3:6379@16379 master - 0 1712300000000 3 connected 10923-16383
# jkl012 10.10.1.4:6379@16379 slave abc123 0 1712300000000 1 connected
# ...

# Убиваем один master
ssh 10.10.1.1 "redis-cli -a strong_password_here DEBUG SLEEP 30"

# Через 5 секунд (cluster-node-timeout):
# Replica 10.10.1.4 промотируется в master
# Клиент получает -MOVED и переподключается
# Общее время failover: 5-8 секунд
# Потерянных запросов: 0 (клиент с retry)

Мониторинг через redis_exporter + Prometheus:

# Ключевые метрики для Redis Cluster
groups:
  - name: redis_cluster
    rules:
      - alert: RedisClusterStateNotOK
        expr: redis_cluster_state == 0
        for: 30s
        labels:
          severity: critical
        annotations:
          summary: "Redis Cluster state is not OK on {{ $labels.instance }}"

      - alert: RedisMemoryUsageHigh
        expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Redis memory >85% on {{ $labels.instance }}"

      - alert: RedisConnectedClientsHigh
        expr: redis_connected_clients > 5000
        for: 5m
        labels:
          severity: warning

      - alert: RedisKeyEviction
        expr: rate(redis_evicted_keys_total[5m]) > 100
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Redis evicting >100 keys/sec on {{ $labels.instance }}"

Useful redis-cli commands для оперативной диагностики:

# Общая информация
redis-cli -a pass INFO all | grep -E "used_memory_human|connected_clients|ops_per_sec|hit_rate"

# Latency мониторинг
redis-cli -a pass --latency-history -i 5
# min: 0, max: 1, avg: 0.32 (1000 samples) -- 5.00 seconds range

# Slowlog
redis-cli -a pass SLOWLOG GET 10
# 1) 1) (integer) 142
#    2) (integer) 1712300000
#    3) (integer) 15234  # 15 мс
#    4) 1) "KEYS"
#       2) "user:*"    # KEYS в продакшене = катастрофа

# Мониторинг клиентов
redis-cli -a pass CLIENT LIST | head -5

Результаты

Миграция с одиночного Redis на Redis Cluster из 6 нод (3 master + 3 replica) заняла 2 недели, включая рефакторинг 14 multi-key операций в приложении.

МетрикаДо (один Redis)После (Redis Cluster 3 master)
Макс. ops/sec1M (потолок CPU)3M+ (линейно масштабируется)
Latency p992.1 мс (с спайками до 500 мс)0.8 мс (стабильно)
Доступная память110 GB180 GB (расширяемо)
Failover времяРучной (10+ минут)Автоматический (5-8 сек)
Потеря данных при аварииДо 1 часа (RDB)До 1 секунды (AOF everysec)
Потребление памяти90 GB62 GB (после оптимизации)

Redis Cluster — правильный выбор для высоконагруженных проектов, где нужно масштабирование и по памяти, и по CPU. Sentinel проще, но ограничен одним master. Если ваш Redis-сервер приближается к потолку — обращайтесь к нам в itfresh.ru.

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

Twemproxy (nutcracker) — прокси для шардирования Redis на стороне клиента. Он проще Redis Cluster, но не обеспечивает автоматический failover (нужен дополнительный Sentinel), не поддерживает resharding без даунтайма и добавляет 0.5-1 мс латентности. Для новых проектов рекомендуем Redis Cluster — он встроен в Redis и активно развивается. Twemproxy оправдан, если у вас уже legacy-архитектура с ним.
Для кэша общего назначения — allkeys-lfu (Least Frequently Used). Он сохраняет наиболее популярные ключи, вытесняя редко используемые. Для кэша с TTL на всех ключах — volatile-lfu. allkeys-lru (Least Recently Used) хуже для рекламы и контента, где есть hot keys — LRU может вытеснить популярный ключ, если к нему не обращались 5 минут во время пика.
Для кэша, который можно восстановить из основной БД — только RDB раз в час (или отключить persistence). Для данных, которые живут только в Redis (сессии, counters) — AOF с appendfsync everysec. Комбинация RDB + AOF даёт лучшее из двух миров: RDB для быстрого старта, AOF для минимальных потерь. Redis 7+ поддерживает Multi Part AOF, который решает проблему с большими rewrite.
Поэтапно: 1) Поднимаем Redis Cluster и синхронизируем данные через redis-shake или RIOT. 2) Переключаем приложение на чтение из Cluster (dual-read). 3) Переключаем запись на Cluster. 4) Отключаем старый Redis. Весь процесс занимает 2-4 часа без даунтайма. Главная сложность — рефакторинг multi-key операций, это нужно сделать заранее.
Минимум 3 master (для кворума). Для большинства проектов 3-6 master достаточно. Каждый master обрабатывает 200-400K ops/sec на одном ядре. Если нужно 1M ops/sec — 3 master хватит с запасом. Увеличивайте количество master-нод, когда данные не помещаются в RAM одной ноды или нужна ещё большая пропускная способность записи.

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

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#Redis#Sentinel#Redis Cluster#failover#hash slots#resharding#maxmemory-policy#RDB
Комментарии 0

Оставить комментарий

загрузка...