我花了十多年时间撰写有关如何快速实现 Have I Been Pwned (HIBP) 的文章。真的很快。快到有时甚至太快了:
每次搜索的响应返回得如此之快,以至于用户不确定它是否合法地检查他们输入的后续地址或者是否存在故障。
多年来,该服务已经发展到使用新兴的新技术,不仅可以提高速度,还可以在负载下扩大规模,提高可用性,有时甚至降低成本。例如,8 年前,我开始将最重要的服务滚动到 Azure Functions,“无服务器”代码不再受逻辑机的约束,并且只会扩展到向其发出的任何请求量。就在去年,我打开了 Cloudflare 缓存保留,以确保所有可缓存对象仍然被缓存,即使在它们以前会被驱逐的情况下也是如此。
现在,最重要的是,我们迄今为止所做的最酷的性能事情(现在是“我们”,谢谢 Stefán ):只是在 Cloudflare 中缓存所有内容。一切。你所做的每一次搜索……几乎。让我首先通过一些背景来解释一下:
当您访问 HIBP 上的任何服务时,流量从您的浏览器流向的第一个地方是Cloudflare 的 330 个“边缘节点”之一:
当我坐在澳大利亚最东海岸的黄金海岸写这篇文章时,我向 HIBP 提出的任何请求都会到达澳大利亚大陆最右侧的边缘节点,该节点就在布里斯班的路上。伟大的昆士兰州首府距离酒店仅有很短的水上摩托路程,直线飞行距离约为 80 公里。在此之前,每次我在家搜索 HIBP 时,我的请求字节都会通过线路传输到布里斯班,然后经过 12,000 公里的路程到达西雅图,美国西部 Azure 数据中的 Azure Function 会在发送响应之前查询数据库向西 12,000 公里到达 Cloudflare 的边缘节点,然后最后 80 公里到达我冲浪者天堂的家。但如果事情不必如此呢?如果该数据已经位于布里斯班的 Cloudflare 边缘节点上怎么办?巴黎的那个,井里的那个,我什至不确定那些蓝点在哪里,但如果它无处不在怎么办?将会发生一些很棒的事情:
- 您会更快地得到响应,因为我们刚刚缩短了 99% 以上的字节需要传输的距离。
- 由于流量遍历的节点少得多,而且当响应被缓存时,可用性将大大提高,我们不再依赖 Azure Function 或底层存储机制。
- 我们可以节省 Azure Function 执行成本、存储帐户命中率,尤其是出口带宽(非常昂贵)。
简而言之,将数据和处理推向“更接近边缘”对我们的客户和我们自己都有利。但是如何对 50亿个唯一的电子邮件地址做到这一点呢? (注:截至今天,HIBP 报告超过 140 亿个被泄露帐户,唯一电子邮件地址的数量低于平均水平,每个被泄露地址都出现在多次泄露中。)为了回答这个问题,让我们回顾一下数据的查询方式:
- 通过网站首页。这会触发“统一搜索”API,该 API 接受电子邮件地址并使用 Cloudflare 的 Turnstile 来禁止并非源自浏览器的自动请求。
- 通过公共 API 。该端点还接受电子邮件地址作为输入,然后返回它出现的所有违规行为。
- 通过 k-anonyity 企业 API 。该端点由 Mozilla 和 1Password 等少数大型订阅者使用。它不是通过电子邮件地址进行搜索,而是实现 k-匿名并通过哈希前缀进行搜索。
让我们进一步深入研究最后一点,因为它是整个缓存模型如何工作的秘诀。为了向该服务的订阅者提供所搜索电子邮件地址的完全匿名性,传递到 API 的唯一数据是完整电子邮件地址的 SHA-1 哈希值的前六个字符。如果这听起来很奇怪,请阅读最后一个要点中链接的博客文章以获取完整的详细信息。不过,目前重要的是,这意味着总共可以向 API 发出 16^6 个不同的可能请求,刚刚超过 1600 万个。此外,我们可以将上面的前两个用例转换为服务器端的 k-匿名搜索,因为它只涉及对电子邮件地址进行哈希处理并获取前六个字符。
总之,这意味着我们可以将整个可搜索的电子邮件地址数据库归结为以下内容:
- AAAAAA
- AAAAB
- AAAAC
- …大约 1600 万个其他值…
- FFFFFD
- FFFFFE
- FFFFFF
这是一个虽然有限但很大的列表,这就是我们现在缓存的内容。因此,通过电子邮件地址搜索如下所示:
- 搜索地址:[email protected]
- 完整 SHA-1 哈希值:567159D622FFBB50B11B0EFD307BE358624A26EE
- 六字符前缀:567159
- API端点:https://[主机]/[路径]/567159
- 如果哈希前缀已缓存,则从那里检索结果
- 如果hash前缀没有缓存,则查询origin并保存到缓存
- 返回结果给客户端
K-匿名搜索显然直接进入第四步,跳过前几步,因为我们已经知道哈希前缀。所有这些都发生在 Cloudflare 工作线程中,因此它是“边缘代码”创建哈希值、检查缓存,然后在必要时从源进行检索。该代码还负责处理转换查询的参数,例如按域过滤或截断响应。这是一个美丽、简单的模型,完全独立于一个工作线程和一个非常简单的原始 API 中。但有一个问题 – 当数据发生变化时会发生什么?
有两种事件可以更改缓存数据,一种是简单的,一种是主要的:
- 有人选择退出公共搜索,他们的电子邮件地址需要被删除。这很简单,我们只需调用 Cloudflare 上的 API 并刷新单个哈希前缀即可。
- 加载了新的数据泄露,并且大量哈希前缀发生了变化。在这种情况下,我们刷新整个缓存并从头开始再次填充它。
第二点有点令人沮丧,因为我们已经建立了这个美丽的数据集合,所有数据都靠近消费者,查询速度非常快,然后我们将其全部删除并从头开始。问题是要么是这样,要么我们有选择地清除了可能有数百万个单独的哈希前缀,这是你无法做到的:
对于企业计划区域,您可以在一次 API 调用中清除最多 500 个 URL。
和:
缓存标签、主机和前缀清除的速率限制为每 24 小时内 30,000 次清除 API 调用。
我们正在进一步考虑这一切,但这是一个不平凡的问题,完整的缓存刷新既容易又(接近)瞬时。
话不多说,让我们来几张照片吧!以下是对企业 k-anonymity API 的典型一周查询:
这是一种非常可预测的模式,很大程度上是由于某个特定订户每天定期查询其整个客户群。 (旁注:我们的大多数企业级订阅者都使用回调,这样当新的违规行为影响到他们的客户时,我们就会通过 Webhook 向他们推送更新。)这是入站请求的总量,但真正有趣的是命中源的请求(蓝色)与 Cloudflare 直接提供的服务(橙色):
让我们以图表末尾的最低蓝色数据点为例:
当时,96% 的请求都是由 Cloudflare 边缘处理的。惊人的!但稍后再看一下:
就在那时,我针对Finsure 漏洞刷新了缓存,100% 的流量开始定向到源头。 (我们仍然通过 Cloudflare 看到 14.24k 次点击,因为不可避免地,该 1 小时块中的一些请求是针对相同的哈希范围并由缓存提供的。)然后,缓存花了整整 20 个小时才重新填充到命中率:未命中率恢复到 50:50 左右:
回顾图表的开头,您可以看到与我加载DemandScience 漏洞时相同的模式。这一切都对我们的原始 API 做了非常时髦的事情:
最后那一次突然的增加,瞬间流量增加了30倍还多!如果我们不小心管理原始基础设施,我们就会构建一个真正的 DDoS 机器。 Stefán 稍后会写到我们如何管理底层数据库以确保这种情况不会发生,但即便如此,当我们处理上面第一张图中看到的周期性支持模式时,我知道加载违规的最佳时间澳大利亚下午晚些时候,交通流量是早上的三分之一。这有助于平滑对源的请求速率,以便在流量增加时,可以直接从 Cloudflare 返回更多内容。您可以在上图中看到这一点;尽管同一时间段内第一个图表的入站流量显着增加,但最后一个图表末尾的那个大峰值块非常稳定。这就像我们试图通过在缓存中建立一个 bugger 来应对不断增加的入站流量。
这是整件事的另一个角度:现在,加载数据泄露比以往任何时候都更需要我们花钱。例如,在上图末尾,我们的缓存命中率达到了 50%,这意味着我们只需支付一半的 Azure Function 执行、出口带宽和底层 SQL 数据库费用否则。刷新缓存并突然将所有流量发送到源头会使我们的成本增加一倍。等到我们恢复到 90% 的缓存比率时,刷新时的成本实际上会增加 10 倍。如果我在经济上完全无情的话,我需要加载更少的漏洞或将它们集中在一起,这样缓存刷新无论如何都只会弹出少量数据,但显然,这不是我一直在做的事情?
美中不足的是只剩下一只苍蝇了……
在这三种查询电子邮件地址的方法中,第一种是显而易见的:从网站首页进行搜索会到达 Cloudflare Worker,在其中验证 Turnstile 令牌并返回结果。简单的。但是,后两个模型(公共 API 和企业 API)需要根据 Azure API 管理 (APIM) 验证 API 密钥的额外负担,并且唯一存在的位置是美国西部的源服务。对于这些端点来说,这意味着在我们可以从距离可能只有很短的摩托艇路程的位置返回搜索结果之前,我们需要一路走到世界的另一端来验证密钥并确保请求是在速率限制之内的。我们以尽可能轻的方式执行此操作,几乎没有任何数据传输请求以检查密钥,此外,如果数据尚未在缓存中,我们会从源服务拉回数据,从而异步执行此操作。换句话说,我们的效率已达到人类所能达到的最高水平,但我们仍然面临着巨大的延迟负担。
在源头进行 API 管理非常令人沮丧,但实际上只有两种选择。第一个是将我们的 APIM 实例分发到其他 Azure 数据中心,问题是我们需要该产品的 Premium 实例。我们目前在 Basic 实例上运行,这意味着我们正在讨论将价格提高 19 倍以解锁该功能。但这只是高级版;然后,我们在其他地方至少需要一个实例才能使这一点有意义,这意味着我们正在谈论 28 倍的增长。我们添加的每个区域都会进一步放大这一点。这在财务上是不可能的。
第二个选项是 Cloudflare 构建 API 管理产品。这是这个难题的杀手锏,因为它将所有制衡放在一个边缘节点内。这是我现在在很多场合提出的建议,谁知道呢,也许它已经在酝酿之中,但这是我出于对公司所做的事情的热爱以及全力以赴拥有它们的愿望而提出的建议。控制我们的交通流量。本周我确实收到了一个关于在工作人员中实施有效的“穷人的 API 管理”的建议,这是一个非常酷的建议,但是当人们改变计划或当我们想要对 API 应用配额而不是速率限制时,它就会变得困难。所以,加油 Cloudflare,让我们实现这一目标!
最后,还有一个关于直接从边缘提供内容的强大功能的统计数据:我上个月分享了 Pwned Passwords 的这一统计数据,该统计数据服务了 Cloudflare 缓存储备中超过 99% 的请求:
就是这样 – 我们现在在 30 天内向 Pwned Password 发送了 10,000,000,000 个请求 ? 这是在@Cloudflare的支持下实现的,大规模边缘缓存数据,使其超级快速且对每个人都高度可用。 pic.twitter.com/kw3C9gsHmB
— 特洛伊·亨特 (@troyhunt) 2024 年 10 月 5 日
平均而言,每秒大约有 3,900 个请求,连续 30 天不间断。显然比巅峰时期要多得多;快速浏览一下上个月,几周前的一分钟内每秒大约有 17k 个请求:
但它有多高并不重要,因为我从来没有想过它。我设置了工作线程,打开了缓存保留,就是这样 ?
我希望您喜欢这篇文章, Stefán 和我将在澳大利亚东部标准时间周五早上 06:00 就该主题进行直播,以进行本周的定期视频更新,之后即可立即重播。为了方便起见,它也嵌入在这里: