Thiết kế service upload file trong .NET (chunked, resumable)
Cách xây service upload file trong .NET: presigned URL, upload chunked resumable lên Azure Blob hay S3, pipeline scan virus, và metadata trong Postgres.
Mục lục
Service upload file là một trong những bài trông đơn giản đến khi
phải scale. Phiên bản đầu - endpoint multipart/form-data stream
xuống đĩa local - sập ngay khi có nhiều replica, file lớn, hay
virus cần scan. Chương này thiết kế hình production: presigned
URL, upload chunked resumable, và pipeline scan bất đồng bộ.
Khi nào service upload file thành nghiêm túc?
Ba tín hiệu.
File lớn hơn 10 MB. Stream qua service dùng băng thông và thread bạn không xử lý nổi dưới tải.
Client mobile hoặc không tin cậy. Upload 50 MB qua Wi-Fi khách sạn sẽ fail; client phải resume thay vì restart.
File từ user không tin cậy. Bất cứ đâu user upload, scan virus là bắt buộc; download phải check status scan trước khi phục vụ.
Nếu không cái nào (avatar nhỏ từ employee đã xác thực), stream thẳng vào blob ổn.
Số nào nên ngân sách?
Upload / ngày 500K
Avg size file 5 MB
Storage / năm 5 MB * 500K * 365 = ~900 TB
Đỉnh upload / giây 500K / 100K * 5 = 25/giây
Egress CDN (download) thường 5x băng thông upload
Băng thông vào (đỉnh) 25 * 5 MB = 125 MB/giây
Số 900 TB / năm cho biết object storage là câu trả lời thực dụng duy nhất; volume đó trong database là thảm hoạ. Đỉnh 25/giây nhỏ cho tier app - ký URL là việc nhẹ.
Kiến trúc trông thế nào?
flowchart LR
Client --> App[ASP.NET Core API]
App -->|1. POST /uploads<br/>trả presigned URL| Client
Client -->|2. PUT chunk| Blob[(Azure Blob / S3)]
Client -->|3. POST /uploads/{id}/complete| App
App --> PG[(Postgres<br/>metadata)]
App --> Q[(queue scan)]
Q --> Scanner[Worker antivirus]
Scanner --> Blob
Scanner --> PG
Reader[Client download] --> App
App -->|signed CDN URL| CDN[(CDN)] --> Reader
Ba đường. Init: client xin URL upload từ API, nhận presigned blob URL kèm upload ID. Upload: client PUT chunk thẳng vào blob. Complete: client báo API xong; API enqueue scan. Download là URL signed CDN ngắn.
Cấu hình .NET 10 cho luồng upload?
public record InitUploadRequest(string FileName, string ContentType, long Size);
app.MapPost("/uploads", async (InitUploadRequest req, AppDbContext db,
BlobContainerClient blobs, ClaimsPrincipal user) =>
{
var upload = new UploadSession
{
Id = Guid.NewGuid(),
UserId = user.GetUserId(),
FileName = req.FileName,
ContentType = req.ContentType,
Size = req.Size,
BlobName = $"{Guid.NewGuid()}/{req.FileName}",
Status = "pending",
CreatedAt = DateTimeOffset.UtcNow
};
db.Uploads.Add(upload);
await db.SaveChangesAsync();
var blobClient = blobs.GetBlobClient(upload.BlobName);
var sas = blobClient.GenerateSasUri(
BlobSasPermissions.Write | BlobSasPermissions.Create,
DateTimeOffset.UtcNow.AddHours(1));
return Results.Ok(new { uploadId = upload.Id, uploadUrl = sas.ToString() });
})
.RequireAuthorization()
.RequireRateLimiting("per-user");
app.MapPost("/uploads/{id:guid}/complete", async (Guid id, AppDbContext db,
IPublishEndpoint bus) =>
{
var upload = await db.Uploads.FindAsync(id);
if (upload is null) return Results.NotFound();
upload.Status = "uploaded";
upload.UploadedAt = DateTimeOffset.UtcNow;
await db.SaveChangesAsync();
await bus.Publish(new ScanRequested(upload.Id, upload.BlobName));
return Results.Ok();
});
// Worker scan
public class ScanConsumer(BlobContainerClient blobs, IAntiVirus av, AppDbContext db)
: IConsumer<ScanRequested>
{
public async Task Consume(ConsumeContext<ScanRequested> ctx)
{
await using var stream = await blobs.GetBlobClient(ctx.Message.BlobName).OpenReadAsync();
var verdict = await av.ScanAsync(stream, ctx.CancellationToken);
var upload = await db.Uploads.FindAsync(ctx.Message.UploadId);
upload!.Status = verdict.IsClean ? "clean" : "quarantined";
upload.ScanResult = verdict.Detail;
await db.SaveChangesAsync();
}
}
// Endpoint download
app.MapGet("/files/{id:guid}", async (Guid id, AppDbContext db, BlobContainerClient blobs) =>
{
var upload = await db.Uploads.FindAsync(id);
if (upload is null || upload.Status != "clean") return Results.NotFound();
var sas = blobs.GetBlobClient(upload.BlobName).GenerateSasUri(
BlobSasPermissions.Read, DateTimeOffset.UtcNow.AddMinutes(15));
return Results.Redirect(sas.ToString()); // 302 to CDN
});
Ba chi tiết. Presigned URL có expiry chặt (1 giờ upload, 15 phút download) nên URL bị rò chết nhanh. Cổng status scan ngăn phục vụ file nhiễm. Endpoint download redirect thay vì stream - CDN phục vụ byte.
Đường scale-out hỗ trợ?
- Tier API: stateless, scale ngang với replica.
- Object storage: S3 / Azure Blob scale vô hạn; chi phí là storage + egress.
- Worker scanner: song song được; partition theo hash file để trùng dedupe.
- CDN: lo mọi traffic download; API không bao giờ phục vụ byte.
- DB metadata: partition
uploadstheo tháng sau một năm.
Cổ chai băng thông biến mất với thiết kế này - mọi byte đi giữa client và blob storage / CDN, không bao giờ qua service bạn.
Tạo failure mode nào?
- Blob mồ côi - session upload tạo, client không complete.
Phòng: job đêm xoá blob cho session cũ hơn 24 giờ với
status=pending. - Backlog scanner - đỉnh upload viral làm đầy queue scan. Phòng: alert độ sâu queue; pool scanner scale theo metric observability.
- Virus upload rồi phục vụ - race giữa complete và scan xong.
Phòng: cổng status download; không bao giờ phục vụ trừ khi
status=clean. - CDN cache poisoning - URL signed cache CDN dùng lại sau expire. Phòng: TTL URL signed ngắn cộng cache key CDN gồm chữ ký.
Khi nào service tuỳ là quá liều?
Cho avatar nhỏ và file đính kèm trong SaaS, dịch vụ host như Uploadcare, Filestack, hay Cloudinary xử lý cả luồng kèm scan và CDN. Họ rẻ hơn xây, vận hành, và bảo mật pipeline riêng. Xây tuỳ khi volume, tuân thủ, hay độ sâu tích hợp biện minh được.
Đi tiếp đâu từ đây?
Case study kế tiếp: typeahead autocomplete - case study nặng cấu trúc dữ liệu, nơi sorted set Redis và trie có giá trị.
Câu hỏi thường gặp
Sao presigned URL thay vì stream qua service?
Upload chunked resumable chạy ra sao?
Scan virus đặt ở đâu?
status=pending, event vào queue scan, worker kéo file từ blob, chạy ClamAV hay Defender, update metadata thành status=clean hay quarantined. Endpoint download từ chối phục vụ file ngoài state clean.