Как устроен Instagram ?! Часть 2.
UniLecs
Заключительная часть разбора архитектуры Instagram!
Не читали нашу первую часть?!
Запись и чтение данных
Очевидно, что операция загрузки фотографий медленнее операции чтения, так как фотографии должны быть загружены в базу данных, а считывание данных может производиться из кэша.
Возможна такая ситуация, что операция «чтения данных» не сможет быть обслужена, т.к. в это время все соединения сервиса заняты запросами на запись. Допустим, наш веб-сервер может иметь максимум 500 одновременных подключений. Следовательно, он не может иметь более 500 одновременных загрузок или чтений. Очевидно, что это слабое место нашей системы, которое требует решения.
И для того, чтобы устранить его, мы можем разделить операции чтения и записи на отдельные службы. То есть у нас будут выделенные серверы для чтения и различные серверы для записи, чтобы гарантировать, что загрузка не перегружает систему.
Разделение запросов на чтение и запись фотографий также позволит нам независимо масштабировать и оптимизировать каждую из этих операций.
Надежность и избыточность
Так как надежность - один из главных критериев нашей системы, то мы должны позаботиться о сохранности каждого загруженного файла. Для этого мы будем хранить несколько копий каждого файла, чтобы в случае отказа одного сервера данных мы могли получить файл из другой копии.
Создание избыточности в системе может устранить отдельные точки отказа и обеспечить резервное копирование или резервные функции, если это необходимо.
Обмен данными (Data sharding)
Рассмотрим различные схемы разделения метаданных:
Разбиение на основе идентификатора UserID
Предположим, что мы используем блок БД (Database shard) на основе UserId, т.е. мы храним все фотографии пользователя в одном блоке БД (Database shard). Если один блок БД (Database shard) составляет 1 ТБ, нам понадобится четыре таких блока для хранения 3,7 ТБ данных. Но для лучшей производительности и масштабируемости мы воспользуемся 10-ю блоками.
Чтобы однозначно идентифицировать любую фотографию в нашей системе, мы можем добавить номер блока БД с каждым PhotoID.
Как мы можем генерировать PhotoID?
Каждый блок БД (Database shard) может иметь собственную последовательность автоинкремента для идентификаторов PhotoID, и поскольку мы добавим ShardID к каждому PhotoID, это сделает его уникальным для всей нашей системы.
Какие могут возникнуть проблемы при этой схеме разделения?
- Как бы мы справились с активными пользователями (звезды, популярные пользователи)? Многие следят за такими активными пользователями и видят любую загруженную ими фотографию.
- У некоторых пользователей будет много фотографий по сравнению с другими, что приведет к неравномерному распределению памяти.
- Что делать, если мы не можем хранить все изображения пользователя в одном блоке БД? И если мы раскидаем фотографии пользователя на несколько блоков, вызовет ли эту задержку при чтении данных?
- Хранение всех фотографий пользователя в одном блоке БД может вызвать проблемы, такие как недоступность всех данных пользователя, если этот блок не работает, или же существует большая задержка.
Разделение на основе PhotoID
В этом случае нам не нужно добавлять ShardID с PhotoID, поскольку PhotoID сам по себе будет уникальным для всей системы.
Как мы можем генерировать PhotoID?
Здесь у нас не может быть автоинкрементной последовательности в каждом блоке (Database shard) для определения PhotoID, потому что нам нужно сначала знать PhotoID, чтобы найти шард, где он будет храниться. Одним из решений может быть то, что мы выделим отдельный экземпляр базы данных для генерации идентификаторов с автоинкрементом. Если наш PhotoID может уместиться в 64 бит, мы можем определить таблицу, содержащую только поле 64 бит ID. Поэтому, когда бы мы ни захотели добавить фотографию в нашу систему, мы можем вставить новую строку в эту таблицу и использовать этот идентификатор в качестве нашего PhotoID новой фотографии.
Как мы можем планировать будущий рост нашей системы?
У нас может быть большое количество логических разделов для удовлетворения будущего роста данных, так что в начале несколько логических разделов находятся на одном физическом сервере БД. Поскольку каждый сервер БД может иметь несколько экземпляров БД, мы можем иметь отдельные БД для каждого логического раздела на любом сервере. Поэтому всякий раз, когда мы чувствуем, что конкретный сервер БД имеет много данных, мы можем перенести некоторые логические разделы с него на другой сервер. Мы можем поддерживать файл конфигурации (или отдельную базу данных), который будет сопоставлять наши логические разделы с серверами базы данных; это позволит нам легко перемещать разделы. Всякий раз, когда мы хотим переместить раздел, нам нужно только обновить файл конфигурации, чтобы объявить об изменении.
Про так называемый шардинг БД (Database sharding) вы можете почитать здесь:
Генерация новостных лент
Чтобы создать новостную ленту для каждого пользователя, нам нужно получить самые последние, самые популярные и актуальные фотографии людей, на которых он подписан.
Для простоты предположим, что нам нужно получить 100 лучших фотографий для новостной ленты пользователя. Наш сервер приложений сначала получит список людей, на которых подписан пользователь, а затем получит метаданные о последних 100 фотографиях от каждого пользователя. На последнем шаге сервер отправит все эти фотографии в наш алгоритм ранжирования, который определит 100 лучших фотографий (по времени, подобию и т.д.) и вернет их пользователю. Возможная проблема с этим подходом - более высокая задержка, так как мы должны запросить несколько таблиц и выполнить сортировку / объединение / ранжирование результатов. Чтобы повысить эффективность, мы можем предварительно сгенерировать ленту новостей и сохранить ее в отдельной таблице.
Предварительная генерация новостной ленты
У нас могут быть отдельные серверы, которые непрерывно генерируют новостную ленту пользователей и сохраняют их в таблице UserNewsFeed. Поэтому, когда любому пользователю нужны последние фотографии для его ленты новостей, мы просто запрашиваем эту таблицу и возвращаем результаты пользователю.
Всякий раз, когда этим серверам нужно сгенерировать ленту новостей пользователя, они сначала запрашивают таблицу UserNewsFeed, чтобы найти последний раз, когда лента новостей создавалась для этого пользователя. Затем с этого момента будут генерироваться новые данные новостной ленты.
Кэширование и балансировка нагрузки
Cервису потребуется большая система для доставки фотографий, чтобы обслуживать глобально распределенных пользователей. Сервис должен "приблизить" свой контент к пользователю, используя большое количество географически распределенных серверов кэша фотографий и использовать CDN (Content Delivery Network).
Поэтому можно ввести кэш для серверов метаданных для кеширования "горячих"" строк БД. Мы можем использовать Memcache для кэширования данных, а серверы приложений, прежде чем обращаться к БД, смогут быстро проверить, есть ли в кэше нужные строки и данные. Так называемый подход "Наименее недавно использованный (LRU - Least Recently Used)" может быть разумной политикой удаления кэша для нашей системы. В соответствии с этой политикой мы сначала отбрасываем наименее недавно просмотренную строку.
Как мы можем построить более "интеллектуальный" кэш?
Если следовать правилу 80-20, то 20% ежедневного объема чтения фотографий генерирует 80% трафика. Это означает, что некоторые фотографии настолько популярны, что их читает большинство людей. Это диктует, что мы можем попробовать кэшировать 20% ежедневного объема чтения фотографий и метаданных.