Files
2026-06-25 20:25:08 +08:00

9.1 KiB
Raw Blame History

title, date, tags, categories, draft
title date tags categories draft
聊聊我博客部署的优化:从 her-cat/upyun-deployer 换到官方 upx 2026-06-25T19:54:00+08:00
Hugo
GitHub Actions
又拍云
部署
折腾
false

端午假期,我把用了挺久的 her-cat/upyun-deployer 又拍云自动部署脚本换成了官方出品的 upx。

https://help.upyun.com/knowledge-base/developer_tools/#upx-efbc88e591bde4bba4e8a18ce5b7a5e585b7efbc89

upx

这个工具是我在逛官方文档时偶然发现的。之前一直用 GitHub 开源作者 hercat 的 her-cat/upyun-deployer,稳定性确实不错,跑了一年多都没出什么问题。不过现在 Hugo 博客的文章越来越多,部署时间也越来越长。

GitHub Actions 对免费用户有每月 2000 分钟的运行时长限制,有时候遇到网络波动,部署就会卡很久,甚至直接超时失败。

这篇文章简单记录一下迁移过程,顺便对比分析两款工具的实现差异,希望能给同样在用 Hugo + 又拍云的朋友一些参考。

先聊聊 her-cat/upyun-deployer 的实现

在说 upx 之前,我让ai 查了查 her-cat/upyun-deployer 的源码,看看它是怎么工作的。作者是 her-cat,这个项目帮了很多人,包括我之前也一直在用,非常感谢作者的付出。

它的核心逻辑在 main.go 里,大概是这样的:

func (d *UpYunDeployer) UploadFiles() {
    // 第一步:获取远程所有文件列表
    files, dirs := d.GetAllRemoteFilesByPath(publishDir)
    
    // 第二步:遍历本地文件
    err := filepath.Walk(d.localDir, func(filename string, file os.FileInfo, err error) error {
        // ...
        // 第三步:对每个文件,先获取远程文件信息
        remoteFileInfo, err := d.getFileInfo(relativeFilename)
        
        // 第四步:判断是否需要上传
        if upyun.IsNotExist(err) {
            d.uploadFile(putObjectConfig, false)
            return
        }
        
        // 第五步:MD5 对比,如果相同就跳过
        if remoteFileInfo.MD5 == fmt.Sprintf("%x", md5.Sum(data)) {
            fmt.Printf("[%s] cached!\n", relativeFilename)
            return
        }
        
        // 第六步:不同的话,先删除再上传
        err = d.deleteFile(relativeFilename, true)
        d.uploadFile(putObjectConfig, true)
        return nil
    })
    
    // 第七步:全量 CDN 刷新
    d.up.Purge(urls)
    
    // 第八步:删除远程多余的文件和目录
    d.deleteFiles(files)
    d.deleteDirs(dirs)
}

整体思路其实很清晰:

  1. 先把远程所有文件列表拉下来
  2. 遍历本地每个文件,和远程对比 MD5
  3. 不一样就先删再上传
  4. 最后全量刷新 CDN,再删掉远程多余的文件

这个思路很不错,就是在实际使用中,有几个地方会比较耗时:

第一个是每个文件都要单独调 API。

上传一个文件之前,先要调 GetInfo 检查远程是否存在;如果存在且 MD5 不同,还要先调 Delete 删除,再调 Put 上传。一个文件搞不好就要发 2-3 次请求,文件多了自然就慢了。

第二个是获取远程文件列表的过程。

GetAllRemoteFiles 是递归遍历所有目录的,虽然用了 goroutine 并发请求,但文件越多,请求次数就越多,这块也会花不少时间。

第三个是全量 CDN 刷新。

每次部署完都会把所有上传过的文件都 Purge 一遍,虽然又拍云的 Purge 是异步的,但全量刷新既浪费配额,也可能导致用户访问的时候缓存命中率下降。

upx 是什么

upx 是又拍云官方出的命令行工具,全称是 UpYun Uploader(我猜的)。

UPX 是专为开发者设计的,基于命令行的云存储管理工具。它可以实现文件上下传、增量文件同步、目录创建删除、文件删除等。

项目地址:https://github.com/upyun/upx

我最看重的就是它的 sync 命令,类似于Linux的 rsync,支持增量同步。而且因为是官方维护的,对 API 的利用应该会更高效一些。

我的部署方案

下面说说我现在的部署配置,大家可以参考一下。

GitHub Actions 配置

我把构建和部署拆成了两个 job,这样构建产物可以复用。

name: Deploy to Production

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      with:
        fetch-depth: 1

    - name: Build Hugo site
      run: hugo --gc --minify

    - name: Upload artifact
      uses: actions/upload-artifact@v4
      with:
        name: public
        path: ./public

  deploy-upyun:
    runs-on: ubuntu-latest
    needs: build
    steps:
    - name: Download artifact
      uses: actions/download-artifact@v4
      with:
        name: public
        path: ./public

    - name: Deploy to UpYun
      env:
        UPYUN_SERVICE: ${{ secrets.UPYUN_BUCKET }}
        UPYUN_OPERATOR: ${{ secrets.UPYUN_OPERATOR }}
        UPYUN_PASSWORD: ${{ secrets.UPYUN_PASSWORD }}
      run: |
        chmod +x scripts/deploy_upyun.sh
        ./scripts/deploy_upyun.sh ./public / 10

部署脚本

然后是具体的部署脚本,放在 scripts/deploy_upyun.sh:

#!/bin/bash
# UpYun 部署脚本 - 使用 upx 官方工具

set -e

LOCAL_DIR="${1:-./public}"
REMOTE_DIR="${2:-/}"
THREADS="${3:-10}"

# 又拍云 upx sync 的线程数限制在 1-10 之间
if [ "$THREADS" -lt 1 ]; then
    THREADS=1
elif [ "$THREADS" -gt 10 ]; then
    THREADS=10
fi

SERVICE_NAME="${UPYUN_SERVICE:-}"
OPERATOR="${UPYUN_OPERATOR:-}"
PASSWORD="${UPYUN_PASSWORD:-}"

if [ -z "$SERVICE_NAME" ] || [ -z "$OPERATOR" ] || [ -z "$PASSWORD" ]; then
    echo "❌ 错误:缺少必要的环境变量"
    exit 1
fi

echo "🚀 UpYun 部署开始"

# 下载又拍云 upx
UPX_VERSION="0.4.9"
UPX_URL="https://collection.b0.upaiyun.com/softwares/upx/upx_${UPX_VERSION}_linux_amd64.tar.gz"
UPX_BIN="/tmp/upyun-upx"

if [ ! -x "$UPX_BIN" ]; then
    echo "📥 下载又拍云 upx v${UPX_VERSION}..."
    curl -sL "$UPX_URL" -o /tmp/upx.tar.gz
    tar -xzf /tmp/upx.tar.gz -C /tmp
    mv /tmp/upx "$UPX_BIN"
    chmod +x "$UPX_BIN"
    echo "✅ upx 安装完成"
fi

# 登录并同步
"$UPX_BIN" login "$SERVICE_NAME" "$OPERATOR" "$PASSWORD"
"$UPX_BIN" sync "$LOCAL_DIR" "$REMOTE_DIR" \
    -w "$THREADS" \
    --delete \
    --strong

echo "✅ UpYun 部署完成!"

脚本很简单,就是下载 upx,登录,然后 sync 一下。

upx 为什么更快?

我自己用下来,upx 确实比 her-cat/upyun-deployer 快不少,我个人认为主要有这几个原因:

第一是官方实现,对 API 的利用更高效。

毕竟是官方自己的工具,很多操作应该是批量处理的,而不是每个文件单独调一次 API。这一点从速度上就能明显感觉出来。

第二是 sync 命令做了专门的优化。

upx sync 作为核心功能,内部的文件对比、差异计算应该都是经过优化的。不像社区项目,作者可能是业余时间维护,精力有限。

第三是线程控制更稳定。

upx 的 -w 参数可以控制并发线程数,而且内部应该有完整的错误重试和限流机制,不容易触发又拍云的 API 频率限制。

不过有一点要注意,又拍云的 upx sync 线程数上限是 10,别开太大了,开了也没用。

一些小技巧

build hash 跳过重复部署

有时候我们可能只是改了一下 README,或者 CI 配置变了,但 Hugo 构建的产物其实是一样的。这时候可以用 build hash 来判断是否需要重新部署。

- name: Generate build hash
  id: hash
  run: echo "build_hash=$(find ./public -type f -exec md5sum {} \; | md5sum | cut -d' ' -f1)" >> $GITHUB_OUTPUT

- name: Cache build
  uses: actions/cache@v4
  with:
    key: ${{ runner.os }}-deploy-upyun-${{ steps.hash.outputs.build_hash }}
    path: ./public

如果缓存命中了,说明构建产物没变化,就可以跳过部署步骤,省不少时间。

注意和系统 upx 重名

Linux 系统里可能已经装了另一个 upx,就是那个可执行文件压缩工具。为了避免冲突,我在脚本里把下载的又拍云 upx 改名叫 upyun-upx,路径放在 /tmp 下面,这样就不会搞混了。

一些注意事项

  1. 敏感文件别传上去了:部署前确认一下 .git、.env 这些东西不在 public 目录里,不然传到 CDN 上就尴尬了。

  2. 权限尽量给小一点:去又拍云后台创建一个专门的操作员,只给上传权限就行,别用主账户密码。安全第一。

  3. CDN 刷新按需来:upx sync 上传的文件会自动更新,一般不需要手动 Purge。除非你改了文件名没变内容,那可能需要手动刷新一下。

  4. 网络问题:GitHub Actions 的服务器在海外,部署到又拍云国内节点可能会慢一些。我自己用下来还能接受,如果觉得慢的话,可以考虑用国内的 CI 服务。

相关链接: