博客CDN缓存自动化:从发现到修复

2026年06月25日7 次阅读0 人喜欢
Next.jsCDN腾讯云CI/CD缓存
所属合集

博客 CDN 缓存自动化:从发现到修复

起因:Chat 页面缓存问题

事情是这样的,我给博客加了个 AI 聊天功能,页面放在 /chat 路径下。按理说这是个 "use client" 的客户端渲染页面,CDN 不应该缓存才对。

结果发现——CDN 不仅缓存了,还缓存了一年。

排查过程

先看响应头:

复制代码
x-nextjs-cache: HIT
x-nextjs-prerender: 1
Cache-Control: s-maxage=31536000

31536000 秒,差不多就是一年。关键是这个 header 是 Next.js 自己返回的,不是 CDN 加的。

原因很简单:虽然 /chat"use client" 组件,但 Next.js 仍然对它做了预渲染(生成初始 HTML shell)。页面没有声明 export const dynamic = 'force-dynamic',所以被当成静态页处理了。prerender-manifest.json 里白纸黑字写着:

json 复制代码
"/chat": {
    "initialRevalidateSeconds": false,
    "srcRoute": "/chat"
}

false 意味着永不 revalidate,Next.js 就给了个一年的 s-maxage

修复:revalidate + CDN 自动刷新

最简单的修复是加一行:

ts 复制代码
export const revalidate = 3600; // 1小时

这样 Next.js 还是预渲染(保留 SEO 优势),但 s-maxage 从一年变成了 3600 秒。

但这只是治标。部署新版本后,CDN 还是会继续缓存旧内容直到 TTL 过期。我需要的是:部署完成后自动刷新 CDN 缓存

搭建 CDN 自动刷新

方案选型

一开始想在 GitHub Actions 里做,但发现服务器上的腾讯云 SecretId/SecretKey 只在 Docker 容器的环境变量里,GitHub Actions 拿不到。也不想为了这事单独在 GitHub Secrets 里再加一组凭据。

最终方案:服务器上执行。部署脚本里直接调腾讯云 CDN Purge API,凭据从 .env 文件读取。

一开始用 Python 手写了 TC3-HMAC-SHA256 签名,结果踩了一堆坑——服务器 Python 3.6.8 不支持新语法、shell 转义把签名搞坏、PurgeUrlsCacheFlushType 参数名和文档不一致。后来换成腾讯云官方 Node.js SDK(tencentcloud-sdk-nodejs),几行代码搞定,本地测试一次通过。

变更感知:不是一把梭

最粗暴的做法是每次部署都全站刷新 CDN。但这样太浪费了——改了个页面组件就得刷全站,没必要。

所以我做了一个变更范围检测:

变更路径 CDN 刷新策略
src/components/** 全站 /
src/app/layout.tsx / globals.css 全站 /
src/app/chat/page.tsx 只刷 /chat/
public/** 精确 URL
docs/** / *.md 跳过

GitHub Actions 在 skip-check 阶段已经拿到了 CHANGED_FILESgit diff --name-only HEAD~1 HEAD),把这个列表通过 SSH 环境变量传到服务器,写入临时文件,再交给 purge-cdn.mjs 分析。

实现细节

整个流程是:

复制代码
push → skip-check (获取 CHANGED_FILES)
  → build-and-deploy
    → SSH 部署 (git pull → 写入变更文件 → deploy.sh)
      → purge-cdn.mjs --changed-file /tmp/.deploy_changed_files
        → analyzeChanges() 按规则匹配
          → 精准刷新对应路由
      → rm /tmp/.deploy_changed_files

purge-cdn.mjs 用腾讯云官方 Node.js SDK 调 CDN Purge API,不用手写签名,代码干净可靠。项目本身是 Next.js,服务器有 Node.js,不需要额外装运行时。

一个意外发现

测试的时候发现服务器上居然没有 .env 文件?凭据全在 Docker 容器的环境变量里。最后发现 .env/www/wwwroot/react.nnnnzs.cn/.env,是 docker-compose.prod.ymlenv_file 引用的那个路径。之前 find 命令没找到是因为搜索深度不够。

踩坑记录:CDN 刷新脚本在容器里跑

第一版 purge-cdn.mjs 是在服务器宿主机上直接 node 执行的。结果部署时报错:

复制代码
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'tencentcloud-sdk-nodejs'

服务器上没装 npm 依赖,也不想装——它是纯 Docker 部署,宿主机保持干净。所以必须把 CDN 刷新脚本挪到容器里执行。

第一次尝试:挂载 scripts 目录

最直觉的做法:在 docker-compose.prod.yml 里挂载 scripts 目录到容器。

yaml 复制代码
volumes:
  - ./scripts:/app/scripts:ro

然后 docker exec 在容器内执行。结果还是找不到包——purge-cdn.mjs 用的是 ES modules(import),NODE_PATH 环境变量对 ES modules 不生效。它只认脚本所在目录的 node_modules,而挂载进来的 scripts 目录下没有。

第二次尝试:复制脚本到 /app

不挂载了,直接 docker cppurge-cdn.mjs 复制到容器的 /app 目录:

bash 复制代码
docker cp scripts/purge-cdn.mjs $CONTAINER_NAME:/app/purge-cdn.mjs
docker exec -w /app $CONTAINER_NAME node purge-cdn.mjs --changed-file /tmp/.deploy_changed_files

这次脚本确实在 /app 下了,但还是报错——Next.js standalone 模式打包的容器里,node_modules精简过的,只有运行 Next.js 所需的最小依赖,tencentcloud-sdk-nodejs 根本不在里面。

第三次尝试:运行时 npm install

在容器里临时装:

bash 复制代码
docker exec -u root -w /app $CONTAINER_NAME npm install tencentcloud-sdk-nodejs --no-save

结果超时了。容器里 npm install 要解析整个依赖树,standalone 模式的 node_modules 不完整,解析直接卡死。10 分钟都没装完,SSH 命令超时。

最终方案:构建时预装

不在运行时装了,直接在 Dockerfile.prod 的 runner 阶段预装:

dockerfile 复制代码
# 在临时目录安装,避免污染 standalone 的 node_modules 解析
RUN cd /tmp && npm init -y > /dev/null 2>&1 && \
    npm install tencentcloud-sdk-nodejs && \
    cp -rn node_modules/* /app/node_modules/ && \
    cd /app && rm -rf /tmp/node_modules /tmp/package.json /tmp/package-lock.json && \
    chown -R nextjs:nodejs node_modules

关键点:/tmp 独立目录安装,装完再 cp -rn 合并到 /app/node_modules。如果直接在 /appnpm install,会因为 standalone 的依赖树不完整而报 ETARGET 错误(找不到 @next/font@16.0.10)。

Docker 拉取重试

在排查 CDN 刷新的过程中,还发现一个问题:从 DockerHub 拉镜像偶尔会超时。GitHub Actions 构建时拉基础镜像超时,服务器部署时拉最新镜像也超时。

给两边都加了重试机制:

GitHub Actionsdocker-release.yml):

yaml 复制代码
- name: 拉取基础镜像(带重试)
  run: |
    MAX_RETRIES=3
    RETRY_DELAY=15
    ATTEMPT=1
    while [ $ATTEMPT -le $MAX_RETRIES ]; do
      if docker pull node:22-alpine; then exit 0; fi
      sleep $RETRY_DELAY
      RETRY_DELAY=$((RETRY_DELAY * 2))
      ATTEMPT=$((ATTEMPT + 1))
    done
    exit 1

服务器部署deploy.sh):

bash 复制代码
docker_pull_with_retry() {
    local image="$1" max_retries="${2:-3}" retry_delay="${3:-10}" attempt=1
    while [ $attempt -le $max_retries ]; do
        if docker pull "$image"; then return 0; fi
        sleep $retry_delay
        retry_delay=$((retry_delay * 2))
        attempt=$((attempt + 1))
    done
    return 1
}

指数退避,初始 10-15 秒,最多重试 3 次。

git pull 失败:stash 处理

服务器上的 docker-compose.prod.yml 不知道什么时候有了本地修改(可能是之前手动调试时改的),导致 git pull 被拒绝:

复制代码
error: Your local changes to the following files would be overwritten by merge

在 workflow 的 SSH 脚本里加了 git stash / git stash pop

bash 复制代码
git stash || true
git pull origin main || echo "⚠️  git pull 失败"
git stash pop || true

不管服务器上有什么本地改动,先暂存、pull、再恢复。

总结

第一版改动涉及 5 个文件:

  1. src/app/chat/page.tsxexport const revalidate = 3600
  2. scripts/purge-cdn.mjs — 新增,腾讯云 CDN 缓存刷新 + 变更检测(Node.js SDK 版)
  3. scripts/deploy.sh — 新增 purge_cdn 函数
  4. .github/workflows/docker-release.ymlCHANGED_FILES 传递链路
  5. .gitignore — 排除部署临时文件

后来又修了一轮容器内执行的问题,额外改了 4 个文件:

  1. Dockerfile.prod — runner 阶段预装 tencentcloud-sdk-nodejs(/tmp 独立安装再合并)
  2. scripts/deploy.sh — CDN 刷新改为容器内执行(docker cp + docker exec)、docker pull 重试、清理文件用 root 权限
  3. .github/workflows/docker-release.yml — 基础镜像拉取重试、git pull 前 stash
  4. docker-compose.prod.yml — 去掉 scripts 挂载

踩坑总结:

  • ES modules 的 NODE_PATH 不生效,只有 CommonJS 的 require 才认
  • Next.js standalone 模式的 node_modules 是精简的,运行时装依赖会解析失败
  • 在 standalone 的 node_modules 上直接 npm install 会因为依赖树不完整报错,要在独立目录装完再合并
  • 容器以非 root 用户运行时,文件操作注意权限

这种「部署后自动清理 CDN」的模式,感觉每个用 CDN 的项目都应该有一套。毕竟用户不应该为你的部署流程买单——你更新了,他就应该看到最新的。

加载评论中...