凌晨一点我守着监控大屏看到那台Redis虚拟机的磁盘占用曲线冲到 100%进程挂掉然后业务侧开始刷屏报错。因为没有副本主节点一死整个缓存层直接停摆。等人工重启、恢复完数据已经过去快一个小时。那次之后我在改造清单上写了一行字缓存层必须带副本必须能自动故障转移。落地的时候选的就是题目里这个方案——在k8s上部署Redis三主三从。这篇文章不聊官方文档那套漂亮话只讲我从架构选型、StatefulSet设计、配置注入、到集群初始化和故障演练这一路踩过的坑以及最后稳定跑下来的那套配置。适合两类人看一是正准备把Redis迁进k8s、但还没想清楚怎么管“状态”的团队二是已经能在k8s上跑无状态服务却对着有状态应用无从下手的同学。看完你至少能知道为什么三主三从比单机靠谱、StatefulSet比Deployment合理、cluster-announce-ip这个参数又是怎么把无数人坑到怀疑人生的。1. 先想明白一个问题三主三从到底要解决什么网上搜“三主三从”能搜到一堆指令和YAML但很少有人先把这个问题讲透。我在动手之前反复确认过需求因为“三主三从”这四个字背后其实藏着两种完全不同的架构取向一种是数据分片一种是高可用。你不分清楚后面所有配置都可能是白做。1.1 主从复制和数据分片是两套逻辑用一个生活里的类比来解释。主从复制就像你把一份重要合同复印了三份分别锁在三个保险柜里。原件被火烧了拿着复印件还能继续办事。数据在三个节点上完全一样多出来的副本只是用来兜底。数据分片则完全不同相当于你把一个大仓库拆成了三个独立的隔间一号隔间放前一万件商品二号隔间放一万到两万件三号隔间放剩下的。每个隔间只有一部分数据合在一起才构成完整数据集。Redis里的三主三从其实是把上面两件事叠加在一起三个主节点分别持有数据的一部分相当于分片每个主节点各自带一个从节点主节点上的数据在从节点上有一份全量复制相当于每个分片都做了高可用。这就是为什么我们经常说三主三从是Redis Cluster的最小高可用骨架。这个区分不是学术抠字眼它直接决定了你的选型。如果业务数据量很小全部塞进单台机器内存也绰绰有余你需要的只是“挂了别丢数据、别停服务”那做一主两从加Sentinel哨兵更合理没必要拆三份。反过来如果单机内存已经放不下全量数据或者写入并发已经打到单实例瓶颈就必须把数据拆到多个主节点也就是走上Cluster这条路。我做这套系统的时候项目里缓存数据量已经超过单机可承受的合理范围而且对缓存命中率、单点故障容忍度都有硬性要求。对应到技术上Cluster模式是当时唯一可以同时满足“数据自动分片”和“故障自动转移”的方案。1.2 为什么放在k8s上而不是继续用虚拟机我遇到过一个很常见的观点Redis这么成熟的东西虚拟机里跑了几十年非要搬到k8s上不是自找麻烦吗这个质疑有一定道理。Redis是有状态服务容器化之后最大的难点就是状态怎么保存、网络怎么固定、节点身份怎么稳定。虚拟机里这些是天经地义的事情容器里却要你手动设计。但我还是选择了k8s核心原因有两个。第一故障自愈。虚拟机方案下主节点挂了之后我只能靠外部监控脚本或者人工介入去触发Sentinel切换整个过程慢且脆。而Pod异常之后k8s本身会自动重建容器Cluster模式又会自动把从节点提升为主节点运维不需要在半夜爬起来敲命令。第二环境一致性。以前测试环境一套Redis、生产环境又是另一套配置漂移是家常便饭。容器化之后镜像、配置、网络策略全部用代码描述测试环境验证通过的部署方式拿到生产环境跑结果是一致的。对于多环境维护来说这个收益很高。代价当然也有你需要理解StatefulSet、PersistentVolumeClaim、Headless Service这些概念还要处理Pod重建后IP变化对Redis集群的冲击。后面这几节我主要就是围绕这些代价展开的。1.3 三种方案对比别一上来就照抄我在选型阶段列过一张对照表三种方案各有各的适用场景不存在绝对优劣。方案数据分片自动故障转移客户端要求适合场景单机主从Sentinel不支持所有节点数据一样支持由Sentinel完成普通客户端即可数据量小、单机内存够用主要诉求是高可用Redis Cluster三主三从支持按哈希槽分片支持集群内部自动完成必须支持Cluster协议的客户端数据量较大、写入并发较高同时需要高可用客户端分片/代理分片由客户端或代理决定数据位置通常依赖额外组件取决于实现业务访问模式特殊需要定制分片策略我最后选了第二种Redis Cluster三主三从因为项目里数据规模已经不允许单机一把梭了。后面所有实操内容也全部基于Redis Cluster模式。如果你只需要小数据量高可用可以去参考一主两从Sentinel的方案不必硬套这篇文章的Cluster配置。2. 架构设计StatefulSet、Headless Service和反亲和怎么配合架构设计阶段我反复推敲过三件事Pod用什么控制器管理、客户端通过什么方式访问、6个Pod怎么分散到不同物理节点上。这三件事想清楚后面的YAML写起来就顺了。2.1 为什么必须是StatefulSetDeployment不行第一次想到用Deployment部署Redis的时候我差点就把方案定了后来细想才发现不行。Deployment管理的是无状态应用它的Pod名字是随机的比如redis-abc12、redis-def34。这种随机性对Redis是致命的。Cluster模式启动之后每个节点会把其他节点的地址写进自己的nodes.conf文件。当某个Pod因为故障被重建Deployment会生成一个全新的Pod名字和IP新的IP地址跟其他节点记录的旧地址对不上整个集群的通信就会出问题。StatefulSet解决的就是这个问题。它给每个Pod一个稳定的、带序号的标识例如redis-cluster-0、redis-cluster-1一直到redis-cluster-5。这个命名是固定的Pod重建后名字不变。虽然IP仍然会变但配合后面要说的集群配置可以把IP变化的影响降到最低。另外StatefulSet天生支持volumeClaimTemplates也就是每个Pod都会自动绑定一份独立的持久化存储。这一点非常关键。Redis的AOF文件和RDB快照必须跟着Pod走Pod被重新调度到另一台机器之后数据还要能挂回来。用Deployment的话你得手动维护一堆独立的存储和Pod的绑定关系每天运维能烦死。所以我给的结论是在k8s上跑Redis Cluster控制器就用StatefulSet不要折腾其他方案。2.2 Headless Service在这里扮演什么角色StatefulSet通常要搭配一个Headless Service一起使用。所谓Headless就是Service的clusterIP设为None。这个Service不提供负载均衡IP而是给每个Pod分配一条固定的DNS记录。这样做的意义在于客户端或者集群内部组件可以通过稳定的Pod名字去解析到某个具体的Pod IP。比如redis-cluster-0这个Pod被重建了IP从10.244.1.5变成了10.244.2.8但只要通过DNS查询redis-cluster-0这个记录拿到的就是最新的IP不需要手工更新配置。在Redis Cluster里面节点之间本身是通过IP通信的所以Headless Service不是用来做负载均衡的它的作用是保证Pod的DNS名字稳定、可解析。我实践中也确实遇到过因为Service类型选错导致集群初始化找不到节点的情况。Headless Service的YAML比较简单先把形态放在这里后面部署部分还会给完整版本。apiVersion: v1 kind: Service metadata: name: redis-cluster-hs spec: clusterIP: None selector: app: redis-cluster ports: - name: redis port: 6379 targetPort: 6379 - name: cluster-bus port: 16379 targetPort: 163792.3 反亲和别让主和从睡在同一张床上三主三从共六个节点如果k8s调度器把它们全塞到一台物理机那这个集群跟单机没有任何区别。机器一挂六个Pod团灭高可用无从谈起。所以必须给Pod配置反亲和让它们尽可能分散到不同节点。这里面还要特别强调一点不光要防止两个主节点在同一台机器更要防止某一对主从节点在同一台机器。主从配对的含义是假如redis-cluster-0是某个分片的主redis-cluster-3是这个分片的从那么这两个Pod一定不能落到同一台物理节点上。否则这台节点宕机主挂了、从也跟着挂这个分片就彻底失去副本了。我在第一次部署时只用了一个很粗糙的podAntiAffinity配置让所有redis-cluster标签的Pod均匀分散。结果主从刚好落到了同一台机器上第一次故障演练就翻车了。后来改进的方案是在配置里明确指定反亲和的拓扑域一般以kubernetes.io/hostname为粒度。下面是一个软反亲和的配置示例意思是在调度时优先把相同标签的Pod分散到不同节点但不是强制约束。affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: redis-cluster topologyKey: kubernetes.io/hostname为什么用“优先”而不是“必须”因为强制反亲和时k8s要求同一标签的Pod不能出现在同一节点那6个Pod就需要至少6台节点否则调度直接Pending。很多测试环境根本没有那么多Node所以生产环境如果节点充裕可以用requiredDuringSchedulingIgnoredDuringExecution做硬约束节点不足时使用preferred加高weight是更现实的选择。严格区分主从配对的反亲和在单StatefulSet里其实比较难做到因为StatefulSet所有Pod的标签都一样调度器不会知道哪个是主哪个是从。我后续会用两个StatefulSet来做主从分离一个管理master节点一个管理replica节点给它们打上不同的标签再分别配置反亲和。这个属于进阶内容本篇给出一套可行的单StatefulSet方案日常测试和小规模生产够用。3. 配置细节把Redis的集群模式揉进容器写到这一节的时候我先说一句大实话Redis本身的配置文件并不复杂最折磨人的是“配置文件怎么塞进容器”以及“容器启动时怎么让配置生效”。我在这上面踩过两个大坑后面会详细说。3.1 ConfigMap里的redis.conf这些参数一个都不能少Redis的配置我用ConfigMap来管理。这样做的好处是配置与镜像分离改配置不需要重新构建镜像Pod滚动重启即可生效。下面是我在项目里使用的redis.conf配置文件内容带注释方便你理解每个参数的作用。# 开启集群模式这是三主三从架构的核心开关 cluster-enabled yes # 集群状态文件保存当前节点视角下的集群节点信息 # 必须放在持久化目录否则Pod重启后节点身份信息会丢 cluster-config-file /data/nodes.conf # 节点超时时间单位毫秒 # 如果另一个节点在这个时间内没有响应会被判定为故障 # 设成5000毫秒故障切换的时间就在几秒到十几秒的区间 cluster-node-timeout 5000 # 开启AOF持久化 appendonly yes # AOF刷盘策略 # everysec表示每秒刷一次性能影响小最多丢一秒数据 # always最安全但性能损耗大不要轻易用 appendfsync everysec # 这里我把RDB快照的默认策略注释掉了只用AOF # 如果你的场景需要快速全量恢复可以保留RDB策略 # save 3600 1 300 100 60 10000 # 关闭保护模式 # 容器环境下必须设置因为Redis默认只监听回环地址 protected-mode no # 最大内存限制 # 这个值必须小于容器内存Limit因为Redis在做持久化时 # 会通过fork子进程复制一份内存页表需要额外预留内存 maxmemory 1536mb # 内存淘汰策略 # allkeys-lru对绝大多数缓存场景都合适 maxmemory-policy allkeys-lru这里有一个非常容易踩的坑千万别在容器里的Redis配置中设置daemonize yes。Redis默认是前台运行也就是进程不退出、日志直接输出到标准输出。而容器要求主进程不能脱离前台。如果你开了daemonizeredis-server会fork到后台容器会以为主进程已退出直接触发Pod重启形成CrashLoopBackOff。这个坑我亲眼见过有人踩过反复重启还找不到原因。ConfigMap挂载到容器里的路径我用的是/etc/redis/redis.conf后面启动命令会引用这个路径。3.2 密码、masterauth和cluster-announce-ip三个关键点密码部分我的建议是不管集群是否暴露到外网都要设置密码。Redis在云上被扫描爆破的案例太多了不要裸奔。在Cluster模式下需要同时配置两个东西一个是requirepass表示客户端访问Redis时需要输入的密码另一个是masterauth表示当前节点在成为从节点时连接主节点需要用到的密码。很多人只配了requirepass忘记配masterauth导致主从切换时副本同步失败。这是Cluster模式下很典型的问题。密码和PodIP我都没有写死在redis.conf里而是通过k8s的Secret和Downward API注入到容器的环境变量再在启动命令里传给redis-server。具体实现是这样Secret存放密码环境变量REDIS_PASSWORD和REDIS_MASTERAUTH从Secret中读取环境变量POD_IP则从pod status.podIP字段读取。下面给出Secret和StatefulSet中的环境变量片段。apiVersion: v1 kind: Secret metadata: name: redis-cluster-secret type: Opaque stringData: redis-password: 请换成你自己的强密码 redis-masterauth: 请换成你自己的强密码env: - name: POD_IP valueFrom: fieldRef: fieldPath: status.podIP - name: REDIS_PASSWORD valueFrom: secretKeyRef: name: redis-cluster-secret key: redis-password - name: REDIS_MASTERAUTH valueFrom: secretKeyRef: name: redis-cluster-secret key: redis-masterauth接下来是cluster-announce-ip。Redis Cluster启动时节点会把自己当前的IP记录到nodes.conf中然后通过这个IP通知其他节点来连接自己。问题在于Pod在k8s里的IP不是固定的一旦Pod重建IP就会变化。如果Redis还拿着旧IP去通知别人客户端拿着返回的地址去连接会直接连不上表现为各种MOVED重定向失败。解决办法就是显式告诉Redis对外宣告的IP用当前Pod的IP。通过环境变量POD_IP把IP传给启动参数配置如下。args: - redis-server - /etc/redis/redis.conf - --requirepass - $(REDIS_PASSWORD) - --masterauth - $(REDIS_MASTERAUTH) - --cluster-announce-ip - $(POD_IP) - --cluster-announce-port - 6379 - --cluster-announce-bus-port - 16379这里面$(VAR)的写法是k8s容器启动时的变量替换不是shell的变量替换。k8s会用容器环境变量的实际值替换它。这个技巧可以说是整套方案里最关键的细节少了cluster-announce-ip外部客户端大概率会在故障转移后失联。3.3 存储与资源规划PVC不是可选项Redis是一个内存型数据库但它还要把数据持久化到磁盘。没有持久卷Pod重建之后数据直接蒸发这个集群就失去了意义。存储我使用StatefulSet自带的volumeClaimTemplates每个Pod自动生成一个PVC。PVC对应的存储类型我建议用本地SSD或者低延迟的云盘。Redis的AOF是边写边刷盘磁盘IOPS直接决定了写入性能上限。如果给Redis挂一个NFS网络存储AOF每次刷盘的延迟会高到让你怀疑人生。资源规划方面我给每个Pod的配置是CPU request 500m、limit 1000m内存request 1Gi、limit 2Gi。注意Redis的maxmemory配的是1536mb加上fork子进程所需的内存总体还有约400多MB的余量。如果limit和maxmemory设置得太接近Redis在触发RDB或AOF重写时可能会因为内存超限被k8s直接OOM杀掉。这里有一条计算公式供参考容器内存limit至少应该是maxmemory的1.5倍以上。2Gi的limit配1536mb的maxmemory比例大概在1.3倍接近临界如果你的数据量更大建议把limit扩到3Gimaxmemory设成2Gi。4. 手把手部署从零到六节点Redis Cluster跑起来前面铺垫了这么多现在进入核心落地环节。我按实际操作的顺序把Service、StatefulSet、集群初始化三部分完整走一遍。4.1 先打通网络普通Service和无头Service一起上我在这次部署中创建了两个Service一个是给客户端访问用的ClusterIP Service暴露6379端口另一个是Headless Service给StatefulSet的Pod提供稳定的DNS记录。普通的ClusterIP Service可以简单理解为一个入口客户端只需要连接这个固定IP不需要关心后面的Pod怎么变。但要注意一个细节Redis Cluster在做MOVED重定向时返回给客户端的是Pod的真实IP而不是Service的ClusterIP。所以客户端必须能够直接访问PodIP。如果客户端跑在同一个k8s集群内这个没问题如果客户端在集群外那还要额外处理Pod网络的暴露方式。我把两种场景整理成了表格。客户端位置访问方式需要注意的问题集群内PodClusterIP Service 允许访问PodCIDR客户端必须支持Redis Cluster协议能跟随MOVED重定向集群外机器NodePort或LoadBalancer端口映射6379和16379需要在Redis配置里把cluster-announce-ip改成客户端实际可达的地址正常业务场景下我建议Redis客户端也部署在同一个k8s集群里这样网络模型最简单跨Pod访问不需要额外打洞。下面是两个Service的完整YAML。apiVersion: v1 kind: Service metadata: name: redis-cluster-svc spec: selector: app: redis-cluster ports: - name: redis port: 6379 targetPort: 6379 - name: cluster-bus port: 16379 targetPort: 16379 --- apiVersion: v1 kind: Service metadata: name: redis-cluster-hs spec: clusterIP: None selector: app: redis-cluster ports: - name: redis port: 6379 targetPort: 6379 - name: cluster-bus port: 16379 targetPort: 16379这里要提醒一下16379端口是Redis Cluster的节点间总线端口不要漏掉。我之前有一回只暴露了6379结果集群初始化时节点之间互相通信失败排查了很久才发现是总线端口没通。4.2 StatefulSet完整YAML逐块拆开讲这是整套部署的核心。StatefulSet副本数设为6每个Pod分配一块持久卷并配置前面提到的环境变量、挂载、启动参数和反亲和。apiVersion: apps/v1 kind: StatefulSet metadata: name: redis-cluster spec: serviceName: redis-cluster-hs 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 command: - /usr/local/bin/docker-entrypoint.sh args: - redis-server - /etc/redis/redis.conf - --requirepass - $(REDIS_PASSWORD) - --masterauth - $(REDIS_MASTERAUTH) - --cluster-announce-ip - $(POD_IP) - --cluster-announce-port - 6379 - --cluster-announce-bus-port - 16379 env: - name: POD_IP valueFrom: fieldRef: fieldPath: status.podIP - name: REDIS_PASSWORD valueFrom: secretKeyRef: name: redis-cluster-secret key: redis-password - name: REDIS_MASTERAUTH valueFrom: secretKeyRef: name: redis-cluster-secret key: redis-masterauth volumeMounts: - name: data mountPath: /data - name: config mountPath: /etc/redis ports: - containerPort: 6379 name: redis - containerPort: 16379 name: cluster-bus resources: requests: cpu: 500m memory: 1Gi limits: cpu: 1000m memory: 2Gi livenessProbe: exec: command: - sh - -c - redis-cli -a $REDIS_PASSWORD ping | grep PONG initialDelaySeconds: 15 periodSeconds: 10 timeoutSeconds: 5 readinessProbe: exec: command: - sh - -c - redis-cli -a $REDIS_PASSWORD ping | grep PONG initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 volumes: - name: config configMap: name: redis-cluster-config items: - key: redis.conf path: redis.conf volumeClaimTemplates: - metadata: name: data spec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi这段YAML里有几个地方值得单独说明。探针这块我用redis-cli ping的方式来确认Redis进程真的能响应命令而不是只检查端口活着。这里有个小细节探针命令里直接用$(REDIS_PASSWORD)但探针的command是在容器启动后执行的shell命令它能不能访问环境变量取决于容器环境变量的注入这里因为REDIS_PASSWORD已经通过env定义所以能被shell访问到。这样探针就不会因为密码没传而误判失败。volumes里config这一段是把ConfigMap中的redis.conf文件挂载到容器的/etc/redis目录。这里要注意ConfigMap挂载默认会覆盖整个目录所以如果目录下还有其他文件要先想清楚。我这个场景没有所以直接挂没问题。volumeClaimTemplates生成的PVC名字会有规律比如data-redis-cluster-0对应redis-cluster-0的存储data-redis-cluster-1对应redis-cluster-1以此类推。这个规律在排查问题时很有用可以通过PVC名字快速定位是哪个Pod的存储。4.3 初始化集群让六个节点“认识”彼此六个Pod都起来之后它们各自都还是独立的Redis实例并没有形成集群。初始化这一步我建议用Redis自带的redis-cli来创建它会自动把16384个哈希槽均匀分配到三个主节点并且给每个主节点配上从节点。首先查看六个Pod的IP。kubectl get pods -l appredis-cluster -o wide拿到IP之后比如假设依次是10.244.1.10、10.244.1.11、10.244.1.12、10.244.1.13、10.244.1.14、10.244.1.15然后在任意一个Pod内执行集群创建命令。kubectl exec -it redis-cluster-0 -- redis-cli -a 换成你的密码 --cluster create \ 10.244.1.10:6379 10.244.1.11:6379 10.244.1.12:6379 \ 10.244.1.13:6379 10.244.1.14:6379 10.244.1.15:6379 \ --cluster-replicas 1注意在执行这条命令之前确认这六个Pod的数据目录都是空的。如果某个节点以前被初始化过、数据目录里已经存在nodes.conf或者AOF文件redis-cli会报错“Node is not empty”拒绝继续。处理方式是把对应PVC里的数据清掉或者重新创建一个干净的PVC。--cluster-replicas 1这个参数的意思是每创建完一个主节点自动指定一个从节点。在输入节点顺序上前三个会成为主节点后三个自动成为从节点。至于每个从节点具体配给哪个主节点redis-cli会随机匹配不一定是我前面举例里的对应关系。执行过程中redis-cli会打印出每个主节点的slot分配计划然后询问是否继续确认后进入正式创建。如果想跳过交互可以加--cluster-yes。初始化成功之后用cluster info检查一下集群状态。kubectl exec -it redis-cluster-0 -- redis-cli -a 换成你的密码 --no-auth-warning cluster info如果看到cluster_state:ok说明集群已经处于正常状态。如果显示cluster_state:fail说明slot没有全部分配或者部分节点失联回去查网络和IP。5. 验证、故障演练与避坑集群初始化成功只是第一步真正的考验在故障演练。这一节我把验证方法、演练过程和常见问题排查整理一遍这些经验是内部文档里不会写的东西。5.1 怎么判断三主三从真的建好了不要只看Pod数量是六个就以为大功告成。我通常会用两步验证。第一步用cluster nodes查看集群拓扑。执行命令kubectl exec -it redis-cluster-0 -- redis-cli -a 换成你的密码 --no-auth-warning cluster nodes输出每一行代表一个节点重点看两列第一列是节点ID第二列包含IP、端口和角色标志。角色标志中master表示主节点slave表示从节点。正常情况下你会看到三个master三个slave且每个master都对应一个slave。第二步检查哈希槽分配是否完整。用cluster slots命令更直观kubectl exec -it redis-cluster-0 -- redis-cli -a 换成你的密码 --no-auth-warning cluster slots输出会显示每个slot区间的起始、结束以及对应的主从节点。对照一下16384个槽应该不多不少全部分配完毕。还有一个运维层面的检查主从节点不能落在同一台物理节点上。这个通过综合cluster slots和kubectl get pods -o wide两步来核对。先看每个Pod的NODE列再看每个主从对分别属于哪个Pod最后确认没有任意一对主从属于同一台机器。这一步很繁琐但很重要。我建议写成一个小脚本定期巡检。5.2 主节点挂了会怎样一次实际故障演练记录理论说得再多不如亲手杀一个主节点看看效果。我通常用两种方式模拟故障。第一种方式删除Pod模拟Pod异常重启。这种方式最贴近真实的k8s场景。kubectl delete pod redis-cluster-0StatefulSet会自动重建redis-cluster-0新Pod的名字不变但IP会变。删除瞬间集群会检测到redis-cluster-0失联超过cluster-node-timeout之后它的从节点会发起failover成为新的主节点。执行完命令后等大约10到30秒再查看cluster nodes。我在演练中看到的现象是原主节点标记为fail状态它的从节点角色从slave变成了master所有原本属于旧主的slot分配到了新主上。旧Pod重建完成后作为新成员重新加入集群并自动在新的主节点上补齐数据角色变成slave。整个过程对客户端来说有短暂的连接中断和重定向但不影响整体服务。第二种方式模拟进程卡死不触发Pod重建。有时候我们想观察集群层面的故障转移而不想让Pod被重建可以在容器内执行debug sleep让Redis主进程阻塞一段时间。kubectl exec -it redis-cluster-0 -- redis-cli -a 换成你的密码 --no-auth-warning debug sleep 30这条命令会阻塞Redis主线程让它无法响应集群的心跳。等待超过node-timeout后集群同样会触发故障转移。这种方式对观察failover过程更有帮助因为它没有新旧Pod切换的干扰。演练之后务必确认两件事一是新主节点上的数据完整二是旧主恢复后能正常同步回最新数据。否则故障转移虽然发生了但数据一致性出问题服务依然不可用。5.3 常见问题速查与避坑记录在我维护这套系统的过程中遇到过不少奇奇怪怪的问题。我挑几个典型的高频问题整理成速查表希望你能少走弯路。现象根因处理办法Pod一直Pending提示节点资源不足PVC无法绑定或者反亲和过度约束检查storageclass是否可用节点数量是否满足反亲和要求必要时改用软反亲和集群初始化时报Node is not empty数据目录已有旧数据或nodes.conf清空/data目录或删除对应PVC重新创建客户端报MOVED错误但重定向连不上cluster-announce-ip未配置或配置的是旧IP确认Pod的env里有POD_IP启动参数里正确传入了--cluster-announce-ip主从切换后复制关系断掉只配了requirepass没配masterauth保持一致同时配置masterauth6379端口可访问16379不通总线端口未暴露或网络策略阻止检查Service、NetworkPolicy以及云平台安全组Redis容器反复重启redis.conf里开了daemonize yes删除daemonize配置让redis-server前台运行内存使用超限被OOM掉maxmemory设置过高接近容器内存limit按照limit的60%-70%设置maxmemory预留fork所需内存还有一个很隐蔽的坑单独拿出来说一下如果你在部署过程中改了ConfigMap里的redis.conf记得要通过重启Pod来让配置生效。修改ConfigMap本身不会自动触发Pod滚动很多人在配置改了之后发现Redis行为没变化以为是配置没生效其实是Pod压根没有重新加载配置。写在最后的一点体会这套三主三从的Redis Cluster在k8s上跑通之后我再回头看最大的收获不是会写那几个YAML而是想明白了有状态服务在容器平台上的生命周期管理状态要交给PVC、身份要交给StatefulSet、节点发现要交给地址宣告、容灾要交给反亲和和Cluster自身的故障转移机制。这几件事环环相扣少一环都会在故障来临的时候显形。如果你正准备做类似的迁移我的建议是先别急着抄YAML回到业务需求上想清楚两件事数据量到底需要不需要分片以及你的k8s集群有没有足够的节点来兜底。想清楚这两个问题后面的每一步就都顺了。