Instagram - американская социальная сеть для обмена фотографиями и видео.
-
Регистрация и авторизация пользователей
-
Возможность загрузки фотографий, а также применения к ним фильтров
-
Комментирование фотографий другими пользователями
-
Возможность оценки фотографий (лайки)
-
Поиск авторов/пользователей
-
Возможность сохранять понравившиеся посты
-
Лента с постами от подписанных пользователей
-
Подписки на пользователей
Метрики трафика и вовлеченности
-
MAU - 2B 1
-
DAU - 500M (по статистике за 2017 год) 2
-
Среднее время использования приложения в день - 34 минуты 2
-
Каждый день на платформу загружается 100M фотографий 3
-
Средняя вовлеченность для постов в Instagram - 0.71% 4
Распределение аудитории по странам 5
Средний возраст аудитории 4
Предположим, что средний пользователь Instagram видит примерно 150 постов в день, при нахождении в приложении 34 минуты 2.
В таком случае, при вовлеченности для одного поста в 0.71% 4 мы можем посчитать, что в среднем пользователь взаимодействует с 1.07 постом за день.
Также, используя данные об усредненной статистике поста по окончании года, мы можем увидеть значения количества лайков, комментариев и сохранений6:
| Лайки | Комментарии | Сохранения |
|---|---|---|
| 513.37 | 15.66 | 19.79 |
Тогда, при MAU в 500M человек, каждый день оставляется примерно 467.8M лайков, 14.25M комментариев, а также 18M сохранений постов.
На основе абсолютных качений, рассчитаем отношение лайков к остальным показателям:
-
Отношение лайков к комментариям - 33:1
-
Отношение лайков к сохранениям - 26:1
Из расчета, что Instagram приводит большинство фотографий к разрешению 1080p x 1080p 7, а также сжимает их в целях сохранения места получаем размер одной фотографии примерно в 500KB 8.Тогда, при 100M загруженных фотографий в день, общий размер хранилища составляет 50TB в день или 100KB на пользователя.
Пусть средняя длина комментария - 60 символов, тогда в кодировке UTF-8 (для поддержки большинства языков и emoji) они занимают 0.24KB. Тогда, при среднем дневном количестве комментариев в 14.25M,всего нужно около 3.42GB в день, или же 0.035KB на пользователя в день.
Примем, что 1% пользователей входят на сервис каждый день.
Из роста MAU с 1B до 2B с 2018 по 2022 год, мы можем рассчитать среднее количество регистраций в 700K в день2.
Около 18% постов в Instagram используют встроенный фильтр 9, тогда примерно 1.8M из 100M фотографий каждый день загружается с фильтром.
Используя данные по усредненной статистике поста, а также о взаимодействии с постами, можем рассчитать дневные действия пользователя:
| Действие пользователя | Количество в день |
|---|---|
| Просмотр постов | 150 |
| Использование поиска | 1(*) |
| Загрузка фотографии | 0.2 |
| Комментирование поста | 0.03 |
| Отметка "нравится" для поста | 0.94 |
| Сохранение поста в "избранное" | 0.04 |
| Подписка на другого пользователя | 0.15(**) |
| Вход | 0.01 |
| Регистрация | 0.0014 |
| Использование фильтров | 0.036 |
(*) Приблизительно, на основе данных использования страницы Explore10.
(**) Приблизительно, на основе технического ограничения на дневное количество подписок11.
| Метрика | Значение |
|---|---|
| MAU | 2B 1 |
| DAU | 500M (по статистике за 2017 год) 2 |
| Среднее время использования приложения в день | 34 минуты 2 |
| Каждый день на платформу загружается | >100M фотографий 3 |
| Посещений instagram.com | 7.24B 12 |
| Средний размер хранилища для фотографий на пользователя в день | 100KB |
| Средний размер комментария на пользователя в день | 0.035KB |
| Среднее количество фотографий на пользователя в день | 0.2шт |
| Среднее количество комментариев на пользователя в день | 0.03шт |
| Тип хранилища | Размер в день | Размер в месяц |
|---|---|---|
| Фотографии | 50TB | 1.5PB |
| Комментарии | 3.42GB | 1TB |
На каждую фотографию кидается запрос на сервер. При примерном количестве постов, которые видит один человек за день в 150, он в среднем делает 150 запросов.Тогда, при DAU в 500M, каждый день происходит 75B запросов, или же 3.125B в час, 52M в минуту, 868K в секунду.
Предположим, что к каждому 10-му посту пользователь прочитает комментарии, и к одному его оставит, либо поставит лайк. При общем количестве этих действий в 15 запросов(один запрос на комментарий + 14 на просмотр, так как для нескольких постов пользователь пролистает их вниз, в связи с чем произойдет еще несколько запросов для загрузкипоследующих комментариев),при DAU в 500M можем рассчитать дополнительное количество запросов в 7.5B в день, 87K в секунду.
Также предположим, что один запрос на получение комментариев возвращает батч на 5 штук, общим весом в 1KB.
Из среднего количества загруженных фотографий в день получаем, что в день 100M запросов на загрузку, 1.2K в секунду.
Предположим, что при поиске выдается JSON с несколькими результатами выдачи, общим размером 1KB. Также предположим, что при подписке, а так же сохранении поста происходит запрос с JSON размером 20B13.
Все фильтры для постов в Instagram работают локально на устройстве и не потребляют сетевой трафик9.
Предположим, что для регистрации и авторизации используются JSON с размерами 40B и 20B, соответственно.
| Тип | Средний RPS | Пиковый RPS(*) | Сетевой трафик (средний) | Сетевой трафик (пиковый) |
|---|---|---|---|---|
| Просмотр поста | 868K | 1.7M | 3.4Тбит/с | 6.8Тбит/с |
| Загрузка поста | 1.2K | 2.4K | 4.8Гбит/s | 9.6Гбит/s |
| Открытие секции комментариев | 81K | 162K | 650Mбит/с | 1.2Гбит/с |
| Оставление комментария | 0.8K | 1.6K | 2Мбит/с | 4Мбит/с |
| Оставление отметки "Нравится" | 5.4K | 10.8K | 22Кбит/c | 44Кбит/с |
| Использование поиска | 5.7K | 11.4K | 45.6Мбит/с | 91.2Мбит/с |
| Подписка на пользователя | 1K | 2K | 160Кбит/с | 320Кбит/с |
| Сохранение поста | 1K | 2K | 160Кбит/с | 320Кбит/с |
| Авторизация | 60 | 120 | 10Кбит/с | 20Кбит/с |
| Регистрация | 8 | 16 | 5Кбит/с | 10Кбит/с |
| Итого | 964K | 1.9M | 3.41 ТБит/с | 6.82ТБит/с |
(*) Считаем, что пиковый RPS в 2 раза больше среднего.
Instagram использует один домен для основных страниц - instagram.com, а также некоторые дополнительные, для различных вспомогательных страниц, не рассматриваемых в MVP (например, help.instagram.com).
Исходя из данных о странах с самым большим количеством пользователей можем рассчитать следующее расположение датацентров:
-
Азия
-
Индия
-
Мумбаи - Будучи одним из самых населенных городов в Индии, находится рядом с Аравийским морем, что позволит обслуживать не только пользователей из Индии, а также из других ближневосточных стран из-за большого количества оптоволоконных кабелей, подведенных к городу14.
-
Нью-Дели - Столица Индии, размещение датацентра здесь позволит обслужить весь северо-восток страны, а также предоставит альтернативный маршрут доставки контента в другие ближневосточные и азиатские страны.
-
-
Сингапур
- Сингапур - Датацентр в Сингапуре позволит обслужить оставшуюся часть Азии, также являющуюся большой долей пользователей сервиса, в частности Малайзию и Филипины. Здесь находится настоящий датацентр Meta.
-
Япония
- Токио - Снизит нагрузку на датацентр в Сингапуре, покроет восточноазиатский регион.
-
Тайланд
- Бангкок - Снизит нагрузку на Азиатский регион.
-
-
Европа
-
Германия
- Франкфурт - Находясь в самом центре Европы, датацентр здесь способен обеспечить минимальные задержки до большинства стран Еврозоны.
-
Венгрия
- Будапешт - Обслуживание восточной части Европы, город обладает развитой сетевой инфраструктурой.
-
-
Ближний восток
-
Турция
- Стамбул - Датацентр снизит нагрузку на индийские дц, а также поможет принять часть нагрузки при аварии.
-
-
Южная Америка
-
Бразилия
- Сан-Паулу - Самый населенный город всего континента, размещение датацентра здесь позволит обслужить всю западную часть континента.
-
Перу
- Лима - Работа с восточной частью континента.
-
-
Северная Америка
-
США
-
Техас - Хьюстон, датацентр ориентирован на работу с Мексикой и южно-восточной части США.
-
Юта - Солт-Лейк-Сити, город обладает большим количеством магистральных линий связи15, позволяющих эффективно работать с Канадой и северо-западной частью США.
-
-
Исходя из этих данных, можем построить карту датацентров:
| Датацентр | Процент от общей нагрузки (*) | Средний RPS | Пиковый RPS | Сетевой трафик (средний) | Сетевой трафик (максимальный) |
|---|---|---|---|---|---|
| Мумбаи | 8% | 77K | 154K | 272Гбит/c | 544Гбит/с |
| Нью-Дели | 8% | 77K | 154K | 272Гбит/c | 544Гбит/с |
| Калькутта | 8% | 77K | 154K | 272Гбит/c | 544Гбит/с |
| Сингапур | 8% | 77K | 154K | 272Гбит/c | 544Гбит/с |
| Бангкок | 8% | 77K | 154K | 272Гбит/c | 544Гбит/с |
| Токио | 7% | 67K | 134K | 238Гбит/с | 476Гбит/с |
| Франкфурт | 7% | 67K | 134K | 238Гбит/с | 476Гбит/с |
| Стамбул | 7% | 67K | 134K | 238Гбит/с | 476Гбит/с |
| Будапешт | 7% | 67K | 134K | 238Гбит/с | 476Гбит/с |
| Сан-Паулу | 7% | 67K | 134K | 238Гбит/с | 476Гбит/с |
| Лима | 7% | 67K | 134K | 238Гбит/с | 476Гбит/с |
| Хьюстон | 8% | 77K | 154K | 272Гбит/c | 544Гбит/с |
| Солт-Лейк-Сити | 8% | 77K | 154K | 272Гбит/c | 544Гбит/с |
(*) Процент нагрузки рассчитывается по MAU.
Расположение датацентров выбиралось исходя из распределения аудитории по странам, наземной сетевой инфраструктуры15, а также карты оптоволоконных кабелей. 
-
Для DNS балансировки будет использоваться Geo-based DNS, так как у нас есть несколько датацентров, расположенных в разных частях мира.
-
Для балансировки внутри континента, будет использован AnyCast BGP, позволяющий получить доступ к контенту с минимальными задержками. Регионы использования - Индия, а также США.
После глобальной балансировки нагрузки, пакет будет прилетать в датацентр, где его будет встречать сначала L4 балансировщик, а затем L7 балансировщик.
Будет использоваться несколько серверов, объединенных VRRP и работающих по схеме active-passive, при выходе из строя одного сервера, активным становится следующий с наивысшим приоритетом.Далее используя алгоритм round-robin для выбора L7 балансировщика.
Пиковый RPS одного датацентра составляет ~160k RPS. Принимая во внимание, что в максимальная нагрузка на Nginx с SSL терминациейсоставляет примерно 50К-80К RPS 16, то оценим ее среднее значение на примерно на 30K RPS. Тогда для того, чтобы держать пиковую нагрузку,нам необходимо примерно 6 экземпляров Nginx, однако необходимо оставить запас на случай возможного выхода из строя нескольких экземпляров и увеличить итоговое количество до 8 штук.
Также Nginx занимается SSL терминацией, расшифровывая https пакет и превращая его в http, а также используя session-ticket, для того чтобы на каждом запросе одного пользователя его каждый раз не расшифровывать.
Для работы сервиса используется несколько kubernetes кластеров, трафик на которые перенаправляется в соответствии с типом запроса. Например, запросы на регистрациюс помощью Kubernetes Service и CoreDNS перенаправляются в определенную группу Pod-ов (ClusterIP), где развернуты (микро) сервисы, работающие с регистрацией.
Далее, когда пакет приходит на ноду k8s, kube-proxy с помощью алгоритма round-robin выбирает какой pod будет обрабатывать конкретный запрос.
Межсервисное взаимодействие происходит похожим образом, запрос из сервиса резолвится с помощью CoreDNS, далее kube-proxy на втором сервисе выбирает один из своих pod и т.д.
Для масштабируемости используем K8s HPA - механизм, позволяющий автоматически увеличивать количество Pod-ов при возрастающей нагрузке.
Для безопасности используются healthchecks, например L4 балансировщик сам проверяет доступность Nginx-ов с помощью ping запросов.Kubernetes сам следит за состоянием pod-ов и при надобности перезапускает их, или исключает из списка доступных.
| Название таблицы | Поля таблицы | Описание таблицы |
|---|---|---|
| user | id - уникальный идентификатор записи. username - имя пользователя. email - почта. password - пароль. bio - описание профиля. followers_num - количество подписчиков. followee_num - количество аккаунтов, на которые подписан пользователь. posts_num - количество постов. is_deleted - удален ли аккаунт. avatar_url - аватар пользователя. |
Таблица, содержащая основную информацию о пользователях сервиса. |
| user_search | id - уникальный идентификатор записи. user_id - связь с пользователем. username - имя пользователя. avatar_url - аватар пользователя. |
Поисковая таблица, необходимая для быстрого поиска пользователей. |
| posts | id - уникальный идентификатор записи. user_id - связь с пользователем. post_image_id - связь с картинками поста. likes_number - количество лайков поста. comments_number - количество комментариев. is_deleted - удален ли пост. |
Основная сущность сервиса, описывающая посты, оставляемые пользователями. |
| post_images | id - уникальный идентификатор записи. image_url - ссылка на картинку. image_order - число, определяющее в какой последовательности картинки будут отображаться в случае, если их несколько. |
Таблица, которая содержит записи картинок к постам. |
| post_likes | id - уникальный идентификатор записи. user_id - связь с пользователем. post_id - связь с постами. |
Таблица, связывающая лайки с постами и пользователями. |
| post_comments | id - уникальный идентификатор записи. user_id - связь с пользователем. post_id - связь с постами. comment_text - текст комментария. is_deleted - удален ли комментарий. |
Таблица, связывающая комментарии с постами и пользователями. |
| comment_replies | id - уникальный идентификатор записи. user_id - связь с пользователем. comment_id - связь с комментарием. reply_text - текст ответа. |
Таблица, связывающая комментарии и ответы на них. |
| comment_likes | id - уникальный идентификатор записи. user_id - связь с пользователем. comment_id - связь с комментарием. |
Таблица, связывающая лайки с комментариями и пользователями. |
| saved_posts | id - уникальный идентификатор записи. user_id - связь с пользователем. post_id - связь с постами. |
Таблица, отображающая сохраненные пользователем посты. |
| followers | id - уникальный идентификатор записи. follower_id - пользователь, на которого подписаны. followee_id - пользователь, который подписан. |
Таблица, связывающая подписчиков и подписанных |
| users_sessions | id - уникальный идентификатор записи. user_id - связь с пользователем. expires_at - время истечения сессии. |
Сессии позволят пользователям не вводить логин и пароль при каждом входе |
Разбиение кластеров по геолокации, к примеру Cassandra-EU, Postgres-EU, Cassandra-US, Postgres-US. Данные пользователей соответствующего регионахранятся в соответствующем кластере.
| Операция | Средний QPS | Пиковый QPS |
|---|---|---|
| Просмотр поста | 1,736K | 3.4M |
| Открытие секции комментариев | 81K | 162K |
| Использование поиска | 5.7K | 11.4K |
| Итого | ~1,892K | ~3,573K |
| Операция | Средний QPS | Пиковый QPS |
|---|---|---|
| Загрузка поста | 1.2K | 2.4K |
| Оставление комментария | 1.6K | 3.2K |
| Лайки | 5.4K | 10.8K |
| Подписка на пользователя | 1K | 2K |
| Сохранение поста | 1K | 2K |
| Авторизация | 60 | 120 |
| Регистрация | 8 | 16 |
| Итого | ~10,168 | ~21,536 |
Шардировние 17
Шардирование производится внутренним механизмом Cassandra, который называется consistent hashing.
Этапы работы алгоритма шардирования:
-
Все узлы и данные располагаются на кольце, представляющем диапазон возможных хеш-значений (токенов). Cassandra по умолчанию использует хеш-функцию Murmur3 для вычисления токена из ключа раздела (partition key) данных.
-
Каждый узел в кластере отвечает за один или несколько диапазонов токенов на этом кольце.
-
Когда данные записываются, Cassandra хеширует их partition key, чтобы получить токен. Данные сохраняются на узле, чей диапазон включает вычисленное значение хеша. Этот узел является основным владельцем данных.
-
Когда данные считываются, запрос может прийти на любой узел кластера, который становится координатором (coordinator) этого запроса, он по тому же алгоритму вычисляет токен, и перенаправляет запрос на нужный кластер.
Репликация 17
Репликация также производится внутренним механизмом Cassandra. Со своей стороны мы определяем фактор репликации (RF), то есть количество копий данных, а также Consistency Level,пусть в нашем случае он будет равен QUORUM - (RF/2 + 1) реплик должно ответить на запрос чтения. Также выберем стратегию репликации, рекомендуемую для датацентров - NetworkTopologyStrategy.
Этапы работы алгоритма репликации:
-
Coordinator так же как и при шардировании вычисляет токен.
-
Двигаясь по кольцу, находит узел, к диапазону которого принадлежит токен. Он становится первичным владельцем данных.
-
Продолжаем двигаться по кольцу, находим следующий узел в датацентре, выбираем его и записываем в него реплику данных.
-
Идем дальше, пока не удовлетворим значению RF.
При чтении координатор смотрит RF и отправляет запросы в соответствующие ноды, а затем ждет подтверждения от QUORUM (большинства) нод. При успешном ответе от нод, отправляет ответ на запрос.
Рассмотрим вес основных типов данных:
| Тип данных | Размер |
|---|---|
| UUID | 16B |
| Timestamp | 8B |
| Bigint | 8B |
| Int | 4B |
| Bool | 1B |
| Text | 1. Имена ~ 15B. 2. Email ~ 30B. 3. URL ~ 60B. 4. Текст ответа/комментария ~ 50B. 5. Bio ~ 50B. 6. Хеш пароля ~ 60B. |
Тогда, можем рассчитать примерный вес каждой таблицы
Таблица: user |
Тип данных | Размер (Байт) |
|---|---|---|
email (PK) |
text | ~30 |
username |
text | ~15 |
password |
text | ~60 |
bio |
text | ~50 |
followers_number |
bigint | 8 |
followee_number |
bigint | 8 |
posts_number |
bigint | 8 |
image_url |
text | ~60 |
is_deleted |
bool | 1 |
created_at |
timestamp | 8 |
updated_at |
timestamp | 8 |
| Итого: | ~256 B |
Тогда, для 2B пользователей: 0.5TB.
Таблица: followers |
Тип данных | Размер (Байт) |
|---|---|---|
user_id (PK) |
uuid | 16 |
follower_id (CK) |
uuid | 16 |
created_at |
timestamp | 8 |
updated_at |
timestamp | 8 |
| Итого: | 48 B |
Тогда, для 2B пользователей: 2B * 175 (среднее количество подписчиков) * 48B = 16.8 TB.
Таблица: followees |
Тип данных | Размер (Байт) |
|---|---|---|
user_id (PK) |
uuid | 16 |
followee_id (CK) |
uuid | 16 |
created_at |
timestamp | 8 |
updated_at |
timestamp | 8 |
| Итого: | 48 B |
Тогда, для 2B пользователей: 2B * 175 (среднее количество подписок) * 48B = 16.8 TB.
Таблица: post |
Тип данных | Размер (Байт) |
|---|---|---|
user_id (PK) |
uuid | 16 |
post_id (CK) |
uuid | 16 |
post_image_id |
uuid | 16 |
likes_number |
bigint | 8 |
comments_number |
bigint | 8 |
is_deleted |
bool | 1 |
created_at (CK) |
timestamp | 8 |
updated_at |
timestamp | 8 |
| Итого: | 81 B |
Тогда, для 2B пользователей: 2B * 6 (среднее постов в месяц из расчета действий пользователя) * 81B = 0.98 TB.
Таблица: post_images |
Тип данных | Размер (Байт) |
|---|---|---|
post_id (PK) |
uuid | 16 |
image_order (CK) |
int | 4 |
image_url |
text | ~100 |
created_at |
timestamp | 8 |
updated_at |
timestamp | 8 |
| Итого: | ~136 B |
Тогда, для 2B пользователей: 2B * 6 (среднее постов в месяц из расчета действий пользователя) * 81B = 0.98 TB.
Таблица: comments |
Тип данных | Размер (Байт) |
|---|---|---|
post_id (PK) |
uuid | 16 |
user_id |
uuid | 16 |
comment_text |
text | ~60 |
is_deleted |
bool | 1 |
created_at (CK) |
timestamp | 8 |
updated_at |
timestamp | 8 |
| Итого: | ~109 B |
Тогда, для 2B пользователей: 2B * 0.9 (среднее количество комментариев в месяц из расчета действий пользователя) * 109B = 0.2 TB.
Таблица: post_likes |
Тип данных | Размер (Байт) |
|---|---|---|
post_id (PK) |
uuid | 16 |
user_id (CK) |
uuid | 16 |
created_at |
timestamp | 8 |
updated_at |
timestamp | 8 |
| Итого: | 48 B |
Тогда, для 2B пользователей: 2B * 28.2 (среднее количество лайков в месяц из расчета действий пользователя) * 48B = 2.7 TB.
Таблица: saved_posts |
Тип данных | Размер (Байт) |
|---|---|---|
user_id (PK) |
uuid | 16 |
post_id |
uuid | 16 |
created_at (CK) |
timestamp | 8 |
updated_at |
timestamp | 8 |
| Итого: | 48 B |
Тогда, для 2B пользователей: 2B * 1.2 (среднее сохраненных постов в месяц из расчета действий пользователя) * 48B = 0.12 TB.
Таблица: comment_replies |
Тип данных | Размер (Байт) |
|---|---|---|
comment_id (PK) |
uuid | 16 |
user_id |
uuid | 16 |
reply_text |
text | ~60 |
created_at (CK) |
timestamp | 8 |
updated_at |
timestamp | 8 |
| Итого: | ~108 B |
Тогда, для 2B пользователей: 2B * 0.45 (среднее количество ответов на комментарии в месяц из расчета действий пользователя) * 108B = 0.097 TB.
Таблица: comment_likes |
Тип данных | Размер (Байт) |
|---|---|---|
comment_id (PK) |
uuid | 16 |
user_id (CK) |
uuid | 16 |
created_at |
timestamp | 8 |
| Итого: | 40 B |
Тогда, для 2B пользователей: 2B * 0.45 (среднее количество ответов на комментарии в месяц из расчета действий пользователя) * 40B = 0.0025 TB.
Таблица: feed |
Тип данных | Размер (Байт) |
|---|---|---|
feed_owner_id (PK) |
uuid | 16 |
post_id |
uuid | 16 |
created_at (CK) |
timestamp | 8 |
updated_at |
timestamp | 8 |
| Итого: | 48 B |
Тогда, для 2B пользователей: 2B * 4500 (среднее количество просмотров постов в месяц из расчета действий пользователя) * 48B = 432 TB.
Для отображения различных количеств (лайков, комментариев и т.д.) будет использоваться lease GET запросы в Redis.
Для формирования ленты для всех активных пользователей помимо Cassandra, посты будут прилетать и в Redis для снижения нагрузки на чтение.
Индексы 18
Для индексирования будем использовать SAI-индексы.
Принцип работы на узле:
-
Планировщик запросов на узле Cassandra обнаруживает запрос с условием.
-
Он обращается к Index View - логическому представлению индекса для всех SSTables на этом узле.
-
Index View использует Column Index Readers, привязанной к каждой SSTable, чтобы обратиться к физическим файлам SAI на диске.
-
Так как SAI - Type Aware, он использует разные структуры данных, специфичные для типа (например, k-d деревья для чисел/дат, Term Index для текста и т.д.), для того чтобы найти первичные ключи строк, удовлетворяющих условию.
-
Параллельно с шагом 4, Cassandra сканирует Memtables в памяти, чтобы найти строки, соответствующие условию, которые еще не были сброшены в SSTable.
-
Найденные первичные ключи объединяются.
-
Используя эти ключи, планировщик считывает полные строки (или запрошенные столбцы) из файлов данных SSTables и Memtable.
Если запрос не содержит Partition key, координатор не может однозначно определить ноду, на которой могут находиться данные.В нашем случае, используется NetworkTopologyStrategy, при такой стратегии все узлы кластера могут считаться потенциальными кандидатами(нодами, содержащими нужные нам данные).
Тогда:
-
Координатор определяет все узлы кластера как кандидатов.
-
Он ждет QUORUM успешных ответов (RF/2 + 1), в нашем случае - 2.
-
Параллельно отправляет запросы первым n узлам-кандидатам, где n - настраиваемый параметр (
concurrency_factor). -
Предположим, что ноды #2 и #3 успешно ответили на запрос.
-
Тогда, координатор не ждет ответа от оставшихся нод и не посылает параллельные запросы следующим n нодам.
-
Он сравнивает версии данных и отправляет самую свежую в ответ.
-
-
Если все ноды ответили не успешно (или не ответили вовсе), либо QUORUM успешных ответов не достигнут (например, успешен был только один ответ, при требовании в 2)
- Тогда, он повторяет шаг 3 со следующими n нодами и ждет ответа от них.
Индексы для таблиц:
| Таблица | Индекс |
|---|---|
| user | username |
| user | |
| user | phone |
| post | post_id |
| comments | user_id |
| comments | comment_id |
| comment_replies | user_id |
| post_likes | user_id |
Алгоритм формирования пользовательской ленты - гибридный подход с Fan-Out on Write и Fan-Out on Read.
Части алгоритма:
-
Fan-Out on Write (рассылка при записи).
-
Пользователь с небольшим количеством подписчиков публикует пост.
-
Мы добавляем этот пост в ленту каждого подписчика.
-
При запрашивании ленты пользователь просто получает готовый вариант с постами.
-
-
Fan-Out on Write (сборка при чтении)
-
Пользователь, который подписан на "звезду" с огромным количеством подписчиков заходит в приложение.
-
В момент запроса ленты, загружается ее часть, сформированная алгоритмом Fan-Out on Write.
-
Происходит запрос последних постов у "звезд", на которых пользователь подписан.
-
Объединяет эти посты с остальной частью ленты и сортирует их по времени, а затем отдает ленту пользователю.
-
| Технология | Область применения | Мотивационная часть |
|---|---|---|
| Golang | Backend | Высокая производительность, встроенная поддержка конкурентности (горутины) для эффективной обработки огромного количества одновременных запросов. |
| TypeScript | Frontend | Добавляет статическую типизацию в JavaScript, улучшая надежность, поддерживаемость и масштабируемость сложного frontend-кода. Снижает количество ошибок на этапе выполнения, упрощает рефакторинг в больших командах. |
| React | Frontend framework | Популярная библиотека с обширной экосистемой и большим сообществом. Позволяет эффективно создавать сложные, интерактивные пользовательские интерфейсы. |
| Cassandra | Database | Распределенная NoSQL СУБД, идеально подходящая для обработки огромных объемов данных, высоких нагрузок на запись и чтение. Обеспечивает горизонтальную масштабируемость из коробки, высокую доступность и отказоустойчивость в нескольких дата-центрах. |
| Redis | Cache | Высокопроизводительное in-memory хранилище. Используется для кэширования часто запрашиваемых данных, снижения задержек и нагрузки на основную БД. |
| Kafka | Message broker | Распределенная, отказоустойчивая и масштабируемая платформа для потоковой обработки событий. Используется для асинхронной обработки задач, общения микросервисов и надежной доставки событий. |
| Cloudflare R2 | S3 storage | S3-совместимое объектное хранилище для медиафайлов. Выбрано за высокую доступность, надежность, масштабируемость и использование возможностей глобальной сети Cloudflare. |
| Nginx | Proxy server | Высокопроизводительный, стабильный обратный прокси-сервер и балансировщик нагрузки. Используется для терминирования SSL-соединений, балансировки L7-нагрузки между backend-сервисами. |
| Prometheus | Metrics | Ведущая система мониторинга для сбора time-series метрик. Необходима для отслеживания производительности, состояния здоровья и потребления ресурсов многочисленных сервисов и инфраструктуры в реальном времени. |
| Grafana | Metrics | Платформа для визуализации метрик и логов. Используется для создания дашбордов на основе данных из Prometheus, позволяя оперативно отслеживать состояние системы, анализировать тренды и быстро диагностировать проблемы. |
| Docker | Containerization | Стандарт для упаковки приложений и их зависимостей в контейнеры. Обеспечивает консистентность окружений, упрощает развертывание и управление микросервисами, является основой для оркестрации с помощью Kubernetes. |
| Kubernetes | Container orchestration | Необходим для управления тысячами контейнеров в распределенной системе, обеспечивая отказоустойчивость, масштабируемость и эффективное использование ресурсов. |
| Elasticsearch | Search engine | Распределенный поисковый движок для реализации полнотекстового поиска пользователей. Обеспечивает быструю индексацию и поиск, релевантность результатов и горизонтальную масштабируемость для обработки поисковых запросов. |
| ELK | Logs | Комплексное решение (Elasticsearch, Logstash, Kibana) для централизованного сбора, обработки, хранения, анализа и визуализации логов со всех компонентов системы. Критически важно для отладки, мониторинга и анализа инцидентов в распределенной среде с большим количеством сервисов. |
| Kotlin | Android app | Официально рекомендованный Google язык для разработки под Android. |
| Swift | iOS app | Официально рекомендованный Apple язык разработки для iOS. |
| Компонент | Надежность |
|---|---|
| Backend | Использование встроенных механизмов обеспечения надежности Kubernetes. В частности, автоматический перезапуск подов, а также различные health-check механизмы, такие как Liveness Probe, Readiness Probe и Startup Probe. |
| Cassandra | Определяем фактор репликации, то есть количество копий данных, хранящихся на нодах. При записи данных вычисляем токен, записываем его в Node 1. Идем по кольцу дальше и в 2 последующие ноды записываем копии этих данных. |
| Redis | Используем Redis Cluster с Master-Replica архитектурой. Обеспечение надежности состоит в общении между мастерами и репликами через gossip сообщения, а также механизме Failover. |
| CloudFlare R2 | Автоматическая репликация данных в пределах региона, strong consistency с помощью checksum, а также высокая доступность через глобальную сеть CloudFlare. |
-
Feed Service
-
Feed getter недоступен - отдаем захардкоженную ленту из Emergency Feed, она одинакова для всех пользователей.
-
Недоступна запись лент - DLQ в брокере сообщений, getter отдает старую ленту.
-
Недоступен feed worker - отдаем старую ленту.
-
-
Profile/Auth Service
- Ретраи при 5xx.
-
Post service
-
При сбое БД при чтении - отдавать заглушку, при записи - отдавать ошибку.
-
При успешной записи поста в БД положить в DLQ.
-
-
Impressions service
-
При недоступности записи - ретраи / допустима потеря некоторого количества реакций.
-
При неработающем воркере - запись в DLQ. Отдаем старую статистику impressions из редиса.
-
Недоступен impressions getter - отдаем заглушки / не отображаем статистику по реакциям.
-
-
Comment service
-
При недоступности записи - ретраи.
-
При недоступности воркера / бд - кладем комментарии в DLQ.
-
При недоступности чтения - отображаем пустую секцию комментариев.
-
-
Follow service
-
При недоступности записи - отображаем неактивные кнопки.
-
При недоступности чтения - не отображаем подписчиков/подписки.
-
-
Saved posts
- При недоступности записи / чтения - выдаем ошибку при попытке зайти на saved posts / сохранить пост.
Используем RateLimiting на прокси для защиты от DDOS атак.
-
Бекенд сервис не отвечает вовремя, либо отправляет 5xx ошибку в ответ, например 503 Service Unavailable.
-
Если запрос идемпотентный, API Gateway ждет определенное количество времени (Backoff)
-
API Gateway повторно отправляет тот же запрос
-
Если повторная попытка успешна, Gateway возвращает 2xx клиенту, если нет, то повторяет шаги 2 и 3.
Стратегия Backoff - Exponential Backoff, то есть время ожидания удваивается с каждой попыткой (100 мс, 200 мс) и т.д.
Когда Kubernetes решает остановить Pod (например, при обновлении Deployment, удалении пода вручную или осушении узла), он запускает процесс плавной остановки (Graceful Shutdown). Сначала Pod помечается как "Terminating", и его IP-адрес удаляется из списка конечных точек (Endpoints) соответствующего Service, чтобы новый трафик больше не направлялся на него. Почти одновременно с этим kubelet на узле отправляет сигнал SIGTERM главному процессу (PID 1) в каждом контейнере пода. Этот сигнал информирует приложение о необходимости начать корректное завершение работы: завершить обработку текущих запросов, сохранить состояние, закрыть соединения с базами данных и т.д.
CQRS — это архитектурный паттерн, который предлагает разделить операции записи и чтения. Вместо единой модели данных и набора операций для чтения и записи, CQRS использует две отдельные модели: одну для обновления данных и другую для их чтения. Команды — это императивные действия, которые изменяют состояние и обычно не возвращают данные. Запросы — это операции, которые извлекают данные и никогда не изменяют состояние системы.
DLQ — это специальная очередь, куда перенаправляются сообщения, которые не могут быть успешно обработаны потребителем из основной очереди после определенного количества попыток или из-за неисправимой ошибки (например, неверный формат сообщения или ошибка бизнес-логики). Использование DLQ предотвращает блокировку основной очереди некорректными сообщениями, которые постоянно вызывают сбой у потребителя и мешают обработке последующих сообщений.
Датацентры:
- Географическое распределение: Размещение 13 дата-центров по всему миру обеспечивает отказоустойчивость на уровне целых регионов и минимизирует задержки для пользователей.
- Резервирование питания: Использование нескольких независимых вводов электропитания и дизель-генераторных установок для обеспечения непрерывной работы при сбоях в городской электросети.
- Резервирование охлаждения: Системы климат-контроля с резервированием для поддержания оптимальной температуры оборудования.
Сеть:
- L4 Балансировка: Используется несколько L4 балансировщиков в режиме active-passive с VRRP для автоматического переключения при сбое.
- L7 Балансировка: Запуск множества экземпляров Nginx (8 на ДЦ) за L4 балансировщиками гарантирует обработку трафика даже при выходе из строя нескольких экземпляров L7.
Серверное оборудование:
- Резервные компоненты: Использование серверов с двумя блоками питания, подключенными к разным линиям электропитания.
- Дисковые массивы: Использование RAID 5 для отказоустойчивости.
- Сбор метрик: Система мониторинга на базе Prometheus используется для сбора time-series метрик со всех компонентов инфраструктуры и приложений.
- Визуализация: Grafana используется для создания дашбордов, визуализирующих собранные метрики.
- Prometheus Alertmanager настроен для генерации оповещений на основе предопределенных правил (высокая загрузка CPU, 5xx/4xx и т.д.).
Грубо оценим удельное потребление ресурсов языка Go на одно ядро схожим с С++.
| Характер сервиса | RPS | RAM |
|---|---|---|
| Тяжелая бизнес-логика | 10 | 100 Мб |
| Средняя бизнес-логика | 100 | 100 Мб |
| Легкая бизнес-логика | 5000 | 10 Мб |
Оценим сложность бизнес логики баз данных как легкую.
Составим таблицу ресурсов, необходимых для всех сервисов проекта. Peak RPS взят из задания 2
| Сервис | Peak RPS | Бизнес-логика | CPU | RAM | Net |
|---|---|---|---|---|---|
| post | 2 400 | средняя | 24 | 2400 | 10 Гбит/c |
| feed | 1 700 000 | средняя | 17 000 | 1 700 000 | 6.8 Тбит/c |
| impressions | 15 000 | простая | 3 | 30 | 1 Мбит/c |
| profile | 10 000 | средняя | 100 | 10 000 | 100 Мбит/c |
| auth | 200 | простая | 1 | 200 | 40 Кбит/c |
| follow | 2 000 | простая | 1 | 10 | 320 Кбит/c |
| comments | 164 000 | простая | 33 | 330 | 1.2 Гбит/c |
| saved posts | 2 000 | простая | 1 | 10 | 320 Кбит/c |
Также, составим таблицу ресурсов, необходимую для Баз Данных.
| База данных | QPS(*) | CPU | RAM(GB) | Disk |
|---|---|---|---|---|
| Cassandra | 486 900 | 110 | 40 | 471 TB |
| Redis | 2 424 200 | 485 | 750 | 1 TB |
(*) На основе RPS
Подберем конфигурации для 2-х разных типов датацентров (7% и 8% нагрузки соответственно). Также примем во внимание потенциальный рост количества пользователей (предположим 7% в год), а также то, что ДЦ должен уметь выдерживать нагрузку при выходе из строя одного из соседей, поэтому он должен выдерживать свою нагрузку + 1/13 нагрузки другого ДЦ.
Для датацентра с нагрузкой 8% - 1420 CPU, 747 GB RAM и 38 TB Disk Storage. С учетом потенциального прироста пользователей и возможность держать дополнительную нагрузку при сбое одного из соседних ДЦ получаем:
| CPU | RAM | DISK | NET |
|---|---|---|---|
| 1981 | 1005 | 43 | 560 |
Конфигурация для данного ДЦ 19:
| Конфигурация | Ядра | Количество на ДЦ | Цена за 1 шт |
|---|---|---|---|
| 1 x Intel Xeon Silver 4416+ 20 Cores/1 x 16GB RAM/ 1x NVME 1T / 1 x 10 Gbit/s | 2000 | 100 | 4100€ |
Итого общая стоимость - 82 * 4100 € = 336 200 €, годовая амортизация (20%): 369 000 € / 5 лет = 67 240 €/год, Месячная амортизация: 67 240 € / 12 месяцев = 5603 €/месяц на все серверы, 56 €/месяц на один сервер.
Для датацентра с нагрузкой 7% - 1243 CPU, 654 GB RAM и 34 TB Disk Storage. С учетом потенциального прироста пользователей и возможность держать дополнительную нагрузку при сбое одного из соседних ДЦ получаем:
| CPU | RAM | DISK | NET |
|---|---|---|---|
| 1673 | 880 | 39 | 490 |
Конфигурация для данного ДЦ:
| Конфигурация | Ядра | Количество на ДЦ | Цена за 1 шт |
|---|---|---|---|
| 1 x Intel Xeon Silver 4416+ 20 Cores/1 x 16GB RAM/ 1x NVME 1T / 1 x 10 Gbit/s | 1500 | 84 | 4100€ |
Итого общая стоимость - 82 * 4100 € = 336 200 €, годовая амортизация (20%): 369 000 € / 5 лет = 67 240 €/год, Месячная амортизация: 67 240 € / 12 месяцев = 5603 €/месяц на все серверы, 56 €/месяц на один сервер.
Аналог одного ДЦ в схожей конфигурации обойдется в 15 257 $ в месяц 20, либо же 183 084 $ в год. Соответственно наиболее выгодный вариант - покупка собственного оборудования.


