概述
CDN 接入后,很多团队的监控就停留在厂商控制台看个命中率数字。命中率 90%?挺好,不管了。直到某天源站带宽被打满、页面加载 8 秒、用户投诉炸了,才发现那个 90% 是请求命中率而不是字节命中率——大文件全在回源,小请求被缓存命中拉高了数字。
这篇文章拆解一套我在生产环境中落地过的 CDN 监控体系。不是堆指标罗列,而是围绕"缓存命中率、回源带宽、边缘节点健康度"三个核心维度,讲清楚每个指标怎么采集、怎么看、出了问题怎么排查。最后用 5 个真实排障案例串起来,每个都带具体数据和决策过程。
先说清楚 CDN 监控和普通应用监控的区别:应用监控是你自己部署的组件,指标完全可控;CDN 监控的对象是第三方或自建的边缘网络,很多指标得从厂商 API 拉取或者从日志里算,采集链路更长,排查时干扰因素更多。
一、CDN 监控的核心指标体系
1.1 三个维度,别贪多
CDN 涉及的指标不下 30 个,但真正影响业务决策的就三类:
| 维度 | 核心指标 | 告诉你什么 | 数据来源 |
|---|---|---|---|
| 缓存效率 | 请求命中率、字节命中率 | CDN 有没有把流量挡在边缘 | 厂商 API / 日志分析 |
| 回源压力 | 回源 QPS、回源带宽、回源错误率 | 源站扛不扛得住 | 厂商 API / 源站 Nginx 日志 |
| 边缘健康 | 节点可用率、边缘延迟 P95/P99、CDN → 源站延迟 | 用户到 CDN 这一段通不通 | 拨测 / RUM / 厂商 API |
很多团队只看命中率,不看回源带宽。这就好比只看超市收银台开了几个,不看仓库门有没有被搬空。命中率 95% 听着不错,但如果那 5% 的回源全是 50MB 的视频文件,源站出口带宽照样被打满。
1.2 请求命中率 vs 字节命中率:90% 的人搞混过
这两个指标经常被搞混,而且厂商控制台默认展示的往往是请求命中率——因为数字好看。
- 请求命中率 = 缓存命中请求数 / 总请求数。一个 2KB 的 CSS 命中和一个 50MB 的视频回源,在请求命中率里权重一样。
- 字节命中率 = 缓存命中字节数 / 总传输字节数。大文件回源会被放大,更真实地反映源站压力。
真实案例:某电商平台页面加载变慢,看控制台命中率 92% 觉得没问题。排查发现请求命中率确实 92%,但字节命中率只有 58%——产品详情页的 1080P 主图 300 张全在回源,每张 3-5MB,源站带宽直接吃满。
这个坑几乎所有第一次做 CDN 监控的人都踩过。控制台数字好看不等于没问题,请求命中率和字节命中率必须同时看。
我的建议:字节命中率低于 80% 就该告警,不管请求命中率多高。因为字节命中率低意味着大量数据在回源,源站带宽压力不会小。
1.3 指标采集方案对比
CDN 指标采集有三种路径,各有适用场景:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 厂商 API 拉取 | 指标全、准 | 有延迟(5-15 分钟)、API 限流 | 阿里云/腾讯云 CDN |
| 日志分析 | 粒度细、可自定义 | 存储成本高、实时性差 | 自建 CDN / 需要细粒度分析 |
| 拨测 + RUM | 实时性好、用户视角 | 只能测外部可达性 | 所有方案必加的补充 |
实际生产中我推荐组合用:API 拉取做核心指标,日志分析做排障下钻,拨测做可用性兜底。三种方案互相补盲区,别指望一种搞定所有。
相关文章:监控数据治理:从指标爆炸到精准可观测——如果你在纠结"CDN 指标这么多到底采哪些",可以参考这篇指标治理方法论。
二、Prometheus 采集方案落地
2.1 从厂商 API 拉取 CDN 指标
主流 CDN 厂商都提供监控 API,但 Prometheus 没有现成的 CDN Exporter。需要自己写一个轻量 Exporter,定期调 API、转成 Prometheus 格式。
下面是一个针对阿里云 CDN 的 Exporter 核心逻辑(Go 实现),其他厂商改 API 调用即可:
package main
import (
"fmt"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// CDN 指标定义
var (
// 缓存命中率指标,label 区分请求命中率和字节命中率
cacheHitRate = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "cdn_cache_hit_rate",
Help: "CDN cache hit rate, hit_type=request|byte",
},
[]string{"domain", "hit_type"},
)
// 回源 QPS
originQPS = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "cdn_origin_qps",
Help: "CDN origin requests per second",
},
[]string{"domain"},
)
// 回源带宽,单位 Mbps
originBps = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "cdn_origin_bps",
Help: "CDN origin bandwidth in Mbps",
},
[]string{"domain"},
)
// 边缘节点 HTTP 状态码分布
edgeStatus = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "cdn_edge_status_total",
Help: "CDN edge response status codes",
},
[]string{"domain", "code"},
)
)
func init() {
prometheus.MustRegister(cacheHitRate, originQPS, originBps, edgeStatus)
}
// 每分钟拉取一次 CDN 监控数据
// 注意:阿里云 CDN API 有 5 分钟数据延迟,拉取频率低于 5 分钟无意义
func collectCDNMetrics() {
domains := getCDNDomains() // 从配置文件读取加速域名列表
for _, domain := range domains {
// 调用阿里云 CDN API 获取监控数据
// 时间窗口取过去 5 分钟
endTime := time.Now().UTC().Format("2006-01-02T15:04:00Z")
startTime := time.Now().Add(-5 * time.Minute).UTC().Format("2006-01-02T15:04:00Z")
// 请求命中率
reqHitRate := callCDNAPI(domain, "RequestHitRate", startTime, endTime)
cacheHitRate.WithLabelValues(domain, "request").Set(reqHitRate)
// 字节命中率
byteHitRate := callCDNAPI(domain, "TrafficHitRate", startTime, endTime)
cacheHitRate.WithLabelValues(domain, "byte").Set(byteHitRate)
// 回源 QPS 和带宽
originQPSVal := callCDNAPI(domain, "OriginQPS", startTime, endTime)
originQPS.WithLabelValues(domain).Set(originQPSVal)
originBpsVal := callCDNAPI(domain, "OriginBps", startTime, endTime)
originBps.WithLabelValues(domain).Set(originBpsVal)
}
}
func main() {
// 每 5 分钟采集一次,和 API 数据延迟对齐
go func() {
ticker := time.NewTicker(5 * time.Minute)
for {
select {
case <-ticker.C:
collectCDNMetrics()
}
}
}()
// 初始采集一次
collectCDNMetrics()
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":9105", nil)
}
关键设计决策:
- 采集间隔 5 分钟,不是常见的 15 秒。CDN API 返回的是聚合数据,最小粒度就是 5 分钟,拉得更频繁只会触发 API 限流。
- 域名作为 label,不是节点。一个域名背后有上百个边缘节点,按节点维度数据量爆炸且没意义——你排障时关心的是"这个域名的命中率掉了",不是"广州电信节点 03 的命中率掉了"。
- 请求命中率和字节命中率分开设,用
hit_typelabel 区分。两个指标都要单独告警。
2.2 拨测补充:用 Blackbox Exporter 探测 CDN 可达性
API 拉取的指标有 5-15 分钟延迟。如果边缘节点挂了,你要 15 分钟后才知道?不行。
补一层拨测:用 Blackbox Exporter 每 30 秒探测一次 CDN 域名,关注 HTTP 状态码和响应时间。
# prometheus.yml 拨测配置
scrape_configs:
- job_name: 'cdn-probe'
scrape_interval: 30s
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://cdn.example.com/health-check.html # CDN 上的健康检查文件
- https://www.example.com/ # 源站直连对比
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
拨测的目的不是替代 API 指标,而是做"1 分钟内发现"的快速告警。API 指标做趋势分析和 5 分钟级别的告警,拨测做秒级可达性告警。
相关文章:Blackbox Exporter:外部探测与拨测监控——拨测配置细节可以参考这篇,这里不展开。
2.3 源站侧补充监控
CDN 厂商的 API 只告诉你"回源 QPS 多少、回源带宽多少",但不告诉你源站扛没扛住。在源站 Nginx 上加一层监控,和 CDN API 数据对照看:
# Nginx 日志中标记 CDN 回源请求
# 通过 $http_x_forwarded_for 判断是否来自 CDN 节点
log_format cdn_origin '$status $body_bytes_sent $request_time '
'$http_x_forwarded_for $remote_addr '
'$upstream_cache_status $request_uri';
server {
listen 443 ssl;
server_name origin.example.com;
# 只记录来自 CDN 的回源请求
access_log /var/log/nginx/cdn_origin.log cdn_origin;
location / {
proxy_pass http://backend;
# 给 CDN 回源响应加缓存状态头,方便排查
add_header X-Origin-Cache-Status $upstream_cache_status;
}
}
配合 mtail 或自定义 Go 脚本解析 Nginx 日志,把回源请求的响应时间、状态码分布推送到 Prometheus。这组数据能帮你回答"CDN 说回源 200QPS,但源站 Nginx 收到 800QPS"——中间的差值通常是缓存刷新、预热或 CDN 配置变更导致的突发回源。
三、Grafana 仪表盘设计
3.1 三层仪表盘结构
CDN 监控仪表盘不要堆在一块大屏上,分三层:
| 层级 | 面板内容 | 受众 | 刷新间隔 |
|---|---|---|---|
| 总览层 | 各域名命中率趋势、回源带宽、4xx/5xx 状态码 | 运维 Leader / 架构师 | 1 分钟 |
| 域名层 | 单域名命中率、回源趋势、Top 回源 URL | 值班 SRE | 30 秒 |
| 排障层 | 单 URL 缓存状态、回源时间线、源站 Nginx 对比 | 排障工程师 | 10 秒 |
3.2 核心面板 PromQL
// 字节命中率趋势(过去 1 小时)
cdn_cache_hit_rate{hit_type="byte"}
// 回源带宽占比(回源带宽 / 总带宽)
cdn_origin_bps / (cdn_origin_bps + cdn_edge_bps) * 100
// 回源 QPS 突增检测(当前值 vs 过去 1 小时均值)
cdn_origin_qps > (avg_over_time(cdn_origin_qps[1h]) * 2)
// 5xx 错误率
rate(cdn_edge_status_total{code=~"5.."}[5m]) /
rate(cdn_edge_status_total[5m]) * 100
// 拨测响应时间 P95
histogram_quantile(0.95, rate(probe_duration_seconds_bucket[5m]))
3.3 一个容易忽略的面板:命中率与回源带宽的关联图
单独看命中率曲线看不出问题,单独看回源带宽也看不出。把两条线叠在一张图上,异常一目了然。
正常情况:命中率上升 → 回源带宽下降,两条线呈镜像。 异常情况:命中率不变 → 回源带宽突增,说明回源的不是普通请求而是大文件回源,命中率没掉但字节命中率掉了。
这个面板帮我定位过 3 次"命中率没变但源站带宽爆了"的问题,每次根因都是有大文件没被缓存。
3.4 成本监控面板:CDN 计费方式决定了监控重点
CDN 计费方式直接影响你该重点盯哪些指标。两种主流计费模式:
| 计费方式 | 监控重点 | 优化方向 |
|---|---|---|
| 按流量计费 | 总流量(边缘流量 + 回源流量) | 提高命中率减少回源流量 |
| 按带宽计费(95 峰值) | 5 分钟带宽峰值 | 削峰——分散大文件回源时间 |
按带宽计费用 95 峰值扣费,意思是取一个月内最高的 5% 带宽数据点去掉,剩下 95% 的最高值就是计费带宽。这种模式下,一次大文件预热导致的带宽尖峰可能让你的月账翻倍。
加一个"带宽峰值追踪"面板,实时看当前 5 分钟带宽在月度排行中的位置。如果快要进入 Top 5%,赶紧排查是不是有不正常的回源流量。
成本监控不是省钱,是防失控。CDN 流量费用在大促期间可能翻 3-5 倍,没有监控面板你根本不知道钱花在哪了,更不知道哪部分是正常增长、哪部分是缓存策略失效导致的浪费。
把成本面板和命中率面板放在一起看。命中率没变但流量翻倍,说明新增流量全被边缘节点消化了,成本正常增长;命中率掉了且流量翻倍,说明钱白花了。
四、告警规则设计
4.1 告警分级
| 级别 | 条件 | 告警方式 | 说明 |
|---|---|---|---|
| P0 | 拨测连续 3 次失败 | 电话 + 飞书 | CDN 域名不可达 |
| P1 | 字节命中率 < 80% 持续 10 分钟 | 飞书 | 缓存效率严重下降 |
| P1 | 回源带宽 > 阈值持续 5 分钟 | 飞书 | 源站压力过大 |
| P2 | 请求命中率 < 90% 持续 15 分钟 | 飞书群 | 缓存效率下降 |
| P2 | 5xx 率 > 1% 持续 5 分钟 | 飞书群 | 边缘或源站异常 |
| P3 | 回源 QPS 环比增长 > 100% | 飞书群 | 可能是缓存刷新或配置变更 |
关键原则:告警阈值用持续时间过滤毛刺。CDN 指标天然有波动(缓存刷新、预热都会导致命中率短暂下降),不用持续时间过滤的话告警风暴会把你淹了。
相关文章:告警策略设计:从噪声到信号——告警分级和降噪的方法论,这里不展开。
4.2 Alertmanager 规则配置
groups:
- name: cdn_alerts
rules:
# 字节命中率低于 80%,持续 10 分钟
- alert: CDNByteHitRateLow
expr: cdn_cache_hit_rate{hit_type="byte"} < 80
for: 10m
labels:
severity: P1
team: sre
annotations:
summary: "{{ $labels.domain }} 字节命中率低于 80%"
description: "当前值 {{ $value }}%,请检查缓存策略和大文件回源情况"
# 回源带宽超过阈值
- alert: CDNOriginBandwidthHigh
expr: cdn_origin_bps > 500
for: 5m
labels:
severity: P1
team: sre
annotations:
summary: "{{ $labels.domain }} 回源带宽超过 500Mbps"
description: "当前值 {{ $value }}Mbps,检查缓存命中率和回源 URL 分布"
# 5xx 错误率超标
- alert: CDNHighErrorRate
expr: |
sum(rate(cdn_edge_status_total{code=~"5.."}[5m])) by (domain)
/ sum(rate(cdn_edge_status_total[5m])) by (domain) * 100 > 1
for: 5m
labels:
severity: P2
team: sre
annotations:
summary: "{{ $labels.domain }} 5xx 错误率超过 1%"
description: "当前值 {{ $value }}%,检查源站健康状态和边缘节点日志"
# 拨测失败
- alert: CDNProbeFailed
expr: probe_success == 0
for: 1m
labels:
severity: P0
team: sre
annotations:
summary: "CDN 拨测失败: {{ $labels.instance }}"
description: "CDN 域名不可达,立即排查"
4.3 一个反直觉的告警设计:不要给请求命中率设 P0
很多人给请求命中率设了 P0 告警,命中率一掉就电话轰炸。问题是请求命中率掉的原因太多了——缓存刷新、预热、新版本发布、流量突增导致新 URL 涌入——全是正常业务行为。
我的建议:请求命中率告警最多 P2,通知到飞书群就行,别打电话。真正需要电话轰炸的是拨测失败(CDN 不可达)和回源 5xx 率飙升(源站可能挂了)。命中率下降是"有事发生了但不一定紧急",拨测失败是"用户已经访问不了了"。
四点五、CDN 缓存策略配置:监控的基础
监控做得再好,缓存策略本身不对,指标也只能告诉你"有多糟"。先把缓存策略配对,监控才有意义。
缓存时间分层
不同类型的资源,缓存时间天差地别。下面是我实际生产环境中的缓存策略模板:
| 资源类型 | 缓存时间 | 理由 | 刷新策略 |
|---|---|---|---|
| HTML 页面 | 5 分钟 | 内容更新频繁,5 分钟延迟可接受 | 自然过期 |
| CSS/JS(带版本号) | 30 天 | URL 含 hash,版本变了就是新 URL | 不需要刷新 |
| 图片(产品图、Banner) | 7 天 | 更新频率低,发布时批量刷新 | 发布时主动刷新 |
| 视频/大文件 | 30 天 | 回源成本高,尽量长缓存 | 按需刷新 |
| API 响应 | 0 秒(不缓存) | 动态内容,不能缓存 | 无 |
关键坑:别给 API 接口设缓存时间。有些团队图省事,全站设 1 小时缓存,结果用户登录态、库存数据全被 CDN 缓存了,返回的是别人的数据。API 接口必须设 Cache-Control: no-cache 或在 CDN 上配置"不缓存动态路径"。
Cache-Control 响应头:源站和 CDN 必须一致
CDN 节点的缓存行为受两层控制:CDN 控制台配置的缓存规则,和源站返回的 Cache-Control 响应头。两者冲突时,不同厂商的处理方式不一样——有的以 CDN 配置为准,有的以源站响应头为准。
踩坑案例:在 CDN 控制台给 .pdf 文件设了缓存 30 天,但源站 Nginx 不小心返回了 Cache-Control: no-store。结果边缘节点完全不存储 PDF 文件,每次请求都回源。命中率看着没问题(因为请求量小),但回源带宽一直在涨。
排查方法:用 curl -v 看响应头,确认 Cache-Control 字段:
# 检查源站返回的响应头
curl -v -o /dev/null "https://origin.example.com/static/doc.pdf" 2>&1 | grep -i "cache-control"
# 检查 CDN 边缘返回的响应头
curl -v -o /dev/null "https://cdn.example.com/static/doc.pdf" 2>&1 | grep -i "cache-control\|x-cache\|age"
如果源站返回 no-store 或 no-cache,CDN 大概率不会缓存。我的建议:源站 Nginx 按 Content-Type 统一设置 Cache-Control,别让应用层自己管——应用代码里随手加个 no-cache 你根本不知道。
# 源站 Nginx 按 Content-Type 统一缓存策略
location ~* \.(jpg|jpeg|png|gif|webp|svg)$ {
add_header Cache-Control "public, max-age=604800"; # 7 天
}
location ~* \.(css|js)$ {
add_header Cache-Control "public, max-age=2592000"; # 30 天
}
location ~* \.(pdf|mp4|zip)$ {
add_header Cache-Control "public, max-age=2592000"; # 30 天
}
# API 路径不缓存
location /api/ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
缓存键规则:查询参数是头号杀手
这个点在案例 1 里已经提到了,但值得单独展开讲——因为查询参数导致的缓存命中率下降太常见了。
CDN 默认把完整 URL(含查询参数)作为缓存键。这意味着 /image.jpg?v=1 和 /image.jpg?v=2 是两个独立的缓存条目。如果你的 URL 带了这些不影响内容的参数,命中率会断崖式下跌:
| 参数类型 | 是否影响内容 | 缓存键处理 |
|---|---|---|
版本号 ?v=123456 | 否(内容相同,只是缓存刷新标记) | 忽略 |
营销标记 ?utm_source=xxx | 否 | 忽略 |
时间戳 ?t=1234567890 | 否 | 忽略 |
分页 ?page=2 | 是(不同内容) | 保留 |
语言 ?lang=en | 是(不同内容) | 保留 |
排序 ?sort=price | 是(不同结果) | 保留 |
阿里云 CDN 在控制台 → 缓存配置 → 缓存 key 中配置"忽略参数";腾讯云在缓存键规则中设置。配置时一定只忽略不影响内容的参数,别全量忽略——分页和语言参数被忽略了,用户看到的就是别人的数据了。
五、5 个生产级排障决策
下面 5 个案例都来自实际生产环境。每个案例先说现象,再说排查过程,最后给决策结论。
5.1 案例 1:字节命中率 58%,请求命中率 92%
现象:某电商平台大促前压测,控制台显示请求命中率 92%,但源站带宽一直在 800Mbps 上下,Nginx 日志显示大量回源请求。
排查过程:
第一步,确认是请求命中率还是字节命中率。控制台默认展示的是请求命中率 92%,切到字节命中率只有 58%。差异来源于产品详情页的 1080P 主图——每张 3-5MB,全在回源。
第二步,查回源 URL 分布。通过 Nginx 日志按 $request_uri 分组统计 Top 20 回源 URL:
# 提取回源请求中 URL,按路径前缀分组统计
awk '{print $7}' /var/log/nginx/cdn_origin.log \
| sed 's/\?.*//' \
| sort | uniq -c | sort -rn | head -20
发现 80% 回源请求指向 /static/images/products/ 下的大图。检查 CDN 缓存配置,发现 .jpg 文件缓存时间设为 1 小时,但 URL 带了 ?v=timestamp 查询参数,导致每次版本号变了就全量回源。
决策:
- 忽略查询参数做缓存键:
?v=版本号不影响图片内容,CDN 缓存键配置为"忽略所有查询参数"。 - 大图缓存时间从 1 小时改为 7 天:产品主图更新频率低,7 天够了。发布新版本时主动刷新。
- 回源 URL 监控面板:加一个 Top 20 回源 URL 面板,以后不用翻日志。
改完之后字节命中率从 58% 拉到 94%,源站带宽从 800Mbps 降到 120Mbps。
腾讯云 CDN 也有类似的缓存键与查询字符串规则配置问题,默认把完整 URL 含参数作为缓存键,业务侧的版本号、营销标记会"污染"缓存键导致命中率断崖式下跌。排查时先看 Top 回源 URL 有没有带不该带的参数。
5.2 案例 2:命中率正常但用户反馈慢
现象:命中率和回源指标都正常,但多地用户反馈页面加载慢。CDN 厂商控制台一切正常。
排查过程:
CDN 厂商控制台的指标是全局聚合的,看不出单个区域或运营商的问题。在 Grafana 上对比拨测数据发现:电信用户拨测延迟 P95 从 30ms 涨到 180ms,联通用户正常。
进一步查 CDN 节点分布,发现电信区域的一个核心节点在维护,流量被调度到远端节点,物理距离增加导致延迟上升。CDN 厂商的聚合指标把这个区域的问题给平均掉了。
决策:
- 拨测节点按运营商分组:之前拨测只看全局 P95,改成按电信/联通/移动分别统计。
- 加延迟偏差告警:同一域名在不同运营商的 P95 延迟差超过 100ms 就告警。
- 厂商 API 补充节点级数据:调用厂商的节点监控 API,把核心节点延迟也采集到 Prometheus。
后来再遇到类似问题,告警 3 分钟内就能定位到"哪个运营商的哪个区域延迟异常",不用等用户投诉。
5.3 案例 3:缓存刷新后命中率暴跌且不恢复
现象:发布新版本后执行了 CDN 全量刷新,命中率从 94% 跌到 30%,预期 30 分钟恢复,结果 2 小时还在 40%。
排查过程:
正常刷新后命中率会短暂下跌,然后随着请求重新填充缓存恢复。但这次恢复太慢了。查回源 URL 发现,大量回源指向新版本引入的 200+ 个 JS chunk 文件,每个文件独立 URL,CDN 节点需要逐个回源拉取。
根因:前端构建把单个 vendor.js 拆成了 200+ 个小 chunk,全量刷新后每个 chunk 都需要回源一次。CDN 节点缓存填充速度跟不上。
决策:
- 发布后主动预热:不要等用户请求触发缓存填充。调用 CDN 厂商的预热 API,把新版本 URL 列表批量推送到边缘节点。
#!/bin/bash
# 发布后批量预热脚本
# 从构建产物中提取需要预热的 URL 列表
DOMAIN="cdn.example.com"
BUILD_DIR="/data/releases/latest"
# 提取所有静态资源 URL
find "$BUILD_DIR" -type f \( -name "*.js" -o -name "*.css" -o -name "*.png" -o -name "*.jpg" \) \
| sed "s|$BUILD_DIR|https://$DOMAIN|" \
> /tmp/prefetch_urls.txt
# 调用预热 API(阿里云示例)
while IFS= read -r url; do
# 每批 50 个 URL,避免 API 限流
echo "$url" >> /tmp/batch.txt
if [ $(wc -l < /tmp/batch.txt) -eq 50 ]; then
aliyun cdn PushObjectCache --ObjectPaths "$(cat /tmp/batch.txt | tr '\n' ',')" --Area mainland
> /tmp/batch.txt
sleep 2 # API 限流保护
fi
done < /tmp/prefetch_urls.txt
# 处理剩余 URL
if [ -s /tmp/batch.txt ]; then
aliyun cdn PushObjectCache --ObjectPaths "$(cat /tmp/batch.txt | tr '\n' ',')" --Area mainland
fi
echo "预热完成,共 $(wc -l < /tmp/prefetch_urls.txt) 个 URL"
- 全量刷新改为增量刷新:只刷新变更的文件,不全量刷新。构建脚本输出变更文件列表,只刷新这些。
- 加刷新影响告警:刷新后 1 小时命中率恢复不到 70% 就告警,说明恢复异常。
改完后发布流程从"刷新 → 等 2 小时 → 命中率恢复"变成"增量刷新 → 预热 → 10 分钟内命中率恢复到 90%+"。
5.4 案例 4:回源 5xx 间歇性出现
现象:CDN 边缘 5xx 率在 0.1%-0.5% 之间波动,没有规律。源站 Nginx 日志正常,没有 5xx。
排查过程:
CDN 5xx 但源站没 5xx,说明问题在 CDN → 源站的回源链路上。查 CDN 回源日志发现,5xx 集中在特定时间段,和源站 Nginx 的 keepalive_timeout 到期时间吻合。
根因:CDN 回源使用 HTTP Keep-Alive 复用连接,但源站 Nginx 的 keepalive_timeout 设为 65 秒,CDN 侧连接复用超时设为 60 秒。两边时间不一致,偶尔 CDN 复用一个已经被 Nginx 关闭的连接,导致回源失败。
决策:
- 统一 Keep-Alive 超时:源站 Nginx 的
keepalive_timeout从 65 秒改为 90 秒,比 CDN 侧的 60 秒长 30 秒,确保 CDN 不会复用过期连接。
# 源站 Nginx 配置
http {
# Keep-Alive 超时要比 CDN 侧长 30 秒以上
# 否则 CDN 会复用已被源站关闭的连接,导致回源 5xx
keepalive_timeout 90s;
keepalive_requests 1000;
}
- 加回源链路监控:在 Exporter 中增加回源连接错误率指标,和源站 Nginx 日志对照看。
- 回源 5xx 告警阈值从 1% 降到 0.3%:间歇性 5xx 也要告警,不能因为量小就忽略。
5.5 案例 5:多 CDN 环境的监控数据对齐
现象:同时用了两家 CDN 厂商做容灾切换,但两家 API 返回的命中率指标口径不一致——A 厂商的"命中率"是请求命中率,B 厂商的是字节命中率。监控面板上两个数字放一起看,完全没有可比性。
排查过程:
这个问题不是技术问题,是指标对齐问题。两家厂商的 API 字段定义不同,采集时如果不统一口径,数据放在一起只会误导判断。
决策:
- Exporter 层统一口径:不管厂商 API 返回什么字段名,Exporter 输出统一用
hit_type=request和hit_type=byte两个 label。A 厂商 API 返回的HitRate映射到hit_type=request,B 厂商的TrafficHitRate映射到hit_type=byte。 - 加
providerlabel:每个指标加provider=a|b,同一域名在两个厂商的数据分别可见。 - 切换决策不靠单一指标:容灾切换时综合看拨测延迟、5xx 率、命中率三个指标,不只看命中率。
5.6 补充:CDN 日志分析——指标看趋势,日志看根因
指标告诉你"命中率掉了",日志告诉你"哪些 URL 在回源、回源响应多慢、状态码分布是什么"。排障时日志比指标重要。
主流 CDN 厂商都支持日志下载或实时日志推送。阿里云 CDN 支持日志转存到 OSS,腾讯云支持实时日志投递到 CKAFKA。拿到日志后的分析流程:
# 1. 按状态码分布统计
awk '{print $9}' cdn_log_20260924.txt | sort | uniq -c | sort -rn
# 2. 按 URL 路径(去掉查询参数)统计回源 Top 20
# $request 字段格式: "GET /path?query HTTP/1.1"
grep "MISS" cdn_log_20260924.txt \
| awk '{print $7}' \
| sed 's/\?.*//' \
| sort | uniq -c | sort -rn | head -20
# 3. 按小时统计回源量,定位突发回源时间段
grep "MISS" cdn_log_20260924.txt \
| awk '{print $4}' \
| cut -d: -f1-2 \
| sort | uniq -c
# 4. 找回源响应慢的 URL(>2 秒)
awk '$10 > 2.0 {print $7, $10}' cdn_log_20260924.txt | sort -k2 -rn | head -20
# 5. 统计回源请求的 Referer 分布,排查是否有盗链
grep "MISS" cdn_log_20260924.txt \
| awk '{print $11}' \
| sort | uniq -c | sort -rn | head -10
把日志分析脚本化、定时跑,输出到 Grafana 面板或飞书日报。日志分析不用实时——每天跑一次足够,用于发现前一天的趋势变化和异常模式。
如果日志量太大(日均 GB 级),建议用 ELK 或 Loki 做日志聚合,别用 grep 硬扛。日志分析的价值不在单条日志,在于统计聚合后的模式发现。
六、踩坑总结:CDN 监控的 6 个反模式
做 CDN 监控这几年,以下 6 个错误我反复见到:
| 反模式 | 后果 | 正确做法 |
|---|---|---|
| 只看请求命中率 | 大文件回源被掩盖 | 请求命中率和字节命中率同时监控 |
| 采集间隔太短 | API 限流、数据不准 | 和 API 数据延迟对齐,5 分钟足够 |
| 告警不加持续时间 | 刷新/预热导致告警风暴 | 至少持续 5-10 分钟才告警 |
| 不做拨测 | API 延迟 15 分钟才发现故障 | 拨测做秒级可达性兜底 |
| 不监控回源 URL | 命中率掉了不知道哪些 URL 在回源 | 加 Top 20 回源 URL 面板 |
| 多厂商指标不对齐 | 容灾切换判断失误 | Exporter 层统一指标口径 |
总结
CDN 监控的核心不是采集多少指标,而是在正确的维度上建立可决策的告警。三句话总结我这几年的经验:
- 字节命中率比请求命中率重要。请求命中率会骗你,字节命中率不会。字节命中率低于 80% 就该告警。
- 拨测是 API 监控的必要补充。API 有 5-15 分钟延迟,拨测能在 1 分钟内发现可达性问题。两层监控缺一不可。
- 回源 URL 分布是最有价值的排障面板。命中率掉了不要猜,看 Top 20 回源 URL 就知道哪些资源没被缓存。
最后给一个实操建议:如果你刚开始做 CDN 监控,不要一上来就搞全套。先做三件事——字节命中率监控、拨测可达性、回源带宽告警。这三样覆盖了 80% 的问题场景。等跑稳了再逐步加回源 URL 分布、运营商维度、多厂商对齐这些高级功能。
CDN 监控体系的搭建不是一次性的工程,而是随着业务变化持续调优的过程。缓存策略变了、上了新的 CDN 厂商、加了新的域名、前端构建方式改了——每一次变化都可能影响缓存命中率和回源模式。监控系统要跟着变,不然下次出问题还是两眼一抹黑。
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- What is a cache hit ratio? — Cloudflare,缓存命中率定义与计算公式,帮助厘清请求命中率和字节命中率的区别
- CDN 缓存场景排障方法 — 阿里云 CDN 官方文档,缓存不生效与命中率低的排查流程,本文案例 1 的排查路径参考了官方建议
- CDN命中率低?缓存规则与Cache-Control响应头优化实战 — 腾讯云开发者社区,Cache-Control 响应头与缓存键污染的分析,帮助理解查询参数对命中率的影响
- CDN 命中率监控与告警体系搭建 — Hostease,CDN 告警与发布联动的实践思路,本文告警分级设计参考了其分层理念
- Prometheus生产级监控深度实践 — 腾讯云开发者社区,Prometheus 分层采集架构设计,本文 Exporter 设计借鉴了其采集层与存储层分离的思路