Запускаем mongodb и приложение
docker compose up -d
Заполняем mongodb данными
./scripts/mongo-init.sh
Откройте в браузере http://localhost:8080
Узнать белый ip виртуальной машины
curl --silent http://ifconfig.me
Откройте в браузере http://<ip виртуальной машины>:8080
Список доступных эндпоинтов, swagger http://<ip виртуальной машины>:8080/docs
Диаграмма доступна по ссылке. Выполненные решения доступны в папках mongo-sharding, mongo-sharding-repl и sharding-repl-cache. У каждого есть README.md с инструкцией по запуску и проверке.
Единственный инстанс MongoDB был заменен на кластер с двумя шардами, у каждого шарда по 2 реплики (primary, secondary, secondary - 3 инстанса на шард). Так как в процессе распродаж будет активно использоваться функционал создания заказа и изменения остатков, то нам важно иметь актуальные данные, а значит приоритет при чтении будет с первичной ноды.
Для работы кластера также добавлены роутер (для определения какой запрос с помощью какого шарда будет обрабатываться) и сервер конфигурации (для настройки и автоматической перебалансировки при отказах каких либо инстансов).
Роутер и сервер конфигурации также имеют по 2 реплики, для повышения отказоустойчивости.
MongoDB рекомендует использовать не менее 3 инстансов, так как используется алгоритм консеснуса при отказах первичной ноды. Если будет первичная и вторичная, то вероятна ситуация при которой после технических проблем станет две мастер ноды. Поэтому все ноды что мы реплицируем имеют по 2 реплики.
Клиентское приложение будет работать автоматически с одним из роутеров, а с каким - будет решать драйвер mongodb из клиентской библиотеки mongodb. В строке подключения мы укажем все наши роутеры и драйвер будет переключаться между ними при отказах.
При подключении остальных нод к конфигурационному ReplicaSet будем указывать все ноды конфигурационных серверов, таким образом каждая нода обоих шардов и каждая нода роутера будет знать о всех 3 нодах конфигурационного сервера.
Роутер и конфигурационный сервер решено было реплицировать так как повышенная нагрузка распродаж слабо прогнозируется, мы не можем точно знать насколько высокая нагрузка окажется, а роутеры могут стать узким местом. Конфигурационный сервер менее вероятно что станет узким местом, но будет очень не к месту если одна единственная нода конфигурационного сервера ляжет как раз во время распродажи. Конфигурационный сервер не требует сильно много ресурсов, поэтому его репликация не является сильно затратной. А роутер однозначно будет получать нагрузку, так как является входной точкой для всех запросов к бд.
Также роутеры не используют алгоритм консенсуса, поэтому там мы можем при желании использовать и 2 ноды. Однако учитывая что именно роутеры являются входной точкой - принято решение сделать некий запас, поэтому 3 инстанса роутеров.
Так как у нас онлайн магазин и нужно обеспечить отказоустойчивость во время распродаж, то решено сделать упор на кешировании всех статичных данных - сам каталог товаров статичен, поэтому мы можем использовать кеширование чтобы значительно снизить нагрузку на базу данных, оставив её на обработке заказов и остатков.
Так как каталог товаров у магазина предполагается что достаточно обширный и помимо каталога мы можем закешировать также профиль пользователя и рекомендации, а также нам крайне важно недопустить резкого наплыва запросов в базу данных - нам важно обеспечить отказоустойчивость кеша. Если вдруг кеш сервер станет недоступен, то все запросы пользователей сразу польются в базу данных, что вполне вероятно создаст заметные задержки, хотя и не должно полностью остановить работу, ведь база данных у нас уже в кластере.
Чтобы обеспечить отказоустойчивость кеша используем рекомендуемую минимальную схему для Redis - 3 мастер ноды и по 1 реплике к каждой, то есть суммарно 6 нод.
Клиентское приложение будет иметь доступ ко всем 6 нодам кеша и при подключении к redis будет использовать специальную строку подключения, где перечислены все ноды redis, для автоматического переключения между нодами в случаях отказов и задержек.
Наше приложение онлайн магазина остается узким местом и теперь именно оно является потенциальной точкой отказа. Поэтому мы должны масштабировать его - сделать несколько инстансов, предварительно обеспечив stateless реализацию, опирающуюся на данные из баз данных и кеша.
Для конечных пользователей мы должны сохранить единый URL для работы с нашим приложением, а также нам нужно получить автоматическую балансировку между несколькими инстансами, поэтому мы поставим API Gateway, а именно APISIX Gateway с использованием Service Discovery на базе Consul.
Взят именно APISIX Gateway так как в бесплатной опенсорс версии поддерживает Consul, а nginx имеет поддержку Consul только в Plus версии.
Также мы добавим в наше приложение отправку данных в Consul для регистрации и оповещению о доступности.
APISIX Gateway используя дополнительный модуль consul_kv будет связваться с Consul для получения информации о доступных нодах нашего приложения, чтобы автоматически балансировать нагрузку между ними.
Реплицирование Consul не предполагаем для экономии ресурсов, так как данный сервис не зависит от объема пользовательской нагрузки.
Реплицирование APISIX также не настраиваем, так как предполагаем что он справится с нагрузкой в режиме балансировщика. Если же нагрузочное тестирование заставит засомневаться в этом - потребуется формировать кластер APISIX и добавлять еще один балансировщик перед ними, например от облачного решения.
Для еще большей разгрузки нашей инфраструктуры и повышению отказоустойчивости в моментах распродаж с наплывом трафика мы уберем часть нагрузки с APISIX Gateway - вся загрузка картинок и видео будет идти отдельно от нашей инфраструктуры с использованием облачного CDN сервиса (например Yandex Cloud) - ресурсы, которые мы расположим на данном CDN сервисе, являются публично доступными ресурсами нашего каталога, поэтому у нас нет критичной нужды хранить их в своём собственном контуре.
Всю публичную статику в виде картинок и видео мы перенесем в Yandex Cloud CDN, распределим по датацентрам наиболее близким к нашим клиентам (посмотрим по аналитике) и в данных нашего сервиса изменим адреса ресурсов на новые.
В результате заметная по объему часть трафика уйдет с нашей инфраструктуры на CDN. Повысится и скорость обработки запросов нашей инфраструктуры (так как гейтвей свободнее и у него заметно меньше активных соединений, так как они станут более короткоживующие), а также повысится скорость загрузки медиа контента у пользователей, ведь их браузеры будут загружать медиа с ближайшего датацентра от CDN.
Помимо добавления шардирования и реплицирования базы данных, а также добавления кеширования, нужно добавить систему мониторинга и отправки оповещений, чтобы в случаях отказов частей системы успеть среагировать, так как распродажи создают большую нагрузку в короткое время.
Система должна успешно выдержать отказ нескольких нод от базы данных и от кеша, но если не предпринимать опреативных действий то количество отказов может достигнуть критического - например из 3 роутеров бд останется 1 и создаст такие задержки запросов, из-за которых создание заказов перестанет успешно выполняться. Либо упадет один из шардов бд, после чего часть данных станут недоступны и пользователи посчитают что часть информации потеряна (например пропадет часть каталога, либо часть заказов).
Поэтому планируется добавить мониторинг в виде Prometheus с настроенными алертами и AlarmManager, а также Graphana для наблюдения за показателями в критический период во время распродаж.
Для передачи данных от redis кластера поднимем 1 инстанс redis-exporter, который будет наблюдать за всеми 6 нодами кластера и передавать данные в Prometheus. Также добавим алерт и на доступность самого redis-exporter на случай его отказа. Также будут настроены и алерты на показатели самих redis нод - доступность, нагрузка на цп, память, количество запросов, задержки запросов, cache hitrate. Потеря мониторинга redis, как и потеря самого кеша redis, не станет критическим отказом, поэтому мы снижаем затраты и сложность реализации в данном месте.
Для передачи данных от mongodb кластера поднимем 3 инстанса mongodb-exporter, они будут следить за отдельными частями - один за первым шардом, второй за другим и третий за роутерами и конфиг серверами. Все данные будут передаваться также в Prometheus и тоже будут настроены алерты на доступность mongodb-exporter'ов. Также будут настроены и алерты на показатели самих mongodb нод - доступность, нагрузка на цп, память, количество запросов, задержки запросов.
Для данных от приложения поставим node-exporter на хосты с приложением, для мониторинга доступности хостов и нагрузки на цп, память.
Также настроим в APISIX Gateway экспорт данных в Prometheus для отслеживания количества запросов, их таймингах, статусов, загруженности хоста и доступности.
Распродажи это ситуативная, волнообразная нагрузка. Так как в распродажа по задаче уже запланирована то мы в первую очередь обеспечиваем запас мощности для максимального получения выручки, но дальше нужна оптимизация расходов на инфраструктуру.
Такой значительный запас прочности не нужен постоянно, большую часть времени ресурсы будут простаивать без нагрузки, поэтому следует перейти к автоматическому масштабированию. Нужно будет рассмотреть возможность перехода в облачные решения с настройкой автоматического горизонтального масштабирования компонентов системы и снижения количества реплик до минимально возможного при минимальной нагрузке. Если какие-то данные нельзя вынести из закрытого контура - стоит рассмотреть разделение данных по двум контурам - в закрытом оставить только то что критично не отдавать в облако, а в облаке всё остальное (гибридная модель облачной инфраструктуры). Также в собственном контуре можно развернуть всё в Kubernates и настроить автоматическое масштабирование с ним, но в закрытом контуре всё равно мы будем вынуждены получить железо и будем использовать его не на полные возможности.
Разумеется такие значительные изменения инфраструктуры обязательно требуют как функционального тестирования системы, так и нагрузочного тестирования. Нагрузочное тестирование следует сделать до и после каждого шага, чтобы понимать насколько сильный запас прочности мы получаем.
Исходя из имеющихся данных по задаче не понятно насколько в действительности большая нагрузка была прошлый раз, когда система не выдержала. Не ясно насколько долго система в итоге лежала, какой объем трафика смогли обработать, а какой отвалился и также не ясно насколько отдел маркетинга удачно запускает новую рекламу.
Поэтому описано решение подразумевающее значительную перестраховку. Вполне возможно что можно обойтись и двумя роутерами бд вместо трех и всего 1 нодой кеширования вместо кластера, особенно если в действительности в кеш будет отправляться не так много данных, как описано выше.
Задача под которую проводилось проектирование и решение которой описано выше не на 100% соответствует примеру, который дан в репозитории, поэтому есть отклонения от схемы сделанной в drawio.
Данный демонстрационный проект расчитан на практику по реализации шардирования и репликации mongodb, а также настройке кеширования redis, при чем поддержки redis cluster у приложения нет, поэтому в отличии от схемы из drawio используется обычный инстанс redis.
Проверить состояние контейнера
docker inspect --format='{{json .State.Health}}' mongo-sharding-shard1-1