我曾共事过的一个团队,为满足合规要求,将公网入口负载均衡器升级到了 TLS 1.3。整个上线过程没有出现任何异常信号:握手正常完成,服务保持健康,一切指标也都平稳。
在撰写本文时,Route 53 HTTPS 健康检查要求目标端点必须支持 TLS 1.2。如果禁用了 TLS 1.2 且仅保留 TLS 1.3,健康检查器将无法完成握手,并将该端点标记为不健康。CDN 因此判定该区域不健康,并停止将流量路由到该区域。
在该区域内部,一切看起来都很正常。服务在线,负载均衡器状态健康,应用程序持续处理请求,仪表盘没有显示任何异常。唯一可见的症状是流量不再流入。
用户流量被透明地重新路由到了数千公里之外的另一个区域。受影响地区的延迟急剧上升。故障转移区域开始扩容,以便应对未曾预估的负载。合成监控最终发现了内部遥测系统无法捕捉到的异常模式。实际问题大约花了四十分钟才定位到。故障并非发生在应用数据平面,而是发生在决定流量去向的控制平面。
系统具备高可用性,但并不具备弹性。
这一区别的重要性远超大多数云架构所认知的程度。在实践中,这两个术语经常被互换使用,即使在工程文化很强的团队中也是如此。然而,它们描述的是不同的问题,有着不同的前提条件。高可用性是指以最小的中断来应对预期故障,而弹性则是指系统能从当初设计时并未明确预料到的异常场景中恢复。造成概念混淆的部分原因在于,可用性可以用正常运行时间百分比、故障转移时间和复制延迟等指标来衡量。弹性则很难用这种方式来衡量。许多故障模式只有在真实压力下才会暴露。有意义的恢复测试往往过于昂贵或风险过高,难以刻意重现。
可用性与弹性不是同一个问题
现代云平台让高可用的实现变得相对容易。一个具备多可用区故障转移的托管数据库、负载均衡器后方的自动扩缩容组,以及前置的 CDN,就能帮助大多数系统实现可观的正常运行时间。当故障局限在架构预设的场景内时——例如实例崩溃、可用区故障或依赖服务超时——这些标准方案效果很好。
问题在于,真实的事故很少完全符合这些假设。高可用性工程假设故障是孤立的且可预测的,而工程的出发点恰恰相反:最终某些关键组件会以没人建模过的方式发生故障。仅靠冗余无法抵御故障。一条从未演练过的故障切换路径,算不上恢复策略,它只是一个假设。下面三种典型场景直观体现了二者的差别。
跨冗余实例的关联故障
现代高可用性确实考虑到了一部分关联性故障,比如多可用区,它明确建模了整个可用区同时宕机的场景。但它没有考虑软件层面的关联性,例如向所有副本同时推送错误配置、所有读副本返回脏缓存记录、依赖库升级后悄悄破坏整个系统依赖的契约。一旦组件之间的独立性在这个层面被打破,冗余就不再是安全保障。
并非真正优雅的优雅降级
大多数系统都假设可以优雅降级,但很少在真实负载下测试过。当读副本的延迟在压力下持续增加时,缓存层是分担压力还是把压力放大压垮主库?没有经过演练前,优雅降级只是猜想,而非保证。
逐渐失效的恢复路径
系统已经运行了两年,在编写重建手册时,当前一半的依赖项还不存在。IAM 策略已经发生漂移,部署工具已经发生变更。恢复路径和其他代码路径一样,长期不用,就会腐化失效。
组织层面的问题在事故发生前早已存在
弹性问题既是架构问题,也是组织问题。架构问题更容易获得关注。画图表比落实运维责任容易得多。
我观察到一个反复出现的问题:即使可用性的归属责任很明确,恢复工作的归属权也往往模糊不清。大多数组织对可用性的归属很明确,比如值班轮岗、SLO 和升级渠道,但很少有组织为弹性建立同等的结构。没有指定的恢复负责人,没有定期的测试演练,也没有在基础设施演进过程中保持手册更新的流程。这个差别很少是刻意的决策,而是无人认领责任后自然形成的默认状态。
让这个差距持续存在的部分原因是弹性的建设成本十分高昂。测试真正关键的场景需要专门的工程时间,承担运维风险,以及一套只为大概率不会触发的灾难准备的基础设施。结果就会走入表演式弹性:架构图看起来是对的,备用区域存在,手册也存在,但实际的恢复能力只是假设,没有经过验证。这不是因为工程师粗心,而是因为严格验证需要真金白银、承担真实风险,且其价值只会在最不利的时刻体现。
在实践中,重大事故的前三十分钟往往浪费在梳理依赖关系上。员工发现权限已经漂移,手册引用的工具一年前就已退役,没人确定还有谁了解备用的环境。这些问题并不特殊,只是常规运维损耗。恢复路径因为极少演练而慢慢失效。
解决方案并不花哨,需要独立于值班体系的恢复负责人。值班负责事故响应,恢复负责人负责事前准备,包含流程、工具,以及保持两者持续更新的责任。如果把二者混为一谈,你会得到优秀的事故指挥官和十八个月没人打开过的操作手册。
恢复能力强的团队将故障转移演练视为一项持续的工程任务。他们从小处着手,先针对单个服务和单个可用区切换。每次演练都会发现一些问题,比如被撤销的权限、过时的手册或静默的告警。你通过执行流程来发现这些问题。这种转变是将恢复视为需要持续维护的东西,而不是架构图上默认自带的属性。
关于恢复能力的五个问题
想要开始测试弹性,不必重新设计整套架构,可以从以下五个问题开始:
-
最先失效的是什么?识别恢复路径依赖的控制平面组件:DNS、身份认证、路由、证书、配置、编排系统或是第三方服务。
-
团队实际会观测到什么?针对每个故障场景定义异常信号。如果故障发生在请求抵达应用之前,应用面板显示正常绿色是不足以发现问题的。
-
你能执行恢复流程吗?测试真实的执行机制,而不是只看图评审。如果故障切换依赖 DNS 变更、路由决策、控制平面 API,就要在贴近真实的环境下演练这条路径。
-
谁负责恢复?值班工程师负责事故响应,但需要专人持续维护恢复机制、操作手册、依赖组件与相关测试。
-
上次测试之后发生了哪些变更?记录故障点、意外现象、哪些假设被证明是错误的。把这些发现转化为工程任务,而不是仅仅留在事故笔记里,弹性才能得到改善。
目标不是测试所有可能的故障,而是定期挑战支撑恢复流程的各项假设,并将验证结果落地为系统变更。
架构图上不会画出的控制平面陷阱
大多数关于云故障转移的讨论集中在数据平面,即流量去向、哪个区域处理请求、实例宕机时会发生什么。而控制平面——决定流量应该流向何处的系统——得到的关注远少得多。上述的 TLS 1.3 事故正是这种差距直接导致的。
数据平面运行完全正常,控制平面却出了问题。
你无法从服务内部看到这一切。服务和负载均衡器都是健康的,但流量就是没有到达。合成监控显示了一种内部遥测无法捕获的差异,因为内部遥测只观察数据平面。故障只能从外部通过本身也是控制平面一部分的监控基础设施看到。
这就是控制平面依赖带来的结构性问题。正常运行时,它们是隐形的。大家不会把 CDN 健康检查看作服务的一部分,只会当成监控。但是,控制流量准入的监控属于业务链路的一环,哪怕它存在于控制平面中。一旦它发生故障,数据平面完好,服务却无法访问。
这种模式在云服务的各个领域反复出现。AWS STS 就是一个明显的例子。全局端点 sts.amazonaws.com “托管在单个 AWS 区域——美国东部(弗吉尼亚北部)。与其他端点一样,它不会自动故障转移到其他区域的端点”,AWS 文档明确指出了这一点。许多表面上的全局服务都带有区域依赖,而这不会出现在标准架构图中。如果该区域降级,全局服务可能发生完全绕过区域冗余机制的故障。
这指向了一个更广泛的问题:很多多区域架构仅仅在数据平面实现了多区域。它们的恢复假设仍然依赖少量共享的控制平面组件,而且这些依赖通常要等到故障发生时才会补充到架构图上。
这种分层设计是有原因的。数据平面的设计目标是在控制平面受损时继续工作。即使 EC2 控制平面降级,Amazon EC2 实例仍在运行。即使 Lambda 控制平面停滞,进行中的 AWS Lambda 调用也会完成。这很好。但当你在控制平面上构建故障转移时——例如通过 Route 53 健康检查实现基于 DNS 的故障转移——你就继承了这些故障模式,无论是否有意为之。故障转移只会在决定故障转移的系统正常工作时才有效。
因此,在设计故障转移时值得问的问题不只是“如果一个区域失效会怎样?”,而是“如果检测区域失效的组件自己降级了会怎样?”这会揭示出那些原本不可见的依赖,避免等到 TLS 版本升级、配置偏移这类事件发生才在事故中痛苦地发现问题。
事故之后,两处告警缺口被识别并补齐:CDN 把整个区域流量切走时没有触发告警;负载均衡入站流量归零,同样静默无告警。这两个指标全程都没有告警。本次故障是上线期间工程师人工查看指标才发现。后续还发现:第一批升级后的负载均衡,CDN 健康检查已经静默失败一周,但其余仍使用 TLS1.2 的目标节点流量正常,所以无人察觉。
ARC 与基于 DNS 的故障转移:客观对比
如果控制平面依赖是已知风险,就要考虑应对方案。大多数团队评估的两种方案:AWS 应用恢复控制器(ARC),以及基于 Route53 健康检查的传统 DNS 故障转移。二者看起来相似,但实际上并非如此。
基于 DNS 的故障转移是成熟的通用方案。Route 53 探测每个端点,将其标记为健康或不健康,并相应地更新 DNS 记录。它简单、普及、成本低。但如果你的组织关注的是分钟级别的恢复目标,DNS 故障转移很快就很难满足要求。DNS 传播延迟(即 TTL、递归解析器和客户端强缓存)都会成为恢复窗口的一部分,无论你是否为此做了规划。再加上上述控制平面依赖,情况就更糟了。如果 Route 53 的健康检查基础设施降级,故障转移要么不会触发,要么会错误触发。
ARC 采用了不同的方法。它不依赖 DNS,而是提供路由控制,由你在决定故障转移时显式切换,并通过持续的准备状态检查让你在切换之前就知道故障转移目标是否真的能接受负载。路由控制平面是一个由五个区域端点组成的集群,设计目标是在区域受损期间保持可用。ARC 并没有消除复杂性,而是将复杂性转移到一个更明确、更专用的运维模型中。
-
速度
DNS 受限于健康检查间隔、传播延迟和客户端 TTL,实际耗时以分钟计,有时甚至更长。ARC 以秒为单位运行。如果你的 RTO 以秒计,DNS 就不是一个可接受的选择。
-
故障模式
DNS 故障转移依赖 Route 53 健康检查基础设施的准确性;TLS 1.3 事故正是这种依赖失误的典型案例。ARC 的五个区域端点集群意味着路由切换不需要任意单个控制平面保持健康。
-
运维复杂性
DNS 是所有工程师都熟悉的东西。ARC 的运维负担更重。路由控制和准备状态检查需要专门的负责人。手册需要保持更新。此外,值班人员需要知道如何在压力下执行切换,而不是在事故中才发现手册有误。这类场景会带来实实在在的运维开销。
-
成本
DNS 实际上是免费的。ARC 按集群和控制项收费;对于单个关键服务来说,这些费用并不昂贵,但会累加上升。
-
故障转移前的信心
ARC 的准备状态检查持续验证目标是否可以接受流量。而如果使用 DNS,你要到故障转移发生时才会知道。
结合每种工具的优势,采用分层方法效果最好:对少数面向客户的关键路径使用 ARC,这类场景无法接受控制平面依赖风险;其余场景使用 DNS。支付流程、身份认证和核心预订路径适合使用 ARC,内部工具和低流量 API 则不需要。
恢复是一种能力,而不是一种属性
恢复能力不是在事故发生时才去发现的东西。到发生事故那一刻,恢复能力早在数月前就已经建好或是被忽视了。
构建可恢复系统的难点不只是组织层面的:弹性无法被完全证明。高可用性有明确基准,包括正常运行时间、冗余、故障转移时间和复制延迟。弹性具有对抗性:关联性故障、过时的手册、控制平面耦合以及压力下的人类协调——这些问题中的大部分只有在条件过于昂贵或风险过高而无法可靠重现的情况下才会暴露。在生产负载下进行完整的区域故障转移,以及在真实用户等待的情况下从损坏的主库中恢复,这些场景不是大多数团队可以在不承担重大运维风险的情况下常规演练的。这就是为什么表演式弹性如此普遍,不是因为团队粗心,而是因为真正去验证弹性的门槛确实很高。
务实的目标不是保证一定能成功恢复。在足够复杂的系统里,弹性是概率性的,无法被彻底证明。现实目标是降低未知风险、演练协同流程、缩小故障爆炸半径、缩短发生故障到被发现的间隔,以此来提升信心。架构图画起来具备弹性,只是因为组件做了冗余。但弹性不由正常状态下的架构图决定,它取决于在压力、可见度不足、信息不全、依赖项表现不符合预期的情况下,人是否真的能恢复系统。
大多数组织最终会发现,那些从未演练过的恢复程序是最先失效的。这个差距在架构中是看不到的。它会在恢复路径第一次必须真正被使用的那一刻显现出来。
查看英文原文:https://www.infoq.com/articles/high-availability-not-resilience-cloud/