← 返回蜂巢洞察

我在自己的网站上添加了CDN缓存,结果网站的加载速度反而变慢了。其实我应该先进行一些计算或分析才采取这个措施。

上周,我在一个静态网站前面加入了CDN缓存,本以为这样会让访问速度变快。 然而结果却恰恰相反——访问速度明显变慢了。这种变化非常明显:在调整之前,有一个独立的爬虫工具检测到了38个页面加载缓慢;而在调整之后,这个工具检测到的缓慢页面数量增加到了 75个 。 我在当天就恢复了原来的设置。下面我将说明到底发生了什么、为什么会出现这种结果,以及如果我在部署之前进行某个计算,就能避免这个错误。 目录 先决条件 设置过程 我做了哪些更改 最终结果 为何会适得其反,第一部分:犯错是要付出代价的 为何会适得其反,第二部分:爬虫工具的反应总是迟缓的 我本应该先进行的计算 那个令人后悔的决定 一个值得保留的方法

上周,我在一个静态网站前面加入了CDN缓存,本以为这样会让访问速度变快。

然而结果却恰恰相反——访问速度明显变慢了。这种变化非常明显:在调整之前,有一个独立的爬虫工具检测到了38个页面加载缓慢;而在调整之后,这个工具检测到的缓慢页面数量增加到了75个。

我在当天就恢复了原来的设置。下面我将说明到底发生了什么、为什么会出现这种结果,以及如果我在部署之前进行某个计算,就能避免这个错误。

目录

先决条件

你之前不需要使用过CDN服务。但如果你对以下内容有所了解,那么理解这个过程会更容易:

  • CDN的基本功能:它会在全球各地的服务器上保存你的网页副本,然后为每位访问者从最近的服务器提供这些页面。

  • 基本的HTTP缓存头信息:了解Cache-Control和max-age的作用是非常重要的。在相关内容中,我还会解释s-maxage和must-revalidate的含义。

  • 如何使用curl命令进行测试:本文中的所有测试数据都是通过运行一行curl命令获得的,你可以用它来检测自己的网站。

  • 这些内容并不需要特定的Cloudflare知识,也不需要安装任何软件。核心在于一些基本的计算原理,而这些原理适用于任何CDN服务。

    设置过程

    这个网站是使用Astro构建工具制作的静态网站,包含77个HTML页面,没有服务器端渲染功能,也没有数据库。它通过Cloudflare的免费计划进行部署,源服务器位于印度孟买纳维区。该网站的访问量相当低,每天大约只有9位访客。

    根据Google Search Console的数据,这个网站的平均响应时间为711毫秒。而Google建议的理想响应时间是200毫秒,这意味着当前的情况严重超出了标准范围,问题看起来也很明显。

    原因其实很简单:Cloudflare并没有对这些HTML页面进行任何缓存处理:

    $ curl -sI https://example.com/ | grep -i 'cache-control|cf-cache-status'
    cache-control: no-cache
    cf-cache-status: DYNAMIC
    
    DYNAMIC表示Cloudflare根本没有对响应内容进行缓存。所有HTML请求——无论来自哪些访问者或爬虫工具——都会被发送到孟买。

    源服务器设置了Cache-Control: no-cache这一头部信息,这是我之前有意做出的选择,这样部署后的更改就能立即被用户看到。静态资源(如JS文件、CSS样式、字体和图片)是可以正常被缓存的,只有HTML内容才会不被缓存。

    因此,由于源服务器距离较远,HTML内容无法被缓存,从而导致响应速度变慢。如果在边缘节点对HTML内容进行缓存,这个问题就能得到解决,对吧?

    我所做的更改

    我按以下顺序进行了两项修改。

    1. 修改源服务器的头部信息。将Cache-Control: no-cache改为:

    Cache-Control: public, max-age=0, s-maxage=31536000, must-revalidate

    这是一个值得了解的有用设置。max-age=0, must-revalidate意味着浏览器会在每次请求时都重新验证缓存内容,因此部署后的更改会立即显示给用户,这正是no-cache设置想要实现的效果。而s-maxage仅适用于共享缓存机制,这样CDN就可以将资源缓存一年时间,而浏览器则会继续进行验证。

    2. 配置Cloudflare的缓存规则。有一个细节让我感到意外:仅仅修改头部信息是远远不够的。Cloudflare不会根据Cache-Control设置来自动缓存没有扩展名的HTML内容。我通过一个临时测试路径进行了验证,而不是直接假设这一设置有效:

    req 1: cf-cache-status: DYNAMIC
    req 2: cf-cache-status: DYNAMIC
    req 3: cf-cache-status: DYNAMIC

    即使将s-maxage设置为一年,五次请求的结果仍然都是DYNAMIC。因此,需要明确配置缓存规则,才能使响应内容被允许被缓存。这一设置其实非常实用,因为它意味着你可以在正式启用缓存功能之前,安全地测试头部信息的修改效果。

    在配置了相应的规则之后,效果果然如预期般理想:

    req 1: cf-cache-status: MISS
    req 2: cf-cache-status: HIT
    req 3: cf-cache-status: HIT

    页面的首字节加载时间也从大约0.28秒缩短到了大约0.17秒。

    我是从印度进行测试的,而源服务器也在印度。不过……先别急,还有后续结果。

    最终结果

    然而下一次爬取测试的结果却更糟了。还是同一个网站、同一天,还是那80个URL:

    。
    爬取时间 HTML缓存情况被标记为慢速的页面数量
    上午11:58 未启用缓存 38
    下午6:04 已启用缓存 75

    慢速页面的数量几乎增加了一倍。我原本试图改善的指标,反而变得更糟了。

    为何会适得其反,第一部分:错误的结果并非毫无代价

    我之前的认知是:缓存机制实际上就是两种结果之间的随机选择:一种是“命中”,这种情况下数据访问速度很快;另一种是“未命中”,这种情况下所消耗的资源与完全不使用缓存时是一样的。

    后半部分的解释有误。当发生缓存未命中的情况时,CDN并不会只是简单地将你的响应内容转发出去,它必须在进行数据传输的同时存储这些数据。而这样的操作必然会消耗一定的资源。

    我通过清除缓存,然后用两种方式来获取相同的页面内容来进行测试:一种是通过Cloudflare,另一种则是直接从原始服务器地址访问,并使用--resolve选项来绕过Cloudflare。

    
    # 通过Cloudflare访问,缓存未命中
    curl -s -o /dev/null -w '%{time_starttransfer}' https://example.com/page
    
    # 直接从原始服务器地址访问,完全不使用缓存
    curl -sk -o /dev/null -w '%{time_starttransfer}' --resolve example.com:443:203.0.113.10 https://example.com/page
    

    针对六页内容进行的测试结果显示:

    访问路径 首次请求所需时间
    缓存命中 0.239秒
    缓存未命中 0.366秒
    直接从原始服务器访问(无缓存) 0.281秒

    可以看出,当发生缓存未命中的情况时,请求所需时间会比完全不使用缓存的情况多出约85毫秒。这意味着,每次发生缓存未命中的情况,性能都会变差。

    为何会适得其反?第二部分:爬虫总是处于“冷状态”

    只有当相同的URL在仍然被缓存在同一台边缘服务器上时,使用缓存才会真正带来效率提升。

    然而,爬虫并不会重复访问相同的URL,它只会一次访问每个URL而已。

    我可以通过精确的实验来验证这一点,这个方法确实值得借鉴:如果某个请求是从CDN的缓存中获取到的,那么它就永远不会到达原始服务器。因此,原始服务器的访问日志可以直接反映出缓存未命中的情况。

    grep -ic "crawler-user-agent" /var/log/nginx/access.log

    在测试了80个URL之后,原始服务器记录了**82次请求。爬虫请求的每一个页面都导致了缓存未命中的情况,因此命中率为0%,而每一次这样的操作都会多消耗约85毫秒的时间。

    对于首次访问网站的用户来说,情况也是一样的——他们同样处于“冷状态”,也就是说他们的请求也会直接从原始服务器获取数据。

    我本应该先进行的计算

    下面是完整的计算过程,整个过程大约需要一分钟时间。

    你需要三个数值:

    • H:缓存命中的情况下所需的时间
    • M:缓存未命中的情况下所需的时间
    • B:不使用任何缓存时的平均响应时间

    只有当平均请求时间低于B时,使用缓存才会带来实际的好处:

    h × H + (1 − h) × M  <  B

    要计算命中率h,需要满足以下条件:

    h  >  (M − B) / (M − H)

    以我的数据为例:

    h > (0.366 − 0.281) / (0.366 − 0.239)
    h > 0.085 / 0.127
    h > 0.67

    我需要至少67%的缓存命中率才能达到收支平衡。

    现在来看看现实情况。文件总共有77页,每天大约只有9位访问者会浏览这些页面,此外还有搜索爬虫每隔几天就会重新获取这些页面的内容。也就是说,每天会有35次HTML请求被发送出去,这些请求分散在77个URL上,并且要经过全球各地的边缘服务器进行处理。而且每次进行部署时,都需要清除所有的缓存数据。

    实际上,这种方案的命中率几乎为零。而我原本需要的命中率却是67%。

    从一开始我就知道,这个方案根本行不通,但在编写配置代码之前,我却完全没有意识到这一点。

    问题所在

    诊断结果是正确的:服务器确实远离了大部分用户的访问路径,因此HTML文件并没有被缓存起来,导致页面加载时间长达711毫秒,这个数据确实是真实的。

    我只是盲目地选择了一个解决方案,却完全没有考虑过“究竟有多少请求会真正从中受益”这个问题。

    而我的验证操作反而使情况变得更糟,因为我在与服务器位于同一国家的笔记本电脑上进行了测试。本地的`curl`命令显示这个方案是有效的,但只有外部爬虫——也就是最初发现这个问题的那个工具——才显示出了问题依然存在。

    所以,请务必根据“最初引发问题的那个指标”来进行验证,而不要使用其他替代指标。

    一个值得保留的方法

    在这个方案中,有一部分内容在恢复原始配置后依然有效,而且确实非常有用。

    如果你在边缘服务器上设置了HTML缓存,那么在每次进行部署时,都必须清除这些缓存数据。但是,即使API返回“success: true”的响应,也不能说明缓存数据真的已经被清除了。因此,千万不要相信这个信号,必须亲自进行检查。

    LOCAL=$(shasum -a 256 dist/index.html | cut -d' ' -f1)
    LIVE=$(curl -s https://example.com/ | shasum -a 256 | cut -d' ' -f1)
    
    if [ "$LOCAL" != "$LIVE" ]; then
      echo "EDGE IS STALE — this deploy is not live" >&2
      exit 3
    fi
    

    这个命令会对比你的内容分发网络实际提供的文件内容和你刚刚生成的文件内容,从而判断缓存是否已经被清除。

    还有一个需要注意的问题:如果这两个哈希值永远都不匹配,那么首先应该怀疑是某些在边缘服务器上修改HTML内容的机制在起作用,比如脚本优化工具、电子邮件混淆功能或者自动压缩程序,而不是先考虑缓存问题。

    更好的做法是,不要默认认为缓存功能一定是开启的。应该通过查询来确认这一点:

    STATE=$(curl -sI https://example.com/ | tr -d '\r' | awk 'tolower($1)=="cf-cache-status:"{print $2}')
    
    case "$STATE" in
      DYNAMIC|BYPASS) echo "HTML isn't cached; a missing purge is harmless" ;;
      *)              echo "HTML IS cached — a failed purge means this deploy is invisible" ;;
    esac
    

    这样,当有人更改了缓存设置时,这个检查机制就能自动反映出变化,而不会导致问题持续存在。

    我建议你采取的做法

    在部署之前,先计算出你的盈亏平衡点对应的命中率。计算公式为:`(M − B) / (M − H)`。如果你无法达到这个目标,那就不要继续进行部署。

    要如实估计你的实际命中率:每天收到的请求总量除以所有URL的数量,再除以全球各地边缘服务器的处理能力,而且每次部署后这个数值都需要重新计算。通常情况下,实际的命中率会比你想象的要低很多。

    应该根据你的用户和爬虫实际所处的位置来设置缓存策略,而不是从位于你的服务器附近的机器出发来进行判断。

    需要记住的是,缓存机制实际上是一种“基于访问频率的奖励机制”:它只会为那些在短时间内被反复访问的网站带来好处。对于一个流量巨大的网站来说,这种机制显然会使其受益;而对于一个每天只有9位访客的网站而言,这种机制根本不起作用。

    如果你的服务器位于远离目标用户群的地方,而且运行速度很慢,那么诚实的解决办法可能根本不是使用缓存,而是将服务器位置迁移到更接近用户的地方。

    我撰写的内容涉及“Healthy Calculator Hub”这一免费健康与健身计算工具平台背后的技术实现细节——其中也包括那些并未产生预期效果的优化措施。

相关文章

技术实践

如何打破人工智能编程代理的循环机制

你很可能已经看过这样的情景:在使用人工智能编码工具构建的应用程序中,出现了某些故障。你让这个工具去修复这些问题,它似乎也“修复”了它们,但实际上错误依然存在,或者又出现了新的问题。 于是你会说“还是不行”。工具会再次尝试修复。二十分钟后,你会发现代码中的错误更多了,可用的功能也更少了,而且完全不知道该如何让应用程序恢复到正常状态。 人们常常用“人工智能反而使问题更加严重”或者“工具陷入了无限循环的修复模式”这样的表述来描述这种情况。这并不是某个特定产品的缺陷,像Replit、Cursor、Claude Code、Base44这类工具都会出现同样的问题,因为这种故障的本质是系统性的。 本教程将解

阅读全文
技术实践

如何利用Claude API构建一个可靠的人工智能助手

大型语言模型能够回答问题、总结文档、编写代码,还能与外部系统进行交互。但构建一个可靠的人工智能应用,仅仅发送指令并显示响应是远远不够的。 一个可用于实际生产的应用程序必须能够管理对话历史记录、提供相关的上下文信息、安全地使用各种工具、处理不同类型的响应,并判断生成的内容是否有用。 在本教程中,我们将构建 ShopHelper ——这个为虚构在线商店设计的客户支持助手。完成制作后,ShopHelper将能够做到以下几件事: 以统一的语气回答常见问题 记住顾客之前说过的话 通过调用代码中的函数来查询订单状态 安全地处理Claude给出的多段式响应 利用工作流程来处理客户支持请求 评估修改提示语后效

阅读全文
技术实践

Cloudflare新增了可配置的HTTP Vary头部功能,以此防止缓存冲突现象的发生。

Cloudflare在其边缘网络中实现了对HTTP Vary头的原生支持,这一功能提升了所有客户计划的缓存管理效果。该特性有助于更有效地协商内容呈现方式,同时最大限度地减少缓存碎片化现象。运营商可以为请求头定义特定的行为规则,从而更好地控制缓存效率并降低服务器负担。 作者:Olimpiu Pop

阅读全文
技术实践

无状态MCP消除了在AWS服务器部署过程中对会话亲和性的要求

AWS详细说明了最新的Model Context Protocol规范是如何取消对远程MCP服务器而言所需的协议级会话机制、粘性会话功能以及会话存储功能的。这一变更使得请求路由能够更加独立地进行,同时也简化了系统的水平扩展流程;而与应用状态管理、重试机制、可观测性以及函数幂等性相关的问题,则被转移到了其他层次来处理。 作者:Leela Kumili

阅读全文