在 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 到底替你做了哪些事
| 运维动作 | 手动 / Helm | Operator |
|---|---|---|
| 创建 StatefulSet | 需要 | 自动 |
| Pod 重建后重新配置主从 | 手工脚本 | 自动(读回 Pod IP 重写配置) |
| 主节点故障触发切换 | Sentinel 自己会做 | Sentinel 做,Operator 兜底重建 |
| 扩缩容并迁移槽位 | 手工 redis-cli --cluster | 自动 |
| 配置变更后滚动重启 | 手工 | 自动(检测 ConfigMap 哈希变化) |
| 备份 CronJob 注入 | 手写 | 内置或模板化 |
| 故障时告警 | 需另配 | 通过 Events 与 Status 暴露 |
二、主流 Redis Operator 对比
生态里有几个成熟度不同的选择,选型前必须搞清各自的能力边界:
| 方案 | CRD | 支持模式 | 特点 | 适合 |
|---|---|---|---|---|
| Spotahome redis-operator | RedisFailover | 主从 + Sentinel | 社区最活跃,Sentinel 自动配置,运维简单 | 中小规模主从高可用 |
| Opstree redis-operator | Redis / RedisCluster | 主从 + Cluster | 同时支持两种模式,配置项丰富 | 需要 Cluster 模式的团队 |
| OT-container-kit redis-operator | Redis / 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 类,机械盘会让 AOFfsync拖垮吞吐。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 重建时失效。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。