一次容灾切换的完成,不是建好冗余容量,而是流量真正搬过去。本文从一次真实的切换演练出发,拆解流量调度如何从静态 DNS 走向动态、单元化调度,让切换变成可小步、可验证、可回退的标准动作。
凌晨两点,一次计划内的同城容灾切换演练正在进行。数据同步早就追平了,备站的容量也预留好了,值班同学按预案把数据库主从切了过去,然后盯着监控等流量跟着走。一分钟过去,五分钟过去,十分钟过去,备站的 QPS 只爬到了预期值的三成,而主站这边,还有六成流量在往一个已经"逻辑下线"的站点里灌,报错率开始攀升。
复盘的时候,问题一层一层浮出来。接入层用的是 DNS 加权解析,TTL 设置的 600 秒,也就是说解析记录的变更最长要 10 分钟才在各个 LocalDNS 上生效;而实际情况更糟,部分运营商的递归缓存并不严格遵守 TTL,还有客户端自己做的本地缓存和连接复用,二十分钟后仍有百分之几的流量解析到旧站点。更麻烦的是,这次切换是"全量或全无"的,没人知道切过去的流量在备站跑得怎么样,发现问题也没办法只回退一小部分。
复盘到最后,锅落不到任何一条配置头上。容灾体系里有冗余的容量、有同步的数据,却缺少一个能指挥流量快速、精确、可回退移动的调度层。冗余是静态的资产,流量真正被调度过去的那一刻,它才变成动态的容灾能力。
流量调度是容灾的执行层:故障检测决定"该不该切",故障止损决定"怎么先止血",而流量调度决定"流量到底往哪里去、走多快、能不能退回来"。
这一讲就来回答几个具体问题:为什么静态的 DNS 调度撑不起千万 QPS 的容灾;动态调度把开关从公网搬进自己家里之后,调度能力是怎么在接入层、网关层、服务层一层层长出来的;单元化架构里,流量调度如何做到按用户维度精确切换;以及最容易踩的坑——切换本身引发的二次故障要怎么防。
容灾闭环里,调度是那只"手"
先把流量调度放回容灾的整体框架里。一次完整的容灾响应,是一个从发现到恢复的闭环:

检测回答"发生了什么",决策回答"切不切、切多少",调度回答"怎么把流量搬过去",验证回答"搬对了没有"。检测与止损在前两讲已经讨论过,这一讲的主角是执行这一环。这里有一条铁律:闭环的总耗时取决于最慢的一环,而调度这一环恰恰是最容易被低估的。
很多团队的容灾建设重"备"轻"调":双机房建了,数据同步做了,容量冗余留了,但流量怎么从 A 机房挪到 B 机房,靠的还是一份操作手册加几条 DNS 记录。结果就是演练时发现的场景:备站空有容量,流量却过不去。
用一句话概括静态与动态的区别:
静态调度是"把地图画好,让流量自己找路";动态调度是"在每一个路口安排交警,实时指挥流量改道"。
静态调度的典型代表是 DNS 加权解析和固定路由表。它们简单、稳定、几乎不依赖额外组件,在规模不大、故障模式单一的时候完全够用。但当系统长到千万 QPS,故障从"机房整体断电"这种粗粒度事件,变成"某个可用区的某批交换机丢包率升高"这种细粒度事件时,静态调度的三个结构性缺陷就会全部暴露出来。
静态调度的三个死穴
第一个死穴是传播时延。DNS 解析是一条从客户端到 LocalDNS 再到权威服务器的链路,每一跳都有缓存,每一跳的缓存策略都不完全受你控制。TTL 你能设,但递归服务器遵守与否是另一回事;客户端的连接复用更彻底,一个长连接建立时用的 IP,在整个连接生命周期内都不会因为解析记录变化而改变。所以 DNS 切换的真实生效曲线不是一条陡直的线,而是一条长尾很重的曲线:TTL 时间点上生效六成左右,剩下的流量要几分钟到几十分钟才慢慢流完。对千万 QPS 的系统,这条长尾上的每一分钟,都是数以亿计的失败请求。
第二个死穴是不可观测、不可控。DNS 切换是"广播式"的:你改了记录,但不知道哪些客户端生效了、生效到了哪个程度,也没有手段只让 5% 的流量先生效。切换的效果只能从业务监控倒推,等发现备站容量不够想回退时,又得再经历一遍同样漫长的传播过程。一来一回,故障窗口被放大成两倍。
第三个死穴是粒度太粗。DNS 能区分的维度基本只有地域(基于 LocalDNS 的位置)和权重,它没法表达"把订单相关流量切走但支付先不动""把某个大客户的流量单独摘出去""只切读流量写流量不动"这类诉求。而真实的故障止损场景,恰恰大量依赖这种细粒度的流量手术。
讨论 TTL 的时候,还有一层麻烦很少被提起:DNS 调度的生效路径上有太多你管不到的环节。你的权威解析服务改了记录,但请求路径上是用户的 LocalDNS 在替你读这条记录,而 LocalDNS 部署在运营商和各类网络服务商那里,它们的缓存实现五花八门:有的严格遵守 TTL,有的按固定时长缓存,有的干脆把 TTL 改成自己的默认值。你做过最严谨的切换预案,最终生效速度却取决于路径上最不可控的那一环。这种"控制权外泄"是静态调度在架构上的原罪,不是调参能解决的。

注意最后一行,这是动态调度的代价:你把开关搬进了自己家里,就要自己保证这个开关永远打得动。控制面在故障中失效,是动态调度体系里最高级别的事故场景,后文会专门讨论这个问题。
把开关搬进自己家里:分层流量调度
动态调度的核心思路只有一句话:流量经过的每一层,都要具备按规则改变流向的能力。从用户请求到达系统到数据落盘,流量会依次穿过接入层、网关层、服务层、数据层,每一层有自己的调度手段、粒度和生效速度:

接入层解决的是"去哪个站点"。手段包括动态 GSLB(全局负载均衡,基于实时探测的机房质量做解析调度)、BGP Anycast(同一 IP 多站点宣告,故障站点撤销路由,网络层自动收敛)、以及自建接入层的健康探测与摘除。这一层的特点是覆盖所有流量入口,包括那些不走 HTTP 的长连接和 SDK 直连流量。BGP 收敛通常在秒级到几十秒完成,是站点级故障时最快的兜底手段。
网关层解决的是"去哪个集群、哪个分组"。网关掌握完整的请求上下文,Header、URL、租户标识、地域标签都能作为路由依据,规则通过配置中心下发,秒级在全集群生效。站点级切流之外,更常用的是细粒度止损:某个下游集群异常时,把命中它的请求路由到备用集群或直接降级返回。
服务层解决的是"调哪个实例"。RPC 框架 的路由规则、负载均衡策略、泳道(swimlane)隔离都在这一层生效,粒度可以到单个实例、单个接口,生效速度是毫秒到秒级。灰度发布、A/B 测试、故障时摘除异常实例,都是服务层调度的日常。
数据层最特殊,它的"调度"本质是数据归属问题:哪个单元拥有哪部分数据的写权限。数据层的切换涉及主从切换、位点追平、脑裂防护,耗时分钟级,且必须提前有预案,不能在故障发生时临时决策。数据层的调度不是流量的调度,而是流量调度必须服从的边界:流量可以被调度到任何有容量的地方,但只能写数据归属允许的地方。
四层里面,接入层值得多说几句。站点级是影响面最大的故障类型,而接入层正是它的主战场。接入层动态调度有两条主流路线。
一条是 GSLB 路线:自己运维一套全局负载均衡系统,用分布在全国各地的探测节点持续探测各站点的接入质量,解析结果根据探测结果和容量水位动态生成。它的优势是控制力强,可以按地域、运营商、容量做精细调度;代价是整套探测和解析体系都要自建自维护,而且它本质上还是在和各级 DNS 缓存打交道,生效速度受 TTL 约束,只能做到十秒到分钟级。
另一条是 Anycast 路线:把同一个 IP 地址在多个站点同时通过 BGP 宣告,用户的请求由网络层自动路由到"最近"的站点;某个站点故障时,撤销它的 BGP 路由,网络收敛后流量自动流向其他站点。它的优势是生效快(BGP 收敛通常在秒级到几十秒)且完全不依赖 DNS 缓存;代价是调度粒度受网络拓扑限制,撤销路由时可能出现部分用户的流量绕路到物理上很远的站点,带来延迟上升。
实践中成熟的接入层往往是两条路线的组合:Anycast 做站点级的快速兜底,保证"站点没了流量也有地方去";GSLB 做精细化的常态调度,保证"流量去的是最合适的站点,而不只是最近的站点"。无论哪条路线,背后都依赖同一套东西:多点分布的健康探测体系。探测节点要覆盖主要运营商和地域,探测信号要能区分"站点真的故障"和"探测链路自己有问题",否则调度决策会建立在失真的输入上。
四层手段叠加起来,才构成一个完整的动态调度体系。单靠任何一层都有盲区:只做接入层,切不动细粒度流量;只做服务层,站点级灾难时连入口都进不来;只做数据层,流量根本到不了正确的地方。
单元化:让调度有了"集装箱"
分层调度解决了"能不能调"的问题,单元化解决的是"按什么单位调"的问题。
前面故障域设计那一讲提到过,单元(cell)是一个流量、服务、数据都闭环的独立部署单位。站在流量调度的视角,单元的意义在于:它把离散的流量变成了可以被整体搬运的标准集装箱。
没有单元化的系统,流量调度只能在域名、接口、实例这些零散维度上操作,切流量的时候要逐个确认每个服务的下游依赖在哪里、数据写到哪里,一次站点级切换本质上是一次全站范围的重新布线。单元化之后,调度单位变成了单元:单元内的流量闭环,不依赖其他单元的服务和数据;调度系统只需要回答一个问题——这个单元整体放在哪个站点。
单元路由的关键是分片键(shard key)。以用户 ID 为例,路由规则是:

路由规则表是单元化调度的核心资产,它描述的是"分片键到单元、单元到站点"的两级映射。平时这张表是稳态的,故障时调度的本质就是改这张表:把故障站点上的单元整体映射到健康站点。因为单元是闭环的,改一张表、推送一次规则,对应的流量就整体迁移了,不需要逐个服务确认。
这里有两个容易混淆的概念值得说清楚。
一个是流量染色(traffic coloring)。染色是给请求打标签,标记它属于哪个泳道、哪个灰度批次、哪次演练,让链路上的每一跳都能识别并一致处理。染色解决的是"识别",路由规则解决的是"去向",两者配合才能实现"把特定的一批流量精确地送到特定的地方"。容灾演练时,演练流量必须全程染色,防止演练流量混入真实业务,也防止真实业务误入演练环境。
另一个是全局服务与单元服务的区别。单元化的系统里仍然存在必须全局部署的服务:账号登录、全局配置、风控名单这类数据天然全局的资源。它们没法被装进单元的集装箱,调度策略也完全不同:全局服务靠多站点多活和就近接入来保证可用,单元服务靠改路由表来整体迁移。混淆这两类服务的调度方式,是单元化系统里经典的故障来源。
路由规则表本身也需要被当成一个核心系统来治理,不少团队是在吃过大亏之后才意识到这一点的。规则表的每次变更都直接改变百万级请求的去向,它的安全级别应该等同于数据库 DDL:变更要有版本号、要能审计谁在什么时间为什么改了哪条规则、要支持一键回滚到任意历史版本、下发前要在影子环境做规则推演验证不会把流量导进黑洞。规则的推送链路也要有保底:正常情况下走配置中心秒级下发,配置中心异常时要有备用的拉取通道,确保"改不动路由表"这种场景本身也有预案。调度系统的每一个部件都要回答同一个问题:你坏了的时候,流量调度这件事还能不能做成——这是动态调度体系特有的递归难题。
单元化给流量调度带来的本质变化,是把"一次故障要不要切"的问题,降维成了"切哪几个单元、每个单元切多少"的选择题。选择题可以小步做、可以验证、可以回退。
调度决策闭环:从人肉审批到自动执行
有了调度能力,下一个问题是:谁来按开关?
早期答案是人。故障发生时,值班同学看监控、拉群、层层确认、手动执行切换。这个模式的问题在 8.1 已经算过账:人工决策链路平均 10 到 30 分钟,而千万 QPS 系统的故障损失按秒计算。但完全交给机器同样危险:自动切换的误判会制造本来不存在的故障,把流量从健康的站点切走,这种"自愈"比故障本身更致命。
成熟的做法是把决策闭环拆成四级,按风险等级分配人和机器的职责:

实例级完全自动:负载均衡 的健康检查摘除异常实例,秒级完成,误判的代价是少一个实例的容量,可以自动重试兜底。集群级自动加审批:规则变更由系统生成,人工一键确认,误判代价开始变大。单元级预案化:每个单元的迁移路径、容量校验、数据校验都提前写成预案,故障时一键执行,执行过程仍然可以暂停和回退。站点级保留人工决策,但执行是一键的:因为站点级切换影响面太大,且往往伴随数据层切换,需要人综合判断,但"判断"和"执行"必须分离,人只做判断,执行由调度系统完成,避免手动操作引入差错。
自动决策要立得住,靠的是三个工程约束。第一是判据冗余:单一探测源不可信,自动切换的触发条件必须来自多个独立信号源,例如机房内探测、跨机房探测、运营商拨测三者一致才认定站点故障。第二是切换预算:给自动切换设定额度,例如一小时内每个单元最多自动迁移一次,防止系统在边界条件下反复横跳。第三是灰度验证:即便判定要切,也先切 5% 的流量过去观察核心指标,确认目标站点真的能接住,再放量。这三个约束分别防的是误判、抖动和雪崩。
误判的代价是不对称的,这一点值得掰开看。漏判(该切没切)的损失是故障持续期间的业务损失,随时间线性增长;误判(不该切却切了)的损失是切换动作本身带来的一次性冲击,加上可能引发的二次故障。前者是确定的、可控速的,后者则往往是突发的,还可能被放大。这决定了自动切换宁可保守:把触发阈值调得更高、把确认窗口拉得更长,用"切得慢一点"换"几乎不误切"。真正需要快的那部分,交给影响面小、误判代价低的实例级和集群级自动策略去覆盖。
切换完成后还有一个容易被省略的环节:效果验证。流量分布到达目标比例,不等于切换成功,还要确认三件事:目标站点的核心业务指标(成功率、延迟、错误率)在承接新流量后保持稳定;被切走的故障流量确实没有残留在原路径上;全链路没有因为路由变化产生意料之外的跨站点调用。这三项都通过,这次切换才能被标记为完成,否则它只是一个"进行中的动作"。
把这套流程承载起来的,通常是一个专门的调度平台,而不是散落在各处的脚本和配置后台。平台至少要做三件事:把所有预案结构化,每条预案写清楚触发条件、执行步骤、容量校验项和回退方式,故障时不需要临场拼凑;把执行过程白屏化,每一步执行到哪、生效了多少台、指标变化曲线,值班同学一屏看清,暂停和回退都是按钮而不是命令;把决策痕迹留档,谁在什么时间基于什么判据触发了哪条预案,事后复盘有据可查。平台化的本质,是把切换从依赖个人经验的临场发挥,变成依赖系统能力的标准动作。
切换的二次灾害:流量潮汐与踩踏
调度体系建好之后,最危险的敌人从"切不动"变成了"切太快"。切换本身是会制造故障的,这类故障有个共同的名字:二次灾害。
第一种是流量潮汐。一个千万 QPS 的站点承接另一个站点切过来的流量,意味着瞬时翻倍。如果目标站点的容量是按 1.5 倍冗余设计的,剩下的 0.5 倍缺口会在几分钟内被击穿:CPU 打满、缓存命中率骤降、连接池耗尽,然后新切过来的流量和原有流量一起崩。对策是分批迁移加容量预校验:调度系统在执行前必须回答"目标站点当前水位多少、切完以后水位多少、超过红线怎么办",答案是超红线就拒绝执行或者只切一部分。
第二种是冷启动踩踏。目标站点的实例可能长期处于低流量状态,JVM 没热、缓存是空的、连接池没建好,骤然承接全量流量时的真实处理能力远低于设计值。对策是预热和慢启动:切流前先给目标单元灌入少量流量做预热,让缓存加载、JIT 编译、连接建立先完成;切流时按比例爬坡,例如先 5% 观察十分钟,再 20%、50%,最后 100%,每一档都要核心指标达标才放下一档。

第三种是调度系统的脑裂。多站点的调度系统如果各自为政,可能出现两个站点都认为自己是调度主体、下发互相冲突的规则的情况。对策是控制面自身的容灾设计:调度控制面必须跨站点部署且有明确的选主机制,同时在"控制面整体不可用"的极端场景下,数据面要能靠本地缓存的规则继续运行,控制面失效的正确姿态是"冻结现状",而不是"各自决断"。
第四种是抖动与横跳。网络探测信号在边界状态下会来回震荡,如果调度策略没有迟滞设计,流量会被反复搬运,每次搬运都有成本也都有风险。对策是滞回区间加冷却时间:触发切换的阈值和恢复的阈值不设为同一个值,例如水位超过 80% 触发迁出、降到 60% 以下才允许迁回,且两次操作之间强制冷却。

评估一个调度体系的成熟度,不是看它切得多快,而是看它敢不敢小步切、能不能随时停、出问题时能不能一键退回来。快是结果,可控才是能力。
演练:把预案变成肌肉记忆
调度规则的复杂度决定了它不可能在第一次真实故障时才被检验。规则表里有几百条映射、预案有几十个分支、自动策略有十几个触发条件,这些东西里藏着多少错误,只有演练能暴露。
容灾调度的演练分三个层次。第一层是桌面推演:把故障场景写在纸上,让值班同学按预案走一遍决策流程,检验的是预案的可读性和决策链路的顺畅度,成本最低,适合高频进行。第二层是真实切流演练:在真实环境里把真实流量切走一部分,检验调度系统的实际生效速度、目标站点的真实承接能力、监控指标的联动是否正常。这一层必须在低峰期做,且演练流量全程染色,随时可回退。第三层是突袭式演练:不提前通知具体时间,由专门的团队注入故障,检验的是从检测、决策到调度执行的全链路真实反应时间,以及值班体系的响应能力。
演练要有明确的度量指标,否则就只是走形式。调度体系至少要量化这几项:切换决策耗时(从故障确认到下发指令)、规则全量生效耗时(从指令下发到所有数据面确认)、流量实际迁移耗时(从规则生效到流量分布达到目标比例)、切换全程的业务损失(错误率积分和成功率跌落时长)。这四项拆开统计,才能定位瓶颈在哪一环。
这些指标还应该反过来成为容量规划的输入。如果演练发现目标站点承接切流后水位逼近红线,那暴露的不是调度问题,而是冗余容量不足;如果规则下发很快但流量迁移很慢,瓶颈在接入层的生效链路而不是决策环节。演练度量的真正价值,是把"容灾能力"从一个模糊的形容词,变成一组可以写进架构评审、可以逐年对比的数字。
演练还有一个很少被写进报告的副产品:它持续校准着团队的"切换信心"。一个每季度都真实切过流量的团队,在真实故障里按下切换键时的犹豫时间,和一个只在文档里见过切换流程的团队,完全是两个量级。
演进的刻度:从 DNS 到单元化调度
把这条演进路径串起来,每一步都在用复杂度换切换的确定性和精细度:

路径是有先后顺序的。GSLB 是性价比最高的第一步,它不需要改动业务架构,把 DNS 这一层从静态改造成动态,就能把站点级切换的生效时间从十几分钟压到分钟级以内。网关和服务层的规则化调度是第二步,它让细粒度止损成为可能,这一步依赖配置中心的推送能力和全链路的标签体系。单元化调度是第三步,也是最贵的一步,它要求业务先完成数据归属的梳理和单元化改造,调度只是把改造的成果变成可执行的能力。
这里要泼一盆冷水:如果系统还停在单体架构、数据没有分片、服务之间网状依赖,直接上单元化调度是空中楼阁。调度能力的上限不取决于调度系统本身有多强,而取决于被调度的对象有多规整。先把流量变成可以整体搬运的集装箱,再去造龙门吊,顺序不能反。
还有一点容易被架构师忽略:调度能力和容量规划是一对锁死的变量。调度敢切多少流量,取决于目标站点有多少冗余;冗余按几倍设计,又取决于调度能多快把流量搬走。常见的错误是容量团队按"每站点独立扛全量"设计冗余,调度团队按"随时可以切一半流量"设计预案,两边各自成立,合起来却因为冗余只有 1.5 倍而切不动。调度的上限是容量的下限,容量规划必须和切流预案坐在同一张桌子上算账。
把这一讲放回容灾设计的坐标系里看。同城容灾解决的是"近距离的冗余怎么建",异地容灾解决的是"远距离的冗余怎么建",故障域设计解决的是"冗余按什么结构组织",而流量调度解决的是最后一公里的兑现问题:冗余已经在那里了,怎么在需要的时候,把流量准确地送过去。前面三讲决定的是容灾体系的上限,这一讲决定的是它能兑现多少。建了双活却切不动流量,等于买了保险却找不到理赔电话。
回到开篇那次演练。后来那个团队的改进路径很有代表性:第一步把接入层从静态 DNS 换成动态 GSLB 加 Anycast 兜底,站点级切换的生效时间从不可控的长尾压到了两分钟以内;第二步在网关层建了规则化的路由能力,支持按比例、按租户切流,切换从全量广播变成了可以分批验证的动作;第三步配合单元化改造,把调度单位收敛到单元,切换预案从十几页操作手册变成了调度平台上的一键执行。一年后的演练里,同样的站点级切换场景,从决策确认到流量完成迁移,全程 8 分钟,且中途做过一次 5% 的灰度验证和一次主动暂停。
流量调度从静态走到动态,表面上是技术栈的升级,实质上是容灾从"靠资产"到"靠能力"的转变。冗余容量是资产,躺在机房里不会自己变成安全;调度能力是把资产兑现成安全的那只手。这只手有多稳、多快、多可控,决定了故障来临时,你的容灾体系是一份真实的保险,还是一张好看的图纸。
你的系统现在做一次站点级切流,需要多长时间、几级审批、几次人工操作?这几个数字,就是当前调度能力最诚实的度量。关于容灾调度与架构演进的更多实践,欢迎到 云栈社区 和技术同行一起交流。