You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Kubernetes中Redis主从集群部分Pod无法写入的问题求助

问题:Kubernetes Redis StatefulSet仅主节点可写入,副本节点写入报错

尝试连接Kubernetes Pod中的Redis时,发现仅1个Pod(redis-0)可以正常读写,其余Pod执行写入操作时均报错:ReplyError: READONLY You can't write against a read only replica.

报错示例

ReplyError: READONLY You can't write against a read only replica.

相关配置文件

pv.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv1
spec:
  storageClassName: local-storage
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/storage/data1"

---

apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv2
spec:
  storageClassName: local-storage
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/storage/data2"

---

apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv3
spec:
  storageClassName: local-storage
  capacity:
    storage: 2Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/storage/data3"

redis-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: redis
spec:
  clusterIP: None
  ports:
  - port: 6379
    targetPort: 6379
    name: redis
  selector:
    app: redis

redis-statefulset.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
spec:
  serviceName: redis
  replicas: 6
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      initContainers:
      - name: config
        image: redis:alpine
        command: [ "sh", "-c" ]
        args:
          - |
            cp /tmp/redis/redis.conf /etc/redis/redis.conf
            
            echo "finding master..."
            MASTER_FDQN=`hostname  -f | sed -e 's/redis-[0-9]\./redis-0./'`
            if [ "$(redis-cli -h sentinel -p 5000 ping)" != "PONG" ]; then
              echo "master not found, defaulting to redis-0"

              if [ "$(hostname)" == "redis-0" ]; then
                echo "this is redis-0, not updating config..."
              else
                echo "updating redis.conf..."
                echo "slaveof $MASTER_FDQN 6379" >> /etc/redis/redis.conf
              fi
            else
              echo "sentinel found, finding master"
              MASTER="$(redis-cli -h sentinel -p 5000 sentinel get-master-addr-by-name mymaster | grep -E '(^redis-\d{1,})|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})')"
              echo "master found : $MASTER, updating redis.conf"
              echo "slaveof $MASTER 6379" >> /etc/redis/redis.conf
            fi
        volumeMounts:
        - name: redis-config
          mountPath: /etc/redis/
        - name: config
          mountPath: /tmp/redis/
      containers:
      - name: redis
        image: redis:alpine
        command: ["redis-server"]
        args: ["/etc/redis/redis.conf"]
        ports:
        - containerPort: 6379
          name: redis
        volumeMounts:
        - name: data
          mountPath: /data
        - name: redis-config
          mountPath: /etc/redis/
      volumes:
      - name: redis-config
        emptyDir: {}
      - name: config
        configMap:
          name: redis-config
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "local-storage"
      resources:
        requests:
          storage: 500Mi

sc.yaml

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
reclaimPolicy: Delete

客户端代码(Main.ts)

import Redis from "ioredis";

const redisClient = new Redis({
  port: 6379,
  host: 'redis',
  password: "a-very-complex-password-here"
})

(() => {
setInterval(async () => {
  try {
    await redisClient.set("TEST::" + Date.now(),"a")
  } catch (error) {
    console.log(error);
  }
},5000)

})()

Pod日志

Pod 1(redis-0)日志

[
    'TEST::1672209760072', 'TEST::1672209760073', 'TEST::1672149366766',
    'TEST::1672149491854', 'TEST::1672149136595', 'TEST::1672149081725',
    'TEST::1672149276699', 'TEST::1672209840134', 'TEST::1672209800104',
    'TEST::1672209655044', 'TEST::1672209659981', 'TEST::1672209594929',
    'TEST::1672209785102', 'TEST::1672209589922', 'TEST::1672209795107',
    'TEST::1672149296731', 'TEST::1672209735048', 'TEST::1672209700017',
    'TEST::1672209750066', 'TEST::1672209740054', 'TEST::1672149306733',
    'TEST::1672149086555', 'TEST::1672209593891', 'TEST::1672149256729',
    'TEST::1672209730062', 'TEST::1672209695012', 'TEST::1672149136714',
    'TEST::1672149181721', 'TEST::1672149221659', 'TEST::1672149456827',
    'TEST::1672209578963', 'TEST::1672149201722', 'TEST::1672209640039',
    'TEST::1672149186635', 'TEST::1672209625042', 'TEST::1672209610038',
    'TEST::1672209605038', 'TEST::1672149111711', 'TEST::1672149151714',
    'TEST::1672209634958', 'TEST::1672149391784', 'TEST::1672149516863',
    'TEST::1672209573871', 'TEST::1672209820125', 'TEST::1672209563958',
    'TEST::1672209560752', 'TEST::1672149521868', 'TEST::1672149291714',
    'TEST::1672209715059', 'TEST::1672209575748', 'TEST::1672209805109',
    'TEST::1672149526866', 'TEST::1672149261689', 'TEST::1672209565751',
    'TEST::1672209615041', 'TEST::1672209595035', 'TEST::1672149381772',
    'TEST::1672149311728', 'TEST::1672209590040', 'TEST::1672149141599',
    'TEST::1672149471832', 'TEST::1672149461828', 'TEST::1672209820119',
    'TEST::1672209725060', 'TEST::1672209588961', 'TEST::1672209790098',
    'TEST::1672209710024', 'TEST::1672149511859', 'TEST::1672149291731',
    'TEST::1672209578882', 'TEST::1672149191723', 'TEST::1672149141712',
    'TEST::1672209750070', 'TEST::1672149336740', 'TEST::1672209573963',
    'TEST::1672209780096', 'TEST::1672149396786', 'TEST::1672149391782',
    'TEST::1672149091708', 'TEST::1672209580751', 'TEST::1672149176629',
    'TEST::1672209585036', 'TEST::1672209553962', 'TEST::1672209730046',
    'TEST::1672209614945', 'TEST::1672149401788', 'TEST::1672149111579',
    'TEST::1672149361761', 'TEST::1672149236728', 'TEST::1672149416802',
    'TEST::1672149501854', 'TEST::1672149376772', 'TEST::1672209600035',
    'TEST::1672149201646', 'TEST::1672149511861', 'TEST::1672209845144',
    'TEST::1672209568866', 'TEST::1672209805112', 'TEST::1672149326734',
    'TEST::1672149146601',
    ... 219 more items
  ]

Pod 2(redis-1)日志

ReplyError: READONLY You can't write against a read only replica.
    at parseError (/usr/src/app/node_modules/redis-parser/lib/parser.js:179:12)
    at parseType (/usr/src/app/node_modules/redis-parser/lib/parser.js:302:14) {
  command: { name: 'set', args: [ 'UFUK::1672209926379', 'a' ] }
}
[]
ReplyError: READONLY You can't write against a read only replica.
    at parseError (/usr/src/app/node_modules/redis-parser/lib/parser.js:179:12)
    at parseType (/usr/src/app/node_modules/redis-parser/lib/parser.js:302:14) {
  command: { name: 'set', args: [ 'UFUK::1672209926381', 'a' ] }
}
ReplyError: READONLY You can't write against a read only replica.
    at parseError (/usr/src/app/node_modules/redis-parser/lib/parser.js:179:12)
    at parseType (/usr/src/app/node_modules/redis-parser/lib/parser.js:302:14) {
  command: { name: 'set', args: [ 'UFUK::1672209931382', 'a' ] }
}
[]

解决方案

问题根源

当前Redis StatefulSet是主从架构:redis-0是主节点(可读写),其余Pod都是从节点(只读,仅同步主节点数据)。而客户端连接的是Headless Service(redis),它会将请求轮询分发到所有Redis Pod,当写请求打到从节点时就会触发READONLY报错。

方案1:部署Redis Sentinel实现自动主从发现与读写分离

  1. 部署Redis Sentinel:创建Sentinel的StatefulSet/Deployment,配置它监控Redis主节点(mymaster)。Sentinel会自动监控主节点状态,当主节点故障时自动切换新的主节点,并通知客户端更新主节点地址。
  2. 修改客户端代码:使用ioredis的Sentinel模式连接,让客户端自动发现主节点,写操作发往主节点,读操作可选择发往从节点:
import Redis from "ioredis";

// 连接Sentinel集群,自动发现主节点
const redisClient = new Redis({
  sentinels: [
    { host: 'sentinel-0.sentinel', port: 26379 },
    { host: 'sentinel-1.sentinel', port: 26379 },
    { host: 'sentinel-2.sentinel', port: 26379 }
  ],
  name: 'mymaster', // Sentinel监控的主节点名称
  password: "a-very-complex-password-here"
})

// 写操作自动发往主节点
(() => {
setInterval(async () => {
  try {
    await redisClient.set("TEST::" + Date.now(),"a")
  } catch (error) {
    console.log(error);
  }
},5000)

})()

方案2:创建专属Service指向主节点

如果不需要自动故障切换,可以直接创建一个Service固定指向redis-0(主节点),客户端连接这个Service进行写操作:

apiVersion: v1
kind: Service
metadata:
  name: redis-master
spec:
  ports:
  - port: 6379
    targetPort: 6379
  selector:
    statefulset.kubernetes.io/pod-name: redis-0

修改客户端代码的host为redis-master:

const redisClient = new Redis({
  port: 6379,
  host: 'redis-master', // 指向主节点专属Service
  password: "a-very-complex-password-here"
})

补充说明

当前StatefulSet的initContainer中已经包含了Sentinel相关的配置逻辑,但实际并未部署Sentinel组件,所以所有非redis-0的Pod默认被配置为redis-0的从节点。如果选择方案1,需要完善Sentinel的部署,确保Sentinel集群正常运行,这样主节点故障时能自动切换,客户端也能自动感知新的主节点。


内容的提问

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 23:40:29