Scaling Security Insights: how we achieved a 10x increase in global scanning capacity

TL;DR · AI 摘要
Cloudflare 通过优化 Kafka 和引入新的扫描架构,将安全扫描吞吐量提升了 10 倍,显著提高了扫描频率和覆盖范围。
核心要点
- Cloudflare 将安全扫描吞吐量从每秒 10 次提升至每秒 100 次,提升了 10 倍。
- 通过优化 Kafka 分区和引入新的扫描架构,解决了系统负载和处理延迟问题。
- 自动扫描功能已扩展至所有账户,包括免费计划用户,提升了整体安全覆盖。
结构提纲
按章节快速跳转。
- §引言
Cloudflare 的 Security Insights 需要提升扫描频率和覆盖范围,以应对日益增长的安全威胁。
- ·扫描机制
Kafka 的分区机制限制了扫描吞吐量,导致处理延迟和系统负载问题。
- ·优化方案
Cloudflare 通过优化 Kafka 分区和引入新的扫描架构,成功将吞吐量提升 10 倍。
优化后,扫描频率翻倍,所有账户(包括免费计划)均可享受自动扫描服务。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- Cloudflare 安全扫描优化
- 背景与挑战
- 扫描频率低
- 免费账户未启用自动扫描
- 优化方案
- Kafka 分区优化
- 引入新的扫描架构
- 结果与影响
- 吞吐量提升 10 倍
- 扫描频率翻倍
金句 / Highlights
值得收藏与分享的关键句。
Cloudflare 将扫描吞吐量从每秒 10 次提升至每秒 100 次,提升了 10 倍。
Kafka 的分区机制限制了扫描吞吐量,导致处理延迟和系统负载问题。
优化后,扫描频率翻倍,所有账户(包括免费计划)均可享受自动扫描服务。
扩展安全洞察:我们如何实现全球扫描能力的十倍提升
2026-06-12
- Dave Baxter
8 分钟阅读
本文也提供以下语言版本:
æ¥æ¬èª
和
íêµì´
.
安全洞察为每个 Cloudflare 账户提供可操作的安全建议。为了找到这些洞察,我们对所有账户、区域和 DNS 记录进行定期扫描,寻找潜在的安全风险和配置错误。
然而,出现了两个关键问题。首先,我们的扫描频率太低。扫描仅每两周进行一次,因此新引入的安全风险可能需要长达两周的时间才能被发现。其次,对于许多免费计划账户,自动扫描是可选的,这意味着很多账户根本未被扫描。
不频繁或没有扫描的风险正在上升:随着自动化攻击的加速,检测安全配置错误的时间窗口正在缩小。确保我们为所有客户发现这些问题,对我们实现为每个人打造更好的互联网的目标至关重要。
我们计算出,为了提高扫描频率并为所有账户启用自动扫描,我们需要将扫描吞吐量平均提高约十倍,从每秒 10 次扫描提高到每秒 100 次扫描。但我们的系统已经难以承受负载:数百万个事件正在等待处理,我们的 API 经常超时,我们的进程经常崩溃。我们需要修复系统,并让它实现扩展。
这就是我们如何将安全洞察的扫描吞吐量提高十倍以上,为数百万客户启用安全洞察,并将所有客户的扫描频率翻倍的故事。继续阅读,了解我们是如何实现这些改进的。
我们如何扫描安全洞察
从高层次来看,我们的自动安全扫描由一个调度器触发。当账户或区域需要扫描时,调度器会将消息(或多个消息)发布到 Apache Kafka,这是一个开源的分布式事件流平台。这些消息会分发到多个检查器:专门的 Go 微服务,用于扫描特定的资产或配置。
对于每条消息,每个检查器会将其结果(发现的安全洞察)发送到我们的内部 API,然后 API 会将这些结果存储到 Postgres 数据库中。
实现扩展
扩展 Kafka
Apache Kafka 并不是一个严格意义上的队列:它是一个分区事件流(尽管最近增加了队列语义)。在分区中,消息必须按顺序消费和处理。这与典型的队列不同,队列中的消息可以按顺序消费,但可能被无序处理。因此,每个消费者组中每个分区只能有一个活跃的消费者。
这对我们有两个影响:
- 处理速度较慢的消息会阻止消费者继续处理下一条消息
- 对于每个检查器,我们只能拥有与分区数量一样多的消费者(每个检查器都有自己的消费者组)
我们本可以尝试通过增加分区数量来实现扩展。然而,这会增加 Kafka 代理本身的资源使用量,而 Kafka 代理被许多其他服务共享。我们将此作为最后的手段,首先致力于改进我们的代码和架构。
引入并行处理
虽然我们只能按顺序消费消息,但没有任何东西阻止我们同时消费多条消息。
我们调整了我们的检查器,使其以批量方式消费消息,并在单独的 goroutine 中处理每条消息。这种做法的权衡在于,如果处理过程中发生崩溃,我们需要重新处理更多的工作,同时内存使用量也会略微增加。在我们的情况下,这些影响都是可以接受的。
避免队头阻塞
我们的一些检查器处理某些消息时需要花费比其他消息更长的时间。例如,一个账户/区域可能拥有比另一个账户/区域多得多的资产。在最坏的情况下,这些消息的处理时间可能需要几分钟甚至几小时,而平均情况下只需要几秒或几毫秒。
我们选择了一个非常简单的方法:将我们的消费者组和检查器分成两组 —— “慢通道”和“快通道”。我们能够快速判断一条消息的处理是快还是慢。如果“快通道”检查器遇到一条慢消息,它会跳过这条消息。
这解决了问题:慢消息可以使用专用的资源和时间进行处理,从而减少延迟,而快消息则可以以正常的速度快速处理。
优化我们的数据库查询
我们找到的每一条洞察都会被写入 Postgres 数据库。这是由一个单一的 API 端点处理的,我们的检查器会通过一个洞察列表调用这个端点。实现方式如下:
for _, issue := range issues {
_, err = tx.Exec(ctx, `INSERT INTO table ... VALUES ($1, $2, ...) ON CONFLICT DO UPDATE ...`, ...)
if err != nil {
return err
}
}敏锐的读者会注意到,对于大量洞察,这段代码会为每个洞察进行一次数据库往返。在观察到的最大数量为 500,000 的情况下,这在一次 API 调用中会导致 500,000 次往返、查询和事务。
我们最初尝试了 Postgres 中批量插入的黄金标准:使用 COPY 命令插入到临时表中。然而,我们发现这种方法导致了 Postgres 系统表的膨胀。
我们最终采用了一种混合方法:
- 当问题数量低于某个阈值时使用 UNNEST
- 当问题数量超过该阈值时使用 COPY
这结合了两种方法的优点:对于大量洞察的插入速度合理(几秒),而对于少量洞察的插入速度更快(几毫秒)。
调查我们的 API 超时问题
当我们尝试扩展时,我们注意到内部 API 出现了一些奇怪的行为:
- 大量请求触发了客户端超时
- 许多检查器在处理时间上花费了 20% 到 90% 的时间在一个 API 调用上
- 当触发大量扫描时,我们的吞吐量一开始很高,但随后逐渐下降
所有这些问题的根本原因都是延迟。
我们的主数据库位于俄勒冈州波特兰。然而,我们的 API 在波特兰和阿姆斯特丹都运行着主动-主动模式。即使以光速传输,波特兰和阿姆斯特丹之间的往返延迟也会达到 50 毫秒。
由于这种延迟,阿姆斯特丹 API 实例的数据库查询需要更长时间,导致客户端连接池中的连接长时间保持打开状态。由于我们对 API 的请求量很大,连接池很快就会被耗尽,导致等待空闲连接时出现超时。我们的 API 调用在波特兰的平均完成时间是 10 毫秒,但在阿姆斯特丹几乎需要 3 秒!
但为什么消息吞吐量会下降呢?每个检查器进程都会被分配一组 Kafka 流的分区进行消费。我们的 API 是负载均衡的。由于我们在进程的整个生命周期中保持连接打开,一些进程连接到了阿姆斯特丹的 API,而另一些进程连接到了波特兰的 API。与波特兰相关的分区处理得很快,但那些由连接到阿姆斯特丹的进程消费的分区却滞后了:
这是其中一个检查器的 Kafka 滞后情况(单个消费者组中等待处理的消息数量)按分区划分的图表。请注意,这种情况下我们有 30 个分区。正好有 15 个分区滞后(这些线在 03/10 03:00 之后才达到或接近零)。这是因为负载均衡器在我们的 API 端点之间平均分配了流量。
这是一个简单的修复方法:我们将 API 改为活动-被动模式,确保活动的 API 与我们的主数据库保持一致。我们的延迟问题在一夜之间就消失了。
重新思考调度器
我们已经扩展了 Kafka。我们已经优化了数据库查询。我们已经修复了 API。然而,我们仍然有一个问题:我们需要确保我们的扫描在时间上大致均匀分布。我们无法同时排队所有扫描,因为我们的 Kafka 主题使用了基于时间的保留策略:扫描会在 Kafka 中堆积,最终在被处理之前就被删除。
我们的调度器在均匀分布扫描方面表现不佳。在特定时间触发的扫描数量是尖峰且不可预测的。在一周中的某些时间点,数以万计的扫描会在几分钟内被触发。这是怎么回事?
调度器在固定的周期上触发扫描。用伪代码表示,调度器的逻辑如下:
无限循环:
找出所有 last_scheduled_at + 扫描频率 <= 当前时间的账户
对于每个账户:
触发账户的扫描
触发账户中所有区域的扫描
更新 last_scheduled_at = 当前时间我们很快注意到,数据库中大量账户的 last_scheduled_at 值非常相似,这导致了部分不均衡。
然而,即使分布完全均匀,增加扫描频率也会加剧这个问题。例如,将扫描频率从每 15 天一次改为每 7 天一次,意味着 53% 的账户将突然需要扫描。
这种逻辑还有一个问题。一些账户拥有大量区域。当这些账户被安排扫描时,会触发所有区域的扫描,这导致 Kafka 分区被饱和,从而造成对扫描量较小账户的延迟。
为了解决这些问题,我们进行了三项关键更改:
- 独立地安排区域的扫描:每个区域都有自己的 last_scheduled_at 字段。
- 为现有账户和区域的 last_scheduled_at 时间进行随机化。
- 引入自适应的速率限制用于扫描调度。
独立安排区域的扫描是解决大账户问题的明显方法。对 last_scheduled_at 时间进行随机化(并确保在此过程中没有扫描被延迟)使我们能够解决数据库中已有的不均衡问题。
自适应速率限制稍微有趣一些。速率限制可以让我们解决在更改扫描频率时出现扫描量激增的问题。例如,如果我们想将扫描频率提高到每7天一次,并且我们有5000万账户,那么大约83次扫描/秒的速率限制可以确保这些扫描在7天内均匀分布。
但如果我们将账户数量增加到1000万呢?那么,这个速率限制将迫使我们花费8天时间扫描所有这些账户。这就是自适应部分的作用:速率限制每半小时根据我们拥有的总账户数和区域数以及扫描频率异步重新计算。这确保了即使我们新增数千甚至数百万账户和区域,我们仍然可以按时完成扫描。
func computeRate(free, pro, biz, ent int64) rate.Limit {
r := float64(free)/freeScanInterval.Seconds() +
float64(pro)/proScanInterval.Seconds() +
float64(biz)/bizScanInterval.Seconds() +
float64(ent)/entScanInterval.Seconds()
// 防止零计数。我们始终希望每秒至少安排一次扫描。
if r < 1 {
r = 1
}
// 将速率限制提高到“理想”值以上,以应对任何停机或负载激增时的缓冲。
r *= rateLimitBufferFactor
return rate.Limit(r)
}我们目前的状况
通过这些修复,我们的检查器在7天内的平均吞吐量随着时间的推移增加了超过10倍。
在这些改进之前,我们每秒执行大约10次扫描。这与我们目标吞吐量每秒100次扫描之间的差距似乎非常大。我们曾讨论过通过投入更多资源来解决这个问题,比如增加Kafka主题的分区数量,甚至彻底抛弃我们现有的架构。
但这些修复带来了显著的改变。如今,Security Insights在高峰调度期间每秒可以维持超过120次扫描,超过了我们10倍的改进目标。我们的内部API不再超时,Kafka滞后指标也看起来更加健康。这些可扩展性改进使我们能够为所有免费账户和区域开启自动扫描,并提高所有客户的扫描频率:
- 免费:每7天一次
- 高级和商业:每3天一次
- 企业:每天一次
系统稳定性的提升让我们有信心构建之前因限制而无法创建的新功能。我们增加了执行精细按需扫描的能力。现在,您可以手动重新扫描Cloudflare账户、区域、洞察或洞察类型。
从Cloudflare仪表板的“安全概览”页面启动精细按需扫描
我们学到的教训是,在丢弃任何东西之前,深入了解现有系统至关重要。通过仔细查看我们的代码、SQL查询、日志和指标(尤其是指标!),我们能够在不简单增加更多Pod或分区的情况下提高我们的容量。通过质疑我们的假设,深入挖掘看起来奇怪的指标,并拒绝采取简单的快捷方式(例如增加API客户端的超时时间),我们构建了一个更加稳定和有弹性的系统。
有时,向问题投入更多资源可能是答案,但在Cloudflare,我们相信通过工程方法来解决问题。
Security Insights扫描在所有Cloudflare计划中默认启用。今天登录Cloudflare仪表板,查看和管理您的安全洞察。
[if astro]>server-island-start<![endif]
应用安全
安全态势管理
工程
Postgres
Kafka