找回密码
 立即注册
首页 业界区 业界 Cloudflare 2025-11-18 宕机事件分析与 “白嫖玩家” 的 ...

Cloudflare 2025-11-18 宕机事件分析与 “白嫖玩家” 的灾备方案设计

坪钗 2025-11-20 17:15:00
1. 故障分析(北京时间时间线)

Cloudflare 官方报告里所有时间都是 UTC,这里先统一换算成北京时间(UTC+8),再结合“普通用户视角”做一遍还原。

  • 报告时间换算:

    • 11:05 UTC → 19:05 北京时间
    • 11:20 UTC → 19:20 北京时间
    • 11:28 UTC → 19:28 北京时间
    • 13:05 UTC → 21:05 北京时间
    • 13:37 UTC → 21:37 北京时间
    • 14:24 UTC → 22:24 北京时间
    • 14:30 UTC → 22:30 北京时间
    • 17:06 UTC → 次日 01:06 北京时间

1.1 事件还原:从“误以为被打”到确认是自己搞崩了

先看一眼官方给出的关键时间线(已换算为北京时间):

  • 19:05 正常
    Cloudflare 在 ClickHouse 数据库中发布了一次权限控制改动,目标是让分布式查询的权限更细粒度、更安全。
  • 19:20 左右 隐患种下
    由于权限改变,用于生成 Bot Management 特征配置文件的 SQL 查询开始返回重复数据,导致特征文件体积翻倍。
    这一刻起,Cloudflare 的节点就开始周期性地“吃到坏配置”。
  • 19:28 故障开始
    部分节点加载到“超规格”的特征文件,Bot 模块在加载时触发内存上限检查,直接 panic。
    对用户来说,就是访问挂在 Cloudflare 后面的站点开始大量返回 HTTP 5xx。
  • 19:31 – 19:35 监控告警 & 内部拉群

    • 19:31 第一条自动化测试报警触发。
    • 19:32 人工排查启动,初始怀疑是 Workers KV 掉链子。
    • 19:35 官方 incident call 建立,大家开始线上“救火”。

  • 19:32 – 21:05 第一阶段:误判方向,先怀疑 Workers KV 和 DDoS
    这一段时间,Cloudflare 的工程师们主要在看:

    • Workers KV 错误率飙升;
    • 下游依赖 KV 的服务(包括 Access 等)跟着爆炸;
    • 加上 Cloudflare 状态页居然也“巧合”挂了,直接把大家往“是不是又被超大规模 DDoS 打了”的方向带。
    所以他们做的事情大概是:

    • 尝试对 Workers KV 做流量限制、账号限流;
    • 调整流量分配,试图先让 KV 这个“看起来最明显的病灶”活过来。
    对用户的体验就是:

    • 有时候能打开页面,有时候直接 5xx;
    • 各种“抽风”,很像被人恶意打挂,而不是稳定的内部错误。

  • 21:05 Workers KV / Access 绕过,影响减轻
    Cloudflare 内部有个“后门”:可以让 Workers KV 和 Access 绕过当前版本的核心代理(FL2),回退到旧版 FL。

    • 不幸的是,旧版 FL 同样依赖这份 Bot 特征文件,所以问题并没有从根上消失;
    • 幸运的是,旧版本在错误处理上的行为稍微“温和”一点,整体错误率比 FL2 好看一些。

  • 21:37 锁定元凶:Bot Management 配置文件
    经过一圈排查之后,他们终于意识到:

    • 真正触发 panic 的,是 Bot Management 模块;
    • 真正的问题,是那份“长歪了”的特征配置文件。
    接下来多条工作流并行:

    • 一边想办法彻底停止坏文件的生成和分发;
    • 一边准备回滚到最后一个“已知良好版本”的特征文件。

  • 22:24 停止坏文件的生成与分发
    这一步很关键,也是这次事故真正“踩住刹车”的时间点:

    • 停止 ClickHouse 那条会产生重复列的查询;
    • 阻断新的坏配置在全网继续传播。

  • 22:24 – 22:30 验证旧文件可用性
    工程团队在小范围节点上验证:

    • 替换为旧版特征文件后,核心代理是否能正常启动;
    • 错误率是否回落,延迟是否恢复正常。
    验证通过之后,开始全网铺开。

  • 22:30 主影响解除
    正常版本的 Bot 配置文件在全网重新下发:

    • 大部分 HTTP 5xx 开始消失;
    • Cloudflare 网络“躺平”了几个小时之后,流量瞬间涌回,局部出现拥挤和抖动。

  • 22:30 – 23:30 第二阶段:流量回流 + 控制面“被人挤爆”
    流量回归后的典型现象:

    • 用户端大部分访问恢复,但偶尔仍感觉卡顿;
    • Dashboard(登录、操作)也被回流的登录请求和重试“打到脸变形”,availability 掉了一段时间。

  • 次日 01:06 全面恢复
    Cloudflare 表示所有下游服务重启完成,错误率恢复到正常水平,这次事故正式画上句号。
1.2 根因复盘:一条 SQL + 一个硬编码上限,搞垮半个互联网

从技术视角看,这次事故非常“经典”:

  • ClickHouse 权限变更
    之前:

    • 用户只在 default 数据库看到分布式表的元数据;
    • 底层真实数据存放在 r0 等库里,通过 Distributed 引擎访问,但用户不直接感知。
    这次变更:

    • 出于“安全 + 可靠性”的考虑,让用户也能显式看到自己有权限访问的底层表元数据。

  • 历史遗留 SQL 假定“只有 default”
    有一条用于生成 Bot 特征配置文件的 SQL 长这样(简化):
    1. SELECT
    2.   name,
    3.   type
    4. FROM system.columns
    5. WHERE
    6.   table = 'http_requests_features'
    7. ORDER BY name;
    复制代码
    注意:这里没有限制 database 字段。
    在权限变更之前:

    • 这条查询只会返回 default 里的那张表;
    • 行数等于特征数 ≈ 60,多年稳定运行,没人觉得有问题。
    在权限变更之后:

    • 查询开始同时返回 default + r0 中同名表的列信息;
    • 从“60 行”变成“1xx 行甚至 2xx 行”,特征配置文件被无辜放大一倍多。

  • Bot 模块的硬编码上限
    为了避免无限内存占用,Bot 模块在加载特征文件时做了一个预分配:

    • 上限设为 200 个特征;
    • 实际日常只用了 ~60 个,看起来非常安全;
    • 但这次被权限改动 + SQL 逻辑一起阴了一把,超过 200 直接触发错误。

  • 防护缺失点

    • 内部配置文件的“安全性假设”过高,没有像用户配置那样严谨校验;
    • 模块级别没有熔断/降级机制(例如超过特征上限时禁用模块而不是炸整个代理);
    • 错误采集系统在高并发 panic 场景下反而“吃掉”大量 CPU,加重延迟。

一句话总结:
一个看起来“只是做权限显式化”的小改动,踩中了“历史 SQL + 硬编码上限 + 缺乏防呆”的组合陷阱,进而把核心代理一锅端。
1.3 那段 Rust 代码到底在什么位置背了锅?

Cloudflare 在官方复盘的 “Memory preallocation” 小节里,把直接触发 panic 的 FL2 代码片段也贴出来了,大意如下(去掉了一些无关细节,格式按截图还原):
  1. /// Fetch edge features based on `input` struct into [`Features`] buffer.
  2. pub fn fetch_features(
  3.     &mut self,
  4.     input: &dyn BotsInput,
  5.     features: &mut Features,
  6. ) -> Result<(), (ErrorFlags, i32)> {
  7.     // update features checksum (lower 32 bits) and copy edge feature names
  8.     features.checksum &= 0xFFFF_FFFF_0000_0000;
  9.     features.checksum |= u64::from(self.config.checksum);
  10.     let (feature_values, _) = features
  11.         .append_with_names(&self.config.feature_names)
  12.         .unwrap();
  13.     // ...
  14. }
复制代码
官方描述是:

  • Bot Management 为“特征数量”设了一个硬编码上限 200;
  • 平时只用了 ~60 个特征;
  • 坏配置文件导致特征数量超过上限时,append_with_names 返回 Err;
  • 上面这段代码直接 .unwrap(),于是整个 FL2 worker 线程 panic。
对应的 panic 日志也在官方文中给出:
  1. thread fl2_worker_thread panicked: called Result::unwrap() on an Err value
复制代码
从事故链路来看,这段 Rust 代码处在“最后一跳”的位置:

  • 上游的 ClickHouse 权限变更 + 历史 SQL 假设,导致特征文件变大;
  • Bot 模块在加载超规格配置时触发特征数量上限;
  • fetch_features 里对结果无脑 unwrap(),让单个 worker 直接崩溃;
  • 大量请求打到挂掉的代理上,最终变成对外可见的大面积 5xx。
所以,网上那种“Cloudflare 被一行 Rust unwrap 弄挂了”的说法,情绪价值比技术价值高:

  • 真正的锅在于整体防线缺失:上游 SQL 对行为变化缺乏防护、中游配置没有安全网、下游模块内部错误直接 panic,没有任何降级;
  • Rust 这行 unwrap() 更像是“扣扳机的人”,但枪是大家一起造的。
站在普通用户的角度,我们既没法帮 Cloudflare 把整条链路重写一遍,也没法保证它以后不再犯类似错误,所以更现实的做法,是在它出问题时,我们自己的系统还能有路可走
2. 在没办法推进 CF 提升可用性的情况下,怎么做灾备?

从这次事故可以看出一个非常现实的问题:

  • Cloudflare 的王牌服务(反向代理/CDN/WAF)要求你把域名 NS 托管给它;
  • 一旦 CF 自身控制面也受影响(Dashboard 登不上、API 不可用),你就没法在 CF 里改解析;
  • 换句话说:事故发生时,你手里没有那根“紧急断电开关”。
先对比一下两种常见使用方式:

  • 托管 NS + CF 代理(大部分免费 / Pro 用户)

    • 平时体验极好,一站式搞定 DNS + 代理 + WAF + 性能。
    • 故障时,如果 Dashboard / API 也受影响:你连“把流量挪回源站”都做不到。

  • 传统 CNAME 接入(部分 SaaS/CDN 场景)

    • 权威 DNS 在你或者第三方手里,可以随时把 CNAME 改成 A 直连源站。
    • 灾备时要批量修改大量 CNAME 记录,非常麻烦,一旦漏改某条记录,对应业务就直接黑掉。

Cloudflare 会不会因为这次事故开放更多“只 CNAME、不托管 NS”的接入方式?
从它自己的产品形态和历史来看,短期内不太乐观,尤其对免费玩家而言。
那在这个前提下,我们能做什么?
答案是:

  • 保持现有“NS 托管给 CF”的体验;
  • 同时,在第三方 DNS 里维护一份完全同步的解析数据;
  • 事故发生时,不去碰 Cloudflare,而是在域名注册商那里把 NS 改回备用 DNS,绕过 CF 直连源站。
这就要求一个硬前提:
域名注册商不能是 Cloudflare
不然你的 NS 改来改去还是在 Cloudflare 自己的体系里,无法做到“彻底脱离 CF”。
2.1 思路概述

核心设计:

  • 平时:

    • NS → Cloudflare;
    • 所有解析变更通过一个“统一脚本”,同时写入 CF 和备用 DNS
    • 再加一个定时同步脚本做兜底,保证两边解析记录尽量一致。

  • 灾备:

    • 不动 Cloudflare 的任何东西;
    • 只在域名注册商后台(注意,不是 CF 注册商)调用 API,把 NS 从 CF 改为备用 DNS;
    • 因为备用 DNS 已经实时同步了记录,所以切换后解析能立即工作(考虑 DNS 缓存会有几分钟的尾巴)。

  • 恢复:

    • Cloudflare 恢复可用且确认稳定之后,再通过脚本把 NS 改回 CF;
    • 解析记录本身两边一直同步,切回不会有记录错乱的问题。

对比 CNAME 玩家:

  • CNAME 灾备时要把所有 CNAME 批量改成 A,工作量和出错概率都挺可观;
  • 这个方案里,灾备时只改 NS,一次完成,子域多少完全不重要。
2.2 正常运行架构

先画一个“正常运行时”的结构:
flowchart LR    subgraph User["用户浏览器 / 客户端"]    end    subgraph Registrar["域名注册商(非 Cloudflare)"]        NS["NS = Cloudflare NS"]    end    subgraph CF["Cloudflare"]        CF_DNS["Cloudflare DNS"]        CF_Proxy["Cloudflare 代理 / CDN / WAF / Bot 等"]    end    subgraph BackupDNS["备用 DNS(如 DNSPod)"]        B_DNS["DNS 记录(实时同步)"]    end    subgraph Origin["源站 / 后端服务"]        APP["应用服务器 / 集群"]    end    Script["解析变更脚本\n(同时写 CF + 备用 DNS)"]    Sync["定时同步脚本\n(以 CF 为源,CF → 备用 DNS)"]    User -->|解析域名| CF_DNS    CF_DNS --> CF_Proxy    CF_Proxy --> APP    Script --> CF_DNS    Script --> B_DNS    Sync --> CF_DNS    Sync --> B_DNS    NS -. 指向 .-> CF_DNS    B_DNS -. 平时不对外 .-> Origin关键点:

  • 用户正常只会问 CF 的 DNS;
  • 备用 DNS 平时“躺平”,只是跟着同步记录,不对外真正生效;
  • 所有解析变更都通过脚本走一遍 CF + 备用 DNS。
2.3 灾备时的 NS 切换

再看“灾备切换”的视图:
flowchart LR    subgraph User["用户浏览器 / 客户端"]    end    subgraph Registrar["域名注册商(非 Cloudflare)"]        NS_CF["NS = Cloudflare NS\n(正常)"]        NS_BK["NS = 备用 DNS NS\n(灾备)"]    end    subgraph CF["Cloudflare(部分或全部故障)"]        CF_DNS["Cloudflare DNS"]        CF_Proxy["Cloudflare 代理 / CDN / WAF"]    end    subgraph BackupDNS["备用 DNS(如 DNSPod)"]        B_DNS["已同步的 DNS 记录"]    end    subgraph Origin["源站 / 后端服务"]        APP["应用服务器 / 集群"]    end    Switch["切换脚本\n(调用注册商 API 改 NS)"]    User -->|正常解析| CF_DNS    CF_DNS --> CF_Proxy --> APP    User -->|灾备解析| B_DNS --> APP    Switch --> NS_CF    Switch --> NS_BK    CF_DNS -. 正常时生效 .-> NS_CF    B_DNS -. 灾备时生效 .-> NS_BK要点:

  • 切换动作发生在非 Cloudflare 的域名注册商那里,Cloudflare 自己只是一个普通 NS 服务;
  • 一旦注册商那边改成“NS = 备用 DNS”,Cloudflare 整套 DNS + 代理对用户就“消失”了。
3. 具体方案实现

下面按“能落地”为目标来设计,默认你:

  • 域名注册商不是 Cloudflare;
  • 有至少一个在 Cloudflare 托管 DNS 的域名;
  • 有一个备用 DNS 服务(比如 DNSPod、Route53 等),支持 API 操作;
  • 有一个支持定时任务的环境(Linux + crontab 之类即可)。
3.1 前提与假设


  • 业务接入方式

    • 现状:NS 已指向 Cloudflare,域名通过 CF 代理公开对外;
    • 目标:不改变日常使用方式,只额外增加灾备能力。

  • 域名注册商支持 API

    • 例如腾讯云、阿里云、Namecheap 等;
    • 用于脚本化修改 NS(切到 CF / 切回备用 DNS)。

  • 备用 DNS 支持 API 管理记录

    • DNSPod、Route53、Cloudflare 以外的任何一家都可以;
    • 要求:

      • 支持添加/修改/删除 A、AAAA、CNAME、TXT 等记录;
      • 支持设置 TTL(建议 60~120 秒)。


  • 源站公网可直连

    • 灾备切换后,流量会直接打到源站或其他 CDN,而不再经过 CF;
    • 源站需要至少能承受一段时间内的流量(可以配合其它 CDN 做前层)。

  • 域名注册商不能是 Cloudflare

    • 否则你的“改 NS”仍然是在 Cloudflare 自己的平台里兜圈子;
    • 真正的“紧急断电开关”必须掌握在第三方注册商手里。

3.2 解析层“双活”:变更脚本 + 定时同步

我们把解析层分成两部分逻辑:

  • “平时改解析”的统一入口;
  • “兜底定时同步”。
3.2.1 统一解析变更脚本

原则:

  • 不允许直接在 CF 控制台或备用 DNS 控制台手改解析;
  • 所有变更统一走一个脚本,比如:
  1. ./dns-update \
  2.   --record "www.example.com" \
  3.   --type "A" \
  4.   --value "1.2.3.4" \
  5.   --ttl 120
复制代码
脚本内部做两件事:

  • 调 Cloudflare API

    • 更新 zone 内对应的记录;
    • 保持 proxied = true(需要走代理的记录)。

  • 调备用 DNS(如 DNSPod)API

    • 为同名记录写入 A 记录,指向源站真实 IP 或上游 CDN;
    • TTL 设置与 CF 尽量一致或者略小。

极简伪代码示例:
  1. function update_record(name, type, value, ttl):
  2.     # 1. Update Cloudflare
  3.     cf_api.update_dns_record(
  4.         zone_id = CF_ZONE_ID,
  5.         name = name,
  6.         type = type,
  7.         content = value,
  8.         ttl = ttl,
  9.         proxied = true  # 走代理的照旧走代理
  10.     )
  11.     # 2. Update Backup DNS (DNSPod)
  12.     backup_api.upsert_record(
  13.         domain = ROOT_DOMAIN,
  14.         subdomain = extract_subdomain(name, ROOT_DOMAIN),
  15.         type = type,
  16.         value = value,
  17.         ttl = ttl
  18.     )
  19.     log("DNS record updated on both CF and Backup DNS")
复制代码
这样可以保证:

  • 从此刻开始,CF DNS 和备用 DNS 里永远是同一套解析数据;
  • 灾备时切 NS 时,不用再临时调整记录。
3.2.2 定时同步脚本(以 CF 为源)

考虑到现实情况:

  • 有人难免会手贱直接在 CF 面板改一次;
  • 或者团队里有人绕过脚本直接操作;
可以每 5 分钟跑一次定时任务,做兜底同步:

  • 从 Cloudflare 拉一遍当前 zone 的所有记录;
  • 按规则过滤掉不需要同步的记录(比如 ACME 临时验证记录等);
  • 对备用 DNS 做“幂等更新”。
伪代码示例:
  1. function sync_from_cf_to_backup():
  2.     cf_records = cf_api.list_dns_records(zone_id = CF_ZONE_ID)
  3.     for r in cf_records:
  4.         if should_ignore(r):
  5.             continue
  6.         backup_api.upsert_record(
  7.             domain = ROOT_DOMAIN,
  8.             subdomain = r.name_without_root,
  9.             type = r.type,
  10.             value = r.content,
  11.             ttl = max(r.ttl, 60)
  12.         )
  13.     log("Sync from CF to backup DNS completed")
复制代码
这样一来,即便偶尔有人违规“直改 CF”,最多也就 5~10 分钟后被同步脚本拉回一致。
3.3 灾备切换脚本(改 NS)

真正出事的时候,核心动作只有一个:

  • 在域名注册商 API 上,把 NS 从 CF NS 改为备用 DNS 的 NS。
伪代码示例:
  1. function switch_to_backup_ns():
  2.     registrar_api.update_ns(
  3.         domain = ROOT_DOMAIN,
  4.         nameservers = BACKUP_DNS_NS_LIST
  5.     )
  6.     log("NS switched to backup DNS")
  7. function switch_back_to_cf_ns():
  8.     registrar_api.update_ns(
  9.         domain = ROOT_DOMAIN,
  10.         nameservers = CLOUDFLARE_NS_LIST
  11.     )
  12.     log("NS switched back to Cloudflare")
复制代码
操作模式可以分两种:

  • 手动触发:

    • 当你确认 Cloudflare 整体不可用,且短时间看不到恢复希望时;
    • 人为执行 switch_to_backup_ns。

  • 半自动触发:

    • 写一个监控程序,每 N 分钟检测 Cloudflare 的可用性;
    • 当连续多次检测失败时,发告警通知 + 提供一键切换命令;
    • 最终是否切换,仍由人来决定,避免误伤。

3.4 故障检测逻辑示例

检测什么?

  • 若干“关键业务域名”的 HTTP 状态;
  • 直接访问 Cloudflare 的边缘节点(比如 trace 或公共 API);
  • 尝试调用 Cloudflare API,判断控制面是否还活着。
伪代码示例:
  1. function health_check():
  2.     errors = 0
  3.     for url in CRITICAL_URLS:
  4.         status = http_get(url, timeout=3)
  5.         if status >= 500:
  6.             errors += 1
  7.     cf_api_ok = cf_api.ping()
  8.     if errors >= ERROR_THRESHOLD or not cf_api_ok:
  9.         return "bad"
  10.     else:
  11.         return "good"
复制代码
守护进程示例:
  1. state = "good"
  2. bad_count = 0
  3. while true:
  4.     result = health_check()
  5.     if result == "bad":
  6.         bad_count += 1
  7.     else:
  8.         bad_count = 0
  9.     if bad_count >= MAX_BAD_ROUNDS and state == "good":
  10.         alert("Cloudflare may be down. Consider switching to backup NS.")
  11.         state = "alerted"
  12.     sleep(CHECK_INTERVAL)
复制代码
这里刻意不自动切 NS,而是改为“告警 + 人工决定”,避免因为网络抖动导致“脚本自己把 NS 切来切去”。
3.5 实际效果与局限

这个方案能解决什么?

  • 当 Cloudflare 控制面 / 代理整体出问题时,你依然有能力在十几分钟内“脱离 CF”;
  • 灾备时的操作非常简单:只改 NS,不用逐条改解析记录;
  • 对“免费玩家”和小团队来说,成本非常低:写几个脚本 + 做一次演练即可。
解决不了什么?

  • 在 CF 恢复之前,你享受不到它的加速和安全能力;
  • NS 切换仍然会受 DNS 缓存影响,极端情况下全网完全一致需要几十分钟;
  • 如果源站本身抗压能力有限,直接暴露到公网同样有被打挂的风险,需要配合其他 CDN 或 WAF。
但在“我们没办法推动 Cloudflare 把可用性做到绝对完美”的前提下,这个方案至少做到了:

  • 控制权回到自己手里;
  • 切换和回切都可以脚本化、可演练;
  • 日常不改变你作为 Cloudflare 用户的使用体验,事故发生时又有一条退路。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册