阿晴把一支十分鐘的料理影片拖進瀏覽器。進度到 83% 時,家裡的網路斷了。重新連線後,她希望繼續上傳;上傳完成後,她把連結傳給朋友,對方卻看到「影片處理中」。隔天有人分享,觀看人數突然增加;再隔天,她發現畫面拍到了不該公開的資訊,按下刪除。
對阿晴來說,她只是上傳、分享,再刪掉一支影片。平台卻得一路回答:檔案收到哪裡了?哪個版本可以播放?誰有權公開?內容進了快取以後,還能在多久內停止存取?我沿著這些問題設計一個隨選影音平台,從第一版的容量與資料模型,走到轉碼重試、熱門流量與下架。
我會先選擇基本畫質完成就能發布、高畫質後補的方案。接下來的設計都要守住這個承諾:觀眾拿到的版本必須完整可播,而較晚完成的工作不能蓋過創作者後來的決定。直播、廣告競價、版權辨識模型與個人化推薦模型留待後續;它們接入時,也要遵守同一套發布與授權規則。
1. 上傳完成,只代表創作者交出了檔案
阿晴看到進度條 100%,系統就顯示「發布成功」,是最容易做出的第一版。直到第一位觀眾打開影片,播放器才發現還沒有能用的清單,或者只有一個瀏覽器不支援的原始檔。
這裡要把完成條件分開:
- 上傳完成:平台已收到預期的原始物件,大小與完整性驗證符合本次工作階段的約定。創作者可以關掉分頁,後續處理由平台負責。
- 可播放:至少一組支援的影音產物與清單已準備好,且經過檢查。
- 公開:創作者選擇公開、內容政策允許,而且目前可播放的版本通過發布交易。
接收完成、可播放與公開,是三份不同的承諾。 UI 可以顯示「檔案已收到,正在準備播放」,不能為了消除等待感,把三份承諾壓成一個成功訊息。
- 01 · 收到原檔創作者:斷線後接著上傳
觀眾:仍看不到成品。檔案收完不等於可播放。
- 02 · 允許發布創作者:最低可播組合已就緒
觀眾:依公開、未公開或私密的規則取得存取資格。
- 03 · 播放開始觀眾:授權後取得清單與分段
影音位元組由 CDN 傳輸;創作者可以繼續等待高畫質版本。
- 04 · 下架生效立即停止發放新的播放授權
搜尋、推薦與快取繼續收斂;已下載的內容無法收回。
收檔成功、資產就緒、可公開與可播放,是不同的承諾。
這項區分會影響 API。建立影片回傳 video ID,不代表觀眾能播放。完成上傳回傳 receipt,不代表轉碼成功。創作者按發布,可以得到「已記錄公開意圖,等待基本畫質完成」。瀏覽器不必一直等到所有畫質處理完。
我選擇「先完成基本播放組合,再允許公開;較高畫質後補」的產品政策。基本組合包含一個可用的影片 rendition、需要的音訊、海報與經驗證的播放清單。實際產品可以要求所有畫質完成才公開,代價是增加等待時間。也可以開放原檔下載,但那是另一種交付契約,不能把下載連結當成串流播放完成。
影片如果被政策審查擋住,原檔依然可能已收到、轉碼也可能成功。創作者後台要能解釋「目前不能公開的原因」。只顯示 failed,會讓她以為又要上傳一次。
2. 分開估算容量,讓影片傳輸離開業務 API
假設產品第一階段有每日 1,000 支上傳,每支平均 500 MB、長度十分鐘;觀眾每日觀看 100,000 次,每次平均觀看五分鐘,平均實際傳輸位元率為 3 Mbps。以下採十進位 MB、GB、TB,忽略協定與容器額外開銷。估算先看哪些參數會左右儲存、頻寬與處理需求。
上傳原始資料是 1,000 × 500 MB = 500 GB/日。若原檔保留三十天,尚未加副本、備份與失敗殘留前,就是 15 TB。假設各種播放產物合計為原檔的 1.5 倍,三十天再增加 22.5 TB。這個倍率取決於內容、編碼設定與畫質組合,不能拿來當廠商保證;原檔保留策略改變,帳單也會跟著改變。
觀看資料約為 100,000 × 300 秒 × 3 Mb/s ÷ 8 = 11.25 TB/日,平均下行約 1.04 Gbps。若尖峰是平均的十倍,規劃情境約為 10.4 Gbps。熱門影片可能讓十分鐘內的負載比這個倍數更高,所以平均值只能用來估成本,不能直接當准入上限。
處理工作量又是另一回事。每天 10,000 分鐘的來源影片,如果「產出整組基本畫質」的實測成本是每分鐘來源需要四個 CPU 分鐘,便是每天 40,000 CPU 分鐘,約 27.8 個持續忙碌的 CPU 核心。四倍只是待驗證參數;codec、preset、解析度、硬體與內容運動量都會改變結果。排程還要加上尖峰、失敗重跑、機器維護與等待時間預算。
觀看傳輸量遠高於上傳量,轉碼又需要獨立的 CPU 預算。因此,應用伺服器不代理整段影片位元組,讓客戶端直接向受限的物件儲存入口上傳,播放由 CDN 分發。API 負責許可、狀態與收據。若讓每個影片片段都穿過 PHP 或 Node 業務程序,原本用來處理帳號與 metadata 的資源會被大量傳輸占住。
可驗收目標必須說明測量起點
示例目標可以寫成:受支援、十分鐘內且 1080p 以下的合格來源,從平台確認上傳完成到基本畫質可播放,95% 在十分鐘內;超過規格的來源不混進同一個分母。播放啟動則從使用者按下播放,到第一幀實際呈現,按裝置與網路類型分組觀測。把伺服器回傳 manifest 的 200 當成第一幀,就量錯了使用者感受。
卡頓比例也要說清楚:我建議記錄播放期間因緩衝停住的秒數與有效觀看秒數,另外記錄啟動失敗率。暫停、切換背景與使用者主動拖曳不能全算成系統卡頓。完整的 SLO 定義方式 與 容量預算 可以另讀;在這個產品裡,我們要用量測結果決定加轉碼機,還是先修播放器與快取。
3. 最小架構可以是模組化單體,加上獨立的轉碼工作者
第一版的 API 應用可以用 Laravel 或 Node,將帳號、影片目錄、上傳工作階段、發布與播放許可分成模組,共用一個 MySQL 寫入來源。轉碼工作者因為資源需求與處理時間不同,獨立執行。物件儲存收原始檔與版本化產物,佇列傳遞工作,CDN 服務觀看流量。
- 控制路徑客戶端 → 模組化單體 API → MySQL
Upload、Video、Playback 模組先共用部署。影片狀態與 outbox 事件在同一交易提交。
- 背景路徑Outbox relay → 持久佇列 → worker
事件可重送,工作可重試;以 job ID、產物版本與條件提交守住發布邊界。
- 影音位元組路徑創作者 → 物件儲存 → CDN → 播放器
創作者直接分塊上傳;worker 讀原檔、寫版本化產物。API 不代理大檔。
線路分開是為了分開擴充與故障影響,不代表第一版就要拆成微服務。
影片目錄與發布規則先留在同一個應用裡。若它們仍由同一團隊維護,拆成服務會多出跨服務一致性問題,暫時沒有必要。轉碼則涉及 CPU 密集、長時間運算與不可信輸入,值得獨立部署。這樣可以單獨擴充工作者,也能限制解碼程序的權限。
MySQL 交易無法直接包住物件儲存、佇列與 CDN。我採用 outbox:接受上傳完成的交易,同時記錄原檔資訊與待處理事件;背景 dispatcher 把事件送進佇列,成功後標記傳遞進度。AWS 的 transactional outbox 指引 說明了資料庫寫入與訊息傳送之間的雙寫風險,也提醒消費者仍須面對重複訊息。
若 dispatcher 已送出事件,但還沒標記就當機,事件會再送一次。因此消費端以 event ID 去重、以工作識別限制可接受的結果。用 outbox 能保留「應該做什麼」的證據,並沒有把所有外部副作用變成一次完成。
用四個 API 把責任接起來
以下節選主要介面;公開意圖的變更與刪除另接第 6、10 節,兩者都需核對擁有者授權與目前版本條件。
先用四個 API 表達建立影片、交付原檔與取得播放許可的責任:
POST /videos
輸入:title、intended_visibility、idempotency_key
回傳:video_id、generation、visibility=draft
POST /videos/{id}/uploads
輸入:generation、expected_bytes、checksum_policy
回傳:upload_session_id、expires_at、受限的 part 上傳資訊
POST /uploads/{session}/complete
輸入:有序 part_number + etag 清單、原檔驗證資訊
回傳:同一 session 的 receipt、received 或 reconciling
POST /videos/{id}/playback-sessions
輸入:觀眾身分與播放能力;不接受客戶端自選任意 object key
回傳:asset_version、manifest_url、短期存取資格、expires_at
第一個 API 的冪等鍵綁定帳號與請求內容;同鍵但不同標題/可見性意圖應回報衝突,避免兩次意外點擊變成兩支影片。完成上傳的冪等範圍是 UploadSession,播放 session 則可以每次重新核發。把所有 POST 都用同一種去重方式,會錯過不同操作的目的。
錯誤回應也要能支援 UI:版本已過期代表需要重新讀取影片狀態;來源格式不支援代表需要換檔;暫時處理失敗可以稍後查詢 receipt。回應只暴露安全的原因碼,不把內部 bucket 路徑、雲端憑證或完整例外堆疊送到創作者頁面。
哪些東西可以先不放進來
Redis 可以快取目錄或短期流量計數,但不需要承擔影片是否公開的唯一真相。搜尋初期可從有限範圍的資料庫查詢開始;只有文字搜尋品質或查詢負載需要時,再接搜尋引擎。推薦先用最新公開影片與有限的熱門清單,讓第一版能完成發現內容的旅程。
另一個方案是使用託管影音服務,讓服務商處理上傳、編碼與分發。它降低媒體基礎設施維護量,但仍要確認回呼重送、素材識別、可見性、授權與刪除的契約,並估算遷移原檔與產物的成本。我選擇分開的元件,是為了看清這些責任;小團隊若沒有影音運維能力,託管方案可能更合理。
4. 把影片身分、檔案版本與公開權限分開保存
一個 status 欄位很快會長成 uploading、transcoding、public、private、blocked、deleted。這些值不在同一個維度:private 的影片仍然可以完成轉碼,public 的影片也可能正在準備替換版本。硬塞在一起,會讓每次增加需求都要修改一大串不完整的轉移規則。
把這些狀態拆成四種記錄,公開權限就不必跟著上傳或轉碼進度一起變動:
- Video 保存穩定的影片身分、owner、標題、visibility、policy 狀態、目前的 source generation、單調遞增的 metadata revision,以及 active playback asset ID。
- UploadSession 保存一次傳輸的 provider upload ID、預期大小、到期時間、原始物件 key 與狀態。
- MediaAsset 保存某一 generation 的原檔與播放產物、驗證結果、manifest 位置。
- TranscodeJob 保存工作鍵、attempt、租約與處理結果。
- UploadSession · 傳輸created → uploading → verified
只有確認物件與完整性後,才能標為 verified。中止與過期走各自的終止路徑。
- MediaAsset · 產物pending → processing → ready
每個 generation、格式與解析度各有資產。失敗能重試;ready 代表已驗證。
- Video · 可見性private / unlisted / public
三者是可選規則。每次變更推進 metadata_revision;下架旗標覆蓋可見性與資產啟用狀態。
- 播放資產閘門 · 一個交易私密影片也能啟用可播產物
檢查目前 generation、最低資產 ready、審核允許與尚未 blocked,再更新 播放資產指標。
資產 ready 不會自動變 public;visibility 也不能證明資產可播放。
其中 generation 是應用層的遞增版本識別,不等同於某個雲端供應商的 object generation。創作者換原檔,或下架後要使舊處理結果失去資格時,系統推進這個版本。另有 metadata revision,任何影響對外狀態的修改都遞增,包含同一原檔的公開/私密切換;不能拿 source generation 當作所有事件的排序鍵。每個工作都帶上它處理的 generation;完成後必須再比對。
資料表的最小約束可以是:UploadSession 的 provider upload ID 唯一;TranscodeJob 的 (video_id, generation, profile_version) 唯一;每個產物指向一個明確的 Video 與 generation;播放資產指標只能指到經驗證、且仍被允許發布的資產。工作可能有多個 attempt,不能因為工作鍵唯一,就假設只有一個程序還在執行。
這些記錄共同決定影片能否公開:
可以公開 = 原檔已確認
且基本播放組合已驗證
且產物 generation = Video 目前 generation
且創作者公開意圖有效
且 policy 允許
且 Video 未被刪除
資產啟用與公開的交易可以鎖定 Video 記錄,讀取相關資產驗證狀態,再以目前版本更新播放資產指標;或採帶版本條件的更新,並檢查影響筆數。不能先查一次狀態、離開交易做一段工作,最後不帶條件地寫 public。這會讓「剛被刪除」與「剛轉碼完成」互相覆蓋。
這也延續了訂單系統的 寫入責任邊界:worker 有權提出產物完成的證據,發布模組有權決定那份證據現在是否仍可生效。
5. 續傳的答案在儲存端,不在瀏覽器的進度條
阿晴重新連線時,瀏覽器記得自己曾送出第八個 part,但不確定伺服器收到了沒有。如果直接從第九個開始,可能少一塊;如果重新建立整個上傳,也可能白白留下半份物件。
以 S3 multipart 為例,API 建立 UploadSession,綁定 owner、video、generation 與受控的 object key,回傳每一 part 的短期上傳許可。
客戶端直接上傳,將 part number 與服務回傳的 ETag 回報平台保存成自己的 part ledger;重連時,以儲存端查詢結果核對 ledger,再補缺失的部分。
ListParts 要處理分頁,只用來核對,不直接拿它的結果組成完成請求;完成請求使用自己維護且確認過的有序 part/ETag 清單。瀏覽器的進度只是提示,續傳也不能悄悄換成另一份來源檔。
先查證原檔,再提交影片狀態
- 建立上傳工作API 配發 video_id 與 upload_id,客戶端保存續傳進度。
- 分片直接送物件儲存每片有 part_number 與明確指定的 checksum;影片 bytes 不繞經 API。
- 斷線後,先問再補傳用 ListParts/查詢確認已收到的分片,只補缺片。本機顯示成功不算服務端確認。
- 冪等完成請求complete 帶冪等鍵。回覆遺失時可重送,取得既有結果。
- 伺服器確認實際物件完成組裝後,查證 object key、bytes 與 checksum,不能只信任客戶端回報。
- DB 狀態與 outbox 一起提交同一交易持久化「原檔已確認」與處理事件,避免重複建立工作。
校驗演算法須明確約定。multipart ETag 不能直接當成檔案 checksum。
S3 的 multipart 概觀 說明建立、上傳 parts、完成三個階段;完成時需要提供各 part 的識別資訊。ETag 不能一律當成整份原檔的 MD5,完整性驗證要依實際 checksum 機制設計。這裡也不應把 S3 的 part 協定與另一家服務的 byte-offset 協定混用,續傳客戶端要知道自己遵守哪一份契約。
平台的完成 API 使用同一個 UploadSession ID 重試。若已完成,回傳同一份 receipt;若儲存端完成、資料庫尚未確認,就進入 reconciliation,查證原物件後補上狀態與 outbox。可以保留一個短期 completing 狀態,但一定要有到期掃描,不能讓中斷的請求永久卡住。
還有一個會藏在正常回應裡的錯誤:CompleteMultipartUpload 可能先送出 HTTP 200,最後在內容中回報錯誤。用官方 SDK 時仍需處理 SDK 解析後的失敗;自寫 HTTP 客戶端不能只檢查 status code,就宣告原檔可用。
上傳許可的範圍要比登入許可更小
客戶端不能自行決定要覆寫哪個正式 object key。上傳工作階段必須綁定帳號與影片,限制預期大小、期限、可用操作與物件位置;完成 API 重新檢查擁有權。後端驗證實際檔案大小與格式,不相信副檔名或 Content-Type。解碼工作者處理的是外部輸入,應隔離執行、限制 CPU/記憶體/磁碟時間,不給它任意讀取其他使用者物件或發布公開狀態的權限。
過期 UploadSession 要清除未完成的 parts。S3 multipart 不會因為使用者關掉分頁就自動完成清理,可用明確 abort 與生命週期規則處理;但正在傳送的 part 與 abort 有競爭,清理程序要依 AbortMultipartUpload 文件 確認殘留。這筆費用不該等帳單出現才發現。
6. 轉碼工作可以重跑,發布只能接受仍有效的產物
假設 worker A 已開始處理 generation 7,心跳中斷後,排程器把工作交給 worker B。A 其實沒死,只是網路暫時不通;幾分鐘後兩個 worker 都回報成功。
把佇列 visibility timeout 拉長,只能改變重送時機。以 SQS standard queue 為例,官方直接要求應用面對至少一次投遞。所以我把「可以重複執行」和「可以採用哪個結果」分開。
每次 attempt 寫到獨立、不可變的產物前綴,例如影片 v42、generation 7、profile p3、attempt b。worker 不覆寫正在服務觀眾的 manifest,也不直接修改播放資產指標。它只提出候選資產,附上工作鍵、attempt token、檔案清單與驗證結果。
video v42/job j8/source generation g7。重試沿用 job_id,產物寫入不同的不可變路徑。
該 job 的 fence 41 已過期。即使舊 worker 恢復,也不能提交。
/v42/g7/a1/…該 job 的新租約與 fence 42。另存新路徑,不覆寫舊產物。
/v42/g7/a2/…- 仍是目前的 source generation
- 未被封鎖,且目前政策允許啟用資產
- manifest 與最低可播資產已驗證
- 該 job 的租約與 fencing token 仍有效,期限以 DB 時間判斷
- 播放資產指標的 bundle revision 仍等於讀取時的 expected revision,才能 CAS 更新
同一交易以 CAS 切換播放資產指標、儲存完成紀錄與寫入 outbox。bundle revision 由 r12 更新為 r13,保留已啟用的較完整資產。這不會改變影片可見性。
同一 job/attempt/candidate 的重複完成回傳已儲存的結果。終態 job 與唯一完成紀錄確保不新增 outbox 事件。 過期 worker 或舊 source generation 不能再次啟用資產。CAS 衝突時重讀目前指標、重組候選 bundle,再重試;不能以最後完成者直接覆蓋。
source generation 保證原檔仍有效;bundle revision 防止跨 job 的較舊候選覆蓋較完整版本。單一 job 的 fence 無法處理跨 job 競爭。 私人/不公開影片也可啟用資產,實際觀看仍須通過權限 API。
想看舊 token 的產物怎麼被擋下?到機制實驗室操作驗印閘門。
先決定哪個 attempt 還有資格完成
相同工作、attempt 與候選資產的完成通知若已被接受,回傳儲存的完成收據即可。收據證明當時接受過這份結果;影片此刻是否仍可播放,須另查目前狀態。唯一完成約束與終止狀態,則避免重送再次產生啟用事件。
第一次接受結果時,完成交易要一起核對 Video generation、目前有效的 job attempt token,以及以資料庫時間判斷的租約期限。claim 新 token 與續約,也都透過帶條件的原子更新。
這是應用的 fencing 規則。B 在租約過期後取得新 token,A 手上的舊 token 就不能再贏得完成狀態。只檢查 generation 不夠:A、B 處理的是同一代原檔,兩者都會通過。generation 擋住過期的來源版本,attempt token 與租約擋住已失去工作資格的程序。
再決定完整的播放組合能否切換
基本畫質完成後,啟用交易一次切換 active playback asset ID。私密與未公開連結影片也可以有可播放資產;是否對大眾公開,由 visibility 與 policy 另外決定。只有要把 visibility 改成 public,才要求創作者的公開意圖有效。
後補高畫質時,還有另一種競爭:兩個 rendition job 都有效,卻各自拿著較舊的清單加入自己的畫質。若讓最後完成者直接覆寫,前一個工作加入的畫質可能消失。因此由同一個 bundle assembler 組裝新的完整清單版本,切換時以 compare-and-swap(CAS)核對預期的 active playback asset ID/bundle revision。指標已被其他工作更新,就重新組裝。
每個 rendition job 的 token 只管理自己的工作,不能替不同工作之間的組合版本排序。bundle 的版本檢查通過後,才原子切換指標;觀眾取得的每份 manifest 都必須引用完整存在的 segments。主清單一旦可見,所列的檔案就要能讀取。
把結果接收、資產啟用與需要的公開操作放進同一筆交易,可寫成以下概念程序。正式實作仍需補 schema、索引、隔離級別與客戶端影響筆數語義:
BEGIN
鎖定 Video,再依固定順序鎖定 Job
若此 (job_id, attempt_token, candidate_asset_id) 已接受,結束交易後回傳原收據
比對 current_generation、active_attempt_token、lease_expires_at
確認 Job 尚未完成、Video 未下架、政策允許啟用此資產
確認候選 Asset 已通過最低播放組合驗證
比對預期的 active playback asset ID/bundle revision
將候選 Asset 設為接受,將 Job 設為完成,保存唯一完成收據
更新 Video 的播放資產指標與 metadata revision
若需轉成 public,再核對公開意圖,於同一交易更新 visibility
INSERT outbox:video.asset_activated,包含交易最後的 revision、visibility 與 asset_id
COMMIT
鎖定順序一致,可以減少不同流程互相等待的機會。需要做遠端物件檢查時,先在交易外完成驗證並保存不可變的結果,交易只檢查其身分與有效性,不在持鎖期間下載影片。若資產可能被清理程序刪除,清理程序也必須遵守引用與候選狀態規則。MySQL 的 UPDATE 行為 與 InnoDB 鎖定說明 支持這些資料庫原語;跨資料庫與物件儲存的整體協定仍是我的設計選擇。
產物驗證至少包含存在性、播放清單能解析、引用的檔案存在、音訊與影片組合符合支援範圍,以及必要的實際解碼抽查。只確認 ffmpeg process exit 0 不足以證明整條播放路徑。GOP 與 ABR 背景可參考站內 影音上傳與轉碼文章;這裡的容量估算仍以前述案例參數為準。
切換成功後,舊產物還不能立刻刪
未被採用的物件交由延後清理處理,清理前須確認沒有目前播放或回復用的引用。已被替換的 manifest/segments 也不能立即刪除:有效的播放 session 可能仍在使用舊版本。這些檔案須保留到允許的 session/許可期限與清理緩衝結束,或依明確的強制撤銷政策處理。新觀眾拿到新版本,不代表舊觀眾已經播完。
什麼值得重試,什麼應該停止
暫時無法讀取原檔、工作者被回收,可以用有上限的指數退避與抖動重試。來源損壞、格式不支援、超過產品規格,反覆重跑只會占滿佇列。系統應保存失敗原因分類、嘗試次數與最後進度,讓創作者看到可採取的動作。死信佇列是待處理清單,必須有負責人與回復程序,不能當成「問題已處理」的終點。
7. 按下播放之後,授權與影片位元組走不同的路
阿晴的朋友打開連結,頁面先取得影片 metadata。按播放後,應用檢查這個觀眾是否能看目前版本,回傳播放 session、manifest 位置與短期存取資格。播放器隨後向 CDN 取得清單和 segments,依網路與緩衝狀態選擇可用畫質。
HLS RFC 8216 定義清單與媒體分段的協定形式,包括 Master Playlist 指向不同 Variant Streams。協定提供描述與選取媒體的工具;這裡的「哪些畫質先公開」是產品政策,不能從 HLS 規格直接推出。
檢查身分、觀看資格、影片可見性與下架狀態。不能確認權限,就不發憑證。
manifest 和每個 segment 都必須受保護;可使用簽名 URL 或 cookie。憑證到期須重新授權。
驗證簽名、範圍與到期時間。快取命中也不能略過;無效憑證直接拒絕。
命中就從快取取資料,未命中才回源。origin 只接受受信任 CDN,影片 bytes 不繞經權限 API。
撤銷的邊界停止新授權後,既有短效憑證仍須失效或到期。已下載的 bytes 無法收回。
想看資格檢查和影片位元組怎麼分開走?到機制實驗室操作售票亭與輸送管。
對私密影片,只保護 manifest 不夠。如果 segments 的 URL 可以被任何人持續讀取,知道路徑的人就可能繞過播放 API。因此我們要求 CDN 在媒體請求到達時驗證資格,包含快取命中的情況;origin 同時限制只能由預期的 CDN 路徑存取。
以 CloudFront 為例,signed cookies 適合授權一組受限檔案,signed URL/cookie 選擇指引 舉的情境就包含 HLS。signed cookies 文件說明,CloudFront 驗證簽章與政策後,才決定從 edge cache 或 origin 回應。這可以避免每個 segment 都回到應用查資料庫,但需要設計 cookie domain、路徑、跨來源請求與播放器相容性。
另一個方案是 signed URL,適合明確的單一資源許可;如果播放清單包含大量獨立網址,生成、傳遞與更新許可會更複雜。也可使用邊緣驗證的 bearer token,但 token 的驗證與快取鍵要分開設計。每位觀眾一個隨機 cache key,可能把共享媒體快取打散;完全忽略 token 驗證,又會洩漏內容。
短期資格到期,不等於立即收回每個位元組
假設播放資格有效五分鐘,播放器在到期前向應用更新;這是示例政策,並非固定推薦值。太短會增加授權依賴與播放中斷風險,太長會拉長撤銷後舊資格仍有效的時間。CloudFront 在每次 HTTP 請求時檢查期限;已開始的傳輸與之後發出的請求要分開理解,不能宣稱時鐘一到就撤回已傳出的片段。
如果產品要求下架後十秒內阻止所有新片段請求,五分鐘的離線可驗證資格就不符合要求。你需要更短的期限、邊緣可查的撤銷狀態,或其他能達到要求的方案,並量測傳播延遲與故障行為。因此,選定許可期限前,要先確認產品容許多長的撤銷延遲。
8. 爆紅影片的成本,先看快取未命中的位元組
第二天,阿晴的影片出現在熱門社群。應用 API 的 QPS 增加,但每個觀眾持續讀取的影片片段才是大量頻寬來源。只看 API CPU 很平穩,可能忽略 origin 已經接近傳輸或請求上限。
沿用前面的尖峰 10.4 Gbps 假設,如果媒體位元組快取命中率為 95%,簡化後的回源約是 0.52 Gbps;命中率降到 70%,回源約為 3.12 Gbps,相差六倍。這裡算的是位元組比例,不是 request hit ratio,尚未包含不同 edge 重複抓取、Range 行為與清單請求等因素。
教學假設:觀眾端輸出 10.4 Gbps。以下命中率皆按位元組加權。
01 邊緣快取 · Edge
假設命中 95%,命中部分就近提供。
未命中的 5%:10.4 × 5% = 0.52 Gbps,向 Shield 取回 ↓
02 來源屏障 · Shield
集中承接多個邊緣節點的回源並快取內容,減少對來源物件的重複讀取。此圖不另假設 Shield 命中率。
Shield 未命中,才讀取來源 ↓
03 來源物件儲存
保存可播放的轉碼產物,承接兩層皆未命中的讀取。最終流量取決於 Shield 命中率。
教學假設:命中率降低的代價
相同 10.4 Gbps 輸出:
95% 命中 → 0.52 Gbps 回源
70% 命中 → 3.12 Gbps 回源
增加 6 倍。比較的是進入 Shield 的邊緣未命中流量,不是物件儲存的最終流量。
簡化估算忽略重試、預熱、協定開銷與流量放大。快取命中仍須符合有效的播放授權。
想看快取命中率掉一點,origin 要多扛多少?到機制實驗室操作水塔與抽水站。
版本化、不變的 segment 路徑有利於長期共享快取。相對地,帶狀態意義的 metadata、播放許可、或會變動的清單,應有不同的 TTL 與快取策略。把所有東西設成一年快取,會讓後來的權限修改與內容替換難以說清楚;把所有東西設成不快取,則把成本推回 origin。
多個 edge 同時遇到冷門轉熱門的影片,可以再加一層共享回源快取。CloudFront Origin Shield 的官方定位,是提供額外的快取層來減少 origin 負載。它不是替代授權的安全層,也不保證每個部署與工作負載都值得增加費用;應比較回源請求、位元組、額外延遲與單位成本。
預熱也要有邊界。只對已確認即將大量觀看的首段與基本畫質做有限預取,可能有幫助;把所有影片、所有畫質預熱到所有區域,常常先製造了成本。快取填充期間可以合併相同物件的並發回源,並限制 origin 的最大同時請求。若超出負載,優先保住已存在的基本畫質,暫緩昂貴的後補格式與非必要背景掃描。
預算要按行為歸因
帳單至少分開觀察原檔保留、播放產物、轉碼 CPU 或服務分鐘、CDN 下行、回源與請求數。播放時間提高可能同時增加產品價值與成本;轉碼重試飆高則可能純粹是故障。用每一有效觀看分鐘與每一成功發布影片的成本,比單看總費用更容易判斷下一個改造是否有意義。
換用更省位元率的編碼不一定立刻省錢。需要比較裝置支援、額外編碼成本、預期被觀看次數與保留期。對幾乎沒人觀看的長尾影片,一開始就產生所有高成本格式,可能無法回收那筆處理費用。
9. 搜尋與觀看數可以晚到,播放許可不能靠它們決定
影片已經公開,搜尋卻還找不到;觀看了一百次,頁面只顯示八十次。這些現象不必然是同一類故障。產品需要明確區分「使用者能不能看」與「讀取模型更新得多快」。
Video 與發布記錄是可見性的權威來源;搜尋、推薦清單與觀看數是衍生資料。影片發布事件帶上 event ID、video ID、metadata revision 與事件類型,分別送到索引、推薦與統計 consumer。consumer 保存處理進度,以每支影片的 metadata revision 拒絕較舊事件覆蓋較新狀態,並支援重建。這個版本在公開、私密、下架等修改都遞增,與只描述來源檔的 generation 分開。
1. 權威資料與 outbox 同時提交
MySQL 保存影片狀態與可見性。每次可見性或發布狀態變更,都遞增 Video.metadata_revision;transactional outbox 在同一筆交易記下事件,避免漏發。
提交後 relay 投遞;失敗可重試 ↓
2. 可重播、至少一次的事件流
中繼資料事件以 event_id 去重,冪等更新;事件攜帶 metadata_revision,讀模型拒絕舊修訂覆蓋新狀態。source_generation 僅判斷來源/轉碼產物有效性,不用於排序權限變更。
三種讀模型各自追趕 ↓
搜尋索引
新發布影片可能稍後才找得到;metadata_revision 檢查防止資料倒退。
推薦讀模型
候選與排序可以降級;真正播放仍須檢查授權。
觀看計數
觀看事件另行匯入並獨立去重;計數可延遲,不作授權依據。
讀模型過期或故障,不能繞過權限。播放仍依權威狀態與有效授權。
影片即使仍殘留在搜尋結果,點擊播放也要通過目前的授權。這項設計可以縮小索引延遲的影響,但還不夠:私密影片的標題與縮圖也可能是敏感資訊。搜尋與推薦端需要刪除/隱藏事件、延遲告警與查詢側的可見性保護,不能只說「反正影片播不了」就接受資訊曝光。
初期可以先將只允許公開的 metadata 放進搜尋索引,私密內容走個人資料庫查詢。未公開連結是一種產品可見性選項,不等於登入授權;若任何取得網址的人都能看,就應明白告知創作者,不能把它包裝成機密資料的保護措施。
觀看數需要先定義什麼算一次有效觀看。頁面打開、開始播放、觀看超過某段時間,是不同事件;重試、重複上報與機器流量也會影響統計。我的最小方案保存去識別化的播放事件與去重鍵,採批次聚合顯示近似進度;用來結算廣告或支付分潤的數字,則需要另一份更嚴格的對帳契約,不能直接沿用頁面上的快取數字。
推薦服務暫時不可用時,可退回最新公開影片;觀看數服務暫停,不應讓現有影片全部無法播放。曝光與統計的故障就能留在各自範圍內。事件管線恢復後,要能從 durable log 或權威資料重建,並檢查刪除狀態沒有被舊事件重新放回去。
10. 下架要阻止舊工作復活,也要說明快取收斂的期限
阿晴按刪除的同時,一個先前失聯的 worker 回來了。它帶著完整的高畫質產物,如果完成 API 只檢查檔案存在,就可能把影片重新設為公開。這個競爭情境比「成功刪掉一列」更值得測試。
下架交易先修改 Video 的可見狀態、清除可發布資格並遞增 generation,再寫出下架事件。之後的播放許可一律拒絕,舊 worker 的完成交易也因版本或狀態條件不符而失敗。這一刻是控制面接受下架的時間,必須有可稽核的 receipt。
- T0 · 權威交易blocked = true;generation 41 → 42
同一交易推進 metadata_revision 並記錄下架事件。新的播放授權依權威狀態拒絕;授權快取必須有明確陳舊上限。
- 立即 · 防止復活舊 worker 的 CAS 提交失敗
工作仍拿 generation 41,但目前版本已變成 42。不得啟用舊資產。
- 憑證窗口已發出的授權等待到期或被撤銷
更快撤銷需要邊緣 denylist 或 epoch 檢查;只清快取不能取代授權。
- 逐步收斂移除曝光 → 清快取 → 儲存清理
搜尋與推薦用版本化 tombstone 擋住舊事件。各層以可重試任務與完成指標確認。
已下載內容無法收回;purge 已送出,也不等於所有邊緣已完成清除。
接下來還有不同速度的收斂工作:搜尋與推薦移除曝光;CDN 撤銷或失效既有存取路徑;物件儲存依保留政策刪除來源與產物;備份與稽核資料依另外的期限處理。UI 可以先顯示「已停止公開,正在清理檔案」,但不能把資料庫變更稱為全球資料已刪除。
CloudFront 失效文件 說明如何讓分發中的檔案在到期前失效。失效請求需要追蹤完成進度,也無法移除已下載到觀眾裝置的資料。若 origin 還會回傳舊物件,單純清 cache 甚至可能讓下一次請求重新抓回內容。因此要一起處理目前授權、origin 存取、舊資格期限與快取清除。
刪除流程應該是可重試的工作清單
每個清理步驟保存目標與結果,重試刪除已不存在的物件應視為達成目標。清理完成前,影片 tombstone 不宜立即消失;否則重送的「發布成功」事件或舊索引快照可能失去比較版本的依據。tombstone 保留多久,要覆蓋可重播事件、離線工作與回復程序的範圍,不能隨便填一個 TTL 就假設安全。
如果法規、申訴或營運需要保留資料,必須將「停止公開」與「實體刪除」分開描述,並由產品與相關專業人員確認政策。保存期限確認後,工程端要提供可查詢的狀態、可稽核的操作與不會偷偷復活的重試行為。
11. 驗收要故意打斷轉碼、回源與授權
阿晴遇到的斷線、爆紅與刪除,已經各有處理路徑。接下來要故意打斷它們,確認逾時和重送時仍會得到預期結果。以下三個演練分別檢查轉碼能否接手、回源能否承受負載,以及授權故障時會放行哪些請求。
01 工作佇列停滯
上傳完成,卻等不到處理或發布結果。
- 受影響
- 新影片的處理/發布延後。
- 仍可繼續
- 既有已發布影片的播放,前提是播放路徑沒有同步依賴這條佇列。
02 中繼資料/授權失效
無法確認可見性或播放權限。
- 必要限制
- 拒絕新授權;不可預設影片公開。
- 既有授權
- 依期限、撤銷機制與既定政策使用。無法滿足驗證條件,也不能繼續放行。
03 Shield/Origin 失效
回源層不可用;影響依快取所在層與命中情況而異。
- 快取命中
- 仍須驗證有效授權,才可能繼續送出。
- 未命中
- 若須讀取失效層,則無法取得內容。快取命中不能讓舊權限無限延長。
畫成不同服務,不代表已完成隔離;仍須檢查同步依賴、授權期限與降級策略。
演練 A:基本畫質工作者失聯
觸發方式是在讀取原檔後終止 worker,或讓它完成產物但無法提交結果。觀察 queue age、lease 到期、重試次數與從接收完成到可播放的延遲。預期結果是新的 attempt 能接手,舊 attempt 回來不能覆蓋;原本已公開的其他影片繼續播放。
停止規則可設為示例:基本畫質 queue age 超過產品承諾的一半,暫停高成本的後補格式;若持續上升,限制新上傳的處理准入並告知等待。實際閾值需要測試決定。恢復時檢查孤兒物件與完成事件,不要一口氣重放所有死信,把系統再次壓垮。
演練 B:熱門影片回源增加
在可控測試環境清除測試物件快取、改變熱門集中度,觀察位元組命中率、origin 並發、回源延遲與第一幀。不能為了測試直接清正式站所有快取。預期降級是限制背景工作、保住基本畫質與必要的播放清單,並確認授權檢查沒有因為追求命中率被跳過。
如果提升 Origin Shield 或合併回源後,第一幀沒有改善,可能瓶頸在播放器、權限服務或網路最後一哩。改造要以同一組負載與觀測分母比較,不能只拿一張 CDN 命中率圖宣稱整個觀看體驗變好。
演練 C:播放授權依賴暫時不可用
讓 metadata/授權查詢回傳逾時,觀察新的播放 session 與既有 session。我選擇對不能確認權限的新請求拒絕發出許可;現有短期許可能持續到什麼時點,依前述政策執行。這會犧牲一部分可用性,但可以避免服務故障時把私密內容全部開放。
測試至少包含公開、私密、已下架與剛改權限的影片。對公開內容是否允許使用有明確期限的舊 metadata,是另外一個可接受性決策;它不能直接套用到私密或撤銷路徑。把這些情境寫成測試案例,才知道系統在降級時究竟保住了什麼。
12. 下一次升級架構,要對著量測到的限制動手
當 metadata 的寫入爭用開始影響發布與授權,才評估拆分寫入責任或讀取副本;採用副本前,要確認剛改權限的讀取不能因複寫延遲被放行。當跨區觀眾的第一幀主要卡在授權往返,才評估區域化的許可服務與撤銷資料分發。當轉碼等待超標,先量測輸入特性、profile 成本與工作排程,再決定擴機器或換編碼。
這些改變都會增加需要恢復與驗證的狀態。多區寫入尤其不能只把資料庫換成全球產品就算完成;同一支影片的下架、替換與發布,仍需要清楚的排序與衝突規則。對初期產品,單一寫入區域加跨區媒體分發,通常更容易建立可驗收的基線。是否值得改變,要看已量測的延遲、可用性與營運能力。
回到阿晴的影片,第一版需要做到的事已經很明確:斷線後能續傳,收到原檔後由平台負責處理,基本畫質備妥才提供播放。高畫質可以晚到,卻不能覆蓋較新的播放組合;創作者刪除後,舊工作也不能把它重新公開。
我會先用模組化單體、單一寫入區域與獨立轉碼工作者完成這條路。CDN 承接媒體傳輸,發布交易與短期許可負責決定誰能看哪個版本。等量測指出是哪一段失守,再增加服務或區域;否則每一次拆分,都可能讓原本可在一筆交易裡核對的條件更難維護。
練習:把接手、刪除與兩次完成畫成四筆交易
影片 v42 的 generation 是 7。worker A 正在處理基本畫質,租約過期後 worker B 接手。創作者這時刪除影片,接著 A、B 依序回報成功。請寫出:刪除交易修改哪些欄位?兩次完成請求各自要查哪些條件?哪些物件留下待清理?播放 API 與搜尋結果各應呈現什麼?
再加一個限制:觀眾在刪除前一秒已取得五分鐘的播放許可。產品經理要求十秒內不能再取得任何新片段。原方案哪一份承諾不成立?你會縮短許可、加入邊緣撤銷,還是與產品重新協商期限?寫出代價與故障時的行為。
參考思路
刪除交易推進 generation、記錄 tombstone/撤銷狀態、取消公開資格並寫出 outbox。A 的 attempt token 已過期;B 雖持有目前工作 token,generation 也已不符,所以兩者都不能推進公開狀態。產物可以存在,但不再有有效發布引用,交由可重試清理。新播放許可拒絕,搜尋移除事件追蹤到完成;索引尚未收斂時也不能成為授權來源。
五分鐘許可若僅離線驗證簽章與期限,不能單靠下架資料庫狀態保證十秒撤銷。加入撤銷查詢會增加邊緣狀態同步與故障成本,縮短期限會增加許可更新量。兩種方案都要測出撤銷傳播時間與故障時的行為,才能承諾十秒的期限。
下一步就用兩個 worker stub 和一個資料庫,固定重現「租約過期、刪除、舊結果回來」的順序。檢查兩次完成請求都被拒絕、沒有新的資產啟用事件,而且播放 API 不再核發許可。這個測試通過後,再接入轉碼工具;先把遲到的結果擋住,才有可靠的發布流程可以擴充。
