如何在美国大带宽服务器的 CentOS 7.9 上,部署多域名 CDN 加速节点,把点播速度卷到飞起
技术教程 2025-09-18 14:59 189


我们部署在美国洛杉矶机房的Slack上客户在吐槽:“东海岸用户晚上看 4K VOD 还在转圈。”我盯着 Nginx 的 access.log,长长的回源链路像一根被拽紧的橡皮筋。那一刻我决定:不再用“临时 CDN 规则 + 回源限流”的补丁术——拉一台美国大带宽裸机,CentOS 7.9 起一个多域名 CDN 边缘节点,把多品牌视频业务(多域名)一锅端地加速起来。下面是我当晚到上线这 48 小时的完整实操手记,坑坑洼洼都在里面。

总体思路与架构

目标:在 1 台(可横向扩容到多台)美国大带宽服务器上,部署支持多域名(多品牌/多业务线)的边缘缓存节点,通过 OpenResty(Nginx + Lua)+ TLS 1.3 + HTTP/2 + 切片缓存(slice)+ 回源长连接 + BBR 等组合拳,把点播首帧、seek、拖动体验拉满;提供在线无损“版本位”失效能力,避免回源打爆;同时预留横向扩容到多 POP 的路径。

逻辑拓扑(文字版)

  • 用户(东/西海岸) → Anycast/DNS → CDN 边缘节点(本教程) → 回源(S3/对象存储/源站 Nginx)
  • 边缘节点:TLS 终止、缓存命中、Range 请求切片回源、跨域与缓存控制、慢源保护、并发锁、后台刷新

硬件与网络选型(我这台的“素体”)

组件 选择 说明
机房 洛杉矶 LAX(10G 接入) 对亚洲/美国双向时延平衡,夜间高峰更稳
CPU AMD EPYC 7302P(16C/32T)或 Xeon 5218 级别 单核性能 + 多并发足够顶住 TLS + 压缩/解压
内存 128GB ECC 大目录缓存 + 大量连接跟踪更稳
NVMe 2 × 3.84TB U.2 NVMe(RAID0) 纯缓存盘,追求吞吐与 IOPS;可按需改 RAID1
网卡 2 × 10GbE(Bonding/LACP) 上行冗余;多数业务跑不满 20G,但抖动小
带宽套餐 10Gbps 计流量/不限流量 我这台是 10G 计流量,月度包 100–200TB
操作系统 CentOS 7.9 最小化安装 稳定、老资格,但要手动升级内核/SSL

生产上我建议 NVMe 独立挂载为缓存盘,系统盘与缓存分离。后来上线一周的命中率稳定在 78–86%(不同域名略有差别),东西岸 4K 首帧 1.2–1.6s。

初始化与系统加固(CentOS 7.9 的“老骨头复健”)

1)基础系统与内核升级(为 BBR、TLS/HTTP2 打地基)

CentOS 7 自带 3.10 内核没有 BBR,OpenSSL 也旧。步骤:

# 1. 基础包
yum -y install epel-release yum-utils htop vim jq git curl wget net-tools

# 2. ELRepo 新内核(启用 BBR)
yum -y install https://www.elrepo.org/elrepo-release-7.0-6.el7.elrepo.noarch.rpm
yum --enablerepo=elrepo-kernel -y install kernel-ml
grub2-set-default 0   # 使用新内核为默认
reboot

重启后:

uname -r  # 建议看到 5.x+ 内核

启用 BBR:

cat >/etc/sysctl.d/99-bbr.conf <<'EOF'
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
EOF
sysctl --system
sysctl net.ipv4.tcp_congestion_control  # bbr

2)文件系统与挂载优化(缓存盘)

# 假设 NVMe 在 /dev/nvme0n1 /dev/nvme1n1,做 RAID0 仅用于缓存
yum -y install mdadm
mdadm --create /dev/md0 --level=0 --raid-devices=2 /dev/nvme0n1 /dev/nvme1n1
mkfs.ext4 -E lazy_journal_init=1 -m 0 /dev/md0

mkdir -p /data/nginx/cache
echo '/dev/md0 /data/nginx/cache ext4 noatime,nodiratime,discard 0 2' >> /etc/fstab
mount -a

3)内核网络调优(命令即文档)

/etc/sysctl.d/98-tuning.conf:

fs.file-max = 1000000
net.core.somaxconn = 8192
net.core.netdev_max_backlog = 262144
net.ipv4.ip_local_port_range = 1024 65000
net.ipv4.tcp_max_syn_backlog = 16384
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_mtu_probing = 1
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.ipv4.tcp_keepalive_time = 120
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_slow_start_after_idle = 0

应用:

sysctl --system

/etc/security/limits.d/99-nofile.conf:

* soft nofile 1048576
* hard nofile 1048576
* soft nproc  65535
* hard nproc  65535

4)防火墙与 SELinux

# firewalld 开 80/443
yum -y install firewalld
systemctl enable --now firewalld
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload

# SELinux 建议 permissive(或写策略)
setenforce 0
sed -ri 's/^(SELINUX=).*/\1permissive/' /etc/selinux/config

安装 OpenResty(Nginx + Lua),吃上 TLS 1.3/HTTP/2 的红利

CentOS 7 自带 OpenSSL 1.0.2,不支持 TLS 1.3。用 OpenResty 官方仓库,省去自己编译 OpenSSL 的麻烦:

yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
yum -y install openresty openresty-resty
systemctl enable --now openresty
openresty -V  # 看到 --with-http_slice_module、TLS1.3 即可

证书自动化(多域名 SNI)

我用 acme.sh 给每个业务域名申请证书(也可 SAN 合并;建议分开,方便独立轮换):

curl https://get.acme.sh | sh -s email=admin@yourcorp.com
mkdir -p /etc/openresty/ssl

# 例:为 cdn.brand-a.com 申请并自动安装
~/.acme.sh/acme.sh --issue -d cdn.brand-a.com --nginx
mkdir -p /etc/openresty/ssl/cdn.brand-a.com
~/.acme.sh/acme.sh --install-cert -d cdn.brand-a.com \
  --key-file /etc/openresty/ssl/cdn.brand-a.com/key.pem \
  --fullchain-file /etc/openresty/ssl/cdn.brand-a.com/fullchain.pem \
  --reloadcmd "systemctl reload openresty"

多域名就批量做几次,或者用脚本一把梭。

Nginx/OpenResty 核心配置(多域名、多源站、切片缓存、回源并发锁)

1)缓存与 Lua “版本位”失效(不删盘文件、在线瞬时生效)

/etc/openresty/nginx/nginx.conf(节选,按需合并到你的配置里):

worker_processes auto;
worker_rlimit_nofile 1048576;

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

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

    # 日志格式加入 Trace
    log_format  main  '$remote_addr - $host [$time_local] "$request" $status $body_bytes_sent '
                      '"$http_referer" "$http_user_agent" $request_time $upstream_response_time '
                      'hit=$upstream_cache_status trace=$request_id';

    access_log  /var/log/openresty/access.log main;
    error_log   /var/log/openresty/error.log warn;

    sendfile        on;
    tcp_nopush      on;
    tcp_nodelay     on;
    aio             threads;
    directio        512k;
    keepalive_timeout  65;
    keepalive_requests 10000;

    # Gzip 文本,视频不压
    gzip on; gzip_comp_level 5; gzip_min_length 1k;
    gzip_types text/plain text/css application/javascript application/json image/svg+xml;

    # DNS 解析器
    resolver 8.8.8.8 1.1.1.1 valid=300s ipv6=off;

    # 共享内存:缓存索引与“版本位”
    proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=cdn_cache:500m
                     max_size=2000g inactive=7d use_temp_path=off;

    lua_shared_dict cachever 10m;

    # 通过 Lua 注入 cache version,不删文件即可逻辑失效
    init_by_lua_block {
        cachever = ngx.shared.cachever
    }

    # 为每个域名配置上游(也可以来自 redis/lua 动态路由)
    map $host $origin_upstream {
        default          origin_pool_default;
        cdn.brand-a.com  origin_pool_a;
        cdn.brand-b.com  origin_pool_b;
    }

    # 每个域名一个 SNI 证书(示例两个,按需扩展)
    upstream origin_pool_a {
        keepalive 64;
        # 示例:对象存储或源站
        server s3.us-east-1.amazonaws.com:443;
    }
    upstream origin_pool_b { keepalive 64; server origin.brand-b.com:443; }
    upstream origin_pool_default { keepalive 64; server origin.default.com:443; }

    # 通用反代与缓存参数(切片 + 并发锁 + 后台刷新)
    map $request_uri $is_video {
        default 0;
        ~*\.mp4$ 1;
        ~*\.m4s$ 1;
        ~*\.ts$  1;
        ~*\.m3u8$ 1;
        ~*\.mpd$ 1;
    }

    # 版本位:全局/按 host/按路径都可以
    set_by_lua_block $cache_ver 'return (cachever:get("global") or 0)';

    proxy_cache_key "$scheme:$host:$request_uri:$is_video:$cache_ver:$slice_range";

    proxy_cache cdn_cache;
    proxy_cache_lock on;
    proxy_cache_lock_timeout 10s;
    proxy_cache_valid 200 206 301 302 30m;
    proxy_cache_valid any 5m;
    proxy_cache_min_uses 1;
    proxy_cache_background_update on;
    proxy_cache_revalidate on;
    proxy_ignore_headers Set-Cookie;
    proxy_hide_header Set-Cookie;

    proxy_connect_timeout 3s;
    proxy_read_timeout    60s;
    proxy_send_timeout    60s;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # 切片——对大文件/Range 请求效果显著
    slice 1m;
    proxy_set_header Range $slice_range;
    proxy_force_ranges on;

    # 缓存目录打开缓存
    open_file_cache max=100000 inactive=60s;
    open_file_cache_valid 120s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;

    # CORS(看业务需求)
    map $http_origin $cors {
        default "";
        ~^https?://(www\.)?(brand-a\.com|brand-b\.com)$ $http_origin;
    }

    # 统一的 Server 模板,按域名 include
    include /etc/openresty/nginx/conf.d/*.conf;

    # 健康页/监控
    server {
        listen 127.0.0.1:8080;
        location /nginx_status { stub_status; }
        # 版本位管理:只允许公司办公网 IP
        location /__admin/cache/bump {
            allow 203.0.113.0/24; deny all;
            content_by_lua_block {
                local key = ngx.var.arg_key or "global"
                local ver = (cachever:get(key) or 0) + 1
                cachever:set(key, ver)
                ngx.say("bumped ", key, " to ", ver)
            }
        }
    }
}

2)按域名拆分的 Server(多域名多证书)

/etc/openresty/nginx/conf.d/brand-a.conf:

server {
    listen 80;
    server_name cdn.brand-a.com;
    # HTTP->HTTPS
    location /.well-known/acme-challenge/ { root /usr/local/openresty/nginx/html; }
    location / { return 301 https://$host$request_uri; }
}

server {
    listen 443 ssl http2;
    server_name cdn.brand-a.com;

    ssl_certificate     /etc/openresty/ssl/cdn.brand-a.com/fullchain.pem;
    ssl_certificate_key /etc/openresty/ssl/cdn.brand-a.com/key.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache   shared:SSL:50m;
    ssl_session_timeout 1d;

    add_header Access-Control-Allow-Origin $cors always;
    add_header Access-Control-Allow-Credentials true always;
    add_header Timing-Allow-Origin * always;

    location / {
        # 视频可延后限速以保证首屏
        if ($is_video) {
            # 首 10MB 不限速,之后按需限速
            # sendfile_max_chunk 0;  # 保持默认
        }
        proxy_pass https://$origin_upstream;
    }
}

brand-b.conf 同理,只换证书与 server_name 即可。

上线前核对:nginx -t && systemctl reload openresty

DNS 与回源策略

  • DNS 建议把 cdn.brand-*.com 指向该节点公网 IP,TTL 设为 60–300 秒,后续多 POP 时配多 A 记录或用 GeoDNS。
  • 回源为 HTTPS,开启上游 keepalive,降低 TCP/SSL 握手成本。
  • 源站建议设置合理 Cache-Control,视频静态文件可 public, max-age=86400, immutable;HLS/DASH 清单较短的 max-age=5–30s。
  • 对大文件/范围请求,slice + proxy_force_ranges + 206 缓存 组合能显著减少源站压力。

运营期核心操作

1)无损刷新(不删缓存盘!)

当某路径视频替换、或紧急纠错,调用:

https://cdn.brand-a.com/__admin/cache/bump?key=global

我在 proxy_cache_key 里挂了 $cache_ver,这个“版本位”变了,新的 Key 立即生效,老文件自然过期,不需要清盘、不需要第三方 purge 模块。

如果要细粒度:把 key 设计成 host:/path/prefix,在 set_by_lua_block 里组合多个维度即可。

2)访问与命中监控

curl 127.0.0.1:8080/nginx_status

通过日志 hit=$upstream_cache_status(HIT/MISS/BYPASS/EXPIRED/STALE/REVALIDATED)拉到 Prometheus/Loki,做命中率、回源带宽、首帧耗时面板。

node_exporter + nginx_exporter 足够应对边缘节点可观测。

实测参数与对比(节选)

回源与命中表现(样本 24 小时)

指标 上线前(直连源站) 上线后(单 POP)
平均首帧(4K) 2.8 s 1.4 s
95 分位首帧 6.1 s 2.3 s
命中率(整体) 82%
源站出口峰值 3.2 Gbps 0.7 Gbps
边缘下行峰值 8.6 Gbps

核心 sysctl(回放)

作用
net.core.default_qdisc fq 配合 BBR 效果更佳
net.ipv4.tcp_congestion_control bbr 拥塞控制
ip_local_port_range 1024 65000 出口 NAT/反代端口池
tcp_rmem/wmem … 134217728 大带宽延迟积(BDP)场景更稳
tcp_mtu_probing 1 PMTU 黑洞场景自动探测

压测与验收(我怎么证明它真快)

链路带宽:iperf3 -c speedtest.server -R(从客户区域到 POP)

HTTP 并发:wrk -t8 -c2000 -d2m --latency https://cdn.brand-a.com/video-4k.mp4

命中回归:灰度阶段让部分用户命中该 POP(低 TTL + 单 IP),对比首帧/seek 指标;看 access.log 中 HIT/MISS 占比和 upstream_response_time。

一次线上回归我踩过坑:slice 开启后,如果源站不支持 Range,会导致 MISS 时整文件回源。临时策略:对不支持的 URI 前缀走“非切片” location,或在源站网关上补齐 Range 支持。

常见坑与我的现场解法

TLS 1.3 客户端握手失败

现象:少量老安卓/机顶盒报错。

处理:保留 TLSv1.2,ssl_ciphers 里不要过度精简;对 UA 黑名单降级到 HTTP/1.1。

回源 TIME_WAIT 堆积 / 源站 502

原因:瞬间 MISS 浪涌、回源连接复用不足。

方案:proxy_cache_lock on、keepalive 64+、proxy_http_version 1.1、Connection "";源站也开 keepalive_requests,并增加 worker_connections。

PMTU 黑洞导致少量地区卡顿

现象:特定运营商偶发“卡首帧”。

方案:tcp_mtu_probing=1;边缘与上游都禁 ICMP 过滤;必要时在边缘路由器上做 TCP MSS clamp。

SELinux 拦截本地缓存目录

现象:Permission denied 写不进 /data/nginx/cache。

方案:生产建议写策略,这次我直接 permissive,并给目录合适 chown -R nginx:nginx、chmod 755。

日志爆盘

现象:峰值期 access.log 飞涨。

方案:logrotate(小时切割)+ 压缩;或接 Loki/S3;关键字段保留,非关键 header 去掉。

扩展:多 POP 与调度

  • 水平扩容:把本机配置做成 Ansible 角色/脚本,复制到纽约/芝加哥/达拉斯几台。
  • 流量调度:短期用 DNS Geo/多 A,长期上 Anycast(需要运营商/托管商支持 BGP),或在云接入层做 全局健康探测 与 RTT 选路。
  • 一致性缓存:边缘节点之间不共享缓存,靠各自命中;核心是版本位统一由 CI/CD 或管理后台触发。

运维日常“口袋清单”(上线后一周我固定会做)

  1. nginx_status 连接数、等待队列、丢包报警
  2. 边缘出/入带宽、源站带宽、命中率、5xx 率
  3. 域名证书过期巡检(acme.sh 自动续期日志)
  4. 缓存盘剩余空间(80% 报警,自动调低 max_size 或 bump 版本位)
  5. 热点资源 TOP100(做预热或静态切片)

一键部署脚本(骨架示例,按需扩展)

提醒:生产环境请分步执行、加回滚保护。

#!/usr/bin/env bash
set -euo pipefail

DOMAINS=("cdn.brand-a.com" "cdn.brand-b.com")

# 基础
yum -y install epel-release yum-utils htop vim jq git curl wget net-tools firewalld
systemctl enable --now firewalld
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload

# 内核与 BBR
yum -y install https://www.elrepo.org/elrepo-release-7.0-6.el7.elrepo.noarch.rpm
yum --enablerepo=elrepo-kernel -y install kernel-ml
grub2-set-default 0
cat >/etc/sysctl.d/99-bbr.conf <<EOF
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
EOF

# OpenResty
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
yum -y install openresty openresty-resty
systemctl enable openresty

# 缓存目录
mkdir -p /data/nginx/cache
chown -R nginx:nginx /data/nginx/cache

# acme.sh
curl https://get.acme.sh | sh -s email=admin@yourcorp.com
mkdir -p /etc/openresty/ssl

for d in "${DOMAINS[@]}"; do
  mkdir -p /etc/openresty/ssl/$d
  ~/.acme.sh/acme.sh --issue -d "$d" --nginx || true
  ~/.acme.sh/acme.sh --install-cert -d "$d" \
    --key-file /etc/openresty/ssl/$d/key.pem \
    --fullchain-file /etc/openresty/ssl/$d/fullchain.pem \
    --reloadcmd "systemctl reload openresty"
done

# 配置(请把上文 nginx.conf 与 conf.d/*.conf 按需落盘)
nginx -t
echo "Reboot now to load new kernel. Then: systemctl start openresty"

系统上线的第三个夜里,我盯着 Grafana。那条 95 分位首帧曲线,像是终于学会了呼吸,从 6 秒一路把自己摁到了 2 秒出头;回源出口也从 3Gbps 掉到 700Mbps。客户没有再在 Slack 上说“还在转圈”,而是发了个“🍻”。
我知道这个节点还不完美——多 POP、Anycast、H3/QUIC 都在路上。但我确定一件事:能落地、能调参、能快速失效的边缘节点,才算真正在生产里活过一遭。

  1. CentOS 7.9 要上新内核 + BBR,OpenResty 吃下 TLS 1.3/HTTP2。
  2. slice + 206 缓存 + 并发锁 + 后台刷新,视频回源稳、命中高。
  3. 用 Lua 共享字典做“版本位”,一键无损刷新,不清盘。
  4. 上游 keepalive + proxy_http_version 1.1 + Connection "",回源连接可复用。
  5. 监控盯:命中率、首帧、5xx、缓存盘容量、证书续期。
  6. 坑:TLS 兼容性、PMTU 黑洞、TIME_WAIT、SELinux、日志爆盘——都能有解。

如果你也在美国机房折腾大带宽 VOD,这一套拿去就能跑。