一、问题初现:命中率数字不好看

前一阵子,我负责的一个视频网站总是出现加载慢的情况。打开监控面板一看,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,代码思路也是相通的。希望这篇记录能为你提供参考。