Redis 集群部署与调优实战:从主从到哨兵到 Cluster 的完整演进 原创
Redis 集群部署与调优实战:从主从到哨兵到 Cluster 的完整演进
Redis 作为最流行的内存数据库,几乎每个互联网后端都在用它扛缓存、队列和分布式锁。但当你的业务从一台机器上的小打小闹,长成需要扛住千万级 QPS 的分布式系统时,单机 Redis 的瓶颈会逐一暴露:单点故障、内存上限、吞吐天花板。
本文不讲理论空谈,直接用可复用的配置和脚本,带你走完 Redis 从单机 → 主从 → 哨兵 → Cluster的完整演进路线,并把持久化、内存管理、性能调优、监控告警、生产加固这些硬功夫一并讲透。
一、演进路线:什么时候该升级,选哪个方案
很多人一上来就想上 Cluster,其实是不必要的。选型的第一性原理是:用当前最小复杂度解决当前真实问题。下面这张表决定了你该站在哪一级台阶上:
| 阶段 | 读 QPS | 数据量 | 核心痛点 | 推荐方案 |
|---|---|---|---|---|
| 单机 | < 5万 | < 20GB | 可用性差,宕机即失忆 | 单机 + AOF |
| 成长 | 5万 ~ 20万 | 20~80GB | 读压力大、怕宕机 | 主从 + 读写分离 |
| 关键业务 | 20万 ~ 50万 | 80~150GB | 自动故障转移 | 哨兵 (Sentinel) |
| 大规模 | > 50万 | > 150GB / 多机房 | 容量、多写、水平扩展 | Cluster 分片 |
决策口诀:
- 怕丢数据 → 先上持久化,再谈高可用。
- 怕宕机 → 主从打底。
- 要自动切换、无需人工介入 → 上哨兵。
- 单机内存装不下、要水平扩展 → 上 Cluster。
注意:哨兵和 Cluster 不是替代关系。哨兵解决的是高可用(failover),Cluster 解决的是高扩展(分片)。如果你数据量不大但要求 99.99% 可用,哨兵足够;只有当你需要把数据摊到多台机器上时,Cluster 才登场。
二、主从复制:replicaof 与同步机制
主从复制是 Redis 高可用的地基。配置极其简单,在从节点 redis.conf 里加一行:
# 从节点配置 (redis-slave.conf) replicaof 10.0.0.10 6379 replica-read-only yes # 从节点只读,防止写错 repl-backlog-size 64mb # 复制积压缓冲区 repl-backlog-ttl 3600 # 积压缓冲区存活时间(秒) replica-priority 100 # 哨兵选举时从节点的优先级(越小越优先)
主从复制分两种模式:
- 全量同步 (Full Resync):从节点第一次连主,或复制中断过久、积压缓冲区已被覆盖时触发。主节点执行 BGSAVE 生成 RDB,传给从节点加载,期间新写入的命令记录在积压缓冲区里,RDB 传完后一并补发。耗时与数据量成正比,是阻塞主从的元凶。
- 增量同步 (Partial Resync):连接短暂中断后,从节点用
PSYNC <runid> <offset>带上自己的复制偏移量,只要该偏移量仍在主的积压缓冲区内,就能只补发缺失的命令,秒级恢复,几乎无感。
复制积压缓冲区调优是减少全量同步的关键。它是个环形缓冲区,用来保存最近写入的命令,以便断连的从节点追进度。调优要点:
# 按"断连期间可能产生的写入量"估算 backlog 大小 # 经验公式:repl-backlog-size ≈ 平均写吞吐(MB/s) × 期望容忍断连时间(s) # 例如写吞吐 5MB/s,希望容忍 60 秒断连: repl-backlog-size 512mb
如果 backlog 太小,从节点断连几十秒后重连,触发的不再是增量同步而是全量同步,主节点会瞬间 CPU 飙升、网络打满,甚至拖垮整个集群。所以 backlog 宁可大一点。
还要注意 避免主节点磁盘压力:全量同步时主节点执行 BGSAVE 会 fork 子进程,若磁盘慢会阻塞主线程。可开启 repl-diskless-sync yes,让主节点直接把 RDB 通过 socket 传给从节点,不落盘。
repl-diskless-sync yes repl-diskless-sync-delay 5 # 延迟几秒等待多个从节点同时同步,避免反复传
三、哨兵模式:自动故障切换
主从复制解决了”读扩展”和”宕机兜底”,但主节点宕机后从节点不会自动上位,需要人工介入。哨兵 (Sentinel) 就是来解决这个问题的:它监控所有 Redis 节点,主节点挂了自动把某个从节点提升为新主。
哨兵本身也要高可用,通常部署 3 个(奇数个)哨兵进程。核心配置:
# sentinel.conf port 26379 sentinel monitor mymaster 10.0.0.10 6379 2 sentinel auth-pass mymaster yourpassword sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 15000 sentinel parallel-syncs mymaster 1
关键参数逐个拆解:
sentinel monitor mymaster <ip> <port> 2:最后那个2是 quorum,表示至少 2 个哨兵认为主节点主观下线 (sdown) 后,才判定为客观下线 (odown) 并触发故障转移。quorum 必须小于等于哨兵数量的一半以上,否则无法达成共识。公式:quorum ≥ 哨兵总数/2 + 1。down-after-milliseconds 5000:哨兵在 5 秒内 ping 不通主节点,就主观判定它下线。这个值影响故障发现速度,太小会误判(网络抖动),太大会拖慢切换。建议 3000~10000。failover-timeout 15000:故障转移总超时,包含选举、晋升、通知的整个过程。parallel-syncs 1:新主产生后,同时允许多少个从节点去同步新主。设 1 避免一次全量同步打爆新主。
Leader 选举机制:当判定主节点客观下线后,哨兵之间会用 SENTINEL is-master-down-by-addr 相互拉票,采用类 Raft 的机制选出 Leader 哨兵,由它执行故障转移。选举规则:
- 候选哨兵给其他哨兵发投票请求,每个哨兵在故障转移超时时间内只投一票;
- 获得超过 quorum(且过半)选票的哨兵当选 Leader;
- Leader 从从节点里挑新主,优先选
replica-priority最小、复制偏移量最大、runid 最小的那个。
启动哨兵并验证:
# 启动 3 个哨兵 redis-server sentinel.conf --sentinel redis-server sentinel2.conf --sentinel redis-server sentinel3.conf --sentinel # 查看哨兵视角的主从状态 redis-cli -p 26379 sentinel masters redis-cli -p 26379 sentinel get-master-addr-by-name mymaster
客户端必须用哨兵 SDK(如 Redis 的 Sentinel 客户端),它会自动从哨兵拿当前主节点地址,并在故障切换后自动重连新主。如果用普通客户端直连旧主 IP,切换后应用就断了。
四、Redis Cluster:16384 槽位分片
当数据量突破单机内存,或写入也需要水平扩展时,上 Redis Cluster。它把整个键空间切成 16384 个哈希槽 (hash slot),公式为 slot = CRC16(key) % 16384,每个节点负责一部分槽位。
官方推荐至少 6 个节点(3 主 3 从)。每个节点开启 cluster 模式:
# redis-cluster-node.conf port 7000 cluster-enabled yes cluster-config-file nodes-7000.conf cluster-node-timeout 5000 appendonly yes
节点握手与初始化:
# 用 redis-cli 一键创建 3 主 3 从集群 redis-cli --cluster create \ 10.0.0.10:7000 10.0.0.11:7001 10.0.0.12:7002 \ 10.0.0.13:7003 10.0.0.14:7004 10.0.0.15:7005 \ --cluster-replicas 1
创建时 redis-cli 会自动把 16384 个槽位均分给 3 个主节点,并为每个主节点配 1 个从节点。
MOVED / ASK 重定向:这是 Cluster 的核心机制。客户端(如 redis-py-cluster、JedisCluster)会维护槽位与节点的映射表 (slot map)。当键的槽位不在当前节点时:
# 客户端没更新映射时,节点返回 MOVED 并告知正确节点 GET user:1001 (error) MOVED 14924 10.0.0.11:7001 # 迁移过程中的键,返回 ASK(临时迁移到目标节点) (error) ASK 14924 10.0.0.11:7001
- MOVED:槽位已永久归属另一节点,客户端应更新本地映射并重定向到新节点。属于正常的槽位归属变更。
- ASK:槽位正处于迁移中,客户端应先用
ASKING命令再访问目标节点,但不要更新映射(迁移完成后会收到 MOVED)。
这也是 Cluster 对客户端的硬性要求:必须用支持 Cluster 协议的客户端,普通客户端无法自动处理 MOVED/ASK。
集群伸缩(在线扩缩容):Cluster 最大的价值是可以在线加节点、搬槽位,不停服。
# 1. 加入新节点 redis-cli --cluster add-node 10.0.0.16:7006 10.0.0.10:7000 # 2. 把部分槽位从旧节点迁到新节点(交互式分配) redis-cli --cluster reshard 10.0.0.16:7006 # 3. 为扩容节点补从节点 redis-cli --cluster add-node 10.0.0.17:7007 10.0.0.16:7006 --cluster-slave # 4. 下线节点(先迁空它的槽位) redis-cli --cluster del-node 10.0.0.11:7001 <node-id>
槽位迁移是逐个键进行的,期间键处于迁移中间态,客户端用 ASK 处理,因此业务无感。但要注意:迁移大量 key 时会消耗带宽和 CPU,建议低峰期操作,且迁移粒度是 key 而非槽位整体,大 key 迁移尤其慢。
五、数据持久化:RDB vs AOF
内存数据再快,断电即失。持久化是 Redis 的命根子,两种方案各有取舍:
| 方案 | 机制 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| RDB | 定时 fork 子进程生成二进制快照 | 恢复快、文件紧凑、易备份 | 两次快照间可能丢数据、fork 耗内存 | 可容忍分钟级丢失的缓存 |
| AOF | 追加每一条写命令日志 | 数据丢失少,可到秒级 | 文件大、恢复慢 | 对数据一致性要求高的业务 |
生产上更推荐混合持久化(Redis 4.0+ 支持),同时开 RDB 和 AOF,兼顾两者的优点:
# redis.conf 持久化配置 save 900 1 # 900秒内至少1次写,触发RDB save 300 10 # 300秒内至少10次写 save 60 10000 # 60秒内至少1万次写 appendonly yes # 开启AOF appendfsync everysec # 每秒刷盘,性能与安全的平衡点 aof-use-rdb-preamble yes # 混合持久化:AOF文件头部是RDB,后续是增量命令 auto-aof-rewrite-percentage 100 # AOF体积增长100%触发重写 auto-aof-rewrite-min-size 64mb # AOF至少64MB才重写
AOF 重写优化:AOF 会无限增长,Redis 通过后台子进程重写 (rewrite),把当前内存状态重新生成一份紧凑的命令序列,替换旧 AOF,体积大幅缩小。触发条件就是上面两个 auto-aof-rewrite-*。手动触发:
redis-cli bgrewriteaof
重写期间新写入会先缓冲到 aof_rewrite_buf,重写完成后合并,不会阻塞也不丢数据。注意重写会 fork 子进程,内存占用峰值约为主进程的 1~2 倍,要给足物理内存或开启 vm.overcommit_memory=1。
appendfsync 三档选择:
always:每条命令都 fsync,最安全但最慢(QPS 骤降);everysec(推荐):每秒刷盘,最多丢 1 秒数据,性能几乎无损;no:交给操作系统决定刷盘时机,最快但可能丢较多数据。
六、内存管理:淘汰策略与碎片监控
Redis 是内存数据库,内存耗尽会直接拒绝写入甚至 OOM。必须设置 maxmemory 并选择淘汰策略:
maxmemory 4gb maxmemory-policy allkeys-lru
淘汰策略选型(重点):
| 策略 | 含义 | 适用场景 |
|---|---|---|
| noeviction | 内存满时拒绝写,报错 | 不允许丢任何数据的业务(如缓存了未落库的热数据) |
| allkeys-lru | 从所有键中淘汰最久未使用 | 通用缓存,热数据访问频次差异大 |
| volatile-lru | 仅淘汰设置了 TTL 的键中最久未使用 | 希望无 TTL 的键永不被淘汰 |
| allkeys-lfu | 按访问频次淘汰(LFU) | 热点集中、有”秒杀”等突发热key场景 |
| volatile-random | 从有过期时间的键中随机淘汰 | 对淘汰哪个不敏感 |
LRU vs LFU:
- LRU(最近最久未使用):看”多久没被访问”,适合访问分布均匀的场景,但对偶发访问的热 key 可能误杀(一次访问就能”续命”很久)。
- LFU(最近最少使用):看”访问频次”,通过计数衰减来反映真实热度,对”秒杀热 key”更精准,但实现复杂、占用更多内存。Redis 用近似算法,可通过
lfu-log-factor、lfu-decay-time调整。
实战建议:缓存业务默认 allkeys-lru;有明确热点(如商品详情、榜单)用 allkeys-lfu;不允许淘汰的核心数据用 noeviction 并配合监控预警。
内存碎片率监控:
redis-cli info memory | grep mem_fragmentation # mem_fragmentation_ratio: 1.4
mem_fragmentation_ratio = 已用物理内存 / Redis 实际数据内存。经验判断:
- 1.0 ~ 1.5:健康区间,正常。
- > 1.5:碎片较多,常见于大量删除/过期操作后,可执行
redis-cli memory purge或重启(低峰期);长期偏高可能是内存分配器不匹配(jemalloc vs glibc)。 - < 1.0:表示发生了 swap(内存交换到磁盘),极其危险,说明物理内存不足,应立刻扩容或清理。
七、性能调优:Pipeline、Lua、慢查询、bigkey
Redis 单线程模型意味着:一条慢命令就会阻塞所有后续命令。优化核心就是”少发命令、避免阻塞命令、消灭大 key”。
1. Pipeline 批量
网络 RTT 是 Redis 性能的最大杀手。一次请求一次往返,批量操作用 Pipeline 把多条命令一次性发到服务端,吞吐可提升数倍甚至一个数量级:
# Python 示例:Pipeline 批量写入 1000 个键
import redis
r = redis.Redis(host='10.0.0.10', port=6379)
pipe = r.pipeline(transaction=False)
for i in range(1000):
pipe.set(f'user:{i}', f'value{i}')
pipe.execute() # 一次性发送,而非1000次RTT
注意 Pipeline 不是原子操作,只是批量发送;需要原子性时用 Lua 脚本。
2. Lua 脚本原子操作
Redis 执行 Lua 脚本是原子的(整个脚本作为一条命令执行),适合需要”读-改-写”多步且要求原子性的场景,如分布式限流、秒杀扣库存:
-- 原子扣减库存,库存不足返回 -1
local stock = tonumber(redis.call('GET', KEYS[1]) or 0)
if stock < tonumber(ARGV[1]) then
return -1
end
redis.call('DECRBY', KEYS[1], ARGV[1])
return stock - ARGV[1]
# 执行 redis-cli EVAL "$(cat deduct.lua)" 1 stock:1001 1
脚本中禁止用 KEYS 遍历(会阻塞),且脚本要尽量短——脚本执行期间整个 Redis 是阻塞的。
3. 慢查询日志
# 慢查询配置:执行超过 10ms 的命令记入日志 slowlog-log-slower-than 10000 # 单位微秒 slowlog-max-len 128 # 查看最近慢查询 redis-cli slowlog get 10
生产上把阈值设在 10ms(10000 微秒),定期巡检 slowlog,重点排查:大 key 的 KEYS/DEL/SMEMBERS、批量 SORT、复杂 Lua、阻塞的 BLPOP。
4. bigkey 检测
大 key(如千万成员的 hash、几 MB 的 string)会让 DEL、GET 等命令阻塞,必须提前发现。Redis 内置的 --bigkeys 扫描:
redis-cli --bigkeys # Scanning the entire keyspace to find biggest keys... # Biggest string found so far 'token:10086' has 5242880 bytes # Biggest hash found so far 'user_tag' has 1000000 fields
它遍历整个键空间统计各类型最大的 key,用于定位。针对已知大 key 的处理:
- 避免 DEL 大 key:用
UNLINK key异步删除(不阻塞主线程)。 - 压缩/拆分:大 hash 拆成多个小 hash(如按用户 ID 分桶),或改用 string 存 JSON。
- TTL 兜底:给缓存 key 都设过期时间,防止无限增长。
八、监控告警:Redis Exporter + Prometheus + Grafana
生产环境必须有可观测性,否则故障发生全靠玄学。业界标准组合是 Prometheus + Grafana + redis_exporter。
1. 部署 redis_exporter 暴露指标:
# 用 systemd 或 docker 运行 exporter,抓取 Redis 指标 docker run -d --name redis-exporter -p 9121:9121 \ -e REDIS_ADDR=redis://10.0.0.10:6379 \ oliver006/redis_exporter:latest # 验证指标 curl http://10.0.0.10:9121/metrics | head
2. Prometheus 配置抓取:
# prometheus.yml
scrape_configs:
- job_name: 'redis'
static_configs:
- targets: ['10.0.0.10:9121', '10.0.0.11:9121', '10.0.0.12:9121']
3. 关键指标(Grafana 面板重点监控):
| 指标 | 含义 | 告警阈值建议 |
|---|---|---|
| redis_connected_clients | 连接数 | 超过 maxclients 的 80% 告警 |
| redis_memory_used_bytes | 已用内存 | > maxmemory 的 80% 告警 |
| redis_mem_fragmentation_ratio | 内存碎片率 | > 1.5 或 < 1.0 告警 |
| redis_rdb_bgsave_in_progress / aof_rewrite_in_progress | 持久化阻塞 | 持续 > 60s 告警 |
| redis_uptime_in_seconds | 运行时长 | 骤降=重启,立即告警 |
| redis_latest_fork_usec | 最近 fork 耗时 | > 1000ms 告警(可能阻塞) |
| redis_keyspace_hits / redis_keyspace_misses | 命中率 | 命中率 < 90% 关注(缓存可能失效) |
4. 告警规则示例(alertmanager):
# rules.yml
groups:
- name: redis-alerts
rules:
- alert: RedisMemoryHigh
expr: redis_memory_used_bytes / (redis_config_maxmemory * 1024 * 1024) > 0.8
for: 5m
labels: { severity: warning }
annotations:
summary: "Redis 内存使用超过 80%"
- alert: RedisDown
expr: up{job="redis"} == 0
for: 1m
labels: { severity: critical }
annotations:
summary: "Redis 实例 {{ $labels.instance }} 不可用"
- alert: RedisFragmentationHigh
expr: redis_mem_fragmentation_ratio > 1.5
for: 10m
labels: { severity: warning }
annotations:
summary: "Redis 内存碎片率过高,建议 memory purge"
Grafana 直接用现成的 redis-dashboard(ID: 763)导入,即可获得完整可视化面板。
九、生产部署 Checklist:安全加固与容灾
把 Redis 裸奔在内网甚至公网,等于把数据送人。以下清单在生产环境逐项核对:
1. 安全加固
# 只监听内网地址,禁止 0.0.0.0 bind 10.0.0.10 127.0.0.1 protected-mode yes # 设置访问密码(集群/哨兵各节点要一致) requirepass YourStrongPassword! masterauth YourStrongPassword! # 从节点连主时也要用 # 禁用危险命令,防误删和渗透 rename-command FLUSHALL "" rename-command FLUSHDB "" rename-command CONFIG "" rename-command KEYS "" # 限制最大连接,防资源耗尽 maxclients 10000
额外提醒:protected-mode yes 配合 bind 是底线;生产绝不能用默认密码或无密码直连;若必须对外,请置于 VPN/防火墙之后,而非暴露公网。
2. 备份策略
#!/bin/bash # backup_redis.sh:每日 RDB 备份 + 保留最近7天 BACKUP_DIR=/data/redis_backup mkdir -p $BACKUP_DIR # 触发 RDB 快照(或直接拷贝最新 dump.rdb) redis-cli -a $REDIS_PASS BGSAVE sleep 2 cp /data/redis/dump.rdb $BACKUP_DIR/redis_$(date +%F).rdb # 只保留最近 7 天 find $BACKUP_DIR -name "redis_*.rdb" -mtime +7 -delete # 可选:同步到异地/对象存储 # scp $BACKUP_DIR/redis_$(date +%F).rdb backup-server:/backup/
恢复:停止 Redis,把备份的 dump.rdb 放到数据目录,重启即可。若开了 AOF,重启时以 AOF 为准(更新)。
3. 容灾方案
- 同机房高可用:哨兵或 Cluster 的主从分布在不同物理机/机柜,避免单机宕机拖垮全部。
- 跨机房灾备:异地从节点(
replicaof指向异地主节点),或定期把 RDB 备份同步到异机房,满足 RPO 要求。 - 多级缓存兜底:Redis 之上再加本地缓存(如 Caffeine),Redis 挂了应用仍能抗住短时读请求,避免雪崩打到数据库。
- 故障演练:定期手动 kill 主节点,验证哨兵/Cluster 能否在预定时间内完成切换,别等真出事才发现脚本有问题。
结语:方案对比总表
最后用一张表收束全文。选型没有绝对最好,只有”当前阶段最合适”。把复杂度留给需要它的阶段,是运维最大的智慧。
| 方案 | 复杂度 | 可用性 | 扩展性 | 适用规模 | 典型用途 |
|---|---|---|---|---|---|
| 单机 | ★☆☆☆☆ | 低(宕机即挂) | 无 | < 20GB / 5万 QPS | 开发、小流量缓存 |
| 主从 + 读写分离 | ★★☆☆☆ | 中(手动切换) | 读可扩展 | 20~80GB / 20万 QPS | 读多写少的业务 |
| 哨兵 (Sentinel) | ★★★☆☆ | 高(自动切换,99.99%) | 读可扩展,写仍单点 | 80~150GB / 50万 QPS | 关键业务高可用 |
| Cluster 分片 | ★★★★★ | 高(自动故障转移) | 读写均可水平扩展 | > 150GB / 百万级 QPS | 海量数据、多机房、高并发写 |
希望这份从单机到 Cluster 的实战手册,能帮你把 Redis 的高可用、可扩展、可观测三条路一次性走通。生产环境多一分演练,上线就少一分惊魂。