概述

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)
}

关键设计决策:

  1. 采集间隔 5 分钟,不是常见的 15 秒。CDN API 返回的是聚合数据,最小粒度就是 5 分钟,拉得更频繁只会触发 API 限流。
  2. 域名作为 label,不是节点。一个域名背后有上百个边缘节点,按节点维度数据量爆炸且没意义——你排障时关心的是"这个域名的命中率掉了",不是"广州电信节点 03 的命中率掉了"。
  3. 请求命中率和字节命中率分开设,用 hit_type label 区分。两个指标都要单独告警。

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值班 SRE30 秒
排障层单 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 分钟飞书群缓存效率下降
P25xx 率 > 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 查询参数,导致每次版本号变了就全量回源。

决策:

  1. 忽略查询参数做缓存键:?v= 版本号不影响图片内容,CDN 缓存键配置为"忽略所有查询参数"。
  2. 大图缓存时间从 1 小时改为 7 天:产品主图更新频率低,7 天够了。发布新版本时主动刷新。
  3. 回源 URL 监控面板:加一个 Top 20 回源 URL 面板,以后不用翻日志。

改完之后字节命中率从 58% 拉到 94%,源站带宽从 800Mbps 降到 120Mbps。

腾讯云 CDN 也有类似的缓存键与查询字符串规则配置问题,默认把完整 URL 含参数作为缓存键,业务侧的版本号、营销标记会"污染"缓存键导致命中率断崖式下跌。排查时先看 Top 回源 URL 有没有带不该带的参数。

5.2 案例 2:命中率正常但用户反馈慢

现象:命中率和回源指标都正常,但多地用户反馈页面加载慢。CDN 厂商控制台一切正常。

排查过程:

CDN 厂商控制台的指标是全局聚合的,看不出单个区域或运营商的问题。在 Grafana 上对比拨测数据发现:电信用户拨测延迟 P95 从 30ms 涨到 180ms,联通用户正常。

进一步查 CDN 节点分布,发现电信区域的一个核心节点在维护,流量被调度到远端节点,物理距离增加导致延迟上升。CDN 厂商的聚合指标把这个区域的问题给平均掉了。

决策:

  1. 拨测节点按运营商分组:之前拨测只看全局 P95,改成按电信/联通/移动分别统计。
  2. 加延迟偏差告警:同一域名在不同运营商的 P95 延迟差超过 100ms 就告警。
  3. 厂商 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 节点缓存填充速度跟不上。

决策:

  1. 发布后主动预热:不要等用户请求触发缓存填充。调用 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. 全量刷新改为增量刷新:只刷新变更的文件,不全量刷新。构建脚本输出变更文件列表,只刷新这些。
  2. 加刷新影响告警:刷新后 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 关闭的连接,导致回源失败。

决策:

  1. 统一 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;
}
  1. 加回源链路监控:在 Exporter 中增加回源连接错误率指标,和源站 Nginx 日志对照看。
  2. 回源 5xx 告警阈值从 1% 降到 0.3%:间歇性 5xx 也要告警,不能因为量小就忽略。

5.5 案例 5:多 CDN 环境的监控数据对齐

现象:同时用了两家 CDN 厂商做容灾切换,但两家 API 返回的命中率指标口径不一致——A 厂商的"命中率"是请求命中率,B 厂商的是字节命中率。监控面板上两个数字放一起看,完全没有可比性。

排查过程:

这个问题不是技术问题,是指标对齐问题。两家厂商的 API 字段定义不同,采集时如果不统一口径,数据放在一起只会误导判断。

决策:

  1. Exporter 层统一口径:不管厂商 API 返回什么字段名,Exporter 输出统一用 hit_type=request 和 hit_type=byte 两个 label。A 厂商 API 返回的 HitRate 映射到 hit_type=request,B 厂商的 TrafficHitRate 映射到 hit_type=byte。
  2. 加 provider label:每个指标加 provider=a|b,同一域名在两个厂商的数据分别可见。
  3. 切换决策不靠单一指标:容灾切换时综合看拨测延迟、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 监控的核心不是采集多少指标,而是在正确的维度上建立可决策的告警。三句话总结我这几年的经验:

  1. 字节命中率比请求命中率重要。请求命中率会骗你,字节命中率不会。字节命中率低于 80% 就该告警。
  2. 拨测是 API 监控的必要补充。API 有 5-15 分钟延迟,拨测能在 1 分钟内发现可达性问题。两层监控缺一不可。
  3. 回源 URL 分布是最有价值的排障面板。命中率掉了不要猜,看 Top 20 回源 URL 就知道哪些资源没被缓存。

最后给一个实操建议:如果你刚开始做 CDN 监控,不要一上来就搞全套。先做三件事——字节命中率监控、拨测可达性、回源带宽告警。这三样覆盖了 80% 的问题场景。等跑稳了再逐步加回源 URL 分布、运营商维度、多厂商对齐这些高级功能。

CDN 监控体系的搭建不是一次性的工程,而是随着业务变化持续调优的过程。缓存策略变了、上了新的 CDN 厂商、加了新的域名、前端构建方式改了——每一次变化都可能影响缓存命中率和回源模式。监控系统要跟着变,不然下次出问题还是两眼一抹黑。

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. What is a cache hit ratio? — Cloudflare,缓存命中率定义与计算公式,帮助厘清请求命中率和字节命中率的区别
  2. CDN 缓存场景排障方法 — 阿里云 CDN 官方文档,缓存不生效与命中率低的排查流程,本文案例 1 的排查路径参考了官方建议
  3. CDN命中率低?缓存规则与Cache-Control响应头优化实战 — 腾讯云开发者社区,Cache-Control 响应头与缓存键污染的分析,帮助理解查询参数对命中率的影响
  4. CDN 命中率监控与告警体系搭建 — Hostease,CDN 告警与发布联动的实践思路,本文告警分级设计参考了其分层理念
  5. Prometheus生产级监控深度实践 — 腾讯云开发者社区,Prometheus 分层采集架构设计,本文 Exporter 设计借鉴了其采集层与存储层分离的思路