Разборы задач

Проектирование Dropbox

Попробуйте решить эту задачу самостоятельно

Практикуйтесь с интерактивными подсказками и моментальной обратной связью

Перейти на Premium

Постановка задачи

☁️ Что такое Dropbox?

Dropbox - это облачный сервис, позволяющий пользователям хранить и обмениваться файлами. Он предоставляет безопасный и надежный способ хранения и доступа к файлам откуда угодно, с любого устройства.

Функциональные требования

Основные требования

  1. Пользователи могут загрузить файл с любого устройства.
  2. Пользователи могут скачать файл с любого устройства.
  3. Пользователи могут делиться файлами с другими пользователями и просматривать файлы, которыми поделились с ними.
  4. Файлы синхронизируются между устройствами.

За рамками задачи

  • Пользователи могут редактировать файлы
  • Пользователи могут просматривать файлы без скачивания

Стоит отметить, что существуют задачи System Design, касающиеся самого хранилища больших бинарных (blob) объектов. Это выходит за рамки данной задачи, но вы можете самостоятельно изучить этот вопрос, чтобы понять, как работает и как устроено хранилище blob-объектов.

Нефункциональные требования

  1. Система должна обладать высокой доступностью (приоритет доступности над согласованностью данных).
  2. Система должна поддерживать файлы размером до 50 ГБ.
  3. Система должна быть безопасной и надежной. Должна существовать возможность восстанавливать файлы в случае их потери или повреждения.
  4. Система должна обеспечивать максимально быструю загрузку, скачивание и синхронизацию.

За рамками задачи

  • Система должна иметь ограничение на объем, доступный каждому пользователю
  • Система должна поддерживать версионирование файлов
  • Система должна сканировать файлы на наличие вирусов и вредоносных программ

Вот как это может выглядеть на доске:

Нефункциональные требования

Многие кандидаты испытывают трудности с компромиссом, связанным с теоремой CAP, при решении этой задачи. Помните, что приоритет согласованности над доступностью вы отдаете только в том случае, если каждое чтение должно получать самую последнюю запись, иначе система неработоспособна. Например, в приложении для торговли акциями, если пользователь покупает акции T в Москве, а затем другой пользователь немедленно пытается купить акции T в Мехико, вам необходимо убедиться, что первая транзакция была реплицирована в Мехико, прежде чем вы сможете продолжить. Однако для файловой системы, такой как Dropbox, допустимо, если пользователь в Москве загружает файл, а пользователь в Мехико не может его увидеть в течение нескольких секунд.

Подготовка

Планирование подхода

Прежде чем переходить к проектированию системы, важно на секунду остановиться и продумать стратегию. К счастью, для "продуктовых" задач план обычно простой: последовательно собирать дизайн, проходя по функциональным требованиям одно за другим. Так вы сохраните фокус и не утонете в деталях.

Когда функциональные требования удовлетворены, используйте нефункциональные требования, чтобы определить направления для погружения в детали, где это необходимо.

Проектирование API

Начнем с определения основных сущностей, это поможет спроектировать API. Пока не обязательно знать каждое поле или колонку, но если у вас уже есть представление о том, что там будет, можно это записать.

В случае Dropbox основные сущности предельно просты:

  1. File: исходные данные, которые пользователи будут загружать, скачивать и которыми будут делиться.
  2. FileMetadata: метаданные, связанные с файлом. Они включают такую ​​информацию, как имя файла, размер, MIME-тип и пользователь, загрузивший его.
  3. User: пользователь нашей системы.

В реальном интервью короткого списка, как выше, часто достаточно. Главное - проговорить сущности с интервьюером и убедиться, что вы оба одинаково их понимаете.

При проектировании ваша задача проста: создать систему, отвечающую всем функциональным и нефункциональным требованиям. Для этого мы рекомендуем начать с удовлетворения функциональных требований, а затем постепенно добавлять нефункциональные. Это поможет вам сосредоточиться и не запутаться в деталях по ходу работы.

API - основной интерфейс, через который пользователи взаимодействуют с системой. Его полезно определить с самого начала, поскольку он направляет high-level дизайн. Обычно нам нужен один эндпоинт на каждое функциональное требование.

Для загрузки файла у нас может быть эндпоинт примерно такого вида:

POST /files
{
  File,
  FileMetadata
}

Для скачивания файла мы можем использовать эндпоинт:

GET /files/{fileId} -> File & FileMetadata

Имейте в виду, что ваш API может меняться или развиваться по мере проектирования. В данном случае API для загрузки и скачивания значительно эволюционируют, поскольку мы взвешиваем компромиссы различных подходов в нашем high-level дизайне (подробнее об этом позже). Вы можете заранее сообщить об этом интервьюеру, сказав: "Я собираюсь описать несколько простых API эндпоинтов, но, возможно, вернусь к ним и улучшу их по мере того, как мы будем углубляться в проектирование".

Для обмена файлами мы можем использовать следующий эндпоинт:

POST /files/{fileId}/share
{
  User[] // Пользователи, с которыми поделились
}

Наконец, нам нужен способ, позволяющий клиентам запрашивать информацию об изменениях в файлах на сервере. Таким образом, мы будем знать, какие файлы необходимо синхронизировать с локальным устройством.

GET /files/{fileId}/changes -> FileMetadata[]

В каждом из этих запросов информация о пользователе передается в заголовках (через session token или auth token). Это распространенный паттерн, так мы можем обеспечивать аутентификацию/авторизацию и безопасность. Не стоит передавать пользовательские данные в теле запроса: в этом случае их можно легко подделать.

API и основные сущности

Высокоуровневый дизайн

1. Пользователи могут загружать файлы с любого устройства

Основное требование к такой системе, как Dropbox, - это возможность загрузки файлов пользователями. При хранении файлов необходимо учитывать два момента:

  1. Где мы храним содержимое файла (бинарные данные)?
  2. Где мы храним метаданные файла?

Для метаданных мы можем использовать NoSQL-базу данных, например DynamoDB. DynamoDB - это полностью управляемая NoSQL-база данных, предоставляемая AWS. Наши метаданные слабо структурированы, с небольшим количеством связей, а основной шаблон запроса

  • получение файлов по пользователю. Это делает DynamoDB хорошим выбором, но не слишком зацикливайтесь на правильном выборе на собеседовании. В действительности, SQL-база данных, такая как PostgreSQL, подошла бы для этого случая не хуже. Узнайте больше о том, как выбрать правильную базу данных (и почему это может не иметь значения), здесь.

Наша схема будет представлять собой простой документ и может быть примерно такой:

{
  "id": "12",
  "name": "file.txt",
  "size": 2000,
  "mime_type": "text/plain",
  "uploaded_by": "user1"
}

Что касается способа хранения самого файла, у нас есть несколько вариантов. Давайте рассмотрим компромиссы каждого из них.

Паттерн: Работа с большими файлами
Загрузка напрямую с использованием предварительно подписанных URL-адресов - это классический пример эффективного перемещения больших файлов. Этот паттерн обхода серверов приложений для передачи данных, использования подписанных URL-адресов для обеспечения безопасности и реализации фрагментированной (chunked) загрузки для надежности встречается во многих распределенных системах, которые обрабатывают загрузку и скачивание больших файлов.
Подробнее о паттерне

2. Пользователи могут скачать файл с любого устройства

Следующий шаг - обеспечить пользователям возможность скачивать сохраненные файлы. Как и в случае с загрузкой, существует несколько разных способов решения этой задачи.

3. Пользователи могут делиться файлами с другими пользователями

Для завершения функциональных требований нам необходимо обеспечить поддержку обмена файлами с другими пользователями. Мы реализуем это аналогично Google Drive, где вам нужно всего лишь ввести адрес электронной почты пользователя, с которым вы хотите поделиться файлом. Можно предположить, что пользователи уже авторизованы.

Главный вопрос на собеседовании здесь - как сделать этот процесс быстрым и эффективным. Давайте разберемся.

4. Файлы синхронизируются между устройствами

Наконец, нам нужно убедиться, что файлы автоматически синхронизируются между различными устройствами. В общих чертах это работает за счет хранения копии определенного файла на каждом клиентском устройстве (локально), а также в удаленном хранилище (т.е., в "облаке"). Таким образом, нам нужно синхронизировать файлы в двух направлениях:

  1. Локально -> Удаленно
  2. Удаленно -> Локально

Локально -> Удаленно

Когда пользователь обновляет файл на своем локальном компьютере, нам необходимо синхронизировать эти изменения с сервером. Мы считаем удаленный сервер источником истины, поэтому важно как можно быстрее обеспечить согласованность данных, чтобы другие локальные устройства могли знать о необходимости подхватить изменения.

Для этого нам нужен агент синхронизации на стороне клиента, который:

  1. Отслеживает изменения в локальной папке Dropbox, используя события файловой системы, специфичные для операционной системы (например, FileSystemWatcher в Windows или FSEvents в macOS).
  2. При обнаружении изменений агент ставит измененный файл в очередь на отправку.
  3. Затем агент использует наш API для загрузки файлов, чтобы отправить изменения на сервер вместе с обновленными метаданными.
  4. Конфликты разрешаются с использованием стратегии "последняя запись побеждает"
    • это означает, что если два пользователя редактируют один и тот же файл, будет сохранена последняя внесенная ими правка.

В данной статье не рассматривается вопрос версионирования, но следует отметить, что обычно не следует перезаписывать единственный файл. Вместо этого следует добавить новый файл (или, по крайней мере, новые фрагменты) и обновить номер версии и указатель в метаданных.

Удаленно -> Локально

В обратном направлении каждому клиенту необходимо знать, когда происходят изменения на удаленном сервере, чтобы он мог загрузить эти изменения.

Существует два основных подхода, которые мы могли бы использовать:

  1. Опрос (Polling): клиент периодически спрашивает у сервера: "Что-нибудь изменилось с момента последней синхронизации?" Сервер обращается к базе данных, чтобы проверить, есть ли у каких-либо файлов, за которыми следит пользователь, метка времени updated_at, более новая, чем время последней синхронизации. Это простой метод, но он может медленно обнаруживать изменения и расходовать ресурсы, если ничего не изменилось.

  2. WebSocket или SSE: сервер поддерживает открытое соединение с каждым клиентом и отправляет уведомления при возникновении изменений. Это более сложный подход, но он обеспечивает обновления в режиме реального времени.

Для Dropbox можно использовать гибридный подход. Мы можем разделить файлы на две категории:

Новые файлы: файлы, которые были недавно отредактированы (в течение последних нескольких часов). Для них мы поддерживаем соединение WebSocket, чтобы обеспечить синхронизацию практически в реальном времени.

Обычные файлы: файлы, которые давно не редактировались. Для них мы можем использовать периодический опрос, поскольку немедленные обновления менее критичны.

Этот гибридный подход позволяет нам получить лучшее из обоих миров: обновления активно изменяющихся файлов в режиме реального времени и экономию ресурсов для остальных.

Синхронизация файлов

Итоговый дизайн

Давайте сделаем шаг назад и посмотрим на нашу систему в целом. На данном этапе у нас есть простой дизайн, который удовлетворяет всем нашим функциональным требованиям.

Итоговый дизайн

Клиент загрузки: клиент, который отправляет файлы. Это может быть веб-браузер, мобильное приложение или настольное приложение. Он также отвечает за мониторинг локальных изменений и отправку обновлений в удаленное хранилище.

Клиент скачивания: клиент, который скачивает файлы. Конечно, это может быть тот же клиент, что и клиент загрузки, но это необязательно. В нашей архитектуре мы разделяем их для большей ясности. Он также отвечает за определение того, когда файл, находящийся у него локально, изменился на удаленном сервере, и скачивает эти изменения.

Балансировщик нагрузки и API-шлюз: отвечает за маршрутизацию запросов к соответствующему серверу и обработку таких операций, как завершение SSL-соединения, ограничение скорости и проверка запросов.

Файловый сервис: отвечает за запись в базу данных метаданных файлов, а также за генерацию предварительно подписанных URL-адресов с использованием SDK S3. Он фактически не обрабатывает загрузку или скачивание файлов. Это всего лишь посредник между клиентом и S3.

База данных метаданных: хранит такие данные, как имя файла, размер, MIME-тип и пользователь, загрузивший файл. Здесь же хранится таблица общих файлов, которая сопоставляет файлы с пользователями, имеющими к ним доступ. Мы используем эту таблицу для обеспечения соблюдения прав доступа при попытке пользователя скачать файл.

S3: здесь фактически хранятся файлы. Мы загружаем и скачиваем файлы напрямую в S3 и из S3, используя предварительно подписанные URL-адреса, которые получаем от файлового сервера.

CDN: кэширует файлы вблизи пользователя для уменьшения задержки.

Потенциальные погружения в детали

1. Как поддержать большие файлы?

Первое, на что следует обратить внимание при работе с большими файлами - это пользовательский опыт. Два ключевых момента должны быть особенно важны и в конечном итоге определять ваш дизайн:

  1. Индикатор выполнения: пользователи должны иметь возможность видеть ход загрузки, чтобы понимать, что она выполняется и сколько времени это займет.

  2. Возобновляемая загрузка: пользователи должны иметь возможность приостанавливать и возобновлять загрузку. В случае потери интернет-соединения или закрытия браузера они должны иметь возможность продолжить с того места, где остановились, вместо того, чтобы повторно загружать 49 ГБ, которые, возможно, уже были загружены до прерывания.

В каком-то смысле, в этом и заключается суть задачи Dropbox, и именно на это обычно тратится больше всего времени при общении с кандидатами на реальном собеседовании.

Прежде чем углубляться в решения, давайте на мгновение остановимся на ограничениях, связанных с загрузкой большого файла с помощью одного POST запроса.

  • Таймауты: веб-серверы и клиенты обычно имеют настройки таймаутов, чтобы предотвратить бесконечное ожидание ответа. Один POST запрос с файлом размером 50 ГБ может легко превысить эти таймауты. На самом деле, это может быть подходящим моментом для быстрых подсчетов на собеседовании. Если у нас есть файл размером 50 ГБ и интернет-соединение со скоростью 100 Мбит/с, сколько времени потребуется для загрузки файла? 50 ГБ * 8 бит/байт / 100 Мбит/с = 4000 секунд, тогда 4000 секунд / 60 секунд/минута / 60 минут/час = 1,11 часа. Это очень долгое время ожидания без ответа от сервера.

  • Ограничения браузера и сервера: в большинстве случаев загрузка файла размером 50 ГБ с помощью одного POST запроса невозможна в принципе из-за ограничений, которые браузеры и веб-серверы часто устанавливают на размер тела запроса. Хотя веб-серверы, такие как Apache и NGINX, могут быть настроены на прием больших объемов данных, большинство современных сервисов, таких как Amazon API Gateway, имеют жесткие ограничения, которые намного ниже и не могут быть увеличены. В случае с Amazon API Gateway, это всего 10 МБ.

  • Сетевые сбои: большие файлы более подвержены сетевым сбоям. Если пользователь загружает файл размером 50 ГБ, и его интернет-соединение обрывается, ему придется начинать загрузку заново.

  • Пользовательский опыт: пользователи фактически не видят хода загрузки. Они понятия не имеют, сколько времени это займет или идет ли вообще процесс.

Для решения этих проблем мы можем использовать метод, называемый "разбивкой на куски" (chunking), чтобы разбить файл на более мелкие фрагменты и загружать их по одному (или параллельно, в зависимости от пропускной способности сети). Распространенная ошибка, которую допускают кандидаты, - это разбивка файла на куски на сервере, в чем фактически нет смысла, поскольку для этого все равно загружается весь файл целиком. Поэтому разбивка должна выполняться на стороне клиента. Обычно мы разбиваем файл на фрагменты размером 5-10 МБ, но это можно скорректировать в зависимости от условий сети и размера файла.

При работе с фрагментами нам довольно просто показать пользователю индикатор выполнения. Мы можем просто отслеживать прогресс каждого фрагмента и обновлять индикатор выполнения по мере успешной загрузки фрагментов.

Следующий вопрос: как мы будем обрабатывать возобновляемые загрузки? Нам нужно отслеживать, какие фрагменты были загружены, а какие нет. Мы можем сделать это, сохраняя состояние загрузки в базе данных, а именно в нашей таблице FileMetadata. Давайте обновим схему FileMetadata , добавив поле chunks.

{
  "id": "12",
  "name": "file.txt",
  "size": 2000,
  "mimeType": "text/plain",
  "uploadedBy": "user1",
  "status": "uploading",
  "chunks": [
    {
      "id": "chunk1",
      "status": "uploaded"
    },
    {
      "id": "chunk2",
      "status": "uploading"
    },
    {
      "id": "chunk3",
      "status": "not-uploaded"
    }
  ]
}

Когда пользователь возобновляет загрузку, мы можем проверить поле "chunks" , чтобы увидеть, какие фрагменты уже загружены, а какие нет. Затем мы можем начать загрузку тех фрагментов, которые еще не были загружены. Таким образом, пользователю не придется начинать загрузку заново, если он потеряет интернет-соединение или закроет браузер.

Но как нам обеспечить синхронизацию поля chunks с фактически загруженными фрагментами файла?

Мы можем использовать два подхода:

Далее поговорим о том, как однозначно идентифицировать файл и его фрагмент. Когда вы пытаетесь возобновить загрузку, первый вопрос, который следует задать, это: (1) Пытались ли мы загрузить этот файл раньше? и (2) Если да, то какие фрагменты уже загружены? Чтобы ответить на первый вопрос, мы не можем наивно полагаться на имя файла. Это связано с тем, что два разных пользователя (или даже один и тот же пользователь) могут загружать файлы с одинаковым именем. Вместо этого нам нужно полагаться на уникальный идентификатор, полученный из содержимого файла. Это называется отпечатком (fingerprint).

Отпечаток - это результат математического вычисления, которое генерирует уникальное хеш-значение на основе содержимого файла. Это хеш-значение, часто создаваемое с помощью криптографических хеш-функций, таких как SHA-256, служит надежным и уникальным идентификатором файла независимо от его имени или источника загрузки. Вычислив этот отпечаток, мы можем быстро и достоверно определить, был ли файл или какая-либо его часть загружены ранее.

Для возобновляемой загрузки процесс включает в себя не только создание отпечатков всего файла, но и генерацию отпечатков для каждого отдельного фрагмента. Создание отпечатков на уровне фрагментов позволяет системе точно определить, какие части файла уже были переданы.

Давайте свяжем все воедино. Вот что происходит, когда пользователь загружает большой файл:

  1. Клиент разбивает файл на части размером 5-10 МБ и вычисляет отпечаток для каждой части. Он также вычисляет отпечаток для всего файла, который станет идентификатором файла (fileId).
  2. Клиент отправляет GET запрос для получения FileMetadata с заданным fileId (отпечатком), чтобы проверить, существует ли он уже - в этом случае мы сможем возобновить загрузку.
  3. Если файл не существует, клиент отправляет POST запрос для инициирования многокомпонентной загрузки. Бэкенд вызывает S3 API CreateMultipartUpload, чтобы получить uploadId, генерирует предварительно подписанные URL-адреса для каждой части, сохраняет метаданные файла в таблице FileMetadata со статусом "uploading" и возвращает uploadId вместе с предварительно подписанными URL-адресами для каждого фрагмента.
  4. Затем клиент загружает каждый фрагмент в S3, используя соответствующий предварительно подписанный URL-адрес (для каждой части требуется свой собственный предварительно подписанный URL-адрес с идентификатором загрузки uploadId и номером части partNumber). После загрузки каждого фрагмента клиент отправляет PATCH запрос в наш бэкенд со статусом фрагмента и ETag. Затем наш бэкенд может проверить загрузку фрагментов с помощью S3 API ListParts, прежде чем обновить поле chunks в таблице FileMetadata, и помечает фрагмент как "uploaded".
  5. Как только все фрагменты в нашем массиве фрагментов будут помечены как "uploaded", бэкенд обновляет таблицу FileMetadata и помечает весь файл как "uploaded".

На протяжении всего процесса клиент несет ответственность за отслеживание хода загрузки и соответствующее обновление пользовательского интерфейса, чтобы пользователь знал, на каком этапе он находится и сколько времени это займет.

Описанный нами подход не нов, на самом деле, эта проблема уже решена поставщиками облачных хранилищ, такими как Amazon S3. У них есть функция Multipart Upload, которая позволяет загружать большие объекты по частям. Это именно то, что мы только что описали. Клиент разбивает файл на части и загружает каждую часть в S3. Затем S3 объединяет части в один объект. Они даже предоставляют удобный JavaScript SDK, который будет обрабатывать всю разбивку на части и загрузку за вас.

При загрузке нескольких частей в S3 уведомления о событиях срабатывают только после завершения всей загрузки (когда все части собраны), а не при загрузке отдельных частей. Для отслеживания прогресса загрузки отдельных частей необходимо использовать API ListParts в S3, который возвращает все загруженные части с их ETags для текущей загрузки.

На практике вы будете полагаться на этот API при проектировании таких систем, как Dropbox. Однако, скорее всего, на собеседовании вы не сможете просто сказать: "Я бы использовал S3 Multipart Upload API", не сумев объяснить, как он работает и как бы вы сами его реализовали, если бы это потребовалось. Тем не менее, сообщить интервьюеру о том что вы знаете про существующее готовое решение - хорошая идея, поскольку это демонстрирует практический опыт.

Поддержка больших файлов

2. Как можно максимально ускорить загрузку, скачивание и синхронизацию данных?

Мы уже обсудили несколько способов ускорения загрузки и скачивания, но есть еще кое-что, что можно сделать. Напомним, для скачивания мы использовали CDN для кэширования файла ближе к пользователю. Это позволило сократить расстояние, которое файл должен преодолевать до пользователя, уменьшив задержку и ускорив время скачивания. Для загрузки, помимо удобства возобновления, значительную роль в ускорении процесса играет разбиение на части. Хотя пропускная способность фиксирована, мы можем использовать разбиение, чтобы максимально эффективно использовать имеющуюся пропускную способность. Отправляя несколько фрагментов параллельно и используя адаптивные размеры фрагментов в зависимости от состояния сети, мы можем максимально использовать доступную пропускную способность. Аналогичный подход с разбиением на части можно использовать для синхронизации файлов - при изменении файла мы можем определить, какие части изменились, и синхронизировать только эти части, а не весь файл целиком, что значительно ускоряет синхронизацию.

Помимо уже обсужденного, мы также можем использовать сжатие для ускорения как загрузки, так и скачивания. Сжатие уменьшает размер файла, а значит, требуется передать меньше байтов. Поскольку мы загружаем файлы непосредственно в S3, сжатие происходит полностью на стороне клиента: клиент сжимает файл перед загрузкой, и сжатые данные хранятся в S3 в неизмененном виде. При скачивании клиент распаковывает файл после его получения.

Однако нам нужно разумно подходить к вопросу сжатия. Сжатие полезно только в том случае, если выигрыш в скорости от передачи меньшего количества байтов перевешивает время, необходимое для сжатия и распаковки файла. Для некоторых типов файлов, особенно медиафайлов, таких как изображения и видео, коэффициент сжатия настолько низок, что время, затрачиваемое на сжатие и распаковку файла, не оправдывает себя. Если вы прямо сейчас возьмете файл .png и сожмете его, вам повезет, если размер файла уменьшится более чем на несколько процентов - поэтому это не стоит того. С другой стороны, для текстовых файлов коэффициент сжатия намного выше, и в зависимости от условий сети это вполне может быть выгодно. Текстовый файл размером 5 ГБ может быть сжат до 1 ГБ или даже меньше в зависимости от содержимого.

В конечном итоге вам потребуется реализовать на стороне клиента логику, которая будет определять, следует ли сжимать файл перед загрузкой, исходя из типа файла, его размера и состояния сети.

Алгоритмы сжатия

Существует ряд алгоритмов сжатия файлов. Наиболее распространенными являются Gzip, Brotli и Zstandard. Каждый из этих алгоритмов имеет свои компромиссы с точки зрения степени сжатия и скорости. Gzip является наиболее распространенным и поддерживается всеми современными браузерами. Brotli - более новый алгоритм с более высокой степенью сжатия, чем Gzip, но он поддерживается не всеми браузерами. Zstandard - самый новый алгоритм с самой высокой степенью сжатия и скоростью, но он также поддерживается не всеми браузерами. Вам нужно будет выбрать алгоритм, исходя из ваших конкретных условий.

Важный момент, касающийся сжатия, заключается в том, что в случаях, когда шифрование необходимо, всегда следует сжимать файл перед шифрованием. Это связано с тем, что шифрование естественным образом вносит в файл случайность, что затрудняет его сжатие. Сжатие перед шифрованием позволит добиться гораздо более высокого коэффициента сжатия.

3. Как можно обеспечить безопасность файлов?

Безопасность - важнейший аспект любой системы хранения файлов. Необходимо обеспечить защиту файлов и доступ к ним только авторизованным пользователям.

  1. Шифрование при передаче: конечно, для большинства кандидатов это очевидно. Мы должны использовать HTTPS для шифрования данных при их передаче между клиентом и сервером. Это стандартная практика, поддерживаемая всеми современными браузерами.

  2. Шифрование при хранении: мы также должны шифровать файлы, когда они хранятся в S3. Это встроенная функция S3, и ее легко включить. Когда файл загружается в S3, мы можем указать, что он должен быть зашифрован. Затем S3 шифрует файл с помощью уникального ключа и сохранит ключ отдельно от файла. Таким образом, даже если кто-то получит доступ к файлу, он не сможет расшифровать его без ключа. Подробнее о шифровании в S3 можно узнать здесь.

  3. Контроль доступа: наш список общего доступа (sharelist) или отдельная таблица/кэш общего доступа - это наш базовый ACL (Access Control List, список контроля доступа). Как обсуждалось ранее, мы гарантируем, что предоставляем ссылки для скачивания только авторизованным пользователям.

Но что произойдет, если авторизованный пользователь поделится ссылкой для скачивания с неавторизованным пользователем? Например, авторизованный пользователь может, намеренно или непреднамеренно, опубликовать ссылку для скачивания на общедоступном форуме или в социальных сетях, и нам необходимо убедиться, что неавторизованные пользователи не смогут скачать этот файл.

Здесь снова вступают в игру подписанные URL-адреса, о которых мы говорили ранее. Когда пользователь запрашивает ссылку для скачивания, мы генерируем подписанный URL-адрес, действительный только в течение короткого периода времени (например, 5 минут). Затем этот подписанный URL-адрес отправляется пользователю, который может использовать его для загрузки файла. Стоит отметить, что подписанные URL-адреса являются токенами "на предъявителя" (bearer token) - любой, у кого есть действительный, непросроченный URL-адрес, может загрузить файл. Короткий срок действия ограничивает уязвимость, но не полностью предотвращает распространение. Для более строгих сценариев безопасности можно добавить дополнительные ограничения, такие как привязка к IP-адресу, или потребовать использования подписанного URL-адреса в сочетании с аутентификационными файлами cookie.

Подписанные URL-адреса также работают с современными CDN, такими как CloudFront, и являются функцией S3. Вот как это работает:

  1. Генерация: на сервере генерируется подписанный URL-адрес, включающий подпись, которая обычно содержит путь к URL-адресу, метку времени истечения срока действия и, возможно, другие ограничения (например, IP-адрес). В случае CloudFront эта подпись создается с использованием закрытого ключа поставщика контента.
  2. Распространение: подписанный URL-адрес распространяется авторизованному пользователю, который может использовать его для прямого доступа к указанному ресурсу из CDN.
  3. Проверка подписи: когда CDN получает запрос с подписанным URL-адресом, он проверяет подпись, используя соответствующий открытый ключ (зарегистрированный в CloudFront), проверяет метку времени истечения срока действия и любые другие ограничения. Если подпись действительна и срок действия URL-адреса не истек, CDN предоставляет запрошенный контент. В противном случае доступ запрещается.

Что ожидается на каждом уровне?

Хорошо, мы обсудили много всего. Возникает резонный вопрос: "сколько из этого реально ожидается от меня на интервью?" Разберем по уровням.

Middle

Ширина vs глубина: от Middle кандидата чаще ожидается ширина кругозора и знаний (примерно 80% vs 20%). Вы должны собрать понятный высокоуровневый дизайн, закрывающий все функциональные требования, но многие компоненты могут оставаться абстракциями, которые вы проработали и обсудили с интервьюером на поверхностном уровне.

Проверка базовых знаний: интервьюер будет прощупывать базу, чтобы удостовериться, что вы понимаете, что делает каждый компонент. Например, добавив API Gateway, ожидайте вопрос "что он делает" и "как работает".

Смешанный формат ведения: вы должны уверенно вести ранние стадии интервью, но не обязательно проактивно находить все проблемы дизайна. Нормально, если позже интервьюер будет вести обсуждение, задавая вопросы и ставя дополнительные задачи.

Задача Dropbox: от Middle кандидата ожидается четко определенный API и модель данных, а также высокоуровневый дизайн, который функционально покрывает все процессы загрузки, скачивания и обмена файлами. Не ожидается, что кандидаты сразу будут знать о предварительно подписанных URL-адресах или о прямой загрузке/скачке в/из S3, или сразу предложат разбиение на части. Однако после уточняющих вопросов, таких как: "Вы сейчас загружаете файл дважды, как этого избежать?" или "Как можно показать прогресс пользователя, позволяя ему возобновить загрузку?", они смогут проанализировать проблему и прийти к решению коммуницируя с интервьюером.

Senior

Глубина экспертизы: от Senior кандидата ожидания смещаются к глубине - примерно 60% ширины и 40% глубины. Нужно уметь уходить в детали там, где у вас есть практический опыт. Крайне важно продемонстрировать глубокое понимание ключевых концепций и технологий, имеющих отношение к поставленной задаче.

Продвинутый дизайн системы: вы должны быть знакомы с современными принципами проектирования систем. Например, знать, как использовать Blob хранилище или как использовать CDN для более быстрой загрузки.

Аргументация решений: вы должны уметь ясно объяснять плюсы/минусы архитектурных решений и их влияние на масштабирование, производительность и поддерживаемость, проговаривая компромиссы.

Проактивность и решение проблем: вы должны продемонстрировать сильные навыки решения проблем и проактивный подход. Это подразумевает обнаружение потенциальных проблем в ваших проектах и предложение улучшений. Вам необходимо уметь выявлять и устранять узкие места, оптимизировать производительность и обеспечивать надежность системы.

Задача Dropbox: от Senior кандидатов ожидается, что они быстро пройдут начальный этап проектирования, чтобы затем подробно обсудить, как обрабатывать загрузку больших файлов. Хотя это и не является обязательным требованием, многие кандидаты имеют опыт работы с загрузкой файлов и могут рассказать о некоторых API (например, о S3 Multipart Upload API) и принципах их работы.

Staff+

Акцент на глубину: от Staff+ кандидата ожидается глубокий разбор нюансов - примерно 40% ширины и 60% глубины. Важна демонстрация того, что, даже если вы не решали именно эту задачу раньше, вы решали достаточно похожих задач в реальном мире, чтобы уверенно спроектировать решение, опираясь на опыт.

Интервьюер понимает, что вы знаете основы (REST, нормализация данных и т. п.), так что вы можете быстро пройти это на high-level дизайне и перейти к самому интересному.

Высокая проактивность: на этом уровне ожидается, что вы будете самостоятельно выявлять и решать проблемы. Это предполагает не только реагирование на проблемы по мере их возникновения, но и их прогнозирование и реализацию упреждающих решений.

Практическое применение технологий: важно уметь говорить о применяемых технологиях не только в теории, но и как это делается на практике - конфигурации, эксплуатационные нюансы, типичные проблемы.

Решение проблем: ожидаются сильные навыки решения проблем с учетом факторов масштабирования, производительности, надежности и поддерживаемости.

Задача Dropbox: от Staff+ кандидата ожидается высокое качество решений по сложным задачам, которые обсуждались выше. Сильные кандидаты глубоко разбирают каждую тему, от них также ожидается четкое понимание компромиссов между различными решениями и способность ясно их сформулировать.

Войдите чтобы отмечать прогресс