拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Redis哨兵高可用方案详解:原理、配置与故障转移实战

凌晨三点被电话叫醒说是线上的Redis主节点挂了。登录服务器一看主库进程确实没了从库还活着数据也没丢太多——问题在于整个系统不知道该听谁的。客户端还在连那个已经死掉的主库地址写也写不进去读也读不到服务就这么瘫痪着直到运维手动把从库提升为主库、再改一遍客户端配置。如果你也经历过这种事就会非常理解为什么会有Redis哨兵。哨兵Sentinel是Redis官方提供的高可用解决方案它就是为了解决这种“主从复制只能保证数据备份却无法自动切换”的问题而生的。它能帮你做的事简单说就是三件盯着主库和从库的状态、发现主库挂了自动把一个从库扶正、把这个“新主库是谁”告诉客户端。这篇文章我想把哨兵的来龙去脉、工作原理和实际部署整个过程掰开揉碎讲清楚也会分享一些我实际踩过的坑。适合正在用Redis但还没上高可用方案的开发、运维也适合准备Redis面试的朋友。1. 主从架构解决了一半问题剩下的一半才是哨兵的起点1.1 主从复制到底解决了什么先说清楚主从复制本身的定位。Redis单机部署时数据全在一个进程里一旦进程退出内存中的数据要么全丢要么只能靠磁盘上的RDB或AOF恢复恢复期间服务也是不可用的。于是有了主从复制一个主库Master负责写多个从库Replica负责同步并分担读请求。这个方案解决了两件事一是数据有了冗余备份主库物理损坏时还能从从库恢复二是读压力能被分摊主库写、从库读缓存场景下读多写少的特征让这个架构很受用。但主从复制有一个很关键的前提是很多人在设计架构时没意识到的它默认主库是永远健康的。主库要是挂了从库依然保持着挂掉之前最后一刻的数据但整个系统就卡在写入了——因为写只能走主库而从库被配置成只读不可能直接接受写入。这个过程里从库倒是还在也能对外提供读服务但读到的数据是旧的而且随着时间推移业务方根本不知道数据已经停止更新了。1.2 主库宕机后的“人肉切换”到底有多慢我在早年维护一个电商后台时Redis主库出过一次故障。当时的操作流程至今印象深刻先登录服务器确认进程状态然后用info replication看从库同步到哪个偏移量确认从库数据没落后太多接着在从库执行SLAVEOF NO ONE让它变为主库再修改应用配置里的Redis地址最后逐个重启应用实例。这一套流程走下来最快也要十几分钟慢的时候半小时都正常。问题在于这半小时内整个业务是不可写的。对于依赖缓存扛峰值的业务来说这意味着大量请求直接穿透到数据库数据库压力瞬间飙升很可能把数据库也拖垮。更头疼的是如果从库不止一个还得去改每个从库的复制关系让它们认新的主库。这套流程一旦出错数据不一致、主从互相脑裂的情况都出现过。所谓“最快十几分钟”还是建立在运维恰好清醒、工具链齐全的前提上凌晨被叫起来手忙脚乱操作时时间只会更长。1.3 为什么客户端不能自己搞定切换有人会问能不能让客户端发现主库挂了就直接去连从库思路是对的但落地很难。客户端本身没有足够的判断力连接超时到底是主库进程挂了还是网络抖动从库的数据落后主库很多直接接手会不会丢大量数据多个从库同时被客户端发现“可切换”到底该选谁这些决策涉及数据一致性、优先级、投票不是一个客户端能独立完成的。而且客户端可能有很多个Java应用、Python脚本、定时任务、消息队列组件它们各自维护着自己的Redis连接信息。就算每个客户端都实现了“故障检测和自动切换”逻辑标准不统一行为不一致一旦发生故障有的切了有的没切系统反而更混乱。所以需要一套集中式的、大家公认的机制来做这件事这就是哨兵。2. 哨兵到底是什么一个独立于Redis的高可用“哨岗”2.1 哨兵的定位和核心职责哨兵本身是一个独立的Redis服务进程但它不存储业务数据它做的只有一件事管理其他Redis实例。官方文档里对哨兵的定义是“Redis Sentinel is a distributed, failure detection and automatic failover system”——分布式、故障检测、自动故障转移这几个词概括了它的全部。具体拆开哨兵有四个核心职责监控Monitoring持续检查主库和从库是否运行正常发送心跳命令并判断返回结果。通知Notification被监控的Redis实例发生故障时通知管理员或其他应用程序。自动故障转移Automatic Failover主库不可用时在从库中挑选一个晋升为主库并让其他从库改为复制新主库。配置提供Configuration Provider客户端连接Redis时向哨兵查询主库地址故障转移发生后客户端能拿到新主库的地址。这四件事看起来简单真正做起来很多细节。比如监控不是简简单单发一次PING而是周期性、持续性的故障转移不是随便挑一个从库就完事还要考虑数据完整性和优先级配置提供要解决客户端缓存旧地址的问题。2.2 为什么哨兵也要组集群如果只部署一个哨兵进程它的故障检测结果是单点判断不可靠。主库没挂只是网络抖动导致哨兵连不上它就可能误判主库下线然后执行错误的故障转移。更极端的情况是哨兵自己挂了那整个高可用体系瞬间失效。所以哨兵必须部署多个组成一个哨兵集群用多数投票的方式来做决策——这就是分布式系统里经典的“少数服从多数”思想。部署哨兵集群时有个最常见的疑问到底部署几个很多人听过“奇数个”的说法理由其实在于容错与选举的配合。哨兵集群的法定人数quorum决定了一个主库要被判定为客观下线至少需要几个哨兵同意而故障转移时哨兵之间要选举出一个领导者领导者必须获得超过半数的投票。如果你有2个哨兵挂掉一个就凑不齐过半票数整个哨兵系统无法工作如果你有3个哨兵挂掉1个还剩2个依然能过半有4个哨兵理论上容忍挂掉1个但挂掉2个就只剩2个无法过半与3个哨兵的容错能力一模一样却多了一个实例的维护成本。所以出于容错最大化、成本最小化的原则生产环境最常见的做法是部署3个哨兵。2.3 哨兵体系里的重要术语理解哨兵代码和文档时经常会碰到几个概念性名词这里统一解释一下SDOWNSubjective Down主观下线单个哨兵自己觉得某个实例下线了。有可能是误判因为网络问题也会导致心跳不通。ODOWNObjective Down客观下线哨兵集群中足够多的哨兵都对同一个主库报告了主观下线达到quorum数量系统才认定主库确实挂了。只有主库才会进入客观下线状态从库和哨兵自身没有这个状态。Failover故障转移用新主库替换掉坏掉的主库重配置从库并通知客户端的整个过程。Leader领导者执行具体故障转移操作的哨兵由哨兵集群选举产生。这几个术语背后的判断逻辑很讲究值得展开讲。3. 哨兵的工作机制心跳、投票与自动切换的完整链路3.1 哨兵如何发现主库挂了从主观下线到客观下线每个哨兵会以每秒一次的频率向所有被监控的Redis实例包括主库、从库和其余哨兵发送PING命令来确认对方是否存活。如果被监控的实例在down-after-milliseconds指定的时间内没有回应或者返回了错误哨兵就会把这个实例标记为“主观下线”。这里一个关键细节是主观下线对从库和主库的处理是不同的。从库被标记为主观下线后不会触发后续任何动作因为从库挂了不影响写入只要主库活着新的从库随时可以顶上主库被标记为主观下线后哨兵也不会立刻动手而是会把这件事广播给其他哨兵。其他哨兵收到“我认为主库下线了”的消息后会去确认自己对主库的连接状态。当确认主库客观下线的哨兵数量达到配置中的quorum值时这个主库才会从主观下线升级为客观下线。quorum一般设置为哨兵总数的一半以上比如3个哨兵就设置为2。这里可以用一个生活化的类比一个人说邻居家的灯灭了可能是邻居没拉窗帘但小区保安、隔壁楼住户都同时确认灯灭了那基本可以确定是真出事了。多一个角度交叉验证误判概率就小很多。3.2 故障转移的触发哨兵集群怎么选出一个“管事”的客观下线确认后事情还没完得先从一个哨兵里选出一个领导者来具体执行故障转移。这一步用的是Raft共识算法中的选举逻辑每个哨兵都有投票权候选者会向其他哨兵发送竞选消息收到消息的哨兵在“本任期”“未投过票”的前提下会把票投给第一个联系它的哨兵。一个哨兵要多得超过半数的票才能成为领导者。这个选举过程保证了即使多个哨兵同时发现了主库问题最终也只有一个哨兵来干活不会出现多个哨兵同时做故障转移、把系统搞得更乱的情况。Raft算法在分布式系统里是很经典的选主算法Etcd、Consul等组件也用它。理解这个机制后再看“部署奇数个哨兵”的建议就会更清楚——过半选票的要求让奇数个节点在容错计算上更高效。在redis源码和官方配置里这个管道其实是这样的当一个哨兵判定主库客观下线后会等待一个failover-timeout的超时窗口这个窗口一般设置为down-after-milliseconds的若干倍避免因网络抖动就仓促切换同时也在等待其他哨兵对“该执行转移”达成一致。窗口过了才真正发起选举。3.3 故障转移的具体步骤成为领导者之后哨兵按以下顺序执行操作从已下线主库的从库中挑选一个作为新的主库。筛选条件是处于线状态、最近一次与主库通信的时间较近数据较新、优先级最高、复制偏移量最大。优先级通过replica-priority配置数值越小越优先。向选中的从库发送SLAVEOF NO ONE让它停止复制切换为主库。修改配置让这个从库变成主库并把这个最新状态同步给哨兵集群中其他哨兵。向其他从库发送SLAVEOF 新主库IP 新主库端口让它们去复制新的主库。如果旧主库恢复上线会让它变成新主库的从库而不是恢复它原来的主库身份——这样能保证数据流的一致性。这几个步骤在运维人员肉眼里可能就是几条日志但每一条背后都有很多设计考虑。比如“为什么选从库要参考复制偏移量”因为复制偏移量越大说明从库同步自旧主库的数据越完整切换后丢的数据越少。Redis的高可用不是把服务恢复就行还要尽量把数据损失降到最低。3.4 客户端如何知道主库换了服务发现机制故障转移做完后如果客户端还死盯着旧主库的地址那一切努力都白费了。所以哨兵还有一个重要的职责作为配置中心让客户端通过它获取当前主库的地址。官方提供了客户端连接哨兵的两种方式客户端直接连接哨兵发送SENTINEL get-master-addr-by-name mymaster拿到当前主库的IP和端口。客户端订阅哨兵频道的switch-master消息在故障转移完成时收到通知从而更新自己缓存的主库地址。这套机制降低了客户端对地址的硬编码依赖。不过实际项目中很多框架里的Redis客户端库比如Java的Lettuce、Spring Data Redis已经封装了这些逻辑你在配置里填上哨兵地址列表读写操作就能自动发现新主库。客户端层面要做的就是别把主库地址写死在本地配置文件里而是通过哨兵来发现。这里常见的坑是某些客户端库只有在启动时才会去查询一次主库地址故障转移后如果连接没重建还是可能连旧地址需要在客户端里开启拓扑刷新或定时重连机制。3.5 还有个高频问题哨兵模式下脑裂怎么防脑裂这个词听起来吓人本质上是网络分区导致旧主库并没有真正宕机只是哨兵和它失去了联系。哨兵那边完成了故障转移旧主库网络恢复后又开始接受写入两个主库同时对外提供服务客户端写入的数据分散在两处重新合并时很容易丢数据。Redis降低脑裂风险的手段是两个配置项min-replicas-to-write和min-replicas-max-lag。它们规定了主库至少要能同步给多少个从库、数据落后不能超过多少秒如果连不上从库主库就拒绝写入。这样在网络分区时旧主库因为无法连接到从库而停止接受写入新主库接手全部写入避免了新旧主库同时写入造成的分叉。这是高可用架构里很实用的兜底策略配置成本极低强烈建议开启。4. 动手实操从零部署一套Redis主从加哨兵架构4.1 环境与版本规划用3台机器来演示分别记作node1、node2、node3。如果你本地想快速试也可以用Docker在一台机器上跑三个容器效果一样只是网络隔离层面稍有差别。这里采用Linux物理机Docker结合的方式演示配置以可操作为主。node1: 172.16.0.11运行Redis主库端口6379node2: 172.16.0.12运行Redis从库1端口6379node3: 172.16.0.13运行Redis从库2端口6379三个节点各自再运行一个哨兵进程端口26379先确保三台机器都装好了Redis。版本尽量用6.x以上因为新版在复制协议和高可用方面优化更多较早的4.x版本存在部分边界问题。我用的是Redis 7.0版本命令和配置都能兼容。4.2 主从节点配置主库节点node1的redis.conf保持默认即可关键是关闭保护模式、绑定本机IP让对端能连上bind 0.0.0.0 port 6379 protected-mode no daemonize yes dir /data/redis appendonly yes从库节点node2和node3的配置和主库类似就是在文件末尾加一行复制指令replicaof 172.16.0.11 6379如果你用的是旧版配置这个参数名还叫slaveof新版统一叫replicaof注意区分。配好后启动三台Redis登录从库执行info replication能看到role:slave且master_link_status:up说明主从复制建立成功。4.3 哨兵节点配置每台机器上新建一个sentinel.conf内容如下port 26379 daemonize yes bind 0.0.0.0 protected-mode no dir /data/sentinel sentinel monitor mymaster 172.16.0.11 6379 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 15000 sentinel parallel-syncs mymaster 1 sentinel auth-pass mymaster 你的密码逐行解释一下sentinel monitor mymaster 172.16.0.11 6379 2告诉哨兵监控一个名为mymaster的主库初始地址是172.16.0.11:63792就是quorum表示至少2个哨兵同意主库下线才判定为客观下线。down-after-milliseconds哨兵和主库心跳超时多久算主观下线单位是毫秒。5000就表示5秒没有心跳响应就标记主观下线。生产环境建议设置在10秒以上太短容易误判。failover-timeout故障转移的超时时间包括选举、切换、从库重新同步各环节。太短会导致切换过程中途失败建议15秒起步。parallel-syncs故障转移后同时允许几个从库同步新主库的数据。设成1表示逐个同步避免瞬间大量复制把新主库压垮。从库数量多时这个值很有讲究我一般保持1。auth-pass如果主库设置了密码哨兵连它需要密码。这里填主库的认证密码和客户端连接密码一致。三台机器的哨兵配置基本一样只是dir路径按各自机器实际目录调整。启动命令redis-sentinel /etc/redis/sentinel.conf启动成功后查看哨兵对主库的感知redis-cli -p 26379 sentinel master mymaster redis-cli -p 26379 sentinel replicas mymaster能看到哨兵已经发现了主库和两个从库代表监控正常。4.4 模拟主库宕机看哨兵如何自动切换接下来做最刺激的实验把主库直接杀掉redis-cli -h 172.16.0.11 -p 6379 shutdown nosave或者直接kill -9主库Redis进程。然后盯着哨兵日志默认输出到/var/log/redis或你在配置里指定的日志文件会依次看到以下关键记录sdown master mymaster 172.16.0.11 6379本哨兵主观判定主库下线。odown master mymaster 172.16.0.11 6379 #quorum 2/2两个哨兵都认为主库下线客观下线成立。try-failover master mymaster 172.16.0.11 6379某个哨兵发起故障转移。failover-election-suc某个哨兵当选为领导者。promoted-slave slave 172.16.0.12:6379从node2中选了一个晋升为主库。switch-master mymaster 172.16.0.11 6379 172.16.0.12 6379主库地址切换完成自此mymaster的主库变成了node2。过几秒在node2上执行info replication能看到role:master。再登录node3能看到master_host已经指向node2。整个切换过程我实测大概在5~10秒之间取决于down-after-milliseconds的配置。对比我之前人肉切换动辄半小时这种体验差距是巨大的。4.5 验证客户端感知与新主库写入用Java的Jedis客户端作为示例SetString sentinels new HashSet(); sentinels.add(172.16.0.11:26379); sentinels.add(172.16.0.12:26379); sentinels.add(172.16.0.13:26379); JedisSentinelPool pool new JedisSentinelPool(mymaster, sentinels, 你的密码);JedisSentinelPool启动时会向哨兵询问当前主库地址并订阅哨兵的switch-master频道。故障转移发生时池内部会丢弃旧连接转为连接新主库。写一个定时任务持续往Redis写入数据再手动杀掉主库观察日志中写入是否中断。正常情况下几秒后写入恢复且数据写到了新主库上。5. 排查思路与避坑指南这些坑我替你踩过了5.1 哨兵日志里的主库地址还是旧的切换不生效这个问题我遇到过好几次大多是配置和启动顺序的问题。哨兵启动后它自己会缓存一份sentinel state到配置文件里注意它会自动重写配置文件。比如你原来的sentinel.conf里写的是sentinel monitor mymaster 172.16.0.11 6379 2运行一段时间后这个文件会被哨兵改写成sentinel monitor mymaster 172.16.0.12 6379 2新主库地址。如果你手动停掉所有哨兵再以旧配置启动哨兵会发现自己监控的旧主库已经不存在触发新一轮故障转移进而造成不稳定。提示哨兵进程会重写自己的配置文件这是正常行为。管理配置时不要用外部脚本覆盖哨兵配置文件除非你明确知道自己要重置整个Sentinel状态。5.2 主观下线阈值设太小导致的频繁误切换有人为了追求“切换快”把down-after-milliseconds设成1000甚至更低。结果Redis主库还在正常服务只是某次网络抖动超过了1秒三个哨兵就集体误判主库下线触发了一次完全没必要的故障转移。切换本身又带来连接抖动和数据同步开销反而影响了可用性。生产环境我建议down-after-milliseconds设置在10000到30000之间配合监控报警系统的独立检测来做兜底不要指望哨兵感知得越快越好。5.3 主库密码配置不一致导致哨兵无法认证从库、哨兵、客户端连主库都需要密码而密码一旦不一致常见现象是主库和从库复制状态正常哨兵也显示master_link_status:up但主库一挂哨兵切到从库后从库连接新主库失败整个系统卡在中间状态。排查这种问题最直接的方法是看哨兵日志里有没有-NOAUTH或者-ERR invalid password字样。所以部署时建议用同一个配置文件模板密码参数统一管理防止遗漏。5.4 客户端连的还是旧主库Jedis这类客户端库对哨兵的支持比较成熟故障转移后能自动感知。但有些老旧客户端或自己封装的连接池只在启动时查询一次主库地址之后即使哨兵切了主库它还是连接旧地址。这个问题最早压测时发现的主库切换了客户端写操作还是报错检查发现是连接池没有订阅switch-master消息。解决方案有两个一是升级客户端库尽可能用支持Sentinel的版本二是在应用里加入定期刷新逻辑比如通过SENTINEL get-master-addr-by-name命令定时比对当前主库地址和缓存不一致时重建连接池。5.5 脑裂问题不只是概念实战中真会丢数据前面讲了脑裂的原理和防脑裂配置。这里再贴一次最小配置方便直接抄min-replicas-to-write 1 min-replicas-max-lag 10第一项表示主库至少需要收到1个从库的同步确认才接受写入第二项表示从库数据同步延迟超过10秒就算失联。两个条件同时满足时主库才拒写。实际故障中旧主库因为网络隔离失去了所有从库的同步10秒后自动拒绝写入客户端的写请求会失败并快速报错应用侧可以做重试等服务恢复后数据不会产生分叉。别小看这两个配置在高可用体系里它们的作用比很多花哨的组件都大。5.6 哨兵自身的高可用别把鸡蛋放一个篮子有人部署了3个哨兵结果都放在同一台物理机上这台机器宕机哨兵全部失效。高可用的核心就是冗余哨兵之间一定要跨机器、跨机柜、甚至跨可用区部署保证任何单点故障都无法让哨兵集群失去法定人数。还有一点容易忽视如果主库和哨兵同机部署主库进程宕机导致机器内存、CPU飙高可能会连累同机的哨兵让它无法及时向其他哨兵通报状态。生产环境我把主库和哨兵分机部署从库和哨兵混部时也做了资源隔离内存、磁盘都有限制。5.7 网络分区哨兵和客户端要能互相连通部署完哨兵后我在本地测试一切正常但放到生产环境后某个机房的客户端总是拿不到主库地址。排查后发现客户端所在的安全组只放行了6379端口没有放行26379端口导致客户端无法连接哨兵。哨兵只是“知道”主库地址地址能不能被客户端访问是另一回事。因此要确保客户端能连上所有哨兵地址和当前的所有主从Redis端口网络策略要提前规划好。6. 哨兵和Redis Cluster高可用方案该怎么选6.1 两种方案的侧重点不同聊哨兵时经常有人拿它和Redis Cluster对比觉得“Cluster不是更高级吗干嘛还要用哨兵”。我的看法是这两个东西解决的问题并不完全重叠。哨兵解决的是主从架构下的高可用问题它本身不参与数据分片所有写入依然集中在一个主库上。而Redis Cluster解决的是数据分片问题它把数据自动分散到多个主节点上每个主节点又可以有从节点做容灾。Cluster自带的故障转移机制本质上就是“每个分片的哨兵逻辑”只是和分片紧密结合在一起。6.2 什么时候优先选择哨兵规模不大、数据量还能装进单机内存的场景哨兵是最佳选择。单主架构简单部署、运维、排查心智负担都低没有槽位迁移、重定向等概念客户端兼容性也最好。很多中大型系统的Redis缓存数据总量其实也就几个GB到几十个GB单个主库完全扛得住此时引入Cluster纯属给自己找麻烦。另外如果你用了Redis的某些特性比如Lua脚本、事务、KEYS命令单主架构下这些行为是全局可预期的而Cluster下多键操作会受限于槽位处理起来要绕很多弯。6.3 什么时候必须上Cluster数据量超过单机内存上限或者写入吞吐远超单个Redis实例的性能表现时Cluster几乎成了必然选择。Cluster通过分片把压力分散到多个节点单节点故障只影响该节点负责的槽位其他分片照常服务。但相应的数据分布策略、客户端路由、批量操作的跨槽处理都带来了额外复杂度。如果业务里大量使用多键操作且无法改造上Cluster前要想清楚是换数据结构、还是加节点后重新设计路由。6.4 一张表说清两者的差异对比项Sentinel模式Redis Cluster主要解决高可用主从自动切换高可用 数据分片数据存储单主库全量存储多主节点分片存储写入扩展受限于单主库性能可横向扩展多键操作支持无槽位限制跨槽位需绑定标签客户端要求支持Sentinel的客户端即可必须支持Cluster协议运维复杂度较低较高涉及槽位、重新分片适合场景数据量适中、读多写少数据量大、写入吞吐高很多团队实际部署时还会做“SentinelCluster”的混用比如Cluster每个分片的主节点再用哨兵保护这种设计常见于数据分片后又追求极高可用性的场景。没有绝对正确的方案只有适合当前业务规模和团队维护能力的方案。我见过有团队数据量不过几个GB硬是上了Cluster结果日常业务没出问题倒是排查槽位迁移和客户端路由问题耗费了大量时间。7. 聊聊我对哨兵的一些个人体会从第一次凌晨爬起来手动切主库到现在用哨兵把故障转移时间压缩到十秒以内这个过程最大的感受是高可用不是某个组件装上去就完事的它是一个体系工程。哨兵解决了“主库挂了怎么办”的问题但你要真正安睡还得把监控告警、客户端重连、数据备份、容量规划这些配套都补齐。我自己的习惯是每次部署Redis高可用都会做一次完整的故障演练不只是杀掉主库还会模拟网络分区、从库滞后、哨兵半数不可用等极端场景。只有这些情况都验证过了心里才有底。哨兵本身并不复杂复杂的是运行环境里的各种意外组合提前演练一次比出问题后再排查高效太多。最后分享一个小经验别把哨兵的配置文件当成启动后就不动的静态文件。每次故障转移后检查一下哨兵自动重写的配置文件内容对照主从架构是否一致这些状态信息是最直观的诊断入口。Redis的哨兵这套机制设计得相当稳配合合理的配置和演练它完全能让你的Redis服务不再成为半夜响起的闹钟。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门