短影片上傳怎麼做|寫個支援分片上傳/斷點續傳/秒傳功能的檔案服務吧

白泽talk發表於2024-08-17

前言

各位平時使用的短影片應用,微信 & 微博等圖文社群,它們的圖文動態 & 影片上傳的能力,都是極其核心的業務。

本質來說,這都是檔案的上傳,這篇文章帶大家寫一個檔案上傳服務,探究其核心原理,相信能為你帶來一些幫助。

感謝我的好友 Trembling 對本文的支援

主要包含以下能力:

  • 檔案上傳
  • 檔案下載
  • 分片上傳
  • 斷點續傳
  • 檔案秒傳

往期影片講解 📺:B站:白澤talk,公眾號:白澤talk

image-20240726234405804

FileService 主要能力

pd定義:

service FileService {
  // pre sign a file url for user get it
  rpc PreSignGet(PreSignGetRequest) returns (PreSignGetResponse);
  // pre sign a file url for user put it
  rpc PreSignPut(PreSignPutRequest) returns (PreSignPutResponse);
  // report a file has been uploaded
  rpc ReportUploaded(ReportUploadedRequest) returns (ReportUploadedResponse);
  // pre sign a file url for user put it with slicing
  rpc PreSignSlicingPut(PreSignSlicingPutRequest) returns (PreSignSlicingPutResponse);
  // get upload progress rate for slicing put
  rpc GetProgressRate4SlicingPut(GetProgressRate4SlicingPutRequest) returns (GetProgressRate4SlicingPutResponse);
  // merge a slicing uploading file
  rpc MergeFileParts(MergeFilePartsRequest) returns (MergeFilePartsResponse);
  // remove a file
  rpc RemoveFile(RemoveFileRequest) returns (RemoveFileResponse);
}

FileService用於向各個業務領域提供檔案上傳、下載的能力。在FileService的所有介面中,都存在一個名為file context的結構體,該引數通常用於指定檔案相關的資訊。其結構如下:

message FileContext {
  // 所屬業務領域,用於建立bucket
  string domain = 1;
  // 所屬業務名稱
  string biz_name = 2;
  // 檔案id
  int64 file_id = 3;
  // 檔案sha256 hash
  string hash = 4;
  // 檔案型別
  string file_type = 5;
  // 檔案大小,單位byte
  int64 size = 6;
  // 檔案訪問連結的過期時間
  int64 expire_seconds = 7;
  // 檔名
  string filename = 8;
}

在各個請求中,domainbiz_name兩個引數為必傳項,這個2個引數組合後,將使得FileService所依賴的表以業務領域為維度進行第一次分表。在此基礎上,可以為每個業務領域配置一個對應的二次分表,可以指定其分為若干張子表。

Quick Start

FileService對於不同業務領域和不同業務項,其檔案數量的擴增速度是不同的,帶來的資料量很有可能天差地別。所以,要使用FileService的能力,需要在FileService中進行配置分表數量。

config.yaml中,可以配置如下內容:

data:
  db_sharding_config:
    file_shortvideo_short_video:
      sharding: file_shortvideo_short_video
      sharding_number: 5

db_sharding_config項用於配置分表數量,其下的file_shortvideo_short_video為分表key,其中"file"為固定值,shortvideo為業務領域,short_video為業務名稱。sharding_number為分表數量。業務領域和業務名稱需要在使用FileService時傳入。如不進行配置,則預設分表數量為1。

主要鏈路

普通上傳/下載

sequenceDiagram participant up as 上游服務 participant fs as FileService participant minio as Minio Server up->>fs: PreSignPut fs-->>up: PreSignPutResponse(Minio上傳連結) up->>minio: 上傳檔案 minio-->>up: 上傳結果 up ->> fs: ReportUploaded fs-->>up: ReportUploadedResponse up->>fs: PreSignGet fs-->>up: PreSignGetResponse(下載連結) up->>minio: 下載檔案 minio-->>up: 返回檔案

分片上傳/斷點續傳

sequenceDiagram participant up as 上游服務 participant fs as FileService participant minio as Minio Server up->>fs: PreSignSlicingPut fs-->>up: PreSignSlicingPutResponse(分片上傳連結陣列) up->>minio: 上傳一部分檔案 minio-->>up: 上傳結果 up->>up: 喝杯咖啡 up->>fs: GetProgressRate4SlicingPut fs-->>up: GetProgressRate4SlicingPutResponse(分片上傳進度) up->>minio: 上傳剩餘檔案 minio-->>up: 上傳結果 up->>fs: MergeFileParts fs-->>up: MergeFilePartsResponse up->>fs: PreSignGet fs-->>up: PreSignGetResponse(下載連結) up->>minio: 下載檔案 minio-->>up: 返回檔案

主要能力

普通上傳

FileService.PreSignPut提供了最基礎的上傳能力。該介面需要額外上傳的引數包括:

  • hash: 檔案的sha256值
  • file_type: 檔案型別
  • size:檔案大小(位元組數)
  • expire_seconds:檔案上傳連結的過期時間

例如,可以向此介面傳入這樣的引數:

{
    "file_context": {
        "domain": "shortvideo",
        "biz_name": "short_video",
        "hash": "8D6BB0819A2C1E66F846031DC54AAF47",
        "file_type": "pdf",
        "size": 1181178,
        "expire_seconds": 86400
    }
}

訪問後,該介面將返回一個上傳連結(http),由上游服務/前端直接將檔案上傳到該連結。一個上傳示例程式碼(Python):

with open(file_path, 'rb') as file_data:
    response = requests.put(
        minio_url, # 介面返回的上傳連結
        data=file_data,
        headers={"Content-Type": "application/octet-stream"}
    )
    print(response)
    return response.status_code

透過上述Python程式碼的方式上傳完成後,並不能直接訪問到該檔案,需要透過FileService.ReportUploaded介面來進行上傳確認,只有上傳確認後的檔案,才會在資料庫中被標記為uploaded並能被訪問到。關於FileService.ReportUploaded的具體使用,將在下文中詳細介紹。

分片上傳

在一些情況下,需要上傳的檔案較大,如果直接上傳,可能出現如下問題:

  1. 上傳較慢
  2. 上傳過程中如果出現問題,則需要重新上傳整個檔案

所以,FileService提供了分片上傳的能力。

首先,可以透過FileService.PreSignSlicingPut預註冊一個分片上傳任務。該介面需要傳入的引數與FileService.PreSignPut相同。介面返回的主要內容包括:

  • urls:陣列,各個分片的上傳連結,且已經按照分片號排序
  • upload_id:上傳任務的id
  • parts:分片總數
  • file_id:檔案id

引數示例:

{
    "file_context": {
        "domain": "shortvideo",
        "biz_name": "short_video",
        "hash": "8D6BB0819A2C1E66F846031DC54AAF47",
        "file_type": "pdf",
        "size": 72246060,
        "expire_seconds": 86400
    }
}

此時,由上有服務/前端對檔案進行分片(每一片大小為5MB),然後將各個分片進行上傳,一個檔案分片的示例如下(Python):

def slicing(filename):
    file_size = 5 * 1024 * 1024  # 10MB
    
    files = list()

    # 開啟檔案
    with open(filename, 'rb') as f:
        index = 0
        while True:
            # 定位到要讀取的位置
            f.seek(index * file_size)
            # 讀取資料
            data = f.read(file_size)
            # 如果已經讀到檔案末尾,退出迴圈
            if not data:
                break
            # 寫入分割後的檔案
            with open(f'{filename}_{index}', 'wb') as f1:
                f1.write(data)
            files.append(data)
            # 更新位置
            index += 1
    return files

全部分片上傳完成後,可以透過FileService.MergeFileParts來合併分片。主要引數包括:

  • file_id
  • upload_id

引數示例:

{
    "upload_id": "ZDNlOWI2MjktMjAzOC00NzJkLWE0ODYtOGMzZTBlZmJlODUwLmRmN2M5ZWQyLTYxMzMtNDM4NS1hNTljLWEwMzRlNTI5NWNkNHgxNzIzNzM5ODA2MTM2NzU3MzE5",
    "file_context": {
        "file_id": 1824123073628999680,
        "domain": "shortvideo",
        "biz_name": "short_video"
    }
    
}

與不同上傳不同的是,分片上傳在上傳完成並呼叫FileService.MergeFileParts後,會自動進行上傳確認,無需再次呼叫FileService.ReportUploaded

斷點續傳

在上述分片上傳的過程中,可以透過FileService.GetProgressRate4SlicingPut來獲取分片上傳的具體情況,主要傳入的引數包括:

  • file_id
  • upload_id

該介面的返回值中包含一個名為parts的map,key為分片號,value為該分片是否上傳完成,上游服務或服務端可以根據該資訊來決定哪些分片需要重新上傳

示例引數:

{
    "upload_id": "ZDNlOWI2MjktMjAzOC00NzJkLWE0ODYtOGMzZTBlZmJlODUwLjA3ZTgyNmY0LWE4YjQtNDQxMC04M2QzLWY4ODQ4MTRiZGM4Y3gxNzIzNzM4ODUzMTMzMjY4ODQ3",
    "file_context": {
        "file_id": 1824119076553756672,
        "domain": "shortvideo",
        "biz_name": "short_video"
    }
    
}

返回值示例:

{
    "parts": {
        "1": true,
        "2": true,
        "3": true,
        "4": true,
        "5": true,
        "6": true,
        "7": true,
        "8": true,
        "9": true,
        "10": true,
        "11": true,
        "12": true,
        "13": true,
        "14": true
    },
    "meta": {
        "reason": [],
        "biz_code": 0,
        "message": "success",
        "domain": ""
    },
    "progress_rate": 100
}

上傳確認

在上傳完成時(分片上傳除外,分片上傳結束時呼叫FileService.MergeFileParts會自動進行上傳確認),都需要呼叫FileService.ReportUploaded來進行上報,該介面必傳引數為:

  • file_id

這一介面主要完成這樣一件事:檢查檔案的hash,檢查透過後將檔案標記為“上傳成功狀態”,否則檔案將不可被查詢到。

引數示例:

{
    "file_context": {
        "domain": "shortvideo",
        "biz_name": "short_video",
        "file_id": "1824118603822141440"
    }
}

下載檔案

透過FileService.PreSignGet介面則可以獲取下載檔案連結,該介面主要傳入的引數包括:

  • file_id
  • expire_seconds

引數示例:

{
    "file_context": {
        "domain": "shortvideo",
        "biz_name": "short_video",
        "file_id": "1824123073628999680",
        "expire_seconds": 86400,
        "filename": "data.mp4"
    }
}

秒傳

FileService.PreSignPutFileService.PreSignSlicingPut介面中,如果傳入的hash已經存在,則會返回一個file_id,這個file_id可以用於獲取下載連結,從而不需要再次上傳檔案。

相關文章