Harbor

Lưu trữ và đồng bộ file

Trung bìnhFile lớnĐồng bộ đa thiết bịDedup theo chunkBlob storageGiống Dropbox, Google Drive

Hiểu đề

Lưu trữ và đồng bộ file là gì? Dịch vụ cho người dùng tải file lên đám mây, tự đồng bộ giữa laptop, điện thoại, web và chia sẻ thư mục cho người khác. Ví dụ một nhóm thiết kế cùng làm trên thư mục 50 GB, sửa file ở máy này thì vài giây sau máy kia đã có bản mới.

Yêu cầu chức năng

  1. Người dùng có thể tải file lên, kể cả file vài GB qua mạng chập chờn
  2. Người dùng có thể tải file xuống và xem lại các phiên bản cũ
  3. Thay đổi trên một thiết bị được đồng bộ sang các thiết bị khác của người dùng
  4. Người dùng có thể chia sẻ file/thư mục cho người khác với quyền xem hoặc sửa

Yêu cầu phi chức năng

  • Không mất dữ liệu: độ bền 99,999999999% (11 số 9) cho file đã tải lên xong
  • Thay đổi xuất hiện trên thiết bị khác trong ≤ 5 s khi cả hai đang online
  • Upload tiếp tục được sau khi rớt mạng, chỉ gửi lại phần còn thiếu (chunk 4 MB)
  • API metadata p95 < 200 ms, độ sẵn sàng 99,9%
  • File tối đa 50 GB

Ngoài phạm vi: Chỉnh sửa tài liệu cùng lúc kiểu Google Docs; Tìm kiếm toàn văn bên trong nội dung file; Quét virus, xem trước (preview) chi tiết từng định dạng; Thanh toán và quản lý gói dung lượng.

Thực thể và API

Namespace
Gốc của một cây thư mục: thư mục cá nhân của một user hoặc một thư mục chia sẻ. Có bộ đếm seq tăng dần cho change log.
FileEntry
Một đường dẫn trong namespace: namespace_id, path, file_id, revision hiện tại, is_deleted.
FileVersion
Một phiên bản của file: revision, kích thước, danh sách hash các chunk theo thứ tự, người sửa, thời điểm.
Chunk
Khối dữ liệu tối đa 4 MB, khóa là SHA-256 của nội dung, vị trí trong blob store, refcount.
ChangeEntry
Dòng change log: (namespace_id, seq) → file_id, thao tác (tạo/sửa/xóa/đổi tên), revision mới.
ShareMember
Ai được vào namespace nào với quyền gì (owner/editor/viewer), cùng link chia sẻ có hạn.
MethodEndpointGhi chú
POST/v1/uploadsBody {path, size, chunkHashes[]}. Trả uploadId, danh sách chunk server chưa có và presigned URL cho từng chunk đó.
PUT{presigned URL của blob store}Client đẩy thẳng từng chunk lên S3/GCS, không đi qua API server.
POST/v1/uploads/{uploadId}/commitBody {baseRevision}. Tạo FileVersion mới; trả 409 nếu file đã bị sửa từ revision khác.
GET/v1/files/{fileId}?rev=Trả metadata và presigned GET URL cho từng chunk của phiên bản cần tải.
GET/v1/changes?cursor=Trả các thay đổi sau cursor cùng cursor mới. Không có gì mới thì giữ kết nối tối đa 30-60 s (long-poll).
POST/v1/sharesBody {folderId, email, role}. Thêm thành viên vào namespace của thư mục chia sẻ.

Thiết kế tổng thể

Đi lần lượt từng yêu cầu chức năng. Mục tiêu ở đây là chạy đúng, việc tối ưu để dành cho phần đào sâu.

1) Người dùng có thể tải file lên

File được cắt thành chunk 4 MB ngay trên client. Client chỉ gửi hash lên API, API trả về chunk nào còn thiếu kèm presigned URL, dữ liệu đi thẳng vào blob store. Metadata chỉ được ghi khi mọi chunk đã nằm an toàn trong storage.

  1. 1Client cắt file thành chunk 4 MB, tính SHA-256 từng chunk
  2. 2POST /v1/uploads gửi danh sách hash; API tra bảng Chunk để biết chunk nào đã có
  3. 3API trả presigned PUT URL (hạn 1 giờ) cho các chunk còn thiếu
  4. 4Client PUT song song 3-4 chunk thẳng lên S3, chunk lỗi thì gửi lại riêng chunk đó
  5. 5POST /commit: API kiểm tra đủ chunk, ghi FileVersion + FileEntry + ChangeEntry trong một transaction

Nguồn: Grokking Modern System Design · ch.20 Blob Store ↗ · Design of a Blob Store · mục "Detailed design" (blob chia thành chunk, manager node lưu chunk ID và data node) ; Grokking Modern System Design · ch.20 Blob Store ↗ · Design Considerations of a Blob Store · mục "Partition data" (chọn kích thước chunk cố định) ; Microservices Patterns ↗ (.zip) · ch.3 · §3.3.7 Transactional messaging (tr. 97–100) ; Chưa có nguồn trong thư viện: presigned URL là tính năng của nhà cung cấp object storage, thư viện không có sách mô tả ; Chưa có nguồn trong thư viện: băm SHA-256 từng chunk để biết chunk đã có

2) Người dùng có thể tải file xuống và xem phiên bản cũ

Tải xuống cũng không đi qua API. API trả danh sách chunk của phiên bản cần tải kèm presigned GET URL, client tải song song và ghép lại. Phiên bản cũ chỉ là một FileVersion khác trỏ tới các chunk cũ nên không tốn thêm bản sao.

  1. 1GET /v1/files/{id}?rev=12 trả danh sách hash chunk theo thứ tự
  2. 2API ký presigned GET URL cho từng chunk (hoặc URL qua CDN với link chia sẻ công khai)
  3. 3Client tải song song, kiểm tra SHA-256 từng chunk, ghép thành file
  4. 4Chunk nào máy đã có sẵn (từ phiên bản trước) thì bỏ qua không tải

Nguồn: Grokking Modern System Design · ch.20 Blob Store ↗ · Design of a Blob Store · mục "Detailed design" (blob chia thành chunk, manager node lưu chunk ID và data node) ; Grokking Modern System Design · ch.20 Blob Store ↗ · Design of a Blob Store · mục "Workflow" (đọc: manager trả chunk và data node, client đọc chunk) ; Grokking Modern System Design · ch.20 Blob Store ↗ · Design of a Blob Store · mục "Workflow" (blob mới chỉ nhận version number, không sửa blob cũ) ; Grokking Modern System Design · ch.11 Content Delivery Network (CDN) ↗ · mục "Pull CDN" ; Chưa có nguồn trong thư viện: presigned URL là tính năng của nhà cung cấp object storage, thư viện không có sách mô tả

3) Thay đổi được đồng bộ sang thiết bị khác

Mỗi namespace có change log với seq tăng dần. Thiết bị giữ cursor (seq cuối cùng đã thấy) và hỏi phần thay đổi sau cursor đó. Ở quy mô lớn, server chỉ đẩy tín hiệu 'có thay đổi' qua kết nối mở, thiết bị tự kéo phần delta.

  1. 1Commit thành công ghi ChangeEntry (namespace_id, seq+1)
  2. 2Notification service báo cho các thiết bị đang theo dõi namespace đó
  3. 3Thiết bị gọi GET /v1/changes?cursor=... nhận danh sách file đổi và cursor mới
  4. 4Với mỗi file đổi, so danh sách chunk mới với chunk đang có, chỉ tải chunk thiếu
  5. 5Thiết bị offline lâu thì khi online lại cũng chỉ cần cursor để bắt kịp

Nguồn: Grokking Modern System Design · ch.36 WhatsApp ↗ · Detailed Design of WhatsApp · mục "Connection with a WebSocket server" ; Grokking Modern System Design · ch.18 Pub-sub ↗ · mục "High-level design" (broker, consumer manager) ; Microservices Patterns ↗ (.zip) · ch.3 · §3.3.7 Transactional messaging (tr. 97–100) ; Chưa có nguồn trong thư viện: mô hình change log + cursor theo namespace là thiết kế của khoá học

4) Người dùng có thể chia sẻ thư mục

Thư mục chia sẻ được tách thành một namespace riêng và gắn vào cây thư mục của từng thành viên. Quyền được kiểm tra ở mức namespace, không phải từng file, nên thêm người không cần sao chép hay cập nhật hàng nghìn bản ghi.

  1. 1POST /v1/shares: nếu thư mục chưa là namespace riêng thì tách ra, ghi ShareMember
  2. 2Người nhận nhận email/thông báo, chấp nhận thì namespace được gắn vào cây của họ
  3. 3Mọi request đọc/ghi kiểm tra ShareMember (cache trong Redis) theo namespace_id
  4. 4Thiết bị của người nhận bắt đầu theo dõi change log của namespace mới

Nguồn: Grokking Modern System Design · ch.20 Blob Store ↗ · Design of a Blob Store · mục "Workflow" (kiểm tra quyền truy cập blob ở front-end) ; Grokking Modern System Design · ch.16 Distributed Cache ↗ · Background of Distributed Cache · mục "Writing policies" (write-around: ghi vào cache sau lần trượt) ; Chưa có nguồn trong thư viện: mô hình namespace dùng chung và bảng ShareMember là thiết kế của khoá học

Hạ tầng theo quy mô

Cùng một sản phẩm, ba mức người dùng. Chọn từng mức để xem sơ đồ, phép tính và cấu hình server thay đổi thế nào.

Người dùng
10.000 DAU (khoảng 20.000 tài khoản)
Tải đỉnh
≈ 160 req/s metadata, ≈ 2,3 MB/s upload đỉnh
Chi phí ước lượng
≈ 150–500 USD/tháng (ước lượng, tăng dần theo dung lượng tích lũy)

Ổ lưu trữ cho một công ty hoặc sản phẩm mới. Hai instance API chỉ lo metadata và ký URL, dữ liệu đi thẳng vào S3, một Postgres chứa toàn bộ metadata. Đồng bộ bằng poll change log 30 s.

Người dùngNgười dùngDịch vụAPI Node.jsLưu trữS3 (chunk)DatabasePostgreSQLWorkerJob dọn rácBên ngoàiEmail (SES)metadata, pollPUT/GET presignedcommitmời chia sẻxóa chunkrefcount

Ước lượng

  • Upload: 10.000 DAU × 2 file × 1 MB = 20.000 MB = 20 GB/ngày → 20 GB × 365 = 7.300 GB ≈ 7,3 TB/năm trên S3
  • Băng thông upload: 20.000 MB / 86.400 ≈ 0,23 MB/s, đỉnh ×10 ≈ 2,3 MB/s, đi thẳng vào S3
  • Đồng bộ: 10.000 × 1,5 thiết bị × 20% online = 3.000 thiết bị, poll 30 s → 3.000 / 30 = 100 req/s
  • Thao tác khác: 10.000 × 50 req / 86.400 ≈ 5,8 req/s, đỉnh ×10 ≈ 58 req/s; tổng ≈ 158 req/s
  • API: ceil(158 / (1.000 × 0,6)) = 1 → lấy tối thiểu 2 instance
  • Metadata: 20.000 tài khoản × 500 file × 500 B = 5.000.000.000 B ≈ 5 GB

Nguồn con số: Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations ↗ · mục "Examples of Resource Estimation" (Number of servers required, Storage requirements, Bandwidth requirements) ; Grokking Modern System Design · ch.20 Blob Store ↗ · Requirements of a Blob Store's Design · mục "Resource estimation" ; Quy tắc ước lượng của khoá học · /hoc/chon-server (1.000 req/s mỗi máy 2 vCPU, chạy tối đa 60% CPU, Redis ×1,3)

Bảng hạ tầng

Thành phầnChọnSố lượngCấu hìnhVì sao
API metadataNode.js (NestJS), Docker trên Cloud Run22 vCPU · 4 GB RAM158 req/s chỉ cần 1 instance, chạy 2 để dự phòng; không có byte file nào đi qua API.
DatabasePostgreSQL managed12 vCPU · 8 GB · 50 GB SSD5 GB metadata nằm gọn trong RAM; transaction commit đơn giản.
Blob storageAmazon S3 / Google Cloud Storage1 bucketChunk 4 MB, key = SHA-256, versioning tắtĐộ bền 11 số 9 có sẵn, 7,3 TB/năm chỉ vài trăm USD/tháng.
Dọn dẹpCloud Scheduler gọi job trong API1Chạy mỗi giờXóa upload session bỏ dở và chunk refcount = 0; khối lượng nhỏ, chưa cần worker riêng.
EmailAmazon SES / SendGrid1Dịch vụ bên ngoàiGửi lời mời chia sẻ.
Thay đổi so với mức trước
  • Điểm xuất phát: API chỉ xử lý metadata, file đi thẳng S3 bằng presigned URL
  • Upload theo chunk 4 MB có thể tiếp tục ngay từ đầu, vì đây là yêu cầu chức năng chứ không phải tối ưu
  • Đồng bộ bằng poll change log 30 s là đủ cho 3.000 thiết bị online
Nút thắt tiếp theo

Khi số thiết bị online lên hàng trăm nghìn, poll 30 s tạo hàng nghìn request rỗng mỗi giây và độ trễ đồng bộ 30 s là quá chậm. Cần kênh đẩy thông báo và cache quyền truy cập.

Cấu hình và chi phí là ước lượng để học và trình bày trong phỏng vấn. Xem cách tính ở bài Chọn server.

So sánh nhanh ba mức

Thành phầnMức 1Mức 2Mức 3
Người dùng10.000 DAU (khoảng 20.000 tài khoản)1 triệu DAU (khoảng 3 triệu tài khoản)50 triệu DAU (khoảng 150 triệu tài khoản)
Chi phí≈ 150–500 USD/tháng (ước lượng, tăng dần theo dung lượng tích lũy)≈ 15.000–35.000 USD/tháng (ước lượng, phần lớn là lưu trữ và băng thông tải xuống)≈ 400.000–1.000.000 USD/tháng (ước lượng, chủ yếu là lưu trữ tích lũy và băng thông)
API metadata
Node.js (NestJS), Docker trên Cloud Run
2 × 2 vCPU · 4 GB RAM
Node.js (NestJS), container autoscale
2–10 (autoscale) × 2 vCPU · 4 GB RAM
Chưa cần
Database
PostgreSQL managed
1 × 2 vCPU · 8 GB · 50 GB SSD
PostgreSQL managed (HA)
1 primary + 2 read replica × 8 vCPU · 32 GB · 2 TB SSD
Chưa cần
Blob storage
Amazon S3 / Google Cloud Storage
1 bucket × Chunk 4 MB, key = SHA-256, versioning tắt
S3 / GCS, lifecycle sang lớp Infrequent Access sau 30 ngày
1 bucket × ≈ 511 TB/năm sau dedup
S3 nhiều lớp (Standard → IA → Glacier) hoặc blob store tự xây dùng erasure coding
Nhiều region × ≈ 25,6 PB/năm sau dedup
Dọn dẹp
Cloud Scheduler gọi job trong API
1 × Chạy mỗi giờ
Chưa cầnChưa cần
Email
Amazon SES / SendGrid
1 × Dịch vụ bên ngoài
Chưa cầnChưa cần
Load balancerChưa cần
Cloud Load Balancing / ALB
1 × Hỗ trợ WebSocket, TLS
Chưa cần
Notification gatewayChưa cần
Node.js WebSocket + Redis pub/sub
15 × 2 vCPU · 4 GB RAM
Node.js WebSocket trên Kubernetes
750 pod × 2 vCPU · 4 GB mỗi pod
RedisChưa cần
ElastiCache / Memorystore
1 primary + 1 replica × 2 vCPU · 8 GB
Chưa cần
CDNChưa cần
CloudFront / Cloud CDN
1 × Link chia sẻ công khai, thumbnail
Chưa cần
Queue + WorkerChưa cần
SQS / Pub/Sub + Node.js consumer
1 queue, 4 worker × 1 vCPU · 2 GB mỗi worker
Chưa cần
API gateway + LBChưa cầnChưa cần
Envoy / cloud LB, đa AZ
1 mỗi region × TLS, auth, rate limit
Metadata APIChưa cầnChưa cần
Node.js trên Kubernetes, 3 AZ
290 pod (HPA) × 2 vCPU · 4 GB mỗi pod
Metadata DBChưa cầnChưa cần
MySQL/Vitess hoặc Postgres + Citus, shard theo namespace_id
64 shard × (1 primary + 2 replica) = 192 node × 8 vCPU · 64 GB · 2 TB SSD mỗi node
Chỉ mục chunkChưa cầnChưa cần
DynamoDB / Cassandra
Managed (hoặc 6 node Cassandra RF 3) × hash → vị trí, refcount
Redis ClusterChưa cầnChưa cần
ElastiCache Redis Cluster
6 shard × (1 primary + 1 replica) × 4 vCPU · 32 GB mỗi node
KafkaChưa cầnChưa cần
MSK / Confluent
3 broker mỗi region × Topic changes 24 partition, RF 3
WorkerChưa cầnChưa cần
Node.js / Go consumer trên Kubernetes
30 pod × 2 vCPU · 4 GB mỗi pod

Đào sâu

Mỗi câu hỏi đi từ cách đơn giản đến cách người phỏng vấn mong đợi ở cấp senior.

1) Tải lên file 5 GB qua mạng 4G chập chờn thế nào cho chắc?

Cách chưa tốt
Gửi cả file qua API server trong một request

Client POST multipart lên Node.js, server ghi tạm ra đĩa rồi đẩy sang S3. Mỗi upload chiếm một kết nối và băng thông của API trong nhiều phút.

Đánh đổi: Rớt mạng ở 4,9 GB là phải tải lại từ đầu, và API phải scale theo băng thông thay vì theo số request.

Nguồn: Grokking Modern System Design · ch.20 Blob Store ↗ · Requirements of a Blob Store's Design · mục "Throughput" và "Number of servers estimation" (front-end server không nên gánh dữ liệu lớn) ; Chưa có nguồn trong thư viện: presigned URL là tính năng của nhà cung cấp object storage, thư viện không có sách mô tả

Cách tốt
Presigned URL, client PUT thẳng lên blob store

API chỉ ký một URL có hạn cho đúng object key, client upload thẳng lên S3/GCS. API server thoát khỏi đường đi của dữ liệu, chỉ còn xử lý metadata.

Đánh đổi: Vẫn là một request duy nhất: S3 giới hạn 5 GB cho một PUT và lỗi giữa chừng vẫn phải gửi lại toàn bộ.

⚠ Chưa có nguồn trong thư viện: presigned URL là tính năng của nhà cung cấp object storage, thư viện không có sách mô tả ; Chưa có nguồn trong thư viện: giới hạn 5 GB mỗi PUT là con số của nhà cung cấp, chỉ giữ như ví dụ minh họa

Cách tốt nhất
Chia chunk 4 MB, upload session có thể tiếp tục, commit khi đủ chunk

Client cắt file thành chunk, tính hash, tạo upload session lưu danh sách chunk cần có. Mỗi chunk là một PUT riêng bằng presigned URL, gửi song song 3-4 chunk. Rớt mạng thì client hỏi lại session xem còn thiếu chunk nào và chỉ gửi phần đó. Khi đủ thì commit ghi metadata trong một transaction, nên người khác không bao giờ thấy file dở dang. Cách cắt blob thành chunk có kích thước cố định cũng là cách blob store tổ chức dữ liệu (Grokking ch.20).

Đánh đổi: Thêm trạng thái upload session phải dọn (session bỏ dở quá 24 giờ thì xóa chunk mồ côi), và chunk nhỏ làm bảng metadata lớn hơn.

Nguồn: Grokking Modern System Design · ch.20 Blob Store ↗ · Design of a Blob Store · mục "Detailed design" (blob chia thành chunk, manager node lưu chunk ID và data node) ; Grokking Modern System Design · ch.20 Blob Store ↗ · Design Considerations of a Blob Store · mục "Partition data" (chunk nhỏ làm metadata lớn, chunk quá lớn lãng phí đĩa) ; Microservices Patterns ↗ (.zip) · ch.3 · §3.3.7 Transactional messaging (tr. 97–100) ; Grokking Modern System Design · ch.20 Blob Store ↗ · Design Considerations of a Blob Store · mục "Garbage collection while deleting a blob" (dọn phần mồ côi sau)

2) Đồng bộ thay đổi giữa nhiều thiết bị và xử lý xung đột thế nào?

Cách chưa tốt
Thiết bị liệt kê toàn bộ thư mục mỗi 30 s rồi so sánh

Client gọi API lấy cả cây thư mục và so với bản local để tìm thay đổi. Thư mục 100.000 file là vài chục MB mỗi lần hỏi.

Đánh đổi: Tốn băng thông và tải DB tỉ lệ với số file chứ không với số thay đổi, độ trễ lên tới 30 s.

Nguồn: Grokking Modern System Design · ch.36 WhatsApp ↗ · Detailed Design of WhatsApp · mục "Connection with a WebSocket server" (so với hỏi lặp lại) ; Quy tắc ước lượng của khoá học · /hoc/chon-server (1.000 req/s mỗi máy 2 vCPU, chạy tối đa 60% CPU, Redis ×1,3)

Cách tốt
Change log theo namespace + cursor, client long-poll

Mỗi commit ghi một dòng ChangeEntry với seq tăng dần trong namespace. Client giữ cursor và gọi /changes?cursor=; không có gì mới thì server giữ request 30-60 s rồi trả rỗng. Chỉ phần thay đổi được truyền đi.

Đánh đổi: Hàng trăm nghìn long-poll đang treo vẫn chiếm kết nối HTTP, và mỗi lần hết hạn là một request mới dù không có thay đổi.

Nguồn: Grokking Modern System Design · ch.36 WhatsApp ↗ · mục "Connection with a WebSocket server" ; Chưa có nguồn trong thư viện: long-polling không được sách trong thư viện mô tả; chỉ giữ như phương án trung gian

Cách tốt nhất
Change log + cursor, push tín hiệu qua WebSocket, xung đột giải bằng revision

Notification gateway giữ kết nối nhẹ và chỉ đẩy 'namespace X có seq mới'. Thiết bị tự gọi /changes bằng cursor, nên mất tín hiệu cũng không mất dữ liệu. Commit gửi kèm baseRevision; nếu revision trên server đã khác thì trả 409, client giữ bản của mình thành file 'tên (bản xung đột của Laptop).ext' thay vì ghi đè. Không tự merge nội dung vì không hiểu định dạng file.

Đánh đổi: Người dùng phải tự xử lý bản xung đột, và phải vận hành thêm một lớp giữ hàng triệu kết nối.

Nguồn: Grokking Modern System Design · ch.36 WhatsApp ↗ · mục "Connection with a WebSocket server" ; Grokking Modern System Design · ch.9 Databases ↗ · Data Replication · mục "Multi-leader replication" (Conflict, Handle conflicts: Last-write-wins, Custom logic) ; Chưa có nguồn trong thư viện: baseRevision và file bản xung đột là cách làm của khoá học

3) Làm sao không lưu cùng một dữ liệu nhiều lần (dedup)?

Cách chưa tốt
Không dedup, mỗi lần sửa lưu một bản đầy đủ

Sửa một dòng trong file 1 GB là lưu thêm 1 GB và tải lại 1 GB lên. 10 phiên bản là 10 GB.

Đánh đổi: Đơn giản nhất nhưng chi phí lưu trữ và băng thông tăng theo số lần sửa.

Nguồn: Grokking Modern System Design · ch.20 Blob Store ↗ · Design of a Blob Store · mục "Workflow" (blob không sửa tại chỗ, tải bản mới) ; Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations ↗ · mục "Examples of Resource Estimation" (Number of servers required, Storage requirements, Bandwidth requirements) (phép tính dung lượng)

Cách tốt
Dedup theo hash của cả file

Tính SHA-256 cả file, đã có file cùng hash thì chỉ thêm một FileEntry trỏ vào blob có sẵn. Hiệu quả với file giống hệt nhau như bộ cài, video được chia sẻ lại.

Đánh đổi: Sửa một byte là hash khác hoàn toàn, nên vẫn lưu và tải lại cả file.

⚠ Chưa có nguồn trong thư viện: dedup theo hash nội dung không có trong thư viện

Cách tốt nhất
Lưu theo chunk, khóa bằng hash nội dung, có refcount và GC

Blob store là content-addressable: key của chunk là SHA-256 của nó. Phiên bản mới chỉ upload các chunk có hash mới, các chunk không đổi được dùng lại, nên sửa đầu file 1 GB chỉ tốn vài chunk. Mỗi chunk có refcount; xóa file chỉ giảm refcount và đánh dấu, garbage collector chạy sau mới xóa chunk về 0 (cách xóa lười như Grokking ch.20). Để chống lộ dữ liệu khi ai đó chỉ biết hash, chỉ dedup giữa các user nếu client đã thực sự upload chunk, hoặc giới hạn dedup trong cùng namespace.

Đánh đổi: Chunk cố định 4 MB mất tác dụng khi chèn dữ liệu vào giữa file (mọi chunk sau đó lệch hash); muốn tốt hơn phải dùng chunk theo nội dung (rolling hash) phức tạp hơn. Refcount sai là mất dữ liệu nên GC phải đối soát kỹ.

Nguồn: Grokking Modern System Design · ch.20 Blob Store ↗ · Design of a Blob Store · mục "Detailed design" (blob chia thành chunk, manager node lưu chunk ID và data node) ; Grokking Modern System Design · ch.20 Blob Store ↗ · Design Considerations of a Blob Store · mục "Garbage collection while deleting a blob" (đánh dấu xóa trong metadata, collector dọn sau) ; Chưa có nguồn trong thư viện: content-addressable storage, refcount và rolling hash không có trong thư viện

4) Chia sẻ thư mục và kiểm tra quyền thế nào khi một thư mục có hàng chục nghìn file?

Cách chưa tốt
Sao chép file sang tài khoản người nhận

Mỗi người nhận có một bản riêng. Người này sửa thì người kia không thấy, và dung lượng nhân theo số người.

Đánh đổi: Sai về mặt sản phẩm và tốn lưu trữ gấp nhiều lần.

⚠ Chưa có nguồn trong thư viện: lập luận sản phẩm về chia sẻ thư mục, không phải kiến thức hệ thống có trong sách

Cách tốt
Bảng ACL cho từng file

Mỗi file có danh sách ai được đọc/ghi. Kiểm tra quyền chỉ là một lookup theo file_id.

Đánh đổi: Chia sẻ thư mục 50.000 file là ghi 50.000 dòng cho mỗi người nhận, và file mới thêm vào thư mục phải tự kế thừa quyền.

Nguồn: Chưa có nguồn trong thư viện: thiết kế ACL từng file là lựa chọn của khoá học ; Grokking Modern System Design · ch.9 Databases ↗ · Data Partitioning · mục "Sharding"

Cách tốt nhất
Thư mục chia sẻ là một namespace riêng, quyền đặt ở mức namespace

Khi chia sẻ, thư mục trở thành namespace có change log riêng và được gắn vào cây của từng thành viên. Thêm người chỉ là một dòng ShareMember, file mới tự thuộc namespace nên tự có quyền. Kiểm tra quyền = (user, namespace_id) cache trong Redis. Link công khai là token ký có hạn trỏ tới namespace hoặc file. Shard metadata theo namespace_id thì mọi thao tác trên một thư mục chia sẻ nằm gọn trong một shard.

Đánh đổi: Di chuyển file giữa hai namespace trở thành xóa ở chỗ này và tạo ở chỗ kia, có thể khác shard; thu hồi quyền phải đẩy xuống thiết bị để xóa bản local.

Nguồn: Grokking Modern System Design · ch.9 Databases ↗ · Data Partitioning · mục "Sharding" (chọn khóa phân mảnh, giữ dữ liệu liên quan trong một shard) ; Grokking Modern System Design · ch.16 Distributed Cache ↗ · Background of Distributed Cache · mục "Writing policies" (write-around: ghi vào cache sau lần trượt) ; Chưa có nguồn trong thư viện: namespace dùng chung, token ký có hạn là thiết kế của khoá học

Trong phỏng vấn

Nên nói
  • “Tôi tách đường dữ liệu và đường metadata: byte file đi thẳng vào blob store bằng presigned URL, API chỉ xử lý metadata.”
  • “File được cắt chunk 4 MB, khóa bằng SHA-256, nên tôi có cùng lúc upload tiếp tục được, dedup và chỉ gửi phần đổi.”
  • “Đồng bộ dựa trên change log có cursor theo namespace; thông báo đẩy chỉ là tín hiệu, mất cũng không sao.”
  • “Xung đột không tự merge: commit kèm baseRevision, lệch thì giữ cả hai bản và đặt tên bản xung đột.”
Bẫy hay gặp
  • Cho file đi qua API server, khiến API phải scale theo băng thông.
  • Ghi metadata trước khi chunk đã nằm trong storage, người khác thấy file hỏng.
  • Đồng bộ bằng cách liệt kê lại cả thư mục định kỳ.
  • Quên chi phí lưu trữ tích lũy: dữ liệu chỉ tăng, không bao giờ giảm.

Đọc thêm

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.
Cập nhật realtime
Đẩy thay đổi tới client trong vài trăm mili giây: chọn giữa polling, long polling, SSE, WebSocket, rồi fan-out qua nhiều gateway, xử lý reconnect và backpressure.
Sharding
Chia dữ liệu theo chiều dọc và chiều ngang, cách chọn shard key và tránh hotspot, so sánh range, hash và directory, resharding, query và transaction xuyên shard, khi nào chưa nên shard và một Postgres chịu được tới đâu.
PostgreSQL
PostgreSQL là lựa chọn mặc định an toàn cho phần lớn hệ thống: ACID, index phong phú, replication, partitioning và hệ extension mạnh. Bài này giải thích MVCC, các loại index, mức isolation, replication, connection pooling, bảng sizing theo cấu hình và dấu hiệu nên rời một node Postgres.
Change data capture (CDC)
Change data capture đọc mọi thay đổi trong database và phát thành luồng sự kiện. Bài này so sánh CDC dựa trên log, trigger và polling, giải thích Debezium với Kafka, quan hệ với transactional outbox, các use case và những bẫy về thứ tự và schema.
Mô hình dữ liệu
Cách chọn và thiết kế mô hình dữ liệu: relational, document, wide-column, key-value, graph; chuẩn hóa và phi chuẩn hóa, embedding và referencing, chọn primary key, thiết kế theo access pattern, kèm ví dụ từ các bài trong khóa.

Sách trong thư viện của bạn. Số chương có thể lệch nhẹ so với bản PDF.