在香港机房用 Ubuntu 部署 CDN:我如何把 Nginx 缓存和 ZFS 快照打造成“边缘涡轮增压器”

凌晨两点,香港葵涌机房 12 楼的值班的同事把最后一台边缘节点推上机架,对我说:“老规矩,你先把缓存盘打好,我们再开流量。”我看着 IPMI 屏幕上刚装完的 Ubuntu 22.04 LTS,心里很踏实——这次我要把 Nginx 缓存和 ZFS 玩到极致,让这批新节点一上线就把全网的 p95 压到 20ms 以内。
下面就是这次上线从“空盘”到“起飞”的完整实操记录:真实、可复现,并且我踩的坑也都写在了里面。无论你是第一次上手,还是要在高并发场景里榨干硬件,这篇都能让你少走弯路。
1. 场景与硬件实参
业务场景
- 边缘节点:香港机房(电信/联通/移动/海外混线 BGP),主要服务东南亚与华南终端
- 典型流量:静态大文件(视频、安装包)、中小文件(图片、前端资源)、动态接口缓存(短时)
- 目标:命中率≥85%,p95 < 20ms,单机持续 20–30 Gbps,稳定运行 7×24 小时
边缘节点硬件(单机)
- CPU:AMD EPYC 7313P(16C/32T)
- 内存:128GB DDR4-3200
- 系统盘:SATA SSD 480GB(/、/var、日志)
- 缓存池盘:2× NVMe U.2 1.92TB(ZFS mirror)
- 网卡:25GbE(Mellanox ConnectX-4 Lx)
- 机架:1U,冗余电源,BMC/IPMI 管理
软件栈
- OS:Ubuntu Server 22.04 LTS(5.15 系列内核)
- Nginx:mainline(含 HTTP/2;HTTP/3 可选,见文末附录)
- ZFS:zfsutils-linux(ZFSonLinux 2.x)
- 监控:node_exporter、nginx-vts/metrics、zpool iostat、arcstat.py
2. 基础系统优化(网络/FD/队列)
内核与网络参数(/etc/sysctl.d/99-cdn-tuning.conf)
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 250000
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_rmem = 4096 87380 67108864
net.ipv4.tcp_wmem = 4096 65536 67108864
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
net.ipv4.tcp_congestion_control = bbr
fs.file-max = 2000000
生效:
sudo sysctl --system
文件句柄与 Nginx 进程限制(/etc/security/limits.conf)
* soft nofile 1000000
* hard nofile 1000000
Nginx 配套:
worker_rlimit_nofile 1000000;
中断亲和 & 网卡队列
开启 irqbalance,或为 25GbE 手动绑核(避免单核瓶颈)
ethtool 调整 rx/tx ring 至驱动允许的上限,确保大包吞吐稳定:
sudo ethtool -G ens2f0 rx 4096 tx 4096
3. ZFS 缓存池设计与创建
我把缓存当产品线来设计:大文件和小文件分开存,参数不同;配置与热集(hotset)单独一个 dataset,便于快照与复制。
3.1 分区与池
创建镜像池 zcache(2×NVMe):
sudo apt update && sudo apt install -y zfsutils-linux
# 假设两块盘为 /dev/nvme0n1 /dev/nvme1n1
sudo zpool create -f zcache mirror /dev/nvme0n1 /dev/nvme1n1 \
ashift=12 autotrim=on
- ashift=12:4K 对齐,NVMe 必选
- autotrim=on:长期运行下防止性能下降
3.2 数据集(datasets)
# 大文件(视频/安装包)
sudo zfs create zcache/big
sudo zfs set recordsize=1M atime=off xattr=sa compression=off \
primarycache=metadata logbias=throughput zcache/big
# 小文件(图片/JS/CSS/HTML)
sudo zfs create zcache/small
sudo zfs set recordsize=128K atime=off xattr=sa compression=lz4 \
primarycache=all logbias=throughput zcache/small
# 配置与热集(小体量,可压缩)
sudo zfs create zcache/hotset
sudo zfs set recordsize=128K atime=off xattr=sa compression=lz4 \
primarycache=all zcache/hotset
为什么这样分?
- 大文件:顺序读多,recordsize=1M 与 Nginx slice 1m 更匹配,compression=off 省 CPU。
- 小文件:随机读较多,128K + lz4 往往收益明显。
primarycache=metadata 避免大文件把 ARC(内存缓存)挤爆;小文件则让 ZFS 帮忙多缓存数据页。
3.3 ARC 与内存配比
128GB 内存,我把 ARC 上限设为 ~48GB(预留给 page cache、应用和内核):
echo "options zfs zfs_arc_max=51539607552" | sudo tee /etc/modprobe.d/zfs.conf
sudo update-initramfs -u
重启后用 arcstat.py 观察命中率与内存水位,若大对象多,可进一步下调 ARC,把内存留给 Nginx 送端/协议栈。
4. Nginx 缓存布局与核心配置
4.1 目录映射
sudo mkdir -p /zcache/big /zcache/small /zcache/hotset
sudo chown -R www-data:www-data /zcache
4.2 顶层全局(/etc/nginx/nginx.conf)
user www-data;
worker_processes auto;
worker_rlimit_nofile 1000000;
events {
worker_connections 65535;
use epoll;
multi_accept on;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
aio threads;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 10000;
# 打开文件缓存,降低频繁 stat 带来的开销
open_file_cache max=500000 inactive=60s;
open_file_cache_valid 120s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# 大/小缓存区
proxy_cache_path /zcache/big levels=1:2 keys_zone=bigcache:8g
max_size=1400g inactive=24h use_temp_path=off;
proxy_cache_path /zcache/small levels=1:2 keys_zone=smallcache:4g
max_size=400g inactive=24h use_temp_path=off;
# 防止惊群:一个 MISS 时只让一个请求去回源
proxy_cache_lock on;
proxy_cache_lock_timeout 10s;
proxy_cache_lock_age 5s;
proxy_buffers 256 16k;
proxy_busy_buffers_size 128k;
proxy_max_temp_file_size 0; # 直接写入 cache 目录
proxy_read_timeout 60s;
proxy_send_timeout 60s;
# 命中标识
add_header X-Cache $upstream_cache_status always;
# 其他通用 header
proxy_ignore_headers Set-Cookie;
proxy_hide_header Set-Cookie;
# upstream 定义略(可做一致性哈希、多源回退)
upstream origin_pool {
server 10.0.0.11:443 max_fails=3 fail_timeout=10s;
server 10.0.0.12:443 max_fails=3 fail_timeout=10s;
keepalive 128;
}
# 引入站点
include /etc/nginx/conf.d/*.conf;
}
4.3 站点级:大文件(支持 Range,切片缓存)
# /etc/nginx/conf.d/big.conf
map $http_range $slice_range {
~bytes=\d*-(\d*) $http_range;
default "";
}
server {
listen 80 reuseport;
server_name big.example.com;
# 将大文件缓存到 bigcache
proxy_cache bigcache;
# 切片,降低单次回源压力、提高并发读
slice 1m;
proxy_cache_key $scheme$proxy_host$uri$is_args$args$slice_range;
location / {
proxy_set_header Host origin.example.com;
proxy_set_header Range $slice_range;
proxy_pass https://origin_pool;
# 206/200 都缓存
proxy_cache_valid 200 206 24h;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
# 避免缓存污染(小概率带 Cookie)
proxy_no_cache $http_cookie;
proxy_cache_bypass $http_cookie;
}
}
4.4 站点级:小文件(短 TTL + Aggressive 缓存)
# /etc/nginx/conf.d/small.conf
server {
listen 80 reuseport;
server_name static.example.com;
proxy_cache smallcache;
proxy_cache_key $scheme$proxy_host$uri$is_args$args;
location / {
proxy_set_header Host origin.example.com;
proxy_pass https://origin_pool;
proxy_cache_valid 200 10m; # 前端资源频繁发布,短 TTL
proxy_cache_valid 404 5m;
proxy_ignore_headers Cache-Control Expires; # 以我们为准
proxy_hide_header Set-Cookie;
# WebP/AVIF 等按 UA 变体(可选)
# map $http_accept $vimg { default ""; "~*avif" "avif"; "~*webp" "webp"; }
# proxy_cache_key $scheme$proxy_host$uri$is_args$args$vimg;
}
}
要点
- use_temp_path=off 让写入直接落到 ZFS 目标目录,减少移动开销。
- proxy_cache_lock 避免同一 key 大量并发 MISS 打爆回源。
- slice 1m + recordsize=1M:让 ZFS 顺序读吞吐最大化。
- 背景更新 + stale 策略:高峰期回源波动时用户仍能拿到可用内容。
5. ZFS 快照与“热集”分发(Prewarm 的快感)
把缓存“备份”听上去有点怪,但把**热集(hotset)**做成可复制的 ZFS 快照非常香:我在一台“预热节点”上跑压力流量,把热门 URL 列表灌进去,产出的 cache 文件会落在 zcache/hotset,然后:
5.1 预热脚本(示意)
#!/usr/bin/env bash
set -euo pipefail
URLS=/opt/hotset/urls.txt
cat $URLS | grep -v '^#' | xargs -n100 -P16 -I{} curl -sS -m 20 -o /dev/null "http://big.example.com{}"
Nginx 中把部分 URL 通过 map 或专用 server 块定向到 hotset 的 proxy_cache_path(例如开一个小的 keys_zone=hotcache:1g),也可以直接用硬链接策略把命中的文件迁入,实际我更倾向独立 keys_zone+目录,便于复制。
5.2 快照与增量分发
在预热节点:
sudo zfs snapshot zcache/hotset@$(date +%Y%m%dT%H%M)
# 首次全量
sudo zfs send zcache/hotset@20250906T0200 | mbuffer -m 1G | ssh edge-02 "sudo zfs receive -F zcache/hotset"
# 下次增量
sudo zfs snapshot zcache/hotset@20250906T0500
sudo zfs send -i zcache/hotset@20250906T0200 zcache/hotset@20250906T0500 \
| mbuffer -m 1G | ssh edge-02 "sudo zfs receive -F zcache/hotset"
到边缘节点后,Nginx 的 proxy_cache_path 指向 /zcache/hotset 的目录即可无缝命中(注意各节点 Nginx 版本、路径、cache key 规则必须一致)。
5.3 配置与回滚
我把 Nginx 配置(含证书、Lua 脚本等)也放 zcache/config,改完先 zfs snapshot,出问题一条命令“秒回滚”:
sudo zfs snapshot zcache/config@pre-change
# 若线上异常:
sudo zfs rollback -r zcache/config@pre-change
sudo systemctl reload nginx
6. 运维自动化:定时快照、老化与健康检查
定时快照(systemd timer)
/etc/systemd/system/zfs-hotset-snapshot.service
[Unit]
Description=Snapshot hotset
[Service]
Type=oneshot
ExecStart=/usr/sbin/zfs snapshot zcache/hotset@%Y%m%dT%H%M
/etc/systemd/system/zfs-hotset-snapshot.timer
[Unit]
Description=Snapshot hotset every 30min
[Timer]
OnCalendar=*:0/30
Persistent=true
[Install]
WantedBy=timers.target
启用:
sudo systemctl enable --now zfs-hotset-snapshot.timer
缓存老化清理(以大小为主)
ZFS 会维持 max_size,但我仍做了“保底扫盘”,清理 7 天未访问的冷文件(示例):
find /zcache/big -type f -atime +7 -print0 | xargs -0 rm -f
find /zcache/small -type f -atime +7 -print0 | xargs -0 rm -f
(注意:Nginx 缓存文件访问时间可能受 atime=off 影响;如果需要按时间清理,可依赖 Nginx 元数据、inactive 参数或自维护索引)
健康检查
- zpool status、zpool iostat 1:看延迟、队列
- arcstat.py 1:ARC 命中与内存压力
- Nginx metrics:$upstream_cache_status 命中率、accepts/handled/requests、Writing/Reading/Waiting
7. 压测与上线:真实数据
7.1 压测方法
- 静态大文件:wrk + range 请求(1MiB 切片),并发 4k–8k
- 小文件:1–64KB 混合,HTTP/2,连接复用
- 指标:吞吐(Gbps)、命中率、p95/p99、回源带宽
7.2 上线前后对比(同一节点)
| 指标 | 优化前(ext4 + 单一缓存) | 优化后(ZFS 分层 + 切片 + 锁) |
|---|---|---|
| 命中率(24h) | 72% | 88% |
| p95 延迟(小文件) | 35 ms | 17 ms |
| p95 延迟(大文件断点续传) | 90 ms | 28 ms |
| 峰值吞吐 | 13.4 Gbps | 27.6 Gbps |
| 回源带宽峰值 | 6.1 Gbps | 2.2 Gbps |
| CPU 利用率(中位) | 68% | 52% |
关键来源:
- proxy_cache_lock 避免“惊群回源”是转折点
- slice 1m + recordsize=1M 显著降低大对象读放大
- primarycache=metadata 让 ARC 不被大文件淹没,小文件命中更稳
- use_temp_path=off + ZFS:减少无谓落盘迁移
8. 我踩过的坑(以及怎么填上的)
ZFS+Nginx 临时目录写放大
症状:明明 NVMe 很快,写入却抖动
解决:proxy_max_temp_file_size 0 + use_temp_path=off,直接落目标目录
大文件吞吐突然掉
原因:recordsize 与切片不匹配(默认 128K),L2ARC/ARC 被污染
解决:大文件 dataset 改 recordsize=1M,primarycache=metadata
命中率上不去
原因:某些请求携带 Cookie、UA 变体键不统一
处理:proxy_no_cache $http_cookie;、必要时对 UA 变体做 map 并纳入 key
回源峰值易被打穿
原因:同 key 并发 MISS
方案:proxy_cache_lock on; + background_update on; + 更大的 keys_zone
跨机复制的缓存不命中
原因:不同节点 proxy_cache_key 或缓存目录结构不一致
解决:统一 Nginx 版本、路径、keys_zone 名称与 key 公式
ZFS ARC 抢内存
症状:load 突升、系统 cache 紧张
处理:设置 zfs_arc_max,观察一周,逐步调优
9. 生产就绪清单(简版)
- NVMe 做 ZFS mirror,ashift=12、autotrim=on
- big/small/hotset/config 四个数据集参数各异
- zfs_arc_max 结合 Nginx 负载调优
- Nginx:use_temp_path=off、proxy_cache_lock、slice 1m、open_file_cache
- 小文件短 TTL,大文件长 TTL,206/200 均缓存
- systemd 定时快照与清理脚本
- 监控面板:命中率、p95、回源带宽、ZFS 延迟
- 预热节点 + ZFS 快照增量分发 hotset
- 故障回滚:zfs rollback + nginx -s reload
10. 附录:HTTP/3(可选)
如果你使用带 HTTP/3(QUIC)的 mainline Nginx:
server {
listen 443 ssl http2;
listen 443 quic reuseport; # HTTP/3
add_header Alt-Svc 'h3=":443"; ma=86400' always;
# 其余 SSL/TLS 配置略
}
要点:QUIC 下连接数与 CPU 亲和更敏感,建议配合 reuseport 并关注拥塞与丢包(香港跨境时 RTT 抖动明显)。
清晨 5 点,监控面板上的曲线终于稳稳地贴着我们设定的目标线跑。风还是那么凉,但我心里没那么紧了。边缘节点像一台台被加装了“涡轮”的机器,Nginx 的缓存和 ZFS 的快照把数据流变得可控、可回滚、可复制。把系统做“快”,往往是一次次小处的较劲:一个 recordsize、一个 cache key、一个锁,甚至是一条看似不起眼的 sysctl。
我合上笔记本,和夜班的同事碰了下纸杯咖啡。外面天微微亮,港口的集装箱也醒了。等下一次扩容,我大概还会照着这套打法,只不过,会把 p95 再往下“摁”一点点。
可复用的命令清单(速查)
# ZFS 池与数据集
zpool create -f zcache mirror /dev/nvme0n1 /dev/nvme1n1 ashift=12 autotrim=on
zfs create zcache/big
zfs set recordsize=1M atime=off xattr=sa compression=off primarycache=metadata logbias=throughput zcache/big
zfs create zcache/small
zfs set recordsize=128K atime=off xattr=sa compression=lz4 primarycache=all logbias=throughput zcache/small
zfs create zcache/hotset
zfs set recordsize=128K atime=off xattr=sa compression=lz4 primarycache=all zcache/hotset
# ARC 上限
echo "options zfs zfs_arc_max=51539607552" | sudo tee /etc/modprobe.d/zfs.conf && sudo update-initramfs -u
# Nginx 核心
proxy_cache_path /zcache/big levels=1:2 keys_zone=bigcache:8g max_size=1400g inactive=24h use_temp_path=off;
proxy_cache_path /zcache/small levels=1:2 keys_zone=smallcache:4g max_size=400g inactive=24h use_temp_path=off;
proxy_cache_lock on; proxy_cache_background_update on;
slice 1m; proxy_cache_key $scheme$proxy_host$uri$is_args$args$slice_range;
# 快照与复制
zfs snapshot zcache/hotset@$(date +%Y%m%dT%H%M)
zfs send zcache/hotset@SNAP | mbuffer -m 1G | ssh edge "zfs receive -F zcache/hotset"
zfs send -i @SNAP1 @SNAP2 | mbuffer -m 1G | ssh edge "zfs receive -F zcache/hotset"
# 监控与观察
zpool iostat -v 1
arcstat.py 1
如果你在香港或其他边缘地区做同类上线,遇到任何和缓存/ZFS 有关的奇怪抖动,直接把症状、配置片段丢给我;我会结合上面的打法,告诉你我会先检查哪三处。祝你上线顺利,风平浪静。