跳过正文
Redis 运维实践:持久化配置、集群模式与生产监控

Redis 运维实践:持久化配置、集群模式与生产监控

·961 字·5 分钟·
目录

Redis 在我们的业务里承担了缓存、会话存储、分布式锁、消息队列等多个角色。运维了两三年,对几个关键决策点有了一些自己的判断。本文把这些经验整理出来,主要面向需要在生产环境管理 Redis 的运维和开发。

持久化选择:RDB vs AOF vs 混合模式
#

这个问题没有统一答案,核心是理解你能接受多少数据丢失。

RDB(快照)
#

RDB 是将内存数据快照写到磁盘,默认配置:

# 触发条件:N 秒内有 M 次写操作就触发
save 900 1      # 900 秒内至少 1 次写
save 300 10     # 300 秒内至少 10 次写
save 60 10000   # 60 秒内至少 10000 次写

# RDB 文件名和路径
dbfilename dump.rdb
dir /var/lib/redis

# 子进程写入失败时,停止接受写操作
stop-writes-on-bgsave-error yes

# RDB 文件压缩(CPU 换磁盘空间)
rdbcompression yes

RDB 适合的场景:

  • 可以接受最多几分钟的数据丢失(两次快照之间的数据)
  • 对恢复速度要求高(RDB 文件直接加载,比 AOF 快很多)
  • 做备份和灾备(RDB 文件结构紧凑,易于传输)

RDB 的问题:

  • 数据丢失窗口较大。如果在两次 save 之间宕机,最多丢失几百秒的数据
  • fork 子进程写 RDB 时会有短暂的内存峰值(COW 机制),大内存实例(32GB+)可能造成明显抖动

AOF(追加写日志)
#

AOF 记录每条写命令,重放可以恢复到最新状态。

appendonly yes
appendfilename "appendonly.aof"

# fsync 策略:这是最关键的配置
# always:每条命令都 fsync,最安全,性能最差
# everysec:每秒 fsync 一次,最多丢 1 秒数据,推荐
# no:由 OS 决定何时 fsync,性能最好,但宕机可能丢更多数据
appendfsync everysec

# AOF 重写触发条件
auto-aof-rewrite-percentage 100   # AOF 文件增长到上次重写后大小的 2 倍
auto-aof-rewrite-min-size 64mb    # 且文件大小超过 64MB

appendfsync everysec 是最常见的生产选择,在性能和数据安全之间取得平衡,最多丢 1 秒的写操作。

AOF 适合的场景:

  • 对数据丢失非常敏感(金融、订单类业务)
  • 写入量适中(AOF 文件过大会影响重放速度)

混合持久化(Redis 4.0+,推荐)
#

混合模式结合了 RDB 和 AOF 的优点:AOF 文件头部是 RDB 快照,后面追加增量 AOF。加载时先快速恢复 RDB,再重放少量 AOF,比纯 AOF 加载快很多。

appendonly yes
aof-use-rdb-preamble yes    # 开启混合持久化

生产环境我的推荐:开启混合持久化 + appendfsync everysec。既有 AOF 的数据安全性,又有接近 RDB 的加载速度。


部署模式:Sentinel vs Cluster
#

Redis Sentinel(哨兵)
#

适合单机数据量不大(内存 < 64GB)、需要高可用的场景。

┌──────────┐  ┌──────────┐  ┌──────────┐
│Sentinel 1│  │Sentinel 2│  │Sentinel 3│
└──────────┘  └──────────┘  └──────────┘
      │              │              │
      └──────────────┼──────────────┘
                     │
          ┌──────────┼──────────┐
          │          │          │
      ┌───────┐  ┌───────┐  ┌───────┐
      │Master │  │Slave 1│  │Slave 2│
      └───────┘  └───────┘  └───────┘

Sentinel 本身至少 3 个节点(保证选主时的多数投票),生产建议 Redis 节点 1 主 2 从,Sentinel 3 节点。

客户端连接 Sentinel,由 Sentinel 告知当前 Master 地址:

import redis

# 通过 Sentinel 获取连接
sentinel = redis.Sentinel([
    ('sentinel-1', 26379),
    ('sentinel-2', 26379),
    ('sentinel-3', 26379),
], socket_timeout=0.5)

# 获取 Master 连接(自动感知主从切换)
master = sentinel.master_for('mymaster', socket_timeout=0.5)
slave = sentinel.slave_for('mymaster', socket_timeout=0.5)

master.set('key', 'value')
value = slave.get('key')

Redis Cluster(集群)
#

适合数据量大、需要水平扩展的场景。数据按 16384 个 slot 分片,每个节点负责一部分 slot。

Node 1 (Master)  Node 2 (Master)  Node 3 (Master)
slots: 0-5460    slots: 5461-10922  slots: 10923-16383
    │                  │                    │
Node 4 (Slave)  Node 5 (Slave)   Node 6 (Slave)

选择边界:

  • 数据量 < 50GB、QPS < 10W:Sentinel 够用
  • 数据量 > 50GB 或需要横向扩展:考虑 Cluster
  • 业务代码用了 MGET/Pipeline 且 key 分散在不同 slot:需要用 HashTag 保证同 slot,或改写代码

K8s 上部署 Redis(Bitnami Helm Chart)
#

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

# Sentinel 模式部署
helm install redis bitnami/redis \
  --namespace redis \
  --create-namespace \
  -f redis-values.yaml

关键配置文件 redis-values.yaml

architecture: replication    # replication(主从+Sentinel)或 standalone

auth:
  enabled: true
  password: "your-strong-password"

sentinel:
  enabled: true
  masterSet: mymaster
  quorum: 2

master:
  persistence:
    enabled: true
    storageClass: gp3
    size: 20Gi
  resources:
    requests:
      memory: 2Gi
      cpu: 500m
    limits:
      memory: 4Gi
      cpu: 2000m

replica:
  replicaCount: 2
  persistence:
    enabled: true
    storageClass: gp3
    size: 20Gi
  resources:
    requests:
      memory: 2Gi
      cpu: 500m
    limits:
      memory: 4Gi
      cpu: 2000m

# 关键配置参数
commonConfiguration: |-
  maxmemory 3gb
  maxmemory-policy allkeys-lru
  appendonly yes
  aof-use-rdb-preamble yes
  appendfsync everysec
  hz 15
  tcp-keepalive 300

metrics:
  enabled: true
  serviceMonitor:
    enabled: true    # 配合 Prometheus Operator 自动发现

内存管理:maxmemory-policy 策略选择
#

这个配置直接影响缓存满了之后的行为,选错了要出事故。

maxmemory 4gb
maxmemory-policy allkeys-lru   # 选择淘汰策略

各策略说明:

策略行为适用场景
noeviction满了直接报错,拒绝写入不能丢数据的持久化场景(慎用)
allkeys-lru从所有 key 中淘汰最近最少使用纯缓存场景,推荐
volatile-lru只从设了 TTL 的 key 中淘汰 LRU混合存储(部分 key 无 TTL)的场景
allkeys-lfu从所有 key 中淘汰使用频率最低热点数据明显,LFU 比 LRU 更精准
allkeys-random随机淘汰几乎不用
volatile-ttl优先淘汰 TTL 最短的 key特定场景下有用

纯缓存场景推荐 allkeys-lruallkeys-lfunoeviction 是最危险的:内存满了,任何写操作都会返回 OOM 错误,包括更新现有 key,直接导致业务故障。


监控指标与 redis_exporter
#

用 redis_exporter 暴露 Prometheus 指标,配合 Grafana 监控。

# 部署 redis_exporter
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-exporter
  namespace: redis
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis-exporter
  template:
    metadata:
      labels:
        app: redis-exporter
    spec:
      containers:
        - name: redis-exporter
          image: oliver006/redis_exporter:v1.58.0
          env:
            - name: REDIS_ADDR
              value: "redis://redis-master.redis.svc.cluster.local:6379"
            - name: REDIS_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: redis
                  key: redis-password
          ports:
            - containerPort: 9121

关键监控指标:

# 缓存命中率(核心指标,低于 90% 要告警)
rate(redis_keyspace_hits_total[5m]) / 
(rate(redis_keyspace_hits_total[5m]) + rate(redis_keyspace_misses_total[5m]))

# 内存使用率
redis_memory_used_bytes / redis_memory_max_bytes

# 连接数
redis_connected_clients

# 每秒操作数
rate(redis_commands_processed_total[1m])

# 命令平均延迟(毫秒)
redis_commands_duration_seconds_total / redis_commands_processed_total * 1000

# 被驱逐的 key 数量(持续驱逐说明内存不够)
rate(redis_evicted_keys_total[5m])

# 复制延迟(主从同步是否正常)
redis_replication_lag

Grafana 告警规则建议:

  • 命中率 < 80%:告警
  • 内存使用率 > 85%:告警
  • 复制延迟 > 30 秒:告警
  • 连接数 > maxclients 的 80%:告警

生产常见问题处理
#

大 Key 扫描与处理
#

大 Key(Value 很大或集合类型元素很多)会阻塞 Redis 主线程,影响所有请求。

# 扫描大 Key(不会阻塞,推荐)
redis-cli --bigkeys -h redis-master -a yourpassword

# 输出示例
# Biggest string found so far 'user:profile:10001' with 524288 bytes
# Biggest list   found so far 'task:queue' with 50000 items
# Biggest hash   found so far 'product:details' with 10000 fields

# 查看具体 key 的大小
redis-cli -h redis-master -a yourpassword debug object <key>
# encoding:embstr serializedlength:524288 lru:...

# 对于大 Hash/List/Set,用 HSCAN/LRANGE/SSCAN 分批处理,不要用 HGETALL
redis-cli -h redis-master -a yourpassword
> HSCAN product:details 0 COUNT 100

处理大 Key 的原则:

  • 大 String:考虑压缩存储,或者拆分成多个小 Key
  • 大 List/Set:分页存储,用多个 Key 分片
  • 删除大 Key 用 UNLINK(异步删除),不用 DEL(同步,会阻塞)
# 安全删除大 Key
redis-cli -h redis-master -a yourpassword UNLINK bigkey:name

热 Key 处理
#

热 Key 是指被频繁访问的少数 Key,所有流量打到同一个 Redis 节点,可能造成单点过热。

诊断方式:

# Redis 4.0+ 的 hotkeys 功能
redis-cli --hotkeys -h redis-master -a yourpassword

# 或者用 monitor 抓取(生产慎用,会影响性能)
redis-cli -h redis-master -a yourpassword monitor | head -1000 | \
  awk '{print $4}' | sort | uniq -c | sort -rn | head -20

处理热 Key 的方法:

  1. 本地缓存:应用层对热 Key 结果做本地内存缓存(Caffeine/Guava Cache),TTL 设短一些(几秒到几十秒)
  2. Key 分散:将热 Key 复制成多份(hot_key:1hot_key:2…),读取时随机选一个
  3. Redis Cluster + 读从节点:把热 Key 的读操作分散到多个从节点

踩坑记录
#

坑1:AOF rewrite 期间内存暴涨
#

触发 AOF 重写时,Redis 会 fork 子进程来写新的 AOF 文件。期间父进程的写操作会走 COW(写时复制),如果写入量很大,内存可能翻倍。

我们有一次在业务高峰期触发了 AOF rewrite(文件增长到触发阈值),实例从正常使用 8GB 内存瞬间涨到 14GB,触发了 OOM Killer,Redis 进程被杀死。

应对措施:

# 方案1:在 rewrite 期间禁止 fsync(牺牲一点数据安全)
no-appendfsync-on-rewrite yes

# 方案2:调高 rewrite 触发阈值,避免频繁 rewrite
auto-aof-rewrite-percentage 200   # 增长到 200% 才触发
auto-aof-rewrite-min-size 512mb

# 方案3:调大内存 limits,给 rewrite 留足空间
# limits 建议是 maxmemory 的 1.5-2 倍

坑2:Redis Cluster 的 MOVED 错误
#

使用 Redis Cluster 时,客户端如果不支持 Cluster 协议,或者连接到了错误的节点,会收到 MOVED 错误:

(error) MOVED 3999 127.0.0.1:6380

这个错误的意思是:key 的 slot 在另一个节点上,请去那个节点操作。

解决方法:

  1. 使用支持 Cluster 的客户端库(Python 用 redis-py-cluster 或 redis-py 4.x,Java 用 Lettuce,Go 用 go-redis)
  2. 代码里不要直接 hardcode Redis 节点地址,用 Cluster 客户端连接,它会自动处理 MOVED 重定向
from redis.cluster import RedisCluster

# 正确的 Cluster 连接方式
rc = RedisCluster(
    host="redis-cluster.redis.svc.cluster.local",
    port=6379,
    password="yourpassword",
    decode_responses=True
)

rc.set("foo", "bar")    # 自动路由到正确节点

坑3:持久化目录满了导致 Redis 拒绝写入
#

有次 RDB 写入失败(磁盘满了),stop-writes-on-bgsave-error yes 配置让 Redis 直接停止接受所有写操作,业务全量报错。

# 检查 Redis 持久化错误
redis-cli info persistence | grep rdb_last_bgsave_status
# rdb_last_bgsave_status:err

# 临时解决(清理磁盘后重置错误状态)
redis-cli config set stop-writes-on-bgsave-error no
# 或者触发一次成功的 BGSAVE
redis-cli bgsave

长期解决:监控持久化目录磁盘使用率,提前告警。在 K8s 上用 PVC,配合 StorageClass 设置合理的容量,并监控 PVC 使用量。


小结
#

Redis 运维最重要的三件事:

  1. 持久化配置要匹配业务需求:纯缓存可以只开 RDB 或不持久化;对数据完整性有要求的,开混合持久化
  2. 内存策略要明确:永远不要在缓存场景用 noeviction,allkeys-lru/lfu 是更安全的默认选择
  3. 监控要到位:命中率、内存使用率、复制延迟这三个指标至少要有告警覆盖

我这几年碰到的 Redis 事故,基本都是这两块没做到位——配置拍脑袋 + 没监控。把监控和告警搞扎实,大半夜被叫起来的次数会少一大截。

Wenzhuo Huang
作者
Wenzhuo Huang
搞运维的工程师,写代码的运维人。专注 Kubernetes、AWS、GitOps 与基础设施可靠性。这个博客既是我的技术笔记本,也是我踩过的坑的受害者档案。

相关文章

PostgreSQL 膨胀治理:把 autovacuum 调到你真正需要的样子

·1457 字·7 分钟
大部分 PostgreSQL DBA 对 autovacuum 的理解停留在"它会自己跑",但一旦膨胀起来才发现:默认参数对现代硬件完全不够用,几十个 autovacuum_* 参数各管一摊,出了问题根本不知道从哪儿看。这篇文章把我在几套 PG 集群上治理膨胀的经验整理出来,从 MVCC 原理讲到参数调优、从监控到应急处置。