1. 起底哨兵机制:先搞清楚它到底解决什么问题
项目标题叫“redis——哨兵”,我第一反应就是有人在搭建或者维护 Redis 高可用架构时,遇到了故障转移的那道坎。其实“哨兵”这个词对应的就是 Redis Sentinel,它不是一个独立的数据库,也不是 Redis 的某个插件,而是一套专门负责监控和故障转移的守护进程。简单说,你有一组 Redis 节点构成主从结构,哨兵负责盯着主节点是否存活、从节点是否健康,一旦主节点挂掉,哨兵能在短时间内从从节点里选出一个新主节点,并把所有客户端自动引导到新主上。这个过程就是常说的 failover。
我刚接触 Redis 哨兵时,最大的困惑是:它和 Redis Cluster 到底什么关系?为什么有了 Cluster 还要哨兵?这两个问题几乎每个初学者都会问。我的理解是,哨兵解决的是“单点故障 + 自动故障转移”的问题,适合主从复制架构;而 Redis Cluster 解决的是“数据分片 + 分布式扩容”的问题。打个不太严谨的比方,哨兵更像是一个值班管理员,时刻盯着服务器机房,出事了马上处理;Cluster 则是把仓库改成多个分区,每个分区存放不同类别的货,可以独立管理。它们解决的是不同维度的问题,可以单独用,也可以组合用。实际业务里,庞大的缓存集群同样需要哨兵逐组值守,负责每组主节点的切换。
这篇文章的主要受众,是那些已经具备 Redis 基本使用经验、正在思考高可用方案,或者被一次生产环境主节点宕机搞得焦头烂额的工程师。读完之后,你会清楚哨兵的设计思路、部署形态、运行流程、配置细节和实战坑点。即便你现在用的是现成的云厂商托管 Redis,理解哨兵的原理依然有价值,因为很多托管实例底层就是哨兵机制,只不过界面帮你隐藏了复杂度。
2. 哨兵的世界观:三个关键角色如何协同工作
要想真正把哨兵用明白,不能只背命令,得先建立一套完整的模型。哨兵体系里至少有三种参与者:主节点、从节点、哨兵进程。有时候还有第四个角色——客户端。我把它分成三层来看。
2.1 数据节点的分工
主节点是唯一的写入入口,所有写操作都打到它身上,它承担数据变更的源头角色。从节点通过replicaof命令或者配置文件声明自己的同步来源,然后从主节点异步拉取数据流。主从之间同步数据用的是 Redis 自身的复制协议,这跟哨兵本身没有直接关系,但哨兵的评判基础——比如从节点是否有最新数据、主节点是否真的无响应——都依赖这些节点向哨兵上报的信息。
从节点有个容易被忽略的参数replica-priority,它是哨兵选主时的重要依据。同时从节点可以配置replica-read-only,控制是否允许直接接收写操作。生产环境里这个参数建议保持默认的 yes,否则客户端误写入从节点,数据会产生分叉,故障切换后会出现诡异的数据丢失现象。
2.2 哨兵进程的内部职能
哨兵进程本质上是一个以特殊模式运行的 Redis 服务,端口默认是 26379,但它不存储业务数据,而是维护一张“主从拓扑关系图”。三个职能贯穿它的生命周期:监控、通知、自动故障转移。
监控很好理解,哨兵通过周期性的PING检查主从节点的健康状态,同时用INFO命令获取节点的角色信息、复制偏移量等关键数据。通知的意思是,当被监控对象状态变化时,哨兵要能通过发布订阅渠道触达外部系统或者客户端,比如一篇日志里记录主节点挂了,哨兵会立即向其他哨兵广播一条+sdown或+odown事件。
故障转移是整个机制的重中之重。当哨兵判定主节点客观下线后,它要开始在从节点里选一个“新主”,并推动剩余从节点切换复制源,最终让客户端通过 Sentinel 感知到新的主节点地址。这个过程不是单个哨兵拍板就行,而是经过一个大致的“投票 + 选举”流程,很像公司开会,必须达到一定人数同意才能做决策。
2.3 主观下线与客观下线的判定逻辑
这是很多教程没讲透的地方,但恰恰是理解哨兵高可用的钥匙。
单个哨兵实例自己尝试连接主节点,如果连续若干次心跳都没有收到有效响应,这个哨兵会先把主节点标记为“主观下线”,英文叫sdown。注意,只是这单个哨兵的判断,它可能处于网络分区边缘或者自己本身有问题。主观下线状态是局部结论,不会引发切换。
接下来,这个哨兵会拿着自己的判断去问其他哨兵:“你觉得主节点挂了吗?”当觉得主节点客观下线的哨兵数量达到自己配置的quorum值,主节点才会被标记为“客观下线”,英文叫odown。只有odown状态才可能触发故障转移流程。
这套设计非常像领导决策机制,单个哨兵极少自己“掀桌子”,必须联合足够多的哨兵形成共识,才允许执行风险动作。这样设计的目的显而易见,是为了降低误判引起的脑裂风险。真实网络环境里,短暂超时、垃圾回收暂停、负载高峰都可能导致一个哨兵误以为主节点无响应,但多个哨兵同时产生误判的概率就小很多。
2.4 故障转移的完整执行流程
当主节点进入客观下线状态后,故障转移流程就正式启动了。首先,持有客观下线结论的哨兵会尝试发起选举,让自己成为执行故障转移操作的 leader。选举机制依赖 Raft 风格的多数派原则,需要获得超过一半哨兵的投票支持,才能获得授权。
成为 leader 后,它开始选新主节点。筛选标准并不复杂,主要看几个维度:从节点是否在线、复制偏移量是否尽量接近主节点、replica-priority数值越小优先级越高。选完之后,leader 会执行replicaof no one让新主节点解除从属关系,变成独立可写的主节点。然后指令其他从节点执行replicaof,指向新的主节点。最后,哨兵会把最新的主从拓扑写入自己的配置,更新监控状态。整个过程对客户端而言,只需要重新向 Sentinel 询问“谁是当前主节点”,就能拿到新地址。
注意:哨兵执行的是“异步故障转移”,这个过程不是事务性的。切换期间簇内可能短暂出现主节点不可写,数据也可能因复制延迟丢失一部分。设计业务时需要知道这个边界条件。
3. 部署与接入实战:从零搭一套哨兵架构
理论讲完了,下面进入实操环节。我会用一个典型场景来演示完整的部署方案:三个 Redis 数据节点(一主两从)加三个哨兵节点。为什么是三台哨兵而不是两台?因为故障转移需要多数派同意,三台中至少两票才能推进;两台的话,一台挂掉就剩一个,无法形成多数派,高可用直接失效。这个教训我是付出了生产事故代价才记住的。
3.1 基础规划与端口约定
先做一个标准化约定,方便后续配置和排查:
| 角色 | 节点 IP 占位 | 端口 | 说明 |
|---|---|---|---|
| 主节点 | 10.10.1.1 | 6379 | 业务写入口 |
| 从节点 A | 10.10.1.2 | 6379 | 备选主节点 |
| 从节点 B | 10.10.1.3 | 6379 | 备选主节点 |
| 哨兵 1 | 10.10.1.1 | 26379 | 可部署在数据节点同机 |
| 哨兵 2 | 10.10.1.2 | 26379 | 可部署在数据节点同机 |
| 哨兵 3 | 10.10.1.3 | 26379 | 可部署在数据节点同机 |
生产环境我倾向于将哨兵独立部署或者跨机柜部署,避免数据节点宕机连带哨兵也挂了。这里为了演示方便放在同机,并不代表推荐。部署形态如下:主节点一开始是10.10.1.1:6379,两个从节点通过replicaof 10.10.1.1 6379同步数据。三台哨兵都监控同一个主节点。
3.2 配置主从环境
数据节点本身无需太多高可用配置,但要做两件小事。第一,要求每台节点开启密码认证,并使用同一种认证方式,比如requirepass和masterauth都设置成一样的强密码。原因很简单:哨兵在故障转移时需要使用认证信息连接新主、调整从节点,如果密码不一致,切换过程会因权限验证失败而中断。第二,设置一个合理的replica-priority,比如主节点候选 A 设为 10,候选 B 设为 20,哨兵选主时就会优先考虑 A。这个值没有统一标准,你可以根据机器性能、地域位置、容量大小自己做权重设计。
启动两个从节点时,直接修改配置文件即可。核心行是:
replicaof 10.10.1.1 6379 replica-read-only yes replica-priority 10 masterauth your-strong-password requirepass your-strong-password注意,从节点的masterauth是同步主节点数据时用的密码,requirepass是别人访问自己时需要的密码。两者最好保持一致,省得后续排查时疯掉。
3.3 编写哨兵配置文件
每个哨兵节点的配置文件结构都很相似,我通常只改几个地方。A 哨兵示范如下:
port 26379 daemonize yes pidfile /var/run/redis-sentinel-1.pid logfile "/var/log/redis/sentinel-1.log" sentinel monitor mymaster 10.10.1.1 6379 2 sentinel auth-pass mymaster your-strong-password sentinel down-after-milliseconds mymaster 10000 sentinel failover-timeout mymaster 60000 sentinel parallel-syncs mymaster 1逐行解释一下参数含义:
sentinel monitor mymaster <主节点IP> <端口> <quorum>:这是最核心的一行。mymaster是逻辑主节点名称,客户端也会用这个名称向哨兵索要主节点地址;quorum=2表示至少两个哨兵同意主节点客观下线,才会触发切换。sentinel auth-pass:访问受密码保护的数据节点时要用的密码。down-after-milliseconds:多少毫秒内连续没收到有效响应,就算主观下线。我这里是 10 秒,适合中等延迟的局域网。failover-timeout:一次故障转移允许消耗的总时长,超过则视为失败。设置过大可能导致客户端长时间等待,过小则可能切换未完成就被打断。parallel-syncs:故障转移后,同时允许几个从节点复制新主。设置为 1 是为了降低瞬间集群压力,如果从节点很多,可以适当调高到 2 或 3。
三个哨兵的配置除了端口、日志文件和 pid 文件名之外,其余基本一致。一定要保证sentinel monitor这行的主从监控对象相同,否则哨兵之间会各说各话,形不成有效共识。
3.4 启动顺序与连通性自检
我第一次搭哨兵时,习惯把所有进程先全启起来,结果日志里一堆连接拒绝,后来才明白启动顺序有讲究。顺序应该是:先启动主节点,再启动从节点,最后启动哨兵。哨兵启动后它会自动发现所有从节点,因为INFO响应里会携带master的复制拓扑信息。
启动哨兵的常用命令:
redis-sentinel /etc/redis/sentinel-1.conf或者用 redis-server 以哨兵模式启动:
redis-server /etc/redis/sentinel-1.conf --sentinel启动后立刻检查连通性:
redis-cli -p 26379 sentinel masters redis-cli -p 26379 sentinel slaves mymaster redis-cli -p 26379 info sentinel如果sentinel masters输出里能看到mymaster,状态是ok,并且sentinel slaves能看到两个从节点,说明监控关系已经建立。此时客户端就可以开始接入哨兵体系。
3.5 客户端接入的正确姿势
客户端不能用老办法直接连接 Redis 主节点的 IP,而是要通过哨兵来发现主节点。拿 Java 生态举例,使用JedisSentinelPool,关键代码大致如下(伪代码示意):
Set<String> sentinels = new HashSet<>(Arrays.asList( "10.10.1.1:26379", "10.10.1.2:26379", "10.10.1.3:26379" )); JedisPoolConfig poolConfig = new JedisPoolConfig(); JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels, poolConfig);客户端启动时,会随机连接一个哨兵,获取当前主节点地址,同时订阅哨兵的通知频道。当主节点发生切换,客户端能收到换主消息并自动更新连接,这是实现无缝故障转移的关键。
其他语言的 SDK 也大同小异,核心点就两个:连接的是哨兵地址而不是数据节点地址;你要知道逻辑主节点的名称,并且它与哨兵配置里的sentinel monitor mymaster中的名称一致。
3.6 手动验证故障转移
搭好后必须演练一次真实切换,不能等到事故发生时再验证配置。最直接的模拟方式是杀掉主节点进程:
redis-cli -p 6379 shutdown nosave此时观察哨兵日志。正常情况下,大约 10 秒后(对应down-after-milliseconds),哨兵 1 会先报主观下线,随后其他哨兵加入判断,2 个哨兵达成一致后进入客观下线,然后进行 Leader 选举。再过几秒,哨兵日志会打印切换结果,比如+switch-master mymaster 10.10.1.1 6379 10.10.1.2 6379。这行意味着新的主节点已经变为从节点 A。
接下来用客户端重新拿一次主地址,确认返回的确实是新主。然后重启旧的主节点,它启动后会自动发现自己已经被哨兵重配为从节点,并同步新主的数据。到这一步,整个闭环才算真正打通。
4. 生产一线的高频故障与排查思路
文章开头说过,踩坑才是最好的学习方式。Sentinel 架构出问题,往往是主节点、从节态和哨兵节点共同努力的结果,以前我以为把 sentinel.conf 改对就完事,真正跑起来才知道问题多在配置细节和网络层面。这里花点篇幅把高频坑列一下,都是实测过的。
4.1 常见故障与排查方向
| 典型现象 | 可能原因 | 排查命令/思路 |
|---|---|---|
| failover 一直不触发 | 主观下线时间过长,或 quorum 参数设置不合理 | 检查 down-after-milliseconds 是否设置过大,模拟故障时拔网线还是杀进程 |
| 主节点已宕,但客户端仍报连接拒绝 | 客户端未集成 Sentinel | 确认客户端使用 sentinel 协议连接,例如 Jedis 的 JedisSentinelPool |
| 新主节点选出后,旧主恢复写入,数据冲突 | 旧主未正确配置 replica-priority,或恢复后未及时转为从节点 | 观察旧主日志,确认其是否执行了 replicaof 转向新主 |
| Sentinel 节点之间意见不一致 | 各 Sentinel 配置的 monitor 主节点名称不一致 | 逐个检查 sentinel 配置并对比日志 |
| 频繁误切换 | down-after-milliseconds 过短,或网络抖动 | 调大参数,并评估是否引入 sentinel myid 的唯一性冲突 |
排查时我习惯先看三个文件:sentinel 日志、主节点日志、从节点日志。日志时序能直接还原当时的网络分区和判定过程。必要的时候抓包看PING、INFO、PUBLISH流量,能确认节点间的通信其实并没有断。
4.2 一些建议直接抄的坑位
第一,参数别照抄默认值。默认的sentinel monitor那个down-after-milliseconds是 30000 ms,也就是 30 秒。但很多业务等不了 30 秒,我通常按“客户端超时 × 3 + 心跳间隔余量”的方式推算。比如客户端读超时是 3 秒,我先把值设为 10000 ms,再结合网络情况压测调整。太短会误判,太长会加大数据丢失窗口,没有一劳永逸的值。
第二,永远不要贪图省事把所有 Sentinel 放在一台机器上。风险不只是物理单点,同一台机器的时间源、网络出口、资源争用都会变得一致,导致误判概率被放大。条件允许时,Sentinel 分布在两三个不同的可用区或机柜,配合独立消息通道,效果会好很多。
第三,客户端侧不要只配一个 Sentinel 地址。至少配两个或三个节点的地址,这样其中一个 Sentinel 进程假死或者被负载均衡网关摘除,客户端还能找到其他“裁判”。有些客户端甚至支持配置多个master名称,这个要看具体 SDK 实现。
第四,不要混淆quorum和概率的关系。quorum=2的意义是“至少2个 Sentinel 一起认定主节点故障”,不只是“简单多数”,它直接影响故障转移是否启动。如果quorum小于sentinel总数的一半,可能发生多个leader同时发起 failover 的情况,结果就是脑裂和数据错乱。
第五,主机层面关掉不必要的透明代理或者负载均衡。有些环境里 Sentinel 之间走负载均衡器,负载均衡器本身会中断空闲连接,导致心跳包被吞,发现不了主节点故障。这一点在生产环境特别容易踩,排查起来也很隐蔽。
5. 从实战视角看哨兵的整体效果
跑过这么多年的集群和高可用方案,我对哨兵的感受可以用一句话总结:它是低成本换来高可用容灾收益的典型代表。对于大多数中小规模业务,一主两从加三个哨兵的组合,已经能应对“单机故障”这种最常见的灾难场景。它不需要引入额外的协调组件,也没有复杂的分布式共识协议需要维护,甚至运维逻辑都能在半个小时内梳理清楚。
但哨兵不是银弹。它的设计初衷是解决单点故障自动切换问题,并不能保证严格的数据一致性。因此它更适用于缓存场景、非强一致业务,或者配合业务侧的补偿机制来保护核心数据。你要是拿它承载交易流水这种需要严格对账的数据,最好再想想别的方案,或者至少摸清楚复制延迟能控制在什么水平。
在我负责过的某个电商缓存项目里,核心商品缓存就是哨兵加一主多从架构,高峰期单日几十亿读取,切换过十几次,客户端几乎无感知。事后复盘,真正起作用的不是复杂的参数调优,而是基础动作做得稳:监控齐全、哨兵独立部署、密码一致、演练频繁。参数的玄学远没有基础动作重要。
最后分享一个我自己的土办法。每次搭完哨兵,我会故意在主节点上写一个大 key,然后在从节点观察INFO replication里的偏移量是否快速跟上。接着手动杀掉主节点,观察从节点变成主节点后,这个 key 是否还在。这个验证虽然粗暴,但能直接测试你的副本数据和切换链路是不是真正可用。比看一百遍日志都管用。