Skip to content

物件儲存與 CDN

💡 學習指南:本文會帶你走完一條完整的鏈路——從檔案上傳到使用者下載。你會看到物件儲存如何像「智慧倉庫」一樣管理海量檔案,CDN 如何像「快遞據點」一樣把內容送到使用者家門口,以及這中間有哪些「坑」等著你跳進去。建議先了解基礎的 HTTP 請求和 DNS 解析原理。

在開始之前,建議你先補幾塊「基礎磚」:


0. 引言:為什麼檔案上傳下載這麼「慢」?

想像一下這個場景:你在一個圖片社群上傳了一張 10MB 的高清照片,結果等了半分鐘才傳完;而你的朋友在北京,點擊下載卻只要 2 秒。為什麼同一張檔案,上傳和下載的體驗天差地別?

或者再想想:你的電商網站雙十一舉辦活動,商品詳情頁突然湧入百萬流量,伺服器直接「躺平」。是頻寬不夠?還是架構設計有問題?

這些問題的答案,都藏在物件儲存CDN 這對「黃金搭檔」裡。


1. 物件儲存:你的「智慧雲端倉庫」

1.1 什麼是物件儲存?

傳統檔案系統就像你家衣櫃:衣服按「上衣/褲子/裙子」分層放,你要找一件襯衫,得先打開衣櫃→上衣區→襯衫格。這種「層級巢狀」的模式,在檔案數量爆炸時會變得極其笨重。

物件儲存則像現代倉儲物流:每個包裹都有一個唯一的「快遞單號」(物件鍵),你只需報單號,倉庫機器人就能從海量包裹中精準取出。

🗄️Object Storage ArchitectureUnderstand the relationship between Bucket, Object, and Metadata.
📦BucketsNamespace isolation and permission control
🖼️
myapp-images-prod
12543 objects
256 GB
🎬
myapp-videos-prod
892 objects
1.2 TB
💾
myapp-backups
3456 objects
500 GB
📄ObjectsFile data + metadata
Click a bucket above to view objects.
💡Core idea:Object storage uses a three-level structure: Account → Bucket → Object. Each object carries rich metadata for retrieval and management.

核心區別一覽

維度傳統檔案系統物件儲存
組織方式層級目錄樹扁平鍵值對
存取協定POSIX(本地檔案操作)HTTP/REST API
擴充性單機容量有限近乎無限水平擴充
中繼資料基礎屬性(大小、時間)豐富的自訂中繼資料
典型場景本地辦公文件圖片/影片/備份/靜態資源

1.2 物件儲存的核心概念

儲存貯體(Bucket):你的「倉庫分區」

儲存貯體是物件儲存的頂級容器,相當於一個獨立的命名空間。所有物件都必須存放在某個儲存貯體中。

命名規則(以阿里雲 OSS 為例):

  • 全域唯一:在整個雲端廠商的所有使用者中不能重複
  • 只能包含小寫字母、數字和短橫線
  • 必須以小寫字母或數字開頭和結尾
  • 長度在 3-63 個字元之間

實戰踩坑:曾經有個團隊按業務線建立了幾十個儲存貯體,結果月底帳單出來傻眼了——每個儲存貯體都有最低儲存費用和請求費用。建議:按「環境+用途」組合規劃儲存貯體,比如 prod-static-assetsdev-backup-archive

物件(Object):你的「資料包裹」

物件是儲存的基本單元,由三部分組成:

  1. 鍵(Key):物件的唯一識別碼,相當於「快遞單號」

    • 範例:images/avatar/2024/user123.jpg
    • 雖然看起來像路徑,但本質只是字串
  2. 資料(Data):物件的內容本身

    • 可以是任意二進位資料
    • 大小限制取決於雲端廠商(通常單個物件 5TB 以內)
  3. 中繼資料(Metadata):描述物件的附加資訊

    • 系統中繼資料:Content-Type、ETag、Last-Modified 等
    • 自訂中繼資料:如 x-oss-meta-ownerx-oss-meta-project

存取控制:誰能動我的「倉庫」?

物件儲存提供多層權限控制:

層級控制方式典型場景
儲存貯體層級Bucket Policy(資源政策)禁止所有外網存取、只允許特定 IP
物件層級ACL(存取控制清單)公開圖片、私有文件
暫時授權STS(安全權杖服務)前端直傳、行動端上傳

安全紅線:永遠不要把 AccessKey ID 和 AccessKey Secret 寫在前端程式碼裡!正確做法是:前端向你的後端申請暫時 STS 憑證,後端驗證身分後回傳帶過期時間的暫時憑證。


2. CDN:你的「全球快遞網路」

2.1 為什麼需要 CDN?

想像你開了一家網路商店,伺服器放在深圳。現在有個使用者在台北存取你的圖片:

  • 沒有 CDN:請求從台北→新竹→台中→高雄→屏東→深圳,跨越 2000 多公里,來回就是 4000 多公里。光網路傳輸就要幾十毫秒,遇到網路壅塞更慘。

  • 有了 CDN:請求從台北直接到台北的 CDN 節點(可能就在台北中華電信機房),距離從 2000 公里變成 20 公里,延遲從 50ms 變成 5ms。

這就是 CDN 的核心價值:讓內容離使用者更近

🌐How CDN Acceleration WorksHow edge nodes, origin server, and origin fetch work together.
👥Global Users
👤
Beijing user
👤
Shanghai user
👤
Guangzhou user
👤
Chengdu user
👤
Overseas user
🌐CDN Edge Nodes
🌐
Beijing node
North China
Cache2.5 TB
Hit92%
🌐
Shanghai node
East China
Cache3.1 TB
Hit89%
🌐
Guangzhou node
South China
Cache1.8 TB
Hit87%
🌐
Chengdu node
Southwest China
Cache1.2 TB
Hit85%
🏢Origin Server
🗄️
Object storage origin
bucket.oss-cn-beijing.aliyuncs.com
Healthy
🎮 Simulation
📊 Access Stats
0
Cache hits
0
Cache misses
0%
Hit rate
0ms
Avg response
💡Core idea:CDN is like opening branches worldwide: users get resources from the nearest branch instead of always visiting the main store.

2.2 CDN 的核心架構

邊緣節點:離使用者最近的「快遞站」

邊緣節點是 CDN 網路中最接近使用者的層級,通常部署在:

  • 電信業者機房(中華電信/遠傳/台灣大哥大)
  • 大城市網際網路交換中心
  • 重要交通樞紐

中國主要 CDN 節點分佈

  • 一線城市:北京、上海、廣州、深圳
  • 二線城市:杭州、南京、成都、武漢、西安
  • 海外:香港、新加坡、東京、矽谷、法蘭克福

Edge Node Distribution Demo

Shows global CDN edge-node distribution and scheduling strategy.

來源站:內容的「總倉庫」

來源站是 CDN 回源獲取內容的地方,可以是:

  • 物件儲存(OSS/COS/S3)
  • 自建伺服器(ECS/物理機)
  • 負載平衡器(SLB/CLB)

關鍵設定

  • 回源 HOST:CDN 節點存取來源站時使用的網域名稱/IP
  • 回源協定:HTTP 還是 HTTPS
  • 回源埠:80、443 還是自訂埠

中間層節點:「區域分撥中心」

在邊緣節點和來源站之間,CDN 通常還有一層或多層中間節點:

  • 匯聚節點:聚合多個邊緣節點的回源請求,減少來源站壓力
  • 區域中心:負責一個大區的內容分發和排程

這種分層架構的好處:

  1. 降低來源站壓力:1000 個邊緣節點的請求,可能只需要向來源站發起 10 次
  2. 提高命中率:熱門內容在中間層就被攔截,不需要回源
  3. 故障隔離:某條鏈路出問題,可以自動切換到其他路徑

2.3 CDN 加速的完整流程

讓我們追蹤一次真實的使用者請求:

⚙️Cache Policy DemoShows CDN and object-storage cache policy configuration, including TTL and refresh.
💡Core idea:Cache policy balances hit rate and freshness. A TTL that is too short causes frequent origin fetches; one that is too long can serve stale content.

Step 1:DNS 解析(智慧排程)

使用者輸入:cdn.example.com/image.jpg

DNS 伺服器回傳:台北中華電信 CDN 節點 IP(1.2.3.4)

這裡的關鍵是智慧 DNS:根據使用者的電信業者、地理位置、節點負載,回傳最優的 CDN 節點 IP。

Step 2:邊緣節點查詢(快取命中?)

請求到達台北中華電信 CDN 節點(1.2.3.4)

節點檢查本地快取:
├─ 命中?直接回傳內容 ✓
└─ 未命中?繼續下一步

Step 3:回源獲取(層層向上)

邊緣節點未命中

向父節點(如:台灣區域中心)請求
├─ 父節點命中?回傳內容
└─ 父節點未命中?繼續向上

    向來源站請求

    來源站回傳內容

Step 4:快取並回傳(下次更快)

內容沿鏈路回傳

每層節點都快取一份

最終到達使用者

這樣,下次有使用者請求同一個檔案時,就能直接從邊緣節點回傳,實現「秒開」。


3. 從上傳到存取:完整鏈路解析

3.1 檔案上傳的三種方式

📤File Upload FlowUnderstand direct upload, multipart upload, and resumable upload.
🚀
Direct upload
Upload small files to object storage in one request
Best for: < 100MB
🔪
Multipart upload
Split large files into parts and upload in parallel
Best for: > 100MB
💾
Resumable upload
Continue from the breakpoint after network interruption
Best for: Any size
🚀 Direct Upload Flow
1
User selects file
Browser selects a 5MB image file
2
Request upload credential
Frontend → backend → temporary STS credential
3
Upload directly to object storage
Browser → OSS/COS, 5MB in one request
4
Upload complete
Return URL; frontend asks backend to save record
💡Core idea:Multipart upload improves reliability for large files. If the network breaks, resumable upload avoids sending the whole file again.

方式一:用戶端 → 伺服器端 → 物件儲存(傳統模式)

瀏覽器 → 你的後端伺服器 → 物件儲存

流程

  1. 使用者選擇檔案,點擊上傳
  2. 檔案先上傳到你的後端伺服器
  3. 後端接收完整檔案後,再轉上傳到物件儲存
  4. 回傳上傳結果給使用者

優點

  • 實作簡單,前後端都好控制
  • 可以在後端做檔案校驗、格式轉換
  • 敏感操作可以記錄日誌、做權限校驗

缺點

  • 頻寬雙吃:使用者上傳佔用一次頻寬,伺服器轉傳又佔用一次
  • 伺服器壓力大:大檔案會佔用大量記憶體和 CPU
  • 上傳慢:相當於多了一道中轉,使用者感知到的上傳時間更長

適用場景:小檔案(<10MB)、需要後端處理(如圖片壓縮、加浮水印)、內部管理系統。

方式二:用戶端直傳物件儲存(現代推薦)

瀏覽器 ──────→ 物件儲存

        後端只簽發暫時憑證

流程

  1. 使用者選擇檔案,前端先向後端申請「上傳憑證」
  2. 後端驗證使用者身分,向物件儲存服務申請暫時 STS 憑證(帶過期時間)
  3. 後端把暫時憑證回傳給前端
  4. 前端拿著憑證,直接上傳檔案到物件儲存
  5. 物件儲存回傳上傳結果,前端通知後端「上傳完成」

優點

  • 上傳快:少了中轉環節,使用者感知速度最快
  • 伺服器壓力小:只處理憑證簽發,不處理檔案串流
  • 頻寬省:只走一次上傳流量
  • 安全性高:暫時憑證有過期時間,洩露也危害有限

缺點

  • 實作稍複雜,需要理解 STS、簽章機制
  • 前端需要處理分片上傳、斷點續傳等邏輯
  • 跨域(CORS)需要設定

適用場景:大檔案上傳、使用者生成內容(UGC)、需要高並行上傳的業務。

方式三:分片上傳 + 斷點續傳(大檔案必備)

10GB 影片檔案

切分成 1000 個 10MB 的分片

並行上傳(同時傳 5 個分片)

斷網了!已傳 600 個

恢復網路,從第 601 個繼續傳

所有分片傳完,發起「合併」請求

為什麼需要分片?

場景不分片分片
網路波動傳了 99% 斷網,全部重傳只重傳失敗的分片
上傳速度單執行緒,速度慢多執行緒並行,速度快
記憶體佔用需要快取整個檔案只需快取目前分片
進度顯示只有 0% 和 100%精確到每個分片的進度

主流雲端廠商的分片規格

廠商分片大小限制最大分片數最小分片大小
阿里雲 OSS100MB10000100KB
騰訊雲 COS5GB100001MB
AWS S35GB100005MB(推薦)
七牛雲100MB100004MB

3.2 CDN 回源策略詳解

⚙️Cache Policy DemoShows CDN and object-storage cache policy configuration, including TTL and refresh.
💡Core idea:Cache policy balances hit rate and freshness. A TTL that is too short causes frequent origin fetches; one that is too long can serve stale content.

什麼是「回源」?

CDN 邊緣節點快取了來源站的內容,但當:

  • 使用者請求的內容第一次被存取
  • 快取的內容已過期(TTL 到期)
  • 快取被手動清除/預熱

CDN 節點就需要向來源站請求最新內容,這個過程就叫「回源」。

回源的三種模式

模式原理適用場景優缺點
直接回源CDN 節點 → 來源站來源站有公網 IP,且流量不大簡單直接,但來源站壓力大
中間源回源CDN 節點 → 中間層 → 來源站大型網站,多層快取架構分擔來源站壓力,架構複雜
OSS/COS 作為來源站CDN 節點 → 物件儲存靜態資源、圖片、影片最佳實踐,成本低、效能好

回源設定實戰

場景 1:物件儲存作為來源站(推薦)

使用者存取:cdn.example.com/images/photo.jpg

            CDN 邊緣節點(台北)

            未命中,回源到來源站

            來源站:bucket-name.oss-cn-beijing.aliyuncs.com

            回傳圖片,CDN 快取並回應使用者

關鍵設定項:

  • 來源站類型:OSS/COS 網域名稱 或 自訂來源站
  • 回源協定:HTTP 還是 HTTPS(建議 HTTPS)
  • 回源 HOST:存取來源站時使用的 Host 標頭
  • 回源 SNI:HTTPS 回源時的伺服器名稱指示

場景 2:多來源站負載平衡

當單個來源站扛不住回源壓力時,可以設定多個來源站:

CDN 邊緣節點
    ├─ 來源站 A (權重 50%)
    ├─ 來源站 B (權重 30%)
    └─ 來源站 C (權重 20%)

主備模式:

CDN 邊緣節點
    ├─ 主來源站 A (健康時全部流量)
    └─ 備來源站 B (主來源故障時切換)

回源頻寬 vs CDN 頻寬

這裡有個容易混淆的概念:

指標定義計費關係
CDN 下行頻寬從 CDN 節點到使用者的流量通常按流量計費的 CDN 費用
回源頻寬從來源站到 CDN 節點的流量通常物件儲存或來源站出流量費用

省錢技巧

  • 提高 CDN 命中率(讓更多請求命中快取,減少回源)
  • 設定合理的快取時間(TTL)
  • 使用預熱功能,在使用者存取前就快取熱點內容
  • 開啟「跟隨 301/302」,避免不必要的回源跳轉

3.3 快取策略設定

⚙️Cache Policy DemoShows CDN and object-storage cache policy configuration, including TTL and refresh.
💡Core idea:Cache policy balances hit rate and freshness. A TTL that is too short causes frequent origin fetches; one that is too long can serve stale content.

快取鍵(Cache Key):決定什麼算「同一個檔案」

CDN 如何判斷兩次請求是否應該回傳同一個快取副本?靠的就是快取鍵

預設快取鍵通常包括

  • URL 路徑(不含查詢參數)
  • 例如:/images/photo.jpg

問題場景

使用者 A 請求:/images/photo.jpg?w=100&h=100  (100x100 縮圖)
使用者 B 請求:/images/photo.jpg?w=800&h=600  (800x600 大圖)

如果快取鍵只包含路徑,兩張不同尺寸的圖片會被認為是同一個檔案,導致混亂。

解決方案:自訂快取鍵規則

規則範例效果
保留指定查詢參數保留 wh不同尺寸分別快取
保留所有查詢參數保留全部完全精確匹配
忽略特定查詢參數忽略 tokentimestamp帶時間戳的 URL 能命中快取
包含請求標頭包含 Accept-Language不同語言回傳不同內容

實戰設定範例(阿里雲 CDN):

快取鍵規則:
- URL 路徑:/images/*
- 保留查詢參數:w, h, format
- 忽略查詢參數:token, timestamp, utm_source

快取時間(TTL):內容「新鮮度」的平衡

TTL(Time To Live)決定了內容在 CDN 節點上快取多久。設定太短,回源多、成本高;設定太長,內容更新後使用者看到舊內容。

按檔案類型設定 TTL 的建議

檔案類型建議 TTL原因
HTML 頁面0-5 分鐘內容頻繁更新,需要即時
JS/CSS 檔案1 年(搭配檔名 hash)內容不變,檔名變化即快取失效
圖片/影片7-30 天更新頻率低,可長期快取
字型檔案1 年幾乎不變
API 回應0-5 分鐘(視業務)資料即時性要求高

前端工程化搭配 CDN 的最佳實踐

javascript
// webpack/vite 設定
output: {
  filename: 'js/[name]-[contenthash:8].js',
  chunkFilename: 'js/[name]-[contenthash:8].chunk.js',
}

產生的檔名:app-a3f2b1c9.js

  • 檔案內容變化 → hash 變化 → 新 URL → 自然快取失效
  • 檔案內容不變 → hash 不變 → URL 不變 → 長期快取命中

快取清除與預熱

手動清除(應急場景)

當你更新了來源站內容,但 CDN 快取還沒過期,使用者看到的還是舊內容:

清除類型效果耗時適用場景
URL 清除指定 URL 的快取失效5-10 分鐘單個檔案更新
目錄清除指定目錄下所有內容失效10-30 分鐘批量更新
全站清除整個網域的快取全部失效30 分鐘以上緊急復原

重要提醒:清除只是讓快取失效,下次請求會回源拉取新內容。不要在高峰期大批量清除,否則可能導致來源站被打爆。

預熱( proactive 最佳化)

清除是被動的(內容已更新),預熱是主動的(提前快取)。

場景:明天上午 10 點要發一篇爆款文章

今晚就提交預熱請求:
- URL: https://cdn.example.com/articles/爆款文章.html
- 預熱範圍:全國所有邊緣節點

效果:
明天 10 點使用者存取時,內容已經在邊緣節點等著了
→ 零回源延遲,秒開體驗

4. 流量排程:讓使用者存取「最近」的節點

🚦Traffic SchedulingUnderstand CDN intelligent scheduling and load balancing.
💡Core idea:Intelligent scheduling combines nearest access, load balancing, and failover to provide global acceleration and high availability.

4.1 智慧 DNS 排程

傳統 DNS 解析:

使用者問:cdn.example.com 的 IP 是什麼?
DNS 答:1.2.3.4(固定的)

智慧 DNS 解析:

使用者(台北中華電信)問:cdn.example.com 的 IP 是什麼?
智慧 DNS:讓我查查... 台北中華電信的 CDN 節點是 1.2.3.4

使用者(高雄遠傳)問:cdn.example.com 的 IP 是什麼?
智慧 DNS:高雄遠傳的 CDN 節點是 5.6.7.8

排程維度

維度說明效果
地理位置按省/市/國家分配就近存取,降低延遲
電信業者中華電信/遠傳/台灣大哥大/BGP同業者傳輸,避免跨網
節點負載即時 CPU/頻寬/QPS避開過載節點
節點健康探查可用性自動移除故障節點
成本因素頻寬單價差異平衡效能與成本

4.2 HTTP DNS 與 IP 直連

傳統 DNS 有個問題:DNS 劫持和解析延遲

HTTP DNS 方案

用戶端 → 繞過系統 DNS → 直接問 HTTP DNS 服務(如 223.5.5.5:80)

    回傳最優 IP 清單(帶權重)

    用戶端根據網路品質探查,選擇最優 IP

優勢:

  • 防劫持:不走電信業者 DNS
  • 更精準:可以按用戶端網路品質選擇 IP
  • 即時性:故障切換更快

實戰建議

  • 行動端 APP 強烈建議接入 HTTP DNS
  • Web 端可以使用 CDN 提供的 CNAME 排程
  • 關鍵業務可以做多 IP 容災(一個網域名稱回傳多個 IP)

5. HTTPS 最佳化:安全與效能的平衡

🔒HTTPS OptimizationUnderstand CDN HTTPS protocol and certificate management.
💡Core idea:HTTPS encrypts traffic with TLS/SSL to prevent man-in-the-middle attacks and data leakage. It is a security baseline for modern web apps.

5.1 為什麼 CDN 上 HTTPS 很重要?

場景對比

無 HTTPS:
使用者存取 http://cdn.example.com/image.jpg

瀏覽器網址列顯示「不安全」

某些瀏覽器/APP 直接攔截存取

SEO 排名降低
有 HTTPS:
使用者存取 https://cdn.example.com/image.jpg

瀏覽器顯示綠色鎖頭標誌

HTTP/2 多工生效

效能 + 安全雙提升

5.2 CDN HTTPS 設定要點

憑證管理

方案說明成本適用場景
雲端廠商免費憑證阿里雲/騰訊雲等提供免費單網域,快速上手
Let's Encrypt社群免費憑證免費自動化部署
商業 DV/OV/EV 憑證Symantec、GeoTrust 等¥幾百-幾萬/年企業級、需要綠條
泛網域憑證*.example.com¥幾千/年多子網域

實戰建議

  • 測試環境:Let's Encrypt 或雲端廠商免費憑證
  • 正式環境:泛網域憑證(省事)或單網域 OV 憑證(省錢)
  • 注意憑證過期時間,設定自動續期提醒

HTTPS 最佳化設定

TLS 版本選擇

推薦設定:僅 TLS 1.2 和 TLS 1.3
相容設定:TLS 1.1 + TLS 1.2 + TLS 1.3(相容老舊瀏覽器)

密碼套件

推薦:ECDHE 金鑰交換 + AES-GCM 加密
停用:DES、RC4、MD5、SHA1

OCSP Stapling

功能:CDN 節點預先獲取憑證撤銷狀態
效果:減少用戶端驗證時間 200-500ms
建議:務必開啟

TLS 工作階段重用

Session ID 重用:用戶端帶著上次 Session ID,伺服器端恢復工作階段
Session Ticket 重用:伺服器端把工作階段狀態加密發給用戶端,下次帶來
效果:避免完整 TLS 交握,減少 1-RTT

5.3 HTTP/2 與 HTTP/3 在 CDN 上的應用

HTTP/2 多工

HTTP/1.1:
請求 1 (index.html) ────────────────→
回應 1 ←──────────────────────────────
請求 2 (style.css) ─────────────────→
回應 2 ←──────────────────────────────
請求 3 (script.js) ─────────────────→
回應 3 ←──────────────────────────────
(序列,一個完了下一個)

HTTP/2:
請求 1 ──→
請求 2 ──→   合併在一個 TCP 連線上,幀交錯傳輸
請求 3 ──→
回應 1 ←──   按優先級串流式回傳
回應 2 ←──
回應 3 ←──
(並行,一個連線多工)

HTTP/2 伺服器端推送

場景:使用者請求 index.html,裡面引用了 style.css 和 script.js

傳統方式:
1. 使用者下載 index.html
2. 解析發現需要 style.css 和 script.js
3. 再發兩個請求獲取

HTTP/2 推送:
1. 使用者請求 index.html
2. CDN 節點回傳 index.html 的同時,主動推送 style.css 和 script.js
3. 使用者解析 html 時,資源已經在快取裡了

注意:推送要謹慎,推多了浪費頻寬,推少了沒效果

HTTP/3 (QUIC)

HTTP/2 的問題:基於 TCP,隊頭阻塞
→ 一個 TCP 封包丟失,整個連線等待重傳

HTTP/3 的解決:基於 QUIC(UDP 之上實現可靠傳輸)
→ 每個串流獨立,一個串流阻塞不影響其他串流
→ 連線遷移:WiFi 切 4G,連線不中斷
→ 0-RTT 交握:第一次存取也能快速建立連線

現狀:2024 年主流 CDN 已支援 HTTP/3,建議開啟

6. 存取分析:看懂你的 CDN 報表

📊Access AnalyticsUnderstand CDN access statistics and log analytics.
💡Core idea:Log analytics shows who accessed which resources and when, helping detect unusual access patterns and security events.

6.1 核心指標解讀

頻寬(Bandwidth)

定義:單位時間內傳輸的資料量
單位:bps(位元每秒)、Mbps、Gbps

CDN 頻寬 = 所有邊緣節點的出流量總和

注意區分:
- 計費頻寬:通常按 95 峰值或日峰值計費
- 實際頻寬:即時傳輸速率

頻寬與流量的關係

1 Mbps 頻寬持續跑 1 小時 = 450 MB 流量
(計算:1,000,000 bps × 3600s ÷ 8 ÷ 1024 ÷ 1024 ≈ 429 MB)

QPS(Queries Per Second)

定義:每秒查詢/請求數

CDN QPS = 所有邊緣節點每秒處理的 HTTP 請求總數

注意:QPS 高不代表頻寬高
- 小檔案場景:QPS 很高,頻寬不高
- 大檔案場景:QPS 不高,頻寬很高

命中率(Hit Ratio)

定義:在 CDN 邊緣節點命中的請求佔總請求的比例

計算公式:
命中率 = (命中數 / 總請求數) × 100%

命中率 = (1 - 回源流量 / 總出流量) × 100%

行業標準:
- 圖片/影片/JS/CSS:> 95%
- HTML 頁面:50-80%(視更新頻率)
- API 介面:通常不快取或極低

命中率低的常見原因

原因現象解決方案
快取時間太短TTL 只有幾分鐘根據檔案類型調整 TTL
查詢參數變化URL 帶隨機數設定忽略特定參數
快取鍵設定不當不該區分的被區分了最佳化快取鍵規則
內容更新頻繁檔案經常被覆蓋使用版本號或 hash 檔名
首次存取多新內容或新節點提前預熱

6.2 日誌分析與問題排查

CDN 日誌欄位解析

典型 CDN 存取日誌包含以下欄位:

時間 | 用戶端 IP | 請求方法 | URL | HTTP 狀態碼 | 回應大小 | 快取狀態 | 回應時間 | Referer | User-Agent

範例:
2024-01-15 14:32:01 | 114.114.114.114 | GET | https://cdn.example.com/images/photo.jpg | 200 | 153600 | HIT | 23 | https://example.com/ | Mozilla/5.0...

關鍵欄位解釋:

欄位說明分析價值
cache_status快取狀態HIT(命中)、MISS(未命中)、EXPIRED(過期)
response_time回應時間(ms)判斷使用者體驗,>500ms 需最佳化
http_statusHTTP 狀態碼404/500 錯誤排查
bytes_sent傳送位元組數頻寬統計

常見問題排查

問題 1:使用者反映存取慢

排查步驟:

1. 看日誌 response_time
   - 如果很大(>500ms):檢查是快取 MISS 還是來源站慢

2. 檢查 cache_status
   - HIT:快取命中,慢可能是檔案太大或節點問題
   - MISS:未命中,需最佳化快取策略或命中率

3. 檢查用戶端 IP 分佈
   - 某些地區慢:可能是該節點負載高或覆蓋不足

問題 2:快取不生效,每次都回源

排查清單:

□ 來源站回應標頭是否有 Cache-Control: no-cache / private?
□ URL 是否帶隨機參數(如 ?_=123456)?
□ 快取鍵設定是否正確?
□ TTL 設定是否過短?
□ 是否命中瀏覽器本地快取而非 CDN?

問題 3:費用暴漲

排查方向:

1. 看帳單明細
   - CDN 流量費高:檢查是否有大檔案被頻繁存取,或被盜鏈
   - 回源流量費高:檢查命中率是否驟降
   - 請求數費用高:檢查是否有 CC 攻擊或爬蟲

2. 看存取日誌
   - 是否有大量 404 請求(可能是掃描或設定錯誤)
   - Referer 是否異常(判斷是否被盜鏈)

3. 安全設定
   - 開啟防盜鏈(Referer 白名單)
   - 開啟 IP 黑名單/白名單
   - 設定 CC 防護

7. 實戰案例:從 0 搭建圖片加速方案

7.1 業務場景

假設你是一個圖片社群的技術負責人,面臨以下挑戰:

  • 使用者上傳:使用者每天上傳 100 萬張圖片(平均 2MB/張)
  • 使用者存取:每天 5000 萬次圖片檢視請求
  • 存取分佈:使用者遍佈全國,海外也有少量存取
  • 效能要求:圖片載入時間 < 500ms
  • 成本預算:盡量控制在每月 5 萬以內

7.2 架構設計

                         ┌──────────────────────────────────────┐
                         │           使用者上傳流程                │
                         └──────────────────────────────────────┘

   使用者瀏覽器                                  後端服務                      物件儲存
       │                                            │                            │
       │  1. 申請上傳憑證                            │                            │
       │───────────────────────────────────────────>│                            │
       │                                            │                            │
       │                                            │  2. 申請 STS 暫時憑證      │
       │                                            │───────────────────────────>│
       │                                            │                            │
       │                                            │  3. 回傳 STS 憑證         │
       │                                            │<───────────────────────────│
       │                                            │                            │
       │  4. 回傳上傳憑證(含 STS)                  │
       │<───────────────────────────────────────────│                            │
       │                                            │                            │
       │  5. 直接上傳檔案(使用 STS 簽章)          │
       │──────────────────────────────────────────────────────────────────────>│
       │                                            │                            │
       │  6. 回傳上傳結果(URL、ETag 等)           │
       │<──────────────────────────────────────────────────────────────────────│
       │                                            │                            │
       │  7. 通知後端上傳完成(儲存到資料庫)        │
       │───────────────────────────────────────────>│                            │


                         ┌──────────────────────────────────────┐
                         │           使用者存取流程                │
                         └──────────────────────────────────────┘

   使用者瀏覽器            DNS 解析              CDN 節點              物件儲存(來源站)
       │                     │                     │                     │
       │  1. 請求圖片 URL    │                     │                     │
       │────────────────────────────────────────>│                     │
       │                     │                     │                     │
       │                     │  2. DNS 查詢        │                     │
       │                     │────────────────────>│                     │
       │                     │                     │                     │
       │                     │  3. 回傳最優節點 IP │                     │
       │                     │<────────────────────│                     │
       │                     │                     │                     │
       │  4. 連線 CDN 節點   │                     │                     │
       │────────────────────────────────────────>│                     │
       │                     │                     │                     │
       │                     │  5. 檢查快取        │                     │
       │                     │                     ├─ 命中?直接回傳     │
       │                     │                     └─ 未命中?繼續       │
       │                     │                     │                     │
       │                     │                     │  6. 回源獲取       │
       │                     │                     │──────────────────>│
       │                     │                     │                     │
       │                     │                     │  7. 回傳檔案       │
       │                     │                     │<──────────────────│
       │                     │                     │                     │
       │                     │  8. 快取並回應      │                     │
       │<────────────────────────────────────────│                     │

7.3 關鍵設定詳解

物件儲存設定

儲存貯體規劃

 Bucket: myapp-images-prod
 ├─ 目錄結構:
 │   ├─ uploads/           # 使用者上傳的原圖
 │   │   ├─ 2024/01/15/user123-abc.jpg
 │   │   └─ 2024/01/15/user456-def.png
 │   ├─ thumbnails/        # 縮圖
 │   │   ├─ small/         # 100x100
 │   │   ├─ medium/        # 400x300
 │   │   └─ large/         # 800x600
 │   └─ processed/         # 處理後的圖片(加浮水印等)

 ├─ 存取權限:
 │   ├─ 原圖目錄:私有(需簽章存取)
 │   ├─ 縮圖目錄:公共讀
 │   └─ 跨域 CORS:允許 *.myapp.com 存取

 └─ 生命週期政策:
     ├─ 上傳 7 天後:低頻儲存(省 40% 費用)
     ├─ 上傳 90 天後:歸檔儲存(省 70% 費用)
     └─ 上傳 3 年後:自動刪除(或轉存到更便宜的冷儲存)

CORS 跨域設定

xml
<CORSConfiguration>
  <CORSRule>
    <AllowedOrigin>https://myapp.com</AllowedOrigin>
    <AllowedOrigin>https://www.myapp.com</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>HEAD</AllowedMethod>
    <AllowedHeader>*</AllowedHeader>
    <ExposeHeader>ETag</ExposeHeader>
    <ExposeHeader>x-oss-request-id</ExposeHeader>
    <MaxAgeSeconds>3600</MaxAgeSeconds>
  </CORSRule>
</CORSConfiguration>

CDN 加速設定

快取策略設定

全域預設規則:
├─ 快取鍵:URL 路徑 + 保留 w、h、format 查詢參數
├─ 預設 TTL:7 天
└─ 回源 HOST:自動跟隨

按檔案類型細分:
├─ *.html:
│   ├─ TTL:5 分鐘
│   └─ 優先從記憶體快取讀取

├─ *.js, *.css:
│   ├─ TTL:1 年
│   └─ 忽略查詢參數(因為檔名有 hash)

├─ *.jpg, *.png, *.gif, *.webp:
│   ├─ TTL:30 天
│   ├─ 保留查詢參數(w、h、format 用於動態裁剪)
│   └─ 啟用圖片自動壓縮最佳化

└─ /api/*:
    ├─ TTL:0(不快取)
    └─ 直接回源

HTTPS 最佳化設定

憑證設定:
├─ 憑證類型:泛網域憑證 *.myapp.com
├─ 部署方式:CDN 主控台上傳,自動續期
└─ 備用憑證:EV 憑證用於主網域(顯示綠色網址列)

TLS 設定:
├─ 最低 TLS 版本:1.2(相容性與安全平衡)
├─ 最高 TLS 版本:1.3
├─ 密碼套件:僅啟用強加密套件
├─ OCSP Stapling:開啟
├─ TLS 工作階段重用:開啟 Session Ticket
└─ HSTS:開啟(max-age=31536000)

HTTP/2 與 HTTP/3:
├─ HTTP/2:開啟(多工、標頭壓縮)
├─ HTTP/2 Server Push:按需開啟(推薦用 Preload 替代)
└─ HTTP/3 (QUIC):開啟(實驗性功能,逐步放量)

7.4 成本控制策略

費用構成分析

月度 CDN + 物件儲存費用構成:

CDN 部分:
├─ 下行流量費(大頭,約 60%)
│   ├─ 中國大陸:0.15-0.30 元/GB
│   ├─ 亞太地區:0.40-0.80 元/GB
│   └─ 歐美:0.30-0.60 元/GB

├─ 請求數費用(小頭,約 5%)
│   ├─ HTTP:0.01-0.05 元/萬次
│   └─ HTTPS:0.05-0.15 元/萬次(因為 TLS 交握消耗資源)

├─ 頻寬峰值費用(可選計費方式)
│   └─ 95 峰值計費:適合流量波動大的場景

└─ 加值功能費(約 5%)
    ├─ HTTPS 憑證管理
    ├─ WAF 防護
    ├─ 即時日誌推送
    └─ 邊緣指令碼/函式

物件儲存部分:
├─ 儲存容量費(約 15%)
│   ├─ 標準儲存:0.12-0.15 元/GB/月
│   ├─ 低頻儲存:0.08-0.10 元/GB/月
│   └─ 歸檔儲存:0.03-0.05 元/GB/月

├─ 請求費用(約 5%)
│   ├─ PUT:0.01-0.05 元/萬次
│   └─ GET:0.005-0.01 元/萬次

├─ 資料取回費用(低頻/歸檔)
│   └─ 提前刪除或取回收取額外費用

└─ 回源出流量費(約 10%)
    └─ CDN 回源到物件儲存的流量費

省錢技巧實戰

技巧 1:儲存分級,自動生命週期管理

yaml
# 生命週期規則範例
rules:
  - id: image-lifecycle
    prefix: uploads/
    transitions:
      # 7 天後轉低頻儲存,省 30% 費用
      - days: 7
        storageClass: IA
      # 90 天後轉歸檔儲存,省 70% 費用
      - days: 90
        storageClass: Archive
    # 3 年後自動刪除
    expiration:
      days: 1095

技巧 2:提高 CDN 命中率,減少回源

命中率從 90% 提升到 95% 意味著什麼?

假設:
- 日流量:10 TB
- 命中率 90%:回源 1 TB
- 命中率 95%:回源 0.5 TB

節省回源流量:0.5 TB/天 × 0.15 元/GB × 30 天 = 2250 元/月

技巧 3:壓縮與格式最佳化

圖片最佳化方案:
├─ 原圖儲存在物件儲存(不對外直接開放)
├─ CDN 開啟圖片處理功能:
│   ├─ 格式自動轉換:JPEG → WebP/AVIF(省 30-50%)
│   ├─ 品質自動壓縮:視覺無損壓縮(省 20-40%)
│   ├─ 尺寸自適應:根據裝置回傳合適尺寸
│   └─ 漸進式載入:先模糊後清晰
└─ 效果:頻寬成本降低 50-70%

技巧 4:頻寬峰值上限與警報

yaml
# 頻寬上限設定
bandwidth_cap:
  daily_limit: 500 # Mbps,日峰值超過則自動停用 CDN
  monthly_limit: 10000 # GB,月流量超過則停用

  # 警報閾值
  alerts:
    - threshold: 70% # 達到 70% 發警報
      channels: [sms, email]
    - threshold: 90% # 達到 90% 打電話
      channels: [phone]

8. 總結:物件儲存 + CDN 的黃金法則

8.1 架構設計原則

原則 1:動靜分離

動態內容(API、HTML)→ 走來源站或邊緣函式
靜態內容(圖片、JS、CSS、影片)→ 走 CDN + 物件儲存

原則 2:就近服務

使用者在哪裡,內容就快取到哪裡
→ 選擇覆蓋廣的 CDN 服務商
→ 啟用智慧 DNS 排程
→ 重要內容提前預熱

原則 3:分層快取

瀏覽器本地快取(最強)

CDN 邊緣節點快取(次強)

CDN 中間層/區域節點(兜底)

物件儲存/來源站(最後防線)

原則 4:成本與體驗的平衡

儲存分級:熱資料標準儲存,冷資料歸檔儲存
快取策略:高頻內容長 TTL,低頻內容短 TTL
壓縮最佳化:WebP/AVIF 格式,智慧品質壓縮
監控警報:設定頻寬上限,防止異常流量

8.2 避坑清單

儲存貯體命名與權限

  • [ ] 貯體名稱全域唯一,避免被佔用
  • [ ] 私有檔案不要設定為公共讀
  • [ ] AccessKey 不要寫在前端程式碼裡,用 STS 暫時憑證
  • [ ] 啟用伺服器端加密(SSE)保護敏感資料

CDN 快取設定

  • [ ] HTML 檔案 TTL 不要太長(建議 < 5 分鐘)
  • [ ] JS/CSS 建議用帶 hash 的檔名,TTL 設為 1 年
  • [ ] 快取鍵要合理,不要把使用者資訊等變數放進去
  • [ ] 重要更新後記得清除快取或預熱

HTTPS 安全

  • [ ] 憑證不要過期,設定自動續期
  • [ ] 最低 TLS 版本建議 1.2
  • [ ] 開啟 HSTS 防止降級攻擊
  • [ ] 敏感 Cookie 設定 Secure 和 HttpOnly

成本控制

  • [ ] 開啟頻寬上限警報,防止異常流量
  • [ ] 低頻/歸檔儲存有最小儲存時間和提前刪除費,注意規則
  • [ ] 回源流量費也很貴,努力提高 CDN 命中率
  • [ ] 定期分析存取日誌,清理殭屍資源

9. 實戰程式碼範本

9.1 前端直傳物件儲存(JavaScript)

javascript
/**
 * 物件儲存直傳工具類別
 * 支援:阿里雲 OSS、騰訊雲 COS、AWS S3
 */
class DirectUploader {
  constructor(config) {
    this.provider = config.provider // 'oss' | 'cos' | 's3'
    this.region = config.region
    this.bucket = config.bucket
    this.getCredentials = config.getCredentials // 獲取暫時憑證的函式
  }

  /**
   * 獲取 STS 暫時憑證
   */
  async fetchCredentials() {
    // 向後端申請暫時憑證
    const credentials = await this.getCredentials()
    return {
      accessKeyId: credentials.accessKeyId,
      accessKeySecret: credentials.accessKeySecret,
      sessionToken: credentials.securityToken || credentials.sessionToken,
      expiration: credentials.expiration
    }
  }

  /**
   * 產生上傳簽章(適用於前端計算簽章)
   */
  generateSignature(credentials, fileKey, fileType, options = {}) {
    const timestamp = new Date().toISOString()
    const date = timestamp.slice(0, 10).replace(/-/g, '')

    // 不同廠商的簽章演算法略有差異
    switch (this.provider) {
      case 'oss':
        return this._ossSignature(credentials, fileKey, date, options)
      case 'cos':
        return this._cosSignature(credentials, fileKey, date, options)
      case 's3':
        return this._s3Signature(credentials, fileKey, date, options)
      default:
        throw new Error('Unknown provider')
    }
  }

  /**
   * 單檔案上傳(小檔案 < 100MB)
   */
  async upload(file, options = {}) {
    const credentials = await this.fetchCredentials()
    const fileKey = this._generateFileKey(file, options.directory)

    const formData = new FormData()

    // 構建表單欄位(不同廠商欄位名不同)
    const formFields = this._buildFormFields(
      credentials,
      fileKey,
      file.type,
      options
    )
    Object.entries(formFields).forEach(([key, value]) => {
      formData.append(key, value)
    })

    formData.append('file', file)

    // 傳送上傳請求
    const uploadUrl = this._getUploadUrl()
    const response = await fetch(uploadUrl, {
      method: 'POST',
      body: formData,
      // 如果上傳大檔案,可能需要設定更長的逾時
      signal: options.signal // 支援 AbortController 取消上傳
    })

    if (!response.ok) {
      const errorText = await response.text()
      throw new Error(`Upload failed: ${response.status} ${errorText}`)
    }

    return {
      url: this._getFileUrl(fileKey),
      key: fileKey,
      etag: response.headers.get('ETag'),
      size: file.size
    }
  }

  /**
   * 分片上傳(大檔案 > 100MB)
   */
  async multipartUpload(file, options = {}) {
    const partSize = options.partSize || 10 * 1024 * 1024 // 預設 10MB/片
    const parallel = options.parallel || 3 // 預設 3 個並行

    const credentials = await this.fetchCredentials()
    const fileKey = this._generateFileKey(file, options.directory)

    // 1. 初始化分片上傳
    const uploadId = await this._initMultipartUpload(
      credentials,
      fileKey,
      file.type
    )

    // 2. 計算分片
    const parts = []
    const totalParts = Math.ceil(file.size / partSize)
    for (let i = 0; i < totalParts; i++) {
      const start = i * partSize
      const end = Math.min(start + partSize, file.size)
      parts.push({
        number: i + 1,
        start,
        end,
        blob: file.slice(start, end)
      })
    }

    // 3. 上傳分片(帶並行控制和斷點續傳)
    const uploadedParts = []
    const failedParts = []

    // 支援斷點續傳:檢查哪些分片已上傳
    if (options.resume) {
      const existingParts = await this._listParts(
        credentials,
        fileKey,
        uploadId
      )
      for (const part of existingParts) {
        uploadedParts.push(part)
      }
    }

    // 過濾出未上傳的分片
    const pendingParts = parts.filter(
      (p) => !uploadedParts.some((up) => up.partNumber === p.number)
    )

    // 並行上傳
    const uploadPart = async (part) => {
      try {
        const etag = await this._uploadPart(
          credentials,
          fileKey,
          uploadId,
          part
        )
        return { partNumber: part.number, etag }
      } catch (error) {
        failedParts.push({ part, error })
        throw error
      }
    }

    // 使用 Promise.all 控制並行
    const chunks = []
    for (let i = 0; i < pendingParts.length; i += parallel) {
      chunks.push(pendingParts.slice(i, i + parallel))
    }

    for (const chunk of chunks) {
      const results = await Promise.allSettled(chunk.map(uploadPart))
      for (const result of results) {
        if (result.status === 'fulfilled') {
          uploadedParts.push(result.value)
        }
      }
    }

    // 檢查是否所有分片都上傳成功
    if (uploadedParts.length !== totalParts) {
      throw new Error(
        `Upload incomplete: ${uploadedParts.length}/${totalParts} parts uploaded`
      )
    }

    // 4. 完成分片上傳(合併分片)
    await this._completeMultipartUpload(
      credentials,
      fileKey,
      uploadId,
      uploadedParts
    )

    return {
      url: this._getFileUrl(fileKey),
      key: fileKey,
      size: file.size,
      parts: totalParts
    }
  }

  /**
   * 產生檔案儲存路徑
   */
  _generateFileKey(file, directory = '') {
    const date = new Date()
    const datePath = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`
    const random = Math.random().toString(36).substring(2, 10)
    const ext = file.name.split('.').pop() || 'bin'
    const key = directory
      ? `${directory}/${datePath}/${random}.${ext}`
      : `${datePath}/${random}.${ext}`
    return key
  }

  // ============ 各廠商特定方法 ============

  _getUploadUrl() {
    switch (this.provider) {
      case 'oss':
        return `https://${this.bucket}.oss-${this.region}.aliyuncs.com`
      case 'cos':
        return `https://${this.bucket}.cos.${this.region}.myqcloud.com`
      case 's3':
        return `https://${this.bucket}.s3.${this.region}.amazonaws.com`
      default:
        throw new Error('Unknown provider')
    }
  }

  _getFileUrl(key) {
    return `https://${this.bucket}.${this.provider === 'oss' ? 'oss' : 'cos'}-${this.region}.${
      this.provider === 'oss'
        ? 'aliyuncs.com'
        : this.provider === 'cos'
          ? 'myqcloud.com'
          : 'amazonaws.com'
    }/${key}`
  }

  // 各廠商的簽章、分片上傳等方法...(根據實際需求實作)
  _buildFormFields(credentials, fileKey, fileType, options) {
    // 各廠商表單欄位構建邏輯
    // 這裡需要根據具體廠商的文件實作
    return {}
  }

  async _initMultipartUpload(credentials, fileKey, fileType) {
    // 各廠商初始化分片上傳邏輯
    return 'upload-id'
  }

  async _uploadPart(credentials, fileKey, uploadId, part) {
    // 各廠商分片上傳邏輯
    return 'etag'
  }

  async _completeMultipartUpload(credentials, fileKey, uploadId, parts) {
    // 各廠商完成分片上傳邏輯
  }

  async _listParts(credentials, fileKey, uploadId) {
    // 各廠商列出已上傳分片邏輯
    return []
  }
}

// 使用範例
const uploader = new DirectUploader({
  provider: 'oss',
  region: 'cn-beijing',
  bucket: 'myapp-images-prod',
  getCredentials: async () => {
    // 向後端申請暫時憑證
    const res = await fetch('/api/upload/credentials')
    return res.json()
  }
})

// 小檔案上傳
async function uploadAvatar(file) {
  try {
    const result = await uploader.upload(file, {
      directory: 'avatars',
      onProgress: (progress) => {
        console.log(`上傳進度: ${progress.percent}%`)
      }
    })
    console.log('上傳成功:', result.url)
    return result
  } catch (error) {
    console.error('上傳失敗:', error)
    throw error
  }
}

// 大檔案分片上傳
async function uploadVideo(file) {
  try {
    const result = await uploader.multipartUpload(file, {
      directory: 'videos',
      partSize: 10 * 1024 * 1024, // 10MB 每片
      parallel: 3, // 3 個並行
      resume: true, // 支援斷點續傳
      onProgress: (progress) => {
        console.log(
          `上傳進度: ${progress.percent}%, 已傳 ${progress.loaded}/${progress.total}`
        )
      },
      onPartComplete: (part) => {
        console.log(`分片 ${part.number} 上傳完成`)
      }
    })
    console.log('上傳成功:', result.url)
    return result
  } catch (error) {
    console.error('上傳失敗:', error)
    // 可以在這裡實作重試邏輯或儲存斷點資訊
    throw error
  }
}

9.2 後端暫時憑證服務(Node.js/Express)

javascript
/**
 * 物件儲存 STS 暫時憑證服務
 * 支援:阿里雲 OSS、騰訊雲 COS、AWS S3
 */
const express = require('express')
const STS = require('ali-oss').STS // 阿里雲
// const COS = require('cos-nodejs-sdk-v5') // 騰訊雲
const router = express.Router()

// 設定
const config = {
  // 阿里雲 OSS 設定
  oss: {
    accessKeyId: process.env.OSS_ACCESS_KEY_ID,
    accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
    region: 'oss-cn-beijing',
    bucket: 'myapp-images-prod',
    // STS 角色 ARN(需要在 RAM 主控台建立)
    roleArn: process.env.OSS_STS_ROLE_ARN
  }
}

/**
 * 獲取 STS 暫時憑證(阿里雲 OSS)
 * POST /api/upload/credentials
 */
router.post('/credentials', async (req, res) => {
  try {
    // 1. 驗證使用者身分(根據實際情況實作)
    const userId = req.user?.id
    if (!userId) {
      return res.status(401).json({ error: 'Unauthorized' })
    }

    // 2. 產生唯一的檔案路徑前綴(用於權限隔離)
    const date = new Date()
    const prefix = `uploads/${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${userId}/`

    // 3. 建立 STS 用戶端
    const sts = new STS({
      accessKeyId: config.oss.accessKeyId,
      accessKeySecret: config.oss.accessKeySecret
    })

    // 4. 申請暫時憑證
    const result = await sts.assumeRole(
      config.oss.roleArn,
      {
        // Policy 限制權限範圍(最小權限原則)
        Statement: [
          {
            Effect: 'Allow',
            Action: [
              'oss:PutObject',
              'oss:InitiateMultipartUpload',
              'oss:UploadPart',
              'oss:CompleteMultipartUpload',
              'oss:AbortMultipartUpload',
              'oss:ListParts'
            ],
            Resource: [`acs:oss:*:*:${config.oss.bucket}/${prefix}*`]
          }
        ],
        Version: '1'
      },
      3600, // 憑證有效期 1 小時
      'web-upload-session-' + Date.now()
    )

    // 5. 回傳憑證和設定
    res.json({
      success: true,
      data: {
        // STS 暫時憑證
        credentials: {
          accessKeyId: result.credentials.AccessKeyId,
          accessKeySecret: result.credentials.AccessKeySecret,
          sessionToken: result.credentials.SecurityToken,
          expiration: result.credentials.Expiration
        },
        // 上傳設定
        config: {
          provider: 'oss',
          region: config.oss.region,
          bucket: config.oss.bucket,
          endpoint: `https://${config.oss.bucket}.${config.oss.region}.aliyuncs.com`,
          prefix: prefix, // 檔案路徑前綴
          // 安全限制
          maxSize: 100 * 1024 * 1024, // 最大 100MB
          allowedTypes: [
            'image/jpeg',
            'image/png',
            'image/gif',
            'image/webp',
            'video/mp4'
          ]
        }
      }
    })
  } catch (error) {
    console.error('Get credentials failed:', error)
    res.status(500).json({
      success: false,
      error: 'Failed to get upload credentials',
      message: error.message
    })
  }
})

/**
 * 回呼通知:前端上傳完成後通知後端
 * POST /api/upload/callback
 */
router.post('/callback', async (req, res) => {
  try {
    const { key, etag, size, mimeType, originalName } = req.body
    const userId = req.user?.id

    // 1. 驗證檔案是否存在
    // 2. 儲存檔案資訊到資料庫
    const fileRecord = await db.files.create({
      userId,
      key,
      etag,
      size,
      mimeType,
      originalName,
      url: `https://cdn.example.com/${key}`,
      createdAt: new Date()
    })

    // 3. 非同步處理:產生縮圖、擷取中繼資料、內容審核等
    await processFileAsync(fileRecord)

    res.json({
      success: true,
      data: {
        fileId: fileRecord.id,
        url: fileRecord.url,
        size: fileRecord.size
      }
    })
  } catch (error) {
    console.error('Upload callback failed:', error)
    res.status(500).json({
      success: false,
      error: 'Failed to process uploaded file'
    })
  }
})

module.exports = router

9.3 防盜鏈與安全設定

javascript
/**
 * CDN 防盜鏈與安全設定範例
 */

// 1. Referer 防盜鏈(防止其他網站直接引用你的資源)
const refererConfig = {
  // 白名單模式:只允許以下 Referer 存取
  allowList: [
    '*.myapp.com', // 主站
    '*.myapp.cn', // 國內站
    'localhost:*', // 本地開發
    '127.0.0.1:*'
  ],

  // 黑名單模式(可選):禁止以下 Referer
  blockList: [
    '*.competitor.com', // 競爭對手
    'spam-site.com'
  ],

  // 空 Referer 處理:是否允許直接存取(瀏覽器網址列輸入 URL)
  allowEmptyReferer: false // 正式環境建議 false,測試環境可 true
}

// 2. URL 鑑權(更安全的防盜鏈,帶時間戳和簽章)
class URLAuth {
  constructor(config) {
    this.key = config.key // 鑑權金鑰,只在伺服器端儲存
    this.expireTime = config.expireTime || 3600 // 預設 1 小時有效期
  }

  /**
   * 產生帶鑑權的 URL
   * @param {string} url - 原始 URL,如 https://cdn.example.com/images/photo.jpg
   * @param {number} expireIn - 有效期(秒)
   * @returns {string} 帶鑑權參數的 URL
   */
  sign(url, expireIn = this.expireTime) {
    const urlObj = new URL(url)
    const pathname = urlObj.pathname
    const timestamp = Math.floor(Date.now() / 1000) + expireIn

    // 構造簽章字串(不同廠商格式不同,這裡是通用範例)
    const signStr = `${pathname}-${timestamp}-${this.key}`
    const signature = this._md5(signStr)

    // 添加鑑權參數
    urlObj.searchParams.set('sign', signature)
    urlObj.searchParams.set('t', timestamp.toString())

    return urlObj.toString()
  }

  /**
   * 驗證 URL 簽章(在 CDN 邊緣或來源站使用)
   */
  verify(url) {
    const urlObj = new URL(url)
    const signature = urlObj.searchParams.get('sign')
    const timestamp = parseInt(urlObj.searchParams.get('t'))
    const pathname = urlObj.pathname

    // 檢查是否過期
    if (timestamp < Math.floor(Date.now() / 1000)) {
      return { valid: false, error: 'URL expired' }
    }

    // 驗證簽章
    const signStr = `${pathname}-${timestamp}-${this.key}`
    const expectedSign = this._md5(signStr)

    if (signature !== expectedSign) {
      return { valid: false, error: 'Invalid signature' }
    }

    return { valid: true }
  }

  _md5(str) {
    // 實際專案中使用 crypto-js 或其他 MD5 函式庫
    // 這裡僅作範例
    return require('crypto').createHash('md5').update(str).digest('hex')
  }
}

// 使用範例
const auth = new URLAuth({
  key: 'your-secret-key-only-known-by-server',
  expireTime: 3600 // 1 小時有效期
})

// 伺服器端產生帶簽章的 URL
const signedUrl = auth.sign(
  'https://cdn.example.com/private/document.pdf',
  7200
)
// 結果:https://cdn.example.com/private/document.pdf?sign=xxxxx&t=1699123456

// CDN 邊緣或來源站驗證
const result = auth.verify(signedUrl)
if (!result.valid) {
  // 回傳 403 Forbidden
}

// 3. IP 黑白名單
const ipConfig = {
  // 只允許特定 IP 存取(適合內部系統)
  whiteList: [
    '192.168.1.0/24', // 內網網段
    '10.0.0.0/8'
  ],

  // 禁止特定 IP 存取(封鎖攻擊者)
  blackList: ['1.2.3.4', '5.6.7.8']
}

// 4. UA(User-Agent)黑白名單
const uaConfig = {
  // 禁止爬蟲/下載工具
  blackList: [
    'Wget',
    'curl',
    'python-requests',
    'Scrapy',
    'AhrefsBot',
    'SemrushBot'
  ],

  // 只允許瀏覽器存取(嚴格模式)
  whiteList: [
    'Mozilla/*', // 現代瀏覽器
    'AppleWebKit/*'
  ]
}

10. 名詞對照表

英文術語中文對照解釋
Object Storage物件儲存一種資料儲存架構,將資料作為物件管理,而非檔案系統層級結構。適合儲存圖片、影片、備份等非結構化資料。
Bucket儲存貯體物件儲存中的頂級容器,用於組織和隔離資料。每個貯體有獨立的權限控制和設定。
Object物件/檔案物件物件儲存的基本單元,包含資料本身、中繼資料(Metadata)和全域唯一鍵(Key)。
CDN內容傳遞網路Content Delivery Network,透過在全球部署邊緣節點,將網站內容快取到離使用者最近的位置,加速存取速度。
Edge Node邊緣節點CDN 網路中部署在各地的快取伺服器,直接為使用者提供內容存取服務。
Origin來源站CDN 回源獲取內容的伺服器,可以是物件儲存、ECS 或自建伺服器。
Cache Hit快取命中使用者請求的內容在 CDN 邊緣節點已存在,直接回傳,無需回源。
Cache Miss快取未命中邊緣節點沒有請求的內容,需要回源獲取。
Hit Ratio命中率快取命中次數佔總請求次數的比例。命中率越高,回源越少,成本越低。
TTL存活時間/快取時間Time To Live,內容在 CDN 節點上快取的有效期。過期後需要重新回源。
Back to Source回源CDN 邊緣節點向來源站請求內容的過程。
Purge/Refresh清除快取強制使 CDN 快取失效,下次請求回源獲取最新內容。
Preheat預熱在正式發布前,主動將內容推送到 CDN 節點,讓使用者第一次存取就能命中快取。
CORS跨來源資源共享Cross-Origin Resource Sharing,瀏覽器的安全機制,控制不同網域之間的資源存取。
Referer參照來源HTTP 請求標頭欄位,指示請求是從哪個頁面連結過來的。用於防盜鏈。
STS安全權杖服務Security Token Service,頒發暫時存取憑證的服務,用於前端直傳等場景。
Multipart Upload分片上傳將大檔案切分成多個小分片並行上傳,支援斷點續傳,提高上傳效率和可靠性。
ETag實體標籤HTTP 回應標頭,用於識別資源的特定版本,常用於快取驗證。
S3 APIS3 相容介面AWS S3 的物件儲存 API 規範,多數雲端廠商的物件儲存都相容此介面。
Canonical Query String規範查詢字串簽章字串的一部分,用於計算請求簽章,確保請求不被竄改。

總結:物件儲存 + CDN 的黃金法則

  1. 上傳走直傳:大檔案用分片,安全用 STS
  2. 快取分層次:瀏覽器 -> CDN -> 來源站,層層快取
  3. 就近服務使用者:智慧 DNS + 全球節點覆蓋
  4. 安全不鬆懈:HTTPS + 防盜鏈 + 存取控制
  5. 成本要監控:命中率、頻寬、儲存分級,持續最佳化

這套架構撐起了網際網路絕大部分的靜態資源存取,理解它,你就理解了現代 Web 效能最佳化的基石。