Спроектируй Google Drive / Dropbox
Спроектируй сервис синка файлов: режь файлы на контент-хешированные чанки ради дедупа и delta-синка, отдели БД метаданных от блоб-хранилища, синкай изменения на все устройства с версионированием и разрешением конфликтов, пуш-уведомляй, чтобы клиенты тянули лишь изменённое.
Ты правишь один абзац в дизайн-документе на 200 МБ. Наивный клиент синка замечает, что изменилось время модификации файла, и заново загружает все 200 МБ — затем твой ноутбук, твой телефон и твой десктоп каждый скачивают все 200 МБ обратно. Однострочная правка стоила тебе 800 МБ трафика и минуты ожидания. Теперь представь, что два устройства правят файл, пока одно было офлайн: клиент, что синкается последним, молча перезаписывает работу другого, и пользователь теряет полдня. Сервис синка живёт или умирает на двух вопросах, на которые он обязан хорошо отвечать: когда что-то меняется, как мало данных я могу передать, и когда двое меняют одно и то же, как не сожрать чью-то работу? Ответы — режь файл на чанки и двигай лишь различающиеся, и никогда не перезаписывай вслепую — диктуют архитектуру, где байты файла и история файла (его метаданные и история версий) хранятся и синхронизируются как две совершенно разные вещи.
Требования
Что превращает «сохрани файл» в трудную задачу распределённых систем? Спроси себя: что происходит, когда два устройства правят один файл в офлайне, и чего стоит «синк» в байтах при десяти устройствах? Ответы форсируют весь дизайн. Функциональные — пользователь хранит файлы и папки; правки на одном устройстве появляются на всех его других устройствах и у любых соавторов; у файлов есть история версий (восстановить старую); папки можно расшарить конкретным людям на конкретных уровнях прав (просмотр/правка). Жёсткое функциональное требование, спрятанное в «появляются на всех устройствах», — синк: изменение должно распространяться быстро и корректно, даже при офлайн-правках.
Нефункциональные — синк должен быть быстрым и экономным по каналу (крошечная правка не должна двигать весь файл), надёжным (никогда не терять и не портить данные — долговечность священна) и достаточно согласованным, чтобы два устройства сходились к одному состоянию, не отбрасывая молча правки. Чтения (скачивания, листание) доминируют, но записи (правки) часты и должны быть дёшевы. Стоимость хранения важна, ведь пользователи хранят всё и дублируют файлы повсюду.
Два нефункциональных факта, формирующих всё: двигай минимум байт (→ чанкинг и delta-синк) и никогда не теряй правку (→ версионирование и разрешение конфликтов, не перезапись).
Оценка
Пользователи: ~500M зарегистрированных, ~100M дневных активных
Файлы: средний юзер ~50 ГБ хранит; миллиарды файлов всего
Правки: DAU × горстка изменений файлов/день → ~1e8 юзеров × ~5 = ~5e8 правок/день
÷ ~1e5 с ≈ ~5 000 правок/сек в среднем, ~15K пик
Чанк: фикс ~4 МБ чанки → файл 200 МБ ≈ 50 чанков
правка трогает 1–2 чанка, так delta-синк двигает ~8 МБ, а не 200 МБ
Дедуп: тот же файл, расшаренный/скопированный многими, хранит одну физ. копию
→ кросс-юзер дедуп режет хранимые байты в разы для частых файлов
Метаданные: на файл: id, имя, родитель, версия, список чанков, права → пара КБ
миллиарды файлов × пара КБ → терабайты метаданных (шард. реляц./KV)Оценка делает раскол очевидным: блоб-байты — петабайты (объектное хранилище, дедуплицировано), метаданные — терабайты (шардированное транзакционное хранилище). А delta-синк превращает повторную загрузку 200 МБ в передачу 8 МБ чанков — выигрыш канала в 25× на каждой правке. Эти цифры и есть обоснование чанкинга и раскола метаданные/блоб.
Высокоуровневый дизайн
Файл моделируется как метаданные (имя, папка, версия, упорядоченный список хешей чанков, права) плюс набор чанков (сами байты, адресуемые по содержимому). Метаданные живут в шардированной транзакционной базе; чанки — в объектном/блоб-хранилище, ключ — их контент-хеш. Сервис синка плюс сервис уведомлений держат устройства в курсе.
Порядок важен: сначала загрузи чанки, затем коммить новую версию. Закоммить ты метаданные первыми — версия могла бы ссылаться на чанки, которых ещё нет в хранилище, и читатель получил бы висячий указатель. Чанки-затем-метаданные значит, что метаданные всегда указывают на существующие байты — чанки неизменяемы и адресуемы по содержимому, так что повторная загрузка того же чанка — безвредный no-op.
Глубокое погружение: чанкинг, адресация по содержимому и дедуп
Ядро идеи — чанки, адресуемые по содержимому: режь каждый файл на чанки и ключуй каждый чанк хешем его содержимого (например, SHA-256). Эта одна идея покупает три вещи разом. Дедуп: два одинаковых чанка хешируются одинаково, так что хранятся однажды — между версиями, между файлами, даже между пользователями (тот же расшаренный PDF, хранимый тысячей людей, — один физический чанк). Delta-синк: чтобы синкнуть изменение, клиент перечанкивает файл, сравнивает хеши чанков с прошлой версией и грузит лишь чанки с новыми хешами — правка одного абзаца трогает один чанк, так что двигаешь ~4 МБ вместо 200 МБ. Целостность: хеш и есть адрес, так что испорченный чанк обнаружим (его байты больше не совпадают с хешем).
Чанкинг фиксированного размера (каждые 4 МБ) прост, но хрупок: вставь один байт в начало файла, и каждая последующая граница чанка сдвигается, так что каждый хеш чанка меняется и дедуп рушится. Content-defined chunking (границы по rolling-hash, в стиле rsync/Rabin) ставит границы по содержимому, так что вставка меняет лишь тот один чанк, куда она попала — остальные остаются выровнены и дедуп выживает. Компромисс — переменные размеры чанков и больше CPU; многие системы берут фиксированные чанки ради простоты и принимают, что вставки ресинкают больше строго необходимого.
▸Почему это работает
Зачем хранить байты и метаданные в двух разных системах, а не в одной? Потому что у них противоположные паттерны доступа и требования. Метаданные малы, должны запрашиваться и транзакционно обновляться (переименовать, переместить, расшарить, закоммитить версию) и выигрывают от индексов и ACID — родная стихия реляционного/транзакционного хранилища. Чанки велики, неизменяемы, write-once-read-many, никогда не запрашиваются по содержимому и нуждаются в дешёвом долговечном объёмном хранилище с высокой пропускной — родная стихия объектного хранилища. Втиснуть оба в одну систему значит либо база давится петабайтами блобов (медленно, дорого, индексировать их всё равно нельзя), либо объектное хранилище притворяется транзакционным индексом метаданных (а оно не он). Раскол даёт каждому масштабироваться и быть согласованным на своих условиях, и потому «БД метаданных + блоб-хранилище» повторяется в каждом медиа-разборе этого юнита.
Глубокое погружение: разрешение конфликтов и уведомления
Два устройства правят один файл; одно было офлайн. Когда оба синкаются, сервис метаданных видит две новые версии, ветвящиеся от одной родительской версии. Кардинальное правило: никогда не перезаписывай молча. Перезапись last-writer-wins — баг из hook: он съедает чью-то работу. Вместо этого обнаружь расхождение (родитель входящей версии больше не текущая голова) и разреши его: для файлов Drive/Dropbox хранят обе как конфликтные копии (report (Anna's conflicted copy).docx), чтобы решал человек; для структурированных совместных документов operational transforms (OT — алгоритмы преобразования операций для слияния параллельных правок) или CRDT (conflict-free replicated data types — структуры данных с гарантированным безконфликтным слиянием) могут сливать правки автоматически. Цепочка версий (каждая версия записывает своего родителя) и есть то, что вообще делает конфликты обнаружимыми — без неё ты не отличишь перезапись от законной последовательной правки.
Распространение идёт через сервис уведомлений, не опрос. Каждый клиент держит долгоживущее соединение (long-poll или WebSocket); когда файл, интересный клиенту, меняется, сервис пушит лёгкий сигнал «что-то изменилось», и клиент затем тянет дифф метаданных и любые недостающие чанки. Уведомление дёшево и удобно для fan-out (просто пинг); сама передача данных — это delta-синк pull, который ведёт клиент. Это держит путь уведомлений быстрым и почти-stateless, пока тяжёлая работа остаётся на эффективном delta-пути.
▸Частая ошибка
Тонкая, дорогая ошибка: заставлять клиентов опрашивать сервис метаданных («есть изменения с версии N?») с коротким интервалом ради отзывчивости. При 100M устройств, опрашивающих каждые пару секунд, ты порождаешь постоянную приливную волну в основном пустых ответов — сервер тратит ёмкость, отвечая «ничего не изменилось» миллиарды раз. Починка — инвертировать: клиенты держат long-poll/WebSocket соединение, а сервер пушит, когда (и только когда) происходит релевантное изменение. Push превращает N простаивающих устройств в N дешёвых простаивающих соединений вместо N×(1/интервал) впустую запросов в секунду. Опрос — для малого числа устройств, push — на масштабе; а на масштабе Drive у тебя нет выбора, кроме push.
Пользователь правит один абзац в документе на 200 МБ. Как эффективный сервис синка распространяет это и почему?
Два устройства правят один файл; одно было офлайн. Оба теперь синкаются на сервер. Что обязан сделать дизайн?
Каждый чанк ключуется хешем своих же байт, так что два одинаковых чанка — между версиями, файлами или даже разными пользователями — хранятся лишь однажды. Это свойство одной-физической-копии называется _______.
Узкие места и компромиссы
Доминирующий сэкономленный ресурс — канал (delta-синк) и хранилище (дедуп); оба идут от адресации по содержимому, что стоит CPU на клиенте ради хеширования и чанкинга. Фикс vs content-defined чанкинг разменивает простоту на устойчивый к вставкам дедуп. Политика конфликтов разменивает удобство автослияния (OT/CRDT, сложно, формат-специфично) на безопасность конфликтной копии (просто, никогда не неверно, но пасует человеку). Push vs опрос разменивает немного состояния соединения на огромное снижение впустую запросов на масштабе. Глубочайший компромисс — согласованность метаданных: коммит метаданных — точка линеаризации, он должен быть транзакционным (коммит версии случается целиком или никак), даже хотя загрузки чанков — eventually-consistent best-effort. Сделай согласованность слоя метаданных правильно — и слой блобов может быть свободным; переверни их — испортишь данные. Как и в каждом разборе этого юнита: сервис метаданных — где ты тратишь бюджет согласованности, а блоб-хранилище — где ты тратишь байты.
- 01Что даёт адресация чанков по содержимому и зачем делить метаданные и блобы?
- 02Как конфликтные правки обнаруживаются и разрешаются без потери данных?
- 03Зачем пушить уведомления вместо опроса и каков порядок коммита?
Сервис синка файлов строится вокруг двух вопросов: двигать наименьшее число байт на изменении и никогда не терять правку. Ответ на первый — адресация чанков по содержимому: режь каждый файл на чанки с ключом по контент-хешу, что разом даёт дедуп (одинаковые чанки хранятся однажды, между версиями, файлами и пользователями), delta-синк (грузи лишь чанки с новыми хешами, превращая повторную загрузку 200 МБ в передачу ~4 МБ чанков) и целостность (хеш — адрес). Это форсирует центральный структурный раскол: файл — это метаданные (имя, папка, версия, упорядоченный список хешей чанков, права) в транзакционной шардированной БД плюс чанки (неизменяемые байты) в блоб/объектном хранилище — две системы, ведь их паттерны доступа противоположны. Ответ на второй вопрос — цепочка версий плюс правило не-перезаписи: каждая версия записывает родителя, так что расходящийся синк (родитель больше не голова) обнаружим как конфликт и разрешается хранением конфликтной копии (или автослиянием совместных документов через OT/CRDT), а не молчаливым затиранием работы. Изменения распространяются push (long-poll/WebSocket уведомление → клиент тянет дифф), никогда опросом с коротким интервалом, что утопил бы сервер на масштабе устройств. А порядок коммита — чанки первыми, затем версия — держит метаданные указывающими лишь на существующие байты. Как во всём этом юните: сервис метаданных держит твою согласованность, блоб-хранилище держит твои байты. Теперь, когда встретишь баг «файлы рассинхронизировались», ты знаешь, куда смотреть в первую очередь: правильно ли конфликт-обнаружение сравнивает родительскую версию, и успел ли чанк загрузиться до коммита метаданных?
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.