
Вам не нужно изучать какую‑либо теорию, кроме этой статьи, чтобы начать собеседоваться. После прочтения смело приступайте к решению типовых System Design задач.
Изучая System Design, вы часто видите только теоретические материалы. В этой статье я постарался показать в том числе практическую реализацию многих вещей, чтобы вы не просто готовились к собеседованиям, но и знали, как эти вещи используются в реальном мире.
Содержание
Зачем изучать проектирование систем?
Что такое сервер?
Задержка и пропускная способность
Масштабирование и его типы
+ Вертикальное
+ ГоризонтальноеАвтоматическое масштабирование
Оценка на коленке
Теорема CAP
Масштабирование базы данных
+ Индексирование
+ Партиционирование
+ Архитектура «master-slave»
+ Multi-master
+ Шардирование
+ Недостатки ШардированияSQL и NoSQL СУБД. Когда какую базу данных использовать?
+ SQL СУБД
+ NoSQL СУБД
+ Особенности масштабирования
+ Когда использовать ту или иную базу данных?Микросервисы
+ Что такое монолит и микросервис?
+ Почему мы разбиваем наше приложение на микросервисы?
+ Когда следует использовать микросервисы?
+ Как клиенты отправляют запросы?Load Balancer
+ Зачем нам нужен балансировщик нагрузки?
+ Алгоритмы балансировщика нагрузкиКэширование
+ Введение в кэширование
+ Преимущества кэширования
+ Типы кэшей
+ Подробное описание RedisХранилище BLOB-объектов
+ Что такое BLOB и зачем нам нужно хранилище BLOB?
+ AWS S3Сеть доставки контента (CDN)
+ Знакомство с CDN
+ Как работает CDN?
+ Ключевые понятия в CDNMessage Broker
+ Асинхронное программирование
+ Зачем мы добавили посредника для передачи сообщений?
+ Queue
+ Stream
+ Кейсы использованияApache Kafka Deep dive
+ Когда использовать Kafka
+ Внутреннее устройство KafkaPub/Sub
Event-Driven Архитектура
+ Введение
+ Зачем использовать EDA?
+ Система нотификаций с id
+ Система с передачей всего состоянияDistributed Systems
Leader Election
Big Data Tools
Consistency Deep Dive
+ Когда использовать Strong Consistency, Eventual Consistency
+ Как добиться Strong, Eventual ConsistencyConsistent 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. Он принимает входящий запрос и перенаправляет его в нужный микросервис.

Вы можете масштабировать каждый сервис независимо друг от друга. Предположим, что у сервиса продуктов больше трафика и ему требуется 2 машины, сервису пользователей требуется 3 машины, а сервису платежей достаточно 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!