前言
各位平時使用的短影片應用,微信 & 微博等圖文社群,它們的圖文動態 & 影片上傳的能力,都是極其核心的業務。
本質來說,這都是檔案的上傳,這篇文章帶大家寫一個檔案上傳服務,探究其核心原理,相信能為你帶來一些幫助。
感謝我的好友 Trembling 對本文的支援
主要包含以下能力:
- 檔案上傳
- 檔案下載
- 分片上傳
- 斷點續傳
- 檔案秒傳
往期影片講解 📺:B站:白澤talk,公眾號:白澤talk
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;
}
在各個請求中,domain
和biz_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。
主要鏈路
普通上傳/下載
分片上傳/斷點續傳
主要能力
普通上傳
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
的具體使用,將在下文中詳細介紹。
分片上傳
在一些情況下,需要上傳的檔案較大,如果直接上傳,可能出現如下問題:
- 上傳較慢
- 上傳過程中如果出現問題,則需要重新上傳整個檔案
所以,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.PreSignPut
和FileService.PreSignSlicingPut
介面中,如果傳入的hash已經存在,則會返回一個file_id
,這個file_id
可以用於獲取下載連結,從而不需要再次上傳檔案。