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

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

发布人:Minchunlin 发布时间:2025-09-05 10:46 阅读量:652


那天凌晨 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 跑,但只操作缓存路径。

清单式复盘(可直接照抄)

  1. 准备目录:/cache/tier1_tmp/{upper,work}、/cache/tier2_nvme/{upper,work}、/cache/tier3_hdd/base、/cache/tier2、/var/cache/cdn
  2. 挂载基础卷:tmpfs、xfs(NVMe/HDD)
  3. 挂载 Tier-2 overlay 到 /cache/tier2
  4. 挂载 Tier-1 overlay 到 /var/cache/cdn
  5. Nginx proxy_cache_path 指向 /var/cache/cdn,启用 use_temp_path=off
  6. 部署 ovl_demote.sh + systemd timer
  7. 配置 sysctl、ulimit、监控项
  8. 压测 + 灰度 + 全量
  9. 写运行手册与应急回滚方法

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_tmpsize=48G,nr_inodes=10m
Tier-2 NVMe (XFS),/cache/tier2_nvmenoatime
Tier-3 HDD (XFS),/cache/tier3_hddnoatime
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 角色”,告诉我你的约束,我把这套脚本和单元文件打成一包给你。

目录结构
全文