我在自己的网站上添加了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”这一免费健康与健身计算工具平台背后的技术实现细节——其中也包括那些并未产生预期效果的优化措施。
相关文章
Cloudflare推出了用于实现源站后缓存控制的缓存响应规则
Cloudflare最近推出了“缓存响应规则”这一功能——这种规则引擎会在源服务器做出响应之后、但在内容被写入Cloudflare的缓存系统之前开始发挥作用。此前,缓存规则仅能针对请求的相关属性进行操作;而“缓存响应规则”则增加了在内容被缓存之前对其响应内容进行评估的环节。 作者:雷纳托·洛西奥
阅读全文
了解人工智能软件开发生命周期流程——构建智能代理功能的完整指南
也许你可以理解这样的场景:本周,你用了同样的说明四次向别人解释人工智能模型的使用方法。 你反复讲解过团队是如何构建演示文稿的框架的,哪些检查步骤需要在部署之前完成,以及为什么测试数据库并不是文档中提到的那个。 每次你都要把这些内容重新写一遍,每次智能助手也能完成得不错,但每次新的会话开始时,一切都得从零开始。 而这正是 智能助手技能 所要解决的问题。 技能 实际上就是一个文件夹,其中只包含一个名为 Skill.md 的文件。智能助手在启动时会阅读其中的一行总结内容,而只有当真正需要时才会打开完整的说明文件。你只需把解释内容编写一次,将其与代码一起提交,那么团队中的每个智能助手就能访问这些信息,
阅读全文
使用OpenTelemetry实现Claude Code的可观测性
像 Claude Code 、 OpenAI Codex 、 Google Antigravity 以及 Cursor 这样的代理编码工具,在日常软件开发中已经变得无处不在。 随着代理系统的不断发展,开发者让这些系统完成的大部分工作都是通过逐个分配子任务来实现的。许多团队也在探索并使用共享的、多租户式的代理基础设施,这种架构的成本不会与某个特定的所有者挂钩。在这种情况下,可观测性就成为了监控基础设施成本的关键因素。 在本指南中,您将了解可观测性的工作原理,然后学习如何启用Claude Code内置的遥测功能,运行后端程序来收集数据,并读取该系统生成的各类指标、日志及追踪信息。这些内容将帮助您更
阅读全文
人工智能工程在实践中的应用:人工智能工程师与现场部署工程师如何利用Claude Code、Codex及Gemini来进行开发工作
METR )。 DORA )。 Stack Overflow )。 Anthropic )。 目录 先决条件 什么是AI原生SDLC? 当代码开发成本降低后,瓶颈会出现在哪里? Claude Code、Codex与Gemini CLI:同一个框架,三种不同的工具/术语体系 如何开展规划阶段的工作 如何开展设计阶段的工作 如何开展构建阶段的工作 如何开展测试阶段的工作 如何开展部署阶段的工作 如何开展维护阶段的工作 一名工程师如何负责一个五人的团队 规划阶段 设计阶段 构建阶段 测试阶段 部署阶段 维护阶段 在全面采用AI原生SDLC之前,需要检查的事项清单 结论 接下来应该探索什么 先决条件
阅读全文