找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1666

积分

0

好友

216

主题
发表于 14 小时前 | 查看: 2| 回复: 0

概述

背景介绍

想象一下,当一位广州的用户访问部署在北京的网站时,仅仅是物理距离带来的网络延迟就可能高达30-50毫秒。再加上TCP握手、TLS协商、服务器处理等环节,首字节时间轻松超过200毫秒。内容分发网络的核心思路就源于此——将内容推送到离用户最近的边缘节点,通过缩短物理链路和减少回源次数来提升访问速度。

一次典型的CDN请求链路是这样的:用户发起DNS查询,智能DNS根据用户的地理位置返回最近的边缘节点IP。边缘节点首先查找本地缓存,如果命中则直接返回内容。如果未命中,则会向中间层节点请求,若中间层也未命中,最终才回源站拉取内容,并在返回给用户的路上逐级缓存。这套多级缓存架构能将源站的请求流量压缩到总流量的5%到15%。

要搭建一套高效的CDN,涉及以下几个关键技术栈:

  • 智能DNS调度:利用GeoDNS根据地理位置进行解析,或者使用Anycast技术让多个节点共享同一个IP,由网络层将用户路由到最近的节点。
  • 内容缓存:基于HTTP缓存协议实现内容的存储和校验,核心是理解 Cache-ControlETagLast-Modified 等头部。
  • 回源优化:通过长连接复用减少TCP握手开销,并使用HTTP/2等多路复用协议降低回源延迟。
  • HTTPS加速:采用TLS 1.3将握手过程从2-RTT减少到1-RTT,利用OCSP Stapling消除证书验证的额外请求,并通过Session Resumption实现0-RTT连接恢复。

技术特点

  • 就近访问:边缘节点覆盖主要运营商和地区,用户请求可在同城甚至同机房完成响应。
  • 多级缓存:L1边缘节点 → L2中间层 → 源站,逐级收敛回源流量,可将源站压力降低85%以上。
  • 智能调度:基于实时网络质量、节点负载和地理位置进行综合决策,动态选择最优节点。
  • 协议优化:结合TLS 1.3、HTTP/2、Brotli压缩等技术栈,显著降低传输延迟和带宽消耗。
  • 故障容灾:节点故障自动摘除,支持在源站宕机时返回过期缓存进行兜底。

自建 CDN vs 云 CDN 对比:

维度 自建 CDN(Nginx/Varnish) 云 CDN(阿里云/CloudFront)
节点覆盖 受限于自有机房,通常 5-20 个节点 全球数百到数千个 PoP 节点
初始成本 高(服务器采购、带宽、机房) 低(按量付费,无前期投入)
运维复杂度 高(需自行处理节点部署、监控、故障切换) 低(全托管,开箱即用)
定制灵活性 高(VCL/Lua 可编程,缓存策略完全可控) 中(受限于厂商提供的配置项)
适用规模 中小规模或特殊合规要求 任意规模,尤其适合全球化业务

缓存代理软件对比:

特性 Nginx(proxy_cache) Varnish 7.x Squid Apache Traffic Server
性能 高,事件驱动 极高,内存缓存为主 中等 高,大规模部署验证
配置语言 nginx.conf 指令 VCL(专用 DSL) squid.conf records.config
缓存存储 磁盘 + 共享内存 内存(可扩展磁盘) 磁盘 + 内存 磁盘 + 内存分层
HTTPS 支持 原生支持 需前置 Hitch/Nginx 原生支持 原生支持
可编程性 Lua(OpenResty) VCL + VMOD 有限 插件 + Lua
社区活跃度 极高 下降 中等(Apache 基金会)
推荐场景 全站加速 + 反向代理一体 纯缓存加速,性能极致 正向代理 大规模 CDN 平台

适用场景

  • 静态资源加速:图片、CSS、JS、字体文件,缓存命中率可达95%以上。
  • 全站加速:静态资源走缓存,动态请求通过智能路由优化链路。
  • 视频点播/直播:对大文件进行分片缓存,对直播流进行边缘分发。
  • API 加速:对响应可缓存的GET接口设置短时缓存,降低后端压力。
  • 安全防护:边缘节点天然具备DDoS吸收能力,可配合WAF规则过滤恶意请求。

环境要求

组件 版本要求 说明
操作系统 Ubuntu 22.04+ / Rocky Linux 9+ 推荐 Ubuntu 22.04 LTS
Nginx 1.26.x 需编译 ngx_cache_purge、Brotli 模块
Varnish 7.x 推荐 7.5+,VCL 4.1 语法
OpenSSL 3.x TLS 1.3 支持,OCSP Stapling
硬件配置 8C16G 起步,SSD 存储 缓存盘建议独立 NVMe SSD
网络 1Gbps+ 带宽 边缘节点建议 10Gbps

详细步骤

HTTP 缓存机制详解

强缓存

强缓存命中时,浏览器直接使用本地缓存,不发送任何请求到服务器,状态码显示 200 (from disk cache)200 (from memory cache)

Cache-Control 指令(HTTP/1.1,优先级高于 Expires):

Cache-Control: public, max-age=31536000, s-maxage=86400
  • max-age=N:客户端缓存有效期,单位秒。max-age=31536000 表示缓存一年,适合带哈希的静态资源。
  • s-maxage=N:CDN/代理缓存有效期,会覆盖 max-age。源站可以让浏览器缓存1小时,而CDN缓存24小时。
  • public:允许任何中间缓存存储响应。
  • private:仅允许浏览器缓存,CDN不得缓存(用于包含用户个人数据的响应)。
  • no-cache:不是“不缓存”,而是每次使用前必须向服务器验证。
  • no-store:真正的不缓存,任何环节都不存储。
  • stale-while-revalidate=N:缓存过期后N秒内,先返回旧缓存,后台异步回源更新。

Expires(HTTP/1.0 遗留,仅作兜底):

Expires: Thu, 01 Jan 2027 00:00:00 GMT

绝对时间格式,依赖客户端和服务器时钟同步,生产环境应以 Cache-Control 为准。

协商缓存

强缓存过期后,浏览器会携带验证信息向服务器确认内容是否变化。如果未变化,服务器返回 304 Not Modified,节省传输带宽。

ETag / If-None-Match(优先级高):

# 首次响应
ETag: "a1b2c3d4e5f6"

# 后续请求
If-None-Match: "a1b2c3d4e5f6"

ETag 是内容的哈希指纹,内容不变则值不变。注意:Nginx默认使用 last_modified_time-content_length 生成 ETag,多台源站需确保文件修改时间和大小一致,否则会导致同一文件在不同源站产生不同的 ETag。

Last-Modified / If-Modified-Since:

# 首次响应
Last-Modified: Wed, 25 Feb 2026 10:00:00 GMT

# 后续请求
If-Modified-Since: Wed, 25 Feb 2026 10:00:00 GMT

精度只到秒级,1秒内多次修改无法感知。当 ETag 和 Last-Modified 同时存在时,服务器会校验两者。

缓存决策流程

请求到达 CDN 节点
 ├─ 查找缓存键(Cache Key)对应的缓存条目
 │   ├─ 未找到 → MISS → 回源获取 → 缓存并返回
 │   └─ 找到缓存条目
 │       ├─ 未过期(max-age/s-maxage 内)→ HIT → 直接返回
 │       └─ 已过期
 │           ├─ 有 ETag/Last-Modified → 带条件回源验证
 │           │   ├─ 源站返回 304 → REVALIDATED → 刷新过期时间,返回缓存
 │           │   └─ 源站返回 200 → EXPIRED → 更新缓存,返回新内容
 │           └─ 无验证信息 → EXPIRED → 回源获取新内容
 └─ Cache-Control: no-store → BYPASS → 直接回源,不缓存

Vary 头与缓存键设计

Vary 头告诉缓存服务器:相同URL但不同请求头值的响应需要分别缓存。

Vary: Accept-Encoding

这意味着 Accept-Encoding: gzipAccept-Encoding: br 的响应会被分别缓存。常见的 Vary 值:

  • Accept-Encoding:按压缩算法区分。
  • Accept-Language:多语言站点按语言区分。
  • Cookie:慎用,Cookie变化多会导致缓存命中率暴跌。

缓存键默认是 scheme + host + URI,可根据业务需要加入查询参数、特定Header等。原则是:缓存键越简单,命中率越高;包含的变量越多,缓存碎片越严重。

Nginx 缓存代理配置

缓存路径与基础配置

# /etc/nginx/conf.d/cache_zone.conf
# 定义缓存存储区域(放在 http 块内)

# 缓存路径配置
# levels=1:2 表示两级目录结构,避免单目录文件过多影响性能
# keys_zone=cdn_cache:256m 在共享内存中分配256MB存储缓存键索引
# max_size=50g 磁盘缓存上限50GB,超出后LRU淘汰
# inactive=7d 7天内未被访问的缓存自动清除
# use_temp_path=off 直接写入缓存目录,避免跨磁盘拷贝
proxy_cache_path /data/nginx/cache
    levels=1:2
    keys_zone=cdn_cache:256m
    max_size=50g
    inactive=7d
    use_temp_path=off;

# 缓存键设计:scheme + host + URI + 查询参数
# 不要把 Cookie 放进缓存键,否则命中率会很低
proxy_cache_key "$scheme$host$request_uri";

缓存策略与回源控制

# /etc/nginx/conf.d/cdn_proxy.conf

server {
    listen 80;
    server_name cdn.example.com;

    # 添加缓存状态头,方便排查(HIT/MISS/EXPIRED/BYPASS)
    add_header X-Cache-Status $upstream_cache_status always;

    # 静态资源缓存
    location ~* \.(jpg|jpeg|png|gif|webp|ico|css|js|woff2|ttf|svg)$ {
        proxy_pass http://backend_origin;
        proxy_cache cdn_cache;

        # 按源站响应码设置缓存时间
        proxy_cache_valid 200 301 7d;      # 正常响应缓存 7 天
        proxy_cache_valid 302 1h;          # 临时重定向缓存 1 小时
        proxy_cache_valid 404 1m;          # 404 缓存 1 分钟,防止穿透
        proxy_cache_valid any 0;           # 其他状态码不缓存

        # 缓存锁:同一资源并发请求时,只有第一个回源,其余等待
        # 防止缓存击穿(热点资源过期瞬间大量请求打到源站)
        proxy_cache_lock on;
        proxy_cache_lock_timeout 5s;       # 等待超时后放行回源
        proxy_cache_lock_age 5s;           # 锁持有超时,防止死锁

        # 源站故障时返回过期缓存(兜底策略)
        proxy_cache_use_stale error timeout updating
                                          http_500 http_502 http_503 http_504;

        # 后台异步更新过期缓存,用户不感知延迟
        proxy_cache_background_update on;

        # 缓存最小使用次数,冷门资源不缓存(减少磁盘写入)
        proxy_cache_min_uses 2;

        # 传递必要头部给源站
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        expires 30d;                       # 客户端浏览器缓存 30 天
    }

    # 动态请求不缓存,直接代理
    location / {
        proxy_pass http://backend_origin;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # 带 Cookie 或特定参数的请求绕过缓存
        proxy_cache_bypass $http_cookie;
        proxy_no_cache $http_cookie;
    }
}

缓存清除(Purge)

需要编译 ngx_cache_purge 模块。

# 缓存清除接口(仅允许内网访问)
location ~ /purge(/.*) {
    allow 10.0.0.0/8;
    allow 172.16.0.0/12;
    deny all;
    proxy_cache_purge cdn_cache "$scheme$host$1";
}
# 清除指定资源缓存
curl -X PURGE http://cdn.example.com/purge/static/css/main.abc123.css

Varnish 缓存加速

Varnish 架构原理

Varnish 的请求处理是一条VCL状态机流水线:

客户端请求 → vcl_recv(接收请求,决定是否查缓存)
 → vcl_hash(计算缓存键)
 → 缓存查找
    ├─ 命中 → vcl_hit(决定是否直接返回)→ vcl_deliver(返回响应)
    └─ 未命中 → vcl_miss → vcl_backend_fetch(回源)
       → vcl_backend_response(处理源站响应,决定是否缓存)
       → vcl_deliver(返回响应)

Varnish 默认将缓存存储在内存中(malloc),性能极高。大容量场景可使用 file 存储引擎或商业版的MSE。

VCL 配置

# /etc/varnish/default.vcl
vcl 4.1;

# 源站定义
backend default {
    .host = "10.0.1.10";
    .port = "80";
    .connect_timeout = 3s;          # 连接超时
    .first_byte_timeout = 10s;      # 首字节超时
    .between_bytes_timeout = 5s;    # 字节间超时
    .max_connections = 256;         # 最大连接数

    # 健康检查
    .probe = {
        .url = "/health";
        .interval = 5s;
        .timeout = 2s;
        .window = 5;
        .threshold = 3;
    }
}

# 接收请求阶段
sub vcl_recv {
    # 缓存清除接口(仅允许内网)
    if (req.method == "PURGE") {
        if (!client.ip ~ internal) {
            return (synth(403, "Forbidden"));
        }
        return (purge);
    }

    # BAN 模式批量失效(正则匹配)
    if (req.method == "BAN") {
        if (!client.ip ~ internal) {
            return (synth(403, "Forbidden"));
        }
        ban("req.url ~ " + req.http.X-Ban-Url);
        return (synth(200, "Banned"));
    }

    # 只缓存 GET 和 HEAD 请求
    if (req.method != "GET" && req.method != "HEAD") {
        return (pass);
    }

    # 移除不影响缓存的 Cookie(保留登录态相关的)
    # 静态资源完全去除 Cookie
    if (req.url ~ "\.(jpg|jpeg|png|gif|webp|css|js|woff2|svg|ico)(\?.*)?$") {
        unset req.http.Cookie;
        return (hash);
    }

    # 动态页面带 session Cookie 的不缓存
    if (req.http.Cookie ~ "session_id") {
        return (pass);
    }

    # 其他请求去除无关 Cookie 后尝试缓存
    unset req.http.Cookie;
    return (hash);
}

# 源站响应处理阶段
sub vcl_backend_response {
    # 静态资源缓存 7 天
    if (bereq.url ~ "\.(jpg|jpeg|png|gif|webp|css|js|woff2|svg|ico)(\?.*)?$") {
        set beresp.ttl = 7d;
        set beresp.grace = 24h;     # Grace 期:源站故障时可返回过期 24 小时内的缓存
        unset beresp.http.Set-Cookie;
    }

    # HTML 页面缓存 5 分钟
    if (beresp.http.Content-Type ~ "text/html") {
        set beresp.ttl = 5m;
        set beresp.grace = 1h;
    }

    # 源站返回 5xx 时不缓存错误页面
    if (beresp.status >= 500) {
        set beresp.ttl = 0s;
        set beresp.uncacheable = true;
        return (deliver);
    }
}

# 返回响应阶段
sub vcl_deliver {
    # 添加缓存命中标识
    if (obj.hits > 0) {
        set resp.http.X-Cache = "HIT";
        set resp.http.X-Cache-Hits = obj.hits;
    } else {
        set resp.http.X-Cache = "MISS";
    }
}

# 内网 ACL
acl internal {
    "10.0.0.0"/8;
    "172.16.0.0"/12;
    "192.168.0.0"/16;
}

全站 HTTPS 加速配置

TLS 1.3 与性能优化

# /etc/nginx/conf.d/ssl.conf

server {
    listen 443 ssl http2;
    server_name cdn.example.com;

    # 证书配置
    ssl_certificate /etc/nginx/ssl/cdn.example.com.pem;
    ssl_certificate_key /etc/nginx/ssl/cdn.example.com.key;

    # 协议版本:只保留 TLS 1.2 和 1.3
    ssl_protocols TLSv1.2 TLSv1.3;

    # TLS 1.3 密码套件(性能优先)
    ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;

    # OCSP Stapling:由服务器代替客户端查询证书吊销状态
    # 省去客户端单独请求 CA 的 OCSP 服务器,减少 100-300ms 延迟
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/nginx/ssl/ca-chain.pem;
    resolver 8.8.8.8 223.5.5.5 valid=300s;
    resolver_timeout 5s;

    # Session 复用:避免每次连接都做完整 TLS 握手
    ssl_session_cache shared:SSL:50m;      # 50MB 共享缓存,约可存 20 万个 Session
    ssl_session_timeout 1d;                # Session 有效期 1 天
    ssl_session_tickets on;                # Session Ticket 支持分布式场景
    # 多台 Nginx 需共享 ticket key 文件
    # ssl_session_ticket_key /etc/nginx/ssl/ticket.key;

    # 103 Early Hints
    location / {
        add_header Link "</static/css/main.css>; rel=preload; as=style" always;
        add_header Link "</static/js/app.js>; rel=preload; as=script" always;
        proxy_pass http://backend_origin;
    }
}

Brotli 与 Gzip 压缩

# /etc/nginx/conf.d/compression.conf
# Brotli 需编译 ngx_brotli 模块

# Brotli 压缩(压缩率比 Gzip 高 15-25%)
brotli on;
brotli_comp_level 6;                       # 1-11,6 是性能和压缩率的平衡点
brotli_min_length 256;                     # 小于 256 字节不压缩
brotli_types text/plain text/css text/javascript
             application/javascript application/json
             application/xml image/svg+xml;

# Gzip 兜底(不支持 Brotli 的客户端)
gzip on;
gzip_comp_level 5;                         # 1-9,5 是常用平衡点
gzip_min_length 256;
gzip_vary on;                              # 输出 Vary: Accept-Encoding
gzip_types text/plain text/css text/javascript
           application/javascript application/json
           application/xml image/svg+xml;

回源策略优化

长连接复用与超时控制

# /etc/nginx/conf.d/upstream.conf

upstream backend_origin {
    server 10.0.1.10:80 weight=5;
    server 10.0.1.11:80 weight=3;
    server 10.0.1.12:80 backup;            # 备用源站,主源全挂时启用

    # 长连接复用:减少 TCP 握手开销
    keepalive 64;                          # 每个 worker 保持 64 个空闲长连接
    keepalive_timeout 60s;                 # 空闲连接超时
    keepalive_requests 1000;               # 单连接最大请求数
}

server {
    location / {
        proxy_pass http://backend_origin;

        # 必须设置 HTTP/1.1 和清空 Connection 头才能启用 keepalive
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        # 回源超时控制
        proxy_connect_timeout 5s;          # 连接源站超时
        proxy_send_timeout 10s;            # 发送请求超时
        proxy_read_timeout 30s;            # 读取响应超时(大文件适当加大)

        # 失败重试:连接错误或超时时尝试下一台源站
        proxy_next_upstream error timeout http_502 http_503;
        proxy_next_upstream_tries 2;       # 最多重试 2 次
        proxy_next_upstream_timeout 10s;   # 重试总超时
    }
}

Range 回源(大文件分片)

大文件按客户端请求的 Range 分片回源,减少带宽和存储压力。

# 大文件分片回源
location ~* \.(mp4|zip|iso|tar\.gz)$ {
    proxy_pass http://backend_origin;
    proxy_cache cdn_cache;

    # 启用 Range 回源,CDN 按分片缓存
    slice 1m;                              # 每片 1MB
    proxy_cache_key "$scheme$host$uri$slice_range";
    proxy_set_header Range $slice_range;
    proxy_cache_valid 200 206 7d;          # 206 Partial Content 也缓存
    proxy_http_version 1.1;
}

回源请求头改写

# 传递真实客户端信息给源站
proxy_set_header Host $host;                       # 保持原始 Host
proxy_set_header X-Real-IP $remote_addr;           # 真实客户端 IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;        # 原始协议(http/https)

# 自定义回源标识,源站可据此区分 CDN 回源和直接访问
proxy_set_header X-CDN-Node $hostname;
proxy_set_header X-CDN-Request-ID $request_id;

示例代码和配置

Nginx 全站加速完整配置

# /etc/nginx/nginx.conf
user nginx;
worker_processes auto;                     # 自动匹配 CPU 核数
worker_rlimit_nofile 65535;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;

events {
    worker_connections 16384;
    use epoll;
    multi_accept on;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # 日志格式:包含缓存状态字段
    log_format cdn '$remote_addr - [$time_local] '
                   '"$request" $status $body_bytes_sent '
                   '"$http_referer" "$http_user_agent" '
                   'cache:$upstream_cache_status '
                   'rt:${request_time}s '
                   'upstream:$upstream_addr';

    access_log /var/log/nginx/access.log cdn;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;

    # 缓存存储区域
    proxy_cache_path /data/nginx/cache
        levels=1:2
        keys_zone=cdn_cache:256m
        max_size=50g
        inactive=7d
        use_temp_path=off;

    proxy_cache_key "$scheme$host$request_uri";

    # Brotli 压缩
    brotli on;
    brotli_comp_level 6;
    brotli_min_length 256;
    brotli_types text/plain text/css text/javascript
                 application/javascript application/json
                 application/xml image/svg+xml;

    # Gzip 兜底
    gzip on;
    gzip_comp_level 5;
    gzip_min_length 256;
    gzip_vary on;
    gzip_types text/plain text/css text/javascript
               application/javascript application/json
               application/xml image/svg+xml;

    # 源站集群
    upstream backend_origin {
        server 10.0.1.10:80 weight=5;
        server 10.0.1.11:80 weight=3;
        server 10.0.1.12:80 backup;
        keepalive 64;
        keepalive_timeout 60s;
        keepalive_requests 1000;
    }

    # HTTP → HTTPS 强制跳转
    server {
        listen 80;
        server_name cdn.example.com;
        return 301 https://$host$request_uri;
    }

    # HTTPS 主配置
    server {
        listen 443 ssl http2;
        server_name cdn.example.com;

        ssl_certificate /etc/nginx/ssl/cdn.example.com.pem;
        ssl_certificate_key /etc/nginx/ssl/cdn.example.com.key;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
        ssl_stapling on;
        ssl_stapling_verify on;
        ssl_trusted_certificate /etc/nginx/ssl/ca-chain.pem;
        ssl_session_cache shared:SSL:50m;
        ssl_session_timeout 1d;
        resolver 8.8.8.8 223.5.5.5 valid=300s;

        # 缓存状态响应头
        add_header X-Cache-Status $upstream_cache_status always;

        # 缓存清除接口
        location ~ /purge(/.*) {
            allow 10.0.0.0/8;
            allow 172.16.0.0/12;
            deny all;
            proxy_cache_purge cdn_cache "$scheme$host$1";
        }

        # 静态资源:长缓存
        location ~* \.(jpg|jpeg|png|gif|webp|ico|css|js|woff2|ttf|svg)$ {
            proxy_pass http://backend_origin;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_cache cdn_cache;
            proxy_cache_valid 200 301 7d;
            proxy_cache_valid 404 1m;
            proxy_cache_lock on;
            proxy_cache_lock_timeout 5s;
            proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
            proxy_cache_background_update on;
            proxy_cache_min_uses 2;

            expires 30d;
        }

        # 大文件分片缓存
        location ~* \.(mp4|zip|iso|tar\.gz)$ {
            proxy_pass http://backend_origin;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            proxy_set_header Host $host;

            proxy_cache cdn_cache;
            slice 1m;
            proxy_cache_key "$scheme$host$uri$slice_range";
            proxy_set_header Range $slice_range;
            proxy_cache_valid 200 206 7d;
        }

        # 动态请求:直接代理
        location / {
            proxy_pass http://backend_origin;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_connect_timeout 5s;
            proxy_read_timeout 30s;
            proxy_next_upstream error timeout http_502 http_503;
            proxy_next_upstream_tries 2;
        }
    }
}

缓存预热脚本

#!/bin/bash
# cdn_warmup.sh - CDN 缓存预热脚本
# 从 sitemap 或 URL 列表批量预热缓存
set -euo pipefail

# ========== 配置区 ==========
CDN_DOMAIN="https://cdn.example.com"
URL_LIST="/opt/cdn/warmup_urls.txt" # URL 列表文件,每行一个路径
SITEMAP_URL="" # 可选:从 sitemap.xml 提取 URL
CONCURRENCY=10                        # 并发数,根据源站承受能力调整
LOG_FILE="/var/log/cdn/warmup_$(date +%Y%m%d_%H%M%S).log"
TIMEOUT=10                            # 单个请求超时(秒)
USER_AGENT="CDN-Warmup-Bot/1.0"

# ========== 函数定义 ==========

# 日志输出
log() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}"
}

# 从 sitemap.xml 提取 URL
extract_from_sitemap() {
  local sitemap_url="$1"
  local tmp_file="/tmp/sitemap_urls_$$.txt"

  log "从 sitemap 提取 URL: ${sitemap_url}"
  curl -s --max-time 30 "${sitemap_url}" \
    | grep -oP '<loc>\K[^<]+' \
    | sed "s|${CDN_DOMAIN}||" \
    > "${tmp_file}"

  local count
  count=$(wc -l < "${tmp_file}")
  log "提取到 ${count} 个 URL"
  echo "${tmp_file}"
}

# 预热单个 URL
warmup_single() {
  local path="$1"
  local url="${CDN_DOMAIN}${path}"
  local http_code
  local cache_status

  # 发送请求,提取状态码和缓存状态头
  local response
  response=$(curl -s -o /dev/null -w "%{http_code}" \
    --max-time "${TIMEOUT}" \
    -H "User-Agent: ${USER_AGENT}" \
    -D - "${url}" 2>/dev/null)

  http_code="${response}"
  echo "${http_code}${path}" >> "${LOG_FILE}"
}

# 并发预热
warmup_batch() {
  local url_file="$1"
  local total
  total=$(wc -l < "${url_file}")
  local completed=0

  log "开始预热,共 ${total} 个 URL,并发数 ${CONCURRENCY}"

  # 使用 xargs 控制并发
  cat "${url_file}" | xargs -P "${CONCURRENCY}" -I {} bash -c '
    url="'${CDN_DOMAIN}'{}"
    code=$(curl -s -o /dev/null -w "%{http_code}" --max-time "'${TIMEOUT}'" \
      -H "User-Agent: '${USER_AGENT}'" "${url}" 2>/dev/null)
    echo "${code} {}"
  ' | tee -a "${LOG_FILE}"

  log "预热完成"
}

# ========== 主流程 ==========

mkdir -p "$(dirname "${LOG_FILE}")"
log "===== CDN 缓存预热开始 ====="

# 确定 URL 来源
if [[ -n "${SITEMAP_URL}" ]]; then
  URL_FILE=$(extract_from_sitemap "${SITEMAP_URL}")
elif [[ -f "${URL_LIST}" ]]; then
  URL_FILE="${URL_LIST}"
else
  log "错误:未找到 URL 列表文件 ${URL_LIST},也未配置 SITEMAP_URL"
  exit 1
fi

warmup_batch "${URL_FILE}"

# 统计结果
log "===== 预热统计 ====="
log "200 响应: $(grep -c '^200' "${LOG_FILE}" || true) 个"
log "非 200 响应: $(grep -cv '^200\|^====\|^开始\|^预热\|^从\|^提取\|^\[' "${LOG_FILE}" || true) 个"
log "===== CDN 缓存预热结束 ====="

缓存命中率分析脚本

#!/bin/bash
# cdn_cache_stats.sh - CDN 缓存命中率分析
# 解析 Nginx 访问日志,统计 HIT/MISS/EXPIRED/BYPASS 比例
set -euo pipefail

# ========== 配置区 ==========
ACCESS_LOG="${1:-/var/log/nginx/access.log}"
REPORT_FILE="/var/log/cdn/cache_report_$(date +%Y%m%d).txt"

# ========== 参数校验 ==========
if [[ ! -f "${ACCESS_LOG}" ]]; then
  echo "错误:日志文件不存在 ${ACCESS_LOG}"
  exit 1
fi

mkdir -p "$(dirname "${REPORT_FILE}")"

# ========== 分析函数 ==========

analyze_cache() {
  local log_file="$1"

  echo "=========================================="
  echo " CDN 缓存命中率分析报告"
  echo " 日志文件: ${log_file}"
  echo " 分析时间: $(date '+%Y-%m-%d %H:%M:%S')"
  echo "=========================================="
  echo ""

  # 提取缓存状态字段(日志格式中 cache: 后面的值)
  local total hit miss expired bypass stale updating revalidated

  total=$(grep -c 'cache:' "${log_file}" || echo 0)
  hit=$(grep -c 'cache:HIT' "${log_file}" || echo 0)
  miss=$(grep -c 'cache:MISS' "${log_file}" || echo 0)
  expired=$(grep -c 'cache:EXPIRED' "${log_file}" || echo 0)
  bypass=$(grep -c 'cache:BYPASS' "${log_file}" || echo 0)
  stale=$(grep -c 'cache:STALE' "${log_file}" || echo 0)
  updating=$(grep -c 'cache:UPDATING' "${log_file}" || echo 0)
  revalidated=$(grep -c 'cache:REVALIDATED' "${log_file}" || echo 0)

  echo "【总体统计】"
  echo "  总请求数:    ${total}"
  echo "  HIT:         ${hit}"
  echo "  MISS:        ${miss}"
  echo "  EXPIRED:     ${expired}"
  echo "  BYPASS:      ${bypass}"
  echo "  STALE:       ${stale}"
  echo "  UPDATING:    ${updating}"
  echo "  REVALIDATED: ${revalidated}"
  echo ""

  # 计算命中率(HIT + STALE + UPDATING + REVALIDATED 都算有效命中)
  if [[ "${total}" -gt 0 ]]; then
    local effective_hit=$((hit + stale + updating + revalidated))
    local hit_rate
    hit_rate=$(awk "BEGIN {printf \"%.2f\", ${effective_hit}/${total}*100}")
    local miss_rate
    miss_rate=$(awk "BEGIN {printf \"%.2f\", ${miss}/${total}*100}")
    local bypass_rate
    bypass_rate=$(awk "BEGIN {printf \"%.2f\", ${bypass}/${total}*100}")

    echo "【命中率】"
    echo "  有效命中率:  ${hit_rate}%(含 HIT/STALE/UPDATING/REVALIDATED)"
    echo "  MISS 率:     ${miss_rate}%"
    echo "  BYPASS 率:   ${bypass_rate}%"
    echo ""

    # 命中率预警
    if (( $(echo "${hit_rate} < 80" | bc -l) )); then
      echo "⚠️  警告:缓存命中率低于 80%,建议检查:"
      echo "    - Cache-Control 头是否正确设置"
      echo "    - 缓存键是否包含不必要的变量(如 Cookie)"
      echo "    - 是否有大量带查询参数的请求未归一化"
      echo ""
    fi
  fi

  # MISS 最多的 URL(Top 20)
  echo "【MISS 最多的 URL(Top 20)】"
  grep 'cache:MISS' "${log_file}" \
    | awk '{print $4}' \
    | sed 's/"//g' \
    | sort | uniq -c | sort -rn \
    | head -20 \
    | awk '{printf "  %6d 次  %s\n", $1, $2}'
  echo ""

  # BYPASS 请求分析
  echo "【BYPASS 请求来源(Top 10)】"
  grep 'cache:BYPASS' "${log_file}" \
    | awk '{print $4}' \
    | sed 's/"//g' \
    | sort | uniq -c | sort -rn \
    | head -10 \
    | awk '{printf "  %6d 次  %s\n", $1, $2}'
}

# ========== 主流程 ==========
analyze_cache "${ACCESS_LOG}" | tee "${REPORT_FILE}"
echo ""
echo "报告已保存至: ${REPORT_FILE}"

最佳实践和注意事项

最佳实践

缓存策略设计

CDN缓存的核心是按资源类型分层设计,不同类型的资源对实时性和一致性的要求差异很大:

  • HTML 页面:短缓存(60s~300s),保证内容更新能快速生效。
  • CSS/JS 静态资源:长缓存(30d+),通过文件名哈希指纹实现版本控制。
  • 图片/视频/字体:长缓存(90d+),变更频率极低,命中率收益最大。
  • API 接口:原则上不缓存,或仅对幂等GET请求做极短缓存(5s~10s)。

Cache-Control 最佳实践矩阵:

资源类型 缓存策略 TTL 缓存键 说明
HTML no-cache 60s~300s URI 每次回源验证,保证内容新鲜
CSS/JS(带 hash) public, immutable 365d URI 文件名含 hash,内容变更即换 URL
CSS/JS(无 hash) public, must-revalidate 7d URI 依赖 ETag/Last-Modified 验证
图片/视频 public 90d URI 大文件长缓存,节省回源带宽
API(GET) private, no-store 不缓存 动态数据禁止 CDN 缓存
字体文件 public 180d URI 变更极少,注意 CORS 头

缓存一致性保障

缓存最棘手的问题不是“缓存了什么”,而是“什么时候该失效”。

主动清除:发布部署后通过API精确清除变更资源的缓存。

# Nginx 缓存清除(需要 ngx_cache_purge 模块)
curl -X PURGE https://cdn.example.com/css/main.abc123.css

版本化URL:前端构建工具自动在文件名中注入内容哈希,内容变更时URL自然变化,无需手动清缓存。

# 构建产物示例
main.3a7b2c1d.js   →  内容变更后  →  main.9e4f8a2b.js
style.f1c2d3e4.css →  内容变更后  →  style.b5a6c7d8.css

stale-while-revalidate策略:缓存过期后先返回旧内容,同时异步回源更新。

proxy_cache_use_stale updating error timeout http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
# 或通过 Cache-Control 头控制
add_header Cache-Control "public, max-age=600, stale-while-revalidate=30";

性能优化

Brotli压缩:相比Gzip,Brotli在同等压缩率下体积更小约15%~25%。生产环境建议静态资源预压缩用级别11,动态压缩用级别4~6。

brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
# 静态预压缩(优先使用 .br 文件)
brotli_static on;

连接预热:在HTML <head> 中提前建立与CDN域名的连接,减少首次请求延迟。

<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="//cdn.example.com">
<!-- 预连接(含 TLS 握手) -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin>

安全防护

防盗链配置

# 基于 Referer 的防盗链
valid_referers none blocked server_names *.example.com;
if ($invalid_referer) {
    return 403;
}

速率限制

# 定义限速区域
limit_req_zone $binary_remote_addr zone=cdn_limit:10m rate=100r/s;
limit_conn_zone $binary_remote_addr zone=cdn_conn:10m;

server {
    # 突发允许 200,超出部分排队
    limit_req zone=cdn_limit burst=200 nodelay;
    # 单 IP 最大并发连接数
    limit_conn cdn_conn 50;
}

源站IP隐藏:CDN架构下源站IP不应暴露在公网DNS中。源站防火墙仅放行CDN节点回源IP段。

注意事项

配置注意事项

警告:动态内容误缓存是CDN事故的头号元凶
用户登录态、购物车、个人中心等包含 Set-Cookie 的响应一旦被CDN缓存,会导致严重安全事故。必须确保:

  • 动态接口响应头包含 Cache-Control: private, no-store

  • 在Nginx配置中通过 proxy_no_cacheproxy_cache_bypass 排除带Cookie的请求。

  • 在VCL中对 req.http.Cookie 存在的请求执行 pass

    # 排除带 Cookie 的请求进入缓存
    set $no_cache 0;
    if ($http_cookie ~* "session_id|token|logged_in") {
    set $no_cache 1;
    }
    proxy_no_cache $no_cache;
    proxy_cache_bypass $no_cache;
  • 缓存空间不足proxy_cache_pathmax_size 设置过小,热点资源被反复淘汰又重新回源。

  • Vary头滥用:源站返回 Vary: * 或过多Vary字段会导致缓存碎片化严重。

  • HTTPS回源证书校验:CDN回源使用HTTPS时,源站证书必须合法有效。

常见错误

错误现象 原因分析 解决方案
缓存命中率长期低于 60% Cache Key 包含了查询参数、Cookie 等变量 精简 proxy_cache_key,忽略无关查询参数
接入 CDN 后源站压力未下降 缓存规则未生效或 TTL 过短 检查 X-Cache-Status,延长静态资源 TTL
用户反馈看到过期/错误内容 缓存未及时清除,或多层缓存中某层残留旧数据 全链路清除缓存,响应头禁止浏览器强缓存
回源频繁出现 5xx 错误 源站过载或回源超时设置过短 启用 proxy_cache_lock 合并回源,配置源站限流

兼容性问题

  • HTTP/2 与旧客户端:HTTP/2 要求 TLS 1.2+,老旧客户端不支持。需保留 HTTP/1.1 降级能力。
  • Brotli 浏览器支持:Brotli 仅在 HTTPS 下生效,且 IE 全系列不支持。需同时保留 Gzip。

故障排查和监控

故障排查

缓存状态诊断

排查CDN问题的第一步是查看响应头。三个关键头字段:

# 查看缓存相关响应头
curl -sI https://cdn.example.com/css/main.abc123.css | grep -iE "x-cache|age|via|cf-cache"

# 典型输出解读
# X-Cache-Status: HIT        → 缓存命中
# X-Cache-Status: MISS       → 缓存未命中
# X-Cache-Status: EXPIRED    → 缓存过期,重新回源
# Age: 3600                  → 缓存已存活3600秒

常见问题排查

问题一:缓存命中率低
排查方向:Cache Key设计不合理、Vary头过多、查询参数差异导致缓存碎片化。

# 忽略指定查询参数,规范化 Cache Key
set $clean_uri $uri;
if ($args ~* "^(.*)(?:&|^)(_t|timestamp|_=\d+)(.*)$") {
    set $clean_uri $uri?$1$3;
}
proxy_cache_key "$scheme$host$clean_uri";

问题二:缓存内容过期不更新
排查方向:PURGE请求是否到达所有节点、浏览器本地强缓存未清除、多层缓存未同步清除。

问题三:回源异常
排查方向:源站超时、DNS解析失败、回源SSL证书问题。

Varnish 调试

# 实时查看请求处理流程
varnishlog -g request -q "ReqURL ~ '/api/'"

# 查看缓存统计概览
varnishstat -1 | grep -E "cache_hit|cache_miss|backend_fail|n_object"

性能监控

关键指标

指标名称 正常范围 告警阈值 说明
缓存命中率 > 85% < 70% 核心指标,低于阈值需优化策略
回源带宽 < 总带宽 20% > 总带宽 40% 回源占比过高说明缓存未起效
响应时间 P95 < 100ms > 500ms 边缘节点响应应在毫秒级
5xx 错误率 < 0.1% > 1% 突增通常意味源站或节点故障

总结与进阶

技术要点回顾

  • 缓存分层是基础:按资源类型设计差异化策略,静态长缓存+内容哈希,动态内容严禁缓存。
  • 一致性保障是关键:版本化URL是最可靠方案,PURGE API作为补充,多层缓存需全链路同步清除。
  • 监控驱动运维:缓存命中率、回源带宽、延迟、错误率四个核心指标必须持续监控。
  • 安全不可忽视:防盗链、速率限制、源站IP隐藏构成基本防护面。
  • 应急预案要提前:缓存雪崩、节点宕机等场景处置流程需提前演练。

进阶学习方向

  1. 边缘计算:将计算逻辑下沉到CDN边缘节点,减少回源依赖,如A/B测试分流、请求改写。
  2. Serverless@Edge:在边缘运行JavaScript/Wasm,实现动态内容的边缘生成,适合个性化内容注入。
  3. QUIC/HTTP3 加速:基于UDP消除TCP队头阻塞,0-RTT连接建立显著降低延迟。
  4. P2P CDN:利用终端用户上行带宽进行内容分发,适合直播、大文件等高带宽场景。

结语

构建一个高效、可靠的CDN系统,远不止是配置一个反向代理那么简单。它要求开发者深入理解HTTP协议、网络架构,并具备精细化的运维能力。从精准的缓存策略设计,到严密的安全防护,再到全面的监控告警,每一个环节都至关重要。希望本文详尽的原理剖析、配置示例和最佳实践,能为你构建或优化自己的CDN架构提供切实的帮助。如果你在实践中遇到更复杂的问题,或者想与其他开发者交流 Nginx网络 优化的心得,欢迎来 云栈社区 一起探讨。


附录

命令速查表

# ========== curl 缓存调试 ==========
curl -sI https://cdn.example.com/path          # 查看响应头
curl -X PURGE https://cdn.example.com/path      # 清除指定 URL 缓存

# ========== Nginx 缓存管理 ==========
nginx -t                                        # 检查配置语法
nginx -s reload                                 # 平滑重载配置
grep "cache=" /var/log/nginx/access.log | awk '{print $6}' | sort | uniq -c  # 统计缓存命中分布

# ========== Varnish 管理命令 ==========
varnishadm ban req.url "~" "^/api/"            # 按正则批量清除缓存
varnishstat -1 | grep -E "hit|miss|backend"    # 快速查看命中率和后端状态

配置参数详解

Nginx proxy_cache 核心指令:

指令 默认值 说明
proxy_cache_path 定义缓存存储路径、分级目录、共享内存区、最大容量
proxy_cache_key $scheme$proxy_host$request_uri 缓存键,决定如何区分不同的缓存对象
proxy_cache_valid 按状态码设置缓存有效期
proxy_cache_lock off 开启后同一资源只允许一个请求回源,防止回源风暴
proxy_cache_use_stale off 源站异常时允许返回过期缓存的条件



上一篇:十年码龄感悟:与“管理岗”和解的坦诚思考
下一篇:瑞芯微MPP开源合规事件始末:代码违规整改与开源协议反思
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-1 22:32 , Processed in 0.387151 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表