Проектирование Dropbox
Глава 56: Проектирование Dropbox
1. Введение
Dropbox — это облачный сервис синхронизации и хранения файлов, позволяющий пользователям загружать файлы на сервер, синхронизировать их между несколькими устройствами и делиться ими с другими людьми. Запущенный в 2008 году, Dropbox стал стандартом де-факто для персонального и корпоративного облачного хранилища, обслуживая свыше 700 миллионов зарегистрированных пользователей.
Почему Dropbox интересен с точки зрения System Design
- Delta sync: система не пересылает весь файл при изменении — передаются только изменённые блоки, что кардинально снижает трафик и ускоряет синхронизацию
- Conflict resolution: при одновременном редактировании файла на нескольких устройствах система должна корректно обрабатывать конфликты
- Масштаб хранения: миллиарды файлов суммарным объёмом в эксабайты данных
- Desktop client: клиент интегрируется на уровне файловой системы и отслеживает изменения в реальном времени
- Eventual consistency: синхронизация между устройствами требует продуманной модели согласованности
Компании с аналогичными системами
- Google Drive
- Microsoft OneDrive
- Apple iCloud Drive
- Box
- Yandex.Disk
2. Функциональные требования
Core features (must-have)
- Upload/Download файлов — загрузка и скачивание файлов любого размера
- Автоматическая синхронизация — изменения на одном устройстве автоматически отражаются на всех остальных
- Delta sync — передача только изменённых блоков файла, а не всего файла целиком
- Sharing — общий доступ к файлам и папкам с настройкой прав (просмотр, редактирование)
- Version history — хранение истории версий с возможностью отката
- Conflict resolution — обнаружение и разрешение конфликтов при одновременном редактировании
- Уведомления — оповещение об изменениях файлов на других устройствах
Extended features (nice-to-have)
- Selective sync — выбор конкретных папок для синхронизации на каждом устройстве
- Smart Sync (on-demand) — файлы-заглушки, которые скачиваются при обращении
- Offline mode — работа без интернета с последующей синхронизацией
- Поиск — полнотекстовый поиск по имени файла и содержимому
- Sharing links — публичные ссылки с опциональной защитой паролем и сроком действия
- Team spaces — корпоративные рабочие пространства
- Audit log — журнал действий для администраторов
3. Нефункциональные требования
| Параметр | Значение |
|---|---|
| DAU | ~50 миллионов |
| Availability | 99.99% (≈52 минуты даунтайма в год) |
| Latency upload | < 500ms для инициации, скорость передачи зависит от сети |
| Latency sync notification | < 5 секунд от момента изменения до уведомления на другом устройстве |
| Consistency | Eventual consistency между устройствами; strong consistency для метаданных в рамках одного аккаунта |
| Durability | 99.999999999% (11 nines) для хранимых данных |
| Max file size | До 50 GB (для десктоп-клиента) |
| Storage per user | 2 GB (free) — 3 TB (paid) |
4. Back-of-the-envelope estimation
Пользовательская база
- Зарегистрированных пользователей: 700M
- DAU: 50M
- В среднем 3 устройства на активного пользователя
- Одновременных подключений: ~150M
Storage
- Средний объём данных на пользователя: ~5 GB
- Платящих пользователей: ~17M × средние 50 GB = 850 PB
- Бесплатных пользователей: ~683M × средний 1 GB = 683 PB
- Суммарно: ~1.5 EB (эксабайт) raw data
- С учётом репликации (3x): ~4.5 EB
Операции
- Среднее число синхронизаций в день на активного пользователя: ~20
- Total sync operations per day: 50M × 20 = 1 billion
- RPS (sync metadata): ~12,000
- Пиковый RPS: ~36,000 (3x peak)
Bandwidth
- Средний размер дельты при синхронизации: ~500 KB
- Upload traffic: 1B × 500KB / 86400 = ~5.8 GB/s
- Download traffic (3 устройства): ~17.4 GB/s
File chunks
- Средний размер файла: 1 MB
- Размер блока (chunk): 4 MB
- Большинство файлов = 1 chunk
- Крупные файлы (>100MB) разбиваются на десятки блоков
5. High-Level Design
Основные компоненты
┌──────────────────────────────────────────────────────┐
│ КЛИЕНТСКИЕ УСТРОЙСТВА │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Desktop │ │ Mobile │ │ Web │ │
│ │ Client │ │ Client │ │ Client │ │
│ │ │ │ │ │ │ │
│ │ ┌─────────┐ │ │ │ │ │ │
│ │ │ Watcher │ │ │ │ │ │ │
│ │ │ Chunker │ │ │ │ │ │ │
│ │ │ Indexer │ │ │ │ │ │ │
│ │ └─────────┘ │ │ │ │ │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
└─────────┼────────────────┼────────────────┼──────────┘
│ │ │
▼ ▼ ▼
┌───────────────────────────────────────────────────────┐
│ API GATEWAY / LB │
│ (Rate Limiting, Auth, TLS) │
└──────┬──────────────────┬───────────────────┬─────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌───────────────┐ ┌──────────────────┐
│ Metadata │ │ Block │ │ Notification │
│ Service │ │ Service │ │ Service │
│ │ │ │ │ │
│ - File tree │ │ - Upload │ │ - WebSocket / │
│ - Versions │ │ - Download │ │ Long Polling │
│ - Sharing │ │ - Delta sync │ │ - Push changes │
│ - Conflicts │ │ - Dedup │ │ to devices │
└──────┬───────┘ └───────┬───────┘ └────────┬─────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌───────────────┐ ┌─────────────────┐
│ Metadata DB │ │ Block │ │ Message Queue │
│ (MySQL / │ │ Storage │ │ (Kafka) │
│ PostgreSQL)│ │ (S3 / GCS) │ │ │
└──────────────┘ └───────────────┘ └─────────────────┘
Data Flow: загрузка файла
1. Desktop Client: Watcher обнаруживает изменение файла
2. Chunker разбивает файл на блоки по 4 MB
3. Indexer вычисляет hash каждого блока (SHA-256)
4. Client → Metadata Service: "файл X изменился, вот список блоков и их хэши"
5. Metadata Service → Client: "мне нужны блоки [B2, B5]" (остальные уже есть — dedup)
6. Client → Block Service: загружает только новые/изменённые блоки
7. Block Service → Block Storage (S3): сохраняет блоки
8. Client → Metadata Service: "загрузка завершена, commit версию"
9. Metadata Service обновляет метаданные, записывает событие в Message Queue
10. Notification Service получает событие из очереди
11. Notification Service → другие устройства пользователя: "файл X обновлён"
12. Другие устройства → Metadata Service: "дай мне изменения"
13. Другие устройства → Block Service: скачивают только недостающие блоки
14. Другие устройства собирают файл из блоков
6. API Design
Upload API
POST /api/v1/files/upload/init
Authorization: Bearer <token>
Request:
{
"path": "/Documents/report.pdf",
"file_size": 15728640,
"checksum": "sha256:abcdef1234...",
"chunks": [
{
"index": 0,
"size": 4194304,
"hash": "sha256:chunk0hash..."
},
{
"index": 1,
"size": 4194304,
"hash": "sha256:chunk1hash..."
},
{
"index": 2,
"size": 4194304,
"hash": "sha256:chunk2hash..."
},
{
"index": 3,
"size": 3145728,
"hash": "sha256:chunk3hash..."
}
]
}
Response 200:
{
"upload_id": "up_abc123",
"chunks_needed": [1, 3], // только блоки, которых нет в storage (dedup)
"chunks_existing": [0, 2], // уже существуют
"upload_urls": {
"1": "https://blocks.dropbox.com/upload/up_abc123/1?sig=...",
"3": "https://blocks.dropbox.com/upload/up_abc123/3?sig=..."
}
}
PUT /upload/{upload_id}/{chunk_index}
Content-Type: application/octet-stream
Content-Length: 4194304
<binary chunk data>
Response 200:
{
"chunk_index": 1,
"status": "uploaded",
"hash_verified": true
}
POST /api/v1/files/upload/commit
{
"upload_id": "up_abc123"
}
Response 200:
{
"file_id": "f_xyz789",
"version": 42,
"modified_at": "2026-03-29T10:30:00Z"
}
Download API
GET /api/v1/files/{file_id}/metadata?version=latest
Response 200:
{
"file_id": "f_xyz789",
"path": "/Documents/report.pdf",
"size": 15728640,
"version": 42,
"modified_at": "2026-03-29T10:30:00Z",
"chunks": [
{"index": 0, "hash": "sha256:chunk0hash...", "size": 4194304},
{"index": 1, "hash": "sha256:chunk1hash...", "size": 4194304},
{"index": 2, "hash": "sha256:chunk2hash...", "size": 4194304},
{"index": 3, "hash": "sha256:chunk3hash...", "size": 3145728}
]
}
GET /api/v1/blocks/{chunk_hash}
Authorization: Bearer <token>
Response 200:
Content-Type: application/octet-stream
<binary chunk data>
Sync API
POST /api/v1/sync/delta
{
"cursor": "c_last_sync_position_abc",
"device_id": "dev_12345"
}
Response 200:
{
"entries": [
{
"type": "file_modified",
"file_id": "f_xyz789",
"path": "/Documents/report.pdf",
"version": 42,
"chunks_changed": [1, 3]
},
{
"type": "file_added",
"file_id": "f_new456",
"path": "/Photos/vacation.jpg",
"version": 1,
"chunks": [{"index": 0, "hash": "sha256:..."}]
},
{
"type": "file_deleted",
"file_id": "f_old111",
"path": "/temp/draft.txt"
}
],
"cursor": "c_new_sync_position_def",
"has_more": false
}
Sharing API
POST /api/v1/sharing/create_link
{
"file_id": "f_xyz789",
"access_level": "viewer", // viewer | editor
"password": "optional_pass",
"expires_at": "2026-04-29T00:00:00Z"
}
Response 200:
{
"link": "https://www.dropbox.com/s/abc123/report.pdf",
"link_id": "sl_abc123"
}
POST /api/v1/sharing/add_member
{
"folder_id": "fld_123",
"email": "colleague@example.com",
"access_level": "editor"
}
7. Data Model
Выбор баз данных
| Хранилище | Технология | Обоснование |
|---|---|---|
| Метаданные файлов | MySQL (sharded) / PostgreSQL | Реляционная модель для файлового дерева, транзакции для consistency |
| Блоки файлов | Amazon S3 / собственный object storage | Оптимизировано под хранение неизменяемых блобов, 11 nines durability |
| Кэш метаданных | Redis | Быстрый доступ к дереву файлов активных пользователей |
| Очередь событий | Apache Kafka | Надёжная доставка sync-уведомлений, упорядоченность по partition key = user_id |
| Поисковый индекс | Elasticsearch | Полнотекстовый поиск по именам файлов и содержимому |
Entity Relationship
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Users │ │ Namespaces │ │ Devices │
├─────────────────┤ ├──────────────────┤ ├─────────────────┤
│ user_id (PK) │──1:N──│ namespace_id (PK)│ │ device_id (PK) │
│ email │ │ user_id (FK) │ │ user_id (FK) │
│ plan_type │ │ type (personal/ │ │ device_name │
│ storage_quota │ │ team) │ │ os_type │
│ storage_used │ │ root_folder_id │ │ last_sync_cursor│
│ created_at │ └────────┬─────────┘ │ last_seen_at │
└─────────────────┘ │ └─────────────────┘
│
┌───────┴────────┐
│ FileEntries │
├────────────────┤
│ entry_id (PK) │
│ namespace_id │
│ parent_id (FK) │──────── self-referencing
│ name │ (folder hierarchy)
│ type (file/dir)│
│ latest_version │
│ size │
│ content_hash │
│ is_deleted │
│ modified_at │
│ modified_by │
└───────┬────────┘
│ 1:N
┌───────┴────────┐
│ FileVersions │
├────────────────┤
│ version_id (PK)│
│ entry_id (FK) │
│ version_num │
│ size │
│ content_hash │
│ created_at │
│ created_by │
└───────┬────────┘
│ 1:N
┌───────┴────────┐
│ FileChunks │
├────────────────┤
│ chunk_id (PK) │
│ version_id(FK) │
│ chunk_index │
│ chunk_hash │──── ссылка на block в
│ chunk_size │ object storage
│ storage_key │
└────────────────┘
┌──────────────────┐ ┌──────────────────┐
│ SharedLinks │ │ SharedFolders │
├──────────────────┤ ├──────────────────┤
│ link_id (PK) │ │ share_id (PK) │
│ entry_id (FK) │ │ folder_id (FK) │
│ created_by │ │ user_id (FK) │
│ access_level │ │ access_level │
│ password_hash │ │ invited_by │
│ expires_at │ │ accepted_at │
│ visit_count │ └──────────────────┘
└──────────────────┘
┌──────────────────┐
│ BlockStorage │ (логическое представление — физически в S3)
├──────────────────┤
│ block_hash (PK) │ ← content-addressable storage
│ ref_count │ ← количество ссылок (для dedup и GC)
│ size │
│ storage_tier │ ← hot / warm / cold
│ created_at │
└──────────────────┘
SQL Schema (ключевые таблицы)
CREATE TABLE file_entries ( entry_id BIGINT PRIMARY KEY AUTO_INCREMENT, namespace_id BIGINT NOT NULL, parent_id BIGINT REFERENCES file_entries(entry_id), name VARCHAR(512) NOT NULL, entry_type ENUM('file', 'directory') NOT NULL, latest_version INT DEFAULT 1, size BIGINT DEFAULT 0, content_hash CHAR(64), -- SHA-256 is_deleted BOOLEAN DEFAULT FALSE, modified_at TIMESTAMP NOT NULL, modified_by BIGINT NOT NULL, UNIQUE INDEX idx_parent_name (namespace_id, parent_id, name), INDEX idx_namespace_modified (namespace_id, modified_at) ); CREATE TABLE file_versions ( version_id BIGINT PRIMARY KEY AUTO_INCREMENT, entry_id BIGINT NOT NULL REFERENCES file_entries(entry_id), version_num INT NOT NULL, size BIGINT NOT NULL, content_hash CHAR(64) NOT NULL, created_at TIMESTAMP NOT NULL, created_by BIGINT NOT NULL, UNIQUE INDEX idx_entry_version (entry_id, version_num) ); CREATE TABLE file_chunks ( chunk_id BIGINT PRIMARY KEY AUTO_INCREMENT, version_id BIGINT NOT NULL REFERENCES file_versions(version_id), chunk_index INT NOT NULL, chunk_hash CHAR(64) NOT NULL, chunk_size INT NOT NULL, storage_key VARCHAR(256) NOT NULL, UNIQUE INDEX idx_version_index (version_id, chunk_index), INDEX idx_chunk_hash (chunk_hash) ); CREATE TABLE block_refs ( block_hash CHAR(64) PRIMARY KEY, ref_count INT NOT NULL DEFAULT 1, size INT NOT NULL, storage_tier ENUM('hot', 'warm', 'cold') DEFAULT 'hot', created_at TIMESTAMP NOT NULL );
8. Deep Dive в ключевые компоненты
8.1 Delta Sync Engine (Block-Level Synchronization)
Delta sync — это ключевое преимущество Dropbox перед конкурентами. Вместо повторной загрузки всего файла при каждом изменении, система передаёт только изменённые блоки.
Алгоритм работы
Dropbox использует подход, вдохновлённый rsync: файл разбивается на блоки фиксированного размера (4 MB), для каждого блока вычисляется хэш. При изменении файла клиент сравнивает хэши новых блоков с хэшами предыдущей версии и отправляет только отличающиеся блоки.
Для более тонкой работы с изменениями внутри блока используется rolling hash (вариация Rabin fingerprint), позволяющий обнаружить смещение данных и минимизировать число «грязных» блоков:
class DeltaSyncEngine: BLOCK_SIZE = 4 * 1024 * 1024 # 4 MB ROLLING_WINDOW = 1024 # 1 KB window для rolling hash def compute_file_blocks(self, file_path: str) -> list[BlockInfo]: """Разбивает файл на блоки и вычисляет хэши.""" blocks = [] with open(file_path, 'rb') as f: index = 0 while True: data = f.read(self.BLOCK_SIZE) if not data: break block = BlockInfo( index=index, size=len(data), hash=hashlib.sha256(data).hexdigest(), weak_hash=self._rolling_hash(data[:self.ROLLING_WINDOW]) ) blocks.append(block) index += 1 return blocks def compute_delta( self, local_blocks: list[BlockInfo], remote_blocks: list[BlockInfo] ) -> SyncDelta: """Определяет, какие блоки нужно загрузить/скачать.""" remote_hashes = {b.hash: b for b in remote_blocks} local_hashes = {b.hash: b for b in local_blocks} blocks_to_upload = [] blocks_to_download = [] blocks_unchanged = [] for block in local_blocks: if block.hash in remote_hashes: blocks_unchanged.append(block) else: blocks_to_upload.append(block) for block in remote_blocks: if block.hash not in local_hashes: blocks_to_download.append(block) return SyncDelta( upload=blocks_to_upload, download=blocks_to_download, unchanged=blocks_unchanged ) def _rolling_hash(self, data: bytes, base: int = 257, mod: int = 2**61 - 1) -> int: """Rabin fingerprint для обнаружения смещений.""" h = 0 for byte in data: h = (h * base + byte) % mod return h
Content-Addressable Storage и дедупликация
Блоки хранятся по их SHA-256 хэшу — это content-addressable storage. Если два разных пользователя загружают один и тот же файл (или блок), он сохраняется единожды:
Файл "report.pdf" пользователя А:
Block 0: sha256:aaa111... → S3 key: blocks/aa/a1/aaa111...
Block 1: sha256:bbb222... → S3 key: blocks/bb/b2/bbb222...
Block 2: sha256:ccc333... → S3 key: blocks/cc/c3/ccc333...
Файл "report_copy.pdf" пользователя Б (тот же файл):
Block 0: sha256:aaa111... → тот же объект в S3 (ref_count++)
Block 1: sha256:bbb222... → тот же объект в S3 (ref_count++)
Block 2: sha256:ccc333... → тот же объект в S3 (ref_count++)
По данным Dropbox, дедупликация экономит до 60% хранилища.
8.2 Desktop Client Architecture
Десктоп-клиент — это критически важная часть системы. Он работает как фоновый процесс и обеспечивает бесшовную синхронизацию.
Компоненты клиента
┌──────────────────────────────────────────────────────────┐
│ Desktop Client │
│ │
│ ┌────────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ File System │ │ Chunker │ │ Local │ │
│ │ Watcher │ │ & Hasher │ │ Index DB │ │
│ │ │ │ │ │ (SQLite) │ │
│ │ - inotify(Lin) │ │ - Split file │ │ │ │
│ │ - FSEvents(Mac)│ │ - SHA-256 │ │ - File tree │ │
│ │ - ReadDir(Win) │ │ - Rolling │ │ - Block map │ │
│ │ │ │ hash │ │ - Sync state │ │
│ └───────┬────────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Sync Controller │ │
│ │ │ │
│ │ - Orchestrates upload/download │ │
│ │ - Manages sync queue (priority based) │ │
│ │ - Handles conflicts │ │
│ │ - Bandwidth throttling │ │
│ │ - Retry with exponential backoff │ │
│ └──────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌──────────────────────┴───────────────────────────┐ │
│ │ Network Layer │ │
│ │ │ │
│ │ - HTTPS for block transfer │ │
│ │ - WebSocket for notifications │ │
│ │ - Connection pooling │ │
│ │ - Compression (LZ4) │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
File System Watcher
Клиент отслеживает изменения в папке Dropbox через API операционной системы:
class FileSystemWatcher: """Наблюдатель за изменениями в файловой системе.""" def __init__(self, sync_folder: str, sync_controller: SyncController): self.sync_folder = sync_folder self.sync_controller = sync_controller self.debounce_map: dict[str, float] = {} # path → last_event_time self.DEBOUNCE_INTERVAL = 0.5 # секунд def on_file_event(self, event: FileEvent): """Обработка события файловой системы.""" path = event.path now = time.time() # Debouncing: игнорируем частые события для одного файла # (редакторы могут генерировать несколько событий подряд) last_time = self.debounce_map.get(path, 0) if now - last_time < self.DEBOUNCE_INTERVAL: self.debounce_map[path] = now return self.debounce_map[path] = now # Игнорируем временные файлы if self._is_temp_file(path): return if event.type == EventType.CREATED: self.sync_controller.enqueue_upload(path, priority=Priority.NORMAL) elif event.type == EventType.MODIFIED: self.sync_controller.enqueue_upload(path, priority=Priority.NORMAL) elif event.type == EventType.DELETED: self.sync_controller.enqueue_delete(path) elif event.type == EventType.MOVED: self.sync_controller.enqueue_move(event.old_path, event.new_path) def _is_temp_file(self, path: str) -> bool: """Фильтрация временных файлов редакторов.""" temp_patterns = ['.tmp', '~', '.swp', '.partial', '.crdownload'] return any(path.endswith(ext) for ext in temp_patterns)
Local Index (SQLite)
Клиент поддерживает локальную базу для отслеживания состояния синхронизации:
-- Локальная БД на клиенте CREATE TABLE local_files ( path TEXT PRIMARY KEY, entry_id BIGINT, size BIGINT, local_hash TEXT, -- хэш локальной версии remote_hash TEXT, -- хэш серверной версии sync_status TEXT, -- synced / pending_upload / pending_download / conflict local_mtime INTEGER, -- mtime локального файла remote_mtime INTEGER -- mtime серверной версии ); CREATE TABLE local_blocks ( path TEXT, block_index INTEGER, block_hash TEXT, PRIMARY KEY (path, block_index) );
8.3 Conflict Resolution
Конфликты возникают, когда один и тот же файл редактируется одновременно на двух устройствах без связи между ними. Dropbox использует стратегию "last writer wins" + сохранение конфликтной копии.
Алгоритм
class ConflictResolver: def resolve( self, base_version: FileVersion, # общий предок local_version: FileVersion, # локальное изменение remote_version: FileVersion # изменение с сервера ) -> ConflictResolution: # Случай 1: Только одна сторона изменила файл — нет конфликта if local_version.hash == base_version.hash: return ConflictResolution(action="accept_remote", result=remote_version) if remote_version.hash == base_version.hash: return ConflictResolution(action="accept_local", result=local_version) # Случай 2: Обе стороны привели к одинаковому результату if local_version.hash == remote_version.hash: return ConflictResolution(action="no_op", result=local_version) # Случай 3: Настоящий конфликт — обе стороны изменились по-разному # Стратегия: remote версия побеждает, локальная сохраняется как копия conflict_copy_name = self._generate_conflict_name( original_name=local_version.name, device_name=self.device_name, timestamp=local_version.modified_at ) return ConflictResolution( action="conflict", result=remote_version, # основной файл = remote conflict_copy_name=conflict_copy_name # "report (conflicted copy).pdf" ) def _generate_conflict_name(self, original_name: str, device_name: str, timestamp: datetime) -> str: name, ext = os.path.splitext(original_name) date_str = timestamp.strftime("%Y-%m-%d") return f"{name} ({device_name}'s conflicted copy {date_str}){ext}"
Предотвращение конфликтов
Чтобы минимизировать конфликты, Dropbox:
- Быстрые уведомления: push-нотификации через WebSocket доставляются за < 5 секунд
- File locking (Teams): корпоративные пользователи могут заблокировать файл
- Cursor-based sync: каждое устройство хранит курсор — позицию в потоке изменений
- Optimistic locking: при commit версии сервер проверяет, что base_version совпадает с ожидаемой
9. Масштабирование
Шардирование метаданных
Метаданные шардируются по namespace_id (который соответствует пользователю/команде):
Shard key: namespace_id
Shard function: namespace_id % num_shards
Shard 0: namespace_id ∈ {0, 1024, 2048, ...}
Shard 1: namespace_id ∈ {1, 1025, 2049, ...}
...
Shard 1023: namespace_id ∈ {1023, 2047, 3071, ...}
Почему namespace_id, а не user_id:
- Все операции над файлами одного пользователя идут в один шард — нет cross-shard joins
- Shared folders получают свой namespace, что позволяет нескольким пользователям работать на одном шарде
- При росте можно разбить крупные namespaces (команды с миллионами файлов) на sub-shards
Block Storage
┌─────────────┐
│ Upload LB │
└──────┬──────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Block │ │ Block │ │ Block │
│ Server 1 │ │ Server 2 │ │ Server N │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────┐
│ S3 / Object Storage │
│ │
│ Bucket: blocks-{region} │
│ Key: blocks/{hash[0:2]}/{hash} │
│ │
│ - 3x replication within region │
│ - Cross-region replication │
│ - Lifecycle: hot → warm → cold │
└─────────────────────────────────────┘
Block servers выступают как прокси перед S3:
- Сжатие: LZ4 на клиенте перед отправкой (быстрый, подходит для real-time)
- Encryption: AES-256 шифрование каждого блока перед записью в S3
- Deduplication: проверка block_hash перед записью; если блок уже существует — increment ref_count
- Tiering: давно не читаемые блоки переносятся в S3 Glacier (cold storage)
Кэширование
┌────────────────────────────────────────────────────────┐
│ Cache Layers │
│ │
│ L1: Client-side cache (SQLite + local block cache) │
│ Hit rate: ~80% для повторных операций │
│ │
│ L2: Redis cluster (metadata cache) │
│ - File tree cache per namespace │
│ - Recent block hashes (dedup lookup) │
│ - User session / device state │
│ Hit rate: ~95% │
│ │
│ L3: CDN (CloudFront / собственная CDN) │
│ - Popular shared files / public links │
│ - Static assets (web client) │
│ Hit rate: зависит от popularity │
│ │
│ L4: S3 Standard (hot storage) │
│ - Активные блоки │
│ │
│ L5: S3 Glacier (cold storage) │
│ - Блоки старых версий, давно не читаемые │
└────────────────────────────────────────────────────────┘
Caching Strategy для метаданных
class MetadataCache: """Cache-Aside pattern для метаданных файлов.""" def __init__(self, redis: Redis, db: MetadataDB, ttl: int = 300): self.redis = redis self.db = db self.ttl = ttl def get_file_tree(self, namespace_id: int, folder_id: int) -> list[FileEntry]: cache_key = f"ns:{namespace_id}:folder:{folder_id}:tree" cached = self.redis.get(cache_key) if cached: return deserialize(cached) entries = self.db.list_entries(namespace_id, folder_id) self.redis.setex(cache_key, self.ttl, serialize(entries)) return entries def invalidate_folder(self, namespace_id: int, folder_id: int): """Инвалидация при любом изменении в папке.""" cache_key = f"ns:{namespace_id}:folder:{folder_id}:tree" self.redis.delete(cache_key) # Также инвалидируем родительскую папку (размер мог измениться) parent_id = self.db.get_parent_id(namespace_id, folder_id) if parent_id: parent_key = f"ns:{namespace_id}:folder:{parent_id}:tree" self.redis.delete(parent_key)
Notification Service
Для быстрой доставки уведомлений используется WebSocket (desktop/mobile) и Long Polling (web, как fallback):
┌─────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Metadata │────>│ Kafka │────>│ Notification │
│ Service │ │ │ │ Service │
│ (publishes │ │ Topic: │ │ │
│ events) │ │ file_events │ │ - Maintains │
└─────────────┘ │ │ │ WebSocket │
│ Partition │ │ connections │
│ key: │ │ - Routes events │
│ user_id │ │ to devices │
└──────────────┘ └──────────────────┘
Kafka partition key = user_id, что гарантирует упорядоченность событий для каждого пользователя.
10. Trade-offs и альтернативы
Принятые решения
| Решение | За | Против | Почему выбрали |
|---|---|---|---|
| Block-level sync (4 MB chunks) | Экономия bandwidth, дедупликация | Overhead для мелких файлов (метаданные > данных) | Выигрыш на больших файлах перевешивает; мелкие файлы — это один chunk |
| Content-addressable storage | Глобальная дедупликация, иммутабельность | Garbage collection ref_count, privacy concerns | 60% экономии хранилища |
| MySQL для метаданных | Транзакции, зрелость, tooling | Ограничения масштабирования на одном узле | Шардирование по namespace решает проблему масштаба |
| Eventual consistency (sync) | Доступность, partition tolerance | Возможны конфликты | Для файлов допустимо: конфликтная копия — приемлемый UX |
| WebSocket для нотификаций | Низкая латентность (<1s) | Stateful connections, сложность масштабирования | Пользователи ожидают мгновенную синхронизацию |
| Cursor-based sync | Инкрементальная синхронизация, устойчивость к разрывам | Нужно хранить cursor per device | Надёжнее timestamp-based подхода |
Альтернативные подходы
1. File-level sync вместо block-level:
- Проще реализовать
- Подходит для приложений с маленькими файлами (например, конфиг-менеджеры)
- Не подходит, если пользователи работают с файлами > 100 MB
2. CRDT вместо last-writer-wins:
- Подходит для структурированных данных (текстовые редакторы)
- Для бинарных файлов (PDF, ZIP, видео) CRDT неприменим
- Google Docs использует OT/CRDT, но это другой тип данных
3. P2P sync (LAN Sync):
- Dropbox реализовал это как дополнение: устройства в одной сети синхронизируются напрямую
- Снижает нагрузку на серверы и latency
- Сложность: discovery, NAT traversal, security
4. Объектное хранилище собственной разработки vs S3:
- Dropbox начинал на S3, затем мигрировал на собственное решение Magic Pocket в 2016
- Причина: стоимость. При масштабе в эксабайты S3 обходится слишком дорого
- Trade-off: огромные инвестиции в инфраструктуру, но снижение стоимости хранения на ~75%
11. Отказоустойчивость
Single Points of Failure и их устранение
| Компонент | SPOF | Митигация |
|---|---|---|
| Metadata DB | Потеря шарда → недоступность файлов пользователей | MySQL с semi-sync replication (1 primary + 2 replicas), automatic failover через orchestrator |
| Block Storage | Потеря данных | 3x replication в S3, cross-region replication для критичных данных |
| Notification Service | Задержка синхронизации | Stateless service, auto-scaling, fallback на polling при потере WebSocket |
| Kafka | Потеря событий | Replication factor = 3, min.insync.replicas = 2 |
| API Gateway | Полная недоступность | Multi-AZ deployment, DNS failover |
Disaster Recovery
┌──────────────────────────┐ ┌──────────────────────────┐
│ Primary Region │ │ Secondary Region │
│ (US-East-1) │ │ (US-West-2) │
│ │ │ │
│ Metadata DB (primary) │────>│ Metadata DB (replica) │
│ Block Storage │────>│ Block Storage (replica) │
│ Kafka Cluster │────>│ Kafka MirrorMaker │
│ All services (active) │ │ All services (standby) │
│ │ │ │
└──────────────────────────┘ └──────────────────────────┘
RTO (Recovery Time Objective): < 30 минут
RPO (Recovery Point Objective): < 1 минута (async replication lag)
Data Backup Strategy
- Metadata DB: ежедневные full backups + continuous binary log replication → Point-in-Time Recovery
- Block Storage: immutable by design; блоки никогда не перезаписываются. Soft delete с retention 30 дней
- User file versions: по умолчанию хранятся 180 дней (free) / бессрочно (business)
Graceful Degradation
При частичных сбоях система деградирует предсказуемо:
- Metadata DB недоступен: клиент продолжает работать offline, синхронизация возобновляется автоматически
- Block Storage недоступен: метаданные обновляются, загрузка/скачивание ставится в очередь
- Notification Service недоступен: клиент переходит на polling (каждые 30 секунд)
- Один регион упал: трафик перенаправляется через DNS/GSLB в резервный регион
12. Мониторинг и Alerting
Ключевые метрики
Sync Performance
| Метрика | Описание | SLI Target |
|---|---|---|
sync.latency.p50 | Время от изменения файла до появления на другом устройстве | < 5s |
sync.latency.p99 | То же, 99-й перцентиль | < 30s |
sync.conflicts.rate | Частота конфликтов | < 0.01% sync operations |
sync.queue.depth | Глубина очереди синхронизации на клиенте | < 100 items |
Upload / Download
| Метрика | Описание | SLI Target |
|---|---|---|
upload.throughput | Скорость загрузки блоков | > 10 MB/s (серверная сторона) |
upload.error_rate | Процент неудачных загрузок | < 0.1% |
download.latency.ttfb | Time to first byte при скачивании | < 200ms |
dedup.hit_rate | Процент дедуплицированных блоков | мониторинг тренда |
Infrastructure
| Метрика | Описание | Alert Threshold |
|---|---|---|
metadata_db.replication_lag | Отставание реплики | > 5s → warning, > 30s → critical |
kafka.consumer_lag | Отставание consumer от producer | > 10,000 messages → warning |
websocket.active_connections | Текущие WebSocket-соединения | мониторинг capacity |
storage.utilization | Использование дискового пространства | > 80% → warning |
block_storage.ref_count_zero | Блоки без ссылок (для GC) | мониторинг тренда |
SLI / SLO
slos: - name: "File Sync Availability" sli: "successful_sync_operations / total_sync_operations" target: 99.99% window: 30d - name: "Upload Success Rate" sli: "successful_uploads / total_uploads" target: 99.95% window: 30d - name: "Sync Latency" sli: "sync_operations_under_10s / total_sync_operations" target: 99.0% window: 30d - name: "API Availability" sli: "non_5xx_responses / total_responses" target: 99.99% window: 30d
Alerting правила
alerts: - name: "High Sync Failure Rate" condition: "sync.error_rate > 1% for 5 minutes" severity: critical runbook: "Check metadata DB health, block storage availability" - name: "Elevated Sync Latency" condition: "sync.latency.p99 > 60s for 10 minutes" severity: warning runbook: "Check Kafka consumer lag, notification service health" - name: "Metadata DB Replication Lag" condition: "metadata_db.replication_lag > 30s for 2 minutes" severity: critical runbook: "Check primary DB load, network between primary and replica" - name: "Block Storage Error Spike" condition: "block_storage.error_rate > 0.5% for 5 minutes" severity: critical runbook: "Check S3/storage health, switch to backup region if needed" - name: "WebSocket Connection Drop" condition: "websocket.active_connections drops > 20% in 5 minutes" severity: warning runbook: "Check notification service instances, load balancer health"
13. Вопросы для самопроверки
-
Почему Dropbox использует block-level sync, а не file-level? Объясните преимущества и недостатки каждого подхода. Для каких типов файлов file-level sync мог бы быть достаточным?
-
Как работает дедупликация на уровне блоков? Что произойдёт, если два пользователя загрузят один и тот же файл? Какие проблемы с privacy может вызвать глобальная дедупликация?
-
Как вы решите проблему конфликтов при одновременном редактировании? Почему Dropbox не использует CRDT или OT, как Google Docs?
-
Опишите, как клиент определяет, какие блоки нужно загрузить при изменении файла. Что такое rolling hash и зачем он нужен?
-
Как шардировать базу метаданных? Почему
namespace_idлучше подходит в качестве shard key, чемuser_idилиfile_id? -
Почему Dropbox мигрировал с Amazon S3 на собственное хранилище? При каком масштабе имеет смысл строить собственную инфраструктуру хранения?
-
Как обеспечить быстрое уведомление устройств об изменениях? Сравните WebSocket, Long Polling и Server-Sent Events для этой задачи.
-
Что произойдёт, если клиент был офлайн 2 недели? Опишите процесс ресинхронизации. Как cursor-based подход помогает?
-
Как реализовать Selective Sync и Smart Sync? Какие изменения в архитектуре клиента потребуются для Smart Sync (on-demand файлы)?
-
Как Dropbox обеспечивает безопасность данных? Опишите шифрование at-rest и in-transit, управление ключами и модель угроз.
14. Дополнительные ресурсы
Papers и технические статьи
- "Scaling the Dropbox sync engine" — статья инженеров Dropbox о переписывании sync engine на Rust (2020)
- "The rsync algorithm" — Andrew Tridgell, 1996. Основа для delta sync
- "Dropbox's migration from AWS to its own infrastructure" — Akhil Gupta, Dropbox Tech Blog, 2016
Блоги
- Dropbox Tech Blog — официальный технический блог с deep dive в архитектуру
- "How we migrated Dropbox from Nginx to Envoy" — Dropbox Tech Blog, 2020
- "Rewriting the heart of our sync engine" — Isaac Goldberg, Dropbox Tech Blog, 2020 (миграция с Python на Rust)
- "Magic Pocket" — серия статей о собственном object storage Dropbox
Talks
- "Scaling Dropbox" — Rajiv Eranki, Dropbox, QCon (раннее масштабирование с 0 до 100M пользователей)
- "Building Magic Pocket" — James Cowling, Dropbox, @Scale Conference 2016
- "How Dropbox rewrote its sync engine" — Isaac Goldberg, Strange Loop 2020
Open Source
- rsync — утилита и алгоритм инкрементальной синхронизации
- Syncthing — open source P2P sync, альтернативная архитектура (без центрального сервера)
- Minio — open source S3-совместимое объектное хранилище
- SeaweedFS — распределённая файловая система, вдохновлённая GFS
В следующей главе мы рассмотрим проектирование Google Drive — систему, которая решает похожие задачи, но с акцентом на real-time collaboration и интеграцию с экосистемой Google Workspace.