Harbor

Xử lý file lớn

Cách upload, lưu và phân phối file lớn mà không để chúng đi qua app server: presigned URL, multipart và resumable upload, chia chunk và dedup theo hash, CDN, signed URL khi tải về, pipeline hậu xử lý bằng sự kiện, tách metadata khỏi blob store.

15 phút đọcPattern thường gặp

Bài toán: file lớn không giống request bình thường

Một request API thông thường nặng vài KB và xong trong vài chục mili giây. Một video 2 GB tải lên qua mạng di động 10 Mbps mất gần 30 phút (phép tính: 2.000 MB × 8 / 10 Mbps ≈ 1.600 giây). Một file thiết kế 500 MB, một bản backup 50 GB, một album 200 ảnh RAW cũng vậy. Khác biệt về kích thước và thời gian lên tới hàng nghìn lần, nên các giả định của kiến trúc API thông thường không còn đúng.

Các câu hỏi bài toán file lớn phải trả lời:

  • Upload đi qua đâu, và nếu mạng rớt ở 90% thì có phải tải lại từ đầu không?
  • File lưu ở đâu, thông tin về file (tên, chủ sở hữu, quyền, trạng thái) lưu ở đâu?
  • Người dùng ở xa tải về bằng cách nào cho nhanh, và làm sao chặn người không có quyền?
  • Sau khi upload, ai tạo thumbnail, transcode video, quét virus?

Grokking mô tả blob store là kho cho dữ liệu phi cấu trúc (ảnh, audio, video, tài liệu), tổ chức phẳng không có thư mục, thường dùng cho tải write-once-read-many.1 Bài này dùng ví dụ một ứng dụng chia sẻ ảnh và video, áp dụng được cho Dropbox, YouTube và Instagram.

Cách ngây thơ: đẩy file qua app server

Cách đơn giản nhất: client POST /upload với multipart/form-data, Node.js server nhận stream, ghi vào S3 hoặc đĩa cục bộ, lưu một dòng vào database, trả về URL.

Đang vẽ sơ đồ…

Cách này gãy theo nhiều hướng cùng lúc:

  1. Băng thông bị đếm hai lần. Mỗi byte vào API server rồi lại ra object storage. Cách tính băng thông của Grokking ch.20 cũng dựa trên tổng dữ liệu mỗi ngày chia cho 86.400 giây.2 Còn băng thông mạng của một instance 2 vCPU vài Gbps là giả định của khoá học, cần đo; 100 người cùng upload video có thể bão hòa vài instance.3
  2. Kết nối bị giữ quá lâu. Một request 30 phút giữ một socket, một phần bộ nhớ đệm, và thường vượt timeout của load balancer hoặc API gateway (nhiều gateway đặt timeout và giới hạn payload, tuỳ cấu hình).3
  3. Rớt mạng là làm lại từ đầu. Không có cách nào nối tiếp một request HTTP bị cắt giữa chừng.
  4. Deploy bị kẹt. Muốn rolling deploy, bạn phải đợi các upload đang chạy xong hoặc cắt ngang chúng.
  5. Scale sai thứ. Bạn thêm instance API vì băng thông, trong khi CPU gần như rảnh.

Nguyên tắc rút ra: dữ liệu file đi thẳng giữa client và object storage. App server chỉ điều phối. Hướng này khớp với thiết kế trong Grokking, nơi manager node chỉ giữ metadata và ánh xạ chunk, còn client đọc/ghi chunk trực tiếp trên data node.4

Giải pháp 1: presigned URL, client upload thẳng lên storage

Object storage như S3, GCS, R2 thường cho phép tạo một URL có chữ ký, cho phép đúng một thao tác (PUT một key cụ thể) trong thời gian giới hạn.5 App server kiểm tra quyền, tạo URL, trả cho client. Client PUT file thẳng lên storage.

Đang vẽ sơ đồ…
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const key = `uploads/${userId}/${fileId}`;
const url = await getSignedUrl(
  s3,
  new PutObjectCommand({ Bucket: "media", Key: key, ContentType: mime }),
  { expiresIn: 900 } // 15 phút
);

Những điểm cần nói rõ:

  • Key do server quyết định. Client không được tự chọn đường dẫn, nếu không họ có thể ghi đè file người khác.
  • Giới hạn kích thước và loại file. Dùng presigned POST với điều kiện content-length-range, hoặc kiểm tra lại sau khi upload xong và xóa nếu sai.
  • Xác nhận hoàn tất từ phía storage. Đừng tin client báo "tôi upload xong rồi". Dùng sự kiện của storage (S3 Event Notification) hoặc gọi HEAD để kiểm tra object tồn tại và đúng kích thước.
  • Dọn rác. Bản ghi pending quá 24 giờ thì xóa, kèm lifecycle rule xóa object mồ côi.

Đánh đổi: một PUT đơn vẫn không nối tiếp được khi rớt mạng, và storage có giới hạn kích thước cho một PUT. Với file vài chục MB, presigned PUT đơn là đủ (nhận định của khoá học).5

Giải pháp 2: multipart và resumable upload

Với file lớn (khoảng vài trăm MB trở lên, ngưỡng của khoá học), hãy chia file thành nhiều phần và upload từng phần; cùng ý tưởng chia blob thành chunk như trong Grokking ch.20.6

S3 multipart upload hoạt động như sau: tạo phiên upload và nhận uploadId, upload từng part bằng presigned URL riêng (mỗi part có kích thước tối thiểu/tối đa và số part tối đa do nhà cung cấp quy định), cuối cùng gọi CompleteMultipartUpload với danh sách ETag của các part. Storage ghép các part thành một object.5

Lợi ích:

  • Nối tiếp được. Rớt mạng ở part 87 thì chỉ upload lại part 87. Client lưu uploadId và danh sách part đã xong (trong IndexedDB hoặc hỏi lại server bằng ListParts).
  • Song song. Client upload vài part cùng lúc (ví dụ 4 tới 6), tận dụng băng thông tốt hơn một kết nối đơn.
  • Retry rẻ. Part 8 MB lỗi thì gửi lại 8 MB.

Nếu không dùng S3, giao thức tus là chuẩn mở cho resumable upload: client hỏi server "đã nhận tới offset nào" rồi gửi tiếp từ đó.

Lưu ý: Multipart upload bị bỏ dở vẫn tính tiền lưu trữ cho các part đã tải lên. Luôn đặt lifecycle rule hủy multipart chưa hoàn tất sau vài ngày.

Chunking và dedup theo hash

Dropbox và các dịch vụ đồng bộ đi xa hơn: chia file thành chunk cố định (ví dụ 4 MB), tính SHA-256 cho mỗi chunk, và lưu chunk theo hash của nó. Một file trở thành danh sách hash. Phần chia chunk cố định giống Grokking ch.20 (blob chia thành chunk cùng kích thước);7 phần băm nội dung để dedup là kỹ thuật của khoá học.8

{ "fileId": "f_91", "version": 7, "size": 52428800,
  "chunks": ["a3f1...", "9bc0...", "77de...", "..."] }

Lợi ích lớn:

  • Dedup. Trước khi upload, client gửi danh sách hash. Server trả về những hash chưa có. 1.000 người upload cùng một file cài đặt thì chỉ lưu một lần.
  • Đồng bộ chênh lệch. Sửa một đoạn trong file 1 GB chỉ làm đổi vài chunk. Client chỉ upload những chunk đó.
  • Version rẻ. Version mới là danh sách hash mới, phần lớn trỏ tới chunk cũ.

Grokking (ch.20 Blob Store) mô tả cấu trúc tương tự ở tầng storage: blob được chia chunk, mỗi chunk có bản sao (sách dùng 3 replica cho mỗi chunk) trên nhiều data node, và một manager node giữ bảng ánh xạ blob sang chunk sang data node.7 Kích thước chunk là một tham số đánh đổi: chunk quá nhỏ thì metadata phình to, chunk quá lớn thì đĩa khó ghi liền khối nên đọc/ghi chậm hơn.7 Việc dedup tốt hơn và retry rẻ hơn khi chunk nhỏ là suy luận của khoá học.

Đánh đổi của dedup theo hash: phải xử lý quyền cẩn thận (biết hash không có nghĩa là được đọc chunk), và xóa chunk cần đếm tham chiếu hoặc garbage collection định kỳ. Về xoá, Grokking cũng không xoá ngay mà đánh dấu DELETED trong metadata rồi garbage collector dọn sau.9

Giải pháp 3: tách metadata khỏi blob, phân phối qua CDN

Metadata và blob ở hai nơi khác nhau

Blob (nội dung file) nằm trong object storage, truy cập theo key. Metadata (tên, chủ sở hữu, kích thước, mime, trạng thái, danh sách chunk, quyền chia sẻ, thư mục cha) nằm trong database. Grokking tách tương tự: metadata storage là một distributed database riêng chứa metadata tài khoản, container và blob (blob nằm ở đâu), còn dữ liệu chunk nằm trên data node.10

Metadata DBBlob store
Ví dụPostgreSQL, DynamoDBS3, GCS, R2
Kích thước mỗi bản ghiVài trăm byte tới vài KBKB tới TB
Truy vấnTheo user, thư mục, tìm kiếmGET/PUT theo key
Chi phí (nhận định của khoá học)Đắt theo GB, rẻ theo truy vấnRẻ theo GB, tính thêm phí request và egress (xem bảng giá nhà cung cấp)

Đừng bao giờ lưu file lớn trong cột bytea hay BLOB của database quan hệ. Backup, replication và cache của database sẽ phải gánh toàn bộ dung lượng đó.11

Tải về qua CDN và signed URL

Grokking (ch.11) giải thích vai trò của CDN: đặt bản sao nội dung ở các proxy server gần người dùng để giảm độ trễ và giảm tải cho origin.12 Với file tĩnh lớn, CDN gần như bắt buộc. Khoảng cách địa lý làm tăng độ trễ: sách ghi round trip US East tới Cape Town khoảng 225 ms, còn US East tới US West khoảng 63 ms.12 Từ edge gần người dùng, độ trễ chỉ còn một phần nhỏ (ước tính của khoá học).

Với nội dung riêng tư, dùng signed URL (hoặc signed cookie) của CDN: server kiểm tra quyền, ký một URL có hạn dùng ngắn, client tải thẳng từ CDN, CDN kiểm tra chữ ký mà không cần gọi về app server.5 Phần caching phía blob store: Grokking nói việc cache blob thường làm bằng CDN, với TTL do origin đặt qua header Cache-Control.13

Đang vẽ sơ đồ…

Mẹo: Đặt key của object bất biến (ví dụ có hash hoặc version trong key) rồi cache trên CDN thật lâu. Khi file đổi, key đổi, không cần invalidate cache. Building Microservices kể một sự cố Expires: Never kẹt trong cache trình duyệt mà cách duy nhất để thoát là đổi URL, minh hoạ vì sao đổi URL là cách chắc chắn nhất.14

Video là trường hợp đặc biệt: Grokking ch.11 mô tả DASH dùng manifest chứa URI của video ở nhiều độ phân giải để client chọn theo điều kiện mạng,15 và ch.20 mô tả đọc file theo từng đoạn byte (stream bằng offset).16 Việc cắt segment vài giây (HLS) để mỗi segment thành một object nhỏ cho CDN là hiểu biết chung của khoá học.3

Giải pháp 4: pipeline hậu xử lý bằng sự kiện

Sau khi upload xong, file thường cần xử lý thêm: tạo thumbnail, nén ảnh nhiều kích thước, transcode video sang nhiều độ phân giải, quét virus, trích xuất văn bản để tìm kiếm. Những việc này chậm (vài giây tới hàng giờ) và không nên chạy trong request upload.

Đang vẽ sơ đồ…

Hướng làm việc nặng bất đồng bộ qua hàng đợi này khớp với lợi ích của messaging queue trong Grokking ch.17 (producer không phải chờ tác vụ chậm, consumer xử lý và tăng giảm số worker theo tải).17

Cách làm:

  1. Object storage phát sự kiện khi object được tạo, đẩy vào SQS, Kafka hoặc một topic pub/sub.
  2. Các worker chuyên biệt đọc sự kiện. Ảnh nhỏ có thể xử lý bằng serverless function. Transcode video cần máy nhiều CPU hoặc GPU, chạy trong worker pool riêng.
  3. Mỗi bước cập nhật trạng thái trong metadata DB: uploaded, scanning, processing, ready, failed. Client poll hoặc nhận thông báo qua WebSocket để biết khi nào file sẵn sàng.
  4. Worker phải idempotent: nhận cùng sự kiện hai lần thì ghi đè cùng output, không tạo bản sao. Microservices Patterns §3.3.6 giải thích message có thể bị giao nhiều lần và consumer phải loại bản trùng hoặc xử lý idempotent.18

Transcode một video dài là tác vụ chạy lâu điển hình: chia video thành đoạn, transcode song song từng đoạn, rồi ghép lại. Pipeline nhiều bước có thể được điều phối bằng workflow engine như Temporal, xem thêm Quy trình nhiều bước.

Scale theo quy mô và tính băng thông, dung lượng

Giả định của khoá học cho ứng dụng chia sẻ ảnh (cách tính dung lượng mỗi ngày và băng thông bằng tổng dữ liệu chia cho 86.400 giây theo Grokking ch.20;2 số đầu vào là giả định, xem Chọn server19): 10% user hoạt động upload mỗi ngày, trung bình 2 ảnh, mỗi ảnh gốc 3 MB, sinh thêm 3 bản thu nhỏ tổng 0,5 MB. Tỷ lệ đọc so với ghi là 20:1 tính theo byte tải về, phần lớn là bản thu nhỏ khoảng 200 KB. Mọi con số là ước tính.

Quy môUpload mỗi ngàyDung lượng mới/ngàyDung lượng/nămBăng thông upload TBBăng thông tải về đỉnh
~10k user2.000 ảnh~7 GB~2,5 TB~0,1 MB/s~10 MB/s
~1M user200.000 ảnh~700 GB~250 TB~8 MB/s~1 GB/s
50M+ user10M ảnh~35 TB~12 PB~400 MB/shàng chục GB/s

Cách tính dòng 1M: 1M × 10% × 2 = 200.000 ảnh. 200.000 × 3,5 MB = 700 GB/ngày. Chia 86.400 giây được khoảng 8 MB/s upload trung bình, đỉnh gấp 5 là 40 MB/s. Lượt xem: giả sử 1M user mỗi người xem 100 ảnh thu nhỏ mỗi ngày, 100M × 200 KB = 20 TB/ngày ≈ 230 MB/s trung bình, đỉnh khoảng 1 GB/s. Băng thông này phải đi qua CDN.

Kiến trúc theo từng tầng:

  • ~10k user: Presigned PUT thẳng lên S3 hoặc R2, một Postgres cho metadata, thumbnail tạo bằng một serverless function khi có sự kiện. CDN vẫn nên bật vì gần như không tốn công.
  • ~1M user: Multipart upload cho file trên 100 MB, CDN với signed URL cho nội dung riêng tư, hàng đợi và worker pool cho hậu xử lý. Lifecycle rule đẩy ảnh gốc ít truy cập sang lớp lưu trữ rẻ hơn sau 30 ngày. Chi phí egress bắt đầu đáng kể: 20 TB/ngày tải về là khoảng 600 TB/tháng, cần đàm phán giá CDN hoặc chọn storage có chính sách egress phù hợp (nhận định của khoá học).
  • 50M+ user: Upload endpoint đặt ở nhiều region, client upload lên region gần nhất rồi sao chép bất đồng bộ giữa data center và region, đúng như Grokking mô tả (đồng bộ trong cluster, bất đồng bộ giữa data center).20 Dedup theo hash để giảm dung lượng. Metadata DB phải shard theo owner_id (xem Scale ghi). Transcode chạy trên cụm hàng trăm worker, tự scale theo độ dài hàng đợi. Dữ liệu 12 PB một năm khiến lớp lưu trữ lạnh và erasure coding trở thành bài toán chi phí chính.21 Với replication, Grokking giữ 4 bản của mỗi blob (một trong data center chính, một ở data center khác cùng region, một ở region khác).20

Trong phỏng vấn: (lời khuyên của khoá học) Câu "client upload thẳng lên S3 bằng presigned URL, app server không chạm vào byte nào của file" là câu đầu tiên người phỏng vấn muốn nghe. Sau đó mới tới multipart, CDN, và pipeline xử lý.

Bài thiết kế nào dùng pattern này

Các bài dưới đây áp dụng blob store, chunk, CDN và xử lý bất đồng bộ.22

BàiTrọng tâm
DropboxChunking, dedup theo hash, đồng bộ chênh lệch, metadata DB cho cây thư mục
YouTubeMultipart upload, transcode nhiều độ phân giải, HLS qua CDN
InstagramPresigned upload, tạo nhiều kích thước ảnh, CDN cho feed
ChatGửi ảnh và file đính kèm qua presigned URL, tin nhắn chỉ chứa tham chiếu
ChatGPTUpload tài liệu, trích xuất văn bản bằng pipeline bất đồng bộ
Web crawlerLưu nội dung trang vào blob store, metadata và trạng thái crawl ở database

Bài liên quan: Caching, Mạng cơ bản, Thiết kế API, Tác vụ chạy lâu.

Nguồn trích dẫn

  1. Grokking Modern System Design · ch.20 Blob Store · bài mở đầu · mục "What is a blob store?". ↩

  2. Grokking Modern System Design · ch.20 Blob Store · bài "Requirements of a Blob Store's Design" · mục "Resource estimation" (Storage estimation, Bandwidth estimation). ↩ ↩2

  3. Chưa có nguồn trong thư viện: băng thông instance, timeout/giới hạn gateway và cách cắt segment HLS là kiến thức vận hành chung, thư viện không có sách về chúng. ↩ ↩2 ↩3

  4. Grokking Modern System Design · ch.20 Blob Store · bài "Design of a Blob Store" · mục "Components" (manager node, data node) và "Workflow" (Write a blob, Reading a blob). ↩

  5. Chưa có nguồn trong thư viện: presigned URL, multipart upload của S3 và signed URL của CDN là tính năng sản phẩm; thư viện không có sách về chúng, chỉ nêu khái niệm cơ bản và không đưa số liệu cụ thể. ↩ ↩2 ↩3 ↩4

  6. Grokking Modern System Design · ch.20 Blob Store · mục "Blob metadata" (blob được chia thành các chunk). ↩

  7. Grokking Modern System Design · ch.20 Blob Store · mục "Blob metadata" (chunk ID, data node, 3 replica, cân nhắc kích thước chunk). ↩ ↩2 ↩3

  8. Chưa có nguồn trong thư viện: dedup theo hash nội dung và đồng bộ chênh lệch kiểu Dropbox không có trong sách của thư viện; chỉ nêu ở mức khái niệm. ↩

  9. Grokking Modern System Design · ch.20 Blob Store · mục "Garbage collection while deleting a blob". ↩

  10. Grokking Modern System Design · ch.20 Blob Store · bài "Design of a Blob Store" · mục "Components" (Metadata storage). ↩

  11. Chưa có nguồn trong thư viện: khuyến nghị không lưu file lớn trong cột BLOB là kinh nghiệm thực hành của khoá học. ↩

  12. Grokking Modern System Design · ch.11 Content Delivery Network (CDN) · mục "Problem statement" và "Proposed solution". ↩ ↩2

  13. Grokking Modern System Design · ch.20 Blob Store · mục "Cache the blob store". ↩

  14. Building Microservices (file .zip, tải về giải nén) · ch.11 Microservices at Scale · mục "Cache Poisoning: A Cautionary Tale" (tr. 230). ↩

  15. Grokking Modern System Design · ch.11 CDN · mục "Dynamic content caching optimization" (ghi chú về DASH). ↩

  16. Grokking Modern System Design · ch.20 Blob Store · mục "Stream a file". ↩

  17. Grokking Modern System Design · ch.17 Distributed Messaging Queue · bài mở đầu · phần lợi ích của messaging queue. ↩

  18. Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.3.6 Handling duplicate messages. ↩

  19. Quy tắc ước lượng của khoá học · /hoc/chon-server. ↩

  20. Grokking Modern System Design · ch.20 Blob Store · mục "Replication" (đồng bộ trong cluster, bất đồng bộ giữa data center và region, bốn bản sao). ↩ ↩2

  21. Chưa có nguồn trong thư viện: lớp lưu trữ lạnh và erasure coding không được mô tả trong sách của thư viện. ↩

  22. Grokking Modern System Design · ch.20 Blob Store; ch.11 CDN; ch.17 Distributed Messaging Queue. ↩