降级与容灾:多机房、故障隔离与预案

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 分槽、链路独立队列与大客户配额;降级靠一组有触发条件、有回退条件、可灰度的开关,顺序永远是先保消息不丢、再保在线可达、最后才保即时送达;所有预案都必须演练过并有量化判据,否则等同于没有。

笔记加载中…