土法炼钢兴趣小组的算法知识备份

【网络工程】CDN 故障调试:缓存命中率、回源异常与头分析

文章导航

分类入口
network
标签入口
#cdn#debugging#cache-hit-ratio#origin-shield#troubleshooting

目录

CDN 出了问题,用户看到的可能是”页面加载慢”“内容没更新”“偶尔报错”。这些模糊的症状背后,需要系统化的排查方法论。本文覆盖 CDN 运维中最常见的四类问题:缓存命中率低、回源异常、内容不一致、性能下降。

一、CDN 响应头解读

排查 CDN 问题的第一步是读懂响应头。CDN 提供商在响应头中暴露了大量调试信息。

1.1 通用 CDN 响应头

# 获取 CDN 响应头(不下载 body)
$ curl -sI https://www.example.com/api/products

HTTP/2 200
date: Sat, 19 Jul 2025 08:30:15 GMT
content-type: application/json
content-length: 4523
cache-control: public, max-age=3600, s-maxage=7200
age: 1847
x-cache: HIT
x-cache-hits: 42
x-served-by: cache-hkg17927-HKG
x-timer: S1721377815.234521,VS0,VE0
via: 1.1 varnish (Fastly)
cf-cache-status: HIT
cf-ray: 8a1234567890-HKG

逐头解析:

响应头               │ 含义                              │ 调试价值
─────────────────────┼──────────────────────────────────┼──────────────────
Cache-Control        │ 源站设置的缓存策略                  │ 是否允许缓存
Age                  │ 对象在缓存中的存活时间(秒)          │ 缓存新鲜度
X-Cache              │ 缓存状态(HIT/MISS/BYPASS)        │ 是否命中缓存
X-Cache-Hits         │ 该对象被缓存命中的次数               │ 缓存热度
X-Served-By          │ 服务该请求的缓存节点                 │ 定位是哪个 PoP
Via                  │ 中间代理链                         │ 请求经过了哪些层
CF-Cache-Status      │ Cloudflare 缓存状态                │ Cloudflare 专用
CF-Ray               │ Cloudflare 请求追踪 ID             │ 关联日志
X-Timer              │ 时间戳和耗时                       │ 性能分析

1.2 各 CDN 提供商的缓存状态头

Cloudflare (cf-cache-status):
  HIT        → 从边缘缓存返回
  MISS       → 缓存未命中,回源获取(已缓存用于后续请求)
  EXPIRED    → 缓存已过期,回源验证/刷新
  STALE      → 返回了过期内容(源站不可达时的容错)
  BYPASS     → 请求被配置跳过缓存(如有 Cookie)
  DYNAMIC    → 资源被认定为动态内容,不缓存
  REVALIDATED → 缓存过期后回源验证,源站返回 304
  UPDATING   → stale-while-revalidate,返回旧内容同时后台刷新
  NONE       → 不走缓存

Fastly (x-cache):
  HIT        → 缓存命中
  MISS       → 缓存未命中
  HIT, HIT   → 经过两层缓存都命中(Shield + Edge)
  MISS, HIT  → Edge 命中,Shield 未命中
  MISS, MISS → 完全回源

CloudFront (x-cache):
  Hit from cloudfront    → 缓存命中
  Miss from cloudfront   → 缓存未命中
  RefreshHit             → 缓存过期后回源验证为 304
  Error from cloudfront  → CDN 层出错

Akamai (x-cache):
  TCP_HIT    → 缓存命中
  TCP_MISS   → 缓存未命中
  TCP_REFRESH_HIT → 缓存过期后验证仍有效
  TCP_MEM_HIT → 从内存缓存命中(比磁盘缓存更快)

1.3 批量分析响应头的脚本

#!/bin/bash
# cdn-header-check.sh — 批量分析 CDN 响应头

URL="${1:-https://www.example.com/}"
ITERATIONS="${2:-10}"

echo "=== CDN Header Analysis: $URL ==="
echo "Iterations: $ITERATIONS"
echo ""

declare -A cache_stats
total_age=0
age_count=0

for i in $(seq 1 $ITERATIONS); do
    headers=$(curl -sI "$URL" 2>/dev/null)
    
    # 提取缓存状态
    cf_status=$(echo "$headers" | grep -i "cf-cache-status" | awk '{print $2}' | tr -d '\r')
    x_cache=$(echo "$headers" | grep -i "x-cache:" | awk '{print $2}' | tr -d '\r')
    cache_status="${cf_status:-$x_cache}"
    cache_status="${cache_status:-UNKNOWN}"
    
    # 统计缓存状态分布
    cache_stats[$cache_status]=$(( ${cache_stats[$cache_status]:-0} + 1 ))
    
    # 提取 Age
    age=$(echo "$headers" | grep -i "^age:" | awk '{print $2}' | tr -d '\r')
    if [ -n "$age" ]; then
        total_age=$((total_age + age))
        age_count=$((age_count + 1))
    fi
    
    # 提取服务节点
    served_by=$(echo "$headers" | grep -i "x-served-by\|cf-ray\|x-amz-cf-pop" | head -1 | tr -d '\r')
    
    printf "  #%02d: cache=%s age=%s node=%s\n" "$i" "$cache_status" "${age:-N/A}" "$served_by"
    
    sleep 0.5
done

echo ""
echo "--- Cache Status Distribution ---"
for status in "${!cache_stats[@]}"; do
    count=${cache_stats[$status]}
    pct=$((count * 100 / ITERATIONS))
    printf "  %-15s %3d/%d (%d%%)\n" "$status" "$count" "$ITERATIONS" "$pct"
done

if [ $age_count -gt 0 ]; then
    avg_age=$((total_age / age_count))
    echo ""
    echo "--- Age Statistics ---"
    echo "  Average Age: ${avg_age}s"
fi

二、缓存命中率分析

缓存命中率(Cache Hit Ratio, CHR)是 CDN 性能的核心指标。一般来说,静态资源的 CHR 应该在 85% 以上。

2.1 命中率低的常见原因

缓存命中率低的排查清单:

1. Cache-Control 配置问题
   ├── no-store / no-cache / private → 不缓存
   ├── max-age 太短(如 60s)→ 频繁过期
   ├── 源站未设置 Cache-Control → CDN 使用默认策略(可能不缓存)
   └── Vary 头太宽泛(Vary: *)→ 每个请求视为不同对象

2. Cache Key 问题
   ├── Query String 导致缓存碎片
   │   /product?id=1&utm_source=google    ← 缓存 Key 1
   │   /product?id=1&utm_source=twitter   ← 缓存 Key 2(内容相同)
   ├── Cookie 被包含在 Cache Key 中
   │   不同用户的 Session Cookie 导致每个用户单独缓存
   └── Accept-Encoding / User-Agent 导致变体过多

3. 长尾内容
   ├── 大量 URL 各自只被访问 1-2 次
   ├── 在单个 PoP 的请求量不足以维持缓存
   └── 缓存被逐出(LRU)后无法再次命中

4. PoP 分散
   ├── CDN 节点太多导致每个节点流量不足
   ├── DNS 负载均衡导致同一用户被路由到不同 PoP
   └── 没有启用 Origin Shield(中间层缓存)

5. 动态内容误分类
   ├── API 响应可以短时间缓存但被标记为 DYNAMIC
   ├── 需要在 CDN 规则中显式开启缓存
   └── 缺少 Cache-Control 头导致 CDN 不缓存

2.2 Query String 优化

Query String 是缓存碎片化的首要原因:

问题场景:

GET /product.jpg?v=1.0&utm_source=google&fbclid=abc123
GET /product.jpg?v=1.0&utm_source=twitter
GET /product.jpg?v=1.0

三个 URL 实际返回相同内容,但 CDN 将它们视为三个独立对象。

Cloudflare 的 Query String 排序和过滤:

解决方案:

1. 忽略无关 Query String(推荐)
   CDN 规则: 只用 path 作为 Cache Key,忽略所有 Query String
   适用: 静态资源(图片、CSS、JS)

2. Query String 排序
   CDN 规则: 对 Query String 参数按字母排序后作为 Cache Key
   ?b=2&a=1 和 ?a=1&b=2 → 同一个 Cache Key
   适用: API 响应

3. 选择性包含
   CDN 规则: 只保留特定参数(如 id, page),忽略追踪参数
   ?id=1&utm_source=google → Cache Key 只包含 ?id=1
   适用: 带追踪参数的页面

Cloudflare Workers 实现自定义 Cache Key:

// custom-cache-key.js — 自定义 Cache Key 逻辑
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    
    // 定义需要保留的 Query String 参数
    const keepParams = new Set(['id', 'page', 'category', 'v']);
    
    // 定义需要忽略的追踪参数
    const ignoreParams = new Set([
      'utm_source', 'utm_medium', 'utm_campaign', 'utm_content',
      'fbclid', 'gclid', 'msclkid', 'ref', '_ga'
    ]);
    
    // 构建干净的 Cache Key
    const cacheUrl = new URL(url.pathname, url.origin);
    const sortedParams = [...url.searchParams.entries()]
      .filter(([key]) => !ignoreParams.has(key))
      .sort(([a], [b]) => a.localeCompare(b));
    
    sortedParams.forEach(([key, value]) => {
      cacheUrl.searchParams.set(key, value);
    });
    
    // 使用自定义 Cache Key 查找缓存
    const cacheKey = new Request(cacheUrl.toString());
    const cache = caches.default;
    
    let response = await cache.match(cacheKey);
    if (response) {
      // 添加调试头
      response = new Response(response.body, response);
      response.headers.set('X-Cache-Key', cacheUrl.toString());
      response.headers.set('X-Cache', 'HIT');
      return response;
    }
    
    // 缓存未命中,回源
    response = await fetch(request);
    
    // 缓存响应
    if (response.ok) {
      const cloned = response.clone();
      event.waitUntil(cache.put(cacheKey, cloned));
    }
    
    response = new Response(response.body, response);
    response.headers.set('X-Cache-Key', cacheUrl.toString());
    response.headers.set('X-Cache', 'MISS');
    return response;
  }
};

2.3 Vary 头的陷阱

Vary 头告诉缓存:对于同一个 URL,不同的请求头值可能得到不同的响应。滥用 Vary 会严重降低命中率:

Vary 头影响分析:

Vary: Accept-Encoding
  → 按 gzip / br / identity 分别缓存
  → 通常 2-3 个变体,影响有限 ✓

Vary: Accept-Language
  → 按语言分别缓存(en, zh, ja, ...)
  → 如果支持 10 种语言 → 10 个变体
  → 命中率下降到 1/10 ⚠

Vary: User-Agent
  → 几乎每个浏览器版本的 UA 都不同
  → 变体数量爆炸 → 命中率接近 0 ✗

Vary: Cookie
  → 每个用户的 Cookie 不同
  → 变体数量 = 用户数 → 完全不可缓存 ✗

Vary: *
  → 等同于不缓存 ✗

最佳实践:
  - 静态资源: Vary: Accept-Encoding(仅此一个)
  - 需要设备适配: 使用 CDN 的设备检测头替代 User-Agent
  - 需要语言适配: 用 URL 路径区分(/en/ /zh/)而非 Vary

2.4 缓存命中率监控

# cdn_metrics.py — CDN 缓存命中率监控
import time
from collections import Counter
from datetime import datetime

class CDNMetrics:
    """CDN 缓存指标采集器"""
    
    def __init__(self):
        self.cache_stats = Counter()    # {status: count}
        self.latency_samples = []       # [(timestamp, latency_ms)]
        self.origin_errors = Counter()  # {status_code: count}
        self.pop_distribution = Counter()  # {pop_id: count}
    
    def record_request(self, cache_status, latency_ms, pop_id, origin_status=None):
        """记录一次请求的指标"""
        self.cache_stats[cache_status] += 1
        self.latency_samples.append((time.time(), latency_ms))
        self.pop_distribution[pop_id] += 1
        
        if origin_status and origin_status >= 500:
            self.origin_errors[origin_status] += 1
    
    def cache_hit_ratio(self):
        """计算缓存命中率"""
        total = sum(self.cache_stats.values())
        if total == 0:
            return 0.0
        
        hits = self.cache_stats.get('HIT', 0) + \
               self.cache_stats.get('STALE', 0) + \
               self.cache_stats.get('REVALIDATED', 0) + \
               self.cache_stats.get('UPDATING', 0)
        
        return hits / total * 100
    
    def report(self):
        """生成指标报告"""
        total = sum(self.cache_stats.values())
        chr_value = self.cache_hit_ratio()
        
        print(f"=== CDN Metrics Report ===")
        print(f"Time: {datetime.now().isoformat()}")
        print(f"Total Requests: {total}")
        print(f"Cache Hit Ratio: {chr_value:.1f}%")
        print(f"")
        print(f"Cache Status Breakdown:")
        for status, count in self.cache_stats.most_common():
            pct = count / total * 100
            print(f"  {status:15s} {count:8d} ({pct:.1f}%)")
        
        if self.latency_samples:
            latencies = [l for _, l in self.latency_samples]
            latencies.sort()
            p50 = latencies[len(latencies) // 2]
            p99 = latencies[int(len(latencies) * 0.99)]
            print(f"")
            print(f"Latency:")
            print(f"  P50: {p50:.0f}ms")
            print(f"  P99: {p99:.0f}ms")
        
        if self.origin_errors:
            print(f"")
            print(f"Origin Errors:")
            for code, count in self.origin_errors.most_common():
                print(f"  HTTP {code}: {count}")
        
        if self.pop_distribution:
            print(f"")
            print(f"Top PoPs:")
            for pop, count in self.pop_distribution.most_common(5):
                pct = count / total * 100
                print(f"  {pop:20s} {count:8d} ({pct:.1f}%)")

Prometheus + Grafana 的 CDN 监控配置:

# prometheus/rules/cdn-alerts.yml
groups:
  - name: cdn_cache
    rules:
      # 缓存命中率低于 80% 告警
      - alert: CDNCacheHitRatioLow
        expr: |
          sum(rate(cdn_requests_total{cache_status=~"HIT|STALE|REVALIDATED"}[5m]))
          /
          sum(rate(cdn_requests_total[5m]))
          < 0.80
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: "CDN 缓存命中率低: {{ $value | humanizePercentage }}"
          description: "过去 15 分钟 CDN 缓存命中率低于 80%,请检查 Cache-Control 配置"
      
      # 回源 5xx 错误率高
      - alert: CDNOrigin5xxHigh
        expr: |
          sum(rate(cdn_origin_requests_total{status=~"5.."}[5m]))
          /
          sum(rate(cdn_origin_requests_total[5m]))
          > 0.05
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "CDN 回源 5xx 错误率高: {{ $value | humanizePercentage }}"
          description: "回源 5xx 错误率超过 5%,源站可能异常"
      
      # 缓存 BYPASS 比例异常
      - alert: CDNCacheBypassHigh
        expr: |
          sum(rate(cdn_requests_total{cache_status="BYPASS"}[5m]))
          /
          sum(rate(cdn_requests_total[5m]))
          > 0.30
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "CDN BYPASS 比例异常: {{ $value | humanizePercentage }}"

三、回源异常诊断

回源(Origin Fetch)失败是 CDN 最严重的故障类型——用户直接看到错误。

3.1 回源超时

回源超时的排查路径:

CDN 边缘 ──[回源请求]──→ 源站

超时类型:
1. 连接超时(Connect Timeout)
   → 无法建立 TCP 连接
   → 原因: 源站 IP 不可达 / 防火墙阻断 / 源站宕机
   → 排查: 从 CDN 边缘 ping/telnet 源站

2. 读取超时(Read Timeout)
   → TCP 连接建立,但源站响应太慢
   → 原因: 源站处理时间长 / 后端服务阻塞 / 数据库慢查询
   → 排查: 检查源站应用日志和慢查询

3. TLS 握手超时
   → TCP 连接建立,TLS 握手卡住
   → 原因: 源站 TLS 配置错误 / 证书问题 / SNI 不匹配
   → 排查: openssl s_client 测试 TLS 握手

模拟和排查回源超时:

# 1. 测试源站可达性(模拟 CDN 边缘视角)
# 使用 CDN 提供的诊断工具或从多个地区测试

# TCP 连接测试
$ nc -zv origin.example.com 443 -w 5
Connection to origin.example.com 443 port [tcp/https] succeeded!

# TLS 握手测试
$ echo | openssl s_client -servername origin.example.com \
    -connect origin.example.com:443 -brief
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384

# 2. 测量源站响应时间
$ curl -o /dev/null -s -w "
  连接: %{time_connect}s
  TLS:  %{time_appconnect}s
  TTFB: %{time_starttransfer}s
  总计: %{time_total}s
" https://origin.example.com/api/health

# 如果 TTFB > 10s → 源站处理太慢

# 3. 持续监控源站响应时间
$ while true; do
    ttfb=$(curl -o /dev/null -s -w "%{time_starttransfer}" \
        https://origin.example.com/api/health)
    echo "$(date '+%H:%M:%S') TTFB: ${ttfb}s"
    sleep 5
done

3.2 回源 5xx 错误

CDN 回源 5xx 错误分类:

502 Bad Gateway
  ├── CDN 无法连接源站
  ├── 源站返回了无效的 HTTP 响应
  └── 排查: 检查源站是否运行、防火墙规则

503 Service Unavailable
  ├── 源站主动返回 503(过载/维护)
  ├── CDN 的 Origin Health Check 失败
  └── 排查: 检查源站负载、健康检查配置

504 Gateway Timeout
  ├── CDN 等待源站响应超时
  ├── 常见于慢 API(数据库查询、外部调用)
  └── 排查: 检查源站响应时间、增大 CDN 超时配置

520-530 (Cloudflare 专用)
  520: 源站返回了未知错误
  521: 源站拒绝连接(Web Server Down)
  522: 连接超时
  523: 源站不可达
  524: 超时(A Timeout Occurred)
  525: SSL 握手失败
  526: 无效的 SSL 证书

3.3 回源流量突增(Origin Stampede)

当缓存大规模过期或被 Purge 后,所有请求同时回源,可能压垮源站:

缓存雪崩场景:

T=0:    热门内容被 Purge
T=0.1s: 1000 个并发请求同时回源
        ↓
        源站过载 → 响应变慢 → CDN 超时
        ↓
        更多请求回源(恶性循环)

防护机制:

1. Request Collapsing(请求合并)
   ┌──────────────────────────────────────────────┐
   │  100 个请求同时 MISS                          │
   │  → 只有 1 个请求回源                          │
   │  → 其他 99 个等待这个请求的响应                 │
   │  → 回源成功后,99 个请求从缓存返回              │
   └──────────────────────────────────────────────┘

2. Stale-While-Revalidate
   → 缓存过期后继续返回旧内容
   → 后台异步回源刷新
   → 源站零压力

3. Origin Shield(回源保护层)
   → 所有 PoP 的回源请求先到 Shield
   → Shield 做 Request Collapsing
   → 源站只看到 Shield 的请求

Nginx 配置 Request Collapsing:

# proxy_cache_lock — 对同一资源的并发回源请求排队
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;     # 等待锁的超时
proxy_cache_lock_age 5s;         # 锁的最大持有时间

# 超过 lock_timeout 后的行为:
# - 超时的请求也回源(防止单个慢请求阻塞所有人)
# - proxy_cache_lock_age 后自动释放锁

# stale-while-revalidate 支持
proxy_cache_use_stale updating;  # 更新时返回旧内容
proxy_cache_background_update on;  # 后台异步更新

# 完整的缓存配置
proxy_cache_path /var/cache/nginx levels=1:2
    keys_zone=cdn_cache:100m
    max_size=10g
    inactive=7d
    use_temp_path=off;

server {
    location / {
        proxy_cache cdn_cache;
        proxy_cache_valid 200 1h;
        proxy_cache_valid 404 1m;
        
        # Request Collapsing
        proxy_cache_lock on;
        proxy_cache_lock_timeout 5s;
        
        # Stale content
        proxy_cache_use_stale error timeout updating
            http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;
        
        # 添加调试头
        add_header X-Cache-Status $upstream_cache_status;
        add_header X-Cache-Key $scheme$proxy_host$request_uri;
        
        proxy_pass http://origin;
    }
}

四、内容不一致问题

用户报告”我看到的是旧内容”或”不同地区看到不同版本”——这是 CDN 缓存一致性问题。

4.1 诊断步骤

#!/bin/bash
# cdn-consistency-check.sh — 检查 CDN 内容一致性

URL="${1:-https://www.example.com/page}"

echo "=== CDN Content Consistency Check ==="
echo "URL: $URL"
echo ""

# 1. 获取源站原始内容的 ETag/Last-Modified
echo "--- Origin Response ---"
origin_headers=$(curl -sI -H "Cache-Control: no-cache" "$URL")
origin_etag=$(echo "$origin_headers" | grep -i "etag" | awk '{print $2}' | tr -d '\r"')
origin_lm=$(echo "$origin_headers" | grep -i "last-modified" | cut -d: -f2- | tr -d '\r')
origin_cl=$(echo "$origin_headers" | grep -i "content-length" | awk '{print $2}' | tr -d '\r')
echo "  ETag: $origin_etag"
echo "  Last-Modified: $origin_lm"
echo "  Content-Length: $origin_cl"
echo ""

# 2. 多次请求检查缓存一致性
echo "--- Cache Consistency (10 requests) ---"
for i in $(seq 1 10); do
    resp=$(curl -sI "$URL")
    
    etag=$(echo "$resp" | grep -i "etag" | awk '{print $2}' | tr -d '\r"')
    age=$(echo "$resp" | grep -i "^age:" | awk '{print $2}' | tr -d '\r')
    cache=$(echo "$resp" | grep -i "cf-cache-status\|x-cache" | head -1 | awk '{print $2}' | tr -d '\r')
    pop=$(echo "$resp" | grep -i "cf-ray\|x-served-by\|x-amz-cf-pop" | head -1 | tr -d '\r')
    cl=$(echo "$resp" | grep -i "content-length" | awk '{print $2}' | tr -d '\r')
    
    match="✓"
    if [ "$etag" != "$origin_etag" ] && [ -n "$origin_etag" ] && [ -n "$etag" ]; then
        match="✗ ETag MISMATCH"
    fi
    if [ "$cl" != "$origin_cl" ] && [ -n "$origin_cl" ] && [ -n "$cl" ]; then
        match="✗ Size MISMATCH"
    fi
    
    printf "  #%02d cache=%-12s age=%-6s etag=%-20s size=%-8s %s\n" \
        "$i" "$cache" "${age:-N/A}" "${etag:-N/A}" "${cl:-N/A}" "$match"
    
    sleep 0.3
done

4.2 常见不一致原因

不一致原因及修复:

1. Purge 未完全传播
   原因: Purge 请求发出后,部分 PoP 还未收到
   表现: 不同地区用户看到不同版本
   修复: 等待传播完成(通常 2-30 秒),或使用版本化 URL

2. 源站返回不一致
   原因: 源站多实例部署时内容未同步
   表现: 相同 URL 有时返回新内容有时返回旧内容
   排查: 多次请求源站检查响应是否一致
   修复: 确保源站部署原子性

3. Cache Key 设计问题
   原因: 相同内容的 URL 被视为不同 Cache Key
   表现: 命中率低,多次请求结果不一致
   修复: 规范化 Cache Key(排序/过滤 Query String)

4. Vary 头导致多变体
   原因: Vary: Accept-Language 导致每种语言单独缓存
   表现: 同一 URL 在不同浏览器设置下内容不同(符合预期)
   修复: 确认是否需要变体缓存,不需要则移除 Vary

5. stale-while-revalidate 窗口
   原因: 缓存过期后返回旧内容同时后台刷新
   表现: 短暂看到旧内容,刷新后变新
   修复: 这是预期行为,减少 SWR 窗口可缩短不一致时间

4.3 使用版本化 URL 避免一致性问题

版本化 URL 策略:

方案 1: 文件名哈希
  /static/app.js      → /static/app.a1b2c3d4.js
  /static/style.css   → /static/style.e5f6g7h8.css
  + 可以设置超长 TTL(1 年)
  + 内容更新时 URL 变化,自动绕过缓存
  - 需要构建工具支持

方案 2: Query String 版本
  /static/app.js?v=1.2.3
  + 实现简单
  - 某些 CDN 不缓存带 Query String 的 URL
  - 中间代理可能忽略 Query String

方案 3: 路径版本
  /v1.2.3/static/app.js
  + CDN 友好
  + 可以同时保留多个版本
  - 需要服务端路由支持

推荐: 方案 1(文件名哈希)用于静态资源
      方案 3(路径版本)用于 API

五、CDN 性能诊断

5.1 延迟分解

# 完整的 CDN 性能诊断
$ curl -o /dev/null -s -w @- https://www.example.com/ << 'EOF'
==== CDN Performance Breakdown ====
DNS Lookup:      %{time_namelookup}s
TCP Connect:     %{time_connect}s
TLS Handshake:   %{time_appconnect}s
TTFB:            %{time_starttransfer}s
Total:           %{time_total}s
Download Size:   %{size_download} bytes
Download Speed:  %{speed_download} bytes/s
Remote IP:       %{remote_ip}
HTTP Code:       %{http_code}
=====================================
EOF

# 解读指标:
# DNS Lookup:   > 50ms → DNS 解析慢,检查 DNS 配置
# TCP Connect:  > 100ms → 到 CDN PoP 距离远,检查 Anycast
# TLS Handshake: > 200ms → TLS 配置不优,检查 TLS 版本/OCSP
# TTFB:         > 500ms → 可能回源了,检查缓存状态
# Total:        > 1s → 整体偏慢,需要逐项分析

5.2 多地域性能对比

#!/bin/bash
# cdn-performance-map.sh — 多地域 CDN 性能测试

URL="${1:-https://www.example.com/}"

# 使用不同 DNS 解析器模拟不同地区
declare -A RESOLVERS=(
    ["Cloudflare"]="1.1.1.1"
    ["Google"]="8.8.8.8"
    ["Quad9"]="9.9.9.9"
)

echo "=== Multi-Region CDN Performance ==="
echo "URL: $URL"
echo ""

printf "%-15s %-15s %-8s %-8s %-8s %-8s %-8s %-15s\n" \
    "Resolver" "IP" "DNS" "TCP" "TLS" "TTFB" "Total" "Cache"

for name in "${!RESOLVERS[@]}"; do
    resolver=${RESOLVERS[$name]}
    
    result=$(curl -o /dev/null -s --resolve "$(echo $URL | awk -F/ '{print $3}'):443:$(dig +short @$resolver $(echo $URL | awk -F/ '{print $3}') | head -1)" \
        -w "%{time_namelookup} %{time_connect} %{time_appconnect} %{time_starttransfer} %{time_total} %{remote_ip}" \
        "$URL" 2>/dev/null)
    
    cache=$(curl -sI "$URL" 2>/dev/null | grep -i "cf-cache-status\|x-cache" | head -1 | awk '{print $2}' | tr -d '\r')
    
    read dns tcp tls ttfb total ip <<< "$result"
    
    printf "%-15s %-15s %-8s %-8s %-8s %-8s %-8s %-15s\n" \
        "$name" "$ip" \
        "$(echo "$dns * 1000" | bc)ms" \
        "$(echo "$tcp * 1000" | bc)ms" \
        "$(echo "$tls * 1000" | bc)ms" \
        "$(echo "$ttfb * 1000" | bc)ms" \
        "$(echo "$total * 1000" | bc)ms" \
        "${cache:-N/A}"
done

5.3 CDN 日志分析

大多数 CDN 提供商支持实时日志或日志推送,用于事后分析:

CDN 日志关键字段:

字段                    │ 用途
────────────────────────┼───────────────────────────────
timestamp               │ 请求时间
client_ip               │ 用户 IP(或代理 IP)
edge_pop                │ 处理请求的边缘节点
cache_status            │ HIT/MISS/BYPASS/EXPIRED
origin_response_time    │ 回源耗时(ms)
total_time              │ 请求总耗时(ms)
status_code             │ HTTP 状态码
request_url             │ 请求 URL
content_type            │ 响应内容类型
bytes_sent              │ 响应大小
tls_version             │ TLS 版本
http_protocol           │ HTTP/1.1 或 HTTP/2
user_agent              │ 客户端标识

分析脚本示例:

# 从 CDN 日志中分析缓存命中率(假设日志格式已解析为 TSV)
# 字段: timestamp client_ip cache_status status_code url edge_pop ttfb

# 1. 缓存命中率
echo "=== Cache Hit Ratio ==="
awk -F'\t' '{stats[$3]++; total++} END {
    for (s in stats) printf "  %-15s %8d (%5.1f%%)\n", s, stats[s], stats[s]/total*100
}' cdn_access.log

# 2. 回源慢请求 Top 10
echo "=== Slowest Origin Requests ==="
awk -F'\t' '$3 == "MISS" && $7 > 1000 {print $7"ms\t"$5}' cdn_access.log | \
    sort -rn | head -10

# 3. 5xx 错误的 URL 分布
echo "=== 5xx Errors by URL ==="
awk -F'\t' '$4 >= 500 {urls[$5]++} END {
    for (u in urls) print urls[u], u
}' cdn_access.log | sort -rn | head -20

# 4. 各 PoP 节点的命中率
echo "=== Cache Hit Ratio by PoP ==="
awk -F'\t' '{
    pop_total[$6]++
    if ($3 == "HIT" || $3 == "STALE" || $3 == "REVALIDATED")
        pop_hit[$6]++
} END {
    for (p in pop_total) {
        hit = pop_hit[p]+0
        ratio = hit / pop_total[p] * 100
        printf "  %-20s %8d total, %5.1f%% hit\n", p, pop_total[p], ratio
    }
}' cdn_access.log | sort -t',' -k2 -rn

六、CDN 调试工具集

6.1 命令行工具速查

# === CDN 调试命令速查表 ===

# 1. 查看完整响应头
curl -sI https://www.example.com/

# 2. 跳过缓存直接回源
curl -H "Cache-Control: no-cache" -H "Pragma: no-cache" https://www.example.com/

# 3. 查看请求经过的所有代理
curl -sI https://www.example.com/ | grep -i via

# 4. 检查特定 CDN 的 PoP 节点
curl -sI https://www.example.com/ | grep -i "cf-ray\|x-served-by\|x-amz-cf-pop"

# 5. 测试特定 PoP 节点(通过 DNS 解析到特定 IP)
curl -sI --resolve www.example.com:443:104.16.1.1 https://www.example.com/

# 6. 查看 CDN 解析结果
dig www.example.com +short
dig www.example.com +trace

# 7. 查看 CNAME 链
dig www.example.com CNAME +short
# www.example.com.cdn.cloudflare.net.

# 8. HTTP/2 帧级别调试
curl -v --http2 https://www.example.com/ 2>&1 | grep -E "^[<>*]"

# 9. TLS 握手详情
openssl s_client -servername www.example.com \
    -connect www.example.com:443 -brief

# 10. 测量 CDN vs 源站的性能差异
echo "CDN:"
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s Total: %{time_total}s\n" \
    https://www.example.com/

echo "Origin (bypass CDN):"
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s Total: %{time_total}s\n" \
    https://origin.example.com/

6.2 排查决策树

CDN 问题排查决策树:

用户报告"页面加载慢"
├── 检查 CDN 缓存状态
│   ├── HIT → 不是 CDN 缓存问题
│   │   ├── 检查 Age 值(是否很大 → 内容可能过旧)
│   │   └── 检查传输延迟(DNS/TCP/TLS)
│   ├── MISS → 缓存未命中
│   │   ├── 检查 Cache-Control 头
│   │   ├── 检查 Query String 是否导致碎片化
│   │   └── 检查 Origin Shield 是否启用
│   ├── BYPASS → 缓存被跳过
│   │   ├── 检查是否有 Cookie 导致 Bypass
│   │   ├── 检查 CDN 规则配置
│   │   └── 检查请求头中是否有 Authorization
│   └── DYNAMIC → 被标记为动态
│       ├── 检查 Content-Type
│       └── 在 CDN 规则中显式启用缓存

用户报告"看到旧内容"
├── 检查 Age 值(过大说明缓存未更新)
├── 检查是否发起了 Purge
├── 检查 Purge 是否传播到所有 PoP
└── 检查源站是否返回了一致的内容

用户报告"页面报错"
├── 检查 HTTP 状态码
│   ├── 502/504 → 回源异常
│   │   ├── 检查源站是否运行
│   │   ├── 检查回源连接(TCP/TLS)
│   │   └── 检查回源超时配置
│   ├── 520-530 → Cloudflare 专用错误
│   │   └── 参考 Cloudflare 错误码文档
│   └── 403 → 可能被 CDN WAF 拦截
│       └── 检查 WAF 规则和日志
└── 检查是否是特定地区/PoP 的问题
    └── 从多个地区测试对比

七、总结

CDN 故障调试的核心方法论:

  1. 读响应头X-CacheAgeViaCF-Ray 等头部是 CDN 状态的第一手信息
  2. 测缓存命中率:CHR 是 CDN 效果的核心指标,低于 80% 需要排查 Cache Key 和 Cache-Control
  3. 查回源链路:回源超时和 5xx 是最严重的问题,需要同时排查 CDN 配置和源站状态
  4. 建监控体系:Prometheus + 告警,覆盖命中率、回源错误率、延迟 P99

上一篇: CDN 与 HTTPS:边缘 TLS、证书管理与安全 下一篇: Socket 编程模型演进:从阻塞到多路复用

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-22 · network

网络工程索引

汇总本站网络工程系列文章,覆盖分层模型、以太网、IP、TCP、DNS、TLS、HTTP/2/3、CDN、BGP 与故障诊断。


By .