DevOps

Redis Cluster и высокая доступность: настройка отказоустойчивого кэша для микросервисов

Ruslan Ismailov Опубликовано 14 мин чтения
R

Введение — зачем нужен Redis Cluster в микросервисной архитектуре

Микросервисная архитектура предполагает десятки, а иногда и сотни независимых сервисов, каждый из которых может обращаться к общему кэшу. Единственный инстанс Redis в такой среде становится узким местом: он не масштабируется горизонтально, не обеспечивает отказоустойчивость и ограничен объёмом памяти одного сервера.

Redis Cluster решает эти проблемы сразу: данные автоматически распределяются по нескольким узлам (шардирование), каждый мастер-узел имеет реплики для аварийного переключения, а клиенты умеют перенаправлять запросы при изменении топологии. Для высоконагруженных микросервисов, где требуется отказоустойчивый Redis с латентностью в миллисекундах, это стандартное production-решение.

Отличия Redis Sentinel от Redis Cluster: когда что использовать

Два основных механизма обеспечения высокой доступности Redis — это Redis Sentinel и Redis Cluster. Они решают разные задачи.

  • Redis Sentinel — система мониторинга и автоматического failover для единственного мастера. Sentinel следит за мастером, при его падении выбирает нового лидера из реплик и уведомляет клиентов. Данные не шардируются, весь датасет хранится в одном месте. Подходит для умеренных объёмов данных и простых сценариев.
  • Redis Cluster — горизонтально масштабируемый режим с автоматическим шардированием данных по нескольким мастерам. Каждый мастер имеет собственные реплики. Cluster поддерживает до 1000 узлов и обеспечивает линейный рост производительности по записи и чтению. Подходит для больших объёмов данных, высоких RPS и требований к горизонтальному масштабированию.

Когда выбирать Sentinel: объём данных помещается в одну машину, нужен простой failover, нет необходимости масштабировать запись.

Когда выбирать Cluster: данные не помещаются на один сервер, нужно горизонтальное масштабирование, высокие RPS на запись, микросервисная архитектура с несколькими независимыми доменами данных.

Архитектура Redis Cluster: шардирование, слоты, репликация

Hash Slots

Redis Cluster делит пространство ключей на 16384 слота (hash slots). Каждый ключ при помощи CRC16 хэша маппируется в один из слотов. Слоты равномерно распределяются между мастер-узлами. Например, в кластере из трёх мастеров:

  • Мастер 1: слоты 0–5460
  • Мастер 2: слоты 5461–10922
  • Мастер 3: слоты 10923–16383

При добавлении или удалении узлов слоты мигрируют без остановки кластера — это называется resharding.

Репликация

Каждый мастер-узел может иметь одну и более реплик. Репликация асинхронная. При падении мастера кластер автоматически проводит failover: реплика становится новым мастером. Для этого большинство мастер-узлов должны согласиться (кворум). Рекомендуемый минимум — 3 мастера + 3 реплики (6 узлов).

Команды внутри кластера

Если ключ находится на другом узле, Redis возвращает ответ MOVED или ASK, и клиент перенаправляет запрос. Умные клиенты кэшируют топологию кластера и сразу обращаются к нужному узлу.

Пошаговая настройка Redis Cluster в Docker и Kubernetes

Настройка в Docker

Создадим конфигурационный файл для каждого узла. Пример redis-7001.conf:

port 7001
cluster-enabled yes
cluster-config-file nodes-7001.conf
cluster-node-timeout 5000
appendonly yes
bind 0.0.0.0
protected-mode no

Аналогичные файлы создаём для портов 7002–7006. Запускаем контейнеры:

for port in 7001 7002 7003 7004 7005 7006; do
  docker run -d --name redis-$port \
    --net host \
    -v $(pwd)/redis-$port.conf:/usr/local/etc/redis/redis.conf \
    redis:7.2 redis-server /usr/local/etc/redis/redis.conf
done

Инициализируем кластер:

redis-cli --cluster create \
  127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 \
  127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 \
  --cluster-replicas 1

Флаг --cluster-replicas 1 означает одну реплику на каждый мастер. Redis CLI автоматически распределит узлы.

Настройка Redis Cluster в Kubernetes

Для Kubernetes используем StatefulSet и Headless Service, чтобы каждый Pod имел стабильное DNS-имя.

apiVersion: v1
kind: Service
metadata:
  name: redis-cluster
spec:
  clusterIP: None
  selector:
    app: redis-cluster
  ports:
    - port: 6379
      targetPort: 6379
    - port: 16379
      targetPort: 16379
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis-cluster
spec:
  serviceName: redis-cluster
  replicas: 6
  selector:
    matchLabels:
      app: redis-cluster
  template:
    metadata:
      labels:
        app: redis-cluster
    spec:
      containers:
        - name: redis
          image: redis:7.2
          ports:
            - containerPort: 6379
            - containerPort: 16379
          command: ["redis-server"]
          args:
            - --cluster-enabled
            - "yes"
            - --cluster-config-file
            - /data/nodes.conf
            - --cluster-node-timeout
            - "5000"
            - --appendonly
            - "yes"
          volumeMounts:
            - name: data
              mountPath: /data
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 1Gi

После деплоя инициализируем кластер, подключившись к одному из Podов:

kubectl exec -it redis-cluster-0 -- redis-cli --cluster create \
  $(kubectl get pods -l app=redis-cluster -o jsonpath='{range.items[*]}{.status.podIP}:6379 {end}') \
  --cluster-replicas 1

Паттерны работы с Redis Cluster из Go и PHP клиентов

Go: библиотека go-redis

Библиотека go-redis поддерживает Redis Cluster из коробки через redis.NewClusterClient.

package main

import (
    "context"
    "fmt"
    "github.com/redis/go-redis/v9"
)

func main() {
    rdb := redis.NewClusterClient(&redis.ClusterOptions{
        Addrs: []string{
            "redis-cluster-0.redis-cluster:6379",
            "redis-cluster-1.redis-cluster:6379",
            "redis-cluster-2.redis-cluster:6379",
        },
        // Автоматически обновлять слоты при изменении топологии
        RouteByLatency: true,
        ReadOnly:       true, // читать с реплик
    })

    ctx := context.Background()
    err := rdb.Set(ctx, "user:1001", "Alice", 0).Err()
    if err != nil {
        panic(err)
    }

    val, err := rdb.Get(ctx, "user:1001").Result()
    if err != nil {
        panic(err)
    }
    fmt.Println("user:1001 =>", val)
}

Опция ReadOnly: true позволяет направлять запросы на чтение к репликам, снижая нагрузку на мастеры. RouteByLatency выбирает ближайший узел по задержке.

PHP: библиотека Predis и phpredis

В PHP наиболее популярны два клиента: Predis и расширение phpredis.

Predis:

<?php
require 'vendor/autoload.php';

$client = new Predis\Client(
    [
        ['host' => 'redis-cluster-0.redis-cluster', 'port' => 6379],
        ['host' => 'redis-cluster-1.redis-cluster', 'port' => 6379],
        ['host' => 'redis-cluster-2.redis-cluster', 'port' => 6379],
    ],
    [
        'cluster' => 'redis',
        'parameters' => [
            'password' => null,
        ],
    ]
);

$client->set('order:555', json_encode(['status' => 'pending']));
$value = $client->get('order:555');
echo $value;

phpredis (расширение):

<?php
$redis = new RedisCluster(null, [
    'redis-cluster-0.redis-cluster:6379',
    'redis-cluster-1.redis-cluster:6379',
    'redis-cluster-2.redis-cluster:6379',
]);

$redis->set('session:abc123', 'user_data');
echo $redis->get('session:abc123');

Важный нюанс в PHP: при использовании multi-key операций (MGET, MSET) все ключи должны хэшироваться в один слот. Для этого используют hash tags — часть ключа в фигурных скобках: {user:1001}:profile, {user:1001}:settings. Кластер хэширует только часть в скобках, гарантируя попадание в один слот.

Обработка failover и переподключения на стороне клиента

Failover в Redis Cluster происходит за 5–15 секунд (зависит от cluster-node-timeout). В этот период часть запросов будет завершаться ошибками. Клиент должен корректно обрабатывать такие ситуации.

Стратегии обработки ошибок

  • Retry с экспоненциальной задержкой: при получении ошибки CLUSTERDOWN или LOADING — повторить запрос через нарастающие интервалы (100ms, 200ms, 400ms...).
  • Circuit Breaker: если процент ошибок превышает порог, временно прекратить запросы к Redis и использовать fallback (например, пустой ответ или обращение к БД).
  • Обновление топологии: при получении MOVED клиент должен немедленно запросить актуальную топологию через CLUSTER SLOTS или CLUSTER SHARDS.

Пример обработки в Go:

func getWithRetry(ctx context.Context, rdb *redis.ClusterClient, key string) (string, error) {
    var val string
    var err error
    for i := 0; i < 3; i++ {
        val, err = rdb.Get(ctx, key).Result()
        if err == nil {
            return val, nil
        }
        if errors.Is(err, redis.Nil) {
            return "", nil // ключ не найден — это не ошибка
        }
        time.Sleep(time.Duration(100*(i+1)) * time.Millisecond)
    }
    return "", fmt.Errorf("redis unavailable after retries: %w", err)
}

Мониторинг Redis Cluster: метрики, алерты, инструменты

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

  • cluster_state — должен быть ok. Любое другое значение требует немедленного алерта.
  • cluster_slots_fail — количество слотов в состоянии FAIL. Должно быть 0.
  • connected_slaves — количество подключённых реплик для каждого мастера.
  • used_memory vs maxmemory — заполненность памяти. При превышении 80% — алерт.
  • instantaneous_ops_per_sec — текущий RPS.
  • rejected_connections, evicted_keys — признаки перегрузки.
  • keyspace_hits / keyspace_misses — hit rate кэша. Должен быть выше 90%.

Инструменты мониторинга

  • Redis Exporter + Prometheus + Grafana — стандартный стек. Деплоим oliver006/redis_exporter как sidecar или отдельный Deployment, настраиваем scrape в Prometheus и используем готовые Grafana dashboards (например, ID 763).
  • redis-cli cluster info — быстрая ручная диагностика: redis-cli -c -h redis-cluster-0.redis-cluster cluster info.
  • redis-cli --cluster check — проверка целостности кластера: redis-cli --cluster check redis-cluster-0.redis-cluster:6379.
  • RedisInsight — официальный GUI-инструмент от Redis Ltd. с визуализацией топологии, медленных запросов и памяти.

Пример Prometheus-алерта

groups:
  - name: redis_cluster
    rules:
      - alert: RedisClusterDown
        expr: redis_cluster_state != 1
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Redis Cluster не в состоянии OK"
      - alert: RedisMemoryHigh
        expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Использование памяти Redis превышает 85%"

Антипаттерны и частые ошибки при работе с Redis Cluster

  1. Использование команд, не поддерживаемых в Cluster. Команды, работающие с несколькими ключами из разных слотов (MGET, SUNION, EVAL с несколькими ключами), выбросят ошибку. Решение: использовать hash tags для группировки ключей в один слот, или переработать логику.
  2. Хранение «горячих» ключей на одном слоте. Если один ключ получает 90% нагрузки, его мастер станет узким местом. Решение: шардировать горячие ключи вручную (user:counter:1, user:counter:2...).
  3. Игнорирование ответов MOVED/ASK. Примитивные клиенты без поддержки Cluster будут получать ошибки при обращении к слотам, лежащим на другом узле. Всегда используйте клиентские библиотеки с нативной поддержкой Redis Cluster.
  4. Отсутствие реплик. Запуск кластера без реплик (--cluster-replicas 0) означает, что потеря любого мастера приводит к потере данных и деградации кластера. В production — всегда минимум одна реплика.
  5. Слишком маленький cluster-node-timeout. Слишком агрессивный таймаут (менее 2000ms) вызывает ложные failover из-за кратковременных сетевых задержек. Рекомендуемое значение — 5000–15000ms.
  6. Lua-скрипты с ключами из разных слотов. EVAL в Redis Cluster требует, чтобы все ключи находились в одном слоте. Нарушение этого правила вызовет ошибку CROSSSLOT.
  7. Отсутствие мониторинга cluster_state. Кластер может перейти в состояние FAIL незаметно, если не настроены алерты. Мониторинг — обязателен в production.

Заключение

Redis Cluster — зрелое и надёжное решение для организации отказоустойчивого кэша в микросервисной архитектуре. Автоматическое шардирование по 16384 слотам, встроенный failover и горизонтальное масштабирование делают его выбором для highload-систем, где единственный инстанс Redis уже не справляется.

При правильной настройке — с корректным количеством реплик, грамотными клиентами на Go или PHP, полноценным мониторингом через Prometheus и Grafana, а также осознанным применением hash tags — Redis Cluster обеспечивает надёжную основу для кэширования в Kubernetes-окружениях и распределённых системах.

Ключевые рекомендации для production: минимум 3 мастера + 3 реплики, cluster-node-timeout не менее 5000ms, мониторинг cluster_state и used_memory, retry-логика на стороне клиента, hash tags для multi-key операций. Следуя этим принципам, вы получите стабильный, масштабируемый и отказоустойчивый Redis для ваших микросервисов.

Технологии

Теги

Руслан Исмаилов

Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →