System Design: Thiết kế hệ thống lưu trữ tương tự dropbox

1. Mục tiêu hệ thống
Dropbox là hệ thống lưu trữ và đồng bộ file trên nhiều thiết bị. Người dùng có thể tạo, sửa, xoá, đổi tên file trên một thiết bị, sau đó thay đổi này cần được đồng bộ lên cloud và lan truyền xuống các thiết bị khác.
Mục tiêu chính của hệ thống:
- Upload và download file an toàn, kể cả file lớn lên tới hàng chục GB.
- Đồng bộ thay đổi giữa local device và cloud.
- Hỗ trợ resume khi upload/download bị gián đoạn.
- Chỉ truyền phần dữ liệu thay đổi khi có thể, thay vì luôn truyền cả file.
- Xử lý conflict khi cùng một file bị sửa ở nhiều nơi.
- Đảm bảo metadata nhất quán với blob data.
- Mở rộng được cho hàng triệu user và hàng tỷ file.
2. Functional Requirements
Hệ thống cần hỗ trợ các chức năng chính sau:
- User có thể upload file.
- User có thể download file.
- User có thể tạo folder, xoá file, rename file, move file.
- Sync agent trên máy user phát hiện thay đổi local và upload lên cloud.
3. Non-functional Requirements
Các yêu cầu phi chức năng quan trọng:
- Reliability: không mất dữ liệu user.
- Scalability: scale được metadata service, storage, sync service.
- Availability: user vẫn có thể truy cập file nhiều nhất có thể.
- Consistency: metadata cần nhất quán đủ để tránh mất file hoặc ghi đè sai.
- Low bandwidth usage: tránh upload/download lại toàn bộ file nếu chỉ thay đổi nhỏ.
- Security: file cần được bảo vệ bằng authentication, authorization, encryption.
- Resumability: upload/download lớn không bị restart từ đầu khi mạng rớt.
4. High-level Architecture
flowchart LR
Client[Desktop / Mobile / Web Client]
Agent[Sync Agent]
LB[Load Balancer / API Gateway]
Auth[Auth Service]
Meta[Metadata Service]
Sync[Sync Service / Change Feed]
Upload[Upload Service]
Object[(Object Storage: S3/GCS)]
DB[(Metadata DB)]
Cache[(Cache)]
MQ[(Message Queue)]
Client --> LB
Agent --> LB
LB --> Auth
LB --> Meta
LB --> Upload
LB --> Sync
Meta --> DB
Meta --> Cache
Upload --> Object
Upload --> DB
Sync --> DB
Meta --> MQ
MQ --> Sync
Các thành phần chính:
- Client / Sync Agent: chạy trên device của user, theo dõi local folder, upload/download changes.
- API Gateway: entry point cho request, xử lý routing, auth, rate limit.
- Auth Service: xác thực user và token.
- Metadata Service: quản lý file metadata như path, size, owner, version, hash, parent folder.
- Object Storage: lưu nội dung file thật.
- Sync Service / Change Feed: cho client biết cloud có thay đổi gì từ lần sync trước.
- Upload Service: tạo upload session, quản lý multipart upload, verify manifest.
- Metadata DB: lưu metadata của file/folder/version.
- Message Queue: phát event khi file thay đổi để phục vụ sync, notification, indexing.
5. Core Data Model
5.1 User
User {
user_id
email
password_hash / oauth_provider
created_at
}
5.2 File Metadata
FileMetadata {
file_id
owner_id
parent_folder_id
name
path
type // file or folder
size
current_version_id
is_deleted
created_at
updated_at
}
5.3 File Version
FileVersion {
version_id
file_id
revision_number
content_hash
size
manifest_id
created_at
created_by_device_id
}
5.4 Chunk Metadata
Chunk {
chunk_id
hash
size
storage_key
created_at
}
5.5 Manifest
Manifest mô tả file được ghép từ những chunk nào.
Manifest {
manifest_id
file_id
version_id
file_size
chunk_size
total_chunks
full_file_checksum
chunks: [
{
part_number
byte_range
size
checksum
chunk_id
storage_key
}
]
}
Manifest rất quan trọng vì nó giúp:
- Biết thứ tự chunk để assemble file.
- Biết byte range của từng chunk.
- Verify integrity bằng checksum.
- Resume upload/download.
- So sánh version local và remote để chỉ tải chunk bị thay đổi.
6. Upload File Flow
Với file nhỏ, client có thể upload trực tiếp bằng một request hoặc upload qua pre-signed URL. Với file lớn, không nên gửi toàn bộ file qua backend vì giới hạn POST body, timeout, memory và network instability.
Flow upload file lớn:
sequenceDiagram
participant C as Client
participant B as Backend Upload Service
participant S as Object Storage
participant M as Metadata DB
C->>B: Create upload session(file metadata)
B->>M: Create upload session record
B-->>C: upload_id + pre-signed URLs / multipart info
C->>C: Split file into chunks
C->>S: Upload chunks directly
C->>B: Complete upload(manifest)
B->>S: Verify uploaded parts
B->>M: Commit file metadata + version
B-->>C: Upload success
Các bước chi tiết:
- Client gọi backend tạo upload session.
- Backend tạo
upload_id. - Client split file thành chunk, ví dụ 8MB, 16MB hoặc 64MB mỗi chunk.
- Client tính checksum cho từng chunk.
- Client upload chunk trực tiếp lên object storage bằng pre-signed URL.
- Client gửi
complete-uploadkèm manifest. - Backend verify chunk list, checksum, size.
- Backend commit metadata và tạo file version mới.
Điểm quan trọng:
Client không upload file 50GB qua một POST request duy nhất.
Client upload từng chunk trực tiếp lên object storage.
Backend chỉ quản lý metadata, permission, session và commit.
7. Resumable Upload
Vấn đề: upload file 50GB có thể bị rớt mạng giữa chừng. Nếu bắt user upload lại từ đầu thì rất tệ.
Giải pháp: dùng upload session và chunk manifest.
Khi bắt đầu upload:
upload_id = upload_abc123
file = video.mp4
total_parts = 800
chunk_size = 64MB
Client lưu local manifest:
UploadManifest {
upload_id
file_path
file_size
chunk_size
total_parts
parts: [
{ part_number: 1, checksum: h1, status: uploaded },
{ part_number: 2, checksum: h2, status: uploaded },
{ part_number: 3, checksum: h3, status: pending }
]
}
Nếu mạng fail, khi app chạy lại:
- Client load
upload_idtừ local manifest. - Client hỏi backend/object storage: upload session này đã có những part nào?
- Backend trả về list uploaded parts.
- Client compare với manifest.
- Client upload lại only missing parts.
- Sau khi đủ parts, client gọi complete upload.
Không nên nói “hỏi S3 next id là gì”, vì chunk có thể upload song song và out of order. Cách chính xác hơn là:
Client asks which parts are already uploaded, then uploads only missing parts.
Ví dụ:
total parts: 1..800
uploaded parts: 1, 2, 3, 5, 6
missing parts: 4, 7, 8, ..., 800
Client cần retry part 4 trước, không phải cứ upload “next part”.
8. Download File Flow
Download cũng nên dùng object storage và pre-signed URL.
Flow:
- Client gọi backend:
GET /files/{file_id}/download. - Backend check permission.
- Backend lấy
storage_keyhoặc manifest của version hiện tại. - Backend trả về pre-signed download URL hoặc list chunk URLs.
- Client download file trực tiếp từ object storage.
Với file nhỏ, download một object là đủ. Với file lớn hoặc delta sync, client có thể download theo chunk.
9. Detect Local Changes
Sync agent trên desktop/mobile cần phát hiện khi user tạo, sửa, xoá hoặc rename file trong Dropbox folder.
Cách làm:
- Dùng OS file watcher:
- macOS: FSEvents
- Windows: ReadDirectoryChangesW
- Linux: inotify
- Khi OS báo event, agent không upload ngay.
- Agent debounce event để tránh xử lý khi file vẫn đang được app ghi.
- Agent đọc current file state từ local filesystem.
- Agent compare với local index.
- Nếu file thay đổi thật, agent upload change lên cloud.
Flow:
User edits file
↓
OS sends file event
↓
Sync agent receives event
↓
Debounce until file is stable
↓
Read current file state from filesystem
↓
Compare with local index
↓
Upload if changed
↓
Update local index
Current file state là gì?
Current file state là trạng thái thật hiện tại của file trên disk:
path
size
modified_time
file_id / inode
is_deleted
content_hash
Nó nằm ở filesystem của user, ví dụ:
/Users/drake/Dropbox/report.pdf
Local index là gì?
Local index là database local của sync agent, ví dụ SQLite/RocksDB:
~/.dropbox/sync_index.db
Nó lưu trạng thái cuối cùng mà agent biết đã sync:
path
file_id
last_synced_version
last_synced_hash
size
modified_time
sync_status
Ý tưởng:
Current file state = file thật trên disk bây giờ.
Local index = trạng thái lần cuối agent đã sync.
Nếu hai cái khác nhau, nghĩa là file có thay đổi.
10. Discover Cloud Changes
Sync agent cũng cần biết có thay đổi nào xảy ra trên cloud do device khác tạo ra.
Không nên poll toàn bộ danh sách file mỗi lần vì rất tốn kém. Nếu user có 1 triệu file, việc list all rồi compare local index là không scale.
Giải pháp tốt hơn: server cung cấp change feed theo cursor.
API ví dụ:
GET /changes?cursor=abc123
Response:
{
"changes": [
{
"type": "modified",
"file_id": "file_1",
"path": "/docs/a.txt",
"version": "r20"
},
{
"type": "deleted",
"file_id": "file_2",
"path": "/docs/old.txt",
"version": "r21"
},
{
"type": "renamed",
"file_id": "file_3",
"old_path": "/x.txt",
"new_path": "/y.txt",
"version": "r22"
}
],
"next_cursor": "abc124"
}
Client lưu cursor trong local index. Lần sau chỉ hỏi:
Give me changes since my last cursor.
Flow:
Agent starts
↓
Load last sync cursor
↓
Poll cloud change feed
↓
Receive remote changes since cursor
↓
Apply changes to local filesystem
↓
Update local index
↓
Save new cursor
Ngoài polling, hệ thống có thể dùng long polling, websocket hoặc push notification để báo client có changes mới. Nhưng ngay cả khi có push notification, client vẫn nên gọi change feed bằng cursor để tránh mất event.
11. Apply Cloud Changes to Local File System
Khi nhận remote change, agent xử lý theo từng loại:
11.1 Remote create
Nếu cloud có file mới:
- Agent download file/chunks.
- Ghi file vào local Dropbox folder.
- Update local index.
11.2 Remote modify
Nếu cloud có version mới:
- Agent kiểm tra local file có bị sửa riêng không.
- Nếu local không đổi, download version mới và ghi đè local file.
- Nếu local cũng đổi, tạo conflict.
11.3 Remote delete
Nếu cloud delete file:
- Nếu local file không đổi, delete local file.
- Nếu local file đã bị sửa offline, tạo conflict hoặc preserve local copy.
11.4 Remote rename/move
Nếu cloud rename/move:
- Agent rename/move file local.
- Update path trong local index.
Điểm quan trọng:
No conflict => apply remote change directly.
Conflict => preserve both versions / merge / ask user.
Không được silently overwrite local changes vì có thể làm mất dữ liệu user.
12. Conflict Detection
Conflict xảy ra khi local và remote cùng thay đổi từ một base version.
Ví dụ:
Base version: r10
Local file changed from r10
Cloud file changed to r20
Local index biết file lần cuối sync là r10. Nhưng bây giờ:
- Local file hash khác r10.
- Remote version cũng khác r10.
Nghĩa là có conflict.
Cách xử lý phổ biến:
report.docx
report (Drake's conflicted copy).docx
Một version giữ nội dung remote, một version giữ nội dung local. Tùy policy, app có thể:
- Giữ cloud version làm file chính, local version làm conflicted copy.
- Giữ local version làm file chính, cloud version làm conflicted copy.
- Merge tự động nếu là text file và merge được.
- Yêu cầu user resolve nếu file phức tạp.
Nguyên tắc quan trọng:
Never silently discard user data.
13. Delta Sync bằng Chunk Manifest
Khi file lớn chỉ thay đổi một phần nhỏ, không nên download/upload lại toàn bộ file.
Giải pháp: chia file thành chunk và lưu manifest cho từng version.
Remote manifest:
file_id: report.pdf
version: r20
chunks:
part 1: hash_a
part 2: hash_b
part 3: hash_c
Local index:
file_id: report.pdf
version: r10
chunks:
part 1: hash_a
part 2: hash_old
part 3: hash_c
Agent compare hash trong manifest:
part 1: same => skip
part 2: different => download/upload this chunk
part 3: same => skip
Điểm quan trọng:
Compare manifests, not raw chunk content.
Client không download toàn bộ chunks để compare. Client chỉ download remote manifest, vì manifest nhỏ hơn file rất nhiều. Sau đó client chỉ tải chunk missing hoặc changed.
Fixed-size chunk vs content-defined chunking
Nếu dùng fixed-size chunk, ví dụ mỗi chunk 5MB, thì insert 1 byte ở đầu file có thể làm nhiều chunk sau đó bị lệch, dẫn tới nhiều hash thay đổi.
Ví dụ:
Before:
ABCDE | FGHIJ | KLMNO
After insert X at beginning:
XABCD | EFGHI | JKLMN | O
Dù chỉ thêm 1 byte, gần như mọi chunk phía sau thay đổi.
Để tối ưu hơn, có thể dùng content-defined chunking. Cách này cắt chunk dựa trên nội dung, nên khi insert/delete ở giữa file, nhiều chunk cũ vẫn được giữ lại.
Trong interview, có thể nói:
For a simple design, fixed-size chunks are acceptable.
For better delta sync efficiency, we can use content-defined chunking.
14. Metadata Consistency
Một vấn đề quan trọng là metadata và blob data phải nhất quán.
Không được để metadata nói file đã upload thành công nhưng object storage lại thiếu chunk.
Flow an toàn:
- Tạo upload session ở trạng thái
pending. - Client upload chunks lên object storage.
- Client gọi complete upload.
- Backend verify chunks tồn tại và checksum hợp lệ.
- Backend commit metadata trong DB transaction.
- File version chuyển sang
committed.
Trạng thái upload:
pending
uploading
completed
failed
aborted
Nếu upload bị bỏ dở, background cleanup job có thể xoá incomplete chunks sau một thời gian.
15. API Design
Create upload session
POST /upload-sessions
Request:
{
"path": "/videos/movie.mp4",
"file_size": 53687091200,
"chunk_size": 67108864,
"content_hash": "full_file_hash"
}
Response:
{
"upload_id": "upload_abc123",
"chunk_size": 67108864,
"total_parts": 800,
"expires_at": "2026-06-01T00:00:00Z"
}
Get uploaded parts
GET /upload-sessions/{upload_id}/parts
Response:
{
"uploaded_parts": [
{
"part_number": 1,
"etag": "etag_1",
"checksum": "hash_1"
}
]
}
Complete upload
POST /upload-sessions/{upload_id}/complete
Request:
{
"manifest": {
"file_size": 53687091200,
"chunk_size": 67108864,
"total_chunks": 800,
"chunks": [
{
"part_number": 1,
"byte_range": "0-67108863",
"size": 67108864,
"checksum": "hash_1",
"etag": "etag_1"
}
]
}
}
Download file
GET /files/{file_id}/download?version=current
Response:
{
"download_url": "https://object-storage/presigned-url"
}
Get changes
GET /changes?cursor=abc123
Response:
{
"changes": [],
"next_cursor": "abc124",
"has_more": false
}
16. Storage Design
Nội dung file nên lưu trong object storage, không lưu trực tiếp trong database.
Object storage key có thể là:
/chunks/{chunk_hash}
/files/{file_id}/versions/{version_id}
/uploads/{upload_id}/parts/{part_number}
Nếu dùng content-addressed storage theo chunk hash, hệ thống có thể deduplicate chunk giống nhau giữa các file/version.
Ví dụ:
chunk_hash = SHA256(content)
storage_key = /chunks/sha256_abcd
Nếu nhiều file dùng cùng chunk, ta chỉ lưu một lần và reference từ nhiều manifest.
17. Scaling Metadata Service
Metadata service thường là bottleneck vì mọi operation đều cần metadata.
Các cách scale:
- Partition/shard theo
user_idhoặcnamespace_id. - Dùng cache cho hot metadata.
- Tách read/write path nếu cần.
- Dùng database transaction cho operation quan trọng như rename/move.
- Index các field thường query:
owner_idparent_folder_idfile_idpathupdated_at
Vấn đề rename folder lớn:
Nếu folder có hàng triệu child files, update path cho tất cả child ngay lập tức rất nặng. Có thể tránh bằng cách lưu tree theo parent_folder_id thay vì materialized full path. Khi cần hiển thị path, resolve từ parent chain hoặc dùng cached path.
18. Change Feed Design
Mỗi thay đổi metadata nên tạo một event:
ChangeEvent {
event_id
user_id
namespace_id
file_id
type
version
timestamp
}
Client sync bằng cursor:
cursor = last_event_id hoặc encoded checkpoint
Khi client gọi /changes?cursor=..., server trả về các event sau cursor đó.
Yêu cầu:
- Event phải ordered trong cùng namespace.
- Cursor phải durable.
- Client phải xử lý idempotent vì có thể nhận duplicate event.
- Nếu cursor quá cũ, server có thể yêu cầu client resync full metadata tree.
19. Idempotency
Network có thể retry request, nên API cần idempotent.
Ví dụ upload complete có thể bị client gọi 2 lần do timeout. Backend cần đảm bảo không tạo hai version trùng nhau.
Dùng:
idempotency_key
upload_id
client_mutation_id
Ví dụ:
POST /upload-sessions/{upload_id}/complete
Idempotency-Key: abc-xyz
Nếu request lặp lại, backend trả về cùng kết quả như lần đầu.
20. Security
Các điểm bảo mật:
- User phải authenticate bằng token.
- Mọi file operation phải check authorization.
- Pre-signed URL phải có expiry ngắn.
- Object storage không public trực tiếp.
- File nên được encrypt at rest.
- TLS cho network transfer.
- Có rate limit để tránh abuse.
- Virus scanning hoặc malware scanning có thể chạy async.
- Audit log cho sharing và permission changes.
21. Handling Deletes
Delete nên là soft delete trước:
is_deleted = true
deleted_at = timestamp
Lợi ích:
- Cho phép restore.
- Hỗ trợ version history.
- Tránh mất dữ liệu do accidental delete.
- Sync agent có thể truyền delete event xuống devices khác.
Sau retention period, background job có thể hard delete object nếu không còn reference.
22. Sharing Model
Một thiết kế đơn giản:
Permission {
resource_id
resource_type // file/folder
principal_type // user/group/link
principal_id
role // viewer/editor/owner
}
Khi user truy cập file:
- Check owner.
- Check explicit permission.
- Nếu file nằm trong shared folder, inherit permission từ folder.
- Nếu public link enabled, verify link token và expiry.
23. Offline Mode
Sync agent nên hoạt động được khi offline:
- Local edits vẫn được ghi nhận vào local index dưới dạng pending changes.
- Khi online lại, agent upload pending changes.
- Agent cũng poll cloud changes từ last cursor.
- Nếu local và remote cùng thay đổi, tạo conflict.
Local queue:
PendingOperation {
op_id
type
path
base_version
local_hash
status
}
Khi network trở lại:
process pending local changes
poll cloud changes
resolve conflicts
update local index
24. Failure Scenarios
Upload chunk fail
Retry chunk đó với exponential backoff.
Complete upload timeout
Client retry complete request với same idempotency_key.
Metadata committed nhưng event chưa publish
Dùng transactional outbox pattern: metadata write và outbox event nằm trong cùng DB transaction. Background publisher đọc outbox và publish event vào queue.
Client mất event
Client dùng cursor để đọc lại change feed. Push notification chỉ là optimization, không phải source of truth.
Local file đổi trong lúc upload
Agent so sánh size/modified_time/file_id trước và sau khi upload. Nếu file đã đổi, abort upload session cũ và tạo upload session mới cho version mới.
25. Trade-offs
Strong consistency vs eventual consistency
Metadata cần consistency mạnh hơn object delivery. Nhưng sync giữa devices có thể eventual consistent.
Fixed-size chunk vs content-defined chunk
Fixed-size chunk dễ implement hơn nhưng delta sync kém khi insert/delete ở đầu file. Content-defined chunk tối ưu bandwidth hơn nhưng phức tạp và tốn CPU hơn.
Polling vs push notification
Polling đơn giản nhưng tốn tài nguyên. Push nhanh hơn nhưng có thể mất event, nên vẫn cần change feed với cursor.
Store full path vs parent pointer
Full path query nhanh hơn nhưng rename folder lớn khó. Parent pointer rename rẻ hơn nhưng resolve path phức tạp hơn.
All Rights Reserved