降级与容灾:多机房、故障隔离与预案
IM 的降级比电商更棘手:电商大促时可以关掉推荐,IM 关不掉“发消息”这个动作,只能决定“用什么代价把消息送到”。本章按故障域从内到外拆开——单进程、单机、单可用区、单机房、单依赖——每一层给出可执行的开关、阈值与回退条件,并列出必须演练过的预案。
故障域分层
| 层级 | 典型故障 | 影响面 | 第一动作 | 目标恢复时间 |
|---|---|---|---|---|
| 单进程 | OOM、死锁、goroutine 泄漏 | 该实例上的连接 | 重启并摘除流量 | 秒级 |
| 单机 | 宿主机宕机、网卡故障 | 该机全部连接(约 8 万) | 客户端重连,路由表收敛 | 30 秒内 |
| 单可用区 | 交换机、AZ 级断电 | 该区约一半连接 | 客户端切到另一区接入点 | 1~3 分钟 |
| 单机房 | 机房出口故障、整机房失联 | 该机房全部用户 | GSLB 摘除 + 客户端切接入点 | 3~10 分钟 |
| 单依赖 | Redis 路由表、MySQL、MQ、推送通道 | 受影响的功能链路 | 按开关降级,绕过该依赖 | 秒级~分钟级 |
故障域的划分决定隔离手段:单进程靠重启与健康检查,单机靠路由表收敛,机房级靠接入点切换,依赖级靠降级开关。把机房级问题当成实例问题处理,是 IM 故障里最常见的误判。
多机房形态与就近接入
| 形态 | 数据写入 | 接入方式 | 代价 | 适用规模 |
|---|---|---|---|---|
| 单机房多可用区 | 单点主库,跨 AZ 同步副本 | 同一组接入点,按 AZ 打散 | 机房级故障不可用 | DAU 百万以内 |
| 同城双活 | 路由表双写,消息库单主 | 两个接入点按 AZ 就近 | 双写冲突需设计,主库仍是单点 | DAU 千万级 |
| 异地多活(单元化) | 按 uid 划分单元,单元内闭环 | GSLB 按地域解析 + 接入点列表下发 | 跨单元消息多一跳,运维复杂度翻倍 | 用户跨地域、SLA 99.95% 以上 |
| 两地三中心 | 一主两备,异地只做数据备份 | 备份机房不接流量 | 故障切换要分钟级,成本高 | 有数据合规或容灾硬要求 |
| 主备冷备 | 备用机房常态不承载 | 手动切流 | 切换要人工决策,容量需预留 | 成本敏感的中小规模 |
客户端接入点发现的三层兜底:
1. 内置域名列表 → DNS/GSLB 按地域与健康检查返回最近接入点
2. 登录接口下发接入点列表与权重,客户端可自选,故障时快速切换
3. 已建连接不迁移:机房故障时只能断开重连(会话重建、路由表重写)
因此“机房切换时间” ≈ 客户端探测 + 重连 + 鉴权 + 路由表重建,要单独压测
三条铁律:路由表按 uid 分片并与单元绑定,单元内自治,不在运行时做跨机房 Redis 复制(延迟与冲突都不可控);跨单元投递走消息总线,不要试图让一个单元直接写另一个单元的用户连接;同一 uid 在同一刻只有一个权威在线单元,多端登录按设备维度记录,否则故障切换后会出现“两个地方都认为用户在线”。
故障隔离:舱壁与配额
- 按 uid 分槽(slot):接入机只服务固定槽位,路由表按槽位查询;单机故障只影响自己的槽位,重连后由同槽位的其他机器接管。
- 链路隔离:上行处理、下行投递、离线落库、推送调用各自独立队列与并发上限,任一条链路变慢不拖死其他链路。
- 大客户与大群隔离:VIP 用户、超大群单独集群或单独配额,避免一个热点群打满共享池。
- 单连接与单 IP 限速:单连接上行正常 20 条/s,异常时降到 5 条/s;单 IP 建连 60 次/分钟,防止脚本刷连接。
降级开关清单
| 开关 | 降级后的行为 | 触发条件 | 用户可感知影响 | 回退条件 |
|---|---|---|---|---|
| 关闭离线推送 | 只投在线用户,离线靠上线拉取 | 推送通道错误率 > 30% 或队列积压 > 10 分钟 | 离线通知变慢 | 通道恢复且积压清零 |
| 降级为拉模式 | 关闭实时下行,客户端改长轮询 /sync | 广播放大打满出口带宽 | 消息延迟从毫秒变秒级 | 下行带宽回落到 60% 以下 |
| 关闭已读回执 | 不再实时同步已读状态,只保留未读数近似值 | 回执链路 QPS 超预算 | 多端已读状态短暂不一致 | 回执队列水位正常 |
| 关闭历史检索 | 在线搜索走离线归档,不做实时检索 | 检索拖慢主库 | 搜索变慢 | 主库负载恢复 |
| 降低限流阈值 | 上行 5 万/s → 2 万/s,登录 1 万/s → 3000/s | 逻辑层或存储水位 > 80% | 高活跃用户被限速 | 水位回落到 60% 以下 |
| 大群降级 | 超 5000 人群只发“有新消息”提示,正文由客户端拉取 | 单个群下行超预算 | 大群消息需点开才看到 | 群下行速率恢复正常 |
| 关闭内容审核同步链路 | 改为先发后审 + 事后撤回 | 审核服务不可用(需先与合规确认) | 违规内容可能短暂可见 | 审核服务恢复 |
| 只保在线用户 | 停止离线落库与推送,仅服务在线连接 | 存储层不可写 | 离线用户上线后可能缺消息 | 存储恢复且补写完成 |
| 关闭非核心埋点 | 停止行为埋点与调试日志 | 日志盘或采集链路打满 | 无 | 磁盘水位恢复 |
开关的使用顺序遵守一条原则:先保“消息不丢”(落库),再保“在线可达”,最后才保“即时”。允许变慢、允许延迟送达、允许退化成拉模式;不允许丢消息、乱序或让用户看到重复消息。
开关怎么落地
- 用配置中心(Nacos、Apollo、etcd 之类)下发,秒级生效,带版本号、操作人与变更审计。
- 开关必须支持灰度维度:能只对某个机房、某批 uid 或某个客户端版本生效,否则一次误操作就是全量故障。
- 每个开关都要写清五项:默认值、影响面、触发条件、观察指标、回退条件。缺一项就不算可用开关。
- 开关数量做减法:超过 20 个开关的组合态没人能推理清楚,合并成 3~5 个降级档位(L0 正常、L1 关非核心、L2 限流、L3 只保在线)。
- 三个月未被触发的开关要么删除、要么补一次演练;长期不用的开关本身就是风险。
预案清单与触发条件
| 场景 | 判定口径 | 动作(按顺序) | 回退条件 |
|---|---|---|---|
| Redis 路由表不可用 | 路由查询错误率 > 5% | 切本地路由缓存 → 停止新登录 → 只保已建立连接 | 错误率 < 0.1% 且缓存已回填 |
| MySQL 主库不可写 | 写入错误率上升或主库切换 | 消息先入 MQ 缓冲 → 关闭历史写入 → 只保在线投递 | 主库恢复且 MQ 积压清零 |
| 单机房失联 | 该机房指标整体消失 | GSLB 摘除 → 通告客户端切接入点 → 路由表按槽位重建 | 机房恢复且连接平稳 |
| 消息队列堆积 | 消费延迟 > 5 分钟 | 扩容消费者 → 降级非核心消费 → 按优先级丢弃可丢消息(如已读回执) | 延迟 < 30 秒 |
| 推送通道大面积失败 | 通道成功率 < 70% | 切换备用通道 → 关闭离线推送 | 成功率达 95% |
| 热点群消息风暴 | 单群下行速率超阈值 | 该群降级为提示模式 → 单用户限速 | 群速率回落 |
| 重连风暴 | 建连速率超设计值 3 倍 | 限速建连 → 延长重连退避 → 优先放行老连接 | 建连速率回落 |
| 内容安全事故 | 违规内容大面积传播 | 关闭相关会话下行 → 保留证据 → 人工介入 | 合规确认后 |
演练与验证
# 1. 单机故障:kill 一台接入机,观察摘除时间与重连速率
kubectl delete pod im-gateway-7d9c --grace-period=0 # 或 kill -9 <pid>
# 2. 网络故障:注入 200ms 延迟与 5% 丢包,验证超时与重试是否放大流量
tc qdisc add dev eth0 root netem delay 200ms loss 5%
tc qdisc del dev eth0 root netem # 演练后必须清理
# 3. 依赖故障:让 Redis 响应变慢,验证路由表降级与本地缓存兜底
redis-cli -h 10.0.1.5 -p 6379 DEBUG SLEEP 30 # 仅限测试环境
| 演练项 | 成功判据 | 常见不合格 |
|---|---|---|
| kill 单台接入机 | 受影响用户 30 秒内重连成功 ≥ 99%,路由表 10 秒内收敛 | 重连无退避,鉴权接口被打满 |
| 摘除单个可用区 | 剩余容量承接 100% 流量,P99 不超过基线 2 倍 | 剩余容量只够 60%,一摘就过载 |
| 关闭离线推送 | 在线送达率不掉,离线用户上线后补拉完整率 100% | 离线消息位点错乱 |
| 降级为拉模式 | 客户端一个版本内可切换且有明确提示 | 老版本客户端不识别,表现为“收不到消息” |
| 降低限流阈值 | 超限请求返回可重试错误码,连接不被断开 | 直接断连,客户端进入重连风暴 |
演练必须有明确的成功判据、一键终止手段和观察人;没有监控与预案的演练只是制造故障。
用户可感知的取舍
- 任何降级都要在客户端有明确反馈(例如“消息延迟送达”标记),否则用户会把延迟当成丢失,转而投诉。
- 降级期间的错误码要区分“可重试”与“不可重试”,客户端据此决定退避还是丢弃。
- 故障结束后按时间线复盘:每个开关的开启时刻、回退时刻与当时的指标,都要能对上(见 25 章可观测性口径)。
小结:降级与容灾的第一步是分清故障域(进程、机器、可用区、机房、依赖),多机房的核心不是“部署两套”而是把路由、会话与数据的权威归属写清楚;隔离靠按 uid 分槽、链路独立队列与大客户配额;降级靠一组有触发条件、有回退条件、可灰度的开关,顺序永远是先保消息不丢、再保在线可达、最后才保即时送达;所有预案都必须演练过并有量化判据,否则等同于没有。