Chương 33. File và object storage

Ở các chương trước, ta đã nói về database, cache, và search.

Bây giờ đến một loại dữ liệu rất quen thuộc:

File.

Ví dụ:

  • Ảnh đại diện.
  • Ảnh sản phẩm.
  • File PDF.
  • Video.
  • Bài nộp của học sinh.
  • File log.
  • File export báo cáo.
  • File kết quả chấm bài.
  • File ghi âm.
  • File đính kèm trong tin nhắn.

Nghe có vẻ đơn giản:

User upload file.
Backend lưu file.
User tải file.

Nhưng trong hệ thống thật, file là một nguồn rắc rối lớn.

Vì file thường:

  • Nặng hơn dữ liệu text.
  • Tốn băng thông.
  • Tốn dung lượng.
  • Cần phân quyền.
  • Cần chống virus/malware.
  • Cần xử lý nền.
  • Cần CDN nếu phục vụ nhiều người.
  • Cần lifecycle để dọn rác.
  • Cần backup/retention.

Thông điệp chính của chương:

> File lớn thường không nên nằm trực tiếp trong database. Database nên giữ metadata và quyền truy cập. Nội dung file nên nằm trong object storage.

---

33.1. Ví dụ dễ hiểu: thư viện sách

Hãy tưởng tượng một thư viện.

Thư viện có:

  • Sách thật trên kệ.
  • Phiếu thông tin trong hệ thống.

Phiếu thông tin ghi:

Tên sách
Tác giả
Mã sách
Kệ nào
Ai được mượn
Ngày nhập
Tình trạng

Nhưng phiếu thông tin không chứa nguyên cuốn sách.

Nếu nhét nguyên cuốn sách vào ngăn phiếu, hệ thống phiếu sẽ rất nặng.

Trong phần mềm cũng vậy.

Database giống hệ thống phiếu.

Object storage giống kho/kệ để chứa file thật.

Database lưu:

file_id
owner_id
file_name
content_type
size
storage_key
created_at
visibility
status

Object storage lưu:

bytes thật của file

Ta không nên bắt database ôm hết mọi thứ.

---

33.2. File khác dữ liệu nghiệp vụ bình thường ở đâu?

Dữ liệu nghiệp vụ thường nhỏ và có cấu trúc.

Ví dụ một đơn hàng:

order_id = 123
user_id = 9
status = paid
total = 250000
created_at = ...

Dữ liệu này rất hợp với database.

File thì khác.

Một ảnh sản phẩm có thể là:

2 MB

Một video bài giảng có thể là:

800 MB

Một file zip bài nộp có thể là:

50 MB

Nếu hệ thống có 100.000 file, dung lượng có thể lên hàng trăm GB hoặc nhiều TB.

File cũng thường được đọc theo kiểu:

Tải nguyên cục bytes.

Database lại mạnh ở việc:

  • Query.
  • Transaction.
  • Index.
  • Constraint.
  • Join.
  • Tìm theo điều kiện.

Object storage mạnh ở việc:

  • Lưu lượng lớn.
  • Lưu file rẻ hơn.
  • Phục vụ download/upload.
  • Scale dung lượng.
  • Kết hợp CDN.
  • Lifecycle/retention.

Vì vậy ta nên dùng đúng công cụ.

---

33.3. Sai lầm phổ biến: lưu file lớn vào database

Một người mới có thể nghĩ:

Tôi có database rồi.
Tôi lưu file vào cột BLOB là xong.

Về mặt kỹ thuật, nhiều database cho phép làm vậy.

Nhưng trong hệ thống web thông thường, cách này thường gây vấn đề.

Database sẽ phình rất nhanh.

Backup nặng hơn.

Restore lâu hơn.

Replication chậm hơn.

Query nghiệp vụ có thể bị ảnh hưởng.

Connection database bị dùng để truyền file lớn.

Khi user tải file, backend/database phải gánh băng thông không cần thiết.

Điều nguy hiểm là:

> File không chỉ chiếm dung lượng. File còn kéo theo băng thông, backup, restore, replication, monitoring, quyền truy cập, và chi phí vận hành.

Database nên được giữ cho việc nó giỏi nhất:

Dữ liệu nghiệp vụ có cấu trúc và cần tính đúng.

File bytes nên đưa sang nơi chuyên lưu file.

---

33.4. Có bao giờ lưu file trong database là hợp lý không?

Có.

Không phải lúc nào cũng sai.

Lưu file trong database có thể chấp nhận được khi:

  • File rất nhỏ.
  • Số lượng file ít.
  • File gắn chặt với transaction nghiệp vụ.
  • Hệ thống nội bộ đơn giản.
  • Không cần phục vụ file với traffic lớn.
  • Backup/restore vẫn nằm trong giới hạn chấp nhận được.

Ví dụ:

Một app nội bộ lưu vài icon nhỏ.

Hoặc:

Một chứng từ nhỏ vài KB, số lượng ít, cần transaction cực chặt.

Nhưng với sản phẩm web có user upload nhiều file, ảnh, video, PDF, bài nộp, tài liệu, export:

Object storage thường là lựa chọn tốt hơn.

Quy tắc nhớ nhanh:

> Metadata nằm trong database. File bytes nằm trong object storage.

---

33.5. Object storage là gì?

Object storage là nơi lưu dữ liệu dưới dạng object.

Một object thường gồm:

bucket
key
content
metadata

Ví dụ:

bucket: synvia-prod-files
key: submissions/2026/05/12/sub_123/source.zip
content: bytes của file zip
metadata:
  content_type: application/zip
  size: 8451200

Các dịch vụ phổ biến:

  • Amazon S3.
  • Google Cloud Storage.
  • Azure Blob Storage.
  • Cloudflare R2.
  • DigitalOcean Spaces.
  • MinIO nếu tự host.

Tên khác nhau, ý tưởng giống nhau:

> Một cái kho lớn để lưu object theo key.

---

33.6. Bucket là gì?

Bucket giống một kho chứa.

Ví dụ:

synvia-prod-files
synvia-staging-files
synvia-public-assets
synvia-private-uploads

Trong bucket có nhiều object.

Object được định danh bằng key.

Ví dụ:

avatars/user_9/avatar.webp
courses/course_18/lesson_3/video.mp4
submissions/job_123/source.zip
exports/report_2026_05_12.csv

Không nên hiểu object storage như folder truyền thống.

Nó giống một bảng key-value khổng lồ:

key -> bytes

Dấu / trong key giúp ta tổ chức tên cho dễ nhìn, nhưng về bản chất object vẫn được tìm bằng key.

---

33.7. Database lưu gì?

Database không lưu nguyên file.

Database nên lưu bản ghi đại diện cho file.

Ví dụ bảng uploaded_files:

id
owner_id
storage_bucket
storage_key
original_filename
content_type
size_bytes
checksum
status
visibility
created_at
deleted_at

Ví dụ:

id: file_123
owner_id: user_9
storage_bucket: synvia-prod-files
storage_key: submissions/2026/05/12/file_123.zip
original_filename: bai_nop.zip
content_type: application/zip
size_bytes: 8451200
status: available
visibility: private

Khi user cần xem file, hệ thống tra database để biết:

  • File này tồn tại không.
  • File thuộc về ai.
  • User hiện tại có quyền xem không.
  • File đang ready hay đang scan/xử lý.
  • File nằm ở bucket/key nào.

Sau đó mới tạo link tải hoặc stream file.

Database giữ sự thật về:

File này là gì và ai được làm gì với nó.

Object storage giữ:

Nội dung file thật.

---

33.8. Luồng upload đơn giản nhất

Cách đơn giản:

Browser
-> Backend
-> Object storage

Luồng:

User chọn file
-> gửi file lên backend
-> backend kiểm tra quyền/kích thước/loại file
-> backend upload file sang object storage
-> backend ghi metadata vào database
-> trả kết quả cho user

Cách này dễ hiểu.

Nhưng có nhược điểm:

File đi qua backend.

Nếu file lớn hoặc nhiều user upload cùng lúc, backend phải gánh:

  • Băng thông upload.
  • RAM/buffer.
  • Connection lâu.
  • Timeout.
  • Chi phí truyền dữ liệu.

Với file nhỏ, cách này ổn.

Với file lớn, nên cân nhắc upload trực tiếp lên object storage.

---

33.9. Upload trực tiếp lên object storage

Luồng phổ biến hơn trong hệ thống hiện đại:

Browser
-> xin quyền upload từ Backend
-> upload trực tiếp lên Object storage
-> báo Backend hoàn tất

Chi tiết:

1. User chọn file.
2. Browser gọi backend: "Tôi muốn upload file này."
3. Backend kiểm tra user/quyền/kích thước/loại file.
4. Backend tạo presigned URL.
5. Browser dùng URL đó upload trực tiếp lên object storage.
6. Browser báo backend: "Upload xong."
7. Backend ghi hoặc cập nhật metadata trong database.
8. Worker có thể scan/xử lý file nếu cần.

Sơ đồ:

Browser
   |
   | xin URL upload
   v
Backend ---- tạo quyền tạm thời ----> Object storage
   ^
   |
   | báo upload xong
   |
Browser ---- upload file trực tiếp ---> Object storage

Điểm hay:

  • Backend không phải nhận toàn bộ file.
  • Backend vẫn kiểm soát ai được upload.
  • Object storage gánh upload bytes.
  • Scale tốt hơn.

Điểm cần cẩn thận:

  • Không được cấp URL upload quá rộng.
  • URL phải hết hạn nhanh.
  • Key phải được backend quyết định.
  • Vẫn cần kiểm tra file sau upload.
  • Không tin hoàn toàn vào thông tin do browser gửi.

---

33.10. Presigned URL là gì?

Presigned URL là một URL có chữ ký tạm thời.

Nó cho phép client làm một việc cụ thể với object storage trong thời gian ngắn.

Ví dụ:

Cho upload file vào key X trong 10 phút.

Hoặc:

Cho download file Y trong 5 phút.

Nói dễ hiểu:

> Backend viết một tờ giấy phép tạm thời. Ai cầm giấy đó có thể upload/download đúng file đó trong thời hạn đó.

Presigned URL thường chứa:

  • Bucket/key.
  • Hành động được phép.
  • Thời gian hết hạn.
  • Chữ ký.

Ví dụ tư duy:

PUT /submissions/file_123.zip?signature=...&expires=...

Client không cần biết secret key của storage.

Backend giữ secret.

Client chỉ cầm URL tạm.

---

33.11. Presigned upload nên giới hạn gì?

Không nên tạo một URL kiểu:

User muốn upload gì cũng được.

Nên giới hạn:

  • Key cụ thể.
  • Method cụ thể: PUT/POST.
  • Thời gian ngắn.
  • Content type nếu có thể.
  • Kích thước tối đa nếu storage hỗ trợ policy.
  • User/session tạo URL.

Ví dụ tốt:

User 9 được upload một file zip tối đa 20 MB
vào key submissions/user_9/file_123.zip
trong 10 phút.

Ví dụ xấu:

User được upload bất cứ file nào vào bucket production.

Presigned URL giúp scale upload.

Nhưng nếu cấp quyền cẩu thả, nó cũng mở cửa cho rác và lỗ hổng.

---

33.12. Download file: backend stream hay presigned URL?

Có hai cách phổ biến.

Cách 1:

Browser -> Backend -> Object storage -> Backend -> Browser

Backend stream file về cho user.

Cách 2:

Browser -> Backend xin link
Backend kiểm tra quyền
Backend trả presigned download URL
Browser tải trực tiếp từ Object storage/CDN

Cách 1 dễ kiểm soát.

Nhưng backend phải gánh download bytes.

Cách 2 scale tốt hơn.

Backend chỉ làm việc quan trọng:

Kiểm tra quyền.

Sau đó object storage/CDN phục vụ file.

Trong nhiều hệ thống, cách 2 là hợp lý hơn.

Nhưng với file cực nhạy cảm, hoặc cần audit chặt từng byte tải xuống, có thể stream qua backend hoặc dùng signed URL/CDN signed cookie với kiểm soát kỹ hơn.

---

33.13. Public file và private file

Không phải file nào cũng giống nhau.

Public file:

  • Logo.
  • Ảnh sản phẩm công khai.
  • CSS/JS bundle.
  • Ảnh bài viết public.

Private file:

  • Bài nộp của học sinh.
  • Hoá đơn.
  • File cá nhân.
  • Feedback riêng.
  • Tài liệu nội bộ.
  • File chứa dữ liệu người dùng.

Public file có thể được phục vụ qua CDN công khai.

Private file cần kiểm tra quyền.

Sai lầm rất nghiêm trọng:

Để cả bucket private thành public.

Hoặc:

Đoán được URL là tải được file.

Với file private, URL không nên là bảo mật duy nhất.

Phải có:

  • Bucket private.
  • Backend kiểm tra quyền.
  • Link tạm thời.
  • Key khó đoán.
  • Log/audit khi cần.

---

33.14. CDN là gì trong câu chuyện file?

CDN là mạng máy chủ phân phối nội dung ở nhiều nơi.

Khi user tải file, CDN có thể phục vụ từ điểm gần user hơn thay vì kéo về origin mỗi lần.

Ví dụ:

User ở Việt Nam tải ảnh sản phẩm.
Nếu CDN có cache gần Việt Nam, ảnh tải nhanh hơn.

CDN đặc biệt hữu ích cho:

  • Ảnh public.
  • Video public.
  • File tĩnh.
  • CSS/JS.
  • File download được nhiều người tải.

Luồng:

Browser -> CDN -> Object storage

Nếu CDN đã có file trong cache:

Browser -> CDN

Nhanh hơn và giảm tải origin.

---

33.15. CDN không phải database

CDN là cache cho nội dung.

Nó không phải nơi giữ sự thật.

Source of truth vẫn là:

Object storage + metadata trong database

CDN chỉ giữ bản sao gần user để tải nhanh.

Vì vậy sẽ có chuyện:

File đã đổi ở origin nhưng CDN vẫn giữ bản cũ một lúc.

Đây là vấn đề cache invalidation.

Với file public, cách thường dùng là:

Đổi tên file khi nội dung đổi.

Ví dụ:

avatar_user_9_v1.webp
avatar_user_9_v2.webp

Hoặc dùng hash:

app.8f3a91.js
image.a17c2.webp

Nếu key đổi, CDN coi đó là file mới.

Cách này đơn giản hơn việc cố xoá cache cũ khắp nơi.

---

33.16. Vì sao không nên dùng tên file user gửi làm storage key?

User upload:

bai nop cuoi ky.zip

Không nên dùng thẳng:

bai nop cuoi ky.zip

làm key chính.

Vì có thể gặp:

  • Trùng tên.
  • Ký tự lạ.
  • Dấu tiếng Việt.
  • Khoảng trắng.
  • Tên quá dài.
  • Path traversal nếu xử lý sai.
  • Lộ thông tin riêng tư trong URL.

Nên để backend tạo key.

Ví dụ:

submissions/2026/05/12/file_01HX...zip

Tên gốc vẫn có thể lưu trong database:

original_filename = "bai nop cuoi ky.zip"

Key storage nên là tên kỹ thuật ổn định.

Tên hiển thị cho user là metadata.

---

33.17. Content type và phần mở rộng file

User có thể upload file tên:

avatar.jpg

Nhưng nội dung thật có thể không phải ảnh JPEG.

Vì vậy không nên chỉ tin vào extension.

Nên kiểm tra:

  • Extension.
  • Content-Type header.
  • Magic bytes nếu quan trọng.
  • Kích thước.
  • Bộ giải mã ảnh/video nếu cần.
  • Virus/malware scan nếu file có rủi ro.

Ví dụ:

.jpg

chỉ là tên.

Nó không chứng minh file an toàn.

Trong hệ thống upload file từ user, luôn nhớ:

> File upload là dữ liệu không đáng tin cho tới khi được kiểm tra.

---

33.18. Upload lớn: đừng để request chết giữa đường

File lớn có vấn đề riêng.

Ví dụ:

Video 800 MB.

Nếu upload qua backend trong một request, có thể gặp:

  • Timeout.
  • Mất mạng giữa chừng.
  • Backend giữ connection quá lâu.
  • User phải upload lại từ đầu.
  • Worker/web server bị chiếm tài nguyên.

Với file lớn, thường cần:

  • Direct upload.
  • Multipart upload.
  • Resumable upload.
  • Progress tracking.
  • Retry từng phần.

Multipart upload nghĩa là chia file thành nhiều phần:

part 1
part 2
part 3
...

Upload xong tất cả phần thì storage ghép lại.

Nếu phần 7 lỗi, chỉ cần upload lại phần 7.

Không phải upload lại cả file.

---

33.19. Sau upload thường chưa nên dùng file ngay

Một file vừa upload xong chưa chắc đã sẵn sàng.

Có thể cần:

  • Scan virus.
  • Kiểm tra định dạng.
  • Tạo thumbnail.
  • Chuyển đổi ảnh.
  • Transcode video.
  • Giải nén.
  • Đọc metadata.
  • Đưa vào search.
  • Tạo bản preview.

Vì vậy metadata nên có trạng thái.

Ví dụ:

pending_upload
uploaded
scanning
processing
available
rejected
deleted

Luồng:

File uploaded
-> tạo job scan/process
-> worker xử lý
-> nếu ok: available
-> nếu lỗi: rejected

Đừng để user tải file private vừa upload nếu file đó chưa qua bước kiểm tra cần thiết.

---

33.20. File processing nên dùng worker

Xử lý file thường là việc lâu.

Ví dụ:

  • Resize ảnh.
  • Tạo thumbnail.
  • Convert PDF sang ảnh preview.
  • Transcode video.
  • Scan virus.
  • Parse file CSV.
  • Giải nén submission.
  • Chạy AI/OCR trên tài liệu.

Những việc này không nên nhét vào request chính.

Luồng tốt hơn:

Upload hoàn tất
-> ghi metadata
-> enqueue job
-> worker xử lý file
-> cập nhật trạng thái

Đây chính là kiến thức từ các chương queue/worker quay lại.

File storage không đứng một mình.

Nó thường đi cùng:

  • Database.
  • Queue.
  • Worker.
  • Object storage.
  • CDN.
  • Monitoring.

---

33.21. Ví dụ AI Judge: học sinh nộp file zip

Trong hệ thống AI Judge, học sinh có thể nộp:

source.zip

Sai lầm:

Lưu source.zip trực tiếp vào PostgreSQL.

Khi số bài nộp tăng, database phình rất nhanh.

Cách hợp lý hơn:

PostgreSQL:
  submission_id
  user_id
  assignment_id
  file_id
  status
  created_at

Object storage:
  submissions/2026/05/12/file_123.zip

Luồng:

Student xin upload URL
-> backend tạo file record status=pending_upload
-> browser upload zip lên storage
-> browser báo upload done
-> backend chuyển status=uploaded
-> enqueue GradingJob
-> worker tải file từ storage
-> giải nén/chấm
-> lưu kết quả vào database
-> lưu artifact nếu cần vào object storage

Artifact có thể là:

  • File log.
  • File report.
  • File feedback PDF.
  • File output chi tiết.

Database giữ kết quả chính.

Object storage giữ file nặng.

---

33.22. Ví dụ AI Judge: feedback file

Sau khi chấm, hệ thống có thể tạo:

feedback.pdf

Hoặc:

grading_report.json

Nếu file nhỏ và cần query nội dung, có thể lưu phần tóm tắt vào database.

Nhưng file đầy đủ nên nằm ở object storage.

Ví dụ:

Database:
  grading_result_id
  score
  summary
  report_file_id

Object storage:
  grading-results/job_456/report.pdf

Khi học sinh mở kết quả:

Backend kiểm tra:
  học sinh này có quyền xem result không?

Nếu có:
  tạo signed URL cho report.pdf

Không nên để link report private thành URL public vĩnh viễn.

---

33.23. Metadata là phần cực kỳ quan trọng

Nhiều người chỉ nghĩ đến file bytes.

Nhưng trong hệ thống thật, metadata mới là thứ giúp ta quản trị file.

Metadata trả lời:

  • File này của ai?
  • Thuộc entity nào?
  • Loại file gì?
  • Dung lượng bao nhiêu?
  • Đang ở trạng thái nào?
  • Có an toàn không?
  • Ai được xem?
  • Khi nào hết hạn?
  • Có bị xoá mềm chưa?
  • Nằm ở bucket/key nào?
  • Checksum là gì?

Không có metadata tốt, object storage sẽ thành một đống file khó kiểm soát.

Object storage là kho.

Database metadata là bản đồ kho.

---

33.24. Lifecycle là gì?

Lifecycle là vòng đời của file.

Một file có thể đi qua:

created
uploaded
processed
available
archived
deleted

Không phải file nào cũng cần giữ mãi.

Ví dụ:

  • File tạm upload lỗi có thể xoá sau 24 giờ.
  • File export báo cáo có thể xoá sau 7 ngày.
  • File log có thể chuyển sang storage rẻ hơn sau 30 ngày.
  • File submission có thể giữ theo chính sách trường học.
  • File backup cần retention riêng.

Object storage thường hỗ trợ lifecycle policy.

Ví dụ:

Xoá object trong prefix tmp/ sau 1 ngày.
Chuyển object trong logs/ sang cold storage sau 30 ngày.
Xoá incomplete multipart upload sau 7 ngày.

Nếu không có lifecycle, storage sẽ âm thầm phình ra.

Chi phí cũng âm thầm tăng.

---

33.25. Dọn rác file

Một vấn đề rất thực tế:

Database record đã xoá nhưng object còn.

Hoặc:

Object đã upload nhưng database record chưa hoàn tất.

Vì hệ thống có nhiều bước, chuyện lệch nhau là bình thường.

Ví dụ:

User xin upload URL.
Backend tạo file record.
User đóng tab giữa chừng.
File record pending mãi.

Hoặc:

User upload xong.
Backend chưa kịp nhận callback.

Cần có cleanup job.

Ví dụ:

Mỗi giờ:
  tìm file pending_upload quá 24 giờ
  đánh dấu expired
  xoá object nếu có

Hoặc:

Mỗi ngày:
  tìm object trong tmp/ quá 1 ngày
  xoá

File storage cần vận hành, không chỉ code upload/download.

---

33.26. Xoá file: xoá mềm hay xoá thật?

Không nên lúc nào cũng xoá vật lý ngay.

Có hai kiểu:

Soft delete:
  đánh dấu deleted trong database
  chưa xoá object ngay

Hard delete:
  xoá object thật khỏi storage

Soft delete hữu ích khi:

  • Cần khôi phục nhầm lẫn.
  • Cần giữ audit.
  • Có yêu cầu retention.
  • Muốn tránh lỗi do xoá nhầm khi transaction chưa rõ.

Hard delete cần khi:

  • Hết thời gian retention.
  • User yêu cầu xoá dữ liệu theo chính sách.
  • File là dữ liệu nhạy cảm không được giữ.
  • Dọn storage.

Thiết kế hay dùng:

User xoá file
-> database marked deleted
-> sau một thời gian grace period
-> cleanup worker xoá object thật

Như vậy hệ thống có khoảng đệm để xử lý lỗi.

---

33.27. Quyền truy cập file

Đừng chỉ hỏi:

File tồn tại không?

Phải hỏi:

User hiện tại có quyền làm việc này với file không?

Các hành động khác nhau:

  • Upload.
  • View.
  • Download.
  • Replace.
  • Delete.
  • Share.
  • Make public.

Một giáo viên có thể xem bài nộp của lớp mình.

Một học sinh chỉ xem bài của mình.

Admin có thể xem nhiều hơn.

Một file report có thể chỉ mở sau khi chấm xong.

Quyền truy cập nên dựa vào dữ liệu nghiệp vụ trong database, không dựa vào việc user đoán được URL.

---

33.28. Key khó đoán không thay thế permission

Dùng key ngẫu nhiên là tốt.

Ví dụ:

files/01HX9Z4Z7SNQ.../report.pdf

Nó làm URL khó đoán.

Nhưng không đủ.

Nếu ai đó có URL, họ vẫn có thể tải nếu object public.

Với file private:

  • Bucket nên private.
  • Backend kiểm tra quyền.
  • Link tải nên hết hạn.
  • Có thể dùng signed cookie/header nếu cần.

Key khó đoán là một lớp phụ.

Permission mới là lớp chính.

---

33.29. Checksum dùng để làm gì?

Checksum là dấu vân tay của nội dung file.

Ví dụ:

sha256 = abc123...

Checksum giúp:

  • Kiểm tra file upload có đúng không.
  • Phát hiện trùng file.
  • Xác minh file không bị hỏng.
  • Làm content-addressing nếu cần.

Ví dụ:

User upload source.zip.
Backend/worker tính SHA-256.
Lưu checksum vào database.

Nếu sau này cần kiểm tra:

File trong storage còn đúng nội dung cũ không?

có thể tính lại checksum để so.

Không phải hệ thống nào cũng cần checksum phức tạp.

Nhưng với file quan trọng, checksum rất đáng có.

---

33.30. Versioning

Một object key có thể bị ghi đè nếu ta dùng cùng key.

Ví dụ:

avatars/user_9/avatar.webp

User đổi avatar.

Nếu upload đè vào cùng key, có thể gặp:

  • CDN vẫn cache bản cũ.
  • Không rollback được.
  • Audit khó hơn.
  • Race condition nếu nhiều request.

Cách đơn giản:

avatars/user_9/file_v1.webp
avatars/user_9/file_v2.webp

Hoặc dùng file id:

avatars/user_9/file_01HX....webp

Database chỉ trỏ tới file hiện tại.

Ví dụ:

users.avatar_file_id = file_456

Khi user đổi avatar:

Tạo file mới.
Cập nhật avatar_file_id.
File cũ dọn sau.

Tránh overwrite giúp hệ thống dễ debug và cache ổn hơn.

---

33.31. Image upload: đừng phục vụ ảnh gốc một cách ngây thơ

Ảnh user upload có thể rất lớn.

Ví dụ:

Ảnh điện thoại 8 MB.

Nếu cứ phục vụ ảnh gốc ở mọi nơi, website sẽ chậm.

Thường nên tạo các phiên bản:

thumbnail
small
medium
large
original

Ví dụ:

products/file_123/thumb.webp
products/file_123/medium.webp
products/file_123/original.jpg

Trang danh sách dùng thumbnail.

Trang chi tiết dùng medium/large.

Chỉ khi cần mới tải original.

Việc resize/convert nên do worker làm sau upload.

---

33.32. Video upload phức tạp hơn ảnh

Video thường nặng hơn ảnh rất nhiều.

Video cần:

  • Multipart upload.
  • Transcoding.
  • Nhiều độ phân giải.
  • Streaming format.
  • Thumbnail.
  • Duration metadata.
  • Progress xử lý.

Ví dụ:

lesson.mp4 uploaded
-> worker transcode thành 1080p, 720p, 480p
-> tạo thumbnail
-> cập nhật status=available

Nếu hệ thống không chuyên về video, nên cân nhắc dùng dịch vụ managed.

Ví dụ:

  • Mux.
  • Cloudflare Stream.
  • Vimeo OTT.
  • AWS MediaConvert.

Video là một mảng riêng.

Đừng đánh giá thấp nó.

---

33.33. File export và file tạm

Một hệ thống thường có file do hệ thống tạo ra.

Ví dụ:

  • Export CSV.
  • Export PDF.
  • Report hàng tháng.
  • Backup nhỏ.
  • Invoice PDF.
  • Data dump.

Những file này thường không cần giữ mãi.

Luồng:

User yêu cầu export
-> tạo job nền
-> worker tạo file
-> upload lên object storage
-> database lưu file_id/status
-> user nhận link tải
-> file hết hạn sau 7 ngày

Điểm quan trọng:

> File export thường nên có expiration.

Nếu không, mỗi lần user bấm export là storage tăng thêm.

---

33.34. Object storage không thay thế database

Có người thấy object storage rẻ và scale tốt, rồi nghĩ:

Vậy lưu hết vào object storage cho rẻ.

Không nên.

Object storage không giỏi:

  • Transaction nghiệp vụ.
  • Query linh hoạt.
  • Constraint.
  • Join.
  • Tìm theo nhiều điều kiện.
  • Cập nhật quan hệ dữ liệu.

Nếu muốn tìm:

Tất cả bài nộp của user 9 trong assignment 12, status graded.

Database làm tốt.

Object storage chỉ biết:

Lấy object theo key.

Vì vậy:

Database quản lý nghiệp vụ.
Object storage giữ bytes.
Search giúp tìm kiếm text.
Cache giúp đọc nhanh.
CDN giúp phân phối file nhanh.

Mỗi thứ có vai trò riêng.

---

33.35. Object storage có eventual consistency không?

Tùy dịch vụ và thao tác.

Nhiều dịch vụ object storage hiện đại đã mạnh hơn trước rất nhiều về consistency.

Nhưng trong thiết kế hệ thống, vẫn nên cẩn thận với các tình huống:

  • Upload vừa xong đã đọc ngay.
  • List prefix để tìm object mới.
  • Xoá object rồi CDN vẫn cache.
  • Metadata database và object storage lệch nhau.

Quy tắc thực tế:

> Đừng dùng "list object trong bucket" làm cách chính để biết nghiệp vụ có gì. Hãy dùng database metadata.

Nếu cần biết user đã upload file nào:

Query database.

Không nên:

List toàn bộ prefix trong object storage rồi suy luận nghiệp vụ.

Object storage là kho chứa.

Database là sổ quản lý.

---

33.36. Tên key nên thiết kế thế nào?

Một key tốt nên:

  • Không lộ thông tin nhạy cảm.
  • Tránh trùng.
  • Dễ phân vùng theo thời gian hoặc loại dữ liệu.
  • Không phụ thuộc vào tên file user gửi.
  • Có thể dọn theo prefix.

Ví dụ:

submissions/2026/05/12/file_01HXABC.zip
avatars/2026/05/file_01HXDEF.webp
exports/2026/05/12/export_01HXGHI.csv
tmp/uploads/2026/05/12/file_01HXJKL

Không nên dùng:

user_9_nguyen_van_a_bai_thi_cuoi_ky.zip

Vì key có thể lộ thông tin cá nhân.

Tên hiển thị lưu ở database.

Key storage nên phục vụ vận hành.

---

33.37. Một thiết kế bảng file đơn giản

Ví dụ:

files
-----
id
owner_type
owner_id
bucket
object_key
original_filename
content_type
size_bytes
checksum
visibility
status
created_by
created_at
updated_at
deleted_at
expires_at

Trong đó:

owner_type: submission, avatar, export, course_material
owner_id: id của entity liên quan
visibility: public/private
status: pending_upload/uploaded/processing/available/rejected/deleted
expires_at: nếu file tạm

Không nhất thiết mọi hệ thống phải có một bảng files chung.

Có thể tách theo domain.

Ví dụ:

submission_files
avatar_files
export_files

Quan trọng là:

> Phải có nơi quản lý metadata và quyền của file.

---

33.38. File có nên thuộc service nào trong microservices?

Trong microservices, câu hỏi thường là:

Có nên có File Service riêng không?

Câu trả lời:

Tùy.

Nếu hệ thống nhỏ, mỗi service có thể tự quản lý file của domain mình.

Ví dụ:

Submission Service quản lý submission files.
Course Service quản lý course materials.
User Service quản lý avatar.

Nếu hệ thống lớn, có thể có File Service cung cấp:

  • Tạo presigned upload URL.
  • Quản lý metadata chung.
  • Scan/process pipeline.
  • Permission integration.
  • Virus scanning.
  • Storage abstraction.
  • Signed download URL.

Nhưng File Service dễ trở thành service trung tâm bị phụ thuộc quá nhiều.

Không nên tạo File Service chỉ vì nghe có vẻ sạch.

Nên tạo khi:

  • Nhiều domain cần upload/download giống nhau.
  • Cần policy thống nhất.
  • Cần scan/process chung.
  • Cần audit chung.
  • Cần quản lý quota/lifecycle tập trung.

Nếu không, để domain tự quản lý có thể đơn giản hơn.

---

33.39. File Service không được bỏ qua domain permission

Nếu có File Service, nó không nên tự quyết định mọi quyền nghiệp vụ trong chân không.

Ví dụ:

Học sinh A có được xem report của submission B không?

Đây là câu hỏi thuộc domain học tập/chấm bài.

File Service có thể biết:

file_id -> object_key

Nhưng quyền thật có thể cần hỏi:

Submission/Assignment/Course membership.

Có vài cách:

  • Domain service kiểm tra quyền rồi gọi File Service tạo signed URL.
  • File Service gọi permission service.
  • File metadata chứa owner/domain reference và policy đủ rõ.

Điểm quan trọng:

> File permission thường là permission nghiệp vụ, không chỉ là permission storage.

---

33.40. Quota

Upload file cần giới hạn.

Nếu không, một user có thể vô tình hoặc cố ý làm storage phình rất nhanh.

Các quota thường gặp:

  • Dung lượng tối đa mỗi file.
  • Tổng dung lượng mỗi user.
  • Tổng dung lượng mỗi organization.
  • Số file upload mỗi ngày.
  • Loại file được phép.
  • Thời gian giữ file tạm.

Ví dụ:

Mỗi submission tối đa 20 MB.
Mỗi user tối đa 2 GB storage.
File export giữ 7 ngày.
File tmp xoá sau 24 giờ.

Quota nên được kiểm tra trước khi cấp presigned URL.

Không nên đợi user upload xong 5 GB rồi mới nói:

Không được.

---

33.41. Virus scan và nội dung nguy hiểm

Nếu user upload file, hệ thống phải nghĩ đến an toàn.

Không phải sản phẩm nào cũng cần mức bảo mật giống nhau.

Nhưng ít nhất nên biết các rủi ro:

  • Malware.
  • File giả dạng.
  • Script trong SVG/HTML.
  • Zip bomb.
  • File quá lớn.
  • File nén chứa quá nhiều file.
  • PDF nguy hiểm.
  • Nội dung vi phạm.

Với hệ thống học tập, bài nộp dạng zip/source code có thể có rủi ro.

Nếu worker giải nén/chạy code, càng phải cô lập.

Quy tắc:

> Upload file từ user là boundary không tin cậy.

Nếu cần scan:

uploaded
-> scanning
-> available hoặc rejected

Trong thời gian scanning, không cho người khác tải file nếu có rủi ro.

---

33.42. Không xử lý file nguy hiểm trên máy chính

Nếu hệ thống nhận file nén, source code, tài liệu lạ, không nên xử lý vô tư trên server chính.

Ví dụ AI Judge:

Học sinh upload source.zip.

Worker cần giải nén và có thể chạy test.

Nếu chạy không cô lập, file độc có thể gây hại.

Nên dùng:

  • Sandbox.
  • Container.
  • Giới hạn CPU/memory/time.
  • Giới hạn network.
  • Thư mục tạm riêng.
  • Cleanup sau xử lý.

Đây là mảng security sâu hơn, nhưng tư duy cơ bản là:

> File user gửi vào không được đối xử như file tin cậy của hệ thống.

---

33.43. Object storage và backup

Object storage thường bền vững, nhưng không có nghĩa là không cần chiến lược backup.

Cần hỏi:

  • Nếu user xoá nhầm file thì sao?
  • Nếu code xoá sai prefix thì sao?
  • Nếu bucket bị cấu hình sai thì sao?
  • Nếu metadata database mất nhưng object còn thì sao?
  • Nếu object mất nhưng metadata còn thì sao?

Các cơ chế hữu ích:

  • Versioning.
  • Soft delete.
  • Retention policy.
  • Cross-region replication.
  • Backup metadata database.
  • Audit log thao tác xoá.

Điểm hay bị quên:

> Backup file và backup database phải ăn khớp.

Nếu restore database về hôm qua nhưng object storage vẫn là hôm nay, trạng thái có thể lệch.

Cần có kế hoạch restore tổng thể.

---

33.44. Monitoring file storage

Cần theo dõi:

  • Tổng dung lượng.
  • Dung lượng theo bucket/prefix.
  • Số object.
  • Chi phí storage.
  • Chi phí bandwidth.
  • Tỉ lệ upload lỗi.
  • Tỉ lệ download lỗi.
  • Thời gian upload.
  • Processing queue backlog.
  • File stuck ở trạng thái pending/processing.
  • Số incomplete multipart upload.

Không theo dõi, file storage thường chỉ được phát hiện khi:

Hoá đơn cloud tăng mạnh.

Hoặc:

User báo không tải được file.

File cũng cần observability như các phần khác của hệ thống.

---

33.45. Chi phí file không chỉ là dung lượng

Chi phí file gồm:

  • Storage per GB.
  • Request upload/download.
  • Data transfer/bandwidth.
  • CDN.
  • Transcoding.
  • Virus scanning.
  • Replication.
  • Backup.
  • Cold storage retrieval.

Ví dụ:

Lưu 1 TB có thể không quá đắt.
Nhưng phục vụ 100 TB download mỗi tháng có thể rất đắt.

Vì vậy khi thiết kế cần hỏi:

  • File được upload bao nhiêu mỗi ngày?
  • File được download bao nhiêu?
  • File public hay private?
  • Có cần CDN không?
  • Có cần giữ mãi không?
  • Có thể nén/resize không?
  • Có thể xoá file tạm không?

Storage cost phải tính cả vòng đời.

---

33.46. Object storage, CDN, cache khác nhau thế nào?

Object storage:

Nơi lưu file gốc.

CDN:

Bản phân phối/cache gần user để tải nhanh.

Cache app/Redis:

Lưu dữ liệu đọc nhanh, thường nhỏ hơn, dùng cho logic app.

Database:

Metadata và dữ liệu nghiệp vụ.

Search:

Tìm kiếm theo text/filter/ranking.

Ví dụ ảnh sản phẩm:

Database:
  product_id, image_file_id

Object storage:
  ảnh gốc và ảnh resize

CDN:
  phục vụ ảnh cho khách

Cache:
  cache product detail nếu cần

Search:
  tìm sản phẩm theo tên/mô tả

Đừng bắt một công cụ làm hết mọi việc.

---

33.47. Pattern thường dùng: upload trước, attach sau

Trong nhiều app, user upload file trước, sau đó mới gắn file vào entity.

Ví dụ:

User soạn bài viết.
User upload ảnh.
Sau đó mới bấm publish bài viết.

Nếu user upload ảnh rồi bỏ dở, file trở thành orphan.

Pattern:

File mới upload có status=temporary.
Khi bài viết được lưu, attach file vào bài viết.
File tạm quá hạn thì cleanup.

Ví dụ:

temporary upload:
  owner_type = draft
  expires_at = now + 24h

publish:
  owner_type = article
  owner_id = article_123
  expires_at = null

Đây là lý do lifecycle và cleanup rất quan trọng.

---

33.48. Pattern thường dùng: database record trước, object sau

Một cách khác:

Tạo database record trước.
Sau đó upload object vào key được chỉ định.

Luồng:

Backend tạo file record:
  id=file_123
  status=pending_upload
  key=...

Backend trả presigned URL.

Client upload.

Client báo complete.

Backend kiểm tra object tồn tại/kích thước.

Backend chuyển status=uploaded.

Ưu điểm:

  • Backend kiểm soát key.
  • Có record để tracking.
  • Dễ cleanup nếu upload không xong.

Nhược điểm:

  • Có thể có record pending bị bỏ dở.

Nhược điểm này xử lý bằng cleanup job.

---

33.49. Pattern thường dùng: outbox/event cho file processing

Sau khi file upload xong, cần xử lý.

Không nên gọi worker bằng logic mong manh.

Có thể dùng event/outbox:

File status chuyển uploaded
-> ghi FileUploaded event vào outbox
-> relay publish event
-> worker xử lý

Worker:

Nhận FileUploaded
-> tải object
-> scan/process
-> cập nhật status
-> tạo derivative files nếu cần

Nếu publish event lỗi, outbox giúp retry.

Nếu worker lỗi, queue retry.

Đây là ví dụ rất thực tế của việc kết hợp:

database + outbox + queue + worker + object storage

---

33.50. Khi nào cần dùng managed service?

Tự ghép object storage + worker là đủ cho nhiều hệ thống.

Nhưng một số bài toán nên cân nhắc managed service:

  • Video streaming.
  • Image optimization quy mô lớn.
  • Document preview.
  • Antivirus scanning managed.
  • OCR.
  • Media transcoding.
  • DRM/protected video.

Lý do:

Những mảng này có nhiều chi tiết vận hành.

Ví dụ video:

  • Codec.
  • Bitrate.
  • HLS/DASH.
  • Subtitle.
  • Adaptive streaming.
  • Thumbnail.
  • Player compatibility.
  • Mobile network.

Nếu sản phẩm chính không phải video platform, dùng dịch vụ có sẵn có thể thực dụng hơn.

---

33.51. Những câu hỏi trước khi thiết kế file storage

Trước khi code upload file, hãy hỏi:

  • File public hay private?
  • Ai được upload?
  • Ai được xem?
  • File tối đa bao nhiêu MB/GB?
  • Tổng dung lượng dự kiến mỗi tháng?
  • Có cần CDN không?
  • Có cần scan virus không?
  • Có cần resize/convert không?
  • Có cần giữ file gốc không?
  • File giữ bao lâu?
  • Khi user xoá thì xoá mềm hay xoá thật?
  • Có cần audit download không?
  • Có cần versioning không?
  • Nếu upload dở dang thì dọn thế nào?
  • Nếu processing lỗi thì user thấy gì?
  • Nếu storage chậm/lỗi thì retry thế nào?

Câu hỏi này quan trọng hơn việc chọn S3 hay GCS.

---

33.52. Các lỗi thực tế hay gặp

Lỗi 1:

Lưu file lớn trong database rồi backup/restore quá chậm.

Lỗi 2:

Để bucket private thành public.

Lỗi 3:

Dùng filename user gửi làm storage key.

Lỗi 4:

Không giới hạn size upload.

Lỗi 5:

Tin vào extension .jpg/.pdf.

Lỗi 6:

Không có cleanup cho file upload dở.

Lỗi 7:

Không có lifecycle, storage tăng mãi.

Lỗi 8:

Cho backend stream mọi file lớn dù có thể dùng signed URL/CDN.

Lỗi 9:

Không theo dõi chi phí bandwidth.

Lỗi 10:

Không kiểm tra quyền trước khi cấp link download.

---

33.53. Bảng chọn nhanh

| Tình huống | Cách làm thường hợp | |---|---| | File nhỏ, ít, app nội bộ | Có thể lưu DB nếu đơn giản | | User upload ảnh/file nhiều | Object storage | | File lớn | Direct upload + multipart/resumable | | File public nhiều người xem | Object storage + CDN | | File private | Bucket private + backend permission + signed URL | | Ảnh user upload | Object storage + worker resize/convert | | Video | Cân nhắc managed video service | | File export tạm | Object storage + expires_at + cleanup | | Bài nộp AI Judge | Object storage + metadata DB + worker | | Cần search nội dung file | Extract text/index sang search, file gốc vẫn ở storage | | Cần audit/download chặt | Signed URL ngắn hạn hoặc stream qua backend có log |

---

33.54. Tóm tắt bằng một luồng hoàn chỉnh

Giả sử học sinh nộp bài zip lên AI Judge.

Luồng thực tế có thể là:

1. Student chọn source.zip.

2. Browser gọi backend:
   "Tôi muốn upload file zip 8 MB cho assignment 12."

3. Backend kiểm tra:
   user có quyền nộp bài không?
   assignment còn mở không?
   file size có hợp lệ không?
   quota còn không?

4. Backend tạo file record:
   status=pending_upload
   key=submissions/2026/05/12/file_123.zip

5. Backend trả presigned upload URL.

6. Browser upload trực tiếp lên object storage.

7. Browser báo backend upload complete.

8. Backend kiểm tra object và cập nhật:
   status=uploaded

9. Backend tạo submission record và enqueue grading job.

10. Worker tải file từ object storage.

11. Worker scan/giải nén/chấm trong sandbox.

12. Worker lưu điểm vào database.

13. Worker lưu report/log vào object storage nếu cần.

14. Khi học sinh xem kết quả:
   backend kiểm tra quyền
   rồi cấp signed download URL cho report.

Trong luồng này:

Database:
  submission, file metadata, grading result

Object storage:
  source.zip, report.pdf, logs

Queue/worker:
  grading, scanning, processing

Signed URL:
  upload/download tạm thời

Permission:
  do backend/domain kiểm tra

Đó là thiết kế thực dụng.

---

33.55. Kết luận của chương

File là phần tưởng đơn giản nhưng rất dễ làm hệ thống phình ra, chậm đi, tốn tiền, hoặc hở dữ liệu.

Nguyên tắc quan trọng:

> Database giữ metadata và quyền. Object storage giữ bytes. CDN phân phối nhanh. Worker xử lý file nặng.

Không nên lưu file lớn trực tiếp trong database trừ khi có lý do rất rõ.

Với file private, đừng dựa vào URL khó đoán. Hãy kiểm tra quyền bằng backend/domain rồi cấp link tạm thời.

Với file lớn, tránh để backend gánh toàn bộ upload/download nếu object storage và signed URL có thể làm tốt hơn.

Với file tạm, export, upload dở, phải có lifecycle và cleanup.

Ở chương tiếp theo, ta sẽ nói về analytics và dữ liệu hành vi: vì sao dữ liệu phân tích khác dữ liệu vận hành, tại sao không nên nhét mọi event hành vi vào database chính, và data warehouse/data lake dùng để làm gì.