分享

优化大型网站用户体验的先进预缓存方案

 黄爸爸好 2024-01-20 发布于上海

图片

有很多前端优化可以改善 FCP,但今天我想讨论的是拼图的另一部分以及我在探索过程中发现的我们的 HTTP 缓存策略的不足之处。

使用页面缓存的目的是为了提高 Time to First Byte(TTFB),但 FCP 取决于 TTFB。因此,对 TTFB 的改进也会影响其他下游指标。我要介绍的具体页面缓存策略是预缓存,也称为缓存预热或缓存加热,它在用户请求之前预先将页面元素放入缓存中。这是减少昂贵计算和对源服务器的压力的好方法,但一旦网站达到一定规模,就需要更多的考虑,而不仅仅是将所有内容保存在一个热缓存中。

一些背景

我主要在阅读为主的杂志风格的网站上工作。为了这次讨论的方便,有以下几点需要知道:

  • 在网站地图中包含最多 100 万个网址;

  • 使用 Cloudflare 进行代理;

  • 托管在 AWS 上。

网址数量包括了来自内容管理系统的自动生成的内容分类项目,例如作者页面、分类和标签。然而,还有很多网址是编辑内容,例如文章、论坛帖子和由编辑人员策划的专辑。

Mo' URLs,Mo'问题

对于任何网站来说,如果请求直接命中源服务器,特别是涉及到昂贵的查询和计算时,突然涌入的流量可能会导致数据库变得非常缓慢。一种常见的主动解决方案是预先缓存页面,使其在缓存中准备就绪,从而完全避免在源服务器上的工作。

然而,预先缓存也存在一些问题和权衡。最简单的策略是解析网站地图并批量预先缓存 URL。一些工具,如 WordPress 领域中的 optimus cache prime 或 W3 Total Cache,可以通过指向网站地图并抓取其中的每个 URL 来完成工作。大多数这类工具都允许你设置批量大小和抓取间隔,但以这种方式进行缓存意味着你可能会缓存一些不必要的内容。此外,如果批量大小或间隔过高,你将面临 CPU 抖动的风险,这将导致与突然涌入的流量命中源服务器时相同的效果。

另一个危险是预缓存错误的内容。在一个拥有许多 URL 的网站中,有些 URL 很少被访问,而其他一些可能非常受欢迎。该网站的主页可能比某个在 30 年前贡献了 1 篇文章的自动的生成作者个人资料页面获得更多的流量。就预缓存而言,主页更为重要。此外,你可能有一个在转化过程中非常关键的步骤,并不会有很多流量,但由于其重要性,你希望将其预缓存。

对于大型网站来说,爬虫速率也是一个问题。当我查看我雇主的预缓存解决方案的设置时,它仍然将爬虫速率设置为每 15 分钟处理 10 个 URL。对于一个拥有 100 万个 URL 的网站来说,这样的效果有点可笑,以这个速率,缓存预热机器人遍历整个站点地图将需要近 3 年的时间。当最后一个 URL 被预缓存时,最早的 URL 早已过期失效。

“预缓存一切”的策略可能非常适合拥有几百个 URL 且内容很少更改的较小网站,但一旦网站达到一定规模,这个策略就行不通了。

基于分析的预缓存

如果你使用 HTTP 缓存,很可能会有某种类型的缓存命中和未命中的日志记录。我之前提到过,这些网站是通过 Cloudflare 进行代理的,他们的分析 API 公开了有关流量、URL 和缓存状态的信息。换句话说,它是一个完美的数据源,可用于预缓存 URL。如果你不使用 Cloudflare,没关系,因为其他供应商也提供类似的 API 来公开这些信息,而且常用于缓存的软件包(如 Redis 或 Varnish)也提供类似的 API。由于数据来自 API,可以定期运行查询,将结果数据保存在持久层中,并爬取你想要预缓存的 URL。Cloudflare 的 API 恰好是 GraphQL,获取第一页结果的查询如下:

{ viewer { zones(filter: { zoneTag: $tag }) { httpRequestsAdaptiveGroups( orderBy: [count_DESC] limit: 250 filter: { date: $date, edgeResponseContentTypeName: 'html', cacheStatus_in: ['hit', 'expired', 'miss'] } ) { count data: dimensions { cacheStatus clientRequestPath edgeResponseContentTypeName } } } }}

在 Cloudflare 的术语中,zoneTag 是来自他们仪表板的 DNS 区域的数字 ID。我们根据事件计数来排序查询结果,并仅返回内容类型为 HTML 且缓存状态为“hit”(在缓存中具有有效的 TTL)、“expired”(在缓存中但已过期 TTL)或“miss”(不在缓存中且从源服务器提供)的 URL。date是前一天的动态变量。

我们只想返回符合预缓存目的的 URL,因此状态仅限于 hit、expired 和 miss。此外,我们希望将请求限制为 HTML,因为像图像、CSS 和 JS 这样的静态资产具有相对较长的 TTL,并且不涉及像请求动态 HTML 页面那样的数据库操作。

生成的数据将如下所示:

{  'data': {    'viewer': {      'zones': [        {          'httpRequestsAdaptiveGroups': [            {              'count': 6130,              'data': {                'cacheStatus': 'miss',                'clientRequestPath': '/',                'edgeResponseContentTypeName': 'html'              }            },            {              'count': 5310,              'data': {                'cacheStatus': 'hit',                'clientRequestPath': '/',                'edgeResponseContentTypeName': 'html'              }            },            {              'count': 1610,              'data': {                'cacheStatus': 'expired',                'clientRequestPath': '/',                'edgeResponseContentTypeName': 'html'              }            },            {              'count': 930,              'data': {                'cacheStatus': 'miss',                'clientRequestPath': '/sponsoredpost/2023/10/03/foster-positive-client-relationships',                'edgeResponseContentTypeName': 'html'              }            },            {              'count': 660,              'data': {                'cacheStatus': 'miss',                'clientRequestPath': '/howto',                'edgeResponseContentTypeName': 'html'              }            }          ]        }      ]    }  },  'errors': null}

我们可以将这些结构化数据插入到数据库中,然后对路径进行聚合和查询。由于一些源站点上的内容类型不正确,可能会出现一些误报,因此最好在将数据写入数据库之前处理好这个逻辑。例如,当我运行这个查询时,我发现一些 JSON 端点被错误地使用 content-type: text/html; charset=UTF-8 来提供,而不是正确的 application/json 值。

缓存分布

如果你使用 Cloudflare,HTTP 缓存是本地化到数据中心的。这意味着,如果你有一个在 IAD 数据中心中缓存的请求,而有人在 ORD 数据中心中访问,该请求不会被认为在那里有缓存,需要被获取。

为了处理跨多个地区的预缓存,你可以使用 AWS Simple Notification System(SNS)并部署 Lambda 函数到战略地区。SNS 可以将跨地区事件分发到 Simple Queue Service(SQS)或 Lambda 函数,因此预缓存的 URL 的爬取可以在多个地区复制。一开始有一些担忧关于 AWS 地区与 Cloudflare 数据中心之间可能存在的不匹配问题,但是 Cloudflare 已经为此提供了解决方案,即他们的 分层缓存。然而,你应该测试和监控缓存命中率,以确保获得预期的结果。

我们希望发布到 SNS 进行预缓存的 URL 可以通过设置参数来确定,例如查询过去两周内每个 URL 的“过期”和“未命中”状态的总和,并按照浏览次数排序。这样可以考虑到流量趋势,并将可预缓存的 URL 列表缩小到可管理的大小。

总体流量如何?

最受欢迎的 URL 可以被视为一种特殊情况。在缓存世界中,存在一种称为缓存奔溃(cache stampede),也称为 thundering herd 或 dog piling 的问题,即同时涌入未在缓存中或已过期的 URL 的流量。这种流量涌入的结果是,在响应被写入缓存之前发生请求,导致相同(可能昂贵的)计算资源的多次调用原始来源。

作为缓存奔溃的对策,有一些内容始终希望保存在缓存中。一种处理 Web 流量的方式是在其 TTL 到期之前主动替换缓存项。你可以通过在我们的解决方案中在 Lambda 函数和 SNS 之间添加 SQS 来将这些特殊 URL 视为优先级队列。

SNS 不再直接发送通知到 Lambda,而是将通知发送到高优先级或标准优先级的 SQS 队列中,以便优先处理 URL,而不是前面部分中描述的其他数据集中的 URL。每个队列后面都有一个 Lambda 函数,仍然负责处理消息,就像以前一样。

进一步的改进

我们从一个无效的预缓存策略开始,现在有了一个范围更窄但更有效的解决方案。然而,到目前为止,我们的解决方案存在一个潜在的问题,即它基于过去的数据。在某些情况下,使用历史数据来预测流量模式可能会更好。

目前看来,很多人都在尝试将人工智能集成到他们的应用程序中,我认为训练一个人工智能模型来进行流量模式的预测分析是很有意义的。流量可能具有季节性,因此你可能希望对预缓存的内容进行积极主动的处理,这就是预测模型可以提供极大帮助的地方。例如,美国的感恩节是 11 月的第四个星期四,传统的餐食是火鸡。如果你等待历史数据,你可能会错过一个超级受欢迎的食谱帖子的预缓存窗口,因为很多人会在感恩节当天查看该食谱,而数据收集会滞后一天。

如果有一个预测模型,它将有助于确定在特定日期附近可能受欢迎的 URL。通过将来自缓存分析 API 的信息存储在我们自己的数据库中,这样就了灵活度,如果我们想要采取这条路线,可以采纳这个选项。

原文链接:

https://calendar./2023/informed-pre-caching-strategy-for-large-sites

声明:本文为 InfoQ 翻译

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多