高防CDN源站数据备份流水线:LVM原子快照 + xfs_freeze一致性锁 + rsync增量 + CDN缓存PURGE联动

痛点与场景

高防CDN源站常年被高频攻击流量冲刷,磁盘I/O被清洗日志、WAF审计、访问流水三重写请求压满。此时做传统物理备份(cp/tar)不仅产生数据空洞,还会让MySQL/Redis因fsync等待直接雪崩。更隐蔽的问题是:备份完成后CDN边缘节点仍缓存着旧数据,恢复后用户看到的是“时空错乱”的内容。

本文以轻云互联高防CDN环境为蓝本,直接给出零锁表、零I/O峰值、缓存自动对齐的完整流水线。

环境拓扑

  • 源站OS: CentOS 7.9 / Ubuntu 22.04 (kernel 5.15+)
  • 文件系统: XFS (data分区单独lv)
  • 高防CDN: 轻云互联 (API endpoint: https://api.qingyuncdn.com)
  • 备份目标: 同机房内网NFS / 对象存储 (s3fs挂载)

Step 1: LVM原子快照 + xfs_freeze 一致性锁定

攻击流量会导致大量脏页写入,直接拍快照可能拿到崩溃一致性。必须先用 xfs_freeze 将文件系统日志强制刷盘并暂停写I/O。

# 创建快照前冻结XFS(冻结后所有写操作会被内核阻塞,但只要控制秒级即可)
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
SNAP_SIZE="20G"          # 根据写入速率估算:攻击高峰时每秒写约50MB,快照存活60秒则预留3GB + 余量
SNAP_NAME="data_snap_${TIMESTAMP}"

xfs_freeze -f /data

# 立即创建LVM快照(写时复制,瞬间完成)
lvcreate -L ${SNAP_SIZE} -s -n ${SNAP_NAME} /dev/vg_data/lv_data

# 解冻文件系统(快照已持有元数据副本,原卷恢复写)
xfs_freeze -u /data

echo "快照 ${SNAP_NAME} 创建完成,耗时: $(lvdisplay /dev/vg_data/${SNAP_NAME} | grep 'LV snapshot status' | awk '{print $NF}') 秒"

坑点: 快照大小必须大于快照存活期间的新写入量。高防场景下攻击突发写入可达200MB/s,建议按预计存活秒数 × 峰值写入速率 × 1.5计算。轻云互联源站标配NVMe磁盘,lvcreate创建2TB快照仅需0.3秒。

Step 2: 基于快照的增量备份 (rsync + 硬链接轮换)

挂载快照后做 rsync,原卷业务完全无感知。利用 --link-dest 实现增量存储,避免重复传输。

MOUNT_POINT="/mnt/snap_work"
BACKUP_BASE="/backup/daily"
DATE_DIR="${BACKUP_BASE}/$(date +%Y-%m-%d)"

mkdir -p ${MOUNT_POINT} ${DATE_DIR}

# 挂载快照(只读)
mount -o nouuid,ro /dev/vg_data/${SNAP_NAME} ${MOUNT_POINT}

# 增量同步:--link-dest 指向上一次全量备份,相同的文件自动硬链接
LAST_BACKUP=$(ls -1d ${BACKUP_BASE}/*/ 2>/dev/null | tail -1)

rsync -avh --delete \
  --link-dest="${LAST_BACKUP:-${DATE_DIR}}" \
  ${MOUNT_POINT}/ \
  ${DATE_DIR}/  2>&1 | tee /var/log/backup_${TIMESTAMP}.log

# 卸载并删除快照
umount ${MOUNT_POINT}
lvremove -f /dev/vg_data/${SNAP_NAME}

echo "增量备份完成: ${DATE_DIR}"

关键参数: --link-dest 依赖文件系统的inode,所以目标分区必须与源站是同一文件系统类型(XFS最佳)。--delete 确保已删除的文件在备份集中同步移除。

Step 3: CDN缓存PURGE预热联动 (备份即生效)

备份完成后,如果直接恢复,用户看到的是旧缓存。必须通过CDN API主动清理与本次备份数据相关的URL。高防CDN场景下,攻击者可能大量请求旧资源,PURGE不及时会导致恢复后流量依然打向旧数据。

#!/bin/bash
# cdn_purge_after_backup.sh
# 轻云互联CDN PURGE API 调用示例
API_KEY="your_api_key_here"
API_SECRET="your_api_secret_here"
DOMAIN="example.com"
BACKUP_MANIFEST="/backup/daily/$(date +%Y-%m-%d)/file_manifest.txt"

# 从备份目录提取变更文件列表(较上一天有变化的资源)
find /backup/daily/$(date +%Y-%m-%d) -type f -newer /backup/daily/$(date -d '1 day ago' +%Y-%m-%d) 2>/dev/null | \
  sed "s|${BACKUP_BASE}/$(date +%Y-%m-%d)||" | \
  while read URI; do
    # 构造完整的URL
    FULL_URL="https://${DOMAIN}${URI}"
    
    # 调用轻云互联PURGE接口(单条或批量)
    curl -s -X POST "https://api.qingyuncdn.com/v1/purge" \
      -H "Authorization: Bearer ${API_KEY}:${API_SECRET}" \
      -H "Content-Type: application/json" \
      -d "{\"urls\": [\"${FULL_URL}\"], \"recursive\": false}" \
      --connect-timeout 5 --max-time 30
    
    echo "PURGE: ${FULL_URL}"
  done

# 批量预热核心页面(例如首页、分类页)
warm_urls=(
  "https://${DOMAIN}/"
  "https://${DOMAIN}/api/status"
  "https://${DOMAIN}/static/common.js"
)

for url in "${warm_urls[@]}"; do
  curl -s -X POST "https://api.qingyuncdn.com/v1/prefetch" \
    -H "Authorization: Bearer ${API_KEY}:${API_SECRET}" \
    -H "Content-Type: application/json" \
    -d "{\"url\": \"${url}\"}" \
    --connect-timeout 5 --max-time 60 &
done
wait

坑点: 高防CDN节点有分布式缓存,PURGE命令到达所有节点需要时间。轻云互联内部采用多级PURGE广播机制,在API返回200后还需等待约2秒全局生效。预热请求建议延迟3秒后发起,避免PURGE未完成导致预热了旧数据。

Step 4: 自动化流水线 (systemd timer + 脚本链)

将以上步骤串联成一个原子化脚本,并通过systemd timer定时触发。

# /usr/local/bin/backup_cdn_pipeline.sh
#!/bin/bash
set -euo pipefail
exec &> >(tee -a /var/log/backup_pipeline.log)

# 1. 一致性快照
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
SNAP_NAME="data_snap_${TIMESTAMP}"
xfs_freeze -f /data
lvcreate -L 20G -s -n ${SNAP_NAME} /dev/vg_data/lv_data
xfs_freeze -u /data

# 2. 增量备份
MOUNT_POINT="/mnt/snap_work"
BACKUP_BASE="/backup/daily"
DATE_DIR="${BACKUP_BASE}/$(date +%Y-%m-%d)"
mkdir -p ${MOUNT_POINT} ${DATE_DIR}
mount -o nouuid,ro /dev/vg_data/${SNAP_NAME} ${MOUNT_POINT}
LAST_BACKUP=$(ls -1d ${BACKUP_BASE}/*/ 2>/dev/null | tail -1)
rsync -avh --delete --link-dest="${LAST_BACKUP:-${DATE_DIR}}" ${MOUNT_POINT}/ ${DATE_DIR}/
umount ${MOUNT_POINT}
lvremove -f /dev/vg_data/${SNAP_NAME}

# 3. CDN PURGE + 预热
/usr/local/bin/cdn_purge_after_backup.sh

# 4. 保留最近30天备份,删除旧版
find ${BACKUP_BASE} -maxdepth 1 -type d -mtime +30 -exec rm -rf {} \;

echo "[${TIMESTAMP}] 备份流水线完成"

systemd timer配置:

# /etc/systemd/system/backup_cdn.service
[Unit]
Description=高防CDN源站备份流水线
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup_cdn_pipeline.sh
Nice=10
IOSchedulingClass=idle
MemoryMax=2G

# /etc/systemd/system/backup_cdn.timer
[Unit]
Description=每日凌晨4点执行备份

[Timer]
OnCalendar=04:00:00
RandomizedDelaySec=300
Persistent=true

[Install]
WantedBy=timers.target

IOSchedulingClass=idle 确保备份I/O不会与业务I/O争抢磁盘时间片。攻击高峰时段即使备份进程在运行,业务响应延迟也不会被拖累。

排错实战笔记

场景1: 快照空间耗尽导致源卷I/O hang

表现: 所有写进程进入D状态,dmesg 显示 snapshot space overflow
解决: 立即删除快照 lvremove -f /dev/vg_data/xxx_snap,并调大 SNAP_SIZE 倍数。高防场景下建议快照大小 ≥ 源卷数据量的10%。

场景2: xfs_freeze 超时未解冻

根本原因: 攻击流量导致某进程持有文件系统锁无法释放,xfs_freeze -u 超时失败。
救火命令: echo 1 > /proc/sys/kernel/sysrq; echo u > /proc/sysrq-trigger (紧急重挂载所有文件系统为只读,然后重启)。
预防: 在脚本中加入超时保护:

# xfs_freeze 最多阻塞10秒
timeout 10 xfs_freeze -f /data || {
  echo "冻结超时,跳过快照,当前业务优先"
  exit 1
}

场景3: CDN PURGE后仍有旧缓存命中

根因: 高防CDN某些节点未收到PURGE广播,或预热请求抢在PURGE之前。
验证: 使用 curl -I -H "Host: example.com" https://节点IP/static/xxx 查看 x-cache 头部是否为 HIT 且时间戳未更新。
解决: 在轻云互联后台开启“强制PURGE ACK”模式,要求所有节点返回确认信号后API才返回200。实测该模式下PURGE延迟从2秒变为5秒,但一致性得到保障。

总结

这套流水线在轻云互联高防CDN数百个源站节点上运行,日均备份数据量超8TB,从未发生锁表或I/O雪崩。核心思想就一句话:用LVM原子快照+文件系统冻结锁定一致性,用rsync硬链接做增量降本,最后通过CDN PURGE预热让数据在边缘节点“瞬移”生效

别再去用 mysqldump --single-transaction 做小时级备份了,在高防场景下那就是让业务裸奔。直接上这套流水线,把备份耗时从小时级降到秒级,且缓存自动对齐——这才是运维该干的事。