Video Sharing Platform: 進階分析上傳影片
前面有討論了 Video Sharing Platform 大概的 Data FlowIn 設計,但是還可以繼續深入討論「影片上傳」這個部分細節和實作,並不只是簡單的把檔案寫進儲存系統就好了而已,還牽涉到安全性把控如 : User 驗證、Data 驗證、error handling、資料轉換、 是否影響 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 來頒發,而設定的參數值也會做簽名以防篡改。
此外通常 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 初始化。
關於 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」,前面整個敘述架構為非同步系統。
這在實作上有非常多的選擇,這裏就簡單參考 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)]
非同步系統的 message queue 和 worker pool,整個實作用 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,然後架設集群去轉檔,這方面也可以做很多研究。
影片處理完存入 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之後:- 回傳狀態給 client
- 寫入狀態到 metadata table 內
以上架構如下:
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 --> receive[Receive Service]
receive --> client
receive --> metadata_storage[SQL Database:
metadata]
影片轉檔完成之後,由於有在 EventBridge rule 中定義了 MediaConvert 的事件,當 MediaConvert job 有變動時會送出該事件到指定 SNS topic ,然後 Lambda: Notification 因為有訂閱該 SNS topic,所以觸發了啟動。
為了讓 MediaConvert 轉檔成功的消息能被發出來,這邊又多建立了 EventBridge、SNS topic、Lambda 。有時候使用雲端功能時,實作複雜性也會上升呢
Lambda: Notification 要做的事情,是要讓 client 能感知到影片已經處理完了這件事情,在這裡 lambda 可擔任一個簡單的 dispatcher,把任務傳給 Receive Service 來做。
Receive Service要做什麼事情呢?
回想一下之前在一開始上傳影片的時候,有提到「頒發 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
架構總結
經過上面很多的討論,最後架構:
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
