Video Sharing Platform: 影片上傳技術細節
前面有討論了 Video Sharing Platform 大概的 Data FlowIn 設計,接著繼續深入「影片上傳」這個部分的細節和實作。並不只是簡單的把檔案藉由後端 server 寫進儲存系統而已,還牽涉到如:
- 安全性相關 : User 驗證、Data 驗證等等
- 錯誤處裡: 上傳網路中斷、資料格式不符、error handling
- 資料轉換: 對應需求,可能對資料做前處理或後處理
- 系統能力: Latency 或者是否影響 server 穩定
應把它視為一條長流程、非同步、且容易受到網路品質影響的工作,而不是單次同步請求,這樣才能面對突發狀況。
在前面的部分得到關於「影片上傳」的 high-level design 如下 :
flowchart LR
client([Client]) --> gateway([LB / API gateway])
gateway --> upload([Upload service])
upload --> metadata_storage[SQL Database:
metadata]
client --> tmp_blob_storage[Tmp Blob Storage: raw video]
tmp_blob_storage --> queue(message queue)
subgraph F[Worker Pool]
W1((Worker
process raw video))
W2((Worker))
W3((Worker))
end
queue --> F
W1 --> blob_storage[Blob Storage:
processed video]
W1 --> metadata_storage
接下來針對流程細節再做進一步補充:
Upload Flow 上傳影片流程細節
如何處理檔案上傳,現在已經有一些公認的準則了 :
- 最好不要將整個大檔案,直接載入後端伺服器的 memory 中
- 使用有效期短、作用範圍限定 pre-signed URL 直接把檔案上傳到 Blob storage
- 為了支援不穩定的連接,上傳操作採用「分段上傳」和「斷點續傳」
Client 從 Upload Service 取得 Tmp Blob Storage 的 pre-signed URL 後將影片上傳
這裏規劃一個 Tmp Blob Storage 去存原始 video,例如說使用 AWS S3 創建一個臨時 bucket 將原始影片上傳到這裡儲存,且為了確保安全性 :
- 使用 presigned URL 來獲得臨時 object storage 存取權限
- presigned URL 另外還可以設定過期時間,當有效期超過之後,用 url 存取文件會提示 403。
範例如下:
s3_client.generate_presigned_post(
Bucket=bucket_name,
Key=key,
Conditions=[
["content-length-range", 0, limit_size_bytes],
["Expires", 3600] # 是給 HTTP Expires header,給瀏覽器/CDN 的 cache 到期時間
],
ExpiresIn=expire_after_sec, # 代表 pre-signed URL 上授權可以用多久,url 多久後失效
)
pre-signed URL 由後端 Upload Service 來頒發,其設定的參數值 generate_presigned_post 函數會做簽名以防篡改。
此外通常 Upload Service 前面會加一個 auth/ratelimit service 來對做 client 詳細認證,例如:
- 短時間調用太多次就暫時不給 url
- 要是平台認證會員身份才能拿到 url
flowchart LR
client -->|presigned URL| tmp_blob_Storage[S3: raw video]
client([client]) --> gateway([LB / API gateway])
gateway --> auth([auth/ratelimit service])
auth --> upload([upload service
`generate_presigned_post`])
upload --> |write video init| metadata_storage[SQL Database:
metadata]
upload --> |return presigned URL| client
以上做法可以讓 upload 安全性增加不少。
另外頒發 pre-signed URL 之後也要在 metadata storage 內寫入相關訊息,代表影片的 init 初始化,主要原因是:pre-signed URL 的頒發本身只代表「後端允許某個 client 直接上傳到 object storage」,不代表系統內已經正式建立一筆可追蹤的 upload,先建立 upload 的 init 狀態,後續整個流程才有一致的可參考點,後續處理才有依據,也就是 :
- 狀態管理:可以明確區分 init、done、failed,而不是只看 bucket 裡有沒有檔案
- 安全控制:若 metadata 中沒有對應初始化紀錄,即使 bucket 出現檔案,也可以視為非預期物件,不進行後續流程
- 過期清理:若 URL 已頒發但 client 沒有真的上傳完成,可依 init 紀錄做清理
關於 video metadata 應該用 Relational Database 儲存 ; 還是用 NoSQL 去存呢?
- 使用 Relational Database,就有機會使用 join 的形式去進行跨表的查詢來處理一些資料上的關聯 ; 缺點在於需要自己去維護 data sharding 和 hostspot
- 用 NoSQL 的好處是 data partition 通常是天然支援的,但缺點就是不支援事務性的操作
對於 video metadata 目前看下來以上兩個方案都是可行的,通常實際決定的重點是「取決於團隊偏好」,也就是說團隊已經在用的就跟著去用,不需要額外增加技術棧。
影片完整上傳到 Tmp Blob Storage 之後,Storage 發出 Task Event,然後 Worker 接收到 Event 後會對影片進行處理
這邊提到「影片完整上傳」到 Tmp Blob Storage,那所謂的 完整上傳 是什麼意思呢?因為考慮到「上傳影片 Latency」 問題,故「上傳的方式」會有不同手法,例如 :
Client 把 video 切成小份 chunk 上傳到 Tmp Blob Storage
例如說 client 把 video 分成每
2M一個 chunk ,那一個約20M的 video 就有10 個 chunk需要上傳,這樣的好處是:- 可以做到並行上傳(paralllel upload),上傳的速度可以達到蠻好的優化
- 例如說用戶是使用手機端 App 上傳,這時通常是在 wifi 這種不穩定的網路,所以隨時可能中斷。切分做法下,如果其中幾個 chunk 失敗了,可以只針對該部分進行重傳。
但是缺點是 :
client 要去熟悉影片 chunk 切割相關 lib 操作 ; 最後傳完之後可能會需要有 finish signal 機制,提示影片已經全部上傳完成
這種做法無法達到藉由分析 video 本身的特性,客製化切分段。例如一分鐘影片,前面 30s 動作幅度不大,那可能可以把前面這部分都壓縮成一小塊,然後後面是多動作和特效片段,可以切得更細更多來處理。
考慮到 client 端的硬體各式各樣,故實務上不太可能是由 client 端來分析影片的 syntax 特性,然後將 video 按照 syntax 來動態 segment 切分
由於後續可能有要把 chunk 再次合併回一個完整的 video 需求(分割上傳之後在 worker 將多個 segment 合併成原來的完整的 video (記得要做完整性的驗證)),這時要考慮一下之前的分割做法有沒有必要。
關於切分多個 chunk 操作,有看到 S3 提供了 CreateMultipartUpload 方法, 可以分段上傳且最後 S3 會直接幫忙合併,但如何和 presigned URL 一起使用需要研究一下。
直接上傳 video 到 tmp storage
現在的影片平台,蠻多都有自適應位元速率串流 (Adaptive Bitrate Streaming)的功能,舉例來說:支援自適應位元速率串流的「HLS 通訊協定」,其本身就有把影片分割成小的片段,然後使用
m3u8對 segment 管理。所以前面的 「client 把 video 切成小份 chunk 上傳」的方式,更好的是切分方法是直接基於通訊協定來做,但這樣 client 實作的難度就更高了。如果是時間很長且容量大的影片,分割上傳還是需要的。雖然目前不確定當影片多長多大的時候,用這種「先分割再合併」的方式才會有比較好的效能,可能需要蠻多測試和調查的
另外這些分割小片段,其實通常還要有多個不同的解析度如 480p、720p、1080p 可選,要產生多個解析度影片這件事情,同樣也不太可能是由 client 端負責,應該由後端來做 ; 再加上現在大部分的影音轉檔服務(可以參考 AWS MediaConvert 服務來做影音轉檔)直觀提供的,都是「給一個完整影片,然後可產出多個指定解析度的分割」,故 client 不錯任何事情直接上傳 video 其實也是一種做法。
關於 S3 上傳,還可以進一步啟用 Amazon S3 Transfer Acceleration,加速 client 網際網路傳輸速度,但定價方面會多一些 Cost。
client = boto3.client(
"s3",
region_name=region_name,
aws_access_key_id=access_key_id,
aws_secret_access_key=secret_access_key,
config=Config(
retries={
"max_attempts": 3,
"mode": "standard",
},
s3={'use_accelerate_endpoint': True}, # 開啟 Transfer Acceleration
),
)
另外還有特別提到,如果檢查開啟 Transfer Acceleration 服務比一般傳輸慢的話,就不會收取費用,而且會繞過 Transfer Acceleration 系統。
之前有發現 Transfer Acceleration 似乎蠻不穩定的,使用 Amazon S3 Transfer Acceleration 速度比較工具來檢查效能,發現沒辦法穩定保持穩定快速…,我想這就是 AWS 特別說「如果沒有比較快就不收費」的原因吧
無論是哪個上傳方式,最後當影片完整成功儲存到 S3 之後,都會有 event 事件發出通知,從高層設計上,「由 storage 發出 Task Event 給 message queue 中間件,而 worker pool 內的 worker 進行異步處理 queue 內 Task」,前面整個敘述架構為非同步系統。
這在實作上有非常多的選擇,這裏就不自己實作了,決定使用現成雲端服務 AWS Elemental MediaConvert,參考 Tutorial: Batch-transcoding videos with S3 Batch Operations 這個 AWS 官方教學簡介,並再把其架構再進一步簡化,拿掉 S3 batch 組件,則架構和流程就是:
flowchart LR
src[(S3 Source Bucket
raw video)] -->|Event trigger| lambda[Lambda: convert-trigger]
subgraph mc[AWS Elemental MediaConvert]
W1((Job1
processed job1))
W2((Job2
processing job2))
...
end
lambda --> |create mediaconvert job| mc
W1 -->|HLS、MP4、Thumbnail| dst[(S3 destination Bucket
processed video)]
high-level design 的非同步系統的 message queue 和 worker pool,整個實作用 AWS Lambda 和 AWS Elemental MediaConvert 實現
所以流程再下一步就是:
S3 Source Bucket 發出 Task Event 後,藉由 AWS Elemental MediaConvert 進行影片轉檔,再把結果儲存到 S3 Destination Bucket
這樣的實作下,詳細流程會是:
S3 Source Bucket發出 Event 後, 由Lambda: convert-trigger接收Lambda職責是創建MediaConvert job給AWS Elemental MediaConvert排程執行AWS Elemental MediaConvert根據 job 內容排程執行,轉出設定檔案- 最後其結果儲存到
S3 Destination Bucket
如果不想使用 AWS Elemental MediaConvert,就會需要自己研究 FFmpeg,然後架設集群去轉檔,這方面也可以做很多研究。
Netflix 在 2015 年從固定比特率編碼(content-aware encoding)轉向按內容編碼(per-title or per-shot encoding),在不損失明顯畫質的情況下,頻寬節省高達 20%,缺點是轉碼過程中需要耗費更多時間
影片處理完存入 S3 Destination Bucket 之後,把完成的消息通知出去
從高層設計上知道影片轉檔是非同步觸發的 Encoding pipeline,所以如果沒有實作「完成轉檔的消息通知」,client 端一定不會知道什麼時候影片轉檔完成,而在上前述架構中,確定知道是否轉檔成功或失敗的組件,就是 AWS Elemental MediaConvert 了。
那 MediaConvert 組件要怎麼傳送完成或失敗的消息通知給 Client 呢?
可以參考這篇文章 Using EventBridge with AWS Elemental MediaConvert,故還需要新增一些雲端組件 :
EventBridgerule 中定義了 MediaConvert 的事件- 創建關於 MediaConvert 的事件
SNS topic - 新增
Lambda並訂閱SNS topic - 轉檔完成會觸發
Lambda
以上架構如下:
flowchart LR
subgraph mc[AWS Elemental MediaConvert]
W1((Job1
processed job1))
W2((Job2))
end
subgraph EventBridge[EventBridge]
rule1{Rule1
MediaConvert Job State}
rule2{Rule2}
end
W1 --> rule1
subgraph sns[Amazon SNS]
t1{topic1
MediaConvertJobAlert}
t2{topic2}
end
rule1 --> t1
lambda2[Lambda: Notification] --> |subscribe| t1
t1 --> lambda2
lambda2 -->|Send notification|r((
))
影片轉檔完成之後,由於有在 EventBridge rule 中定義了 MediaConvert 的事件,當 MediaConvert job 有變動時會送出該事件到指定 SNS topic ,然後 Lambda: Notification 因為有訂閱該 SNS topic,所以觸發了啟動。
為了讓 MediaConvert 轉檔成功的消息能被發出來,這邊又多建立了 EventBridge、SNS topic、Lambda 。有時候使用雲端功能時,實作複雜性也會上升呢
Lambda: Notification 要後續的任,是要讓 client 能感知到影片已經處理完了這件事情,在這裡 lambda 可以簡單一點,只做 dispatcher,把任務傳給 Receive System 來做。Receive System 要做什麼呢? 主要任務至少要做:
- 轉檔狀態寫入 metadata storage
- 通知 client 端轉檔任務完成
先寫 DB,再發通知,不要反過來。client 收到通知後,最好再 call 一次 status API 確認最終狀態
轉檔狀態寫入 metadata storage
回想一下之前在一開始上傳影片的時候,有提到「頒發 pre-signed URL 後要在 metadata storage 內寫入相關訊息,代表影片的 init 初始化」,而這裡就需要在 metadata storage 內寫入影片最後轉檔成功/失敗的訊息。
那 metadata table 要更新什麼呢?
基本上 table 內應該會儲存 video 的狀態。比如說 :
- client 一開始去調用 upload service,影片狀態設置為
Init - 當影片上傳完整後會發個 signal,這時狀態設置為
Ready且創建 Task 放到 queue 裡面 - Worker 會來領取這個任務,當開始處理時候影片狀態設置為
Processing處理當中 - 處理完後 Worker 最後將 MetaData 改成
Complete
進一步可以考慮出錯的情形,例如 Processing 如果失敗了可觸發 Retry,然後把狀態設置回 Ready 且重新創建 Task 放到 queue 裡面等著下一次執行 ; 或者多次失敗後要變成 Failed,以下是狀態圖:
flowchart LR
Start --> Ready --> Processing --> Complete
Processing -.-> Retry[retry > 3 ?]
Retry -->|yes| Failed
Retry -->|no| Ready
通知 client 端轉檔任務完成
最後的通知方式也是一個系統設計,簡單可以分成:
- Polling: client 定時打 GET /videos/{video_id}/status,雖然最簡單但缺點是不即時,也會多一些 request
- WebSocket: client 上傳影片時,要訂閱 video_id
- SSE: 標準「伺服器單向通知 client」場景
通常 Client 端可能會有比較多情形會導致連線停掉,所以基本上一定要有 Polling status API 做最後保底其他時候可以靠 WebSocket 和 SSE 來提升體驗。
架構總結
經過上面很多的討論,最後架構:
flowchart TD
client([Client])
gateway([LB / API gateway])
auth([Auth/Ratelimit service])
upload([Upload Service
`generate_presigned_post`])
metadata_storage[SQL Database:
metadata]
src[(S3 Source Bucket
raw video)]
lambda_convert[Lambda: convert-trigger]
subgraph mc[AWS Elemental MediaConvert]
W1((Job1
processed job1))
end
subgraph EventBridge[EventBridge]
rule1{Rule1
MediaConvert Job State}
end
subgraph sns[Amazon SNS]
t1{topic1
MediaConvertJobAlert}
end
lambda_notification[Lambda: Notification]
receive[Receive Service]
dst[(S3 destination Bucket
processed video)]
client --> gateway
gateway --> auth
auth --> upload
upload --> |return presigned URL| client
upload --> |write video init| metadata_storage
client -->|presigned URL| src
src -->|Event trigger| lambda_convert
lambda_convert --> |create mediaconvert job| mc
W1 --> rule1
W1 --> dst
rule1 --> t1
lambda_notification --> |subscribe| t1
t1 --> lambda_notification
lambda_notification --> receive
receive --> |write video complete| metadata_storage
receive --> |notify video processing finished| client
