惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hugging Face - Blog
Hugging Face - Blog
腾讯CDC
阮一峰的网络日志
阮一峰的网络日志
博客园_首页
Last Week in AI
Last Week in AI
月光博客
月光博客
D
DataBreaches.Net
WordPress大学
WordPress大学
雷峰网
雷峰网
酷 壳 – CoolShell
酷 壳 – CoolShell
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 叶小钗
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
U
Unit 42
Recent Announcements
Recent Announcements
宝玉的分享
宝玉的分享
MyScale Blog
MyScale Blog
C
Check Point Blog
F
Fortinet All Blogs
B
Blog
小众软件
小众软件
Vercel News
Vercel News
罗磊的独立博客
有赞技术团队
有赞技术团队

豆沙工作室

不想(二)十七岁 小点阵字 Get an Oscilloscope 杰理 WTS 格式音频转换和打包 站起来,为你的成果答辩吧 公理还是定理? [11ty] 处理资产文件的 CDN 缓存问题 [11ty] 无障碍设计:正确地标识图和表 停止 Mac 上的时间机器备份 从 Gitea 迁移到 Forgejo 评论里的私信消息 记得清理你的日志文件 招魂 Annihilation [11ty] 使用 Pagefind 实现静态站点的搜索 直面死亡 [11ty] 修复 RSS 里的数学公式 看到了一个神奇的 UA PreFound:10B - 四次击键 一点小小的 AI 震撼 PreFound:10A - 文本之外,还有(绘)文字 PreFound:09 - 色彩运算子 计算机不予申辩 Re-Game 2.0 Addendum C - Press START PreFound:08 - 动效与仿射 PreFound:07 - 图片拼贴报 PreFound:06 - 漫游雪花海 PreFound:05 - 写出到屏幕 And Trek I Will PreFound:04 - 举起渲染器
[11ty] 增量式地刷新 CDN 缓存
dousha99 · 2026-08-10 · via 豆沙工作室

计算机科学中困难的事情有两件:缓存管理、取名,以及差一错误。

刚刚上期我们讨论了如何在 11ty 中将对资产文件的修改传播到内容页。但是结尾发现 CDN 会缓存页面文件本身,使得我们必须要手动刷新页面缓存才能实际应用这个更改。

之前的架构中,由于涉及到 Docker 镜像的推送和拉取,对 CDN 的操作需要异步地进行。由于 Watchtower 已经终止维护,再加上感觉并没有必要专门为了一个静态博客上这么复杂的架构,所以我决定撤掉 Docker, 直接使用 Nginx 返回静态页面。

通过 rsync 部署站点

如果没有需要重写以前 WordPress 站点 URL 的历史包袱的话,完全可以将整个站点通过 rclone 放到对象存储中;或者直接用各种 Pages 方案托管整个站点,这样连 Nginx 都不需要设置。

但是拖着需要用一堆 rewrite 保持不知道在哪的老 URL 还能用的我最终选择了用 rsync 将编译产物推到服务器上。至少比 scp 来的强一点。

首先,先在服务器上安装 rsync. 没错,是在服务器上安装 rsync. 在通过 rsync 同步文件时,需要起一个服务端的 rsync 来比对和解压文件。

新建一个系统用户作为 rsync 的登录用户:

sudo useradd -r rsync

rsync 设置登录密钥:

sudo mkdir -p /home/rsync/.ssh
sudo chown -R rsync:rsync /home/rsync
sudo chmod 700 /home/rsync/.ssh

生成一份 SSH 密钥供 rsync 用户使用:

ssh-keygen -t ed25519 -C 'rsync@myserver.arpa'

为了配置方便,私钥密码 (passphrase) 部分可以留空(直接敲回车):

Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/user/.ssh/id_ed25519): /home/user/rsync_key
Enter passphrase for "/home/user/rsync_key" (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/user/rsync_key
Your public key has been saved in /home/user/rsync_key.pub
The key fingerprint is:
SHA256:jeDIoknecb4KHf8Zu9PavXoBf1j5jzuwC935zGi69t4 rsync@dsstudio.tech
The key's randomart image is:
+--[ED25519 256]--+
|                 |
|                 |
|      .       .  |
|   . o . +   o   |
| ..oo.. S + o .  |
|ooo.*      =.o o |
|oo o o .. . +oo..|
|  .   o.=..oo +*.|
|   ... *+oo+=B=oE|
+----[SHA256]-----+

我们将公钥的内容写入服务器端的 rsync 用户授权的密钥中:

cat rsync_key.pub | sudo tee /home/rsync/.ssh/authorized_keys
sudo chmod 600 /home/rsync/.ssh/authorized_keys

同时,我们需要限制一下 rsync 用户通过 SSH 的登录权限:

# 追加到 /etc/ssh/sshd_config 尾部
Match User rsync
    PubKeyAuthentication yes # 打开公钥登录
    X11Forwarding no         # 禁止 X11 流量转发
    AllowTcpForwarding no    # 禁止 TCP 流量转发
    PermitTTY no             # 禁止交互式登录

编辑 CI 流程。由于我在用 Forgejo, 所以我需要明确地指定一些步骤来自于 GitHub 镜像;如果你在用 GitLab 或者 Gitea 则 uses 项目应该写 burnett01/rsync-deployments@v9.

name: deploy-site
on:
  push:
    branches:
      - main

jobs:
  builder:
    runs-on: ubuntu-latest
    steps:
      # 前略,之前的步骤构建好了站点
      - name: Deploy the Thing
        uses: https://github.com/burnett01/rsync-deployments@v9
        with:
          switches: -avzr --delete
          path: _site/
          remote_path: $
          remote_host: $
          remote_user: $
          remote_key: $

为 CI 设置以下秘密变量:

  • RSYNC_REMOTE_PATH: 要推送到的绝对路径,包含末尾斜杠,比如 /srv/http/
  • RSYNC_REMOTE_HOST: 你的服务器 IP 地址
  • RSYNC_REMOTE_USER: 通过 SSH 登录的用户名,就是刚刚设置好的 rsync
  • RSYNC_REMOTE_KEY: 登录 SSH 使用的私钥

你的私钥应该有如下的格式:

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
(中间内容省略)
Q/3FHReMNjmJStfqUxzpAAAAE3JzeW5jQGRzc3R1ZGlvLnRlY2gBAg==
-----END OPENSSH PRIVATE KEY-----

添加完成后,如果你不计划再配置其他项目,则最好删除私钥文件:

shred -u rsync_key

完成之后便可以推到 Git 上验证 CI 流程能否正常将站点推送到服务器上了。

刷新 EdgeOne 的缓存

我是用腾讯云的 EdgeOne 作为站点 CDN 的。如果你在使用其他云服务,则需要自行接入对应的 API SDK.

安装腾讯云的 NodeJS SDK:

npm i tencentcloud-sdk-nodejs

按照 ESM 的方式导入并调用 EdgeOne 的缓存清除任务:

// in scripts/teo-purge-pages.js
import tencentcloud from 'tencentcloud-sdk-nodejs'

const TeoClient = tencentcloud.teo.v20220901.Client

const clientConfig = {
  credential: {
    secretId: process.env.TENCENTCLOUD_SECRET_ID,
    secretKey: process.env.TENCENTCLOUD_SECRET_KEY,
  },
  region: '',
  profile: {
    httpProfile: {
      endpoint: 'teo.tencentcloudapi.com',
    },
  },
}

const client = new TeoClient(clientConfig)

let params = {
  ZoneId: '*',
  Type: 'purge_prefix',
  Method: 'invalidate',
  Targets: [ 'https://blag.dsstudio.tech' ]
}

client.CreatePurgeTask(params).then(
  (data) => {
    console.log(data)
  },
  (err) => {
    console.error('error', err)
  }
)

执行此脚本时,需要配置环境变量 TENCENTCLOUD_SECRET_IDTENCENTCLOUD_SECRET_KEY. 这两个 API 密钥可以在账户配置里获取。推荐是新建一个 RAM 子账户、仅分配 EdgeOne 的操作权限并禁止管理后台登录。

要在 CI 中调用,则可以通过秘密变量传递到对应的环境变量:

jobs:
  builder:
    runs-on: ubuntu-latest
    steps:
      # 前略
      - name: Purge the CDN Cache
        run: npm run purge-pages
        env:
          TENCENTCLOUD_SECRET_ID: $
          TENCENTCLOUD_SECRET_KEY: $

提交成功后,会输出类似如下信息:

{
  RequestId: 'e4db7ad5-315c-470c-b3c4-02fe78d1b780',
  FailedList: [],
  JobId: '3tlugj3jud1f'
}

获取实际更新的文件表

由于每次编译都会导致产物的文件时间和服务器上存放的文件时间不同,这导致了每次构建站点都会全量替换所有的文件。这肯定不是我们需要的。

修改 rsync 的调用方式:要求检查文件摘要 (-c) 并忽略时间戳 (--no-times)

jobs:
  builder:
    steps:
      # 前略
      - name: Deploy the Thing
        uses: https://github.com/burnett01/rsync-deployments@v9
        with:
          switches: -avczr --delete --no-times
          # 后略

但目前我们仍然是会刷新全站缓存,未免有点浪费,能不能只刷新真正被修改的文件呢?

要求 rsync 输出修改摘要到文件以便之后处理 (--log-file):

jobs:
  builder:
    steps:
      # 前略
      - name: Deploy the Thing
        uses: https://github.com/burnett01/rsync-deployments@v9
        with:
          switches: -avczr --delete --no-times --log-file=rsync.log
          # 后略

rsync.log 会包含类似如下的输出:

2026/08/10 08:05:56 [22] building file list
2026/08/10 08:05:57 [22] .d..t...... ./
2026/08/10 08:05:57 [22] .f..t...... feed.xml
2026/08/10 08:05:57 [22] .f..t...... index.html
2026/08/10 08:05:57 [22] <fc.t...... sitemap.xml
2026/08/10 08:05:58 [22] *deleting   pagefind/pagefind.zh_fa64fcd6ad.pf_meta
2026/08/10 08:05:58 [22] *deleting   pagefind/index/zh_facb98c.pf_index
2026/08/10 08:05:58 [22] *deleting   pagefind/index/zh_f5e7107.pf_index
2026/08/10 08:05:58 [22] *deleting   pagefind/index/zh_e5caa93.pf_index
2026/08/10 08:05:58 [22] *deleting   pagefind/index/zh_e51ff77.pf_index

最小化地刷新缓存

有了 rsync.log, 就可以从中解读哪些文件受到影响了。

对于每行日志信息,抛去时间戳前缀后,我们需要关心以下两种输出:

  • <f 开头的,表示传输到服务器的文件
  • *delete 开头的,表示从服务器上移除

那么,我们只需要按行读取日志、按照上述条件筛选出受影响的文件即可:

function compileAffectedFileList(content) {
    const modifiedFiles = rsyncLogLines
        .map((line) => {
            const parts = line.split(' ')
            const flags = parts[3]
            const file = parts.slice(4).join(' ')
            return { flags, file }
        })
        .filter((line) => !!line.flags && !!line.file)
        .filter((line) => line.flags.startsWith('<f'))
        .map((line) => line.file)
    const deletedFiles = rsyncLogLines
        .map((line) => {
            const parts = line.split(' ')
            const flags = parts[3]
            const file = parts.slice(4).join(' ')
            return { flags, file }
        })
        .filter((line) => !!line.flags && !!line.file)
        .filter((line) => line.flags.startsWith('*deleting'))
        .map((line) => line.file)
    const affectedFiles = [...modifiedFiles, ...deletedFiles]
    return affectedFiles
}

读取日志文件:

import { readFileSync } from 'fs'
import { exit } from 'process'

const rsyncLog = readFileSync('rsync.log', 'utf-8')
const affectedFiles = compileAffectedFileList(rsyncLog)

if (affectedFiles.length === 0) {
    // 没有受到影响的文件,可以直接跳过刷新
    exit(0)
}

然后组织并提交:

const TeoClient = tencentcloud.teo.v20220901.Client
const clientConfig = { /* 从略 */ }
const client = new TeoClient(clientConfig)

let params = {
  ZoneId: '*',
  Type: 'purge_prefix',
  Method: 'invalidate',
  Targets: affectedFiles.map(
    (path) => `https://blag.dsstudio.tech/${path}`
  )
}

client.CreatePurgeTask(params).then(
  (data) => {
    console.log(data)
  },
  (err) => {
    console.error('error', err)
  }
)

容易想到,或许 Targets 一次可以提交的数量是有限的;我们会需要分批提交这个数据:

const batchSize = 100
const batches = Array.from({ length: Math.ceil(affectedFiles.length / batchSize)}, (_, i) => affectedFiles.slice(i * n, (i + 1) * n))
const jobs = batches.map(batch => client.CreatePurgeTask({
    ZoneId: '*',
    Type: 'purge_prefix',
    Method: 'invalidate',
    Targets: batch.map(
        (path) => `https://blag.dsstudio.tech/${path}`
    )
}))
Promise.all(jobs).then(console.log).catch(console.error)

这样,每次就只会刷新收到影响的页面啦!