Kubernetes上部署Redis集群:StatefulSet与持久化完整实践 想在生产环境里把Redis集群跑在Kubernetes上并不是把官方的Redis容器直接怼上去就完事。网络寻址、持久化、主从关系、故障自愈每个环节都能卡你一下尤其是第一次操作时经常会遇到“容器起来了但集群一直认不到对方”这种状况。这篇文章就把我在Kubernetes里安装Redis集群的完整过程拆开讲一遍从方案选型、环境准备到Pod资源编排、集群初始化、常见问题排查全部走一遍尽量让你照着操作就能复现。这套部署适合有一定Kubernetes基础、已经不满足于单机Redis的读者看。如果你只是为了本地测试跑个单实例直接deployment加PVC就够了但如果你面对的是需要在集群内提供内网缓存服务、希望主节点挂了能自动顶上去、又不想自己手工维护一堆sentinel的场景那一个三主三从的Redis Cluster会是更务实的起点。下面内容基于我在模拟项目X上的一次完整部署实践整理环境是标准的Kubernetes集群存储使用的是动态StorageClass整体方案全部使用原生资源完成不依赖额外控制面组件。1. 先想清楚容器里的Redis集群为什么麻烦1.1 状态、网络与节点身份这三座大山Redis Cluster不像Nginx那种无状态服务它每一个节点都带着数据、槽位和集群状态。Kubernetes里的普通Deployment追求的是“任意Pod随时可以被替换”IP变了无所谓但Redis集群内部会把每个节点的IP和端口记录下来节点之间靠这些信息相互通信。如果一个Redis节点Pod被重新调度IP变了旧IP可能还残留在其他节点的配置里整个集群就面临脑裂风险。这是第一座大山状态需要被保存PVC是必须的不能用emptyDir当生产环境的数据目录。第二座大山是网络Redis集群节点间有两种连接一个是客户端访问用的6379端口另一个是集群内部总线端口16379用于传播消息和选举。所以Kubernetes里你不能只暴露637916379也得让Pod之间能通。第三座大山是身份Redis集群里每个节点都需要一个唯一ID这个ID默认存在nodes.conf里一旦Pod重建这个文件如果丢失等于换了一个新节点旧信息全部作废。理解了这一点再去看各种部署方式你就能判断哪种方案是合适的。1.2 为什么StatefulSet是主流选择Kubernetes里给有状态服务提供了一套约定俗成的编排对象就是StatefulSet。它最大的价值在于给每个Pod一个稳定的网络标识也就是podName.serviceName这种DNS名字。创建顺序固定缩容顺序固定PVC也可以按volumeClaimTemplates自动生成配合Headless Service使用可以保证Pod之间互相通过稳定的域名访问。Deployment的问题很直接它生成的Pod名称是随机的重建后Pod名都会变给集群维护、节点识别、备份恢复都带来额外负担。用Deployment也能跑Redis但每次重建都跟“拆盲盒”一样我不推荐。还有一类方案是走Operator把创建集群、扩缩容、故障恢复全部自动化这个方向很省心。但Operator本身是额外控制面学习成本不低而且要维护Operator版本和Redis版本的兼容性在很多内部环境里还会被安全审计卡一道。所以这篇内容选择的是“ConfigMap加Headless Service加StatefulSet”这个原生组合没有额外依赖所有细节都看得见出了问题也能定位适合作为标准部署模板。2. 部署前准备环境校验与资源规划2.1 三个必须确认的前提条件动手指前先把环境确认清楚。第一个前提是Kubernetes集群本身可用并且你有权限创建Namespace、Service、StatefulSet、ConfigMap、Deployment这类资源。Redis节点需要写数据卷所以第二个前提是集群里有可用的StorageClass而且要支持动态创建PV。你可以用下面这行命令看当前环境有哪些存储类kubectl get sc如果输出里加粗的PROVISIONER是空的或者没有一个默认StorageClass那你后面PVC创建会一直Pending这一步不能跳过。第三个前提是节点之间能互相访问Pod网段尤其是跨工作节点时网络插件CNI要正常工作。Redis集群的16379端口必须互通验证方法可以临时起一个测试Pod去访问预留的IP不过更直接的办法是等StatefulSet建完后直接用redis-cli -c cluster nodes检查。规划方面我建议至少准备6个节点。因为Redis Cluster要求至少3个主节点才能组成完整集群如果每个主节点还要配一个从节点用于故障转移最基础的生产配置就是3主3从。物理机资源如果不是很紧张尽量别做“单节点塞6个Pod”这种极限操作Redis是CPU和内存敏感型服务多实例抢资源会让延迟变得不可控。2.2 网络与存储方案设计网络方案并不复杂但要按下面的思路设计建一个独立的命名空间比如redis-cluster后续资源都放在里面权限和网络策略好收敛。用Headless Service提供稳定的DNS地址Pod之间通过这些域名互相访问。集群内部总线端口16379不需要被外部访问但Pod之间必须有路由。如果需要限制外部流量可以单独建一个ClusterIP类型的Service来对业务暴露6379不要直接把StatefulSet里的某个Pod地址扔给业务用。存储方案我采用了动态PVC每个节点一块5Gi的数据盘具体大小你按实际缓存量调整。要注意的是StorageClass的回收策略如果你用的是Delete那删除PVC会连着把数据盘一起删掉千万别在没备份的情况下手滑。生产环境可以考虑保留PVC方便回滚。做完这些准备后信息整理如下资源类型名称/规划用途命名空间redis-cluster隔离所有Redis相关资源StorageClassstandard以当前环境默认值为准动态PV供给Serviceredis-cluster-headless提供稳定的Pod DNSStatefulSetredis-cluster管理6个Redis节点ConfigMapredis-cluster-config写入Redis配置文件端口6379 与 16379客户端访问与集群通信3. 核心实操一步步拉起一个三主三从的Redis集群3.1 建立命名空间与ConfigMap先创建命名空间后面所有命令都带-n redis-cluster避免混到别的环境里kubectl create namespace redis-cluster然后准备ConfigMap把所有Redis节点的公共配置放在这里。Redis Cluster要求必须开启集群模式数据持久化用AOF形式密码和主从认证也必须在配置文件里统一设置。下面是redis-cluster-config.yamlapiVersion: v1 kind: ConfigMap metadata: name: redis-cluster-config namespace: redis-cluster data: redis.conf: | port 6379 cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 cluster-require-full-coverage no appendonly yes appendfilename appendonly.aof dir /data protected-mode no requirepass YourStrongPassword masterauth YourStrongPassword maxmemory 256mb maxmemory-policy allkeys-lru逐个说明关键参数。cluster-enabled yes这是集群模式的总开关不开的话后面初始化直接报错。cluster-config-file nodes.conf每个节点都会生成自己的集群状态文件这个文件必须放在可写目录里也就是下面的dir /data。cluster-node-timeout 5000判断节点超时的时间太短会误判网络抖动太长会影响故障转移速度5秒在大多数内网环境足够。cluster-require-full-coverage no这个参数容易忽略。默认情况下一旦有槽位没有覆盖到集群会拒绝写入。生产环境宁可牺牲一点一致性也要保证部分节点故障时服务还能用所以我设置成no。requirepass和masterauth两个值必须一致前者限制客户端访问后者让主从和集群节点间认证通过。只在配置文件里设一个集群内部通信会被拒绝。maxmemory-policy allkeys-lru所有key都用LRU策略淘汰避免内存打满后Redis变成只读也避免触发系统OOM把Pod杀掉。从Redis 7开始如果集群节点所在Pod的IP会变化还可以启用cluster-announce-hostname让节点之间通过稳定的Pod域名通信。生产环境建议开启但演示环境我们先按最直接的Pod IP初始化。创建并检查kubectl apply -f redis-cluster-config.yaml kubectl get configmap redis-cluster-config -n redis-cluster3.2 创建Headless ServiceHeadless Service和普通Service的区别是clusterIP: None。这样一来Kubernetes不会为它分配ClusterIP而是直接把DNS记录解析到每个Pod的IP上。对于StatefulSet里的PodDNS会变成podName.serviceName.namespace.svc.cluster.local这种格式。关键点是publishNotReadyAddresses: true。默认情况下Kubernetes只把Ready状态的Pod加入Service Endpoint但我们初始化集群时Pod可能还没进入集群状态如果DNS查不到所有节点redis-cli --cluster create就不知道该连谁。加上这个参数后无论Pod是否Ready只要没有被删除都能被DNS解析到。apiVersion: v1 kind: Service metadata: name: redis-cluster-headless namespace: redis-cluster labels: app: redis-cluster spec: clusterIP: None publishNotReadyAddresses: true ports: - name: redis port: 6379 targetPort: 6379 - name: clusterbus port: 16379 targetPort: 16379 selector: app: redis-cluster应用命令kubectl apply -f redis-cluster-headless.yaml3.3 用StatefulSet部署6个节点这是最核心的一块。StatefulSet会通过volumeClaimTemplates为每个Pod生成一块独立的PVC分配名类似>apiVersion: apps/v1 kind: StatefulSet metadata: name: redis-cluster namespace: redis-cluster spec: serviceName: redis-cluster-headless replicas: 6 selector: matchLabels: app: redis-cluster template: metadata: labels: app: redis-cluster spec: terminationGracePeriodSeconds: 30 containers: - name: redis image: redis:7.2-alpine command: - /bin/sh - -c - | redis-server /etc/redis/redis.conf ports: - name: client containerPort: 6379 - name: clusterbus containerPort: 16379 readinessProbe: exec: command: - /bin/sh - -c - redis-cli -a YourStrongPassword -p 6379 ping | grep -q PONG initialDelaySeconds: 10 periodSeconds: 10 livenessProbe: exec: command: - /bin/sh - -c - redis-cli -a YourStrongPassword -p 6379 ping | grep -q PONG initialDelaySeconds: 20 periodSeconds: 15 resources: requests: cpu: 100m memory: 256Mi limits: cpu: 500m memory: 512Mi volumeMounts: - name: redis-config mountPath: /etc/redis - name: data mountPath: /data volumes: - name: redis-config configMap: name: redis-cluster-config volumeClaimTemplates: - metadata: name: data spec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi有几点细节值得解释。镜像用了redis:7.2-alpine这是Redis官方镜像的一个轻量tag里面已经带了redis-cli和redis-server省得再单独装客户端。如果你所在环境访问不了公共镜像仓库可以提前把镜像同步到内网镜像服务然后对应修改image字段。sleep目录没有额外做脚本因为ConfigMap已经把配置作为/etc/redis/redis.conf挂载进去启动命令里直接用redis-server /etc/redis/redis.conf加载。注意容器启动时当前用户如果是默认rootRedis进程能访问/data但很多云环境出于安全考虑会强制使用非root用户此时需要给PVC加fsGroup之类的方式处理权限后面专门讲这个坑。resources里的limits不能乱设。如果你在Redis配置文件里设置了maxmemory 256mb而Pod内存limit是512Mi最终Redis能用的内容只有256Mi还不会触发系统OOM。但如果你limit设成128Mi比Redis的maxmemory还小进程被内核杀掉的风险就很高。踩坑经验是先设Pod内存limit再设maxmemory并留出至少20%余量给AOF缓冲和进程本身。创建后等待Pod全部Runningkubectl apply -f redis-cluster-statefulset.yaml kubectl get pods -n redis-cluster -o wide正常情况下你会看到redis-cluster-0到redis-cluster-5一共6个Pod状态为RunningREADY显示1/1。3.4 初始化集群分配主从与槽位6个Redis进程现在都只是独立节点还没有组成集群。我们需要用redis-cli --cluster create把它们绑在一起同时指定每个Master带几个副本。--cluster-replicas 1就是3主3从。先找到所有Pod的IPkubectl get pods -n redis-cluster -l appredis-cluster -o jsonpath{range .items[*]}{.status.podIP}:6379 {end}把输出的IP列表带入到下一段命令。执行初始化选择一个Pod作为入口比如redis-cluster-0kubectl exec -it redis-cluster-0 -n redis-cluster -- \ redis-cli -a YourStrongPassword \ --cluster create \ 10.244.1.3:6379 10.244.1.4:6379 10.244.1.5:6379 10.244.2.3:6379 10.244.2.4:6379 10.244.2.5:6379 \ --cluster-replicas 1你会看到Redis自动把前三个IP安排成Master后三个IP安排成Replica并给出槽位分配计划确认后输入yes。执行完输出里会包含类似下面的信息[OK] All 16384 slots covered.到这一步集群已经初始化完成。整个流程里最容易被忽略的是这6个IP必须是所有节点互相之间都能访问到的地址不要用localhost也不要用ClusterIP Service否则初始化虽然成功其他节点记录的就是错误地址。3.5 验证读写和自动故障转移初始化完成后验证一下集群状态。查看集群信息kubectl exec -it redis-cluster-0 -n redis-cluster -- \ redis-cli -a YourStrongPassword -c cluster info重点看cluster_state:ok和cluster_slots_assigned:16384。再看节点视图kubectl exec -it redis-cluster-0 -n redis-cluster -- \ redis-cli -a YourStrongPassword -c cluster nodes输出里每个节点前面有M或S标记分别代表Master和Slave。然后做一个跨节点写入读取测试写入某个键后如果当前节点不是该键槽位的Master-c模式会自动跳转。kubectl exec -it redis-cluster-0 -n redis-cluster -- \ redis-cli -a YourStrongPassword -c set site hello-cluster kubectl exec -it redis-cluster-3 -n redis-cluster -- \ redis-cli -a YourStrongPassword -c get site第二条命令如果从某个非槽位Master的节点执行也能正确返回hello-cluster说明集群路由正常。到这里一个可用集群已经起来了。但“可用”和“能稳定运行”之间还差着一堆细节下面把实操中常见的坑集中说一下。4. 常见问题与排错经验实录4.1 集群初始化报错怎么办初始化时最常见的就是报[ERR] Node xxx is not empty。这通常意味着某个Redis节点里已经存在数据或旧配置常见原因有两个一是之前做过单机测试AOF文件虽然没数据但nodes.conf残留了二是PVC里带了上一轮集群的节点ID信息。解决方案是把旧集群清理干净确保没有集群信息残留kubectl delete statefulset redis-cluster -n redis-cluster kubectl delete pvc -n redis-cluster -l appredis-cluster如果是测试环境可以这样粗暴处理如果是生产环境先备份/data下内容再删除。另一个常见报错是[ERR] Not all 16384 slots are covered by nodes。这代表槽位分配失败了通常是因为某个节点在初始化过程中掉线。检查所有Pod是否Running然后清空重试。还有一次我遇到过初始化命令一直卡住不动原因是集群节点之间16379端口不通。可以先进入一个Pod验证kubectl exec -it redis-cluster-0 -n redis-cluster -- \ redis-cli -p 6379 -a YourStrongPassword cluster nodes如果发现某个节点状态是disconnected就要回过去查CNI网络策略别在初始化命令上反复磨。4.2 PVC一直Pending数据写不进去创建StatefulSet后>kubectl describe pvc>spec: securityContext: fsGroup: 1000fsGroup让Kubernetes在挂载Volume时把目录组权限调整为1000这样用户ID为999的redis进程能写入/data。如果遇到SELinux开启的环境还需要确保PV层允许容器写入这类问题只能结合具体存储插件排查。4.3 重启Pod后集群状态异常如果你用的初始化方式是Pod IPPod一旦被重新调度到别的节点IP变了其他节点里记录的还是旧IP就会出现“节点状态持续断连”的情况。在自愈能力强的Kubernetes集群里这几乎是生产环境一定会遇到的问题。两个方向解决。简单粗暴的方向是给Pod分配固定IP让重调度后IP不变但这依赖网络插件能力不是所有环境都能做到。更通用的方向是把集群节点的广播地址改成稳定DNS也就是Redis 7的cluster-announce-hostname。改造思路是每个Pod通过环境变量知道自己序号和Headless Service名称然后动态拼出自己的域名写进配置。引入这个机制后cluster nodes输出里看到的是域名而不是IPPod重建后只要DNS不变集群就能自动恢复联系。4.4 性能与资源限制带来的坑比较隐蔽的是内存limit和maxmemory的关系。很多文档只让你在Redis配置里写maxmemory 512mb但忘了Pod limit结果Kubernetes cgroup把Redis进程直接杀掉。反过来如果limit给了2Gi但maxmemory只有256MiRedis虽然不会被打死但在大量写入时会出现明显的淘汰波动。个人建议先按业务预估缓存总量再设置limit maxmemory * 1.2到1.3比如maxmemory 512MiPod limit至少留到600Mi。另一个性能坑是AOF落盘策略。appendfsync默认是everysec对大部分场景够用。如果你在容器里把AOF策略改成always写入吞吐会明显下降而且每次fsync都可能因为底层存储抖动导致Redis挂起。容器环境下的磁盘性能有限不建议追求极限一致性。cluster-node-timeout也要注意。我最初用默认的15000ms主节点挂掉后从节点要等很久才发起选举业务恢复时间偏长。内网环境下改成5000ms比较合适但也不要低于2000ms否则短暂的GC停顿会被误判成节点宕机引发频繁的选举切换。5. 运维扩展扩容、缩容与备份恢复的实用建议5.1 平滑扩容一个从节点当业务增长需要给某个Master增加从节点时最简单的做法是基于现有StatefulSet把副本数加1kubectl scale statefulset redis-cluster -n redis-cluster --replicas7新Pod起来后确认状态正常然后执行add-node把它加入到集群。这里需要从已有的任意节点取一个地址新节点地址是redis-cluster-6示例kubectl exec -it redis-cluster-0 -n redis-cluster -- \ redis-cli -a YourStrongPassword \ --cluster add-node \ redis-cluster-6.redis-cluster-headless.redis-cluster.svc.cluster.local:6379 \ redis-cluster-0.redis-cluster-headless.redis-cluster.svc.cluster.local:6379 \ --cluster-slave不带--cluster-slave时它会成为一个新Master带上了就是某个Master的从节点。Redis会自动选择哪个Master挂复制关系你也可以在命令后加--cluster-master-id 目标MasterID指定。扩容后需要重新分配槽位如果是新Master就用reshard手动迁移槽位如果是纯从节点不需要迁移。多数情况下补充从节点比新增Master简单。5.2 缩容的正确姿势缩容不能直接kubectl scale down。Redis集群里如果还有槽位或副本数据直接删Pod会导致集群认为节点宕机严重时可能丢数据。标准流程是如果目标节点是Master先执行reshard把它的槽位全部迁移走。执行cluster forget让集群其他节点把它遗忘。最后再缩容StatefulSet副本数并且确认PVC删除计划。迁移槽位命令较长建议交互式执行kubectl exec -it redis-cluster-0 -n redis-cluster -- \ redis-cli -a YourStrongPassword \ --cluster reshard 某个保留节点的地址 --cluster-from 要下线节点ID --cluster-to 接收槽位节点ID --cluster-slots 4096槽位数要跟该Master持有的一致每轮迁移数量不要太大否则迁移期间网络开销会明显上升。缩容前务必检查是否有客户端正在使用要下线节点最好先通知业务切流。5.3 备份恢复思路Redis持久化开了AOF但它只解决“进程退出后恢复数据”的问题解决不了“误删除所有key”的问题。日常备份还是建议用RDB快照方式。备份时不需要停服务找个节点直接生成RDB文件kubectl exec -it redis-cluster-0 -n redis-cluster -- \ redis-cli -a YourStrongPassword --cluster-backup其实更通用的做法是执行redis-cli --rdb dump.rdb它会把当前节点的完整数据写到dump.rdb文件里。然后把文件下载到本地kubectl cp redis-cluster/redis-cluster-0:/data/dump.rdb ./backup.rdb恢复时在空节点上暂时关闭cluster-enabled用AOF或RDB启动单机Redis再迁移数据如果只是想快速演示可以把RDB文件放回PVC目录然后重新初始化一个单节点Redis读取。要注意的是Redis Cluster的每个Master只持有部分槽位单节点备份只覆盖部分数据需要把所有Master节点的数据都做快照才能拼一个完整备份。我在实际部署中的体会是Kubernetes上跑Redis集群最大的敌人不是Redis本身而是你如何看待Pod的“临时性”。如果你非要用对待物理机的方式把Pod IP当固定资源来依赖一定会被动态调度反噬。用StatefulSet稳定域名加上Redis 7对域名的原生支持再配好存储和资源限制后期会省很多精力。最后再分享一个小技巧初始化集群后尽快在业务客户端里配置密码和超时重试Kubernetes里网络抖动频率比物理机高客户端不做好重试一次Pod原地重启就能让应用报出一堆连接异常。