单机 Redis 挂了,整个网站就白屏
给网站加 Redis 缓存是提速的常规操作,我之前也写过单机 Redis 的接入。但很多人(包括当年的我)会把 Redis 当成"反正是缓存,挂了无所谓"的东西,直到某天凌晨 Redis 进程因为 OOM 被系统杀掉,而你的 PHP 代码里 new Redis() 后第一句就是 get(),于是所有动态页面集体 500,网站直接白屏——缓存不但没保住速度,反而成了单点故障。
高级一点的站长会加个 try/catch 降级,但这只解决了"挂掉时不崩",没解决"挂掉后多久恢复"。Redis 官方给出的高可用方案叫 Sentinel(哨兵):一组独立进程,持续监控主从节点,主节点挂掉时自动选举一个从节点提升为主,并通知应用切换。本文记录我在三台小内存 VPS 上搭 Redis Sentinel 主从高可用的完整过程。
Sentinel 到底解决了什么,又没解决什么
先说清楚定位,避免期望错位:
- 解决了:主节点宕机时的自动故障转移(failover)。不需要人工登录去改配置、去
SLAVEOF。 - 解决了:故障后应用能通过"问哨兵谁是主"来拿到新的主节点地址,实现自动重连。
- 没解决:数据分片。Sentinel 只管高可用,不管你数据放不下的问题。那是 Redis Cluster 的活。
- 没解决:数据一致性。Redis 主从是异步复制,主宕机时可能丢掉最后一小段写入。所以关键业务数据不能只存在 Redis 里。
对个人站来说,一台主 + 一台从 + 三个哨兵(或同机混合部署)已经能覆盖绝大多数需求。
架构规划:三节点的最小高可用拓扑
Sentinel 有个硬性要求:至少三个哨兵实例,才能形成多数派(quorum)做决策。两个哨兵的集群在一台宕机时无法判定另一台是死是活,会陷入脑裂。所以标准做法是:
机器 A (10.0.0.11): redis-master (6379) + sentinel (26379)
机器 B (10.0.0.12): redis-slave (6379) + sentinel (26379)
机器 C (10.0.0.13): redis-slave (6379) + sentinel (26379)三台机器各放一个哨兵互相投票,同时 A 做主、B/C 做从。这样任意一台挂掉,剩下两台哨兵仍能构成多数派(2/3),完成故障转移。如果你是单机,也可以用 docker 起三个哨兵进程模拟,但那只防 Redis 进程崩溃,防不了宿主机宕机,意义有限。
第一步:配置主从复制
先在 A 上编辑 /etc/redis/redis.conf:
bind 0.0.0.0 # 内网场景;若公网务必配合防火墙与 requirepass
protected-mode yes
port 6379
daemonize yes
appendonly yes # 建议开 AOF,故障转移时数据更完整
appendfsync everysec
# 安全:设置密码
requirepass YourStrongRedisPass
masterauth YourStrongRedisPass # 从节点连主的密码
# 复制积压缓冲区(主从断连后部分重同步用)
repl-backlog-size 64mbB、C 的 redis.conf 在此基础上,把 masterauth 和 requirepass 保持一致(通常设成同一个),并加上:
replicaof 10.0.0.11 6379
replica-read-only yes重启三台 Redis,然后在主节点上验证从节点是否挂上:
redis-cli -a YourStrongRedisPass info replication看到类似 role:master、connected_slaves:2,以及两个 slaveN:...,state=online 就对了。
第二步:配置 Sentinel
每个节点新建 /etc/redis/sentinel.conf(Debian 系也可用 /etc/redis/sentinel.conf,注意权限和目录存在):
port 26379
daemonize yes
dir /var/lib/redis
logfile /var/log/redis/sentinel.log
# 监控名为 mymaster 的主节点,quorum 设为 2
sentinel monitor mymaster 10.0.0.11 6379 2
# 主节点有密码时,哨兵也要能认证
sentinel auth-pass mymaster YourStrongRedisPass
# 判定主观下线:连续 30 秒 ping 不通
sentinel down-after-milliseconds mymaster 30000
# 故障转移超时(一次 failover 最多等 3 分钟)
sentinel failover-timeout mymaster 180000
# 选主后,同时向新主发起同步的从节点数量
sentinel parallel-syncs mymaster 1参数解释:
quorum 2表示至少两个哨兵认为主节点主观下线,才会进入客观下线、开始故障转移。三个哨兵里设为 2 是标准值。down-after-milliseconds 30000是主观下线判定时间。设太短会因网络抖动误判,设太长则恢复慢。30 秒对普通站点比较平衡。parallel-syncs 1表示选新主后,从节点一个一个地同步,避免同时同步把新主压垮。数量越多恢复越快但新主压力越大。
三台机器用完全相同的 sentinel.conf(除了 logfile/dir 路径),启动:
redis-sentinel /etc/redis/sentinel.conf
# 或用 systemd
systemctl enable --now redis-sentinel用 redis.service 之外的独立 unit 更清晰。验证哨兵是否互相发现:
redis-cli -p 26379 sentinel master mymaster
redis-cli -p 26379 sentinel sentinels mymaster # 应看到另外两个哨兵第三步:应用侧如何"问哨兵要主节点地址"
这是最容易出错的一环。应用不能写死 Redis 的 IP,而要连哨兵,用它拿到当前主节点地址。以 PHP 的 phpredis 扩展为例:
$sentinels = [
['host' => '10.0.0.11', 'port' => 26379],
['host' => '10.0.0.12', 'port' => 26379],
['host' => '10.0.0.13', 'port' => 26379],
];
$redis = new Redis();
$redis->connect(...); // phpredis 的 Sentinel 支持写法因版本而异
// 更通用的做法:先连任一哨兵,问出主地址
$sentinel = new Redis();
$sentinel->connect('10.0.0.11', 26379);
$master = $sentinel->rawCommand('SENTINEL', 'get-master-addr-by-name', 'mymaster');
// $master = ['10.0.0.11', '6379']
$redis->connect($master[0], (int)$master[1]);
$redis->auth('YourStrongRedisPass');⚠️ 记得最后要用 try/catch 包住整个缓存逻辑,并在拿不到连接时降级为直接查数据库。否则缓存层的抖动会被放大成整站故障。
第四步:演练一次真实的故障转移
没演练过的容灾等于没有容灾。动手验证:
# 在主节点 A 上直接杀掉 Redis
redis-cli -a YourStrongRedisPass shutdown nosave
# 在任意节点观察哨兵日志
tail -f /var/log/redis/sentinel.log正常的话几秒到几十秒内你会看到:+sdown(主观下线)→ +odown(客观下线)→ +failover-state-select-slave → +switch-master mymaster 10.0.0.11 6379 10.0.0.12 6379。最后一行意味着主已经切到了 B。
再用 redis-cli -p 26379 sentinel get-master-addr-by-name mymaster 确认新主地址。然后把 A 重新启动,它会自动作为从节点加入并同步数据(因为 Redis 4.0+ 支持 replicaof 由哨兵动态改写)。
我踩过的坑
- protected-mode 拦截。Redis 3.2 之后,若
bind配了 0.0.0.0 又没设密码,会拒绝外部连接。生产上必须同时设requirepass并让哨兵配auth-pass,否则故障转移后哨兵连不上新主。 - 哨兵配置文件的自动改写。Sentinel 会在运行时重写自己的配置文件(记录已知的从节点和当前主),所以配置文件所在目录必须可写,否则启动就报错。这也是为什么不能把 sentinel.conf 放只读目录。
- 忘了 masterauth。从节点转型为主之后再降级为从时,需要
masterauth才能连回新主,缺了它故障转移后从节点会一直master_link_status:down。 - 数据丢失。故障转移时如果主节点上还有未同步的写操作,这部分会丢。所以像订单、支付这类数据必须落库,不能只放 Redis。
- 单机伪哨兵没意义。我在一台机器上起过三个哨兵进程,结果宿主机一重启全挂,压根没起到高可用作用。哨兵必须分布在不同物理/虚拟机上。
Sentinel vs Cluster:我到底该选哪个
把系统缓存升级成高可用时,很多人一上来就问"是不是该用 Cluster"。先把两者的取舍理清:
- Sentinel 主从:一主多从,数据全量复制,容量受单机内存限制。故障转移是"整机切换",客户端只需重连新主。运维复杂度低,适合数据量在单机内存以内的站点。
- Cluster 分片:数据按 key 哈希分散到多个主节点,容量和吞吐能横向扩展。但代价是:不支持多 key 的跨槽操作、事务受限、客户端必须支持集群协议、扩容要迁移槽位。运维难度高一截。
结论很直接:如果你的 Redis 数据能装进一台机器的内存,就用 Sentinel;只有当单机内存实在放不下、或者单机吞吐被限死时,才上 Cluster。个人站点的缓存、Session,绝大多数情况下 Sentinel 就是正确答案,过早引入 Cluster 只会给自己找麻烦。
客户端接入的最佳实践:封一层获取主节点的逻辑
前面给的是最小示例,真实项目里应该把"问哨兵要主节点 + 重连 + 降级"封装成一个函数,避免每个业务文件都重复这段逻辑:
function redis_get(): ?Redis {
static $conn = null;
if ($conn !== null) return $conn;
$sentinels = [
['10.0.0.11', 26379],
['10.0.0.12', 26379],
['10.0.0.13', 26379],
];
shuffle($sentinels); // 随机起连,避免总压第一台
foreach ($sentinels as [$h, $p]) {
try {
$s = new Redis();
$s->connect($h, $p, 1.0); // 短超时,快速失败
$addr = $s->rawCommand('SENTINEL', 'get-master-addr-by-name', 'mymaster');
$r = new Redis();
$r->connect($addr[0], (int)$addr[1], 1.0);
$r->auth('YourStrongRedisPass');
$conn = $r;
return $conn;
} catch (Throwable $e) {
continue; // 换下一个哨兵
}
}
return null; // 全部失败,调用方降级查库
}三个要点:随机起连避免三个哨兵负载不均;短连接超时让故障发现更快;全部失败返回 null,由调用方决定是否降级——绝不要让缓存层的异常直接冒泡成 500。
监控:让哨兵的状态可观测
搭好高可用不等于可以高枕无忧,哨兵本身也会出问题。我最关心三个信号:
- 当前主节点是谁。写个 cron 每分钟执行
redis-cli -p 26379 sentinel get-master-addr-by-name mymaster,若结果和上一次不同,立刻发告警——说明发生了故障转移,即使服务没受影响也要知道。 - 从节点复制延迟。在主节点上执行
redis-cli info replication,关注每个 slave 的offset差值。差值持续增大说明网络或从机负载有问题。 - 哨兵之间是否互相可见。
sentinel sentinels mymaster应该稳定返回另外两个哨兵;如果少了一个,说明那台哨兵或它的网络出了问题。
把这些采样结果接进你现有的监控(无论是 node_exporter 自写的 textfile collector,还是简单的脚本告警),就能在故障发生的第一时间收到通知,而不是等用户投诉网站变慢。
小结
Redis Sentinel 是个人站长给缓存层做高可用的性价比之选:三台小机器、一份不长的配置、一点应用侧改动,就能把"Redis 挂了整站白屏"这个隐患消掉。核心要点就三条:哨兵至少三个分布在不同机器、quorum 设 2、应用通过哨兵动态获取主节点地址并做降级。最后,务必真的演练一次 shutdown,看着日志里的 +switch-master 出现,你才能放心地说这套高可用是可信的。