一条 SQL 的蝴蝶效应,给所有人的三个提醒


一、先抛结论,再讲故事

  1. 11 月 18 日,Cloudflare 全球宕机 5 小时 46 分,根因不是 DDoS,而是一条漏写 WHERE database= 的 SQL。
  2. 任何“内部配置”都要当“用户输入”来防;任何“小权限优化”都要当“全链路变更”来审。
  3. 把“能回滚”做成“一键杀”,比“能灰度”更重要;把“可观测”做成“可定位”,比“多监控”更值钱。

二、时间线速写(所有时间 UTC+8)

  • 19:05 数据库权限补丁上线,开始滚动重启 ClickHouse 节点。
  • 19:20 首个 5xx spike 出现,Bot Management 特征文件尺寸翻倍 → 代理 panic。
  • 19:32 内部告警响起,on-call 误判为“超大规模 DDoS”。
  • 21:05 给 Workers KV、Access 做了“紧急Bypass”,部分流量恢复。
  • 22:24 停止特征文件自动生成,手动灌入“上周备份”。
  • 22:30 核心流量回到基线。
  • 01:06 最后一条长尾 5xx 消失, incident close。

三、技术细节,一句话版本

ClickHouse 的分布式表分两层:default(逻辑视图)和 r0(物理分片)。
为了“让子查询跑在用户账号下”,运维把 r0 的元数据可见性打开;
特征生成脚本只按表名捞列,结果把两份 schema 当成双倍特征写进文件;
Rust 代码里硬编码 MAX_FEATURES=200,直接 panic,cascade 失败。


四、三个深坑,对照自查

坑 1:配置 ≠ 代码,却按代码走灰度

  • 特征文件是“数据面”配置,分钟级全网扩散,却没用“版本号+回滚通道”封装。
  • 如果把它当成“普通代码”,至少要有 staged canary:先 1%→5%→20%→100%。
  • 教训:任何能触发进程 panic 的“文件”,都要走“红蓝双桶”模型——新文件先读、不 crash 再切流。

坑 2:权限变更 ≠ 性能变更,却缺回归 Case

  • DBA 视角:只是“让用户看到本来就能访问的列”,属于安全加固。
  • 研发视角:schema 结果集翻倍,属于语义破坏。
  • 两边都没写端到端回归,因为“只是权限”。
  • 教训:只要查询结果可能变长,就是“契约变更”,必须加 diff 断言。

坑 3:观测丰富 ≠ 观测清晰,错把噪音当信号

  • 为了抓“攻击”,开了全量采样 + 堆栈 dump,CPU 打满,延迟雪上加霜。
  • 同时 status.cloudflare.com 正好也挂(外部供应商),两条曲线一叠加,心理暗示“这是协同攻击”。
  • 教训:应急观测要“分级闸门”——先保业务,再保排障;采样策略必须能 10s 内一键关。

五、给自己的作业清单

  1. 把“能回滚”做成平台能力

    • 任何文本型配置→对象存储→带版本→带签名→统一 Agent 拉取;
    • Agent 里内置“大小/哈希/字段数”三重校验,超限自动 reject。
  2. 把“权限变更”塞进 CI

    • 用 ClickHouse 的 EXPLAIN SYNTAX 把“变更前后”结果集拍 diff;
    • 超过 1% 行数变化就阻断 MR,让 DBA 和研发一起 review。
  3. 把“观测开关”做成红色按钮

    • 采样、debug log、core dump 各带全局 kill 开关,写入 on-call 手册第一页;
    • 每周做一次“关灯演练”,确保 30 秒内能关掉高耗观测。

六、写在最后

很多人把这次事故当成“一条 SQL 引发的血案”,我却更愿意叫它“一次完美风暴”:

  • 小变更(权限可见性)× 隐式依赖(脚本无库过滤)× 硬编码阈值(200 特征)× 分钟级扩散(文件热更新)× 认知偏差(DDoS 先入为主)。
    链条里任意一环如果多 10 分钟思考,都不会炸成全球新闻。

SRE 的日常工作,就是跟“完美风暴”抢那 10 分钟。
愿下一次,我们都能提前把蝴蝶的翅膀捆好,让风暴只在 PPT 里发生。

(全文 1 100 余字,首发于 lyuy.top,转载请注明出处。)