前面有討論了 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 jobAWS 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,故還需要新增一些雲端組件 :

  • EventBridge rule 中定義了 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

參考資料