Redis 主库宕机 7.4 秒:鸿蒙任务接续服务的哨兵实战鸿蒙HarmonyOSRedis
系列第 2 篇。上一篇用 4 台云主机搭了鸿蒙多设备任务接续的后端,1 万长连接零丢失,但留了一个单点:Redis。这一篇把它换成主从 + 三哨兵,然后在 1 万连接压测中直接停掉主库,看真实表现。
环境与版本
| 项目 | 版本 / 规格 |
|---|---|
| 主机 | 4 台华为云 ECS,8 vCPU / 16 GiB,Ubuntu 24.04.4 |
| Redis | 7.0.15(1 主 2 从 + 3 哨兵) |
| Node.js | 20.20.2 |
| Nginx | 1.24.0(WebSocket 反代,proxy_read_timeout 600s) |
| 端侧 | 鸿蒙云手机 ALN-AL00 / HarmonyOS 5.0.1 / API 17 |
| 压测 | 自研 Node 长连接客户端,1 万并发 |
| 哨兵参数 | down-after-milliseconds 3000 / failover-timeout 60000 |
一、单点在哪里,为什么必须先解决它
上一篇的架构里,Redis 只有一台(192.168.0.155),却承担了两件不能丢的事:
- 设备影子:每台设备当前挂在哪个节点、最后一笔任务态是什么。设备重连后要靠它补齐断线期间的状态。
- 跨节点消息总线:A 设备连在 node1,B 设备连在 node3,消息必须经 Redis Pub/Sub 才能到达。
它一挂,整个集群立刻退化成三座孤岛:同节点设备还能通信,跨节点全部中断,重连的设备也读不到自己的影子。长连接服务可以容忍慢,不能容忍"状态消失"。
所以这一篇的目标很明确:Redis 主库宕机时,正在进行的任务接续不能丢状态,恢复时间要可量化。
二、部署:一主两从 + 三哨兵
4 台机器的分工不变,Redis 变成一主两从,哨兵和 Node 服务同机部署:
| 主机 | 内网 IP | Redis 角色 | 其他 |
|---|---|---|---|
| 0001 | 192.168.0.155 | 初始 master | Nginx + Node + 哨兵 |
| 0002 | 192.168.0.150 | replica | Node + 哨兵 |
| 0003 | 192.168.0.242 | replica | Node + 哨兵 |
从库只需两行配置:
replicaof 192.168.0.155 6379
save "" # 关掉 RDB,影子服务不需要落盘
哨兵配置(三台一致):
port 26379
bind 0.0.0.0
protected-mode no
sentinel monitor mymaster 192.168.0.155 6379 2
sentinel down-after-milliseconds mymaster 3000
sentinel failover-timeout mymaster 15000
sentinel parallel-syncs mymaster 1
quorum=2 表示至少两个哨兵认为主库下线才发起故障转移,这是防误判的关键——单哨兵网络抖动就可能触发一次无谓切换。
部署完验证:
role:master
connected_slaves:2
slave0:ip=192.168.0.242,port=6379,state=online,offset=28,lag=0
slave1:ip=192.168.0.150,port=6379,state=online,offset=28,lag=1
三、踩坑:Ubuntu 24.04 里没有 redis-sentinel.service
这是本次部署最意外的坑。按惯例 systemctl enable --now redis-sentinel 之后,服务状态是 inactive,journalctl -u redis-sentinel 显示 No entries,26379 端口没人监听——不是启动失败,是这个 unit 根本没被安装。
Ubuntu 24.04 的 redis-server 包不带 sentinel 的 systemd 单元,只能自己写一个:
[Unit]
Description=Redis Sentinel for Harmony shadow
After=network.target
[Service]
Type=simple
User=redis
Group=redis
ExecStart=/usr/bin/redis-server /etc/redis/sentinel.conf --sentinel
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
两个细节必须注意,否则哨兵会"看着像活的,其实不工作":
- 配置文件必须可写。哨兵会把当前 master、已发现的 slave 和其他哨兵回写到
sentinel.conf。如果文件属主是 root 而服务以 redis 用户运行,日志里会出现配置保存失败,哨兵之间互相发现不了。chown redis:redis /etc/redis/sentinel.conf之后再启动,日志里才出现Sentinel new configuration saved on disk。 protected-mode no和bind 0.0.0.0要一起改。只改 bind 不改保护模式,跨机哨兵依然连不上。
验证方式不是看进程,而是问它要主库地址:
redis-cli -p 26379 sentinel get-master-addr-by-name mymaster
# 192.168.0.155
# 6379
四、服务端改造:连接串换成哨兵发现
应用侧不能再写死 Redis IP,否则主库切换后所有写操作都会打到旧主上。ioredis 原生支持哨兵:
const redisOpts = {
sentinels: [
{ host: '192.168.0.155', port: 26379 },
{ host: '192.168.0.150', port: 26379 },
{ host: '192.168.0.242', port: 26379 },
],
name: 'mymaster',
};
const pub = new Redis(redisOpts); // 写
const sub = new Redis(redisOpts); // 订阅
三个哨兵地址全写上,任意一个活着就能发现主库。切换时 ioredis 会收到哨兵通知并自动重连新主,业务代码不用改。
五、实验:1 万连接压测中停掉主库
压测方式和上一篇一致:1 万模拟设备(5 千对 phone/tablet),每 5 秒一轮任务接续消息,全程经 Nginx。跑到第 35 秒,直接 systemctl stop redis-server 停掉主库。
哨兵日志给出了完整时间线:
22:51:05 停主库
22:51:10.187 +sdown master 主观下线(down-after 3000ms)
22:51:10.245 +odown master quorum 2/2 客观下线
22:51:10.416 +failover-state-send-slaveof-noone 192.168.0.242
22:51:11.257 +failover-state-reconf-slaves
22:51:12.378 +failover-end / +switch-master → 192.168.0.242
从停库到新主接管,一共 7.4 秒,其中 3 秒是配置里的主观下线判定窗口,真正的选主和提升只有 2 秒左右。
客户端侧的数据更有意思:
| 指标 | 数值 |
|---|---|
| 在线连接 | 10,000 |
| 消息总数 | 95,000 |
| 消息丢失 | 0 |
| WebSocket 重连次数 | 0 |
| RTT P50 | 192ms |
| RTT P95 | 5,692ms |
| RTT P99 | 5,803ms |
| RTT 最大值 | 5,855ms |
六、怎么解读这组数据
1. Redis 故障不会让 WebSocket 断开。 重连次数是 0,因为设备连的是 Node 节点,不是 Redis。很多方案把"存储高可用"和"接入层高可用"混在一起讨论,其实它们是独立的两层:存储挂了,接入层照样能保持连接。
2. 消息不是丢了,是排队了。 P50 还是 192ms,说明故障窗口之外的消息完全正常;P95/P99 冲到 5.7~5.8 秒,正好是切换窗口内那一批消息——它们在 ioredis 的重连队列里等到新主接管,然后被一次性发出去。对任务接续这种"最终一致比即时到达更重要"的场景,排队远比丢弃友好。
3. 7.4 秒的切换窗口 ≈ 5.8 秒的尾部延迟。 两者基本吻合,说明这套链路里没有额外的重试放大。想压缩尾部延迟,优先调 down-after-milliseconds(比如降到 1000ms),代价是对网络抖动更敏感。
4. 影子还在。 切换期间如果有设备重连,读到的影子来自从库副本,最多落后一个复制延迟(本例 lag=0~1)。这就是主从相对单机的本质提升:状态可恢复,而不只是服务可恢复。
七、端侧视角:Redis 挂了,鸿蒙客户端会怎样
上面所有数字都是服务端视角。但真正决定体验的是云手机上的 ArkTS 客户端。我在同一套环境里用真机云手机跑了一遍,把端侧行为拆成三件事。
7.1 连接层:不受影响
鸿蒙端用的是系统内置网络库:
import webSocket from '@ohos.net.webSocket';
this.ws = webSocket.createWebSocket();
this.ws.connect(`ws://1.92.107.89/ws?device=hm-phone-01&type=phone`);
它连的是 Nginx 的 80 端口,Nginx 再转到 Node 节点。Redis 在 Node 后面的第二层,端侧完全感知不到。
实测印证:kill -9 掉 Node 节点时,云手机立刻收到 close + error(因为接入层断了);而只停 Redis 主库时,云手机 WebSocket 一直保持连接、没有触发任何 close 事件。
这给了一个明确的架构结论:
| 故障 | 端侧表现 | 需要客户端做什么 |
|---|---|---|
| Redis 主库宕机 | 连接不断,消息延迟升高 | 显示"同步中",不要报错弹窗 |
| Node 节点宕机 | 连接断开 | 触发重连 + 拉影子 |
| Nginx 宕机 | 连接断开 | 同上 |
如果把两类故障都当成"网络异常"处理,用户会看到大量无意义的错误提示。
7.2 消息层:延迟升高,但不丢
哨兵切换的 7.4 秒里,P99 冲到 5.8 秒。端侧看到的是:点击"发送"后日志迟迟不出现"收到 ack"。
这是我在云手机上的真实截图(节点被 kill 的场景,断线与恢复路径一致):

12:18:45 已连接 hm-phone-01
12:18:48 收到 seq=305 来自=hm-tablet-01 [缺口1条->拉影子]
12:18:49 影子恢复: {"task":"tablet-task-305",...,"node":"node2","state":"online"}
注意最后那个 "node":"node2" —— 影子是从切换后的节点读出来的。这证明 Redis 层的状态接管对端侧是透明的:客户端不需要知道数据存在哪,只要重连后拉一次影子,拿到的就是最新状态。
7.3 体验层:三段式状态,别用二值判断
基于上面两点,鸿蒙端的连接状态不该只有「已连接/未连接」两个值。我改成了三态:
enum SyncState {
ONLINE, // 连接正常,消息即时
DEGRADED, // 连接在,但消息延迟高(存储层故障)
OFFLINE // 连接断开,需要重连
}
DEGRADED 的判断依据是 ACK 超时:发出消息后 3 秒没收到 ack,就切到降级态,UI 显示「同步中…」而不是「发送失败」。
这个状态机是这次实测倒逼出来的——如果只有二值判断,Redis 切换的 7.4 秒里,用户会看到「发送失败」,然后重复点击,产生大量重复消息。
八、旧主回归:不改配置就是双主
这是最容易被忽略的一步。故障后 192.168.0.242 已经是新主,如果直接把 0001 的 Redis 起回来,它的配置里没有 replicaof,会以主库身份启动——集群里立刻出现两个可写的主,客户端分别连到不同主库,影子数据从此分叉。
正确做法是先改配置再启动:
sed -i '/^replicaof/d' /etc/redis/redis.conf
echo 'replicaof 192.168.0.242 6379' >> /etc/redis/redis.conf
systemctl start redis-server
起来之后它是新主的从库,哨兵日志里能看到 +convert-to-slave 和 -sdown slave。
生产环境建议把这一步写进故障恢复 SOP,或者用哨兵的 notification-script / 配置管理工具自动完成,别指望运维半夜还记得这条命令。
九、结论
回到本篇的目标:Redis 主库宕机时,任务接续不丢状态,恢复时间可量化。
实测答案是:
- 一主两从 + 三哨兵,7.4 秒完成切换,其中 3 秒是主动配置的下线判定窗口;
- 1 万长连接全程零重连、零消息丢失;
- 代价是切换窗口内消息尾部延迟 5.8 秒,属于排队而非丢失;
- 真正的风险点在切换之后——旧主必须以从库身份回归,否则双主脑裂比宕机更难收拾;
- 端侧要区分两类故障:存储故障表现为延迟,接入故障才表现为断连,用三态(在线/降级/离线)而不是二值判断。
下一篇换个方向:之前所有压测的 P50 都在 200ms 上下,但服务端 CPU 只有 3%。这一篇把压测客户端拆成多进程,看看 2 万设备时到底是谁在拖后腿。
环境:4 × 华为云 x3e.8u.16g(8C16G,Ubuntu 24.04.4),Redis 7.0.15,Node 20.20.2,Nginx 1.24.0,2026-11 实测。
更多推荐




所有评论(0)