第 42 章

Redis in Kubernetes:StatefulSet 与持久化存储

第42章 Redis in Kubernetes:StatefulSet 与持久化存储

将 Redis 部署到 Kubernetes 是当今生产环境的主流选择,但 Redis 的有状态特性与 K8s 的无状态抽象之间存在根本矛盾。本章深入讲解为何必须使用 StatefulSet、主流部署方案对比、持久化存储配置,以及 K8s 环境下独有的运维细节与故障预防。


42.1 为何必须用 StatefulSet

42.1.1 Deployment 的致命缺陷

Kubernetes Deployment 为无状态应用设计,其 Pod 具有以下特性:

Redis 主从 / Cluster 的依赖

一旦 Pod 名称变化,节点 ID 与数据不对应,集群会陷入混乱状态,轻则需要手动 CLUSTER RESET,重则数据丢失。

42.1.2 StatefulSet 的核心优势

StatefulSet 专为有状态应用设计,提供:

特性 Deployment StatefulSet
Pod 命名 随机(deploy-pod-xyz) 有序(redis-0, redis-1)
网络标识 Pod IP(不稳定) 稳定 DNS 名称
存储绑定 竞争同一 PVC(危险) 每 Pod 独立 PVC(volumeClaimTemplate)
启动/停止顺序 并行(随机顺序) 有序(0→1→2 启动,2→1→0 停止)
滚动更新 并行替换 逐一替换(保证任一时刻只有一个 Pod 处于更新中)

稳定 DNS 名称(配合 Headless Service):

redis-0.redis-headless.namespace.svc.cluster.local
redis-1.redis-headless.namespace.svc.cluster.local
redis-2.redis-headless.namespace.svc.cluster.local

Pod 重建后,DNS 名称不变,其他 Pod 可以通过固定 DNS 名称重新建立连接。


42.2 Redis Operator 与 Helm Chart 对比

42.2.1 主流方案横向对比

方案 类型 支持模式 成熟度 适合场景
spotahome/redis-operator Operator Sentinel 中等 简单 HA 需求
ot-container-kit/redis-operator Operator Standalone / Cluster / Sentinel 较高 需要 CRD 管理
Redis Stack Operator(官方) Operator Standalone + 模块 较低(较新) 需要 RediSearch/RedisJSON
Bitnami Redis Helm Chart Helm Chart Standalone / Replication / Cluster 极高(最流行) 大多数生产场景
Bitnami Redis Cluster Helm Chart Helm Chart Cluster 极高 大规模 Cluster

Operator vs Helm Chart 的本质区别

42.2.2 Bitnami Helm Chart 完整部署示例

# values.yaml —— 主从 + Sentinel 模式
architecture: replication       # standalone | replication | cluster

auth:
  enabled: true
  password: "your-strong-password-here"
  # 生产推荐:从 K8s Secret 读取
  existingSecret: redis-auth-secret
  existingSecretPasswordKey: redis-password

master:
  count: 1
  persistence:
    enabled: true
    storageClass: "ssd-storage"
    accessModes:
    - ReadWriteOnce
    size: 20Gi
    # 重要:annotations 防止意外删除
    annotations:
      "helm.sh/resource-policy": keep
  resources:
    requests:
      cpu: "500m"
      memory: "2Gi"
    limits:
      cpu: "2000m"
      memory: "4Gi"
  # Redis 核心配置
  configuration: |
    maxmemory 3gb
    maxmemory-policy allkeys-lru
    activedefrag yes
    lazyfree-lazy-eviction yes
    lazyfree-lazy-expire yes

replica:
  replicaCount: 2
  persistence:
    enabled: true
    storageClass: "ssd-storage"
    size: 20Gi
  resources:
    requests:
      cpu: "250m"
      memory: "2Gi"
    limits:
      cpu: "1000m"
      memory: "4Gi"
  # 允许副本服务读请求(配合客户端读写分离)
  readinessProbe:
    enabled: true

sentinel:
  enabled: true                # 启用 Sentinel HA
  masterSet: "mymaster"
  quorum: 2
  downAfterMilliseconds: 5000
  failoverTimeout: 60000
  resources:
    requests:
      cpu: "100m"
      memory: "128Mi"
    limits:
      cpu: "200m"
      memory: "256Mi"

metrics:
  enabled: true                # 启用 redis-exporter 侧车
  serviceMonitor:
    enabled: true              # 创建 Prometheus ServiceMonitor

podSecurityContext:
  fsGroup: 1001
  runAsUser: 1001
  runAsNonRoot: true

# 节点亲和性:分散到不同物理节点
affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
    - labelSelector:
        matchLabels:
          app.kubernetes.io/name: redis
      topologyKey: kubernetes.io/hostname
# 安装
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install redis bitnami/redis \
  -f values.yaml \
  -n redis \
  --create-namespace \
  --version 18.x.x       # 锁定版本,避免自动升级

# 查看状态
kubectl get pods -n redis -w
kubectl get pvc -n redis

# 连接测试
kubectl exec -it redis-master-0 -n redis -- \
  redis-cli -a your-strong-password-here ping

42.3 持久化存储:StorageClass 设计

42.3.1 StorageClass 选型原则

存储类型 IOPS 延迟 适用场景
AWS gp2 EBS 3000 基准 开发/测试
AWS gp3 EBS 3000-16000(可配) 生产推荐
AWS io2 EBS 最高 64000 极低 高 IOPS 生产
GCP pd-ssd 最高 30 IOPS/GB GCP 生产
本地 NVMe (local-path) 极高 极低 高性能,无 HA

关键选型原则

  1. reclaimPolicy: Retain:PVC 删除时保留底层存储卷,防止误删数据
  2. volumeBindingMode: WaitForFirstConsumer:延迟绑定,确保 PV 在 Pod 调度的同一 AZ 创建
  3. 不使用 allowVolumeExpansion: false(关闭扩容):生产环境务必开启扩容能力

42.3.2 AWS gp3 StorageClass 配置

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: redis-ssd
  annotations:
    storageclass.kubernetes.io/is-default-class: "false"
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  iops: "6000"               # gp3 可独立配置 IOPS(3000-16000)
  throughput: "250"          # MB/s(125-1000)
  encrypted: "true"          # 静态加密
  kmsKeyId: "arn:aws:kms:..."  # 使用 CMK
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer

42.3.3 VolumeClaimTemplate(StatefulSet 专用)

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
spec:
  serviceName: redis-headless
  replicas: 3
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:7.2
        command: ["redis-server", "/etc/redis/redis.conf"]
        ports:
        - containerPort: 6379
        volumeMounts:
        - name: redis-data
          mountPath: /data
        - name: redis-config
          mountPath: /etc/redis
      volumes:
      - name: redis-config
        configMap:
          name: redis-config
  # volumeClaimTemplates:每个 Pod 自动创建独立 PVC
  volumeClaimTemplates:
  - metadata:
      name: redis-data
      annotations:
        "helm.sh/resource-policy": keep
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: redis-ssd
      resources:
        requests:
          storage: 20Gi
  # 生成的 PVC 名称:redis-data-redis-0, redis-data-redis-1, redis-data-redis-2

42.4 Service 设计与客户端路由

42.4.1 三种 Service 的作用

# 1. Headless Service:StatefulSet 稳定 DNS 的基础
apiVersion: v1
kind: Service
metadata:
  name: redis-headless
spec:
  clusterIP: None          # 无 ClusterIP = headless
  publishNotReadyAddresses: true   # Pod 未 Ready 时也发布 DNS(防止 Cluster 初始化失败)
  selector:
    app: redis
  ports:
  - name: redis
    port: 6379
    targetPort: 6379
---
# 2. 主节点 Service:写入入口(客户端读写不分离时的统一入口)
apiVersion: v1
kind: Service
metadata:
  name: redis-master
spec:
  selector:
    app: redis
    role: master            # 通过标签选择主节点 Pod
  ports:
  - port: 6379
    targetPort: 6379
---
# 3. 副本 Service:读入口(客户端读写分离时使用)
apiVersion: v1
kind: Service
metadata:
  name: redis-replica
spec:
  selector:
    app: redis
    role: replica
  ports:
  - port: 6379
    targetPort: 6379

Sentinel 模式下的注意事项:主从切换后,原 master Pod 变为 replica,但 Pod 标签不会自动更新。需要使用 Sentinel 客户端直接连接 Sentinel,由客户端动态解析当前主节点地址,而非通过 K8s Service 静态绑定。

42.4.2 Redis Cluster 的 Service 配置

Redis Cluster 需要客户端直接连接到各个节点(MOVED 重定向),因此:

# Cluster 模式:每个节点需要可寻址,使用 Headless Service
# 客户端连接入口:任意一个节点地址(用于初始握手,获取集群拓扑)
apiVersion: v1
kind: Service
metadata:
  name: redis-cluster-entry
spec:
  selector:
    app: redis-cluster
  ports:
  - port: 6379
    targetPort: 6379
  # 不使用 LoadBalancer,避免 MOVED 重定向时客户端无法直连

集群节点间通信使用 Bus 端口(6379 + 10000 = 16379),需确保节点间 16379 端口互通:

# NetworkPolicy 允许集群节点间通信
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: redis-cluster-internal
spec:
  podSelector:
    matchLabels:
      app: redis-cluster
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: redis-cluster
    ports:
    - port: 6379
    - port: 16379

42.5 资源限制与 OOM 预防

42.5.1 内存配置三层关系

maxmemory (Redis 配置)
    ↓ × 1.2~1.3
resources.requests.memory (K8s 调度依据)
    ↓ × 1.3~1.5
resources.limits.memory (K8s OOM Kill 阈值)

实际计算示例

切勿limits.memory < maxmemory。Redis 达到 maxmemory 后会执行淘汰策略(或拒绝写入),但不会主动释放内存到 limits 以下。若 limits < maxmemory,操作系统可能在 Redis 正常运作时触发 OOM Kill。

42.5.2 Linux 内核参数调优(通过 initContainer)

initContainers:
# 1. 禁用透明大页(THP)
- name: disable-thp
  image: busybox:1.35
  command:
  - /bin/sh
  - -c
  - |
    if [ -f /sys/kernel/mm/transparent_hugepage/enabled ]; then
      echo never > /sys/kernel/mm/transparent_hugepage/enabled
      echo never > /sys/kernel/mm/transparent_hugepage/defrag
    fi
    echo "THP disabled"
  securityContext:
    privileged: true
  volumeMounts:
  - name: sys
    mountPath: /sys

# 2. 设置 vm.overcommit_memory(避免 fork 失败)
- name: set-overcommit
  image: busybox:1.35
  command:
  - /bin/sh
  - -c
  - |
    sysctl -w vm.overcommit_memory=1
    sysctl -w net.core.somaxconn=65535
  securityContext:
    privileged: true

volumes:
- name: sys
  hostPath:
    path: /sys

为何需要 vm.overcommit_memory=1: Redis BGSAVE 通过 fork() 创建子进程。Linux 默认的 overcommit 模式(0)会在 fork 时检查是否有足够的物理内存来支撑子进程的虚拟地址空间(与父进程相同大小)。若 Redis 占用 8GB,但系统空闲内存不足 8GB,fork 会失败,导致 RDB 持久化失败并报错:

MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist on disk

设置 overcommit_memory=1 后,内核允许 overcommit,fork 不再做空间检查,只在实际写入时才分配物理页(COW 机制)。

42.5.3 OOM Score 管理

# 降低 Redis Pod 被 OOM Kill 的优先级(值越低越不容易被 kill)
# 通过 Pod QoS 类别间接控制(不建议直接修改 oom_score_adj,需要 privileged 权限)
# 最佳实践:为 Redis Pod 设置 Guaranteed QoS(requests == limits)
resources:
  requests:
    memory: "10Gi"
    cpu: "2000m"
  limits:
    memory: "10Gi"    # requests == limits → Guaranteed QoS
    cpu: "2000m"      # K8s 优先 Kill BestEffort > Burstable > Guaranteed

42.6 备份与恢复

42.6.1 CronJob 定期备份到 S3

apiVersion: batch/v1
kind: CronJob
metadata:
  name: redis-backup
  namespace: redis
spec:
  schedule: "0 2 * * *"        # 每天凌晨 2:00 UTC
  concurrencyPolicy: Forbid    # 禁止并发执行(防止上次未完成时新任务启动)
  successfulJobsHistoryLimit: 7
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      activeDeadlineSeconds: 3600   # 最长 1 小时
      template:
        spec:
          restartPolicy: OnFailure
          serviceAccountName: redis-backup-sa
          containers:
          - name: backup
            image: amazon/aws-cli:2.x
            env:
            - name: REDIS_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: redis-auth-secret
                  key: redis-password
            - name: S3_BUCKET
              value: "my-redis-backups"
            command:
            - /bin/sh
            - -c
            - |
              set -e
              DATE=$(date +%Y%m%d_%H%M%S)

              # 触发 BGSAVE 并等待完成
              redis-cli -h redis-master -a "$REDIS_PASSWORD" BGSAVE
              echo "Waiting for BGSAVE to complete..."
              while [ "$(redis-cli -h redis-master -a "$REDIS_PASSWORD" LASTSAVE)" = "$PREV_LASTSAVE" ]; do
                sleep 2
              done
              echo "BGSAVE complete"

              # 从 master Pod 复制 RDB 文件
              kubectl cp redis/redis-master-0:/data/dump.rdb /tmp/dump_${DATE}.rdb

              # 上传到 S3(带校验)
              aws s3 cp /tmp/dump_${DATE}.rdb \
                s3://${S3_BUCKET}/redis/$(date +%Y/%m/%d)/dump_${DATE}.rdb \
                --storage-class STANDARD_IA \
                --sse aws:kms

              echo "Backup complete: s3://${S3_BUCKET}/redis/$(date +%Y/%m/%d)/dump_${DATE}.rdb"

42.6.2 从 S3 恢复

# 1. 下载备份文件
aws s3 cp s3://my-redis-backups/redis/2024/01/15/dump_20240115_020005.rdb /tmp/dump.rdb

# 2. 停止 Redis(StatefulSet scale down)
kubectl scale statefulset redis-master -n redis --replicas=0

# 3. 将 RDB 文件复制到 PVC
kubectl run restore-helper --image=busybox --restart=Never \
  --overrides='{"spec":{"volumes":[{"name":"data","persistentVolumeClaim":{"claimName":"redis-data-redis-master-0"}}],"containers":[{"name":"restore-helper","image":"busybox","volumeMounts":[{"name":"data","mountPath":"/data"}],"command":["sleep","3600"]}]}}' \
  -n redis
kubectl cp /tmp/dump.rdb redis/restore-helper:/data/dump.rdb
kubectl delete pod restore-helper -n redis

# 4. 重新启动 Redis
kubectl scale statefulset redis-master -n redis --replicas=1

# 5. 验证数据
kubectl exec -it redis-master-0 -n redis -- redis-cli -a "$REDIS_PASSWORD" DBSIZE

42.7 监控与告警

42.7.1 关键 Prometheus 指标

# redis-exporter 暴露的关键指标(通过 ServiceMonitor 采集)

# 内存
redis_memory_used_bytes                    # 实际使用内存
redis_memory_max_bytes                     # maxmemory 配置值
redis_mem_fragmentation_ratio             # 内存碎片率(>1.5 告警)

# 连接
redis_connected_clients                    # 当前连接数
redis_connected_slaves                     # 副本连接数

# 持久化
redis_rdb_last_bgsave_status              # 最后一次 BGSAVE 状态(ok/err)
redis_aof_last_rewrite_duration_sec       # 最后一次 AOF rewrite 耗时

# 复制
redis_replication_offset                   # 主库复制偏移量
redis_slave_repl_offset                    # 从库复制偏移量(差距过大告警)
redis_replication_backlog_histlen          # 积压队列当前大小

# 性能
redis_commands_duration_seconds_total     # 命令执行总时间(计算 ops 延迟)
redis_keyspace_hits_total                  # 命中数
redis_keyspace_misses_total               # 未命中数

告警规则示例(PrometheusRule):

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: redis-alerts
spec:
  groups:
  - name: redis
    rules:
    - alert: RedisMemoryHigh
      expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.85
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "Redis memory usage > 85%"

    - alert: RedisReplicationLag
      expr: redis_replication_offset - redis_slave_repl_offset > 104857600  # 100MB
      for: 2m
      labels:
        severity: critical
      annotations:
        summary: "Redis replica lag exceeds 100MB"

    - alert: RedisFragmentationHigh
      expr: redis_mem_fragmentation_ratio > 1.5
      for: 10m
      labels:
        severity: warning
      annotations:
        summary: "Redis memory fragmentation ratio > 1.5"

本章小结

在 Kubernetes 上运行 Redis 的核心原则:

  1. 必须用 StatefulSet:稳定的 Pod 名称和网络标识是 Redis 主从/集群运作的前提,Deployment 在任何情况下都不应用于有状态的 Redis 部署。

  2. 持久化存储选型:使用 reclaimPolicy: Retain 防止数据意外删除;gp3 EBS 或等效的云 SSD 提供足够的 IOPS;volumeClaimTemplate 确保每个 Pod 有独立且可复用的 PVC。

  3. 内存三层规划:maxmemory → requests(预留 COW)→ limits(预留碎片),层层留出缓冲。

  4. 内核参数:vm.overcommit_memory=1 和禁用 THP 通过 initContainer 在 Pod 启动时配置,是 Redis 稳定运行的必要条件。

  5. 备份验证:定期备份不等于安全,每月执行一次恢复演练,验证备份文件的可用性,是生产 Redis 运维的基本要求。

本章评分
4.7  / 5  (3 评分)

💬 留言讨论