2024 年 6 月 20 日(星期四),两起单独的事件导致互联网资产和 Cloudflare 服务的延迟和错误率增加,持续了 114 分钟。在影响达到峰值的 30 分钟内,我们发现 1.4% 到 2.1% 的 CDN HTTP 请求收到了通用错误页面,并且观察到第 99 百分位的首字节时间 (TTFB) 延迟增加了 3 倍。
发生这些事件的原因如下:
自动网络监控检测到性能下降,以次优方式重新路由流量,并在 UTC 时间 17:33 至 17:50 之间造成主干网拥塞
UTC 时间 14:14 至 17:06 之间部署的新型分布式拒绝服务 (DDoS) 缓解机制触发了我们的速率限制系统中的一个潜在错误,该错误允许特定形式的 HTTP 请求导致其处理进程在 UTC 时间 17:47 至 19:27 之间进入无限循环
全球许多 Cloudflare 数据中心都受到了这些事件的影响。
对于主干网拥塞事件,我们已经开始着手扩大受影响数据中心的主干网容量,并改进我们的网络缓解措施,以便在采取行动时使用有关替代网络路径上可用容量的更多信息。在这篇博文的其余部分,我们将更详细地介绍这些事件中影响更大的第二起事件。
作为我们保护机制例行更新的一部分,我们创建了一条新的 DDoS 规则,以阻止我们在基础设施上观察到的特定类型的滥用。这条 DDoS 规则按预期工作,但在特定的可疑流量案例中,它暴露了我们现有速率限制组件中的潜在错误。需要明确的是,我们没有理由相信这个可疑流量是故意利用这个错误,也没有证据表明存在任何类型的违规行为。
我们对所造成的影响深表歉意,并已进行更改以防止这些问题再次发生。
背景
对可疑流量进行速率限制
根据 HTTP 请求的配置文件和所请求的互联网资产的配置,Cloudflare 可能会通过限制访问者在一定时间范围内可以发出的请求数量来保护我们的网络和客户的来源。这些速率限制可以通过客户配置激活,也可以响应检测到可疑活动的 DDoS 规则而激活。
通常,这些速率限制将根据访问者的 IP 地址应用。由于许多机构和互联网服务提供商 (ISP) 可能让许多设备和个人用户具有一个 IP 地址,因此基于 IP 地址的速率限制是一种大范围的做法,可能会无意中阻止合法流量。
平衡网络中的流量
Cloudflare 有多个系统共同提供持续的实时容量监控和重新平衡,以确保我们尽可能快速、高效地服务尽可能多的流量。
第一个是 Unimog,Cloudflare 的边缘负载平衡器。到达我们 Anycast 网络的每个数据包都会经过 Unimog,Unimog 会将其传送到适当的服务器来处理该数据包。该服务器的位置可能与数据包最初到达我们网络中的位置并不相同,具体取决于计算能力的可用性。在每个数据中心内,Unimog 旨在使所有活动服务器的 CPU 负载保持均匀。
为了全面了解我们的网络,我们依赖于 Traffic Manager。在我们所有的数据中心位置,它会接收各种信号,例如总体 CPU 利用率、HTTP 请求延迟和带宽利用率,以指导重新平衡决策。它具有内置安全限制,可防止造成过大的流量转移,并且在做出任何决策时还会考虑目标位置的预期负载。
事件时间线和影响
所有时间戳均为 2024 年 6 月 20 日的 UTC 时间。
14:14 DDoS 规则开始逐步部署
17:06 在全球部署 DDoS 规则
17:47 第一个 HTTP 请求处理进程中毒
18:04 根据检测到的高 CPU 负载自动声明发生事件
18:34 在一台服务器上进行服务重启表明可以恢复服务,在一个数据中心内测试完全重启
18:44 服务重启后数据中心的 CPU 负载恢复正常
18:51 开始对具有许多卡住进程的所有服务器进行持续的全局重新加载
19:05 全球观测点 HTTP 错误率峰值为 2.1% 服务不可用/总计 3.45%
19:05 Traffic Manager 首先采取行动恢复服务
19:11 全球观测点 HTTP 错误率减半至 1% 服务不可用/总计 1.96%
19:27 全球观测点 HTTP 错误率降至基线水平
19:29 认定 DDoS 规则部署可能是发生进程中毒的原因
19:34 DDoS 规则完全禁用
19:43 工程师停止在有许多卡住进程的服务器上例行重启服务
20:16 事件响应停止
下面,我们从 Cloudflare 的一些内部指标来看一下影响。第一张图表显示了由于服务中毒导致无法访问而产生错误响应的所有观测点(从外部设备输入)HTTP 请求的百分比。我们看到请求数最初增加到 0.5%,然后增加到 2.1%,之后由于服务重新加载而开始恢复。
为了更全面地了解错误,我们可以看到在同一时间段内我们的网络返回的所有 5xx 响应,包括来自源服务器的响应。这些响应达到 3.45% 的峰值,您可以更清楚地看到,在 UTC 时间 19:25 到 20:00 之间,随着 Traffic Manager 完成其重新路由活动,错误率逐渐恢复。UTC 时间 19:25 的下降与最后一次大规模重新加载一致,之后的错误增加主要源自上游 DNS 超时和连接限制,这与高负载和不平衡负载一致。
以下是我们在第 50、90 和 99 百分位的 TTFB 测量结果,显示第 99 百分位的延迟增加了近 3 倍。
这个错误的技术描述及其如何发生
事件期间使用过多 CPU 的 HTTP 请求处理进程的全球百分比
早些时候,即 UTC 时间 6 月 20 日 14:14 - 17:06 期间,我们在网络上逐步启用了一条新的 DDoS 规则。Cloudflare 最近一直在构建一种缓解 HTTP DDoS 攻击的新方法。此方法结合使用速率限制和 cookie,以允许被错误识别为攻击部分的合法客户端继续运行。
使用这种新方法,被视为可疑的 HTTP 请求将经历以下关键步骤:
检查是否存在有效的 cookie,否则阻止请求
如果找到有效的 cookie,则根据 cookie 值添加速率限制规则,以便稍后进行评估
在运行所有当前应用的 DDoS 缓解措施后,应用速率限制规则
我们使用这种“异步”工作流程,因为在没有速率限制规则的情况下阻止请求更有效率,从而为应用其他规则类型提供了机会。
总的来说,该流程可以用以下伪代码来概括:
在评估速率限制规则时,我们需要为每个客户端创建一个_密钥_,用于查找正确的计数器并将其与目标速率进行比较。通常,此密钥是客户端 IP 地址,但还有其他选项可用,例如此处使用的 cookie 的值。我们实际上重用了速率限制逻辑的现有部分来实现这一点。在伪代码中,它看起来是这样的:
for (rule in active_mitigations) {
// ... (ignore other rule types)
if (rule.match_current_request()) {
if (!has_valid_cookie()) {
// no cookie: serve error page
return serve_error_page();
} else {
// add a rate-limit rule to be evaluated later
add_rate_limit_rule(rule);
}
}
}
evaluate_rate_limit_rules();
这个简单的_密钥_生成函数存在两个问题,这两个问题与特定形式的客户端请求相结合,导致处理 HTTP 请求的过程中出现无限循环:
function get_cookie_key() {
// Validate that the cookie is valid before taking its value.
// Here the cookie has been checked before already, but this code is
// also used for "standalone" rate-limit rules.
if (!has_valid_cookie_broken()) { // more on the "broken" part later
return cookie_value;
} else {
return parent_key_generator();
}
}
DDoS 逻辑生成的速率限制规则以意想不到的方式使用内部 API。这导致上述伪代码中的
parent_key_generator
指向get_cookie_key
函数本身,这意味着如果采用该代码路径,该函数将无限期地调用自身由于这些速率限制规则仅在验证 cookie 后添加,因此第二次验证应该会得到相同的结果。问题是这里使用的
has_valid_cookie_broken
函数实际上是不同的,如果客户端发送多个 cookie,其中一些有效,而另一些无效,则两者可能会不一致
因此,结合这两个问题:损坏的验证函数告诉 get_cookie_key
该 cookie 无效,导致采用 else
分支并一遍又一遍地调用相同的函数。
许多编程语言都采取了一项保护措施来帮助防止此类循环,即对函数调用堆栈可以达到的深度进行运行时保护限制。如果尝试调用已达到此限制的函数,只需一次即会导致运行时错误。阅读上述逻辑时,初步分析可能表明我们在这一事件中已经达到了限制,因此请求最终导致错误,堆栈中反复包含这些相同的函数调用。
然而,这里的情况并非如此。用于编写此逻辑的部分语言(包括 Lua)还实施了一种称为“正确的尾调用”的优化。尾调用是指函数的最后一个动作是执行另一个函数。我们不必将该函数添加为堆栈中的另一层,因为我们确信,之后不会将执行上下文返回给父函数,也不会使用其任何局部变量,而是可以用此函数调用替换堆栈中的顶部帧。
最终结果是请求处理逻辑中出现循环,该循环永远不会增加堆栈的大小。相反,它只会消耗 100% 的可用 CPU 资源,并且永远不会终止。一旦处理 HTTP 请求的进程收到应应用该操作的单个请求,并且该进程混合了有效和无效的 cookie,该进程就会中毒,并且永远无法处理任何进一步的请求。
每个 Cloudflare 服务器都有数十个这样的进程,因此单个中毒进程不会造成太大影响。然而,紧接着开始发生一些其他事情:
服务器 CPU 利用率的增加导致 Unimog 降低服务器接收的新流量的数量,将流量转移到其他服务器,因此在某个时候,更多的新连接将从部分进程中毒的服务器转移到中毒进程较少或没有中毒进程的服务器,从而降低 CPU 利用率。
数据中心 CPU 利用率的逐渐增加开始导致 Traffic Manager 将流量重定向到其他数据中心。由于此操作无法修复中毒进程,CPU 利用率仍然很高,因此 Traffic Manager 继续重定向越来越多的流量。
两种情况下的重定向流量都包含中毒进程的请求,导致发送此重定向流量的服务器和数据中心开始以相同的方式出现故障。
几分钟之内,多个数据中心就出现了大量中毒进程,Traffic Manager 已将尽可能多的流量从这些进程中转移出去,但无法再转移更多。这部分是由于其内置的自动化安全限制,但也因为越来越难以找到具有足够可用容量作为目标的数据中心。
第一个中毒进程出现在 UTC 时间 17:47,到 UTC 时间 18:09(事件宣布五分钟后),Traffic Manager 已将大量流量重新路由出欧洲:
截至 UTC 时间 18:09 的 Traffic Manager 容量操作摘要图。每个圆圈代表流量被重新路由离开或到达的数据中心。圆圈的颜色表示该数据中心的 CPU 负载。它们之间的橙色丝带显示有多少流量被重新路由,以及从哪里/到哪里。
如果我们查看 HTTP 请求服务进程中使 CPU 达到饱和的进程所占的百分比,就会很容易明白原因。在这些时区的高峰流量时段,西欧的容量已消耗 10%,东欧的容量已消耗 4%:
使 CPU 达到饱和的所有 HTTP 请求处理进程的百分比(按地理区域划分)
许多地点部分中毒的服务器难以应对请求负载,其余进程无法跟上,导致 Cloudflare 返回最少的 HTTP 错误响应。
在 UTC 时间 18:04,我们的全球 CPU 利用率达到某个持续水平,Cloudflare 工程师自动收到通知,并开始调查。我们许多当值的事件响应人员已经在处理由主干网络拥塞引起的未决事件,在最初几分钟内,我们调查了与网络拥塞事件的可能关联。我们花了一些时间才意识到 CPU 最高的位置是流量最低的位置,之后的调查不再将网络事件视为触发因素。此时,焦点转移到两个主要方面:
评估重新启动中毒进程是否能够让它们恢复,如果可以,则在受影响的服务器上大规模重启服务
确定进程进入此 CPU 饱和状态的触发因素
在初始事件宣布 25 分钟后,我们确认重启对一台样本服务器有帮助。五分钟后,我们开始执行更广泛的重启——最初是一次性重启整个数据中心,然后随着识别方法的完善,对有大量中毒进程的服务器进行重启。一些工程师继续在受影响的服务器上定期重启受影响的服务,而其他人则加入正在同步进行的另一项工作——识别触发因素。UTC 时间 19:36,在全球范围内禁用了新的 DDoS 规则,在执行了又一轮大规模重启和监控后,该事件被宣布解决。
同时,事件所呈现的情况触发了 Traffic Manager 中的潜在错误。触发后,系统尝试通过启动正常重启来从异常中恢复,导致其活动停止。该错误首次在 UTC 时间 18:17 触发,然后在 UTC 时间 18:35 至 18:57 之间多次触发。在这两个时间段内(UTC 时间 18:35-18:52 和 UTC 时间 18:56-19:05),系统未发出任何新的流量路由操作。这意味着,虽然我们已经恢复了受影响最严重的数据中心的服务,但几乎所有流量仍在重新路由。值班工程师在 UTC 时间 18:34 收到有关该问题的警报。到 UTC 时间 19:05,流量团队已经编写、测试并部署了修复程序。恢复后的首批操作对恢复服务产生了积极影响。
补救及后续步骤
为了解决请求中毒对我们网络的直接影响,Cloudflare 发起了受影响服务的大规模滚动重启,直到确定触发该情况的更改并还原该更改。该更改(即激活一种新型 DDoS 规则)仍将完全回滚,在我们修复损坏的 cookie 验证检查并完全确信这种情况不会再次发生之前,该规则都不会重新激活。
我们非常重视这些事件,并认识到它们造成的影响有多大。我们已经确定可以采取一些步骤来解决这些具体情况,并确定了未来再次发生此类问题的风险。
设计:为我们 DDoS 模块使用的速率限制实施是一个遗留组件,客户为其互联网资产配置的速率限制规则使用具有更多现代技术和保护措施的较新引擎。
设计:我们正在探索经历进程中毒的服务内部和周围的选项,以限制通过尾调用无限循环的能力。从长远来看,Cloudflare 正在进入完全替换此服务的早期实施阶段。此替换服务的设计将使我们能够对单个请求的不间断执行时间和总执行时间施加限制。
流程:新规则的首次激活是在少数几个生产数据中心进行验证,然后在几个小时后在所有数据中心进行。我们将继续加强我们的暂存和推出程序,以最大限度地减少与变更相关的潜在影响范围。
总结
Cloudflare 经历了两次接连的事件,影响了使用我们 CDN 和网络服务的大量客户。第一起事件是网络主干网拥塞,我们的系统已自动修复。对于第二起事件,我们通过定期重启故障服务来缓解,同时我们识别并停用了引发故障的 DDoS 规则。对于由此给我们的客户和试图访问服务的最终用户造成的任何干扰,我们深表歉意。
我们已经在生产环境中消除了激活故障服务中的潜在错误所需的条件,且正在进行进一步的修复和检测。