版本信息最后核对:2026-10-25
Lab 状态:实跑通过
故障切换、双主隔离与 Sentinel 收敛通过;计划内回切按实际主节点判断
安全边界:本 Lab 只允许在一次性虚拟机或本地 Docker Compose 中进行。不要在生产 Redis 上执行
REPLICAOF NO ONE、SENTINEL FAILOVER、SHUTDOWN或任何摘除节点的命令。真实切换涉及连接池、Sentinel quorum、DNS/VIP、CDC 和业务写入方,必须走变更流程并明确 RPO。实验数据卷在结束后整体删除。
准备 Docker 与 Docker Compose v2,预留约 1 GB 内存。所有容器只挂在实验专用网络 lab-redis-net,不映射端口到宿主机。macOS 若没有 timeout,可用 coreutils 的 gtimeout 替代,不要省略超时。
timeout 10 mkdir -p /tmp/lab-redis/{backup,sentinel-1,sentinel-2,sentinel-3}
timeout 30 docker version --format '{{.ServerVersion}}'
timeout 30 docker compose version
三个 Sentinel 各用一份配置。resolve-hostnames 和 announce-hostnames 让哨兵在容器网络中保存服务名而不是易变 IP;quorum 为 2,三哨兵中允许一台失联。下面的 chmod 777 只针对一次性实验目录,生产配置必须使用最小权限:
for i in 1 2 3; do
cat > /tmp/lab-redis/sentinel-$i/sentinel.conf <<'EOF'
port 26379
sentinel resolve-hostnames yes
sentinel announce-hostnames yes
sentinel monitor labmaster redis-master 6379 2
sentinel down-after-milliseconds labmaster 5000
sentinel failover-timeout labmaster 60000
sentinel parallel-syncs labmaster 1
EOF
done
timeout 30 chmod -R 777 /tmp/lab-redis/sentinel-1 /tmp/lab-redis/sentinel-2 /tmp/lab-redis/sentinel-3
编写 Compose 文件。主库开启 AOF 便于停止后恢复;副本显式只读;client 容器只作为模拟应用,用来验证静态地址与 Sentinel 发现两种连接方式:
cat > /tmp/lab-redis/docker-compose.yml <<'EOF'
name: lab-redis
services:
redis-master:
image: redis:7.2
container_name: lab-redis-master
command: ["redis-server", "--appendonly", "yes", "--save", ""]
redis-replica-1:
image: redis:7.2
container_name: lab-redis-replica-1
depends_on: [redis-master]
command: ["redis-server", "--replicaof", "redis-master", "6379"]
redis-replica-2:
image: redis:7.2
container_name: lab-redis-replica-2
depends_on: [redis-master]
command: ["redis-server", "--replicaof", "redis-master", "6379"]
sentinel-1:
image: redis:7.2
container_name: lab-redis-sentinel-1
depends_on: [redis-master]
command: ["redis-server", "/etc/redis/sentinel.conf", "--sentinel"]
volumes: ["./sentinel-1:/etc/redis"]
sentinel-2:
image: redis:7.2
container_name: lab-redis-sentinel-2
depends_on: [redis-master]
command: ["redis-server", "/etc/redis/sentinel.conf", "--sentinel"]
volumes: ["./sentinel-2:/etc/redis"]
sentinel-3:
image: redis:7.2
container_name: lab-redis-sentinel-3
depends_on: [redis-master]
command: ["redis-server", "/etc/redis/sentinel.conf", "--sentinel"]
volumes: ["./sentinel-3:/etc/redis"]
client:
image: redis:7.2
container_name: lab-redis-client
command: ["sleep", "infinity"]
networks:
default:
name: lab-redis-net
EOF
timeout 240 docker compose -f /tmp/lab-redis/docker-compose.yml up -d
timeout 180 bash -c 'until docker exec lab-redis-replica-1 redis-cli INFO replication | grep -q "master_link_status:up"; do sleep 2; done'
timeout 180 bash -c 'until docker exec lab-redis-replica-2 redis-cli INFO replication | grep -q "master_link_status:up"; do sleep 2; done'
timeout 60 docker exec lab-redis-sentinel-1 redis-cli -p 26379 SENTINEL masters
写入基线并确认副本可读。复制是异步的,写入主库成功不代表副本已经收到,后续偏移对比才有意义:
timeout 20 docker exec lab-redis-master redis-cli SET lab:order:1 '{"amount":100}'
timeout 20 docker exec lab-redis-master redis-cli HSET lab:session:1 user alice
timeout 20 docker exec lab-redis-master redis-cli EXPIRE lab:session:1 600
timeout 20 docker exec lab-redis-master redis-cli INCR lab:counter
timeout 60 bash -c 'until [ "$(docker exec lab-redis-replica-1 redis-cli GET lab:counter)" = "1" ]; do sleep 1; done'
timeout 20 docker exec lab-redis-client redis-cli -h redis-replica-1 GET lab:counter
READONLY_OUT=$(timeout 20 docker exec lab-redis-client redis-cli -h redis-replica-1 SET lab:readonly test 2>&1); echo "replica_write_output=$READONLY_OUT"
最后一条应返回 READONLY。redis-cli 对服务端错误通常仍以退出码 0 结束,判据是输出中的 READONLY 文本,而不是 $?。在注入故障前做一次可验证快照,作为回滚和取证材料,文件放到宿主机实验目录:
timeout 60 docker exec lab-redis-master redis-cli SAVE
timeout 30 docker cp lab-redis-master:/data/dump.rdb /tmp/lab-redis/backup/before-failover.rdb
timeout 30 shasum -a 256 /tmp/lab-redis/backup/before-failover.rdb | tee /tmp/lab-redis/backup/before-failover.sha256
先固定偏移基线,再停止主节点。docker stop 是受控停止,不能代表断电;真实宕机可能丢失尚未传播的写入:
timeout 30 docker exec lab-redis-master redis-cli INFO replication > /tmp/lab-redis/backup/master-before.txt
timeout 30 docker exec lab-redis-replica-1 redis-cli INFO replication > /tmp/lab-redis/backup/replica-1-before.txt
timeout 30 docker exec lab-redis-replica-2 redis-cli INFO replication > /tmp/lab-redis/backup/replica-2-before.txt
# 以下变量和后续切换命令应在同一个 shell 会话中执行;跨终端会丢失 OLD_OFFSET/NEW_MASTER。
OLD_OFFSET=$(awk -F: '/^master_repl_offset:/{print $2+0}' /tmp/lab-redis/backup/master-before.txt)
echo "old_master_offset=$OLD_OFFSET"
timeout 60 docker stop lab-redis-master
timeout 30 docker ps -a --filter name=lab-redis-master --format '{{.Names}} {{.Status}}'
等待 Sentinel 完成提升。默认 down-after-milliseconds 为 5 秒,多数派满足后才会客观下线并发起故障转移,不要在这段时间内重复手工提权:
timeout 120 bash -c 'until for c in lab-redis-replica-1 lab-redis-replica-2; do docker exec "$c" redis-cli --raw ROLE | head -1; done | grep -qx master; do sleep 3; done'
timeout 30 docker logs --since 10m lab-redis-sentinel-1 | grep -E '\+sdown|\+odown|\+try-failover|\+promoted-slave|\+switch-master' | tail -20
NEW_MASTER=$(for c in lab-redis-replica-1 lab-redis-replica-2; do
if [ "$(timeout 20 docker exec "$c" redis-cli --raw ROLE | head -1)" = master ]; then echo "$c"; fi
done | head -1)
echo "new_master=$NEW_MASTER"
timeout 30 docker exec "$NEW_MASTER" redis-cli INFO replication | head -12
对比旧主最后记录的偏移与新主当前偏移。注意两者不一定同一量纲:Redis 7 默认的 diskless 全量同步会把 RDB 字节计入新主的 master_repl_offset,因此差值可能为负,也可能明显大于实际丢失量。差值只能在同一次同步上下文里做方向性参考,不能单独量化 RPO:
NEW_OFFSET=$(timeout 30 docker exec "$NEW_MASTER" redis-cli INFO replication | awk -F: '/^master_repl_offset:/{print $2+0}')
echo "old=$OLD_OFFSET new=$NEW_OFFSET delta=$((OLD_OFFSET-NEW_OFFSET))"
timeout 20 docker exec "$NEW_MASTER" redis-cli INFO replication | grep -E 'sync_full|sync_partial_ok|sync_partial_err'
timeout 20 docker exec "$NEW_MASTER" redis-cli SET lab:order:2 '{"amount":200}' for c in lab-redis-replica-1 lab-redis-replica-2; do echo "== $c"; timeout 20 docker exec "$c" redis-cli ROLE | head -1 timeout 20 docker exec "$c" redis-cli GET lab:order:2 done
验证客户端重连。静态地址仍指向已停止的旧主,必然失败;Sentinel 感知路径会重新解析主节点并重建连接,这也是应用连接池在切换后应实现的行为:
```shell
timeout 20 docker exec lab-redis-client redis-cli -h redis-master GET lab:counter; echo "static_exit=$?"
timeout 20 docker exec lab-redis-client sh -c 'H=$(redis-cli -h lab-redis-sentinel-1 -p 26379 --raw SENTINEL get-master-addr-by-name labmaster | head -1); echo "resolved=$H"; redis-cli -h "$H" INCR lab:counter'
现在制造双主窗口。暂停全部 Sentinel,阻止它们把旧主降级;重新启动旧主,它会按本地 AOF 恢复并以主节点身份接受写入。此时新主和旧主都可写,正是最危险的脑裂状态:
timeout 30 docker pause lab-redis-sentinel-1 lab-redis-sentinel-2 lab-redis-sentinel-3
timeout 60 docker start lab-redis-master
timeout 30 bash -c 'until docker exec lab-redis-master redis-cli PING | grep -q PONG; do sleep 1; done'
timeout 20 docker exec lab-redis-master redis-cli ROLE | head -2
timeout 20 docker exec lab-redis-master redis-cli SET lab:rogue checkout-2026
timeout 20 docker exec "$NEW_MASTER" redis-cli SET lab:good verified
恢复 Sentinel 后,它们会把旧主重新配置为新主的副本。旧主偏移领先或复制历史不匹配时只能全量同步,分叉写入会被覆盖。生产中不能依赖这一步“自动修好”,必须先把旧主隔离并评估损失:
timeout 30 docker unpause lab-redis-sentinel-1 lab-redis-sentinel-2 lab-redis-sentinel-3
timeout 120 bash -c 'until [ "$(docker exec lab-redis-master redis-cli --raw ROLE | head -1)" = slave ]; do sleep 3; done'
timeout 120 bash -c 'until docker exec lab-redis-master redis-cli INFO replication | grep -q "master_link_status:up"; do sleep 3; done'
timeout 20 docker exec lab-redis-master redis-cli EXISTS lab:rogue
timeout 20 docker exec lab-redis-master redis-cli INFO replication | grep -E 'role|master_link_status|slave_repl_offset'
主节点停止后,副本的 master_link_status 变为 down,Sentinel 日志依次出现 +sdown、+odown、+try-failover、+promoted-slave 和 +switch-master。被提升的副本 role 变为 master,另一个副本自动改指向新主并恢复 master_link_status:up。旧主重新启动后先以 role:master 出现,说明它的本地持久化数据仍在,Sentinel 恢复后才被降为副本。
成功的判据是:Sentinel 多数派健康且只发生一次切换;新主可写、副本重新指向新主并追平;旧主最终只读;lab:rogue 在权威数据中不存在;静态客户端失败后能通过 Sentinel 重新发现新主;切换前快照的校验和可验证。失败的判据是:两个节点长期同时可写;Sentinel 反复 +switch-master;新主写入报错;副本长期 master_link_status:down;关键 key 数量、值或 TTL 与基线无法解释;旧主的分叉写入在新主上仍然存在。任意一条成立都应停止流量切换,先隔离写入方。
INFO replication 的 role、master_link_status、master_last_io_seconds_ago 和 slave_repl_offset 能区分主、从、断链和延迟;字段名随版本可能显示 slave 或 replica,以当前输出为准。sync_full 计数和监控写入量共同判断。REPLICAOF NO ONE。role:master 只代表进程角色,不代表它仍是权威数据源。它可能在隔离期间接受过写入,必须比较偏移、key 和业务日志后再决定。WAIT 只影响当前连接此前的写入,不提供事务级强一致。若新主尚未接受业务写入,回滚相对简单:停止新主,确认旧主数据完整后启动旧主,再让 Sentinel 重新收敛,最后把新主按副本重建。若新主已经接受写入,绝不能直接把旧主拉回读写入口,否则旧主上的分叉数据会与新主冲突。
演练中的收敛路径已经演示:旧主作为副本重新加入,全量同步覆盖分叉写入。真实事故中要先隔离旧主、保留快照,再决定是否让它以副本身份回归。若数据有业务价值,只能从快照或 AOF 中提取并做幂等补偿,不能直接把旧数据写回新主。
需要计划内回切时,等旧主追平并保持健康,再用受控切换把主节点切回。回切是一次新变更,必须重新验证客户端、监控和备份,不要在故障刚结束时立即执行:
# SENTINEL FAILOVER 按 replica-priority/offset 选择副本,返回 OK 只代表命令被接受,不保证选中旧主。
# 如需回切到旧主,先把旧主优先级调低(数值越小越优先),并等待 Sentinel 刷新 INFO:
timeout 20 docker exec lab-redis-master redis-cli CONFIG SET replica-priority 10
timeout 20 docker exec lab-redis-replica-1 redis-cli CONFIG SET replica-priority 100
timeout 20 docker exec lab-redis-replica-2 redis-cli CONFIG SET replica-priority 100
CURRENT_MASTER=$(timeout 30 docker exec lab-redis-sentinel-1 redis-cli -p 26379 --raw SENTINEL get-master-addr-by-name labmaster | head -1)
echo "current_master=$CURRENT_MASTER"
timeout 60 docker exec lab-redis-sentinel-1 redis-cli -p 26379 SENTINEL FAILOVER labmaster
# 等 Sentinel 报出的主地址变化并连续稳定 15 秒;旧主短暂成为主又切走不能算成功。
timeout 240 bash -c '
stable=0; last=""
while [ "$stable" -lt 3 ]; do
a=$(docker exec lab-redis-sentinel-1 redis-cli -p 26379 --raw SENTINEL get-master-addr-by-name labmaster | head -1)
if [ -n "$a" ] && [ "$a" = "$last" ]; then stable=$((stable+1)); else stable=0; last="$a"; fi
sleep 5
done
echo "stable_master=$last"
'
RESOLVED_MASTER=$(timeout 30 docker exec lab-redis-sentinel-1 redis-cli -p 26379 --raw SENTINEL get-master-addr-by-name labmaster | head -1)
echo "resolved_master=$RESOLVED_MASTER"
for c in lab-redis-master lab-redis-replica-1 lab-redis-replica-2; do
ip=$(timeout 20 docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$c")
echo "$c ip=$ip role=$(timeout 20 docker exec "$c" redis-cli --raw ROLE | head -1)"
done
把 resolved_master 与上面的容器 IP 对照:只有它指向 lab-redis-master 且角色为 master,才说明回切真的完成。若 Sentinel 选了另一个副本,SENTINEL FAILOVER 依然返回 OK,但“旧主已切回”的判断是错的;按实际结果记录,不要继续套用后续验证里“新主”的假设。
MASTER_ADDR=$(timeout 30 docker exec lab-redis-sentinel-1 redis-cli -p 26379 --raw SENTINEL get-master-addr-by-name labmaster | head -1)
echo "authoritative_master=$MASTER_ADDR"
timeout 30 docker exec lab-redis-client redis-cli -h "$MASTER_ADDR" INFO replication | grep -E 'role|connected_slaves|master_repl_offset'
for c in lab-redis-master lab-redis-replica-1 lab-redis-replica-2; do
echo "== $c"; timeout 20 docker exec "$c" redis-cli ROLE | head -1
timeout 20 docker exec "$c" redis-cli DBSIZE
timeout 20 docker exec "$c" redis-cli EXISTS lab:rogue
done
timeout 20 docker exec lab-redis-client redis-cli -h "$MASTER_ADDR" --scan --pattern 'lab:*' | sort
timeout 20 docker exec lab-redis-client redis-cli -h "$MASTER_ADDR" TTL lab:session:1
timeout 20 docker exec lab-redis-client redis-cli -h "$MASTER_ADDR" INCR lab:counter
+switch-master,没有持续 sdown/odown 抖动。role:master 且可读写;其余节点为副本,master_link_status:up,偏移持续接近新主。DBSIZE 与关键 key 一致;lab:rogue 为 0,说明分叉数据已被权威数据覆盖。lab:order:1、lab:order:2、lab:session:1 的值与 TTL 符合基线或业务解释;计数器可继续递增。全部验证通过后再清理本项目的容器、网络和卷,不要对任何生产项目执行 down -v。清理前确认快照、Sentinel 日志和偏移记录已经复制到宿主机:
timeout 120 docker compose -f /tmp/lab-redis/docker-compose.yml down -v --remove-orphans
timeout 30 docker volume ls --filter name=lab-redis
rm -rf /tmp/lab-redis
READONLY?客户端需要做什么?READONLY。客户端必须通过 Sentinel 重新解析当前主节点、重建连接池;Cluster 客户端还要处理 MOVED/ASK 并刷新槽位,不能依赖固定地址。master_repl_offset 与新主提升后的偏移:差值大于 0 可能意味着有未传播写入,但 diskless 全量同步会把 RDB 字节计入新主偏移,差值为负同样正常。基线是停止前的采样,最后一条写入也可能发生在采样之后;复制是异步的,差值只能说明风险窗口,必须结合业务 key、AOF 和监控写入量核算 RPO。
小程序版
微信扫码,随时查命令