一、问题初现:命中率数字不好看
前一阵子,我负责的一个视频网站总是出现加载慢的情况。打开监控面板一看,CDN的缓存命中率只有百分之六十多,离预期的百分之九十差了一大截。很多用户反馈说看视频的时候老转圈,尤其是晚上八点到十一点的高峰期,卡得没法看。我赶紧拉上运维同事一起查,一开始以为是源站带宽不够,结果看了下源站流量,发现回源请求多得吓人,明明很多热门视频是重复请求的,却每次都穿透到源站去拿数据,这才意识到问题核心是CDN缓存没运作好。
其实说白了,就是我们没有把资源“喂”到CDN节点上,也没能根据实际的请求热度动态调整回源的方式,导致节点上该有的内容没有,不该回源的疯狂回源。如果我们只是干等着CDN自动去缓存,效果往往很被动。于是我们决定做一次完整的调优,把预热调度和回源策略都重新梳理一遍。
二、先把概念说人话
2.1 命中率和回源是什么
CDN缓存命中率,简单理解就是用户要的数据,CDN节点上有没有。有,就直接从节点返回给用户,这叫“命中”。没有,CDN节点就得去你的源站服务器把数据拿过来,再返回给用户,同时自己存一份,这叫“回源”。命中率就是命中的次数占总请求次数的比例。打个比方,你去食堂打饭,如果菜已经摆好了,你盛完就走,这就是命中;如果没菜了,厨师现去后厨炒,你得等半天,这就是回源。命中率高,说明大多数时候菜都是现成的,速度自然快。
2.2 为什么命中率低了会影响钱和速度
回源越多,你的源站服务器压力就越大。源站带宽是按量付费的,回源流量一大,账单蹭蹭往上涨。更重要的是,回源延迟很高,用户访问慢,体验就差。即使CDN厂商的节点遍布全国,从边缘节点回源站这一步,往往要走很长的链路,可能跨地域甚至跨运营商,所以耗时卡在这里。命中率低,等于把CDN的加速作用废掉了一大半。
三、排查思路:先看数据再动手
3.1 拉取监控数据
调优不能拍脑袋,先把数据拿到手。我们用CDN厂商提供的API拉取了最近七天的命中率、回源流量、热门URL排行等指标。为了方便展示,我用命令行的方式把数据存成了JSON文件。技术栈是Node.js,后面所有的脚本也都是Node.js。
# 模拟拉取CDN监控数据并保存到文件
curl -s "https://api.cdn.example.com/v1/metrics?type=hit_ratio&days=7" \
-H "Authorization: Bearer your_token" \
-o cdn_metrics.json
# 查看文件内容
cat cdn_metrics.json
大家可能没有真实的CDN API权限,但没关系,我们可以用Node.js模拟一份数据,方便理解后续的处理逻辑。
// 模拟最近7天每小时命中率数据
const data = [];
for (let day = 1; day <= 7; day++) {
for (let hour = 0; hour < 24; hour++) {
// 模拟白天和晚上的命中率差异:晚上低,早上高
const base = hour >= 20 || hour <= 2 ? 60 : 80;
// 随机波动
const ratio = base + Math.floor(Math.random() * 10);
data.push({
day: day,
hour: hour,
hitRatio: ratio,
originTraffic: Math.floor(1000 + (100 - ratio) * 50) // 回源流量与命中率反相关
});
}
}
// 输出成JSON文件
console.log(JSON.stringify(data, null, 2));
3.2 分析日志看规律
拿到数据后,我们进一步看了CDN的访问日志。发现几个特点:
第一,每天同时段的热门视频,比如正在热播的电视剧,会有大量用户请求,但这些视频并没有被提前缓存到节点上,所以第一波用户总是需要回源。第二,一些老视频,按理说不会再热门了,但是缓存过期时间设置太短,导致CDN频繁回源刷新。第三,部分URL带了随机的参数,比如视频播放地址会带上用户token,这样即使内容一样,CDN也会认为是不同的文件,导致缓存无法命中。
这里最坑的就是URL带随机参数。我们后来在回源策略里做了处理,算是一个小坑。下面详细说说优化过程。
四、预热调度:把数据提前送到门口
4.1 什么资源需要预热
预热,就是在用户请求之前,主动把资源从源站拉取到CDN节点上。不是所有资源都适合预热,比如刚上传的几百G视频,全部预热很耗时间和带宽。我们只预热两类资源:
- 即将上新的热门内容:比如平台自制剧集,播出前半小时开始预热。
- 已知的高频资源:根据历史数据统计出的TOP 1000热门文件,每天凌晨定时预热。
有人会问,为什么不等用户访问了再缓存呢?因为第一波用户很关键,如果让他们等待回源,体验就会不好,而且回源压力大。预热相当于“先派个专人把菜端上桌”,用户来了直接拿。
4.2 写一个预热调度脚本(Node.js)
我们写了一个Node.js脚本,读取一个待预热URL列表,然后调用CDN预热API。这个脚本可以挂在定时任务里,也可以在发布新内容的时候手动执行。
// preheat.js — 预热调度脚本
// 环境:Node.js 14+,需要安装 axios (npm install axios)
const axios = require('axios');
// CDN厂商的预热API地址(示例)
const PREHEAT_API = 'https://api.cdn.example.com/v1/preheat';
// 假设我们是热播剧《代码人生》的第1-10集
const urls = [];
const baseUrl = 'https://media.example.com/episodes/code-life';
for (let i = 1; i <= 10; i++) {
urls.push(`${baseUrl}/S01E${i.toString().padStart(2, '0')}.mp4`);
}
// 额外的热门资源,比如首页推荐位上的视频
const hotUrl = 'https://media.example.com/hot/recommended-video.mp4';
urls.push(hotUrl);
// 设置请求超时时间,避免卡死
axios.defaults.timeout = 10000;
async function preheat(urlList) {
console.log(`准备预热 ${urlList.length} 个资源`);
// 把URL分成每批50个,防止一次请求太多
const batchSize = 50;
for (let i = 0; i < urlList.length; i += batchSize) {
const batch = urlList.slice(i, i + batchSize);
try {
// 调用预热接口,body中带上URL数组
const resp = await axios.post(PREHEAT_API, {
urls: batch,
// 指定预热的区域,比如华北、华东,可以让资源更贴近用户
areas: ['north-china', 'east-china']
}, {
headers: {
'Authorization': 'Bearer your_preheat_token'
}
});
// 打印本次批量预热的结果
console.log(`批次 ${Math.floor(i / batchSize) + 1} 完成,任务ID: ${resp.data.taskId}`);
} catch (err) {
// 如果一批失败,记录失败信息,不要中断整个脚本
console.error(`批次 ${Math.floor(i / batchSize) + 1} 失败: ${err.message}`);
// 这里可以补充重试逻辑,比如重试3次
}
// 每批之间暂停100毫秒,避免触发API限流
await delay(100);
}
console.log('预热任务全部提交完成');
}
// 一个简单的延迟函数
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 执行预热
preheat(urls);
执行的时候很简单:
# 运行预热脚本
node preheat.js
这个脚本本质上就是把我们想预热的URL告诉CDN,CDN会自己调度节点去源站拿内容。需要注意的是,预热不是立刻完成,通常需要几十秒甚至几分钟,取决于文件大小和节点数量。我们在脚本里只是提交任务,实际生效要等一会儿。
用了预热之后,第一波用户的回源率明显下降。但是,光预热还不够,因为总有新的内容出来,而且有些资源是长尾的。这时候还得靠回源策略的调优。
五、回源策略动态调优
5.1 回源策略有哪些
回源策略,决定了一个请求没有命中CDN时,CDN节点如何处理。常见的有两种:
- 回源到源站:直接去你的服务器拿数据。
- 回源到上级节点:比如多级CDN架构中,边缘节点先找中间层节点要,要不到再找源站。这样能减少源站压力。
除了回源路径,还有一些参数会影响命中率,比如缓存过期时间(TTL)、是否忽略URL参数、是否开启过滤Cookie等。我们这次优化主要做了两件事:一是对带随机参数的URL开启“忽略参数缓存”,二是在业务高峰时临时调长TTL,减少回源频率。
5.2 根据实时命中率调整回源权重(Node.js示例)
这里要说一个我们遇到的坑。我们一部分流量走了另一个备用源站,两个源站的文件版本可能不一致。CDN回源时如果轮询到版本较旧的源站,就会拿到旧文件,于是我们想通过动态调整回源权重来保证稳定。我们写了一个小的动态调度器,每隔五分钟调用一次监控API,拿到命中率之后,如果低于阈值,就提高主源站的权重,让更多请求回主源站获取最新文件,同时减少回源总次数。
// dynamic-origin.js — 动态回源权重调整
// 环境:Node.js 14+,axios
const axios = require('axios');
// 配置两个源站的ID和初始权重
const origins = [
{ id: 'origin-main', weight: 80 }, // 主源站,文件最新
{ id: 'origin-backup', weight: 20 } // 备用源站,成本较低
];
const THRESHOLD = 75; // 命中率低于75%时触发调整
const CHECK_INTERVAL = 5 * 60 * 1000; // 检查间隔:5分钟
// 模拟获取当前命中率,实际中应该从CDN监控API拉取
async function fetchCurrentHitRatio() {
// 假设我们请求监控API,这里直接返回模拟值
const mock = await axios.get('https://api.cdn.example.com/v1/realtime-metrics/hit-ratio', {
headers: { 'Authorization': 'Bearer your_token' }
});
// 实际API返回的数值字段名需要自己看文档
return mock.data.hitRatio;
}
// 动态调整权重的函数
async function adjustOriginWeights() {
const currentRatio = await fetchCurrentHitRatio();
console.log(`当前命中率: ${currentRatio}%`);
// 如果命中率低,说明回源太多,我们需要提升主源站的权重,让回源更快拿到最新内容
// 同时可以把一部分次要文件彻底交给CDN缓存,减少回源
if (currentRatio < THRESHOLD) {
// 把主源站权重提高10%,备用源站相应降低
origins[0].weight = Math.min(origins[0].weight + 10, 95);
origins[1].weight = 100 - origins[0].weight;
console.log(`命中率低了,调整权重:主源站 ${origins[0].weight}%,备用源站 ${origins[1].weight}%`);
// 调用CDN API下发权重配置
await axios.post('https://api.cdn.example.com/v1/origin/weight-config', {
origins: origins
}, {
headers: { 'Authorization': 'Bearer your_token' }
});
} else {
// 命中率正常,我们保持当前配置不变
console.log('命中率正常,不调整权重');
}
}
// 启动定时任务
async function main() {
await adjustOriginWeights();
// 每5分钟检查一次
setInterval(adjustOriginWeights, CHECK_INTERVAL);
}
main().catch(err => {
console.error('调度器出错:', err);
process.exit(1);
});
这个脚本虽然简单,但很有用。我们把它部署在服务器上,配合云函数的定时触发,实现了“命中率一旦下降,自动调整权重”的效果。配合之前做的预热,回源量下降了很多。
另外,关于URL参数的忽略策略,我们也在CDN控制台里做了配置。比如视频播放器会带上?userId=123&playToken=abc,这些参数不影响内容,就开启“忽略参数”缓存。但是要注意,某些URL的参数是版本控制用的,比如?v=20231001,如果忽略参数,旧版本的内容就不会被更新。所以我们要精细化配置,只对特定路径生效。这里给出一个CDN配置的示例片段,大家参考一下。
{
"cacheRules": [
{
"path": "/episodes/*",
"ignoreQueryString": true,
"ttl": 86400
},
{
"path": "/hot/*",
"ignoreQueryString": false,
"ttl": 3600
}
]
}
上面的配置意思是,对于/episodes/路径下的视频,忽略查询参数,缓存一天;对于/hot/下的推荐资源,不忽略参数,缓存一小时。这样既能兼顾内容更新,又能提升命中率。
5.3 预热与回源之间的配合
预热和回源不是孤立的。我们的经验是,用预热解决“已知热点”的第一波冲击,用回源策略控制“未知热点”的缓存效率。比如某天有个视频突然被网友转发了,我们原本没有预热它,但是CDN看到请求量上来,会自动缓存。如果此时回源策略中设置了合理的TTL,那么后续的请求就能命中。所以我把这两个手段当成一套组合拳。
六、优化效果与注意事项
6.1 效果对比
调优之后,我们观察了一周的数据。CDN命中率从原来的百分之六十几,稳定提升到了百分之九十二左右。源站峰值带宽下降了约百分之六十,月账单也省了不少。用户侧反馈说,晚上黄金时间卡顿几乎没有出现,顶多偶尔加载一两秒。这个结果让我们很满意。
这里要强调,命中率提升不是一劳永逸的。因为资源热度会变化,新的热门内容不断出现,老旧内容会过期,如果不持续维护,命中率还会掉回去。
6.2 注意事项
- 不要过度预热:把所有资源都塞给CDN,不仅浪费时间,还会占用源站和CDN的带宽。预热前一定要用数据分析哪些是真正高热的资源。
- 缓存过期时间不要一刀切:静态大文件可以设置长TTL,但动态接口类型的资源,比如用户信息,不能缓存太久,否则数据会不准确。我们只对纯静态资源做长缓存。
- 注意URL参数过滤的风险:忽略参数会提升命中率,但也会导致不同内容指向同一个缓存文件。一定要确认参数不影响内容语义。
- 回源权重调整要谨慎:如果主源站本身不稳定,盲目提高它的权重会让故障范围扩大。我们的主源站和备用源站是双活的,所以可以在低命中率时切换,但如果是主备模式,需要先做好健康检查。
- 日志监控要跟上:没有监控,调优就是瞎调。我们每天都会看一眼命中率曲线、回源流量排名、预热任务成功率这三项指标。
七、总结
这次调优让我明白了一个道理:CDN不是配完就完事,而是需要持续运营。预热调度相当于我们主动给CDN“喂饭”,回源策略相当于给CDN设定一套“怎么吃饭”的规则。两者配合好了,命中率自然会上去。如果你也在为命中率低而头疼,建议先拉出监控数据,找到最热的资源,写脚本做预热;再检查一下自己URL里是不是有干扰缓存的参数,合理设置忽略规则;最后根据实时监控动态调整回源配置。按照这个路径走一遍,命中率一定会有明显改善。
所有脚本我们用的都是Node.js,你只需要把API地址和密钥替换成自己实际的,就能跑起来。如果你们技术栈是Python,代码思路也是相通的。希望这篇记录能为你提供参考。
Comments