Kubernetes 部署 Redis Cluster 实战:StatefulSet 与 Headless Service 核心指南 1. 为什么要在 Kubernetes 上部署 Redis ClusterRedis Cluster 是 Redis 官方提供的分布式方案核心目标就两个数据分片和高可用。16384 个哈希槽分散在多个主节点上每个主节点配一个或多个从节点做故障接管。这套机制在物理机时代已经跑了很多年成熟度不用怀疑。但把 Redis Cluster 搬到 Kubernetes 上事情就变得有意思了。Kubernetes 天然适合跑无状态服务Pod 挂了拉起来就行IP 变了也无所谓Service 帮你兜底。可 Redis Cluster 偏偏是个有状态 节点互相感知的东西——每个节点要记住其他节点的 IP 和端口集群总线靠 gossip 协议通信节点重启后 IP 变了整个集群的拓扑信息就得跟着更新。这就跟 Kubernetes 的调度逻辑产生了天然的摩擦。我见过不少团队一开始图省事直接用 Deployment 跑 Redis结果 Pod 重建后 IP 变化集群直接脑裂或者槽位分配错乱。后来换成 StatefulSet用 Headless Service 给每个 Pod 固定 DNS 名称才算把这个问题按住。所以这篇文章的核心就是围绕StatefulSet Headless Service 初始化 Job这套组合拳把 Redis Cluster 在 Kubernetes 上跑稳。这篇文章适合谁看如果你已经会用 Docker 跑单机 Redis对 Kubernetes 的基本对象Pod、Service、StatefulSet有概念但还没在生产环境部署过 Redis Cluster那这篇就是写给你的。我会把每个配置项为什么这么写、参数怎么算、踩过哪些坑全部摊开讲。如果你已经是老手也可以看看我在探针配置和资源限制上的取舍说不定有能对上的地方。注意本文所有操作基于 Kubernetes 1.24 和 Redis 7.x 版本不同版本间 API 有差异尤其是apps/v1之前的旧版本请自行对照调整。2. 整体架构设计与核心思路拆解2.1 为什么是 StatefulSet 而不是 DeploymentRedis Cluster 的每个节点都是有状态的这个状态不只是数据本身还包括节点 ID、集群配置文件nodes.conf、槽位分配信息。用 Deployment 的话Pod 名字是随机后缀重启后 DNS 名称变了其他节点找不到它集群总线就断了。StatefulSet 给每个 Pod 提供三个关键能力稳定的网络标识redis-0.redis-headless、稳定的持久化存储PVC 模板、有序的部署和扩缩容。这三点正好对应 Redis Cluster 的需求。尤其是稳定网络标识配合 Headless Service每个 Pod 都能通过固定 DNS 名称被其他节点访问IP 变了也不影响。我试过用 Deployment 固定 IP 的方案手动给 Pod 指定 IP结果 Kubernetes 调度器没法正常工作了节点一多就乱套。StatefulSet 是正路别绕。2.2 集群规模怎么定3主3从还是6主6从Redis Cluster 最少需要 3 个主节点才能形成多数派官方推荐的最小生产配置是 3 主 3 从共 6 个节点。为什么是 3 主因为集群投票需要过半数3 个主节点允许挂 1 个还能正常工作。如果只搞 2 主挂 1 个就失去多数派集群直接不可用。从节点数量看你的可用性要求。3 主 3 从意味着每个主节点有一个从节点做备份主挂了从顶上。如果对可用性要求更高可以 3 主 6 从每个主配两个从。但节点越多gossip 通信开销越大集群总线的心跳包会占用更多网络带宽。一般业务 3 主 3 从够用了除非你的 QPS 特别高或者数据量特别大。资源规划上我一般按每个 Redis 节点 2C4G 起步生产环境根据实际数据量和 QPS 调整。内存方面Redis 官方建议留出一半内存给 RDB 快照和 AOF 重写所以如果你的数据量是 4G那 Pod 内存限制至少给 8G。这个账要算清楚不然 OOM 杀进程的时候哭都来不及。2.3 网络方案Headless Service 是核心Redis Cluster 节点之间需要互相通信包括集群总线端口默认是客户端端口 10000即 16379和客户端端口6379。在 Kubernetes 里Pod IP 是动态分配的重启就变。所以必须用 Headless ServiceclusterIP: None给每个 Pod 一个固定的 DNS 记录。Headless Service 的 DNS 格式是pod-name.service-name.namespace.svc.cluster.local。比如redis-0.redis-headless.default.svc.cluster.local。这个 DNS 名称在 Pod 生命周期内不变即使 Pod 重建、IP 变化DNS 记录也会自动更新到新 IP。Redis Cluster 的cluster-announce-ip就填这个 DNS 名称其他节点通过 DNS 解析找到它。这里有个细节Redis 默认用 IP 做集群总线通信但在 Kubernetes 里必须改成用 DNS 名称。配置项是cluster-announce-hostnameRedis 6.2 之后支持。如果你用的版本低于 6.2那就得用cluster-announce-ip填 Pod IP但 Pod 重建后 IP 变了集群就乱了。所以强烈建议用 Redis 6.2直接上 hostname 方案。2.4 持久化存储选型local PV 还是网络存储Redis 的持久化有两种RDB 和 AOF。RDB 是快照AOF 是追加日志。生产环境一般两个都开RDB 做冷备AOF 做数据恢复。存储选型上local PV 性能最好但 Pod 漂移到其他节点后就读不到数据了。网络存储比如 NFS、Ceph RBD可以跨节点挂载但延迟比 local PV 高。我的建议是如果对性能要求极高用 local PV 节点亲和性把 Pod 固定在某些节点上。如果对可用性要求更高Pod 可以漂移那就用网络存储。折中方案是用支持 ReadWriteOnce 的块存储比如 Ceph RBD 或者云厂商的 SSD 云盘性能和可用性兼顾。存储大小怎么定看你的数据量和持久化策略。如果只开 RDB存储可以小一点因为 RDB 是压缩的。如果开 AOF存储至少是数据量的 2-3 倍因为 AOF 文件会不断增长重写时还需要额外空间。我一般按数据量的 3 倍来申请 PVC留足余量。3. 核心配置细节与实操要点3.1 StatefulSet 配置逐项拆解先看 StatefulSet 的完整配置我逐项解释为什么这么写apiVersion: apps/v1 kind: StatefulSet metadata: name: redis-cluster spec: serviceName: redis-headless replicas: 6 selector: matchLabels: app: redis-cluster template: metadata: labels: app: redis-cluster spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: redis-cluster topologyKey: kubernetes.io/hostname containers: - name: redis image: redis:7.2-alpine command: - redis-server - /etc/redis/redis.conf - --cluster-announce-hostname - $(POD_NAME).redis-headless ports: - containerPort: 6379 name: client - containerPort: 16379 name: gossip env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name volumeMounts: - name: data mountPath: /data - name: config mountPath: /etc/redis readinessProbe: exec: command: - sh - -c - redis-cli -h $(POD_NAME).redis-headless ping initialDelaySeconds: 10 periodSeconds: 5 livenessProbe: exec: command: - sh - -c - redis-cli -h $(POD_NAME).redis-headless ping initialDelaySeconds: 30 periodSeconds: 10 resources: requests: cpu: 1 memory: 2Gi limits: cpu: 2 memory: 4Gi volumeClaimTemplates: - metadata: name: data spec: accessModes: [ReadWriteOnce] storageClassName: ssd resources: requests: storage: 20GiserviceName: redis-headless必须和 Headless Service 的名字一致这是 StatefulSet 的硬性要求。podAntiAffinity用preferred而不是required是因为如果节点不够强制反亲和会导致 Pod 一直 Pending。用preferred让调度器尽量打散实在打不散也能跑起来。cluster-announce-hostname这个参数很关键它告诉 Redis 用 hostname 而不是 IP 来标识自己。$(POD_NAME).redis-headless会被展开成redis-0.redis-headless这样的 DNS 名称。注意这里没有加 namespace 和svc.cluster.local后缀因为 Redis 集群内部通信时其他节点也在同一个 namespace 下短域名就能解析。探针配置上readinessProbe用redis-cli ping检查节点是否就绪。这里有个坑如果 Redis 还没加入集群ping也会返回 PONG所以这个探针只能判断进程是否活着不能判断集群状态。更严格的探针应该检查cluster info里的cluster_state:ok但那样在集群初始化阶段会一直失败导致 Pod 永远不就绪。我的做法是初始化阶段用简单探针集群建好后手动改成严格探针或者用 Init Container 做集群初始化主容器只负责跑 Redis。资源限制上requests和limits的 CPU 差距不要太大否则节点 CPU 争抢时 Redis 性能波动会很厉害。内存limits一定要大于requests因为 Redis 的 RDB 快照和 AOF 重写会 fork 子进程子进程和父进程共享内存但 fork 瞬间内存占用会翻倍。如果limits设得太紧fork 时直接 OOM。3.2 Headless Service 与集群总线端口Headless Service 的配置很简单但有个细节容易忽略apiVersion: v1 kind: Service metadata: name: redis-headless spec: clusterIP: None selector: app: redis-cluster ports: - name: client port: 6379 targetPort: 6379 - name: gossip port: 16379 targetPort: 16379clusterIP: None声明这是 Headless ServiceKubernetes 不会给它分配 ClusterIPDNS 查询会直接返回所有 Pod 的 IP。gossip端口 16379 必须暴露这是 Redis Cluster 总线通信端口默认是客户端端口 10000。如果这个端口不通集群节点之间无法交换拓扑信息集群就建不起来。注意有些网络插件比如 Calico 的某些模式默认会拦截非标准端口部署前先用telnet或nc测试一下 16379 端口是否可达。我踩过这个坑排查了半天才发现是网络策略的问题。3.3 redis.conf 关键参数详解Redis 的配置文件我一般用 ConfigMap 挂载核心参数如下cluster-enabled yes cluster-config-file /data/nodes.conf cluster-node-timeout 5000 cluster-announce-hostname $(POD_NAME).redis-headless cluster-announce-port 6379 cluster-announce-bus-port 16379 appendonly yes appendfsync everysec save 900 1 save 300 10 save 60 10000 maxmemory 3gb maxmemory-policy allkeys-lru protected-mode nocluster-node-timeout 5000是节点超时时间单位毫秒。这个值决定了节点多久没响应就被标记为疑似下线PFAIL。设太小网络抖动就触发故障转移集群频繁切换主从设太大真挂了半天才发现。5000ms 是官方默认值一般够用。如果网络质量差可以调到 10000ms。maxmemory要小于 Pod 的内存limits留出空间给 fork 和系统开销。比如 Pod 内存限制 4Gmaxmemory设 3G 比较稳妥。maxmemory-policy用allkeys-lru还是noeviction看业务场景。如果 Redis 只做缓存用allkeys-lru内存满了自动淘汰旧数据。如果 Redis 存的是关键数据不能丢用noeviction内存满了写入报错让上层业务感知到。appendfsync everysec是 AOF 刷盘策略每秒刷一次。always性能太差no又可能丢数据everysec是折中方案最多丢 1 秒数据。大部分业务能接受。protected-mode no必须关掉否则 Redis 只允许本地连接集群节点之间连不上。这个参数在容器环境里经常被忽略导致集群建不起来。4. 完整实操流程与集群初始化4.1 部署顺序与资源创建整个部署流程分四步创建 Namespace、创建 ConfigMap、创建 Headless Service、创建 StatefulSet。顺序不能乱因为 StatefulSet 依赖 Headless Service 提供 DNSConfigMap 提供配置文件。kubectl create namespace redis-cluster kubectl apply -f redis-configmap.yaml -n redis-cluster kubectl apply -f redis-headless-svc.yaml -n redis-cluster kubectl apply -f redis-statefulset.yaml -n redis-cluster创建完 StatefulSet 后Pod 会按顺序启动redis-0、redis-1、...、redis-5。StatefulSet 默认是 OrderedReady 策略前一个 Pod 就绪后才启动下一个。这个策略在集群初始化时很有用因为集群节点需要逐个加入。但如果你觉得启动太慢可以改成 Parallel 策略同时启动所有 Pod然后手动做集群初始化。Pod 全部 Running 后用kubectl get pods -n redis-cluster -o wide确认每个 Pod 的 IP 和节点分布。再用kubectl exec -it redis-0 -n redis-cluster -- redis-cli cluster info检查集群状态这时候应该是cluster_state:fail因为还没做集群初始化。4.2 集群初始化redis-cli --cluster create集群初始化用redis-cli --cluster create命令指定所有节点的地址和主从分配kubectl exec -it redis-0 -n redis-cluster -- redis-cli --cluster create \ redis-0.redis-headless:6379 \ redis-1.redis-headless:6379 \ redis-2.redis-headless:6379 \ redis-3.redis-headless:6379 \ redis-4.redis-headless:6379 \ redis-5.redis-headless:6379 \ --cluster-replicas 1--cluster-replicas 1表示每个主节点配一个从节点。Redis 会自动把前 3 个节点设为主节点后 3 个设为从节点并分配 16384 个槽位。执行过程中会提示你确认槽位分配方案输入yes继续。这里有个坑如果 Pod 的 DNS 解析有问题redis-cli会卡住或者报连接超时。先用kubectl exec -it redis-0 -- nslookup redis-1.redis-headless确认 DNS 能解析。如果解析不了检查 Headless Service 的 selector 是否匹配 Pod 标签以及 CoreDNS 是否正常运行。初始化完成后用redis-cli cluster info检查集群状态应该显示cluster_state:ok。再用redis-cli cluster nodes查看节点列表确认每个节点的角色和槽位分配。如果一切正常集群就建好了。4.3 验证集群读写与故障转移集群建好后先做基本读写测试kubectl exec -it redis-0 -n redis-cluster -- redis-cli -c set foo bar kubectl exec -it redis-0 -n redis-cluster -- redis-cli -c get foo-c参数表示跟随重定向因为 Redis Cluster 的键分布在不同的槽位上客户端需要根据 MOVED 响应重定向到正确的节点。如果返回bar说明读写正常。故障转移测试手动删掉一个主节点 Pod观察从节点是否自动接管。比如删掉redis-0kubectl delete pod redis-0 -n redis-cluster等几秒钟用redis-cli cluster nodes查看应该能看到redis-3假设它是redis-0的从节点变成了主节点槽位也转移过去了。然后 StatefulSet 会自动重建redis-0重建后它会作为从节点加入集群同步redis-3的数据。注意故障转移的时间取决于cluster-node-timeout和cluster-slave-validity-factor。默认情况下主节点挂掉后从节点会在cluster-node-timeout毫秒后发起选举选举需要多数主节点投票整个过程大概 10-15 秒。如果业务对恢复时间敏感可以适当调小cluster-node-timeout但别小于 3000ms否则网络抖动容易误判。4.4 扩缩容操作与槽位迁移扩容就是增加 StatefulSet 的replicas然后手动把新节点加入集群。比如从 6 节点扩到 8 节点kubectl scale statefulset redis-cluster --replicas8 -n redis-cluster等redis-6和redis-7启动后用redis-cli --cluster add-node把它们加入集群kubectl exec -it redis-0 -n redis-cluster -- redis-cli --cluster add-node \ redis-6.redis-headless:6379 \ redis-0.redis-headless:6379新节点默认是主节点没有槽位。需要用redis-cli --cluster reshard重新分配槽位kubectl exec -it redis-0 -n redis-cluster -- redis-cli --cluster reshard \ redis-0.redis-headless:6379 \ --cluster-from all \ --cluster-to new-node-id \ --cluster-slots 1000 \ --cluster-yes--cluster-from all表示从所有现有主节点各拿一部分槽位给新节点。--cluster-slots 1000是给新节点分配的槽位数量根据数据量调整。槽位迁移过程中集群仍然可用但迁移的槽位上的键会短暂阻塞。建议在低峰期操作。缩容就是反过来先把槽位迁走再用redis-cli --cluster del-node删除节点。缩容比扩容风险高因为如果槽位没迁干净就删节点会导致数据丢失。操作前一定要用redis-cli --cluster check确认所有槽位都有主节点负责。5. 常见问题与排查技巧实录5.1 集群状态 fail 的排查思路cluster_state:fail是最常见的问题原因通常有三个槽位没分配完、节点之间通信不通、多数派主节点不可用。先看槽位分配redis-cli cluster info里的cluster_slots_assigned应该是 16384。如果小于这个数说明有槽位没分配。用redis-cli cluster slots查看哪些槽位没主节点然后用redis-cli --cluster fix修复。再看节点通信redis-cli cluster nodes里每个节点的状态应该是connected。如果有disconnected或者fail的节点检查网络连通性。用kubectl exec进入 Podping其他节点的 DNS 名称或者telnet16379 端口。如果端口不通检查 NetworkPolicy 是否拦截了流量。最后看多数派如果超过一半的主节点挂了集群会进入 fail 状态拒绝所有读写请求。这时候只能手动恢复先把挂掉的主节点拉起来或者用redis-cli --cluster failover强制从节点接管。5.2 Pod 重启后集群拓扑错乱Pod 重启后 IP 变了如果 Redis 配置里用的是 IP 而不是 hostname集群拓扑就会错乱。症状是cluster nodes里出现重复节点或者节点状态一直是handshake。解决方案就是前面说的用cluster-announce-hostname配置 hostname并且确保 Redis 版本在 6.2 以上。如果已经出问题了需要手动清理nodes.conf文件然后重新加入集群。nodes.conf在/data目录下删掉后重启 Redis它会以新节点身份加入集群。但这样会丢失该节点的数据需要从其他节点同步。注意删nodes.conf之前一定要确认该节点上没有独占的槽位否则槽位会丢失。用redis-cli cluster nodes查看该节点的槽位分配如果有槽位先用redis-cli --cluster reshard迁走。5.3 内存告警与 OOM 处理Redis 内存告警通常是因为maxmemory设得太大或者数据量增长超出预期。先看redis-cli info memory里的used_memory和used_memory_rss。used_memory是 Redis 逻辑内存used_memory_rss是操作系统实际分配的内存。如果used_memory_rss远大于used_memory说明有内存碎片可以用redis-cli memory purge手动清理或者重启节点。如果 Pod 被 OOM 杀掉检查kubectl describe pod里的Last State确认是 OOMKilled。然后调大limits内存或者调小maxmemory。我一般把maxmemory设为 Pod 内存限制的 75%留 25% 给 fork 和系统开销。5.4 常见问题速查表问题现象可能原因排查命令解决方案cluster_state:fail槽位未分配完redis-cli cluster inforedis-cli --cluster fix节点状态 handshakeDNS 解析失败nslookup redis-1.redis-headless检查 Headless Service集群总线不通16379 端口被拦截telnet redis-1 16379检查 NetworkPolicyPod OOMKilled内存限制太小kubectl describe pod调大 limits 或调小 maxmemory故障转移不触发node-timeout 太大redis-cli cluster nodes调小 cluster-node-timeout槽位迁移卡住目标节点不可达redis-cli --cluster check检查目标节点状态5.5 实操心得与避坑建议第一个坑别用 latest 标签。Redis 镜像用redis:7.2-alpine这种固定版本避免自动升级导致配置不兼容。我见过用latest结果某天镜像更新了集群直接起不来。第二个坑PVC 回收策略。StatefulSet 删除后PVC 默认保留。如果重新部署旧 PVC 会被挂载里面可能有旧的nodes.conf导致新集群初始化失败。要么手动删 PVC要么在 StatefulSet 里设置persistentVolumeClaimRetentionPolicy为Delete。第三个坑探针超时时间。Redis 在 fork 做 RDB 快照时会短暂阻塞如果探针超时时间太短会误判为失败导致 Pod 重启。timeoutSeconds至少设 5 秒failureThreshold设 3 次避免误杀。第四个坑集群初始化 Job 的幂等性。如果用 Job 做集群初始化Job 失败重试时会重复执行cluster create导致报错。解决方案是在 Job 里先检查cluster info如果已经是ok就跳过初始化。或者用--cluster-yes参数让 Redis 自动处理已存在的集群。6. 生产环境加固与监控建议6.1 资源限制与 QoS 等级Kubernetes 的 QoS 等级分三档Guaranteed、Burstable、BestEffort。Redis 这种对延迟敏感的服务最好设成 Guaranteed即requests和limits完全相等。这样 Pod 在节点资源不足时不会被驱逐性能也最稳定。但 Guaranteed 的代价是资源利用率低因为requests已经占住了资源。折中方案是 CPU 用 Burstablerequests小于limits内存用 Guaranteedrequests等于limits。因为 CPU 是可压缩资源争抢时只会变慢不会导致 Pod 被杀。内存是不可压缩资源超了直接 OOM。6.2 监控指标与告警规则Redis 的关键监控指标包括used_memory、connected_clients、instantaneous_ops_per_sec、keyspace_hits、keyspace_misses、cluster_state。这些指标可以通过redis_exporter采集然后接入 Prometheus Grafana。告警规则我一般设这几条cluster_state不等于ok持续 1 分钟used_memory超过maxmemory的 90% 持续 5 分钟connected_clients超过 1000 持续 5 分钟instantaneous_ops_per_sec突降 50% 持续 2 分钟。这些规则能覆盖大部分异常场景。6.3 备份策略与恢复演练Redis Cluster 的备份不能只备份单个节点因为数据是分片的。要么用redis-cli --cluster backup做全集群备份要么在每个主节点上分别做 RDB 备份然后合并。我一般用 CronJob 在每个主节点上定时执行BGSAVE然后把 RDB 文件上传到对象存储。恢复演练很重要。我见过备份文件没问题但恢复时发现nodes.conf和 RDB 不匹配集群起不来。所以定期做恢复演练确保备份真的能用。演练时在测试环境重建集群导入备份数据验证数据完整性。6.4 版本升级与滚动更新Redis 版本升级用 StatefulSet 的滚动更新策略默认是RollingUpdate从最后一个 Pod 开始逐个更新。但 Redis Cluster 的滚动更新有个问题更新主节点时从节点会接管更新完成后主节点重新加入需要同步数据。如果数据量大同步时间会很长。我的做法是先把主节点手动切换成从节点redis-cli cluster failover然后更新这个节点更新完再切回主节点。这样每次更新的都是没有槽位的从节点不影响集群读写。全部更新完后再逐个把主节点切回来。注意版本升级前一定要看 Redis 的 Release Notes确认有没有不兼容的配置变更。比如 Redis 6 到 Redis 7有些配置项改名了直接升级会导致启动失败。6.5 网络策略与安全加固生产环境一定要配 NetworkPolicy只允许 Redis 集群内部的 Pod 互相访问 6379 和 16379 端口以及业务 Pod 访问 6379 端口。禁止外部直接访问 Redis 端口。apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: redis-cluster-policy spec: podSelector: matchLabels: app: redis-cluster ingress: - from: - podSelector: matchLabels: app: redis-cluster ports: - port: 6379 - port: 16379 - from: - podSelector: matchLabels: app: business-app ports: - port: 6379另外Redis 密码认证也要开。在redis.conf里设requirepass和masterauth客户端连接时用AUTH命令认证。密码用 Kubernetes Secret 管理不要硬编码在 ConfigMap 里。6.6 性能调优的几个关键参数除了前面提到的maxmemory和maxmemory-policy还有几个参数对性能影响很大tcp-backlog默认 511高并发场景可以调到 1024 或 2048。timeout设 0 表示不主动断开空闲连接但如果客户端连接池管理不好容易积累大量空闲连接建议设 300 秒。tcp-keepalive设 300 秒定期探测死连接。io-threads在 Redis 6 之后支持多线程 IO如果 CPU 核数多可以设成 4 或 8提升网络吞吐。但io-threads不要超过 CPU 核数否则线程切换开销反而降低性能。lazyfree-lazy-eviction和lazyfree-lazy-expire设成 yes让 Redis 在后台线程释放内存避免主线程阻塞。这两个参数在高写入场景下效果明显。7. 写在最后这套方案我在几个生产环境跑过最长的稳定运行了一年多中间经历过节点故障、网络抖动、版本升级整体表现符合预期。Redis Cluster 在 Kubernetes 上的核心难点就是网络标识和状态管理把 StatefulSet 和 Headless Service 用对了后面的事情就顺了。如果你刚开始搭建议先在测试环境跑一遍完整流程包括部署、初始化、故障转移、扩缩容、备份恢复。每个环节都手动操作一遍比看十篇文档都管用。生产环境上线前一定要做压力测试确认 QPS 和延迟满足业务要求。最后分享一个小技巧用kubectl debug临时起一个带redis-cli的容器加入集群网络排查问题时特别方便。不用每次都exec进 Pod也不怕把生产 Pod 搞挂。