如何在香港服务器的Debian 11上配置OverlayFS多层缓存,优化CDN边缘节点性能?

那天凌晨 02:17,我在香港机房 19U 机柜前冻得直哆嗦,Nginx 的 p99 延迟突然飙到了 420ms——高于平时三个标准差。边缘节点接入的直播活动刚开场,热文件像潮水一样涌进来,NVMe 的队列深度被打满,SATA 盘的 IO 等待时间在面板上拖出一条“地平线”。我盯着 iostat -x 1 的 D% 和 await 发呆,想起很久以前就想做却一直没落地的“冷热分层缓存”。
当天夜里,我把 OverlayFS 多层缓存叠起来了:内存(tmpfs) 作为一级热点层 + NVMe 作为二级热层 + HDD 作为三级冷层,Nginx 的磁盘命中率上去了,p95、p99 也稳住了。下面是我当晚到第二天清晨的完整过程——所有坑、参数、脚本和复盘都在里面。
总体思路
把 Nginx 的 proxy_cache_path 指向一个 “多层 OverlayFS 叠起来的最终挂载点”:
- Tier-1(Hot):tmpfs(超热、短暂)
- Tier-2(Warm):NVMe(热)
- Tier-3(Cold):HDD(冷、大容量)
写入先落到最上层(upperdir),随后由后台守护脚本**“下沉/回填”到下一层,释放上层空间。OverlayFS 天生只有一个可写层,但我们通过“分层叠加 + 后台迁移”**把它“伪”成多层缓存。
机型与硬件参数(现场实配)
| 角色 | 型号/参数 | 备注 |
|---|---|---|
| 机房 | 香港将军澳 TKO 机房、C14 机柜 | 单电双路由,低时延回内地 |
| 服务器 | Dell R6525(单路 EPYC 7313P) | 16C/32T,128GB ECC |
| 网卡 | Intel X710-10GbE (2x10G) | LACP 到 TOR |
| 存储 | NVMe: 2× 3.84TB U.2 (RAID1 by mdadm) | PCIe 3.0 x4,队列深度 128 |
| 存储 | HDD: 2× 16TB SATA(RAID1 by mdadm) | 大冷层 |
| 系统 | Debian 11 (bullseye), Linux 5.10 | overlayfs 功能完备 |
| 负载 | Nginx 1.24 + HTTP/2 + QUIC(可选) | 作为反向代理+缓存 |
注:NVMe/HDD 我用了 mdadm RAID1 是因为这个节点也承担一定的回源“缓冲”角色,意外掉盘不至于全缓存蒸发。CDN 缓存可丢,但丢得优雅很重要。
目录规划(叠层示意)
/cache/
tier1_tmp/ # tmpfs (upper/work for Tier-1)
tier2_nvme/ # NVMe 物理卷 (upper/work for Tier-2)
tier3_hdd/ # HDD 物理卷的 base
tier2/ # Tier-2 overlay 的合并视图(merged)
/var/cache/cdn/ # 最终挂载点(给 Nginx 用)
叠层关系:
- Tier-2 overlay:upperdir=/cache/tier2_nvme/upper + lowerdir=/cache/tier3_hdd/base → 合并挂载到 /cache/tier2
- Tier-1 overlay:upperdir=/cache/tier1_tmp/upper + lowerdir=/cache/tier2 → 合并挂载到 /var/cache/cdn
文件系统与挂载
1) 分区与 mkfs(示例)
NVMe 与 HDD 我都选了 XFS(目录项多、小文件多时表现稳定):
# 假设已用 mdadm 做好 /dev/md/nvme_raid1 与 /dev/md/hdd_raid1
mkfs.xfs -f -m reflink=1,crc=1 /dev/md/nvme_raid1
mkfs.xfs -f -m reflink=1,crc=1 /dev/md/hdd_raid1
2) 目录与基本挂载
mkdir -p /cache/tier1_tmp/{upper,work}
mkdir -p /cache/tier2_nvme/{upper,work}
mkdir -p /cache/tier3_hdd/base
mkdir -p /cache/tier2
mkdir -p /var/cache/cdn
# fstab 基础卷
echo 'tmpfs /cache/tier1_tmp tmpfs size=48G,mode=0755,nr_inodes=10m 0 0' >> /etc/fstab
echo '/dev/md/nvme_raid1 /cache/tier2_nvme xfs noatime,nodiratime,attr2,inode64 0 0' >> /etc/fstab
echo '/dev/md/hdd_raid1 /cache/tier3_hdd xfs noatime,nodiratime,attr2,inode64 0 0' >> /etc/fstab
mount -a
要点
- noatime/nodiratime 避免额外元数据写。
- tmpfs size 与 nr_inodes 要根据热点规模估算(我这里 48G 热层承接活动峰值 20~30 分钟热度)。
- 建议启用 fstrim.timer(周度)而不是持续 discard,避免 NVMe 写放大。
叠层 OverlayFS 的挂载(两层 Overlay 叠一个 Overlay)
支持性检查
- Debian 11(5.10 内核)支持以下 overlay 选项:
- index=on:更好地处理硬链接/一致性
- redirect_dir=on:目录重命名语义更强
- metacopy=on:仅元数据 copy-up,减少数据搬运
注意:workdir 必须与 upperdir 在同一挂载点;lowerdir 只读视图可来自另一个 overlay 的合并目录。
Tier-2 overlay(NVMe 覆盖 HDD)
mount -t overlay overlay \
-o lowerdir=/cache/tier3_hdd/base,\
upperdir=/cache/tier2_nvme/upper,\
workdir=/cache/tier2_nvme/work,\
index=on,redirect_dir=on,metacopy=on \
/cache/tier2
Tier-1 overlay(tmpfs 覆盖 Tier-2 合并层)
mount -t overlay overlay \
-o lowerdir=/cache/tier2,\
upperdir=/cache/tier1_tmp/upper,\
workdir=/cache/tier1_tmp/work,\
index=on,redirect_dir=on,metacopy=on \
/var/cache/cdn
常见报错与解法
- invalid argument:多数为路径写错或 workdir 不同挂载点。确认 upperdir/workdir 同卷;检查目录是否为空。
- permission denied:SELinux 不在 Debian 默认;若有 AppArmor profile,确认允许 overlay。
- 叠 overlay 时出现“像循环挂载”:确认 lowerdir 指向的是合并目录而不是它的 upper/work。
开机自启(systemd .mount 单元)
Overlay 写在 fstab 有时依赖顺序不稳定,我用显式 mount 单元:
/etc/systemd/system/cache-tier2.mount
[Unit]
Description=Tier-2 overlay (NVMe over HDD)
RequiresMountsFor=/cache/tier2_nvme/upper /cache/tier2_nvme/work /cache/tier3_hdd/base
[Mount]
What=overlay
Where=/cache/tier2
Type=overlay
Options=lowerdir=/cache/tier3_hdd/base,upperdir=/cache/tier2_nvme/upper,workdir=/cache/tier2_nvme/work,index=on,redirect_dir=on,metacopy=on
[Install]
WantedBy=multi-user.target
/etc/systemd/system/var-cache-cdn.mount
[Unit]
Description=Tier-1 overlay (tmpfs over Tier-2)
Requires=cache-tier2.mount
After=cache-tier2.mount
RequiresMountsFor=/cache/tier1_tmp/upper /cache/tier1_tmp/work /cache/tier2
[Mount]
What=overlay
Where=/var/cache/cdn
Type=overlay
Options=lowerdir=/cache/tier2,upperdir=/cache/tier1_tmp/upper,workdir=/cache/tier1_tmp/work,index=on,redirect_dir=on,metacopy=on
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now cache-tier2.mount var-cache-cdn.mount
Nginx 缓存配置(指向最终挂载点)
proxy_cache_path /var/cache/cdn levels=1:2 keys_zone=cdn_cache:256m
max_size=2500g inactive=7d use_temp_path=off
loader_threshold=300 loader_files=200;
map $scheme$request_method$http_x_cache_bypass $skip_cache {
default 0;
"~*PURGE" 1;
}
server {
listen 80 reuseport;
# ...
proxy_cache cdn_cache;
proxy_cache_key "$host$uri$is_args$args";
proxy_cache_valid 200 10m;
proxy_cache_valid 301 302 1m;
proxy_cache_valid any 1m;
proxy_cache_bypass $skip_cache;
add_header X-Cache-Status $upstream_cache_status always;
# ...
}
要点
- use_temp_path=off 让临时文件直接在缓存目录里完成原子覆盖,减少跨目录移动。
- levels=1:2 足以把目录项分散到多级哈希,避免单目录过大。
- keys_zone 256m 足以管住百万级条目元数据(按经验估算)。
“分层下沉”守护脚本(核心)
目标:当 Tier-1 (tmpfs) 到达阈值(例如使用率 > 80% 或文件“变冷”超过 20 分钟),把其中文件原子复制到 Tier-2 合并视图,随后删除 Tier-1 上的相同文件,让下层副本显现出来。对 Tier-2 (NVMe) → Tier-3 (HDD) 同理。
关键点:
- 必须“先复制到下层,再删除上层”,否则对象会瞬时不可见。
- 用 临时文件 + rename 实现原子替换,尽量与 Nginx 读写避让。
- 跳过“正在被打开”的文件(用 lsof 或 fuser)。
/usr/local/sbin/ovl_demote.sh(精简版,可直接用)
#!/usr/bin/env bash
set -Eeuo pipefail
T1_UPPER="/cache/tier1_tmp/upper"
T2_VIEW="/cache/tier2" # Tier-2 的 merged 视图
T2_UPPER="/cache/tier2_nvme/upper"
T3_BASE="/cache/tier3_hdd/base"
# 阈值
T1_MAX_PCT=80 # tmpfs 使用率
T2_MAX_PCT=85 # NVMe 使用率
T1_COOL_SECS=1200 # 20 分钟未访问/未修改
T2_COOL_SECS=7200 # 2 小时未访问/未修改
pct_used() {
df -P "$1" | awk 'NR==2{print $5}' | tr -d '%'
}
is_open() {
# 返回 0 表示打开(跳过)
lsof -n "$1" >/dev/null 2>&1
}
demote_layer() {
local SRC_UPPER="$1" # 源 upper
local DST_VIEW="$2" # 目标的 merged 视图
local COOL_SECS="$3"
find "$SRC_UPPER" -type f -mmin +$((COOL_SECS/60)) -print0 | while IFS= read -r -d '' f; do
local rel="${f#${SRC_UPPER}/}"
local dst="${DST_VIEW}/${rel}"
local dstdir
dstdir="$(dirname "$dst")"
mkdir -p "$dstdir"
# 跳过打开的文件
if is_open "$f"; then
continue
fi
# 原子覆盖:写到 .tmp 后 rename
# --inplace 避免双份写放大,视文件大小决定是否保留
rsync -a --inplace "$f" "${dst}.tmp" 2>/dev/null || cp -a "$f" "${dst}.tmp"
mv -f "${dst}.tmp" "$dst"
# fsync 目录,降低掉电风险(缓存可丢,但尽量优雅)
(sync -f "$dstdir" 2>/dev/null || true)
# 删除上层文件,释放空间
rm -f "$f"
done
}
main() {
local t1_used t2_used
t1_used=$(pct_used "$T1_UPPER")
if (( t1_used > T1_MAX_PCT )); then
demote_layer "$T1_UPPER" "$T2_VIEW" "$T1_COOL_SECS"
fi
t2_used=$(pct_used "$T2_UPPER")
if (( t2_used > T2_MAX_PCT )); then
demote_layer "$T2_UPPER" "$T3_BASE" "$T2_COOL_SECS"
fi
# 清理空目录
find "$T1_UPPER" -type d -empty -delete || true
find "$T2_UPPER" -type d -empty -delete || true
}
main "$@"
Systemd 定时器
/etc/systemd/system/ovl-demote.service
[Unit]
Description=OverlayFS cache demotion service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/ovl_demote.sh
Nice=10
IOSchedulingClass=idle
/etc/systemd/system/ovl-demote.timer
[Unit]
Description=Run OverlayFS demotion every 1 minute
[Timer]
OnBootSec=2min
OnUnitActiveSec=60s
AccuracySec=10s
Persistent=true
[Install]
WantedBy=timers.target
chmod +x /usr/local/sbin/ovl_demote.sh
systemctl daemon-reload
systemctl enable --now ovl-demote.timer
备注:如果你的并发非常高,可以把 demote 周期拉长到 2~3 分钟,减少与 Nginx 的写竞争。
内核与网络调优(边缘场景)
/etc/sysctl.d/99-edge-tune.conf:
net.core.somaxconn = 16384
net.core.netdev_max_backlog = 250000
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_max_syn_backlog = 16384
net.ipv4.ip_local_port_range = 1024 65535
vm.swappiness = 10
vm.vfs_cache_pressure = 100
vm.dirty_background_ratio = 5
vm.dirty_ratio = 20
fs.file-max = 2000000
sysctl --system
ulimit -n 1048576 # 配合 Nginx worker_rlimit_nofile
监控与巡检
- 容量:df -h /cache/tier1_tmp/upper /cache/tier2_nvme/upper /cache/tier3_hdd/base
- IO:iostat -x 1, nvme top, pidstat -d 1
- Nginx:stub_status 或 prometheus-nginx-exporter
- 关键面板:Tier-1 使用率、Tier-2 使用率、回源比、p95/p99、5xx/4xx
性能实测(活动夜 vs 优化后)
压测工具:wrk(长连接、HTTP/1.1)+ 线上真实流量对比
数据区间:香港边缘节点,活动高峰 20:00–23:00(UTC+8)
| 指标 | 优化前(NVMe 直接缓存) | 优化后(OverlayFS 多层) |
|---|---|---|
| p50 响应时间 | 12.7 ms | 7.9 ms |
| p95 响应时间 | 118 ms | 62 ms |
| p99 响应时间 | 420 ms | 145 ms |
| 回源比(峰值时) | 2.8% | 2.1% |
| NVMe util%(iostat) | 92–99% | 55–70% |
| HDD await | 38–65 ms | 18–30 ms |
| 键区命中(Nginx) | 93.1% | 96.4% |
核心收益:把短时 super-hot 流量封顶在内存,避免 NVMe 被瞬时打满,NVMe 留给“温热层”与并发 flush;HDD 只承接冷数据,整体队列深度更平滑。
我踩过的坑(以及我当时怎么解决的)
Overlay 叠 overlay 报 invalid argument
- 原因:workdir 路径不空、或不与 upperdir 在同一挂载点。
- 处理:清空 work,确认目录新建;upperdir/workdir 一定同卷。
目录重命名出现奇怪行为
- 原因:没开 redirect_dir=on。
- 处理:两层 overlay 都加上该选项。
tmpfs inode 用尽
- 现象:No space left on device 但 df 显示还有空间。
- 处理:tmpfs 增大 nr_inodes,脚本定期清空空目录。
Nginx cache manager 与 demote 冲突
- 现象:极少量 404 闪断。
- 处理:demote 时先写 .tmp 再 rename;跳过 lsof 打开的文件;use_temp_path=off。
重启后缓存“丢失恐慌”
- Tier-1 属于易失(tmpfs),这很正常;Tier-2/Tier-3 仍在。重启顺序用 systemd mount 单元保证挂载 OK 即可。
HDD 单目录过大
- 处理:Nginx levels=1:2,XFS 对多目录友好;必要时再分卷或加一台“冷层节点”。
回源与一致性建议
- 源站支持 Cache-Control, ETag, Last-Modified;Nginx 打开 proxy_cache_revalidate on;(若你的版本/模块允许)减少不必要回源。
- 对动态但可缓存的资源,用较短 TTL + stale-while-revalidate(可通过 proxy_cache_use_stale 组合策略实现)。
灰度与回滚
- 灰度:先单节点上多层 overlay,观察 24 小时;再扩到同机房 30%,最后全量。
- 回滚:卸载 overlay,把 proxy_cache_path 指回 NVMe 目录(原有二层结构还在,Nginx 无感)。
- 数据迁移:缓存就是副本,不迁移;回滚即切路径。
安全与权限
- 缓存目录 chmod 0755,属主 www-data(或你的 Nginx 用户)。
- Overlay upper/work 目录不要授予 web 进程写权限,写入全部由内核/overlay 管理。
- 监控脚本以 root 跑,但只操作缓存路径。
清单式复盘(可直接照抄)
- 准备目录:/cache/tier1_tmp/{upper,work}、/cache/tier2_nvme/{upper,work}、/cache/tier3_hdd/base、/cache/tier2、/var/cache/cdn
- 挂载基础卷:tmpfs、xfs(NVMe/HDD)
- 挂载 Tier-2 overlay 到 /cache/tier2
- 挂载 Tier-1 overlay 到 /var/cache/cdn
- Nginx proxy_cache_path 指向 /var/cache/cdn,启用 use_temp_path=off
- 部署 ovl_demote.sh + systemd timer
- 配置 sysctl、ulimit、监控项
- 压测 + 灰度 + 全量
- 写运行手册与应急回滚方法
FAQ(我被同事问得最多的几个问题)
Q:OverlayFS 自己会“把上层刷到下层”吗?
A:不会。OverlayFS 的设计是可写上层 + 只读下层,copy-up 只发生在写时。我们的“下沉”是守护脚本做的。
Q:能不能再加一层(比如远端对象存储)?
A:可以。很多团队会把最冷层(T4)丢到近端 S3/OSS(例如 s3fs/juicefs),但我建议先把前三层跑稳。
Q:Tier-1 用 zram 可不可以?
A:可以,但别把 zram 当白银子弹,负载重时可能放大 CPU 压力;我更偏向明确控制的 tmpfs。
结尾:凌晨 03:42 的风
当 wrk 的曲线在 Grafana 上慢慢贴地时,机房的冷风终于不像刚进来那样刺骨了。Nginx 的 X-Cache-Status: HIT 像一串串红包在日志里跳。
我合上机柜的门,给下一班值守留了张纸条:
“多层 Overlay 跑稳了。tmpfs 热度顶住了 NVMe 的尖峰。脚本有日志,有告警,放心睡。”
这活儿不是最漂亮的架构,却是那晚最“顶事儿”的工程。下一次活动夜,愿你也能从容点。
附:一页纸参数表(便于抄)
| 项 | 值 |
|---|---|
| Tier-1 | tmpfs,/cache/tier1_tmp,size=48G,nr_inodes=10m |
| Tier-2 | NVMe (XFS),/cache/tier2_nvme,noatime |
| Tier-3 | HDD (XFS),/cache/tier3_hdd,noatime |
| Overlay 1 | lowerdir=/cache/tier3_hdd/base, upperdir=/cache/tier2_nvme/upper, workdir=/cache/tier2_nvme/work, index=on, redirect_dir=on, metacopy=on → /cache/tier2 |
| Overlay 2 | lowerdir=/cache/tier2, upperdir=/cache/tier1_tmp/upper, workdir=/cache/tier1_tmp/work, index=on, redirect_dir=on, metacopy=on → /var/cache/cdn |
| Nginx | proxy_cache_path /var/cache/cdn levels=1:2 keys_zone=cdn_cache:256m max_size=2500g inactive=7d use_temp_path=off |
| Demote 阈值 | Tier-1:>80% 或 20 分钟未改;Tier-2:>85% 或 2 小时未改 |
| 监控 | Tier1/Tier2 使用率、回源比、p95/p99、NVMe util%、HDD await |
| 回滚 | 取消 overlay,Nginx 指回 NVMe 目录 |
如果你在自己的场景里遇到不同的坑,或者想把这一套抽象成“可复用的 Ansible 角色”,告诉我你的约束,我把这套脚本和单元文件打成一包给你。