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 具有以下特性:
- Pod 名称随机:
redis-7d4f9b-abc12,每次重建名称不同 - 网络标识不稳定:Pod IP 随重建变化
- PVC 绑定不确定:Deployment 通过标签(Label)选择 PVC,多 Pod 竞争同一 PVC 时行为不可预期
Redis 主从 / Cluster 的依赖:
- Redis Cluster 节点 ID 存储在
nodes.conf文件中,与持久化数据绑定 - Sentinel 模式需要通过主机名识别主从关系,Pod 名称变化导致 Sentinel 监控混乱
- 主从复制使用
SLAVEOF <master-host>命令,master-host 必须是稳定的 DNS 名称
一旦 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 的本质区别:
- Operator:通过 CRD(自定义资源定义)扩展 K8s API,具备领域知识(如自动故障切换、自动扩缩容),行为更智能
- Helm Chart:模板化 K8s 资源文件,部署后不再主动管理,运维操作(如故障切换)仍需手动或脚本
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 |
关键选型原则:
reclaimPolicy: Retain:PVC 删除时保留底层存储卷,防止误删数据volumeBindingMode: WaitForFirstConsumer:延迟绑定,确保 PV 在 Pod 调度的同一 AZ 创建- 不使用
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 阈值)
实际计算示例:
maxmemory 8gb:Redis 主动限制在 8GBrequests.memory: 10Gi:预留 COW(写时复制)空间(BGSAVE/BGREWRITEAOF 期间父子进程共享内存,脏页 COW 膨胀)limits.memory: 12Gi:留出碎片率缓冲(mem_fragmentation_ratio 可能达 1.3-1.5)
切勿将 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 的核心原则:
-
必须用 StatefulSet:稳定的 Pod 名称和网络标识是 Redis 主从/集群运作的前提,Deployment 在任何情况下都不应用于有状态的 Redis 部署。
-
持久化存储选型:使用
reclaimPolicy: Retain防止数据意外删除;gp3 EBS 或等效的云 SSD 提供足够的 IOPS;volumeClaimTemplate确保每个 Pod 有独立且可复用的 PVC。 -
内存三层规划:maxmemory → requests(预留 COW)→ limits(预留碎片),层层留出缓冲。
-
内核参数:vm.overcommit_memory=1 和禁用 THP 通过 initContainer 在 Pod 启动时配置,是 Redis 稳定运行的必要条件。
-
备份验证:定期备份不等于安全,每月执行一次恢复演练,验证备份文件的可用性,是生产 Redis 运维的基本要求。