上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-09-06 09:41 阅读量:745


凌晨两点,香港葵涌机房 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 有关的奇怪抖动,直接把症状、配置片段丢给我;我会结合上面的打法,告诉你我会先检查哪三处。祝你上线顺利,风平浪静。

目录结构
全文