
凌晨两点半线上Redis主节点无响应从节点数据落后了十多万条命令。爬起来手动执行SLAVEOF NO ONE把读写切到从节点前后折腾了二十分钟。那还是带主从复制的架构但因为没有Redis Sentinel主节点挂了就得人肉切换。这个场景做过后端或者SRE的朋友应该都不陌生。Redis主从复制能解决数据冗余和读写分离但它不会自动选出新主节点。Redis Sentinel高可用架构就是专门补上这一块的持续监控主从节点的健康状态发现主节点挂了之后自动完成故障转移并且让客户端能自动感知到新的主节点地址。这篇文章我会从Sentinel的原理讲起然后用Docker Compose从零搭建一套一主两从三哨兵的高可用集群再做一次完整的主节点故障演练最后聊一聊生产环境里最常见的坑和调优参数。1. 为什么有了主从复制还不够Sentinel 要解决的核心问题1.1 主从复制解决了什么又留下了什么隐患先回到最基础的问题Redis主从复制解决的是什么问题主从复制最直接的价值是数据冗余。master上的数据通过异步复制同步到replicamaster所在机器硬盘坏了数据还在replica上。第二个价值是读写分离读流量可以分流到从节点减轻master压力。第三个价值在于为主节点故障恢复提供了基础——你手里有数据几乎一样的备机理论上是可以顶上去的。但“顶上去”这三个字恰恰是主从复制做不到的。master宕机之后任何一台从节点都不会自动变成主节点Redis本身没有内置选举机制。你需要人肉登录到某台从节点执行SLAVEOF NO ONE让它变成独立节点再去改所有客户端的连接地址。如果写入量不小客户端在切换完成之前会全部报错业务直接不可用。这个窗口期有多难受经历过的人都知道。而且手动切换还有第二个隐患从节点和主节点之间的数据是有延迟的复制偏移量不同步切换之后可能丢数据。人肉操作的时候还要判断哪台从节点数据最全压力非常大。Redis官方其实清楚这个痛点Sentinel就是为了解决主从复制“不能自动顶上去”的问题而生的。1.2 Sentinel 的三件核心工作Sentinel本质上是一个独立运行的Redis实例但它的角色不是存储业务数据而是以一个监控者的身份工作。它做的事情可以概括成三类。第一监控。Sentinel持续向master、replica以及其他Sentinel发送心跳探测判断它们是否存活。这里有个关键点Sentinel自身不能是单点它要求部署成集群形态这样监控判断才不会因为某个Sentinel进程出问题而失效。第二通知。当被监控的节点状态发生变化时Sentinel可以触发脚本或通过API通知管理员。如果接了监控系统可以把Sentinel日志和事件直接接入告警平台。第三自动故障转移。这是整个机制的核心。Sentinel检测到master客观下线后会自动从剩余从节点里选出一个提升为新master然后让其他所有从节点改为复制新master最后让客户端感知到新的主节点地址。Sentinel还有一个容易被忽略的能力——为客户端提供配置。主流Redis客户端库只要开启了Sentinel模式就会先连上Sentinel通过SENTINEL GET-MASTER-ADDR-BY-NAME拿到当前master的地址再建立真正的数据连接。master切换后客户端会再次向Sentinel询问新地址这就是为什么不用在客户端写死master地址的原因。1.3 一次自动故障转移的完整决策链路故障转移不是瞬间完成的它分多个阶段每个阶段都有对应的日志关键词。排查问题的时候看到这些关键词你就知道卡在哪一步了。第一步是主观下线日志标记为sdown。单个Sentinel在down-after-milliseconds指定的时间内没有收到master的有效回复就把master标记为主观下线。注意这只是单个Sentinel自己的判断网络抖动可能导致误判。第二步是客观下线日志标记为odown。当多个Sentinel都认为master主观下线且同意数量达到quorum时master才会被标记为客观下线。Sentinel之间通过pub/sub通道和gossip协议传播状态信息。主观下线是初步怀疑客观下线是集体确认。第三步是选举Leader。确认master客观下线后Sentinel集群需要选出一个Leader来执行故障转移。这个选举机制类似Raft算法每个Sentinel都有资格参与竞选获得多数派投票支持的Sentinel胜出。第四步是选主。Leader从所有从节点中选出一个最适合成为新master的节点。判断标准按优先级排序replica-priority越小越优先优先级相同则复制偏移量最大的优先因为它的数据最接近旧master再相同则运行ID字典序最小的优先。第五步是执行切换。Leader向选中从节点发送SLAVEOF NO ONE使其变成新master然后向其他从节点发送SLAVEOF命令让它们复制新master。最后更新自身配置中记录的master信息。第六步是通知与收敛。所有Sentinel都能感知到新master地址了。旧master恢复之后重新加入集群时Sentinel会发现它已经变成了“异类”自动将其作为从节点挂到新master下。完整的故障转移通常在几秒到几十秒完成具体时长取决于down-after-milliseconds、failover-timeout以及Sentinel之间的通信延迟。2. 动手前先规划节点怎么定quorum 怎么配2.1 一主两从三哨兵为什么是这个组合部署Redis Sentinel前第一个问题是要准备多少个Sentinel实例。答案不是拍脑袋定的而是取决于决策机制。Sentinel系统本质上是一个分布式决策系统判定master是否客观宕机、发起故障转移都需要遵循少数服从多数原则。既然少数服从多数节点数量就必须保证故障情况下还能凑出多数派。最少需要3个Sentinel。为什么不是1个单点Sentinel自己挂了谁来监控为什么不是2个2个Sentinel挂掉1个就只剩1个在线达不到多数派无法完成故障转移。3个及以上挂1个还有2个在线仍然能凑出多数派。那是不是节点越多越好也不是。每个Sentinel都会监控所有Redis节点都会占用连接资源和网络流量。Sentinel数量太多对Redis节点的连接压力反而增大。生产上3到5个是比较常见的规模。Redis节点这边最小的生产配置是1个master加2个replica。2个replica的价值在于故障转移时选主有候选余地切换完成后新master立即有从节点即使有一台从节点也出问题集群仍然可以运转。读多写少的场景可以再加replica但这里我们先用一主两从三哨兵作为标准模板。2.2 quorum 与 majority两个关键数字的关系配置Sentinel时有一行非常关键的命令sentinel monitor mymaster redis-master 6379 2最后一个数字2就是quorum。它表示当某个Sentinel发现master无响应时至少需要多少个Sentinel都认为master无响应才能确认客观下线。这个“多少个”之间会通过相互通信同步状态信息。quorum设多少合适取决于Sentinel总数。常见设置如下Sentinel总数quorum推荐值容忍Sentinel故障数容忍Redis节点故障数3211个master或1个replica5322个节点7433个节点quorum设太大会怎样比如3个Sentinel设quorum3如果1个Sentinel挂掉剩下2个就算都认为master挂了也凑不满3永远无法触发自动故障转移这等于亲手把自己的高可用掐掉了。quorum设太小又容易误判网络抖动时可能只有单个Sentinel在报故障就触发切换反而造成不必要的抖动。一般建议quorum不要超过Sentinel总数的一半加一。还有一个关键概念majority。即便quorum满足完成一次故障转移还需要获得大多数Sentinel的投票支持。这个“大多数”指当前在线且能参与投票的Sentinel数量超过Sentinel总数的一半。3个Sentinel挂掉2个剩下1个即使它自己认为master挂了也凑不到2个投票因此无法切换。这是设计上的安全网防止极端情况下做出错误决策。总结下来quorum决定什么条件下可以判定master客观下线majority决定什么条件下能真正执行故障转移。两者都是保护机制防止误切换和防止脑裂。2.3 连接关系设计Docker 网络内的服务发现使用Docker部署时节点之间的连接关系设计比裸机部署更讲究。最核心的问题是Sentinel和从节点应该用哪个地址去连接master很多人在这一步踩坑。容器启动后IP是动态分配的如果不做特殊处理重启一次IP就变了。如果Sentinel配置文件里写死master的IP一旦master容器重建Sentinel就找不到它了。Docker提供的标准解法是自定义网络。在同一个bridge网络下容器之间可以通过容器名互相解析IP。只要master容器名是redis-masterSentinel和从节点就用redis-master这个域名去访问它无论底层IP怎么变都能找到。因此架构上要做两件事。第一创建自定义bridge网络把所有Redis和Sentinel容器都放进去。第二所有配置文件里的master地址都用容器名不用IP。Sentinel之间会自动互相发现你只需告诉每个Sentinel同一个master地址它们会通过master上的pub/sub通道交换彼此的信息。还要注意端口映射。Redis容器如果确实需要外部访问可以把6379映射出去。但三个Sentinel的26379端口最好不要全部都映射到宿主机因为默认端口一样映射会冲突。非要远程访问Sentinel的话可以只映射其中一个或为每个Sentinel映射不同宿主机端口并同步修改配置文件里的port。3. 用 Docker Compose 搭建整套环境文件准备与启动步骤3.1 目录结构与配置文件清单部署第一步把目录结构和配置文件想清楚。我习惯用一个统一目录把整套环境的文件都放在一起方便迁移和版本管理。我的目录结构如下redis-sentinel-demo/ ├── docker-compose.yml ├── data/ │ ├── master/ │ ├── slave1/ │ └── slave2/ └── configs/ ├── sentinel1.conf ├── sentinel2.conf └── sentinel3.confdata目录用来挂载Redis节点的持久化数据configs存放三个Sentinel的配置文件。三个Sentinel的配置内容基本一致唯一可能有区别的是各自的端口和hostname设置。在Docker场景下通常保持默认即可。这里有个小细节提前说明免得你被吓到真实生产环境里Sentinel运行过程中会自动修改自己的配置记录它发现的新master地址、replica列表和其他Sentinel地址。如果挂载了宿主机目录你会看到配置被自动重写这是正常现象不是被污染了。3.2 Redis 主从节点的 compose 配置下面给出docker-compose.yml完整内容。镜像tag使用redis:7.0-alpine原因很简单稳定、镜像小、功能完整。生产环境建议固定一个测试过的tag不要用latest。version: 3.8 services: redis-master: image: redis:7.0-alpine container_name: redis-master command: [redis-server, --appendonly, yes, --requirepass, YourStrongPass123, --min-replicas-to-write, 1, --min-replicas-max-lag, 10] ports: - 6379:6379 volumes: - ./data/master:/data networks: - redis-net redis-slave1: image: redis:7.0-alpine container_name: redis-slave1 command: [redis-server, --appendonly, yes, --replicaof, redis-master, 6379, --masterauth, YourStrongPass123, --requirepass, YourStrongPass123] volumes: - ./data/slave1:/data networks: - redis-net redis-slave2: image: redis:7.0-alpine container_name: redis-slave2 command: [redis-server, --appendonly, yes, --replicaof, redis-master, 6379, --masterauth, YourStrongPass123, --requirepass, YourStrongPass123] volumes: - ./data/slave2:/data networks: - redis-net sentinel1: image: redis:7.0-alpine container_name: sentinel1 command: [redis-sentinel, /etc/redis/sentinel.conf] volumes: - ./configs/sentinel1.conf:/etc/redis/sentinel.conf depends_on: - redis-master - redis-slave1 - redis-slave2 networks: - redis-net sentinel2: image: redis:7.0-alpine container_name: sentinel2 command: [redis-sentinel, /etc/redis/sentinel.conf] volumes: - ./configs/sentinel2.conf:/etc/redis/sentinel.conf depends_on: - redis-master - redis-slave1 - redis-slave2 networks: - redis-net sentinel3: image: redis:7.0-alpine container_name: sentinel3 command: [redis-sentinel, /etc/redis/sentinel.conf] volumes: - ./configs/sentinel3.conf:/etc/redis/sentinel.conf depends_on: - redis-master - redis-slave1 - redis-slave2 networks: - redis-net networks: redis-net: driver: bridge写配置时有几个考量点解释一下。requirepass和masterauth必须同时配置。requirepass告诉别人访问本节点需要密码masterauth告诉本节点连接上游master时使用哪个密码。故障转移后原来的从节点变成新master连接关系全部变化但只要两个参数都配好复制链路就能自动重建。min-replicas-to-write和min-replicas-max-lag是防脑裂丢数据的保护参数设置在master节点上。它们的含义是如果master发现自己连不上任何从节点并且这个状态持续超过min-replicas-max-lag指定的秒数就拒绝写入。这样网络分区导致master变成孤岛时它不会继续接收客户端写请求避免恢复后数据冲突。后面详细讲。command里优先使用replicaof这是Redis 7.0的推荐写法。老版本常用的slaveof虽然兼容但新项目里尽量用新参数。3.3 Sentinel 节点的配置与启动三个Sentinel的配置内容基本一致区别在于需要指向同一个master地址。以下是sentinel1.confport 26379 dir /tmp sentinel monitor mymaster redis-master 6379 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 10000 sentinel parallel-syncs mymaster 1 sentinel auth-pass mymaster YourStrongPass123 bind 0.0.0.0sentinel2.conf和sentinel3.conf内容相同可以直接复制。逐行说明几个关键参数port 26379Sentinel的默认端口。Docker内部三个Sentinel使用同一个端口没有问题因为它们在不同容器里。sentinel monitor监控的master名称、地址、端口、quorum。地址填redis-master容器名不能填127.0.0.1。down-after-milliseconds判断节点下线的时间阈值这里是5秒。生产环境建议调到10到30秒之间太短容易因GC停顿或网络抖动误判。failover-timeout故障转移超时时间。从确认master客观下线到完成切换超过这个时间未完成则判定切换失败。parallel-syncs故障转移后同时允许几个从节点同步新master的数据。设1是为了防止多个从节点同时全量同步把新master压垮。auth-pass连接master所需密码必须与master的requirepass一致。bind 0.0.0.0让Sentinel接受任意网卡连接。在容器里如果不设置可能默认只绑定回环地址导致其他容器无法访问。配置写好后执行cd redis-sentinel-demo docker-compose up -d启动后先确认所有容器正常运行docker ps3.4 启动后的连通性检查启动只是第一步验证才是关键。建议按以下顺序从底往上检查。第一步检查复制关系是否建立docker exec redis-master redis-cli -a YourStrongPass123 info replication输出里应该能看到connected_slaves:2并且每个replica的状态是online。第二步检查Sentinel是否认识所有节点docker exec sentinel1 redis-cli -p 26379 sentinel master mymaster docker exec sentinel1 redis-cli -p 26379 sentinel replicas mymaster docker exec sentinel1 redis-cli -p 26379 sentinel sentinels mymastersentinel master输出的address应该是redis-master:6379sentinel replicas会列出两个从节点sentinel sentinels会显示另外两个Sentinel的信息。第三步简单验证读写docker exec redis-master redis-cli -a YourStrongPass123 set hello world docker exec redis-slave1 redis-cli -a YourStrongPass123 get hello从节点能读到写入的数据说明复制链路正常。4. 故障演练实测主节点宕机后发生了什么4.1 手动触发主节点故障部署完成后最值得做的验证就是让主节点真的宕机一次看看Sentinel是不是真的能自动切换。执行docker stop redis-master这时master容器被停止6379端口无服务。因为停掉的是容器Redis进程直接消失这是最典型、最极端的宕机场景。4.2 观察 Sentinel 的决策过程建议一边停master一边开着另一个终端盯Sentinel日志这样能直观看到整个决策过程。docker logs -f sentinel1几秒后日志出现关键事件sdown master mymaster 172.x.x.x 6379 odown master mymaster 172.x.x.x 6379 #quorum 1/2 try-failover master mymaster vote-for-leader ... electing-leader master mymaster failover-state-select-slave master mymaster selected-slave slave 172.x.x.x:6379 failover-state-send-slaveof-noone slave 172.x.x.x:6379 failover-end-for-select-slave master mymaster switch-master mymaster 172.x.x.x 6379 172.x.x.x 6379这些事件行就是整个故障转移时间线。sdown表示单个Sentinel发现master没响应odown表示达到quorum确认客观下线switch-master表示切换完成。从sdown到switch-master整个过程大约10秒以内。切换完成后再执行docker exec sentinel1 redis-cli -p 26379 sentinel get-master-addr-by-name mymaster返回的IP就是新master的地址。检查新master的复制关系docker exec redis-slave1 redis-cli -a YourStrongPass123 info replication你会看到slave1或slave2的role变成了master取决于选主规则。另一台从节点会显示role:slave并且master_host已经指向新master的IP。这个时候验证数据docker exec redis-slave1 redis-cli -a YourStrongPass123 get hello之前写入的helloworld还在。因为开启了appendonly数据有持久化不会因故障转移丢失。有个观察点值得注意日志里的quorum显示为1/2而不是2/2。这是正常的意思是“除当前这个Sentinel之外还有1个Sentinel同意”加起来正好等于quorum2。不用被这个数字迷惑。4.3 恢复旧主节点后的拓扑变化接下来把旧master重新启动观察会发生什么docker start redis-master启动后旧master检测到自己已经不是集群中的master会自动以从节点身份加入新master。检查一下docker exec redis-master redis-cli -a YourStrongPass123 info replication输出里role应该是slavemaster_host指向新master的IP。这验证了前面讲的Sentinel不仅负责切换master还负责在旧master恢复后把它重新纳入集群。故障转移完成后你还可以查看Sentinel配置文件会发现里面的主从拓扑已经自动更新。这说明Sentinel的配置是动态收敛的生产环境里不要随意手工改Sentinel配置文件内容让它自己维护就行。5. 生产环境中的坑反客为主、脑裂和配置同步5.1 密码配置导致 failover 后复制失败排在第一位的坑必然是密码配置。不少人部署Redis时为了省事只在master上配requirepass从节点和Sentinel都不配。平时一切正常但一旦发生故障转移从节点被提升为master后其他从节点再连接它就需要密码而如果这些从节点没有配置masterauth复制就会失败。症状是故障转移看似完成了Sentinel也报告switch-master了但新master的connected_slaves一直是0日志里大量出现Cant connect to master或MASTER auth failed。解决方法就是我在compose配置里强调过的每个Redis节点的启动参数里同时设置--requirepass和--masterauth值保持一致。这样无论哪个节点将来变成master其余节点都有正确密码去连接。还有一个隐蔽问题如果密码里有#或$这类特殊字符在docker-compose的command数组里通常没问题但如果你用命令行直接启动容器shell可能会吃掉特殊字符。建议密码只用字母、数字和下划线省心。5.2 脑裂与数据丢失的防护参数第二个坑是脑裂。故障转移过程中如果旧master不是物理宕机而是网络分区比如交换机抖动Sentinel和从节点联系不上它于是把从节点提升为新master。但这个旧master并没有真正挂掉它还在继续接收客户端的写入。两边同时写入数据就分叉了。网络恢复后旧master被降为从节点它那些新写入的数据在新master身上不存在这部分数据就丢了。Redis Sentinel本身无法完全避免这个场景但可以尽量降低影响。标准做法是给master配置两个保护参数min-replicas-to-write 1 min-replicas-max-lag 10含义是master只有在至少还有1个从节点连接着、并且数据同步延迟不超过10秒的情况下才接受写入。当网络分区导致旧master孤立时它失去所有从节点连接超过10秒后就开始拒绝写入。客户端收到错误响应后会感知到异常并切换连接。这两个参数加在redis-server启动命令里或者放在Redis的配置文件里。加上之后脑裂期间旧master最多只丢10秒写入数据而不是持续写到网络恢复。这个取舍在生产环境通常可以接受。5.3 排查命令与日常监控生产环境的Redis Sentinel不能部署完就不管了。日常监控要盯几件事Sentinel数量是否正常、master的角色和地址是否符合预期、replica是否都online、有没有发生过切换事件。常用监控命令汇总# 查看Sentinel集群整体状态 docker exec sentinel1 redis-cli -p 26379 info sentinel # 查看当前master docker exec sentinel1 redis-cli -p 26379 sentinel master mymaster # 查看所有replica docker exec sentinel1 redis-cli -p 26379 sentinel replicas mymaster # 查看Sentinel之间的状态 docker exec sentinel1 redis-cli -p 26379 sentinel sentinels mymasterSentinel日志里值得关注的关键词有sdown和odown表示有节点被判定下线switch-master表示发生master切换-sdown表示节点恢复。如果网络抖动导致误判日志会频繁出现sdown、odown然后靠后续事件恢复。这类情况说明down-after-milliseconds值设得太短了需要调大。另外一定要关注Sentinel日志文件大小。Sentinel运行久了会记录大量事件如果不做日志轮转日志可能无限增长。生产上建议把Sentinel日志输出到宿主机文件然后用logrotate或者Docker的logging driver做轮转。5.4 客户端正确接入方式最后是客户端接入。很多人部署完Sentinel集群之后客户端还是直接连master的IP这等于把高可用架构白白浪费了——主节点一切换客户端直接失联。正确做法是让客户端感知到Sentinel。主流Redis客户端都支持Sentinel模式。以Spring Boot为例配置文件这样写spring: redis: password: YourStrongPass123 sentinel: master: mymaster nodes: - 127.0.0.1:26379 - 127.0.0.1:26380 - 127.0.0.1:26381这里填的是Sentinel节点的地址不是Redis master的地址。客户端启动后会先连上任意一个可用Sentinel通过Sentinel拿到当前master的地址再建立连接。master切换后客户端通过Sentinel重新感知新地址。如果用Python的redis-py思路相同使用RedisSentinel类传入Sentinel地址列表然后通过sentinel.master_for拿到master客户端。其他语言的客户端库几乎都有对应的Sentinel模式配置核心逻辑一致。还有一个容易忽略的点Sentinel的端口映射问题。客户端要连Sentinel就必须能访问到Sentinel端口。如果Sentinel只运行在Docker内网客户端也在同一个Docker网络里那没问题。如果客户端在宿主机或外部机器上就需要至少一个Sentinel端口能被外部访问到。多个Sentinel端口都映射到同一个宿主机端口会冲突一种解法是只映射一个Sentinel端口其他在外部客户端不可见另一种是映射多个不同宿主机端口并在每个Sentinel配置里设置对应port值。整套环境搭下来从原理到故障演练再到生产经验基本上把Redis Sentinel高可用架构从里到外过了一遍。这个方案最让我踏实的点是真正出故障时系统能自己兜住不需要人半夜爬起来敲命令。搭建完成之后建议主动做几次故障演练把sdown到switch-master的时间摸清楚把切换流程跑熟后面处理线上问题的时候心态会完全不一样。