- 1. Тема и целевая аудитория
- 2. Расчёт нагрузки
- 3. Глобальная балансировка нагрузки
- 4. Локальная балансировка нагрузки
- 5. Логическая схема базы данных
- 6. Физическая схема базы данных
- 7. Алгоритмы
- 8. Технологии
- 9. Обеспечение надёжности
- 10. Схема проекта
- 11. Расчёт ресурсов
- Список использованных источников
Google Calendar — сервис для планирования встреч, событий и дел.
Информация о DAU отсутствует, поэтому примем его, равным 500 млн. пользователей, т.е. MAU = DAU
, т.к. данный тип сервиса используется для планирования встреч, событий и дел, а также для синхронизации данных на устройствах. [3]
1.2 Географическое распространение (данные за январь 2024) [4]
Страна | Процент пользователей от общего числа, % | Количество, млн. |
---|---|---|
США | 42,93 | 214,65 |
Япония | 8,17 | 4,085 |
Великобритания | 3,68 | 1,84 |
Канада | 3,56 | 1,78 |
Франция | 3,43 | 1,715 |
Другие | 38,24 | 191,2 |
1.3 Распространение среди различных возрастных групп (данные за январь 2024) [4]
Возрастная группа | Процент пользователей от общего числа, % |
---|---|
18 - 24 | 14,74 |
25 - 34 | 29,81 |
35 - 44 | 20,58 |
45 - 54 | 16,89 |
55 - 64 | 11,33 |
65+ | 6,65 |
- Регистрация, авторизация;
- Создание, редактирование календарей;
- Каждый календарь можно сделать общедоступным либо предоставить отдельным людям доступ к нему (просмотр или редактирование) через приглашение по электронной почте;
- Уведомления о предстоящих событиях по почте;
- Создание и редактирование событий.
Базовый функционал этого приложения включает в себя следующие типы данных:
Пользователь (профиль) | Календарь | Событие в календаре | Уведомление | Ответ на уведомление | Напонимание | Подписка на календарь |
---|---|---|---|---|---|---|
Название | Дата создания | Время до события | email пользователя | Время до события | Календарь | |
Хэшированный пароль | Описание | Дата обновления | Событие (ссылка) | Ответ пользователя | Событие (ссылка) | Пользователь |
Имя и фамилия | Дата создания | Дата начала | Дополнительная нагрузка | Событие (ссылка) | Дополнительная нагрузка | |
Часовой пояс (ссылка) | Дата обновления | Дата окончания | ||||
Телефонный номер | Часовой пояс (ссылка) | Название | ||||
Страна | Владелец (ссылка) | Описание | ||||
Город | Календарь (ссылка) | |||||
Пользователь (ссылка) | ||||||
Рекуррентность события | ||||||
Период создания |
- Примем число личных календарей, равным
5
и число календарей, в которые пользователь может быть приглашён, равным10
. - Максимальное число пользователей, которых можно пригласить на событие примем равным
200
[14]. - Число событий, создаваемым пользователем в день примем равным 3 (при общем числе событий в 1,5 млрд [3] и DAU = 500 млн. число событий 1500/500 = 3) и хранятся 5 лет, поэтому за 5 лет у пользователя накопится 5 * 365 * 3 =
5475
. - Все символы будут храниться в кодировке
UTF-8
, где каждый символ занимает около 3-х байт [15].
Далее проведём подробный расчёт с целью выяснение необходимого места для одного пользователя на каждый из этих типов.
Тип данных | Размер |
---|---|
255 B [6] | |
Хэшированный пароль | 72 B [7] |
Имя и фамилия | 300 B [8] |
Часовой пояс | 8 B (ссылка) |
Телефонный номер | 15 B [8] |
Страна | 165 B [8] |
Город | 150 B [8] |
Общий размер | 255 + 72 + 300 + 8 + 15 + 165 + 150 = 965 B |
Тип данных | Размер |
---|---|
Название | 128 символов UTF-8 (384 B) |
Описание | 1024 символа UTF-8 (3072 B) |
Дата создания | 8 B [9] |
Дата обновления | 8 B |
Часовой пояс | 8 B (ссылка) |
Владелец (ссылка) | 8 B |
Общий размер | 5 * (372 + 3072 + 8 + 8 + 8 ) = 5 * 3480 = 17400 B < 17.4 KiB (принимаем 17.4 KiB) |
Тип данных | Размер |
---|---|
Дата создания | 8 B |
Дата обновления | 8 B |
Дата начала | 8 B |
Дата окончания | 8 B |
Название | 128 символов UTF-8 (384 B) |
Описание | 1024 символа UTF-8 (3072 B) |
Календарь | 8 B (ссылка) |
Пользователь | 8 B (ссылка) |
Рекуррентность события | 1 B (bool) |
Период создания | 8 B |
Общий размер | 5475 * (8 + 8 + 8 + 8 + 384 + 3072 + 8 + 8 + 1 + 8) = 5475 * 3513 = 19 233 675 B < 19.25 MiB (принимаем 19.25 MiB) |
В виду сложности оценки пересылаемой информации в уведомлении примем размер дополнительной полезной нагрузки, равным 1 KiB
.
Тип данных | Размер |
---|---|
Событие | 8 B (ссылка) |
Время события | 8 B |
Дополнительная нагрузка | 1024 B |
Общий размер | (8 + 8 + 1024) = 1040 B |
Тип данных | Размер |
---|---|
email пользователя | 255 B |
Ответ пользователя | 1 B (uint_8t) |
Событие | 8 B (ссылка) |
Общий размер | (255 + 1 + 8) = 264 B |
В виду сложности оценки пересылаемой информации в напоминании примем размер дополнительной полезной нагрузки, равным 1 KiB
.
Тип данных | Размер |
---|---|
Событие | 8 B (ссылка) |
Время до события | 8 B |
Дополнительная нагрузка | 1024 B |
Общий размер | (8 + 8 + 1024) = 1040 B |
Тип данных | Размер |
---|---|
Календарь | 8 B (ссылка) |
Пользователь | 8 B (ссылка) |
Общий размер | 10 * (8 + 8) = 10 * 16 = 160 B |
Тип | Размер хранилища |
---|---|
Пользователь (профиль) | 965 B |
Календари | 6 KiB |
Подписки на календари | 160 B |
События | 19.25 MiB |
Уведомление | 1040 B |
Ответ на уведомление | 264 B |
Напоминание | 1040 B |
Общий размер | 965 B + 6 KiB + 160 B + 19.25 MiB + 1040 B + 264 B + 1040 B < 19.3 MiB |
- Общий размер здесь проведён приближённо, для оценки размера хранилища.
Т.к. количество зарегистрированных аккаунтов в Google Workspace равно 3 млрд [1]-[4] и, учитывая, что Calendar создаётся для каждого пользователя, примем количество зарегистрированных пользователей равным 3 млрд.
Для расчёта хранилища учтём, что календари есть у всех пользователей, а события актуальны лишь для активных пользователей.
Тип | Размер хранилища |
---|---|
Пользователь (профиль) | 965 B * 3 млрд. = 2.63 TiB |
Календари | 6 KiB * 3 млрд. = 16.763 TiB |
Подписки на календари | 160 B * 3 млрд. = 447.035 GiB |
События | 19.25 MiB * 500 млн. = 8.96 PiB |
Здесь также не учтены приглашённые пользователи, т.к. это динамически меняющийся массив и можно установить лишь ограничение на присоединение к календарю в течение короткого промежутка времени [16].
Остальные элементы: уведомление, ответ на уведомление и напиминание относятся лишь к сети (за исключением того, что по ответу на уведомление меняется динамически список пользователей, участвующих в событии); поэтому не учтены в размере хранилища.
-
Средний RPS
DAU * количество действий * количество запросов для действия / 86400 сек
-
Средний суточный трафик:
DAU * количество действий * количество байт пересылаемых запросами для совершения действия / 86400 сек
Для определения необходимых метрик требуется установить количество событий на каждый из типов данных, поэтому приведём их в таблице ниже.
Допущения
- Каждый пользователь может отменить подписку на события, но для расчёта примем, что у всех пользователей подписка есть.
- Примем, что из календаря приходит по 1 уведомлению в день и столько же ответов на уведомления и напоминаний.
Тип события | Количество действий в день |
---|---|
Просмотр календарей | 5 + 10 = 15 |
Взаимодействия с календарями | 3 |
Получение уведомлений | 15 |
Получение ответов на уведомление | 15 |
Получение напоминаний | 15 |
Для расчёта технических метрик необходимо знать количество пересылаемых данных, которые определим из следующих соображений.
- Просмотр календарей, кроме
запроса на получение данных самого календаря
, включаетзапрос на получение данных пользователя
, поэтомусуммарное количество запросов
равно (1 + 1) * 15 =30
, а количество данных равно соответственно (17400 + 965) * 15 =275475 B
. - Взаимодействие с календарями включает лишь создание/обновление события, поэтому
суммарное количество запросов
равно3
, а количество данных равно соответственно 3 * 17400 =52200 B
. - Получение уведомлений включает лишь один запрос, поэтому суммарное
количество запросов на получение уведомлений
равно15
, а количество данных уведомления соответственно данным о событии, т.е. получим 15 * 1040 =15600 B
. - Получение ответа на уведомление включает лишь
запрос на отправку ответа на уведомление
, поэтому получим 15 * 264 =3960 B
. - Получение напоминания составляет столько пересылаемой информации, как и получение уведомления, поэтому 15 * 1040 =
15600 B
.
Тип метрики | RPS | Средний суточный трафик |
---|---|---|
Просмотр календарей | 500 млн. * 15 / 86400 сек = 86806 |
500 млн. * 15 * 275475 / 86400 = 191.3 Гбит/с |
Взаимодействия с календарями | 500 млн. * 3 / 86400 сек = 17362 |
500 млн. * 3 * 52200 / 86400 = 7.25 Гбит/с |
Получение уведомлений | 500 млн. * 15 / 86400 сек = 86806 |
500 млн. * 15 * 15600 / 86400 = 10.83 Гбит/с |
Получение ответов на уведомления | 500 млн. * 15 / 86400 сек = 86806 |
500 млн. * 15 * 3960 / 86400 = 2.75 Гбит/с |
Получение напоминаний | 500 млн. * 15 / 86400 сек = 86806 |
500 млн. * 15 * 15600 / 86400 = 10.83 Гбит/с |
Для получения пиковых значений RPS и трафика умножим среднее потребление трафика в два раза (возьмём коэффициент запаса, равный 2-м).
Действие | RPS | Пиковый RPS | Средний трафик | Пиковый трафик |
---|---|---|---|---|
Просмотр календарей | 86806 | 173612 | 191.3 Гбит/с | 382.6 Гбит/сек |
Взаимодействия с календарями | 17362 | 34724 | 7.25 Гбит/с | 14.5 Гбит/сек |
Получение уведомлений | 86806 | 173612 | 10.83 Гбит/с | 21.66 Гбит/сек |
Получение ответов на уведомления | 86806 | 173612 | 2.75 Гбит/с | 5.5 Гбит/сек |
Получение напоминаний | 86806 | 173612 | 10.83 Гбит/с | 21.66 Гбит/сек |
Для анализа основных регионов потоков трафика были выбраны следующие статистики:
Страна | hypestat.com [11] | webstatsdomain.org [12] | similarweb.com [5] |
---|---|---|---|
USA | 18.8% | 32.7% | 42.93% |
India | 10.7% | 7.7% | — |
China | — | 4.4% | — |
Japan | 5.2% | 4.3% | 8.17% |
United Kingdom | — | — | 3.68% |
Canada | — | — | 3.56% |
Russia | 2.9% | — | — |
France | — | — | 3.43% |
Brazil | 2.8% | — | — |
Iran | 3% | — | — |
Similarweb.com был выбран, несмотря на свою неточность для обобщения с остальными статистиками, т.к. это позволит увеличить выборку.
Обобщим эти статистики (Китай был исключён из этой выборки ввиду особенностей доступности и использования в нём иностранных сервисов):
- USA — 42.93%
- India — 10.7%
- Japan — 8.17%
- United Kingdom — 3.68%
- Canada — 3.56%
- France — 3.43%
- Iran — 3%
- Russia — 2.9%
- Brazil — 2.8%
- Other — 25.26%
Исходя их этой статистики можно сделать вывод, что основная часть дата-центров должна располаться в США, остальные — региональные.
Возможные города распложения дата-центров возьмём из [13].
Для анализа возможного расположения дата-центров учтём то, что факт задержки (Latency) не важен для данного сервиса, поэтому лучше выбрать немного дата-центров, но сделать их максимально надёжными. Следовательно, их необходимо располагать в городах, в которых находятся наиболее крупные дата-центры (это можно сделать, анализируя, например, их энергопотребление) и максимально близкими к основной аудитории.
Из вышесказанного следует, что дата-центры будут расположены следующим образом:
- Основные дата-центры в США:
- Вирджиния (Северо-восток)
- Лос-Анджелес (Юго-запад)
- Дополнительные дата-центры:
- Индия (Мумбаи)
- Германия (Франкфурт)
Расположение в США двух дата-центров обосновано тем, что США — основной потребитель трафика подобного сервиса, в Индии — потому что следующим регионом по аудитории данного сервиса после США является Индрия, а в Лондоне, который является очень крупным хабом для датацентров Европы, — для обеспечения покрытия Европы, которая суммарно составляет также крупный регион потребления трафика.
Ссылка на карту (https://yandex.ru/maps/?um=constructor%3Aaea556dba6be4bcec4779f984ed8f4823e591b580d93639704395d6a6a3eb012&source=constructorLink)
Отсюда можно заключить, что наиболее целесообразно будет использовать следующие технологии:
BGP Anycast
Geo-based DNS
Выбор BGP Anycast
обоснован тем, что пользователя будет направлять в оптимальный по расположению дата-центр в пределах региона (с точки зрения BGP), т.к. позволяет распределить нагрузку с помощью настройки anycast-сетей, которая позволит гранулярно "отправлять" пользователей в различные дата-центры. Использование Geo-based DNS
целессобразно для разграничения пользователей по регионам с помощью настройки GeoIP-базы
, что может использоваться для персонализации контента, например, региональной рекламы.
Также откажемся технологий:
Latency-based DNS
Использование CDN в качестве хаба для обеспечения более стабильного соединения
Как уже было сказано, задержка (Latency) для данного сервиса не важна, поэтому Latency-based DNS
здесь не имеет смысл. Использование CDN в качестве хаба для обеспечения более стабильного соединения
не нужен, т.к. нет необходимости в стабильном и качественном подключении для создания событий календаря.
Для обеспечение надёжности необходимо решить несколько вопросов:
- резервирование ДЦ в случае отказа всех в данном регионе (это возможно ввиду небольшого их числа).
- пропускная способность ДЦ в случае прилива аудитории (например, в случае отказа одного из ДЦ).
Резервирование в рамках одного региона будет осуществляться BGP Anycast
, который будет перенаправлять трафик из одного ДЦ в другой в рамках одного региона (это актуально для США). В случае остальных регионов балансировка будет осуществляться с помощью Geo-based DNS
, который будет перенаправлять трафик из одного региона в другой.
Пропускная способность ДЦ в данном случае гарантирована тем, что часть трафика можно распределить между отдельными ДЦ (например, между Европейским и Американскими), а также тем, что для своих регионов были выбраны крупнейшие ДЦ.
Для балансирования межсервисных запросов наиболее рационально выбрать L7
балансировщик, т.к. у него много достоиств, несмотря на возросший overhead
, следовательно, Latency, что не является критическим для сервиса календаря.
Входящий трафик будем балансировать с помощью протокола HSRP.
Один из способов добиться доступности сети, близкой к 100%, — использование протокола HSRP, который обеспечивает избыточность IP-сетей, предоставляя возможность немедленного прозрачного восстановления пользовательского трафика в случае отказа первого перехода в оконечных сетевых устройствах или каналах доступа.
Благодаря совместному использованию IP-адреса и MAC-адреса (уровень 2), два и более маршрутизаторов могут действовать как единый "виртуальный" маршрутизатор. Члены группы виртуальных маршрутизаторов непрерывно обмениваются сообщениями о состоянии. Это позволяет маршрутизаторам подменять другие маршрутизаторы при их запланированном или незапланированном отключении. Узлы продолжают переадресовать IP-пакеты на согласованный IP- и MAC-адрес, и аварийное переключение устройств, производящих маршрутизацию, происходит незаметно для пользователей и приложений.
Межсервисные запросы будут балансироваться с помощью Sticky Session. В качестве стратегии балансировки выберем алгоритм Round Robin
в nginx
из его Ingress-контроллера, т.к. предполагается, что будет использоваться неблокирующий ЯП для backend-сервисов.
Для улучшения производительности будет использоваться Session Cache
, который будет кэшировать SSL-сессии.
Для уменьшения пересылаемого трафика будет осуществляться сжатие gzip
.
Также используем оркестрацию сервисов через K8
, что позволит:
- выполнять auto-scaling для полного использования ресурсов доступных машин.
- service discovery для оценки работоспособности сервиса.
- распределять экземпляры сервиса по узлам.
Для обеспечения отказоустойчивости используем дополнительный keepalived
, который является:
- Программным монитором доступности нод.
- Сигнализирует балансеру при падении/подъёме ноды.
- Умеет
VRRP/CARP
резервирование нод между собой для обеспечения отказоустойчивости самих балансировщиков.
В VRRP/CARP хосты разделяют один ip-адресс, но активен он только на одном их хостов, а на другом находится в виртуальном интерфейсе, но не анонсируется в сеть. Поэтому при прекращении работы одной из машин тот же самый ip-адресс поднимается на другой машине и туда переносится весь трафик.
Балансеров устанавливаем в 2 раза больше, чем нужно для пикового трафика, ставим их попарно.
Проверку работоспособности сервиса будем проводить с помощью livenessProbe
в K8
.
Т.к. уже используется L7 балансировщик nginx
, то терминацию SSL также осуществляем с помощью nginx
.
При расчёте размера таблиц принимаются те же размеры данных, что и при расчёте нагрузки
User | Calendar | Event | Session | User_to_calendar | User_to_event | Timezone | Error | |
---|---|---|---|---|---|---|---|---|
id (8 B) | id (8 B) | id (8 B) | id (8 B) | id (8 B) | id (8 B) | id (8 B) | id (8 B) | |
time_zone_id (8 B) | time_zone_id (8 B) | calendar_id (8 B) | user_id (8 B) | user_id (8 B) | user_id (8 B) | time_zone (8 B) | event_id (8 B) | |
email (255 B) | owner_id (8 B) | user_id (8 B) | created_at (8 B) | calendar_id (8 B) | event_id (8 B) | name (64 B) | payload (1 KiB) | |
username (300 B) | name (384 B) | creation_date (8 B) | expired_at (8 B) | access (1 B) | error_code (2 B) | |||
hash_password (72 B) | description (3072 B) | update_date (8 B) | ||||||
telephone (15 B) | creation_date (8 B) | start_date (8 B) | ||||||
country (165 B) | update_date (8 B) | end_date (8 B) | ||||||
city (150 B) | name (384 B) | |||||||
description (3072 B) | ||||||||
reccurent (1 B) | ||||||||
creation_period (8 B) | ||||||||
Всего | 978 B | 3496 B | 3521 B | 32 B | 25 B | 24 B | 80 B | 1042 B |
Наибольшую сложность для оценки представляют данные об участниках календарей и событий, поэтому проведём оценку по нижней границе, предположив, что пользователей в календаре не превышает 10
, а количество событий в день составляет около 5 при числе участников, не превышающих 10, откуда за 5 лет получим 5 * 10 * 5 * 365 = 91250
шт.
Для оценки размера таблицы Error
необходимо знать количество уведомлений и напоминаний, которые будут возвращать ошибку. Для приблизительной оценки примем, что это 5% от общего числа, т.е. 2 * 173612 (пиковый RPS) * 0.05 = 17361.2 (примем 17632
).
В следующей таблице приведены примерные данные о суммарном объёме данных в БД.
Для оценки нагрузки на чтения для таблиц User
, Calendar
и Event
возьмём данные пикового RPS.
Некоторые пункты из нагрузки на запись оценить затруднительно, т.к. в источниках нет данных об активности пользователей, поэтому примем, что нагрузка на запись составляет около 10% нагрузки на чтение.
Нагрузка на запись и чтение для таблицы Error
сопоставима с нагрузкой на чтение.
Таблица | Размер данных | Нагрузка на чтение | Нагрузка на запись |
---|---|---|---|
User | 873 B * 3 млрд. = 2.38 TiB |
500 млн. / 86400 = 5787 |
579 |
Calendar | 3496 B * 500 млн. * 5 шт. = 7.95 TiB |
173612 |
17362 |
Event | 3521 B * 500 млн. * 5475 шт. = 8.56 PiB |
34724 |
3473 |
Session | 32 B * 500 млн. = 14.9 GiB |
500 млн. / 86400 = 5787 |
5787 |
User_to_calendar | 25 B * 500 млн. * 10 шт. = 116.4 GiB |
- | - |
User_to_event | 25 B * 500 млн. * 91250 шт. = 1.01 PiB |
- | - |
Timezone | 80 B * 38 шт. = 3040 B |
5787 + 173612 = 179 399 |
0 |
Error | 1042 B * 17632 шт. = 17.52 MiB |
173612 * 2 * 0.05 = 17362 |
17362 |
Для денормализации БД проведём следующие преобразования:
- Уберём таблицу
Timezone
, чтобы не делать дорогостоящий JOIN, и будем хранить информацию о временной зоне в таблицахUser
иCalendar
напрямую в виде строкиname
и timespampztime_zone
. - Пользователей, приглашённых на событие, добавим в качестве массива
members
в таблицуEvent
. - Пользователей, приглашённых в календарь, и их уровни доступа добавим в качестве словаря
members_with_access
в таблицуCalendar
.
В случае сервиса календаря нагрузка на чтение намного превышает нагрузку на изменение/создание, поэтому для хранения информации о пользователях будем использовать релиционную СУБД PostgreSQL
, т.к. она очень хорошо справляется с таким типом нагрузки и обладает высокой надёжностью.
Для хранения сессий пользователей будем использовать неряляционную СУБД Redis
, т.к. она осуществяет хранение данных in-memory, имеет поддержку неблокирующей репликации master-slave и возможность организации кластера Redis cluster. Сессии наиболее рационально хранить в in-memory базе, т.к. данный тип данных довольно часто меняется и важно получать быстро данные о сессии пользоваталей, при этом в случае падения базы не произойдёт потери критически важной информации, как, например, данных пользователей. Использование SQL-решения здесь неэффективно, т.к. при одинаковом оборудовании разница в производительности будет примерно в 10 раз быстрее в сторону Redis.
Также в Redis
будем хранить информацию об ошибках при отправке уведомлений и напоминаний, т.к. частота записи в данном случае сопоставима с частотой чтения.
Таблица | База данных |
---|---|
User | PostreSQL |
Calendar | PostreSQL |
Event | PostreSQL |
Session | Redis |
Error | Redis |
Используем стандартное горизонтальное шардирование, в котором:
- Таблицу
User
шардируем по ключуid
. - Таблицу
Calendar
шардируем по ключуid
. - Таблицу
Event
шардируем по ключуid
.
Ключами шардирования для каждой из таблиц выступают id
, чтобы обеспечить псевдоравномерное распределение нагрузки между шардами и избежать случая, когда несколько шардов перегружены в сравнении с остальными (например, случая, когда один шард нагружен на 70%, а другой — на 30%).
В качестве схемы репликации будем использовать стандартную схему с физической репликацией. Одна база данных на запись master
и много на чтение slave
, так как запросов на чтение в приложении будет на несколько порядков больше, чем на запись.
Индексирование проведём, исходя из критического функционала приложения.
- Индекс на
email
для быстрого поиска и проверки уникальности.
- Индекс на
owner_id
для поиска календарей, созданных пользователем.
- Индекс на
start_date
иend_date
(комбинированный индекс) для быстрого поиска событий в определённый период времени. - Индекс на
calendar_id
для поиска событий календаря. - Индекс на
owner_id
для поиска событий, созданных пользователем.
Для резеврного копирования используем следующие подходы [17]:
- Еженедельное полное резервное копирование.
- Ежедневное непрерывное инкрементное резервное копирование.
Для обеспечения лучшей производительности будет использоваться язык программирования Go на стороне backend, поэтому используем следующие клиентские бибилотеки:
- Для PostgreSQL: pgx.
- Для Redis: go-redis.
Т.к. в календаре существует довольно сильный дисбаланс между активными пользователями и пользователями, которые планируют немного событий в день, возможен случай, что при неправильном шардировании, будет сильно неравномерое распределение нагрузки между шардами. Это приведёт к тому, что наиболее нагружен будут несколько шардов, а не все равномерно, поэтому шардирование потеряет смысл.
Для решения этой проблемы было принято решение испльзовать в качестве ключа шардирования id
для каждой из таблиц User
, Calendar
и Event
, т.к. ввиду псевдослучайной генерации id
между пользователями с разной активностью будет создано псевдоравномерное распределение нагрузки между шардами.
В календаре для двух типов данных (уведомление и напоминание) необходимо использовать Apach Kafka, т.к. каждый из них требует отправки в определённое время. Под уведомлением здесь следует понимать сообщение, оправленное на почту со временем события и опросом о готовности пользователя прийти на ней, а под напоминанием — такое же уведомление, но за небольшое время до начала встречи, например, 15 минут.
При использовании Kafka возникают сразу несколько проблем:
- Возможность "торможения" очереди из-за возможных ошибок при отправке.
- Определение событий, для которых нужно оправить напоминание.
Первую из проблем можно решить с помощью дополнительной таблицы Error
, которую необходимо разместить в отдельной БД, например, Redis
, чтобы микросервис отправки сообщений при любой возможной ошибке не тормозил очередь, а отправил в эту БД данные для отправки с указанным кодом ошибки, а далее уже в зависимости от кода будет отпределяться предпринимать ли новую попытку отправки (эта попытка снова проходит через Kafka).
Для решения второй проблемы существует несколько вариантов:
- Хранить все напоминания, которые создаются при создании события, а при редактировании также редактируются.
- Генерировать их динамически.
В данном случае был выбран вариант с динамической генерацией, т.к. можно так распределить нагрузку между шардами, что поиск всех необходимых событий, для которых необходимо отправить напоминание, не будет сильно нагружать сервер и не создаст дисбаланс между шардами. Отказ от хранения напоминаний обусловлен сложностью работы с рекуррентными событиями, которые создаются через определённый период времени. Хранение событий привело бы к тому, что всё равно потребовалось бы сканировать БД, но уже с напоминаниями, поэтому использование такого варианта не приведёт к заметным преимуществам в сравнении с динамической генерацией.
Технология | Область применения |
---|---|
TypeScript | Языке программирования frontend |
React | Frontend framework |
Go | Язык программирования backend |
PostgreSQL | Основаная реляционная СУБД для хранения данных |
Redis | Нереляционная СУБД для хранения сессий пользователей и ошибок в Apach Kafka при отправке уведомлений и напоминаний |
nginx | L7 балансировщик нагрузки |
Apach Kafka | Брокер сообщений для создания очереди отправки уведомлений и напоминаний пользователям |
Prometheus | Сбор статистики |
Grafana | Визуализация статистики |
Kubernetes | Оркестрация контейниризованных сервисов |
- TypeScript — статически типизируемый язык программирования для frontend, который транслируется в JavaScript код. Основное преимущество данного языка в том, что при масштабировании проекта могут возникать ошибки, которые намного проще заметить на этапе компиляции. Также данный язык поддерживает новые функции ECMAScript и модульность, как и JavaScript.
- React — один из самых распространённых framework для frontend-разработки, с помощью которого можно существенно ускорить разработку приложения и улучшить его производительность за счёт реактивности из коробки. Также благодаря своей популярности данный framework имеет огромную кодовую базу, довольно невысокий порог вхождения и хорошую масштабируемость дл поддержки крупных проектов, что делает его применение наиболее рационалным.
- Go — компилируемый многопоточный язык программирования backend, который благодаря своей неблокирующей модели многопоточности очень хорошо подходит для разработки высоконагруженных проектов, что, учитывая довольно простой порог вхождения в него, делает данный язык программирования хорошим выбором для высоконагруженного сервиса.
- PostgreSQL — реляционная СУБД с открытым исходным кодом, которая очень хорошо масштабируется и хорошо работает с нагрузкой в случае, когда запросов на чтение существенно больше, чем запросов на запись. Также обеспечивает надёжноси, т.к. обладает механизмами сохранения целостности данных, транзакционной безопасности и отказоустойчивости.
- Redis — неряляционная in-memory СУБД, которая в случае данного приложения находится перед "настоящей" базой данных. Данная СУБД используется для хранения сессий пользователей, т.к. они относительно редко меняются и к ним часто обращается приложение и они не относятся к критически важным. Также, в отличие от
Memcached
имеет поддержку транзакций, очереди, поддерживает репликацию и кластеризацию. - nginx — используется в качестве L7 балансировщика нагрузки, т.к. очень хорошо изучен сообществом, имеет очень высокую производительности и гибкость настройки.
- Apach Kafka — используется для создании очереди отправки уведомлений и напоминаний пользователям. В качестве основных критериев выбора данного решения: масштабируемость (позволяет распределять данные по нескольким серверам), скорость (разеляет потоки данных), надёжность (разделы распространяются и реплицируются на многих серверах).
- Prometheus — сбор метрик с их последующим хранением.
- Grafana — платформа для визуализации собранных данных.
- Kubernetes — открытое программное обеспечение для оркестровки контейнеризированных приложений, автоматизации их развёртывания, масштабирования и координации в условиях кластера.
Компонент | Метод | Обоснование |
---|---|---|
Глобальная балансировка | Geo-based DNS , BGP Anycast , фильтрация трафика L4 балансировщиком |
В случае падения одного из ДЦ резервирование достигается с помощью распределения нагрузки между остальными ДЦ с помощью Geo-based DNS и BGP Anycast , а фильтрация входящего трафика производится, например, компанией qrator , чтобы защититься от входящих атак до уровня ДЦ. |
nginx | резервирование ресурсов и сервисов, использование VRRP/CARP |
Ввиду того, что балансировщик является критически важным компонентом системы, необходимо обязательно резервировать под него ресурсы. Это включает в себя дополнительные серверы, которые с помощью механизма VRRP/CARP , будучи установленными в паре, резервируют друг друга. |
PostgreSQL | Репликация | Ввиду критической важности для бизнеса, информация пользователей должна храниться в нескольких копиях, чтобы в случае отказа одной из БД не произошла потеря данных. Также использование репликации позволит снизить нагрузку на основую master БД, т.к. запросов на чтение существенно больше запросов на запись. |
Redis | Репликация | Сессии пользователей не представляют ценности для бизнеса, но их всё равно необходимо дублировать, чтобы потом не произошёл заброс трафика ввиду единомоментной авторизации всех пользователей региона, а также это может повредить бизнесу косвенно, негативно повлияв на общее представление о приложении. Данные об ошибках необходимо резервировать, т.к. информация из уведомлений очень важна для пользователей. |
API Gateway | Резервирование логики | Главный шлюз является узким местом системы, поэтому имеет смысл реплицировать его для повышения пропускной способности и предотвращения отказа всего сервиса в случае отказа основного шлюза. |
Kafka | Репликация | Ввиду того, что брокер сообщений является критически важным элементом архитектуры, обеспечивающим важный аспект бизнес-логики, необходимо реплицировать его для обеспечения отказоустойчивости. |
- Использование
Graceful Shutdown
для обеспечения безопасного релизов новых версий приложения (не должно запускаться никаких новых процессов и не должно приниматься никаких новых веб-запросов и закрытия всех открытых соединений с внешними сервисами и базами данных). - Использование
Graceful Degradation
для обработки отказов определённых компонентов, чтобы при отказе одних микросервисов не происходил отказ всего приложения. - Использование на стороне клиента offline-логики для более бесшовной работы в случае обрыва интернет соединения.
- Использование на стороне сервера для уменьшения посылаемых запросов на проблемный хост и использование curcuit breaker паттерна для предотвращения каскадного отказа сервисов.
- Использование кеширования данных на стороне сервиса в случае отказа БД с последующей их записью при восстановлении её работоспособности.
- Использование логирования, мониторинга и оповещений для своевременного реагирования на возможные ошибки со стороны сервисов.
В качестве основнова узла архитектуры используется API Gateway
, который балансирует запросы между микросервисами и является своего рода медиатором, который отвечает за взаимодействие микросервисов между собой.
Микросервисы:
- Авторизации (auth).
- Пользователей и календарей (users and calendars).
- Событий (event).
- Уведомлений (notification).
- Ошибок (error).
Микросервис auth
пользователей отвечает, соответственно, за авторизацию пользователей, что выражается также в виде взаимодействия с БД Users Storage, в которой хранятся сессии пользователей.
Микросервис users and calendars
объединяет в себе логику получения данных пользователя и логику обработки данных календаря. Решение объединить эти две функции в один микросервис обусловлено тем, чтобы не плодить искуссвенные микросервисы, т.к. никогда данные календаря не запрашиваются отдельно от данных пользователя и наоборот. Этот микросервис взаимодействует с БД, в которой хранятся данные о пользователях и их календарях.
Микросервис events
вынесен отдельно от пользователей и календарей по причине того, что именно события в отличие от календарей ежеминутно посылают запрос в БД для нахождения тех событий, для которых нужно отправить напоминание. Также этот микросервис ответственен за оправку таких событий в брокер сообщений Apach Kafka. Следовательно, добавление этой логики, а также логики взаимодействия с событиями, привело бы к не нужным усложнениям и созданию монолита в случае объединения с микросервисом users and calendars
. Этот микросервис взаимодействует с БД, в которой хранятся данные о событиях.
Микросервис notifications
отвечает за отправку уведомлений (при создании/изменении) и напоминаний (за определённое время до) о предстоящих событиях. Он напрямую взаимодействует с микросервисом error
, т.к. в некоторых случаях возможны ошибки отправки.
Как указано выше, при отправке уведомлений возможны некоторые ошибки, которые обрабатывает сервис error
. В зависимости от кода ошибки он принимает решение о повторной попытке либо о удалении этого уведомления/напоминания. Все будущие уведомления/напоминания, для которых будет предпринята ещё одна попытка отправки, кладутся в соответствующую БД. Эта БД также, как и в случае БД с событиями, ежеминутно сканируется и передаёт извлечённые данные обратно в микросервис error
. Далее микросервисом эти данные передаются снова в брокер сообщений Apach Kafka.
Для обеспечения надёжного функционирования схемы проекта необходимо указать методы, которыми будет достигаться необходимая отказоустойчивость.
- Случай возможного отказа ДЦ в одном из регионов.
Как было описано в п.3 про глобальную балансировку, такой сценарий должен обрабатываться механизмом Geo-based DNS
, который перенаправляет пользователей из одного региона в другой.
- Резервирование информации при отказе одного из ДЦ.
Описанный выше сценарий обработки отказа ДЦ в одном из регионов невозможен без резервирования информации, т.к. иначе может сложиться такая ситуация, что при хранении данных всех пользователей, например, из азиатского региона на сервере в Мумбаи (Индия) и отказе этого сервера, всех пользователей перенаправит, например, на европейский сервер во Франкфурте (Германия), но их данных там не окажется. Т.о., сложится ситуция, при которой пользователям не будут доступны их данные, а это сильно повлияет на репутацию продукты и, следовательно, доходы бизнеса.
Поэтому необходимо резервировать ДЦ попарно, учитывая фактор катастрофоустойчивости, т.е. что ДЦ должны находиться друг от друга на расстояни не менее 1000 км. Следовательно, в этом случае для ДЦ в Мумбаи (Индия) назначим резервным ДЦ во Франкфурте (Германия) и наоборот. Для ДЦ в Нью-Йорке (восточное побережье США) назначим резервным ДЦ в Лос-Анджелесе (западное побережье США) и наоборот.
Пересылка информации будет осущетвляться по внутренним 100 Гбит/с линиям, как на серверах Amazon
, что возможно, учитывая общую тенденцию роста данных, т.к. данные не будут копироваться все сразу, а лишь те, которые изменились.
Для оценки производительности Go будем опираться на таблицу из методических указаний [19]
Учтём, что согласно бенчмарку [20] производительности C++ и Go ориентировочно равны (если брать одинаковый уровень написания приложения), однако по памяти Go затратнее, чем C++, поэтому примем ориентировочно, что для Go требуется на 20% больше памяти, чем для C++.
Тогда таблица для оценки производительности будет иметь следующий вид.
ЯП | Характер сервиса | RPS | RAM |
---|---|---|---|
Go | Тяжёлая бизнес-логика | 1 | 120 MiB |
Go | Средняя бизнес-логика | 100 | 120 MiB |
Go | Лёгкая бизнес-логика (JSON API) | 5000 | 12 MiB |
Nginx | SSL handshake (CPS) | 500 | 10 MiB |
Для оценки требуемых ресурсов сначала проведём расчёт нагрузки на сервисы, чтобы, отталкиваясь от этого, уже выбирать аппаратное оснащение серверной.
Допущения:
-
Пусть cookies выдаётся пользователю каждый день в течение первого захода на сайт [18].
-
Данные о нагрузке на сервис по RPS возьмём из пункта про расчёт нагрузки.
-
Данные по нагрузке на сервис
auth
возьмём, как RPS для просмотра календарей, т.к. это всегда требует обращения в этот сервис. -
Нагрузку на сервисы, совмещающие несколько функций, например, как микросервис
users and calendars
, представим в виде суммы нагрузок. -
Данные о нагрузке на
API Gateway
представим в виде суммы нагрузок на все сервисы, связанные с календарями и событиями, т.к. он является медиатором системы. -
Данные о нагрузке на БД возьмём из пункта про логическую схему БД.
-
Для компенсации возможной неточности расчёта между средней и лёгкой бизнес-логикой в плане RAM установим минимальное значение RAM в 8 GiB.
Сервис | Пиковый RPS | CPU cores | RAM | Пиковый трафик |
---|---|---|---|---|
auth | 173612 (просмотр календарей) = 173612 |
1737 | 206 GiB | 382.6 Гбит/сек |
users and calendars | 173612 (просмотр календарей) + 34724 (взаимодействие с календарями) + 173612 (ответы на уведомления) = 381948 |
3820 | 448 GiB | 382.6 Гбит/сек + 14.5 Гбит/с + 5.5 Гбит/с = 402.6 Гбит/с |
events | 34724 (взаимодействие с календарями) = 34724 |
348 | 40.8 GiB | 14.5 Гбит/с |
notifications | 173612 (отправка напоминаний) + 173612 (отправка уведомлений) = 347224 |
70 | 8 GiB | 21.66 Гбит/с + 21.66 Гбит/с = 43.32 Гбит/с |
error | (173612 (отправка напоминаний) + 173612 (отправка уведомлений)) * 0.1 = 34723 |
348 | 40.8 GiB | 4.34 Гбит/с |
API Gateway | 381948 (календари) + 34724 (события) = 416671 |
4167 | 488.3 GiB | 402.6 Гбит/с + 14.5 Гбит/с = 417.1 Гбит/с |
- Все данные дублируются и для учёта возможных неточностей умножим данные для диска, RAM и CPU минимум на
2.5
. - Минимальный объём диска установим в 4 GiB.
- Нагрузку на запись примем 25% от нагрузки на чтение, учитывая дополнительно пересылку данных для резервирования между ДЦ.
Хранилище | Объём диска | Пиковый RPS на чтение | Пиковый RPS на запись | CPU cores | RAM | Нагрузка на чтение | Нагрузка на запись |
---|---|---|---|---|---|---|---|
Redis (sessions storage) | 37.25 GiB |
5787 |
5787 |
58 * 2.5 = 174 |
14.13 GiB |
382.6 Гбит/сек |
95.65 Гбит/сек |
Redis (errors storage) | 4 GiB |
17362 |
17362 |
174 * 2.5 = 435 |
43 GiB |
4.34 Гбит/с |
1.09 Гбит/сек |
PostgreSQL (users and calendars storage) | (2.38 TiB + 7.95 TiB) * 2.5 = 25.83 TiB |
5787 + 173612 = 179399 |
579 + 17362 = 17941 |
180 * 2.5 = 450 |
43.8 GiB |
402.6 Гбит/с |
161.04 Гбит/сек |
PostgreSQL (Events storage) | 21.4 PiB |
34724 |
3473 |
35* 2.5 = 88 |
8.5 GiB |
14.5 Гбит/с |
3.63 Гбит/сек |
Ввиду того, что важно выбирать серверы с хорошим соотношением частоты ядра на количество ядер, выберем серверы с процессорами AMD EPYC.
Сервис | Хостинг | Конфигурация | CPU cores | Количество | Покупка, $ (1 шт) | Амортизация, $/мес (сумма на 5 лет) |
---|---|---|---|---|---|---|
auth | own | 2x AMD EPYC 9654 (96C Cache L3 384M Cache 2.40GHz) / 768 (24x 32GB) / 3xSSD 800GB (до 24 HDD 2.5") / H755 / 2x БП 2400W | 192 | 10 | 38222 | 6371 |
users and calendars | own | 2x AMD EPYC 9654 (96C Cache L3 384M Cache 2.40GHz) / 768 (24x 32GB) / 3xSSD 800GB (до 24 HDD 2.5") / H755 / 2x БП 2400W | 192 | 20 | 38222 | 12742 |
events | own | 2x AMD EPYC 9654 (96C Cache L3 384M Cache 2.40GHz) / 768 (24x 32GB) / 3xSSD 800GB (до 24 HDD 2.5") / H755 / 2x БП 2400W | 192 | 2 | 38222 | 1275 |
notifications | own | 1x AMD EPYC 9654 (96C 384M Cache 2.40 GHz) / 2x 16GB DDR5 RDIMM 4800MHz (Поддержка до 6TB максимально, 24 DIMM портов) / HPE MR216i-p Gen11 (ZM) / noHDD (до 48 HDD 2.5'' SFF) / 1x HP 800W | 192 | 2 | 38222 | 1275 |
notifications | own | 1x AMD EPYC 9654 (96C 384M Cache 2.40 GHz) / 2x 16GB DDR5 RDIMM 4800MHz (Поддержка до 6TB максимально, 24 DIMM портов) / HPE MR216i-p Gen11 (ZM) / noHDD (до 48 HDD 2.5'' SFF) / 1x HP 800W | 96 | 1 | 14126 | 236 |
error | own | 2x AMD EPYC 9654 (96C Cache L3 384M Cache 2.40GHz) / 768 (24x 32GB) / 3xSSD 800GB (до 24 HDD 2.5") / H755 / 2x БП 2400W | 192 | 2 | 38222 | 1275 |
API Gateway | own | 2x AMD EPYC 9654 (96C Cache L3 384M Cache 2.40GHz) / 768 (24x 32GB) / 3xSSD 800GB (до 24 HDD 2.5") / H755 / 2x БП 2400W | 192 | 22 | 38222 | 14017 |
- https://explodingtopics.com/blog/google-workspace-stats
- https://zipdo.co/statistics/google-calendar/
- https://marketsplash.com/google-workspace-statistics/
- https://www.patronum.io/key-google-workspace-statistics-for-2023/
- https://www.similarweb.com/website/calendar.google.com/#geography
- https://emaillistvalidation.com/blog/demystifying-email-validation-understanding-the-maximum-length-of-email-addresses/#:~:text=Defining%20the%20Maximum%20Length&text=Domain%20Part%3A%20The%20domain%20part,email%20address%20is%20320%20characters.
- https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
- https://www.geekslop.com/technology-articles/2016/here-are-the-recommended-maximum-data-length-limits-for-common-database-and-programming-fields#:~:text=35%20chars%20(US)%2C-,50%20(other),-Last%20name
- https://www.postgresql.org/docs/8.2/datatype-datetime.html
- https://stackoverflow.com/questions/24138581/what-is-the-maximum-allowed-expiration-time-for-a-google-notification-channel
- https://hypestat.com/info/calendar.google.com
- https://webstatsdomain.org/d/calendar.google.com
- https://www.visualcapitalist.com/cp/top-data-center-markets/
- https://support.google.com/calendar/answer/37161?hl=en&co=GENIE...&co=GENIE.Platform%3DDesktop#zippy=%2Cguest-limit-for-invitations
- https://stackoverflow.com/questions/10229156/how-many-characters-can-utf-8-encode
- https://support.google.com/a/answer/2905486?hl=en
- https://backupsolution.ru/backup-types/
- https://developer.chrome.com/blog/cookie-max-age-expires?hl=ru
- https://github.com/init/highload/blob/main/highload_l11_hosting.md
- https://www.techempower.com/benchmarks/#section=data-r22&test=json&hw=ph&l=zii9rz-cn3