
1. 为什么单机跑 Redis 哨兵不是脱裤子放屁很多人第一次听到在一台机器上部署 Redis 哨兵模式这个需求时第一反应是哨兵本来就是用来防机器宕机的你全塞一台机器上机器一挂不全完了这话对但只对了一半。哨兵模式解决的核心问题其实有两个层面第一是主节点故障后的自动切换第二是客户端能自动发现新的主节点。在真实生产环境里这两件事确实需要跨机器才有意义。但在开发、测试、演示、教学这些场景下我们需要的往往只是把哨兵这套机制跑起来验证主从复制配置对不对、故障切换逻辑通不通、客户端代码能不能正确连上新主节点。这时候如果非要凑三台机器成本高、协调麻烦纯属给自己找不痛快。所以单机部署哨兵的真实价值在于用一台机器模拟出多节点的拓扑结构把哨兵的核心机制完整跑通。你可以在本地把主从切换的整个流程走一遍观察日志、验证数据一致性、测试客户端重连行为。等这套流程摸熟了再迁移到多机环境就是改几个 IP 地址的事。这里有个关键点需要提前说清楚单机部署哨兵必须用 Docker 的网络隔离能力把每个 Redis 实例的端口和目录分开否则多个实例会互相踩踏配置文件、日志文件和 RDB/AOF 持久化文件。用 Docker 的好处就在这——每个容器有独立的文件系统和网络命名空间天然适合模拟多台机器。我这次用的方案是一台宿主机上跑 1 个主节点、2 个从节点、3 个哨兵节点总共 6 个容器。哨兵节点为什么也是 3 个因为哨兵判断主节点客观下线需要多数派投票3 个哨兵能容忍 1 个挂掉这是最小的可用奇数集群。下面把完整步骤和踩过的坑一个个说清楚。2. 环境准备与目录规划别小看这一步2.1 基础环境确认我用的环境是 Linux 服务器内核 5.x 以上Docker 版本 20.10docker compose 用的是 v2 版本命令是docker compose而不是老的docker-compose。你先确认一下docker --version docker compose version如果 compose 还是 v1建议升级到 v2因为 v2 对网络和依赖管理的处理更干净。另外确认一下宿主机上 6379、6380、6381、26379、26380、26381 这几个端口没有被占用ss -tlnp | grep -E 6379|6380|6381|26379|26380|26381有输出就说明被占了要么改端口要么把占用的进程停掉。2.2 目录结构设计这一步是很多人会偷懒的地方但恰恰是后面出问题最多的源头。我的建议是给每个节点单独建目录结构如下redis-sentinel-lab/ ├── docker-compose.yml ├── master/ │ ├── redis.conf │ └── data/ ├── slave1/ │ ├── redis.conf │ └── data/ ├── slave2/ │ ├── redis.conf │ └── data/ ├── sentinel1/ │ ├── sentinel.conf │ └── data/ ├── sentinel2/ │ ├── sentinel.conf │ └── data/ └── sentinel3/ ├── sentinel.conf └── data/为什么要这么分因为 Redis 在运行时会往配置目录写持久化文件RDB、AOF和日志。如果多个实例共用一个目录主节点生成的dump.rdb可能被从节点覆盖哨兵的sentinel.conf在运行时会被重写哨兵会自动修改配置文件记录已知的哨兵和主节点状态多个哨兵共用配置文件会直接导致状态混乱。提示哨兵的配置文件在运行过程中会被哨兵进程自动改写所以每个哨兵必须有自己独立的配置文件且容器内对该文件要有写权限。2.3 拉取镜像我用的是 Redis 7.2 的官方镜像这个版本对哨兵的支持很成熟docker pull redis:7.2不建议用latest标签因为不同大版本之间哨兵配置项有差异锁定版本能避免昨天还好好的今天跑不起来这种破事。3. 主从节点的配置与启动3.1 主节点配置master/redis.conf内容如下bind 0.0.0.0 port 6379 protected-mode no daemonize no appendonly yes appendfilename appendonly.aof dir /data logfile masterauth redis123 requirepass redis123逐条解释一下关键项。bind 0.0.0.0是让 Redis 监听所有网卡容器内必须这样配否则容器外部连不进来。protected-mode no是关闭保护模式因为我们要跨容器访问。daemonize no是因为容器里 Redis 必须前台运行否则容器启动完就退出了。appendonly yes开启 AOF 持久化方便后面验证数据同步。masterauth和requirepass都设成同一个密码前者是主节点连从节点时用的哨兵切换后原主变从会用到后者是客户端连接密码。注意masterauth这个配置在主节点上看起来没用但一旦发生主从切换原主节点会变成从节点去连新主这时候就需要它。所以主节点也建议配上。3.2 从节点配置slave1/redis.confbind 0.0.0.0 port 6380 protected-mode no daemonize no appendonly yes appendfilename appendonly.aof dir /data logfile replicaof master 6379 masterauth redis123 requirepass redis123slave2/redis.conf除了端口改成 6381其他一样。注意replicaof master 6379这里写的是容器名master不是 IP。这是 Docker 自定义网络的一个便利之处——容器之间可以用服务名互相解析不用关心具体 IP。这也是我推荐用 docker compose 而不是docker run的原因之一。3.3 docker-compose 编排docker-compose.ymlservices: master: image: redis:7.2 container_name: redis-master ports: - 6379:6379 volumes: - ./master/redis.conf:/usr/local/etc/redis/redis.conf - ./master/data:/data command: redis-server /usr/local/etc/redis/redis.conf networks: - redis-net slave1: image: redis:7.2 container_name: redis-slave1 ports: - 6380:6380 volumes: - ./slave1/redis.conf:/usr/local/etc/redis/redis.conf - ./slave1/data:/data command: redis-server /usr/local/etc/redis/redis.conf depends_on: - master networks: - redis-net slave2: image: redis:7.2 container_name: redis-slave2 ports: - 6381:6381 volumes: - ./slave2/redis.conf:/usr/local/etc/redis/redis.conf - ./slave2/data:/data command: redis-server /usr/local/etc/redis/redis.conf depends_on: - master networks: - redis-net networks: redis-net: driver: bridge这里有个细节从节点的depends_on只保证启动顺序不保证主节点已经准备好接受连接。所以从节点启动时如果连不上主节点会不断重试这是正常行为不用慌。启动主从docker compose up -d master slave1 slave23.4 验证主从复制进主节点写个 keydocker exec -it redis-master redis-cli -a redis123 set foo bar然后分别进两个从节点读docker exec -it redis-slave1 redis-cli -a redis123 get foo docker exec -it redis-slave2 redis-cli -a redis123 get foo都能读到bar就说明主从复制通了。如果从节点报NOAUTH或者连不上先看从节点日志docker logs redis-slave1常见错误是masterauth没配或者密码写错日志里会明确提示NOAUTH Authentication required。4. 哨兵节点的配置细节与启动顺序4.1 哨兵配置文件的写法sentinel1/sentinel.confport 26379 bind 0.0.0.0 protected-mode no daemonize no dir /data logfile sentinel monitor mymaster master 6379 2 sentinel auth-pass mymaster redis123 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 10000 sentinel parallel-syncs mymaster 1sentinel2和sentinel3除了端口分别改成 26380、26381其他一样。这里几个参数值得展开说。sentinel monitor mymaster master 6379 2里的mymaster是主节点别名客户端连接哨兵时要用这个名字master是主节点容器名6379是端口最后的2是法定票数quorum意思是至少 2 个哨兵认为主节点挂了才会触发客观下线判断。3 个哨兵配 2 是标准做法。sentinel auth-pass是哨兵连接主从节点时用的密码必须和 Redis 的requirepass一致否则哨兵连不上主节点监控直接失效。down-after-milliseconds 5000表示哨兵在 5 秒内 ping 不通主节点就认为它主观下线。这个值我设得比较短方便测试时快速看到切换效果。生产环境建议 30000 左右避免网络抖动导致误判。sentinel failover-timeout 10000是故障转移的超时时间包括选举新主、让从节点同步新主等步骤的总超时。sentinel parallel-syncs 1表示故障转移后同一时间只允许 1 个从节点向新主发起同步避免新主被多个从节点同时全量同步压垮。4.2 哨兵启动的坑配置文件权限哨兵启动时会对配置文件做重写所以容器内对sentinel.conf必须有写权限。如果你在宿主机上把文件权限设成了只读或者挂载时用了:ro哨兵会启动失败日志里报Failed to rewrite config file。我的做法是挂载时不加只读标志并且确保宿主机上文件属主对容器内进程可写。Redis 官方镜像默认以 root 运行所以一般不会有权限问题但如果你改过镜像的 user就要注意了。4.3 启动哨兵在 compose 文件里追加哨兵服务sentinel1: image: redis:7.2 container_name: redis-sentinel1 ports: - 26379:26379 volumes: - ./sentinel1/sentinel.conf:/usr/local/etc/redis/sentinel.conf - ./sentinel1/data:/data command: redis-sentinel /usr/local/etc/redis/sentinel.conf depends_on: - master - slave1 - slave2 networks: - redis-net sentinel2: image: redis:7.2 container_name: redis-sentinel2 ports: - 26380:26380 volumes: - ./sentinel2/sentinel.conf:/usr/local/etc/redis/sentinel.conf - ./sentinel2/data:/data command: redis-sentinel /usr/local/etc/redis/sentinel.conf depends_on: - master - slave1 - slave2 networks: - redis-net sentinel3: image: redis:7.2 container_name: redis-sentinel3 ports: - 26381:26381 volumes: - ./sentinel3/sentinel.conf:/usr/local/etc/redis/sentinel.conf - ./sentinel3/data:/data command: redis-sentinel /usr/local/etc/redis/sentinel.conf depends_on: - master - slave1 - slave2 networks: - redis-net注意哨兵的启动命令是redis-sentinel而不是redis-server虽然两者在 Redis 7.x 里其实可以互换redis-server加--sentinel参数也行但用redis-sentinel更明确。启动docker compose up -d sentinel1 sentinel2 sentinel34.4 验证哨兵状态连上任意一个哨兵docker exec -it redis-sentinel1 redis-cli -p 26379 sentinel master mymaster输出里应该能看到ip、port、num-slaves 2、num-other-sentinels 2这些信息。如果num-other-sentinels是 0说明哨兵之间没互相发现检查一下它们是不是在同一个 Docker 网络里以及sentinel monitor里写的主节点名是否一致。再看从节点列表docker exec -it redis-sentinel1 redis-cli -p 26379 sentinel slaves mymaster应该列出两个从节点的信息。5. 故障切换实测把主节点干掉看会发生什么5.1 触发切换直接停掉主节点容器docker stop redis-master然后盯着哨兵日志看docker logs -f redis-sentinel1你会看到类似这样的输出序列先是sdown master mymaster主观下线然后odown master mymaster客观下线说明达到了 quorum接着vote-for-leader开始选举 leader 哨兵最后switch-master mymaster 172.x.x.x 6379 172.x.x.x 6380切换完成新主是 6380。这个过程我实测下来大概 6 到 8 秒取决于down-after-milliseconds和网络延迟。5.2 验证新主切换完成后连上原来的从节点 6380docker exec -it redis-slave1 redis-cli -a redis123 info replication应该看到role:master。再连 6381应该看到role:slave且master_port:6380。然后往新主写数据docker exec -it redis-slave1 redis-cli -a redis123 set after_failover ok去 6381 读能读到就说明切换后复制关系正常。5.3 原主恢复后会怎样把原来的主节点重新启动docker start redis-master注意这时候它不会自动变回主节点而是会被哨兵降级为从节点去同步新的主节点。你可以在哨兵日志里看到convert-to-slave这样的记录。这是哨兵模式的正确行为——它不会因为原主回来了就把主节点身份还回去避免来回抖动。5.4 客户端怎么感知新主这是哨兵模式最容易被忽略的一环。哨兵本身不转发数据它只是告诉客户端现在主节点是谁。所以你的客户端代码必须支持哨兵模式。以常见的 Redis 客户端为例连接时不是填单个 IP而是填哨兵地址列表和主节点别名from redis.sentinel import Sentinel sentinel Sentinel( [(127.0.0.1, 26379), (127.0.0.1, 26380), (127.0.0.1, 26381)], socket_timeout0.5 ) master sentinel.master_for(mymaster, passwordredis123) master.set(client_test, hello)客户端会向哨兵查询当前主节点地址并在收到切换通知后自动重连新主。如果你用的是直连方式直接填 IP 和端口那切换后客户端就会一直连不上这是很多人踩的坑。6. 几个我实际踩过的坑和排查思路6.1 哨兵一直报sdown但主节点明明活着这种情况八成是密码问题。哨兵连主节点时用的是sentinel auth-pass里配的密码如果和主节点的requirepass不一致哨兵 ping 主节点会收到NOAUTH错误但它不会直接报密码错而是表现为连接异常最终判定主观下线。排查方法进哨兵容器手动用redis-cli连主节点试试docker exec -it redis-sentinel1 redis-cli -h master -p 6379 -a redis123 ping如果这里报错那就是密码或网络问题。6.2 切换后从节点同步失败有一次我遇到切换后新主起来了但从节点一直卡在MASTER - REPLICA sync started不动。查了半天发现是新主的masterauth没配。因为切换后新主需要以从节点身份去连其他从节点吗不是是因为哨兵在切换过程中会让新主执行SLAVEOF NO ONE但新主本身如果之前是从节点它的masterauth配置决定了它能不能接受其他从节点的同步请求。实际上更常见的原因是requirepass在新主上没生效导致从节点连不上。解决办法确保所有节点的masterauth和requirepass都配成一样的别偷懒。6.3 哨兵配置文件被重写后格式乱了哨兵在运行时会自动往配置文件里追加sentinel known-replica、sentinel known-sentinel这些行。如果你手动编辑过配置文件再启动时可能会因为格式问题报错。我的建议是哨兵的配置文件只写初始配置运行后不要手动改。如果确实要改先停掉哨兵删掉自动追加的那些行再改。6.4 Docker 网络里容器名解析失败如果你不用 compose 而是用docker run容器之间默认不在同一个网络里replicaof master 6379里的master解析不了。解决办法是手动创建网络docker network create redis-net docker run --network redis-net --name master ...用 compose 的话它会自动创建网络省事很多。7. 从单机到多机迁移时改什么这套单机方案跑通后迁移到多机环境其实只改几个地方。第一把replicaof和sentinel monitor里的容器名改成真实 IP 或域名。第二哨兵的sentinel monitor里主节点地址改成真实 IP。第三如果多机之间有时间差注意down-after-milliseconds要设得比机器间最大时钟偏差大否则容易出现误判。另外多机环境下哨兵的bind可以不用0.0.0.0改成具体内网 IP 更安全。protected-mode也可以根据实际情况决定是否开启。我个人在实际操作中的体会是单机哨兵最大的价值不是模拟生产而是让你在一个可控环境里把哨兵的日志、切换流程、客户端行为全部摸一遍。等你真正上多机的时候心里是有底的知道每一步该看什么日志、该验证什么状态。这比直接上生产环境试错成本低太多了。最后再分享一个小技巧测试切换的时候不要用docker stop用docker pause。stop会发送 SIGTERM 让 Redis 优雅退出哨兵可能来不及判定就收到正常关闭信号pause是直接冻结进程更接近真实的宕机场景能让你看到完整的故障判定和切换流程。