高可用不等于有韧性:一次 TLS 升级如何让健康区域“消失”

2026-10-01 19 预计阅读时间: 1 分钟
来源: infoq.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:14 分钟

一次常规的 TLS 1.3 升级,可能不会让应用进程崩溃,也不会触发内部服务告警,却足以让外部流量彻底绕开一个健康区域。

问题出在系统边缘:Route 53 健康检查无法再正确探测目标,CDN 因此停止向该区域路由流量。与此同时,区域内的实例、负载均衡器和业务仪表盘仍然显示正常。组件都“活着”,用户却无法通过预期路径访问它们。

这正是高可用与韧性的区别:高可用关注冗余是否存在,韧性则关注系统遇到意外失效时,能否发现、理解并恢复服务。

一个区域健康,为什么仍然接不到流量

典型的多区域架构会包含几层不同的判断:

  1. 应用判断自身是否健康;
  2. 负载均衡器判断后端实例是否可用;
  3. DNS 或全局流量管理器判断区域是否可用;
  4. CDN 根据源站状态和路由策略决定把请求送到哪里;
  5. 客户端最终通过 DNS、TLS 和 HTTP 完成访问。

内部仪表盘通常覆盖前两层。事故却可能发生在后三层。

TLS 升级尤其容易暴露这种错位。应用团队看到的是:

  • 服务进程正常;
  • CPU、内存和错误率正常;
  • 区域负载均衡器仍能处理请求;
  • 内部探针返回 200。

但外部健康检查关心的是另一份契约:它能否解析域名、建立 TCP 连接、协商双方都支持的 TLS 版本和密码套件、验证证书,并在规定时间内获得预期的 HTTP 响应。任何一步不兼容,控制平面都可能把健康区域标记为不可用。

因此,“应用健康”不能推出“用户路径健康”。真正需要验证的是完整链路:

客户端 -> DNS -> CDN -> 全局路由/健康检查 -> 区域入口 -> 应用

如果监控只从区域内部访问应用,它就绕过了最可能出问题的 DNS、CDN、证书和外部 TLS 协商环节。

高可用解决冗余,韧性解决失控

高可用设计通常可以回答这些问题:

  • 是否有多个实例?
  • 是否跨可用区部署?
  • 是否存在第二个区域?
  • 数据是否有副本?
  • 流量能否自动切换?

这些能力很重要,但它们隐含了一个前提:负责切换的控制平面必须正确理解系统状态。

如果健康检查错误地判定一个区域失效,自动切流反而会扩大故障。备用区域即使存在,也可能因为容量不足、配置漂移或同类探针问题而无法接管。此时系统拥有冗余,却没有可靠的恢复路径。

韧性需要回答另一组问题:

  • 谁能发现控制平面的错误判断?
  • 能否区分“应用不可用”和“探针不兼容”?
  • 自动切换是否有容量和持续时间边界?
  • 如何安全地覆盖错误路由决策?
  • 上一次人工演练是什么时候?
  • 哪个团队对恢复动作负最终责任?

换句话说,HA 是架构属性,韧性是架构、观测、操作和组织责任共同形成的能力。

控制平面为什么容易成为盲区

控制平面通常不承载业务请求,却决定业务请求去哪里。DNS 健康检查、CDN 源站选择、证书自动化、服务发现和流量策略都属于这一类。

它们有三个常见风险。

1. 监控对象与决策对象不同

团队监控的是应用实例,但 CDN 依据 DNS 健康状态做决策。应用指标全绿,并不能解释 CDN 为什么停止发送请求。

至少应同时观察:

  • 区域应用的实际健康状态;
  • Route 53 等外部健康检查的状态;
  • CDN 到各源站的请求量和失败率;
  • 每个区域接收的流量比例;
  • 公网 DNS 解析结果;
  • 从公网建立 TLS 会话的结果。

一个很有价值的告警不是“服务错误率升高”,而是状态矛盾:

区域应用健康 = true
且
全局路由认为区域健康 = false

这种告警直接指向控制平面与数据平面之间的分歧。

2. 低频变更缺少端到端验证

TLS、证书、DNS 和 CDN 策略的变更频率通常低于应用发布,因此更容易被当成基础设施细节。实际上,它们会改变外部探针和客户端能否完成连接。

TLS 升级验收不能只运行一次 curl https://internal-endpoint/healthz。还应验证:

  • 健康检查器支持的 TLS 能力;
  • 证书链与主机名;
  • SNI 行为;
  • 公网入口和直接区域入口;
  • IPv4 与 IPv6 路径;
  • CDN 使用的源站协议策略;
  • 变更后的真实流量分布。

3. 恢复能力会自然衰减

故障切换脚本可能仍在仓库里,但权限已经变化;文档中的联系人已经离职;备用区域的配额跟不上当前流量;手工覆盖命令从未在生产条件下验证。

恢复能力不是一次性交付物。没有明确负责人、定期演练和可验证指标,它就会随着系统变化逐渐失效。

可以这样实践:从外部验证 TLS 与区域入口

下面的脚本使用 Python 标准库,对多个区域域名分别尝试 TLS 1.2 和 TLS 1.3,然后请求 /healthz。它不等同于 Route 53 的实际探针,但适合作为独立合成监控或变更前检查,用于发现协议兼容性和证书问题。

运行前,把 HOSTS 改成可以从公网访问的区域入口。脚本假设健康接口允许 HEAD 请求,并且 TLS 1.2 与 TLS 1.3 都是当前兼容性要求;如果你的安全策略不同,请调整 versions 列表。

#!/usr/bin/env python3
import os
import socket
import ssl
import sys

hosts = [
    item.strip()
    for item in os.environ.get(
        "HOSTS",
        "origin-us.example.com,origin-eu.example.com",
    ).split(",")
    if item.strip()
]

path = os.environ.get("HEALTH_PATH", "/healthz")
timeout = float(os.environ.get("TIMEOUT_SECONDS", "5"))
versions = [ssl.TLSVersion.TLSv1_2, ssl.TLSVersion.TLSv1_3]
failures = []

for host in hosts:
    for version in versions:
        label = version.name
        try:
            context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
            context.load_default_certs()
            context.minimum_version = version
            context.maximum_version = version

            with socket.create_connection((host, 443), timeout=timeout) as tcp:
                with context.wrap_socket(tcp, server_hostname=host) as tls:
                    request = (
                        f"HEAD {path} HTTP/1.1\r\n"
                        f"Host: {host}\r\n"
                        "Connection: close\r\n\r\n"
                    )
                    tls.sendall(request.encode("ascii"))
                    status_line = tls.recv(4096).split(b"\r\n", 1)[0]
                    status = status_line.decode("ascii", errors="replace")

                    if " 200 " not in status:
                        raise RuntimeError(f"unexpected response: {status}")

                    print(
                        f"OK host={host} requested={label} "
                        f"negotiated={tls.version()} status={status}"
                    )
        except Exception as exc:
            message = f"FAIL host={host} tls={label} error={exc}"
            failures.append(message)
            print(message, file=sys.stderr)

if failures:
    sys.exit(1)

保存为 check_tls_health.py 后运行:

chmod +x check_tls_health.py
HOSTS="origin-us.example.com,origin-eu.example.com" \
HEALTH_PATH="/healthz" \
./check_tls_health.py

更重要的是,不要只在部署机器或云内网运行它。可以从独立云账号、外部监控服务或至少一个不共享生产网络路径的位置执行。否则,探针仍可能绕过公网 DNS、CDN 或边缘 TLS 配置。

如果使用 Route 53 健康检查,还可以把控制平面状态纳入排障脚本:

export HEALTH_CHECK_ID="replace-with-health-check-id"

aws route53 get-health-check \
  --health-check-id "$HEALTH_CHECK_ID"

aws route53 get-health-check-status \
  --health-check-id "$HEALTH_CHECK_ID"

这里要检查的不只是 HEALTHY 或 UNHEALTHY,还要核对探测端口、协议、路径、域名以及各检查节点返回的信息是否符合当前 TLS 和入口配置。

为 TLS 和路由变更增加恢复护栏

一次更稳妥的变更流程可以包含以下步骤:

  1. 记录兼容性契约:明确健康检查器、CDN、源站和客户端必须支持哪些 TLS 版本,而不是默认“升级后都会工作”。
  2. 同时测试区域入口和用户入口:区域入口证明应用可达,用户入口证明 DNS、CDN、TLS 与路由链路整体可达。
  3. 观察流量分布:变更后检查每个区域的请求量。某个区域错误率为零,可能只是因为它已经没有流量。
  4. 设置状态矛盾告警:当区域内部健康但外部探针失败,或 CDN 流量突然归零时立即告警。
  5. 准备可逆变更:保留经过验证的 TLS 配置回滚方式,而不是在事故中临时研究旧配置。
  6. 演练人工覆盖:确认值班人员知道如何暂停自动切换、恢复某个区域或调整流量权重。
  7. 限制覆盖时长:人工强制路由应有审批、到期时间和后续复盘,避免临时措施成为永久风险。

不应把“永久允许旧协议”当作韧性方案。兼容窗口可以帮助回滚和迁移,但最终仍应根据安全要求收敛。关键是先确认健康检查、CDN 和客户端都已具备新协议能力,再移除旧协议。

上线前应问的几个问题

在批准多区域、TLS、DNS 或 CDN 变更前,可以用这份简短清单做最后检查:

  • 是否有从公网执行的端到端探针?
  • 探针是否使用与真实用户相同的域名、SNI 和证书路径?
  • 是否单独监控每个区域的流量,而不只是错误率?
  • 能否看到全局健康检查的原始失败原因?
  • 备用区域是否经过容量验证?
  • 自动切流错误时,谁有权执行人工覆盖?
  • 回滚命令是否在近期演练过?
  • 谁对恢复时间和恢复流程负责?

多区域、自动故障转移和健康检查只能提供构建韧性的材料。真正的韧性来自端到端观测、可逆变更、定期演练和清晰所有权。系统最危险的状态并不总是全面宕机,而是每个局部仪表盘都显示正常,唯独用户到服务之间的路径已经断开。


相关推荐