
Вам не нужно изучать какую‑либо теорию, кроме этой статьи, чтобы начать собеседоваться. После прочтения смело приступайте к решению типовых System Design задач.
Изучая System Design, вы часто видите только теоретические материалы. В этой статье я постарался показать в том числе практическую реализацию многих вещей, чтобы вы не просто готовились к собеседованиям, но и знали, как эти вещи используются в реальном мире.
Содержание
-
Зачем изучать проектирование систем?
-
Что такое сервер?
-
Задержка и пропускная способность
-
Масштабирование и его типы
+ Вертикальное
+ Горизонтальное -
Автоматическое масштабирование
-
Оценка на коленке
-
Теорема CAP
-
Масштабирование базы данных
+ Индексирование
+ Партиционирование
+ Архитектура «master-slave»
+ Multi-master
+ Шардирование
+ Недостатки Шардирования -
SQL и NoSQL СУБД. Когда какую базу данных использовать?
+ SQL СУБД
+ NoSQL СУБД
+ Особенности масштабирования
+ Когда использовать ту или иную базу данных? -
Микросервисы
+ Что такое монолит и микросервис?
+ Почему мы разбиваем наше приложение на микросервисы?
+ Когда следует использовать микросервисы?
+ Как клиенты отправляют запросы? -
Load Balancer
+ Зачем нам нужен балансировщик нагрузки?
+ Алгоритмы балансировщика нагрузки -
Кэширование
+ Введение в кэширование
+ Преимущества кэширования
+ Типы кэшей
+ Подробное описание Redis -
Хранилище BLOB-объектов
+ Что такое BLOB и зачем нам нужно хранилище BLOB?
+ AWS S3 -
Сеть доставки контента (CDN)
+ Знакомство с CDN
+ Как работает CDN?
+ Ключевые понятия в CDN -
Message Broker
+ Асинхронное программирование
+ Зачем мы добавили посредника для передачи сообщений?
+ Queue
+ Stream
+ Кейсы использования -
Apache Kafka Deep dive
+ Когда использовать Kafka
+ Внутреннее устройство Kafka -
Pub/Sub
-
Event-Driven Архитектура
+ Введение
+ Зачем использовать EDA?
+ Система нотификаций с id
+ Система с передачей всего состояния -
Distributed Systems
-
Leader Election
-
Big Data Tools
-
Consistency Deep Dive
+ Когда использовать Strong Consistency, Eventual Consistency
+ Как добиться Strong, Eventual Consistency -
Consistent Hashing
-
Data Redundancy and Data Recovery
+ Зачем мы делаем резервные копии баз данных?
+ Различные способы резервного копирования данных
+ Непрерывное резервное копирование -
Proxy
+ Что такое прокси сервер?
+ Прямой и обратный прокси сервер
+ Создание собственного обратного прокси-сервера -
Как решить любую проблему, связанную с проектированием системы?
Мы рассмотрели разделы 1-7 в части I. 8-9 в части II. Пришло время Микросервисов, Балансировки и Кэширования. Поехали!
Микросервисы
Что такое Монолит и Микросервис?
Монолит: Все приложение строится как единое целое в монолитной архитектуре. Предположим, вы создаёте приложение для электронной коммерции. В монолите вы создаёте только один сервер и все функции (например, управление пользователями, список товаров, заказ, оплата и т. д.) в одном приложении.
Микросервис: Разбивайте большие приложения на более мелкие, управляемые и независимо развертываемые сервисы.
Пример: вы можете разделить приложение для электронной коммерции на следующие сервисы:
-
Сервис для пользователей
-
Сервис для товаров
-
Сервис для заказов
-
Сервис для платежей
Создайте отдельные серверные приложения для каждой службы.
Почему мы разбиваем наше приложение на микросервисы?
-
Предположим, что один из компонентов получает много трафика и требует больше ресурсов. В этом случае вы можете масштабировать только этот сервис отдельно.
-
Гибкость в выборе технологического стека. В монолитной системе весь бэкенд написан на одном языке/технологии. Но в микросервисной архитектуре вы можете писать разные сервисы на разных технологических стеках. Например, вы можете создать сервис для пользователей на NodeJS, а сервис для заказов на Golang.
-
Сбой одного сервиса не обязательно повлияет на другие. В монолитной системе, если одна часть серверной части выйдет из строя, то выйдет из строя всё приложение. Но в микросервисах, если выйдет из строя сервис «Заказ», другие части, такие как сервисы «Пользователь» и «Товар», не пострадают.
Когда следует использовать микросервис?
-
«Микросервисы определяются внутренней структурой организации». Предположим, что в стартапе есть 3 команды, работающие над 3 разными бизнес-функциями. Тогда у него будет 3 микросервиса, и по мере роста числа команд микросервисы будут разделяться.
-
Большинство стартапов начинают с монолитной архитектуры, потому что на начальном этапе только 2–3 человека работают над технической частью, но со временем, когда количество команд увеличивается, они переходят на микросервисы.
-
Если мы хотим избежать единичного сбоя, то также выбираем микросервисы.
Как клиенты выполняют запросы в микросервисной архитектуре?
Каждый микросервис может развёртываться независимо.
Предположим, что user service развернут на компьютере с IP-адресом 192.168.24.32 , product service на 192.168.24.38 и так далее с другими сервисами. Все они развернуты на разных компьютерах. Очень неудобно использовать разные IP-адреса (или доменные имена) для каждого микросервиса. Поэтому мы используем API Gateway для этого.
Клиент выполняет каждый запрос в одной конечной точке шлюза API. Он принимает входящий запрос и перенаправляет его в нужный микросервис.

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

API Gateway предоставляет и ряд других преимуществ:
— Ограничение скорости
— Кэширование
— Безопасность (аутентификация и авторизация)
Балансировщик Нагрузки
Зачем нам нужен балансировщик нагрузки?
Как мы видели ранее, при горизонтальном масштабировании, если у нас есть много серверов для обработки запросов, мы не можем предоставить клиенту все IP-адреса машин и позволить клиенту самому выбирать, на каком сервере выполнять запрос.
Балансировщик нагрузки выступает в качестве единой точки взаимодействия для клиентов. Они запрашивают доменное имя балансировщика нагрузки, и балансировщик перенаправляет их на один из наименее загруженных серверов.

По какому алгоритму балансировщик нагрузки определяет, на какой сервер отправлять трафик? Мы рассмотрим это в алгоритмах балансировщика нагрузки.
Алгоритмы балансировки нагрузки
1. Алгоритм циклического перебора (Round Robin)
Как это работает: запросы последовательно распределяются между серверами по кругу.
Предположим, что у нас есть 3 сервера: Сервер-1, Сервер-2 и Сервер-3.
Тогда при циклическом распределении 1-й запрос отправляется на Сервер-1, 2-й запрос — на Сервер-2, 3-й запрос — на Сервер-3, 4-й запрос снова отправляется на Сервер-1, 5-й запрос — на Сервер-2, 6-й запрос — на Сервер-3, 7-й запрос снова отправляется на Сервер-1, 8-й запрос — на Сервер-2 и так далее.

Преимущества:
-
Простой в реализации.
-
Работает хорошо, если все серверы имеют одинаковую мощность.
Недостатки:
-
Игнорирует загруженность сервера.
2. Взвешенный циклический перебор (Weighted Round Robin)
Как это работает: аналогично Round Robin, но серверам присваиваются веса в зависимости от их производительности. Серверы с более высоким весом получают больше запросов. На рисунке ниже вы можете увидеть количество запросов, чтобы понять, как это работает. На рисунке ниже 3-й сервер больше (имеет больше оперативной памяти, хранилища и т. д.). Таким образом, он получает в два раза больше запросов, чем 1-й и 2-й.

Преимущества:
-
Лучше справляется с серверами с неодинаковой мощностью.
Недостатки:
-
Статические веса могут не отражать производительность сервера в реальном времени.
3. Алгоритм наименьшего количества соединений (Least Connections)
Как это работает: направляет трафик на сервер с наименьшим количеством активных подключений. Подключения могут быть любыми: HTTP, TCP, WebSocket и т. д. Здесь балансировщик нагрузки перенаправит трафик на сервер с наименьшим количеством активных подключений к балансировщику нагрузки.
Преимущества:
-
Балансирует нагрузку динамически в зависимости от активности сервера в реальном времени.
Недостатки:
-
Может плохо работать с серверами, обрабатывающими соединения разной продолжительности.
4. Алгоритм, основанный на хэше (Hash-Based Algorithm)
Как это работает: балансировщик нагрузки принимает на вход любые данные, например IP-адрес клиента, идентификатор пользователя и т. д., и хеширует их, чтобы найти сервер. Это гарантирует, что конкретный клиент всегда будет перенаправлен на один и тот же сервер.
Преимущества:
-
Полезно для поддержания постоянства сеанса.
Недостатки:
-
Изменения на сервере (например, добавление/удаление серверов) могут нарушить согласованность хеширования и сеансов.
Вот и все для балансировщика нагрузки. Как вы думаете, какой алгоритм используется в балансировщиках по дефолту?
Кэширование
Введение в кэширование
Кэширование — это процесс сохранения часто используемых данных на высокоскоростном уровне хранения, чтобы будущие запросы к этим данным обрабатывались быстрее.
Пример: предположим, что для получения данных из базы данных MongoDB требуется 500 мс, затем требуется 100 мс, чтобы выполнить некоторые вычисления с этими данными на сервере и, наконец, отправить их клиенту. Таким образом, в общей сложности клиенту требуется 600 мс, чтобы получить данные. Если мы кэшируем эти вычисленные данные и сохраняем их в быстром хранилище, например в Redis, и получаем их оттуда, то мы можем сократить время с 600 мс до 60 мс. (Это гипотетические цифры).
Кэширование означает хранение предварительно вычисленных данных в хранилище с быстрым доступом, таком как Redis, и когда пользователь запрашивает эти данные, их можно получить из Redis, а не запрашивать в базе данных.
Пример: Возьмем в качестве примера сайт с блогами. Когда мы переходим по маршруту /blogs, мы получаем все блоги. Если пользователь переходит по этому маршруту в первый раз, то в кэше нет данных, поэтому нам нужно получить данные из базы данных, и предположим, что время отклика составляет 800 мс. Теперь мы сохранили эти данные в Redis. В следующий раз, когда пользователь перейдет по этому же адресу, он получит данные из Redis, а не из базы данных. Время отклика составляет 20 мс. Когда добавляется новый блог, мы должны каким-то образом удалить старое значение блогов из Redis и заменить его новым. Это называется удалением из кэша. Существует множество способов удаления из кэша. Мы можем установить срок действия (Time to live — TTL); каждые 24 часа Redis будет удалять блоги, и когда запрос поступит от любого пользователя в первый раз после 24 часов, он получит данные из БД. После этого они будут кэшироваться для следующих запросов.
Преимущества кэширования:
-
Улучшенная производительность: Сокращает время ожидания для конечных пользователей.
-
Сниженная нагрузка: разгружает внутренние базы данных и службы.
-
Экономическая эффективность: Снижает сетевые и вычислительные затраты.
-
Масштабируемость: позволяет лучше справляться с большими нагрузками.
Типы кэшей
-
Кэш на стороне клиента:
+ Хранится на устройстве пользователя (например, в кэше браузера)
+ Сокращает количество запросов к серверу, загрузку сетевого канала
+ Примеры: файлы HTML, CSS, JavaScript. -
Кэш на стороне сервера:
+ Хранится на сервере
+ Примеры: кэши в оперативной памяти, такие как Redis или Memcached. -
Кэш CDN:
+ Используется для доставки статического контента (файлов HTML, CSS, PNG, MP4 и т. д.)
+ Кэшируется на географически распределённых серверах
+ Примеры: AWS CloudFront, Cloudflare CDN -
Кэш-память на уровне приложения:
+ Встроена в код приложения
+ Кэширует промежуточные результаты или результаты запросов к базе данных.
Это было лишь введение. Мы подробно рассмотрим каждый тип кэша.
Погружение в Redis
Redis – это хранилище структур данных в памяти.
В оперативной памяти данные хранятся в ОЗУ. И если у вас есть базовые знания в области компьютерных наук, то вы знаете, что чтение и запись данных из ОЗУ происходит очень быстро по сравнению с диском.
Мы используем этот быстрый доступ для кэширования.
Базы данных используют диски для хранения данных. И чтение/запись выполняются очень медленно по сравнению с Redis.
Один из вопросов, который может возникнуть у вас, заключается в том, что если Redis такой быстрый, то зачем использовать базу данных? Разве мы не можем положиться на Redis для хранения всех данных?
Ответ: Redis хранит данные в оперативной памяти, а в оперативной памяти очень мало памяти по сравнению с диском. Если вы решали задачи в leetcode или codeforces, то иногда вы могли бы получать сообщение “Превышен лимит памяти”. Точно так же, если мы храним слишком много данных в Redis, это может привести к ошибке нехватки памяти.
Redis хранит данные в парах «ключ-значение». В базе данных мы получаем доступ к данным из таблицы так же, как и в Redis: мы получаем доступ к данным по ключам.
Значения могут быть любого типа данных. Например – строка, список и т.д:

Существуют и другие типы данных, но в основном используются вышеперечисленные.
Я расскажу обо всём, что касается Redis в командной строке, но вы можете настроить всё необходимое в любом приложении, например в NodeJS, Springboot и Go.
Выполните приведенную ниже команду, чтобы установить и запустить Redis на вашем локальном ноутбуке.
docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
Соглашение об именовании ключей в Redis
Вы можете давать ключам любые имена, но, как правило, в разных отраслях используется следующий подход:
— Ключ для пользователя с идентификатором 1 будет называться «user:1»
— Ключ для электронной почты пользователя с идентификатором 2 будет называться «user:2:email»
Используйте “:” для разделения объектов
Теперь давайте обсудим каждый тип данных
-
Строка
-
SET значение ключа: устанавливает ключ с определенным значением.
-
GET ключ: извлекает значение, связанное с ключом.
-
SET значение ключа NX: сохраняет строковое значение только в том случае, если ключ ещё не существует
-
MGET key1 key2 … keyN: извлекает несколько строковых значений за одну операцию

2. Список
-
LPUSH: добавляет значение левее
-
RPUSH: добавляет значение правее
-
LLEN: возвращает длину списка
-
LPOP: извлекает левое значение и возвращает его
-
RPOP: извлекает правое значение и возвращает его.

Чтобы создать очередь, нужно только выполнять операции LPUSH и RPOP, то есть добавлять элементы слева и удалять справа (FIFO).
Чтобы создать стек, используйте только LPUSH и LPOP, то есть выталкивайте и заталкивайте элементы с одной стороны (LIFO).
Попробуйте самостоятельно изучить другие команды и типы данных из документации Redis.
Ниже приведено базовое использование NodeJS
Не стесняйтесь попробовать его на предпочитаемом вами языке бэкенда, например Django, Go и т. д.

Я написал код для того же примера блога. Если /blog был вызван в первый раз, то данные будут получены из apiCall или базы данных. Но после этого они кэшируются и обслуживаются из Redis. Данные в Redis действительны в течение 24 часов. После этого они автоматически удаляются из Redis.

Cash Hit означает, что данные присутствуют в кэше.
Cash Miss означает, что данных в кэше нет
Другой способ кэширования заключается в том, что каждый раз, когда сервер записывает данные в базу данных, он одновременно записывает их и в кэш (Redis).
Пример: когда на Codeforces проводится соревнование, каждый раз, когда пользователь отправляет какие-либо вопросы, вы немедленно обновляете список рейтингов в базе данных и в Redis, чтобы пользователь видел текущий рейтинг, если вы предоставляете список рейтингов из Redis.
На этом третья часть перевода подошла к концу. В следующий раз поговорим про брокеры сообщений.
Меня зовут Невзоров Владимир. Работаю старшим backend разработчиком на HighLoad проекте с порядком пиковой нагрузки в миллион rps. Приветствую) Веду телеграмм канал по Архитектуре, System Design, Highload бэкэнду.
На канале провожу архитектурные каты, публикую полезные материалы, делюсь опытом. Сейчас с участниками канала разбираем книгу Мартина Клеппмана “Высоконагруженные приложения” на стримах(youtube запись).
Для пополнения багажа знаний по теме заходите на мой канал System Design World <=
Материал для подготовки к System Design интервью в виде чек листов на моём boosty. Смотреть.
Успехов в дальнейшем изучение темы System Design!
Автор: avovana7