一、Redis 哨兵集群是什么?

Redis 主从复制保证了数据的冗余备份,但如果主节点挂了,需要人工介入手动切换——在生产环境这是不可接受的。Redis Sentinel(哨兵)就是来解决这个问题的:

  • 监控(Monitoring):持续监控主从节点是否存活
  • 自动故障转移(Automatic Failover):主节点宕机后,自动选举新主并重新配置从节点
  • 通知(Notification):故障转移后通知客户端新的主节点地址

Redis Sentinel vs Redis Cluster

对比项 Sentinel(哨兵) Cluster(集群)
数据分片 无,所有数据在同一实例 有,16384 个槽自动分片
高可用 ✅ 自动故障转移 ✅ 故障自动分片迁移
写能力 只有主节点可写 每个分片主节点可写
适用场景 单节点写压力、数据量适中 数据量大、需要水平扩展
最小节点 1 主 + 2 从 + 3 哨兵 = 6 个 至少 6 个节点(3 主 3 从)

本文聚焦哨兵模式,适合不需要数据分片、但必须保证高可用的业务场景。

二、整体架构设计

生产环境推荐的最小高可用架构:

🟢 Redis Master(主节点)—— 接收读写请求

🔵 Redis Slave x2(从节点)—— 异步复制 + 读请求分发

🟡 Sentinel x3(哨兵节点)—— 监控 + 投票 + 故障转移

(哨兵节点应分布在不同物理机/可用区,避免单点)

K8s 部署拓扑

  • 1 个 Master StatefulSet(1 副本,固定域名 redis-master)
  • 2 个 Slave StatefulSet(2 副本,通过 Service 分发读请求)
  • 3 个 Sentinel Deployment(3 副本,共享同一个 Service)
  • 3 个 Headless Service(稳定 Pod DNS)
  • 1 个 Master Service(固定指向当前主节点)
  • 1 个 Slave Service(负载均衡所有从节点)
  • 1 个 Sentinel Service(客户端连接哨兵用)

三、配置文件准备

3.1 Redis Master 配置

📄 redis-master.conf

bind 0.0.0.0
port 6379
protected-mode no
daemonize no
pidfile /var/run/redis.pid
loglevel notice
logfile ""

# 持久化(AOF + RDB 混合)
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec

# RDB 快照
save 900 1
save 300 10
save 60 10000
rdbcompression yes
rdbchecksum yes

# 连接数
maxclients 10000
timeout 300

# 内存(根据节点规格调整)
maxmemory 2gb
maxmemory-policy allkeys-lru

3.2 Redis Slave 配置

📄 redis-slave.conf

bind 0.0.0.0
port 6379
protected-mode no
daemonize no
pidfile /var/run/redis.pid
loglevel notice
logfile ""

# 从节点配置
replica-read-only yes
repl-diskless-sync yes    # 无盘复制,减少 IO
repl-diskless-sync-delay 5

# 指向主节点(会被动态覆盖,但需要默认值)
replicaof redis-master 6379

# 持久化
appendonly yes
appendfilename "appendonly.aof"
maxmemory 2gb
maxmemory-policy allkeys-lru

3.3 Sentinel 配置(核心)

📄 sentinel.conf

port 26379
protected-mode no
daemonize no
pidfile /var/run/redis-sentinel.pid
loglevel notice
logfile ""

# 监控的主节点配置
# sentinel monitor    
# quorum = 哨兵投票数,超过半数即认定主节点宕机
sentinel monitor mymaster redis-master 6379 2

# 主节点无响应多久后认定宕机(毫秒)
sentinel down-after-milliseconds mymaster 5000

# 故障转移超时时间
sentinel failover-timeout mymaster 180000

# 故障转移后并行同步的从节点数量(越多越快,但压力大)
sentinel parallel-syncs mymaster 1

# 通知脚本(可选)
sentinel notification-script mymaster /opt/notify.sh



⚠️ quorum 解读:假设有 3 个哨兵节点,quorum=2 表示至少 2 个哨兵认为主节点挂了,才触发故障转移。推荐哨兵数量 N,quorum 设置为 (N/2+1)。

四、K8s 资源编排

4.1 ConfigMap(统一管理配置)

📄 redis-configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: redis-config
  namespace: redis
data:
  redis-master.conf: |
    bind 0.0.0.0
    port 6379
    protected-mode no
    daemonize no
    appendonly yes
    maxmemory 2gb
    maxmemory-policy allkeys-lru
  redis-slave.conf: |
    bind 0.0.0.0
    port 6379
    protected-mode no
    daemonize no
    replica-read-only yes
    repl-diskless-sync yes
    replicaof redis-master 6379
    appendonly yes
    maxmemory 2gb
    maxmemory-policy allkeys-lru
  sentinel.conf: |
    port 26379
    protected-mode no
    daemonize no
    sentinel monitor mymaster redis-master 6379 2
    sentinel down-after-milliseconds mymaster 5000
    sentinel failover-timeout mymaster 180000
    sentinel parallel-syncs mymaster 1

4.2 Master StatefulSet

📄 redis-master-sts.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis-master
  namespace: redis
spec:
  serviceName: redis-master
  replicas: 1
  selector:
    matchLabels:
      app: redis
      role: master
  template:
    metadata:
      labels:
        app: redis
        role: master
    spec:
      initContainers:
      - name: init
        image: redis:7-alpine
        command: ["sh", "-c", "cp /config/redis-master.conf /redis.conf"]
        volumeMounts:
          - name: conf
            mountPath: /config
          - name: redis-config
            mountPath: /redis.conf
              subPath: redis-master.conf
      containers:
      - name: redis
        image: redis:7-alpine
        command: ["redis-server", "/redis.conf"]
        ports:
        - containerPort: 6379
        volumeMounts:
          - name: data
            mountPath: /data
          - name: conf
            mountPath: /config
      volumes:
      - name: conf
        emptyDir: {}
      - name: redis-config
        configMap:
          name: redis-config
            items:
            - key: redis-master.conf
              path: redis-master.conf

4.3 Slave StatefulSet(带 initContainers 自动配置 replicaof)

📄 redis-slave-sts.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis-slave
  namespace: redis
spec:
  serviceName: redis-slave
  replicas: 2
  selector:
    matchLabels:
      app: redis
      role: slave
  template:
      metadata:
        labels:
        app: redis
        role: slave
      spec:
        initContainers:
        - name: init
          image: redis:7-alpine
          command: ["sh", "-c", "cp /config/redis-slave.conf /redis.conf && echo 'replicaof redis-master 6379' >> /redis.conf"]
          volumeMounts:
            - name: conf
              - name: redis-config
                mountPath: /config
                mountPath: /redis.conf
                  subPath: redis-slave.conf
        containers:
        - name: redis
          image: redis:7-alpine
          command: ["redis-server", "/redis.conf"]
          ports:
          - containerPort: 6379
          volumeMounts:
          - name: data
            mountPath: /data
          - name: conf
            mountPath: /config
        volumes:
        - name: conf
          emptyDir: {}
        - name: redis-config
          configMap:
            name: redis-config

4.4 Sentinel Deployment

📄 redis-sentinel-deploy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-sentinel
  namespace: redis
spec:
  replicas: 3
  selector:
    matchLabels:
      app: redis-sentinel
  template:
    metadata:
      labels:
        app: redis-sentinel
    spec:
      initContainers:
      - name: init
        image: redis:7-alpine
        command: ["sh", "-c", "cp /config/sentinel.conf /sentinel.conf"]
        volumeMounts:
          - name: conf
            mountPath: /config
          - name: sentinel-conf
            mountPath: /sentinel.conf
              subPath: sentinel.conf
      containers:
      - name: sentinel
        image: redis:7-alpine
        command: ["redis-sentinel", "/sentinel.conf"]
        ports:
        - containerPort: 26379
        volumeMounts:
          - name: conf
            mountPath: /config
        volumes:
      - name: conf
        emptyDir: {}
      - name: sentinel-conf
        configMap:
          name: redis-config

4.5 Service 配置

📄 redis-services.yaml

---
# Master Headless Service(哨兵需要通过 DNS 发现 master)
apiVersion: v1
kind: Service
metadata:
  name: redis-master
  namespace: redis
spec:
  type: ClusterIP
  clusterIP: None    # Headless
  selector:
    app: redis
    role: master
  ports:
    - port: 6379
      targetPort: 6379
---
# Slave Service(读请求分发)
apiVersion: v1
kind: Service
metadata:
  name: redis-slave
  namespace: redis
spec:
  type: ClusterIP
  selector:
    app: redis
    role: slave
  ports:
    - port: 6379
      targetPort: 6379
---
# Sentinel Service(客户端连接哨兵用)
apiVersion: v1
kind: Service
metadata:
  name: redis-sentinel
  namespace: redis
spec:
  type: ClusterIP
  selector:
    app: redis-sentinel
  ports:
    - port: 26379
      targetPort: 26379

五、一键部署

📋 部署命令

# 1. 创建命名空间
kubectl create ns redis

# 2. 部署 ConfigMap
kubectl apply -f redis-configmap.yaml -n redis

# 3. 部署 Master
kubectl apply -f redis-master-sts.yaml -n redis

# 4. 部署 Slave
kubectl apply -f redis-slave-sts.yaml -n redis

# 5. 部署 Sentinel
kubectl apply -f redis-sentinel-deploy.yaml -n redis

# 6. 部署 Service
kubectl apply -f redis-services.yaml -n redis

# 7. 验证部署
kubectl get pods -n redis -o wide
kubectl get svc -n redis

六、验证集群状态

6.1 验证主从复制

# 连接到 Master,写入数据
kubectl exec -it redis-master-0 -n redis -- redis-cli -p 6379 SET test-key "Hello Sentinel" GET test-key

# 连接到 Slave,验证复制成功
kubectl exec -it redis-slave-0 -n redis -- redis-cli -p 6379 GET test-key
# 预期输出:Hello Sentinel

6.2 验证哨兵集群状态

# 查看哨兵感知到的主节点信息
kubectl exec -it $(kubectl get pod -l app=redis-sentinel -n redis -o jsonpath='{.items[0].metadata.name}') -n redis -- redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster

# 查看所有哨兵节点
kubectl exec -it $(kubectl get pod -l app=redis-sentinel -n redis -o jsonpath='{.items[0].metadata.name}') -n redis -- redis-cli -p 26379 SENTINEL masters

# 查看从节点列表
kubectl exec -it $(kubectl get pod -l app=redis-sentinel -n redis -o jsonpath='{.items[0].metadata.name}') -n redis -- redis-cli -p 26379 SENTINEL slaves mymaster

6.3 验证集群整体健康

# 查看所有 Pod 状态
kubectl get pods -n redis -l 'app in (redis,redis-sentinel)'

# 查看 Redis info
kubectl exec -it redis-master-0 -n redis -- redis-cli -p 6379 INFO replication

# 查看 Sentinel 感知的主节点 IP
kubectl exec -it $(kubectl get pod -l app=redis-sentinel -n redis -o jsonpath='{.items[0].metadata.name}') -n redis -- redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster

七、故障转移实战演示

模拟 Master 宕机

📋 Step 1: 记录当前主节点

# 记录当前主节点 IP
kubectl exec -it $(kubectl get pod -l app=redis-sentinel -n redis -o jsonpath='{.items[0].metadata.name}') -n redis -- redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster

📋 Step 2: 注入故障(杀死 Master Pod)

kubectl delete pod redis-master-0 -n redis
kubectl get pods -n redis -w

📋 Step 3: 观察故障转移

# 等待 5-10 秒后,查看主节点是否已切换
kubectl exec -it $(kubectl get pod -l app=redis-sentinel -n redis -o jsonpath='{.items[0].metadata.name}') -n redis -- redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster

# 查看哪个 Pod 被选为新主(role=master)
for p in $(kubectl get pods -n redis -l app=redis -o jsonpath='{.items[*].metadata.name}'); do
  echo "=== $p ===" && kubectl exec -it $p -n redis -- redis-cli -p 6379 ROLE 2>/dev/null
done

📋 Step 4: 验证数据完整性

# 旧主节点重新上线后变为从节点
kubectl exec -it redis-master-0 -n redis -- redis-cli -p 6379 ROLE
# 预期输出:master 或 slave

# 验证数据是否丢失(好的故障转移应该零数据丢失)
kubectl exec -it redis-master-0 -n redis -- redis-cli -p 6379 GET test-key

八、客户端正确使用姿势

8.1 Python 客户端(正确连接哨兵)

📄 redis_client.py

from redis.sentinel import Sentinel

# 连接哨兵集群(填写 K8s Service 地址)
sentinel = Sentinel(
  [('redis-sentinel.redis.svc.cluster.local', 26379)],
  socket_timeout=0.1
)

# 获取当前主节点连接(用于写操作)
master = sentinel.master_for('mymaster', socket_timeout=1)

# 获取从节点连接(用于读操作)
slave = sentinel.slave_for('mymaster', socket_timeout=1)

# 写操作走主节点
master.set('key1', 'value1')

# 读操作走从节点(负载均衡)
result = slave.get('key1')

# 自动获取最新主节点(故障转移后无需改配置)
# Sentinel 会感知主节点变化,master_for 自动返回新主

8.2 Java 客户端(Jedis)

📄 RedisSentinelClient.java

import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisSentinelPool;

JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(100);
poolConfig.setMaxIdle(20);

Set sentinels = new HashSet<>();
sentinels.add("redis-sentinel.redis.svc.cluster.local:26379");

JedisSentinelPool pool = new JedisSentinelPool(
  "mymaster",               # 与哨兵配置中的 master-name 一致
  sentinels,
  poolConfig,
  3000                  # 连接超时
);

// 获取连接(自动感知主节点变化)
Jedis jedis = pool.getResource();
jedis.set("hello", "world");
String value = jedis.get("hello");
jedis.close();    // 归还连接池,不是关闭!

✅ 正确做法:客户端始终连接哨兵集群,由哨兵返回当前主节点地址。故障转移后哨兵会自动更新,客户端无需重启或改配置。

九、生产环境高级配置

9.1 数据持久化(推荐 AOF + RDB 混合)

# 推荐持久化配置(AOF everysec + RDB 快照)
appendonly yes
appendfsync everysec        # 性能与安全的平衡
save 900 1                 # 15分钟至少1个key变化
save 300 10                # 5分钟至少10个key变化
save 60 10000              # 1分钟至少10000个key变化

9.2 资源限制(生产必须设置)

📄 redis-master-sts-resources.yaml(添加资源限制)

# 在 containers 下添加 resources 字段
resources:
  requests:
    cpu: "500m"
    memory: "2Gi"
  limits:
    cpu: "2000m"
    memory: "4Gi"

9.3 探针配置(保障服务可用性)

# 在 containers 下添加 livenessProbe 和 readinessProbe
livenessProbe:
  tcpSocket:
    port: 6379
  initialDelaySeconds: 15
  periodSeconds: 10
readinessProbe:
  exec:
    command: ["redis-cli", "ping"]
  initialDelaySeconds: 5
  periodSeconds: 5

9.4 故障转移通知脚本

📄 notify.sh(挂载到哨兵容器)

#!/bin/sh
while true; do
  sleep 60
  # 发送告警到钉钉/企微/飞书
  curl -H "Content-Type: application/json" \
    -d '{"msgtype":"text","text":{"content":"Redis Sentinel 故障转移通知"}}' \
    https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN
done

十、排障命令速查

# 查看所有 Redis 进程
kubectl get pods -n redis -l app=redis -o wide

# 查看主从角色
kubectl exec -it redis-master-0 -n redis -- redis-cli -p 6379 ROLE

# 查看复制延迟
kubectl exec -it redis-master-0 -n redis -- redis-cli -p 6379 INFO replication | grep -E "role|master_link_status|master_repl_offset"

# 查看哨兵集群状态
kubectl exec -it $(kubectl get pod -l app=redis-sentinel -n redis -o jsonpath='{.items[0].metadata.name}') -n redis -- redis-cli -p 26379 INFO sentinel

# 查看哨兵选举详情
kubectl exec -it $(kubectl get pod -l app=redis-sentinel -n redis -o jsonpath='{.items[0].metadata.name}') -n redis -- redis-cli -p 26379 SENTINEL ckquorum mymaster

# 查看 Redis 日志
kubectl logs redis-master-0 -n redis --tail=100
kubectl logs $(kubectl get pod -l app=redis-sentinel -n redis -o jsonpath='{.items[0].metadata.name}') -n redis --tail=50

更多推荐