一站多 CDN 怎么配?从 Nginx 到 Envoy 的香港服务器落地步骤与常见坑

香港机房的业务监控里北京、上海、广州三地的延迟曲线不断抖动,用户投诉“视频偶发卡顿”,运营同事的手机一直在闪。老板一句话:“能不能一站多 CDN?坏一家别全掉线。”
我当时的边界是 Nginx 做边缘终止、上游是应用集群;但想把“流量治理”这层进一步抽象和自动化,于是决定在香港把入口从 Nginx 迁到 Envoy,同时打通多家 CDN 的接入与自动故障转移。下面是我那一夜的完整过程。
我们要解决什么
- 一站多 CDN:同一主站域名接入 2~3 家 CDN(示例:CDN-A、CDN-B、CDN-C),做权重调度 + 健康切换。
- 边缘代理演进:从 Nginx 迁到 Envoy,引入更灵活的路由、观测、限流与熔断能力。
- 真实用户 IP 还原:多 CDN 头部不统一,源站要可靠恢复真实客户端 IP。
- 证书与 TLS:在 CDN 与源站混合场景下,证书自动化 + TLS/HTTP2/HTTP3 配置优化。
- 观测与回滚:全链路指标、可回滚发布、分钟级失效切换。
逻辑拓扑(ASCII)
[Client]
| (Anycast/Edge)
v
+------------------------------+
| CDN-A CDN-B CDN-C | <- 一站多CDN,DNS权重/地域/健康调度
+------------------------------+
| (X-Forwarded-For / CF-Connecting-IP / True-Client-IP)
v
[Hong Kong IDC]
|
+-----------+
| Envoy | <- L7入口: TLS终止/路由/限流/熔断/观测
+-----------+
|
+-----------+
| Nginx | <- 应用前置/静态/缓存/本地健康
+-----------+
|
[App Cluster]
实际环境与参数
硬件与网络(香港)
| 项 | 参数 |
|---|---|
| 机柜 | 1/4 Rack,220V,双路供电 |
| 服务器 | 2× 节点(可单节点起步):AMD EPYC 7302P 16C/32T,128GB RAM |
| 存储 | 2× NVMe 企业盘(Intel P5510 3.84TB,RAID1 via mdadm) |
| 网卡 | Intel X710 双口 10GbE(其中 1 口上联,1 口备用/内网) |
| 带宽 | 单机 1Gbps 保底,峰值 10Gbps(95 计费),多运营商混接 |
| 交换 | ToR 10G,支持 LACP(但入口只做单口以简化问题) |
注:配置并非“唯一正确”,重点是稳定+可扩展。NVMe 选企业盘是为保证随机读写与寿命;10G 是为后续横向扩容。
系统与版本
- OS:CentOS 7.9(内核 3.10.x,稳定为主)
- Nginx:1.24.x(自编译,启用 http_realip_module、http_v2_module、stream、brotli 可选)
- Envoy:1.29.x(Docker 运行,规避 CentOS 7 glibc 版本问题)
- 证书:Let’s Encrypt(DNS-01)或企业证书(ECDSA + RSA 双证)
- DNS:Route53(演示用,任意支持权重/健康检查的 DNS 均可)
- 监控:Prometheus + Grafana;日志:ELK/ClickHouse 二选一
步骤一:DNS 设计与一站多 CDN 调度
记录规划
| 记录 | 类型 | 说明 |
www.example.com |
CNAME | 指向 edge.example.com(可被权重/故障转移) |
edge.example.com |
CNAME | 指向 CDN 权重池(如 pool.example.com) |
pool.example.com |
多条 CNAME | 分别指向 www.example.com.cdn-a.net、www.example.com.cdn-b.net、www.example.com.cdn-c.net |
策略要点
- TTL:建议 30~60s,降低切换成本(太短会增加权威 DNS 压力)。
- 策略:起步用加权轮询;若 DNS 支持则加上故障剔除(基于健康检查 URL)。
- 健康检查 URL:https://www.example.com/cdn/healthz 返回 200,正文短小且不可缓存。
- 地域策略(可选):对中国大陆/东南亚/北美设置不同权重,基于 RTT 与错误率滚动调整。
小技巧:池子层面用“别名域”,能在大故障时一把切。
步骤二:证书与 TLS(CDN+源站混合场景)
为何 DNS-01
多数 CDN 在回源/回源验证时会挡住 HTTP-01 挑战,或者你不想暴露回源口。DNS-01 更稳。
使用 acme.sh(示例 Cloudflare DNS)
curl https://get.acme.sh | sh
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
export CF_Token="<token>"
export CF_Account_ID="<id>"
export CF_Zone_ID="<zone>"
~/.acme.sh/acme.sh --issue -d example.com -d *.example.com --dns dns_cf --keylength ec-256
~/.acme.sh/acme.sh --install-cert -d example.com \
--ecc --fullchain-file /etc/ssl/example.com/fullchain.pem \
--key-file /etc/ssl/example.com/privkey.pem --reloadcmd "docker exec envoy kill -HUP 1 || systemctl reload nginx"
TLS 最佳实践(边缘代理)
- TLS1.3 优先,保留 TLS1.2 兼容;
- 优先 ECDSA 证书,兼容 RSA;
- 开启 OCSP Stapling;
- ALPN:h2,http/1.1;
- CDN 与源站之间若需双向 TLS(mTLS),在 Envoy 配置中定义验证 CA。
步骤三:Nginx 基线(作为上游应用前置)
编译与安装(CentOS 7)
yum install -y gcc gcc-c++ make pcre-devel zlib-devel openssl-devel git
useradd --system --no-create-home --shell /sbin/nologin nginx
curl -O http://nginx.org/download/nginx-1.24.0.tar.gz && tar xf nginx-1.24.0.tar.gz
cd nginx-1.24.0
./configure \
--prefix=/etc/nginx \
--sbin-path=/usr/sbin/nginx \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_realip_module \
--with-stream \
--with-pcre \
--with-threads
make -j && make install
真实客户端 IP 还原(多 CDN 头部)
不同 CDN 头字段可能是:CF-Connecting-IP、True-Client-IP、X-Forwarded-For 等。原则:
- 只信任 CDN 回源 IP 段(防止 XFF 伪造);
- 统一回落到 X-Forwarded-For 最左侧。
自动维护 CDN IP 段(ipset)
# /usr/local/bin/update_cdn_ipset.sh
#!/usr/bin/env bash
set -euo pipefail
TMP=$(mktemp)
# 这里示例性抓取,你应替换为各CDN官方IP段获取地址或内部配置库
curl -fsSL https://example.com/cdn-a-ips.txt >> "$TMP"
curl -fsSL https://example.com/cdn-b-ips.txt >> "$TMP"
/sbin/ipset create cdn_ips hash:ip -exist
while read -r ip; do
[[ -z "$ip" ]] && continue
/sbin/ipset add cdn_ips "$ip" -exist || true
done < "$TMP"
rm -f "$TMP"
crontab:*/10 * * * * /usr/local/bin/update_cdn_ipset.sh >/dev/null 2>&1
Nginx 里只信任这些 IP
# /etc/nginx/conf.d/realip.conf
geo $trusted_proxy {
default 0;
# 通过 ipset 动态判断(需要 ngx_http_auth_request_module 或 lua;简化起见,这里直接列网段或用include文件)
}
# 简化:用 include 引用各 CDN 网段(手工或脚本更新)
# include /etc/nginx/trusted_cdn_a.conf;
# include /etc/nginx/trusted_cdn_b.conf;
real_ip_header X-Forwarded-For;
set_real_ip_from 10.0.0.0/8; # 内网/自有代理
set_real_ip_from 103.21.244.0/22; # 示例:某CDN网段(替换为真实)
# ... 其余网段
real_ip_recursive on;
多头字段回落
# 提前在server级用map统一抽取真实IP
map $http_cf_connecting_ip $client_ip_cand1 { default $http_cf_connecting_ip; }
map $http_true_client_ip $client_ip_cand2 { default $http_true_client_ip; }
map $http_x_forwarded_for $client_ip_cand3 { default $proxy_add_x_forwarded_for; }
map "$client_ip_cand1$client_ip_cand2$client_ip_cand3" $client_real_ip {
~([0-9a-f:\.]+) $1; # 简化匹配
}
log_format main '\n$remote_addr => real:$client_real_ip - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent "$http_referer" '
'"$http_user_agent" rt=$request_time uct=$upstream_connect_time uht=$upstream_header_time urt=$upstream_response_time';
access_log /var/log/nginx/access.log main;
经验:不要全信任 XFF。一定结合 IP 白名单或 ipset,否则有人可以伪造 XFF 打穿你所有限流与审计。
基线站点(含健康检查)
server {
listen 127.0.0.1:8080;
server_name _;
location = /cdn/healthz {
add_header Cache-Control "no-store";
return 200 'ok';
}
location /static/ {
root /data/www;
expires max;
add_header Cache-Control "public, max-age=31536000, immutable";
}
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $client_real_ip;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://app_cluster;
proxy_read_timeout 30s;
}
}
upstream app_cluster {
server 10.10.10.11:9000 max_fails=3 fail_timeout=10s;
server 10.10.10.12:9000 max_fails=3 fail_timeout=10s;
keepalive 256;
}
步骤四:引入 Envoy 作为统一入口
我把 Envoy 放在最前面,Nginx 退为上游前置与静态/缓存层。Envoy 负责更复杂的路由、观测与限流。
容器运行(CentOS 7 推荐)
yum install -y yum-utils device-mapper-persistent-data lvm2
yum install -y docker-ce
systemctl enable --now docker
mkdir -p /etc/envoy
cat >/etc/envoy/envoy.yaml <<'YAML'
static_resources:
listeners:
- name: listener_https
address: { socket_address: { address: 0.0.0.0, port_value: 443 } }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
use_remote_address: true
xff_num_trusted_hops: 1 # 如果 CDN 只在最前面一跳
normalize_path: true
request_timeout: 30s
common_http_protocol_options: { idle_timeout: 60s }
http2_protocol_options: {}
route_config:
name: local_route
virtual_hosts:
- name: app
domains: ["*"
]
routes:
- match: { prefix: "/cdn/healthz" }
direct_response: { status: 200, body: { inline_string: "ok" } }
- match: { prefix: "/" }
route:
cluster: nginx_upstream
timeout: 30s
retry_policy: { retry_on: 5xx, num_retries: 2 }
access_log:
- name: envoy.access_loggers.file
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
path: /var/log/envoy/access.log
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
common_tls_context:
alpn_protocols: ["h2", "http/1.1"]
tls_certificates:
- certificate_chain: { filename: "/etc/ssl/example.com/fullchain.pem" }
private_key: { filename: "/etc/ssl/example.com/privkey.pem" }
clusters:
- name: nginx_upstream
connect_timeout: 1s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: nginx_upstream
endpoints:
- lb_endpoints:
- endpoint: { address: { socket_address: { address: 127.0.0.1, port_value: 8080 } } }
circuit_breakers:
thresholds:
- max_connections: 2048
max_pending_requests: 2048
max_requests: 2048
health_checks:
- timeout: 1s
interval: 5s
unhealthy_threshold: 3
healthy_threshold: 2
http_health_check: { path: "/cdn/healthz" }
admin:
access_log_path: /var/log/envoy/admin.log
address: { socket_address: { address: 127.0.0.1, port_value: 9901 } }
YAML
docker run -d --name envoy --net host \
-v /etc/envoy:/etc/envoy \
-v /etc/ssl/example.com:/etc/ssl/example.com \
-v /var/log/envoy:/var/log/envoy \
envoyproxy/envoy:v1.29-latest \
-c /etc/envoy/envoy.yaml
本地限流示例(防抖突刺)
# 片段: 在 http_connection_manager 的 http_filters 中加入
- name: envoy.filters.http.local_ratelimit
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
stat_prefix: http_local_rate_limiter
token_bucket: { max_tokens: 2000, tokens_per_fill: 2000, fill_interval: 1s }
filter_enabled: { runtime_key: local_rate_limit_enabled, default_value: { numerator: 100, denominator: HUNDRED } }
filter_enforced: { runtime_key: local_rate_limit_enforced, default_value: { numerator: 100, denominator: HUNDRED } }
实战体会:把尖峰削平,能省下 CDN 与源站很多钱,还能减少“锯齿”报警。
步骤五:一站多 CDN 的健康切换落地
目标
- 当某 CDN 区域失败时,DNS 在 1~2 分钟内权重降为 0;
- 全局失败时,一键把 pool.example.com 指向“最后保险”的那家;
- 切换基于 外部探测(非机房内部),避免假健康。
实施
- 准备 2~3 个 探测点(香港/新加坡/美西的小云主机),cron 每分钟请求 https://www.example.com/cdn/healthz,记录 HTTP 状态与 RTT。
- 写个小服务(例如 Python FastAPI)汇总结果,调用 DNS API 调整权重:
- RTT > 500ms 或 5xx 错误率 > 5%:降低 10% 权重;
- 连续 3 次失败:权重置 0,触发告警;
- 恢复观测 5 分钟稳定才逐步加权回升。
- 变更记录写入 Git(IaC),确保审计与回滚。
“不要迷信健康检查 URL”,一线流量才是健康。建议叠加业务指标(如 2xx/非 2xx 比例)。
性能优化清单(源站)
内核与系统(CentOS 7)
cat >>/etc/sysctl.d/99-tuning.conf <<'SYS'
net.core.somaxconn = 1024
net.core.netdev_max_backlog = 250000
net.ipv4.tcp_max_syn_backlog = 4096
net.ipv4.ip_local_port_range = 1024 65000
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_nodelay = 1
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728
net.ipv4.tcp_mtu_probing = 1
fs.file-max = 1048576
SYS
sysctl --system
ulimit -n 1048576
Nginx
worker_processes auto;
worker_rlimit_nofile 1048576;
events { worker_connections 65535; use epoll; multi_accept on; }
http {
sendfile on; tcp_nopush on; tcp_nodelay on;
keepalive_timeout 65; keepalive_requests 10000;
gzip on; gzip_comp_level 5; gzip_types text/plain text/css application/json application/javascript; # CDN 场景谨慎双压缩
brotli on; brotli_comp_level 5; brotli_types text/plain text/css application/json application/javascript;
}
Envoy 关键参数
- common_http_protocol_options.idle_timeout = 60s(避免长时间占用)
- http2_protocol_options 开启(对 CDN/客户端更友好)
- circuit_breakers 合理设置连接与请求上限(见上文)
- preconnect(1.29+ 支持预连接)适度开启以降低首包延迟
观测与告警
指标(Prometheus)
- Envoy:listener.downstream_cx_active、cluster.upstream_rq_time、http.ingress.downstream_rq_xx
- Nginx:导出 stub_status 或使用 exporter
- 系统:带宽、丢包、SLA;磁盘延迟(NVMe)
日志
- 接入层日志需包含:真实客户端 IP、CDN 头部、上游时延、状态码;
- 示例字段:client_ip, cdn_vendor, xff, req_time, upstream_time, status, route, ua
常见坑与解决过程
| 坑 | 现象 | 定位与解决 |
| 真实 IP 错乱 | 统计里用户 IP 全是 CDN 回源 IP | 只信任 CDN 网段 + 统一 XFF 最左侧;对不同 CDN 的自定义头设置回落逻辑;开启 real_ip_recursive on |
| 证书续期失败 | acme http-01 一直失败 | 切 DNS-01;或给回源开临时直连域名仅内网使用 |
| 回源 499/504 | 峰值期大量 499/504 | Envoy 本地限流、上游 keepalive、内核队列增大;检查 CDN 到源站 RTT;提升 proxy_read_timeout |
| WebSocket 断流 | 页面可开但 WS 频繁断线 | 确保 CDN 支持 WS;Envoy upgrade_configs 打开;上游超时同步 |
| CORS 混乱 | 小范围前端报 CORS | 在边缘统一 Access-Control-Allow-*;避免 CDN/源站双重覆盖 |
| HTTP/2 帧错 | 少量浏览器报 PROTOCOL_ERROR |
关闭过老的 TLS1.0/1.1;检查 ALPN;升级有问题的中间盒 |
| 回源头过大 | 413/414 | 控制 Cookie/头大小;Nginx large_client_header_buffers;Envoy max_request_headers_kb |
| 直接打源 | 绕 CDN 直连源站 | 源站仅放行特定 CDN 网段;源域名随机化;在 Envoy 做 SNI/Host |
灰度与回滚:我当时的操作单
- 新增 pool.example.com,把 www.example.com CNAME 指向它,TTL 60s;
- 先只放 10% 流量到 CDN-B,观察 30 分钟:错误率、P95、带宽曲线;
- Envoy 加入限流,逐步把 xff_num_trusted_hops 从 0 -> 1;
- 夜间低峰切 Envoy 为默认入口,Nginx 退居上游;
- 故障演练:手动置 CDN-A 权重 0,确认切换时延 < 2 分钟;
- 留下“红钮”:pool.example.com 指向单一 CDN 的别名,必要时一键回滚。
成本与收益(粗略示例)
| 项目 | 之前(单 CDN + Nginx) | 之后(多 CDN + Envoy) |
| 峰值可用性 | 99.90% | 99.97% |
| P95 首字节(CN 南部) | 420ms | 300ms |
| P95 首字节(SEA) | 380ms | 280ms |
| CDN 成本 | 基线 1× | -8%(按区域分流与限流削峰) |
| 运维干预次数/月 | 8 次 | 2 次 |
注:示例数据,真实值以你业务曲线为准。核心是“多 CDN + 动态权重 + 边缘限流”带来的稳定性与成本平衡。
安全补强
- 源站仅放行 CDN 网段;
- Envoy 对 Host/SNI 白名单校验;
- 合规日志(含审计)外发到集中平台;
- WAF 优先放在 CDN;特殊路径(如管理接口)强制绕 CDN,只走专线 + mTLS。
切换完成那一刻,曲线像熨斗熨过一样平顺。报警频道静了,运营同事回了个 “👍”。我站在风机后面看着那台机子,风从 10G 口呼啸着进来,又从 1G 账单里悄悄地省了出去。第二天的站会我只说了一句:“今晚别打扰我,睡觉。”
附录:关键文件模板
1)Route53 权重记录(思路示例)
{
"Comment": "Weighted multi-CDN",
"Changes": [
{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "pool.example.com.",
"Type": "CNAME",
"SetIdentifier": "cdn-a",
"Weight": 60,
"TTL": 60,
"ResourceRecords": [{"Value": "www.example.com.cdn-a.net."}]
}
},
{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "pool.example.com.",
"Type": "CNAME",
"SetIdentifier": "cdn-b",
"Weight": 30,
"TTL": 60,
"ResourceRecords": [{"Value": "www.example.com.cdn-b.net."}]
}
},
{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "pool.example.com.",
"Type": "CNAME",
"SetIdentifier": "cdn-c",
"Weight": 10,
"TTL": 60,
"ResourceRecords": [{"Value": "www.example.com.cdn-c.net."}]
}
}
]
}
2)Envoy 追加:上游 mTLS(如有需要)
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
common_tls_context:
validation_context:
trusted_ca: { filename: "/etc/ssl/ca.crt" }
tls_certificates:
- certificate_chain: { filename: "/etc/ssl/client.crt" }
private_key: { filename: "/etc/ssl/client.key" }
3)Nginx 大头优化(防 413/414)
http {
client_max_body_size 100m;
large_client_header_buffers 8 32k;
}