改用rclone 來同步靜態文件到S3 和R2

Written by

in

之前從CloudFlare 改用CloudFront 主要是因為中國訪問的速度不佳,因為CloudFlare 會直接將中國人的訪問送去美國,但是CloudFront 可以選擇送去香港。

CloudFlare 其實也可以,只不過要付更多錢。

因此我想同時將wordpress 的靜態文件同步到S3 和R2 ,就當多一個備份。

修改前的狀態:

同步工具使用 aws s3 sync,腳本是 /usr/local/bin/aws-cli-sync-s3.sh。

#!/bin/bash
/usr/bin/inotifywait -m -r -e modify,create,delete /home/www/bbken.org/wp-conten
t/uploads/ |
while read path action file; do
    echo "Change detected: $action $file"
    /usr/bin/aws s3 sync /home/www/bbken.org/wp-content/uploads/ s3://bbken.org/
wp-content/uploads/ --delete
done

由 systemd 服務 s3-sync.service 以 www 使用者執行。腳本用 inotifywait watch /home/www/bbken.org/wp-content/uploads/ 的 modify、create、delete 事件,每偵測到一次事件就對 s3://bbken.org/wp-content/uploads/ 完整同步一次,帶 –delete。但是,這不太通用,移植到其他雲端的時候會有一點問題

市面上同步工具其實蠻多,但是rclone這東西歷史悠久,就他了。

未來要擴充到其他雲端的話,添加更多的profile 就可以。aws cli 本質為 S3 設計,連 R2 要靠 –endpoint-url;rclone 原生支援數十種後端,全部共用同一套 rclone sync 指令。

接下來我們建立並驗證 rclone 的 S3 remote:建一個名為 s3 的 remote,type 選 s3、provider 選 AWS、env_auth 設為 true,直接沿用 EC2 的 IAM role,不需填AK/SK。驗證方式是 rclone lsd s3:,成功列出帳號內所有 bucket 含 bbken.org。

初次用預設參數空跑耗時 23.9 秒,原因是 rclone 預設比對檔案原始修改時間,該值存在物件的 X-Amz-Meta-Mtime 自訂中繼資料裡,LIST 結果不含這項,必須對每個物件各發一次 HEAD 請求,11358 個檔案就是 1.1 萬次額外請求,需要調整 rclone 參數解決效能問題。

改用 –fast-list –update –use-server-modtime 後,改以 LIST 就能取得的 LastModified 比對,不再發 HEAD。實測結果是 aws s3 sync 空跑 24.8 秒,rclone 加上這組參數後 2.1 秒,快約 12 倍。

這個調整對免費仔使用R2 的意義比速度更大:HEAD 在 R2 算 Class B 操作,免費額度每月 1000 萬次,若不調參數,每次同步就消耗約 1.1 萬次,雖然也不是不可以,但平均每天只有3次同步機會似乎顯得略少。

S3 的同步很簡單,因為不需要AK/SK,接下來在 Cloudflare 建立 bucket 與憑證:建立 bucket bbken-org,產生 R2 API Token 取得 Access Key ID、Secret Access Key 與 endpoint。

接下來建立並驗證 rclone 的 R2 remote:用 rclone config 新增名為 r2 的 remote,type 選 s3、provider 選 Cloudflare,填入金鑰、region 設 auto、endpoint 設 R2 提供的url,並加上 no_check_bucket = true。設定檔權限設為 600。

首次完整同步 R2 並驗證,需要從EFS上同步的文件總量為 11358 個檔案、1.048 GiB,約占 R2 免費儲存額度 10 GB 的一成。

禁用舊腳本,新增同步腳本 /usr/local/bin/rclone-sync-uploads.sh。相較舊版做了五項改動。

#!/bin/bash
# 監看 WordPress uploads 目錄,變動時用 rclone 同步到多個目的地

SRC=/home/www/bbken.org/wp-content/uploads/
RCLONE=/usr/bin/rclone
DEBOUNCE=5   # 最後一次事件後等待幾秒才同步

# 要擴充目的地,在這裡加一行即可
DESTS=(
    "s3:bbken.org/wp-content/uploads"
    "r2:bbken-org/wp-content/uploads"
)

sync_all() {
    for dest in "${DESTS[@]}"; do
        if "$RCLONE" sync "$SRC" "$dest" --fast-list --update --use-server-modtime; then
            echo "Synced to $dest"
        else
            echo "ERROR: sync to $dest failed" >&2
        fi
    done
}

# 啟動時先完整同步一次,補上服務停止期間漏掉的變更
sync_all

/usr/bin/inotifywait -m -r -q \
    -e close_write,create,delete,moved_to,moved_from \
    --format '%e %w%f' "$SRC" |
while read -r event; do
    echo "Change detected: $event"
    # 去抖動:持續讀取後續事件,直到連續 DEBOUNCE 秒沒有新事件
    while read -r -t "$DEBOUNCE" event; do
        echo "Change detected: $event"
    done
    sync_all
done

一是目的地改成陣列 DESTS,同時包含 s3:bbken.org/wp-content/uploads 與 r2:bbken-org/wp-content/uploads,用loop逐一同步,未來擴充只需加一行,比如要同步去GCP。

二是加入 5 秒去抖動,用 read -t 持續吸收後續事件,直到連續 5 秒沒有新事件才同步一次,避免 WordPress 一次上傳產生多個縮圖就觸發多次完整同步。

三是監看事件由 modify 改為 close_write,避免寫入過程重複觸發,並補上 moved_to 與 moved_from,涵蓋以改名或搬移方式進出目錄的檔案。

四是服務啟動時先完整同步一次,補上服務停止期間漏掉的變更。

五是各目的地分開判斷結果,單一目的地失敗會寫入日誌但不影響其他目的地。

接下來修改 systemd 服務,變更為新的腳本,用 systemctl edit –full s3-sync.service,將 ExecStart 指向新腳本,Description 改為 Uploads Sync Service (rclone to S3 and R2),並把 After 從 network.target 改為 network-online.target 並加上對應的 Wants。接著 daemon-reload 與 restart。

[Unit]
Description=Uploads Sync Service (rclone to S3 and R2)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=www
ExecStart=/usr/local/bin/rclone-sync-uploads.sh
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

驗證:目測沒問題,我應該沒瞎。

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *