
我们部署在美国洛杉矶机房的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 或管理后台触发。
运维日常“口袋清单”(上线后一周我固定会做)
- nginx_status 连接数、等待队列、丢包报警
- 边缘出/入带宽、源站带宽、命中率、5xx 率
- 域名证书过期巡检(acme.sh 自动续期日志)
- 缓存盘剩余空间(80% 报警,自动调低 max_size 或 bump 版本位)
- 热点资源 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 都在路上。但我确定一件事:能落地、能调参、能快速失效的边缘节点,才算真正在生产里活过一遭。
- CentOS 7.9 要上新内核 + BBR,OpenResty 吃下 TLS 1.3/HTTP2。
- slice + 206 缓存 + 并发锁 + 后台刷新,视频回源稳、命中高。
- 用 Lua 共享字典做“版本位”,一键无损刷新,不清盘。
- 上游 keepalive + proxy_http_version 1.1 + Connection "",回源连接可复用。
- 监控盯:命中率、首帧、5xx、缓存盘容量、证书续期。
- 坑:TLS 兼容性、PMTU 黑洞、TIME_WAIT、SELinux、日志爆盘——都能有解。
如果你也在美国机房折腾大带宽 VOD,这一套拿去就能跑。