Kubernetes Operator 运维:Redis 集群的声明式管理

Redis on Kubernetes Operator 运维实战:CRD 与控制器协调循环、Spotahome RedisFailover 与 Opstree 及官方 Operator 对比、StatefulSet 与 PVC 编排、Pod 反亲和与拓扑分布约束、PDB 保障、Sentinel 与 Cluster 的节点发现与 announce-ip 陷阱、扩缩容与槽位迁移、备份与监控接入

在 Kubernetes 上跑 Redis,从「能跑起来」到「敢放生产」,中间隔着不少坑。最朴素的做法是手写一个 StatefulSet 加 Service,主从靠 redis.conf 里的 replicaof 静态配置——一旦 Pod 重建、IP 变化,配置就失效了。进阶一点用 Helm chart,但 chart 只能渲染模板,不会持续观测实际状态:Pod 挂了它不会知道,节点扩容它不会帮你迁移槽位。

Operator 模式解决的就是这个问题:把「运维知识」编码进控制器,让集群状态持续向声明式目标收敛。本文对比主流 Redis Operator,拆解 CRD 设计、底层资源编排,以及 Sentinel / Cluster 模式在 K8s 里特有的节点发现问题。

一、Operator 模式:控制器如何工作

Operator 的核心是两个 Kubernetes 原生概念的组合:

  • CRD(Custom Resource Definition):定义一种新的资源类型,例如 RedisFailover 或 RedisCluster。用户写一份 YAML 描述「我要 3 个 Redis 副本 + 3 个 Sentinel」,这就是期望状态。
  • Controller:一个常驻进程,监听(watch)这些自定义资源的变化,然后调用 Kubernetes API 创建/更新 StatefulSet、Service、ConfigMap、Secret 等实际资源。

协调循环(Reconcile Loop)的伪代码大致是:

for {
    desired := 读取 CR 中的 spec
    actual  := 查询集群里实际存在的资源
    if desired != actual {
        执行动作:创建 Pod / 改副本数 / 触发主从切换 / 迁移槽位
    }
    等待下一次事件(watch 回调或定时重入)
}

这与运维工程师手动 kubectl get pods 再决定要不要 kubectl scale 是同一套逻辑,区别在于它是持续运行、毫秒级响应、且不会忘记。CRD 与控制器运行时(controller-runtime)的通用原理可参考 Kubernetes Operator 与 CRD 开发 ,本文只聚焦 Redis 场景的具体选择。

1.1 Operator 到底替你做了哪些事

运维动作手动 / HelmOperator
创建 StatefulSet需要自动
Pod 重建后重新配置主从手工脚本自动(读回 Pod IP 重写配置)
主节点故障触发切换Sentinel 自己会做Sentinel 做,Operator 兜底重建
扩缩容并迁移槽位手工 redis-cli --cluster自动
配置变更后滚动重启手工自动(检测 ConfigMap 哈希变化)
备份 CronJob 注入手写内置或模板化
故障时告警需另配通过 Events 与 Status 暴露

二、主流 Redis Operator 对比

生态里有几个成熟度不同的选择,选型前必须搞清各自的能力边界:

方案CRD支持模式特点适合
Spotahome redis-operatorRedisFailover主从 + Sentinel社区最活跃,Sentinel 自动配置,运维简单中小规模主从高可用
Opstree redis-operatorRedis / RedisCluster主从 + Cluster同时支持两种模式,配置项丰富需要 Cluster 模式的团队
OT-container-kit redis-operatorRedis / RedisCluster / RedisReplication三种功能全面,含备份与监控集成一体化需求
Redis Enterprise Operator(官方)RedisEnterpriseCluster / RedisEnterpriseDatabase企业版专有支持多租户、Active-Active、自动分层已采购企业版
Bitnami Helm Chart无 CRD主从 + Sentinel只是模板渲染,无持续协调简单场景、无运维自动化需求

一个务实的建议:如果是主从 + Sentinel 的高可用需求,Spotahome 是最省心的起点;如果确实需要 Cluster 分片且希望自动化槽位管理,选 Opstree 或 OT-container-kit。官方 Operator 只在买了 Redis Enterprise 授权时才有意义——它管的是企业版实例,不是开源版。

Sentinel 模式本身的原理与配置细节(quorum、down-after-milliseconds、failover-timeout)见 Sentinel 生产级高可用 ,Operator 只是把这份配置自动化了,并不改变 Sentinel 的语义。

三、RedisFailover CRD 实战

以 Spotahome 的 RedisFailover 为例,一份生产可用的 CR 大致长这样:

apiVersion: databases.spotahome.com/v1
kind: RedisFailover
metadata:
  name: redis-ha
  namespace: cache
spec:
  sentinel:
    replicas: 3
    resources:
      requests:
        cpu: 100m
        memory: 128Mi
      limits:
        cpu: 500m
        memory: 256Mi
  redis:
    replicas: 3
    resources:
      requests:
        cpu: 500m
        memory: 2Gi
      limits:
        cpu: "2"
        memory: 4Gi
    storage:
      persistentVolumeClaim:
        spec:
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 20Gi
          storageClassName: fast-ssd
    customConfig:
      - "maxmemory 3gb"
      - "maxmemory-policy allkeys-lru"
      - "appendonly yes"
      - "save 900 1"

应用后 Operator 会创建:

  • 一个 StatefulSet(3 个 Redis Pod),名为 rfr-redis-ha
  • 一个 StatefulSet(3 个 Sentinel Pod),名为 rfr-redis-ha-sentinel
  • 两个 Service:rfr-redis-ha(指向 master)、rfr-redis-ha-sentinel(Sentinel 端点)

查看状态:

kubectl -n cache get redisfailover redis-ha
kubectl -n cache describe redisfailover redis-ha   # 看 Events 与 Status
kubectl -n cache get pods -l app.kubernetes.io/name=redis

describe 输出的 Status 字段会记录当前 master 是谁、Sentinel 是否就绪——这是排障的第一入口。

四、底层资源编排:三个必须显式配置的东西

Operator 生成的 StatefulSet 提供了默认值,但生产上必须手动覆盖三处,否则高可用只是纸面上的。

4.1 Pod 反亲和:别把三个副本塞到一个节点

默认情况下,Kubernetes 调度器可能把 3 个 Redis Pod 全放到同一个节点。该节点宕机,主从一起没,Sentinel 也救不了。必须加反亲和:

affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels:
            app.kubernetes.io/name: redis
        topologyKey: kubernetes.io/hostname

requiredDuringSchedulingIgnoredDuringExecution 是硬约束:不满足就 Pending。如果节点数少于副本数,Pod 会一直挂起——这是有意的保护,避免「看起来部署成功了,其实全在一台机器上」。

更精细的做法是用拓扑分布约束(Topology Spread Constraints),允许在可用区层面均衡:

topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        app.kubernetes.io/name: redis

4.2 持久化卷:数据不能跟着 Pod 走

Redis 的 RDB/AOF 必须落在 PersistentVolumeClaim 上,否则 Pod 重建数据就没了。几个要点:

  • storageClassName 要选低延迟的 SSD 类,机械盘会让 AOF fsync 拖垮吞吐。
  • accessModes 用 ReadWriteOnce(RWO)。不要用 ReadWriteMany——Redis 是单写者模型,多挂载只会带来脑裂风险。
  • 容量按 maxmemory 的 1.5~2 倍规划,因为 RDB 落盘和 AOF 重写期间会有内存翻倍。

底层存储的 Provisioner、volumeBindingMode(WaitForFirstConsumer 还是 Immediate)与回收策略见 Kubernetes 存储与 CSI 。一个常见坑是用了 Immediate 绑定模式,PVC 被绑定到某个可用区,而 Pod 调度到了另一个可用区,导致 Pod 永久 Pending。

4.3 PodDisruptionBudget:别让滚动升级把集群全停

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: redis-ha-pdb
spec:
  minAvailable: 2          # 3 副本至少保持 2 个可用
  selector:
    matchLabels:
      app.kubernetes.io/name: redis

PDB 的作用是约束自愿中断(kubectl drain、节点升级)。没有 PDB 时,运维执行 kubectl drain node-1 会一次性驱逐该节点上的所有 Pod;有 PDB 后,驱逐会被阻塞直到满足 minAvailable。

配合 StatefulSet 的默认滚动更新策略(OrderedReady,从大到小逐个更新),可以实现「先切主、再重建从」的平滑升级。

五、Sentinel / Cluster 在 K8s 的节点发现陷阱

这是 K8s 上跑 Redis 最容易踩的坑,与裸机部署差异最大。

5.1 问题:Pod IP 会变,Sentinel 记的是旧地址

Sentinel 通过 INFO replication 获取主从关系,并把「主节点 IP」持久化在自己的配置里。在 K8s 中,Pod 重建后 IP 会变化,如果 Sentinel 记的是 Pod IP,切换就会指向一个已经不存在的地址。

两种解法:

方案 A:用 Headless Service 的稳定 DNS 名。 每个 Pod 有稳定的域名 <pod-name>.<service-name>.<namespace>.svc.cluster.local,Pod 重建后域名不变。

apiVersion: v1
kind: Service
metadata:
  name: redis-headless
spec:
  clusterIP: None            # Headless
  selector:
    app.kubernetes.io/name: redis
  ports:
    - port: 6379
      name: redis

方案 B:配置 replica-announce-ip。 让 Redis 主动对外声明自己的地址:

replica-announce-ip redis-0.redis-headless.cache.svc.cluster.local
replica-announce-port 6379

如果不设置 replica-announce-ip,Redis 默认把 INFO replication 里报告的地址填成自己看到的对端 IP——在 NAT 或 Service 转发场景下,这个 IP 可能是错的,导致 Sentinel 与客户端都拿不到可达地址。

5.2 Cluster 模式的 cluster-announce-ip

Cluster 模式的问题更严重:节点之间靠 Gossip 协议交换拓扑,如果 cluster-announce-ip 不对,节点会互相认为对方在错误的地址上,集群永远处于 fail 状态。

cluster-announce-ip redis-0.redis-headless.cache.svc.cluster.local
cluster-announce-port 6379
cluster-announce-bus-port 16379

部分 Operator 会通过 Downward API 注入 Pod IP:

env:
  - name: POD_IP
    valueFrom:
      fieldRef:
        fieldPath: status.podIP

再用启动脚本生成 cluster-announce-ip ${POD_IP}。两种做法都可行,关键是必须显式设置,不能依赖默认推断。Cluster 本身的槽位与故障转移机制见 高可用 Cluster 集群架构 。

六、扩缩容与槽位迁移

改 CR 里的 replicas 后,Operator 会重建 StatefulSet。但要注意:扩 Pod 数不等于扩分片数。

在 Cluster 模式下:

  • 把 replicas: 3 改成 6,只是把分片数从 3 变成 6(如果是 3 主 3 从的拓扑)。
  • 新节点加入后需要 redis-cli --cluster reshard 重新分配槽位,把 16384 个槽重新均摊。
  • 部分 Operator 会自动做 reshard,部分只做 MEET(节点互相发现)与 ADDSLOTS,槽位迁移仍需人工介入。

判断 Operator 是否真的自动化了,看它的 CR 里有没有类似 slotsRebalance、clusterRebalance 的开关。若没有,扩容后必须手工执行:

# 查看当前槽位分布
redis-cli --cluster check redis-0.redis-headless:6379

# 从已有节点迁移 2000 个槽到新节点
redis-cli --cluster reshard redis-0.redis-headless:6379 \
  --cluster-from <source-node-id> \
  --cluster-to <new-node-id> \
  --cluster-slots 2000 \
  --cluster-yes

缩容则相反:必须先把槽位迁走,再删除 Pod,否则槽位会丢失,整个集群进入 CLUSTERDOWN。

操作正确顺序错误顺序的后果
扩容加 Pod → MEET → reshard槽位不均,热点集中
缩容reshard 迁走槽位 → 删 Pod槽位丢失,集群不可用
换机器新 Pod 就绪 → 迁槽 → 删旧 Pod数据短暂不可访问

七、可观测性与备份

Operator 只管编排,指标与备份要另外接。

指标暴露:给 Redis Pod 加一个 redis_exporter sidecar,或让 Operator 支持 PodMonitor 注入:

# Prometheus Operator 的 PodMonitor
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: redis
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: redis
  podMetricsEndpoints:
    - port: metrics
      interval: 15s

关键指标包括 redis_up、redis_connected_clients、redis_memory_used_bytes、redis_master_link_up(从节点与主节点的链路是否正常)、redis_cluster_state。指标口径与告警阈值的完整清单属于监控专题的范畴,这里只需保证 exporter 能连上每个 Pod。

备份:RDB 快照本身不够,需要定期把快照上传到对象存储。做法是挂一个 CronJob:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: redis-backup
spec:
  schedule: "0 3 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: backup
              image: redis:7-alpine
              command:
                - sh
                - -c
                - |
                  redis-cli -h redis-0.redis-headless BGSAVE
                  sleep 30
                  mc cp /data/dump.rdb backup/redis/$(date +%F).rdb
              volumeMounts:
                - name: data
                  mountPath: /data

没有演练过的备份等于没有备份,这在 K8s 环境尤其成立,因为 PVC 的恢复路径与裸机完全不同。

八、版本升级与回滚

Redis 大版本升级(如 6.2 → 7.2)在 K8s 上通常表现为改 CR 里的镜像 tag,但直接改会带来两个风险:RDB/AOF 格式兼容性与滚动升级期间的主从切换。

安全流程:

# 1. 先确认从节点能加载新版本的持久化文件
#    在小规模灰度环境用同一份 dump.rdb 起新版本验证

# 2. 逐个升级从节点(不触碰主节点)
kubectl -n cache patch redisfailover redis-ha --type=merge \
  -p '{"spec":{"redis":{"image":"redis:7.2-alpine"}}}'
# StatefulSet 会从序号最大的 Pod 开始滚动

# 3. 观察从节点同步状态
kubectl -n cache exec redis-2 -- redis-cli info replication | grep -E 'master_link|master_sync'

# 4. 全部从节点升级完成后,手动触发一次主从切换
kubectl -n cache exec redis-sentinel-0 -- \
  redis-cli -p 26379 SENTINEL FAILOVER mymaster

# 5. 切换后,旧主节点变成从节点,被滚动升级覆盖

回滚同样依赖镜像 tag:

kubectl -n cache rollout undo statefulset/rfr-redis-ha

但要注意:回滚只回滚镜像,不回滚数据。如果新版本已经写入过 RDB,旧版本可能无法读取。因此升级前必须做一次完整备份,且灰度环境要跑通「新版本写、旧版本读」的兼容性验证。RDB 版本兼容矩阵需要在升级前用 redis-check-rdb 单独验证。

场景是否可原地升级说明
补丁版本(7.2.1 → 7.2.3)可以直接滚动
小版本(7.0 → 7.2)可以需验证配置项兼容
大版本(6.2 → 7.2)谨慎备份 + 灰度 + 主从切换
换镜像仓库/发行版谨慎确认 redis.conf 默认值差异

九、生产实践清单

  • 用 Operator 而非纯 Helm,除非你确定不需要持续协调。
  • 主从 + Sentinel 场景优先 Spotahome;需要 Cluster 自动化槽位则选 Opstree / OT-container-kit。
  • 必须显式配置 Pod 反亲和或拓扑分布约束,避免副本同节点。
  • PVC 用 RWO + SSD storageClassName,容量按 maxmemory 的 1.5~2 倍规划。
  • 必须设置 PDB,否则节点维护会一次性打挂集群。
  • Sentinel 与 Cluster 模式都要显式配置 replica-announce-ip / cluster-announce-ip,指向 Headless Service 域名。
  • 扩容后确认槽位是否自动均衡;缩容前必须手工迁槽。
  • 指标与备份单独接入,备份要定期做恢复演练。

小结

Redis Operator 的价值不是「少写几个 YAML」,而是把「主从切换后重新配置」「扩容后迁移槽位」「配置变更后滚动重启」这些容易遗忘的运维动作变成控制器里的持续协调。选型的核心问题是:你需要的是 Sentinel 高可用,还是 Cluster 分片自动化?前者 Spotahome 足够,后者才需要更重的方案。

K8s 环境与裸机最大的差异在地址稳定性:Pod IP 会变,必须用 Headless Service 域名加 announce-ip 显式声明。这一条配置漏掉,其他所有高可用机制都会在第一次 Pod 重建时失效。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「redis」更多文章

  1. 多租户隔离与资源配额:共享 Redis 的边界设计
  2. Key 设计与命名规范:Redis 里唯一的结构约束
  3. 代理与路由方案:客户端直连之外的另一种选择