Вам не нужно изучать какую‑либо теорию, кроме этой статьи, чтобы начать собеседоваться. После прочтения смело приступайте к решению типовых 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. Пришло время Баз Данных.
Масштабирование базы данных
Обычно у вас один сервер баз данных. Ваше приложение запрашивает данные из этой БД и получает результат.
Когда вы достигаете определённого масштаба, этот сервер баз данных начинает медленно отвечать или может выйти из строя из-за своих ограничений. В такой ситуации необходимо масштабировать базу данных, что мы и рассмотрим в этом разделе.
Масштабировать базу данных стоит постепенно. Это значит, что если у нас всего 10 тысяч пользователей, то масштабировать её для поддержки 10 миллионов — пустая трата времени. Это избыточная инженерия. Будем масштабировать только до того предела, который достаточен для нашего бизнеса.
Предположим, у вас есть сервер баз данных, в котором есть таблица пользователей.
С сервера приложений поступает множество запросов на чтение, чтобы получить информацию о пользователе с определённым идентификатором. Чтобы ускорить запрос на чтение, сделайте следующее:
Индексирование
База данных проверяет каждую строку в таблице, чтобы найти запрашиваемые данные. Это называется полным сканированием таблицы(full table scan) и может быть медленным для больших таблиц. Проверка каждого идентификатора занимает O(N) времени.
При индексировании база данных использует индекс для быстрого перехода к нужным строкам, что значительно ускоряет работу.
Вы индексируете столбец «id», после чего база данных создаёт копию этого столбца «id» в структуре данных (называемой B-деревом). B-дерево используется для поиска конкретного идентификатора. Поиск выполняется быстрее, потому что идентификаторы хранятся в отсортированном виде, так что вы можете использовать двоичный поиск для поиска за O(logN) (прим. переводчика. В PostgreSQL - индекс для Primary Key создаётся автоматически)
Если вы хотите включить индексирование в каком-либо столбце, вам нужно просто добавить одну строку кода, и все операции по созданию B-деревьев и т.д. будут выполняться БД. Вам не нужно ни о чём беспокоиться.
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50),
email VARCHAR(100),
created_at TIMESTAMP NOT NULL
);
CREATE INDEX idx_users_email ON users(email);
-- Ускоряем поиск по email'ам
Это было очень короткое и простое объяснение по поводу индексации.
Партиционирование
Означает разбиение большой таблицы на несколько маленьких таблиц.
Вы видите, что мы разделили таблицу пользователей на 3 таблицы:
- user_table_1
- user_table_2
- user_table_3
Эти таблицы хранятся на одном сервере базы данных.
Пример партиционирования по диапазону (Range Partitioning):
CREATE TABLE users (
id SERIAL,
username VARCHAR(50),
email VARCHAR(100),
created_at TIMESTAMP NOT NULL
) PARTITION BY RANGE (created_at);
-- Партиция для данных за январь 2023 года
CREATE TABLE users_2023_01 PARTITION OF users
FOR VALUES FROM ('2023-01-01') TO ('2023-02-01');
-- Партиция для данных за февраль 2023 года
CREATE TABLE users_2023_02 PARTITION OF users
FOR VALUES FROM ('2023-02-01') TO ('2023-03-01');
-- Партиция для данных за март 2023 года
CREATE TABLE users_2023_03 PARTITION OF users
FOR VALUES FROM ('2023-03-01') TO ('2023-04-01');
Пример запроса
При вставке данных PostgreSQL автоматически направляет строки в соответствующую партицию на основе значения столбца created_at
:
INSERT INTO users (username, email, created_at)
VALUES ('user1', 'user1@example.com', '2023-01-15');
Пример запроса
При выполнении запросов PostgreSQL автоматически выбирает нужные партиции на основе условий. Например:
SELECT * FROM users WHERE created_at BETWEEN '2023-02-01' AND '2023-03-01';
В этом случае PostgreSQL будет искать данные только в партиции users_2023_02
.
В чём преимущество такого разделения?
Когда ваш индекс становится очень большим, при поиске также возникают проблемы с производительностью. Теперь, после разделения на разделы, у каждой таблицы есть свой индекс, поэтому поиск в таблицах меньшего размера выполняется быстрее.
Вам может быть интересно, как мы определяем, из какой таблицы выполнять запрос. До этого мы могли выполнить запрос SELECT * FROM users where ID=4
. Не волнуйтесь, вы можете снова выполнить тот же запрос. PostgreSQL работает за кулисами. Он найдёт нужную таблицу и выдаст результат. Но вы также можете настроить это на уровне приложения, если хотите.
Архитектура ведущего-ведомого устройства (Master-Slave)
Используйте её, если даже после индексирования, партиционирования и вертикального масштабирования ваши запросы выполняются медленно или ваша база данных не может обрабатывать дальнейшие запросы на одном сервере.
Суть подхода заключается в репликации данных на несколько серверов.
При выполнении любого запроса на чтение (запросы SELECT)
он будет перенаправлен на наименее загруженный сервер. Таким образом вы распределяете нагрузку.
Но все запросы на запись (INSERT, UPDATE, DELETE)
будут обрабатываться только одним сервером.
Узел/сервер/нода, который обрабатывает запрос на запись, называется главным узлом(master).
Узлы, которые принимают запросы на чтение, называются подчиненными узлами(slaves).
Когда вы отправляете запрос на запись, он обрабатывается и записывается на главном узле, а затем асинхронно (или синхронно в зависимости от конфигурации) реплицируется на все подчиненные узлы.
Несколько мастеров
Если запросы на запись выполняются медленно или один главный узел не может обработать все запросы на запись, вы можете сделать следующее.
В этом случае вместо одной главной базы данных для обработки записей используются несколько главных баз данных.
Пример
Очень распространённая практика — использование двух главных узлов. Один для Европы, а другой для Азии. Запросы из данных регионов обрабатываются на соответствующем ближайшем узле. Узлы периодически синхронизируют свои данные.
В системе с несколькими ведущими серверами самая сложная часть — это обработка конфликтов. Если для одного и того же идентификатора в обоих ведущих серверах есть два разных набора данных, то вам нужно написать логику в коде:
Хотите ли вы принять оба набора данных
Заменить предыдущий набор данных последним
Объединить их и т. д.
Здесь нет единого правила. Всё зависит от бизнес-сценария.
Шардирование базы данных
Шардинг — это очень сложная процедура(плюсом ссылка на хабр статью). Старайтесь избегать этого в реальной жизни и делайте это только в том случае, если всего вышеперечисленного недостаточно и требуется дальнейшее масштабирование.
Шардинг похож на партиционирование, что мы видели выше. Но вместо того, чтобы размещать разные таблицы на одном сервере, мы размещаем их на разных серверах.
На изображении выше вы видите, что мы разделили таблицу на 3 части и поместили их на 3 разных сервера. Эти серверы обычно называют шардами.
Здесь мы выполнили шардирование на основе идентификаторов, поэтому этот столбец с идентификаторами называется ключом шардирования.
Примечание: ключ шардирования должен равномерно распределять данные по сегментам, чтобы избежать перегрузки одного.
Каждый раздел хранится на независимом сервере баз данных (называемом сегментом/шардом). Таким образом, теперь вы можете масштабировать этот сервер по отдельности в соответствии с вашими потребностями, например, используя архитектуру «ведущий-ведомый» для одного из шардов, на который поступает много запросов.
Почему шардирование является сложной задачей?
При партиционирование(сохранении фрагментов таблицы на одном сервере БД) вам не нужно беспокоиться о том, из какой таблицы выполнять запрос. PostgreSQL делает это за вас. Но при шардирование (сохранении фрагментов таблицы на разных серверах БД) вам нужно обрабатывать это на уровне приложения. Вам нужно написать код таким образом, чтобы при запросе от идентификатора 1 до 2 он отправлялся в БД-1, а при запросе от идентификатора 5 до 6 — в БД-3. Кроме того, при добавлении новой записи вам нужно вручную обработать логику в коде приложения, чтобы определить, в какой шард вы собираетесь добавить эту новую запись.
Стратегии шардирования
Шардирование на основе диапазонов(Range-Based Sharding)
Данные делятся на сегменты на основе диапазонов значений в ключе сегментирования.
Пример:
Шард 1: пользователи сuser_id 1–1000
Шард 2: пользователи сuser_id 1001–2000
Шард 3: пользователи сuser_id 2001–3000
Плюсы: просто в реализации.
Минусы: неравномерное распределение, если данные искажены (например, в некоторых диапазонах больше пользователей).Шардирование на основе хеширования(Hash-Based Sharding)
К ключу шардирования применяется хеш-функция и результат определяет шард.
Пример:HASH(user_id) % number_of_shards
определяет шард
Плюсы: обеспечивает равномерное распределение данных.
Минусы: при добавлении новых сегментов сложно выполнить повторную балансировку, так как результаты хеширования меняются.Географическое шардирование(Geographic/Entity-Based Sharding)
Данные разделяются на основе логической группировки, например по региону.
Пример:
Шард 1: пользователи из Америки.
Шард 2: пользователи из Европы.
Плюсы: полезно для географически распределенных систем.
Минусы: некоторые сегменты могут стать «горячими точками» с неравномерным трафиком.
Недостатки шардирования
Сложно реализовать, потому что вам придётся самостоятельно писать логику, чтобы знать, из какого шарда выполнять запрос и в какой шард записывать данные. (прим. переводчика - шардирование средствами PostgreSQL опишу на канале)
Разделы хранятся на разных серверах/шардах. Поэтому при выполнении объединений вам приходится извлекать данные из разных шардов, чтобы выполнить объединение с разными таблицами. Это дорогостоящая операция.
Вы теряете согласованность. Поскольку разные части данных находятся на разных серверах. Поэтому поддерживать согласованность сложно.
Подведение итогов масштабирования базы данных
Давайте подведём итоги и запомним эти правила:
Во-первых, всегда и везде отдавайте предпочтение вертикальному масштабированию. Это просто. Вам достаточно увеличить характеристики одного устройства. Если после этого вас настигнут проблемы с производительностью, выполняйте действия ниже.
Когда вы столкнетёсь с интенсивным трафиком(а лучше чуть заранее), реализуйте архитектуру master-slave.
Если у вас много операций записи, используйте шардирование, потому что все данные не поместятся на одном компьютере. Просто старайтесь избегать запросов между шардами.
Если у вас большой трафик, но архитектура «ведущий-ведомый» работает медленно или не справляется с нагрузкой, вы также можете использовать шардирование и распределять нагрузку. Но обычно это происходит в очень больших масштабах.
Базы данных SQL против NoSQL. Когда какую использовать
Выбор подходящей базы данных — важнейшая часть проектирования системы, поэтому внимательно прочитайте этот раздел.
База данных SQL
Данные хранятся в виде таблиц.
Есть предопределённая схема. То есть структура данных (таблицы, столбцы и их типы) должна быть определена до вставки данных.
Соответствует свойствам ACID, обеспечивая целостность и согласованность данных.
Пример: MySQL, PostgreSQL, Oracle, SQL Server, SQLite.
База данных NoSQL
Они делятся на 4 типа:
– Документоориентированные БД(Document-based)
Хранят данные в документах, таких как JSON или BSON.
Пример: MongoDB.
– Хранилища «ключ-значение»(Key-value stores)
Хранят данные в парах «ключ-значение».
Пример: Redis, AWS DynamoDB
– Колоночные СУБД(Column-family stores)
Хранят данные в столбцах, а не в строках.
Пример: Apache Cassandra, ClickHouse
– Графовые СУБД(Graph databases)
Ориентированы на взаимосвязи между данными, которые хранятся в виде графа. Полезно в приложениях для социальных сетей, например, для создания общих друзей, друзей друзей и т. д.
Пример: Neo4j.Такие БД обладают гибкой схемой. То есть, мы можем добавлять новые типы данных или поля, которые могут не быть определены в исходной схеме.
Они не следуют строгому принципу ACID. Отдают приоритет другим факторам, таким как масштабируемость и производительность.
Преимущество гибкой схемы NoSQL:
Гибкость схемы
В NoSQL базе данных, такой как MongoDB, вы можете хранить данные от разных устройств в одной коллекции, даже если их структура отличается.
Пример документа для датчика температуры:
{
"device_id": "sensor_temp_001",
"timestamp": "2023-10-01T12:00:00Z",
"temperature": 25.3,
"location": "Room 101"
}
{
"device_id": "sensor_humidity_001",
"timestamp": "2023-10-01T12:00:00Z",
"humidity": 60.5,
"battery_level": 85
}
Обратите внимание, что структура документов разная, но они могут храниться в одной коллекции. Вспоминаем гибкие в этом плане коллекции питона )
Динамическое добавление полей
Если датчик начинает отправлять новые данные (например, уровень заряда батареи), вы можете просто добавить это поле в документ без изменения схемы коллекции.
Пример обновлённого документа:
{
"device_id": "sensor_temp_001",
"timestamp": "2023-10-01T12:05:00Z",
"temperature": 25.5,
"location": "Room 101",
"battery_level": 90 // Новое поле
}
Запросы к данным
Вы можете выполнять запросы к данным, даже если структура документов разная. Например, найти все документы, где battery_level
меньше 20:
db.sensors.find({ battery_level: { $lt: 20 } });
Масштабирование в SQL по сравнению с NoSQL
SQL СУБД при росте нагрузки в первую очередь стоит масштабировать вертикально - увеличивать аппаратные ресурсы(процессора, оперативной памяти, хранилища) одного сервера для обработки больших объёмов данных.
NoSQL СУБД в первую очередь предназначены для горизонтального масштабирования. То есть, для добавления в кластер дополнительных серверов (узлов) для обработки растущих объёмов данных.
Как правило, шардирование применяется в базах данных NoSQL для обработки больших объёмов данных.
Разделение на сегменты/шарды(прим. переводчика: shard - a small piece or part) также можно реализовать в базе данных SQL. Но, как правило, мы этого избегаем, потому что используем базу данных SQL для реализации ACID гарантий. А обеспечение согласованности данных
("C" в ACID)
становится очень сложной задачей, когда данные распределены по нескольким серверам, а запросы к данным с помощью объединений шардов также сложны и затратны.
Когда использовать какую базу данных?
Если данные неструктурированы и вы хотите использовать гибкую схему, выбирайте NoSQL.Пример: отзывы, рекомендации в приложении для электронной коммерции
Если данные структурированы и имеют фиксированную схему, используйте SQL.Пример: таблица учётных записей клиентов в приложении для электронной коммерции
Если вам нужна целостность и согласованность данных, выбирайте SQL БД, потому что она поддерживает свойство ACID.
Пример:+ Финансовые транзакции, операции с остатками на счетах в банковском приложении
+ Заказы, платежи в приложении для электронной коммерции
+ Платформы для торговли акциямиЕсли вам нужна высокая доступность, масштабируемость (то есть хранение больших объёмов данных, которые не помещаются на одном сервере) и низкая задержка, выбирайте NoSQL из-за горизонтальной масштабируемости и сегментирования.
Пример:
+ Публикации, лайки, комментарии, сообщения в приложении для социальных сетей
+ Храните большие объёмы данных в реальном времени, например, местоположение водителя в приложении для доставкиЕсли вам нужно выполнять сложные запросы, объединения и агрегацию данных, используйте SQL. Как правило, при анализе данных нам приходится выполнять сложные запросы, объединения и т. д. Храните необходимые для этого данные в SQL.
На этом вторая часть перевода подошла к концу. Позже постараюсь сделать новый подход. Изучим разделы Микросервисы, Load Balancer с алгоритмами балансировки нагрузки, Кэширование с примером Redis'a.
Меня зовут Невзоров Владимир. Работаю старшим backend разработчиком на HighLoad проекте с порядком пиковой нагрузки в миллион rps. Приветствую) Веду телеграмм канал по Архитектуре, System Design, Highload бэкэнду.
На канале провожу архитектурные каты, публикую полезные материалы, делюсь опытом. Сейчас с участниками канала разбираем книгу Мартина Клеппмана "Высоконагруженные приложения" на стримах(youtube запись). Скоро встретимся на разборе 3ей главы - событие на timepad.
Для пополнения багажа знаний по теме заходите на мой канал <=
Успехов в дальнейшем изучение темы System Design!
Комментарии (3)
Dhwtj
30.01.2025 14:47Острые углы настолько сглажены что в реальной жизни не пригодно. Только как маразматичка, вспомнить какой-то термин.
beskov
Не оставляет ощущение, что авторы таких текстов предполагают, что если я узнаю составные части вертолёта и их назначение, то я буду уметь проектировать вертолёты
Но что-то мне подсказывает, что это не так
avovana7 Автор
Я думаю всё зависит от цели. Если вовлечься в архитектурную тематику, пообщаться с архитекторами, то может оказаться что не каждый каждый день что-то создаёт. Перед архитектором стоит большой скоуп задач - в т.ч. управление сложностью систем, интеграции, трансформации, которые могут затрагивать целую пачку систем и т.д.
Эту серию статей можно воспринимать как вводную для проблематики System Design. Есть специалист, который изучил свой язык. Разбирается в задачах, функционале, модулях. И решает выходить из этого понятного мира разработки во вне. Приобретать тот самый опыт, теорию, навыки проектирования вертолётов.
Понятно, что ему никто не даст сразу спроектировать такой. Возможно, часть. Возможно, дадут первые интеграции. Т.е. понятно по функционалу как часть системы работает внутри. Теперь давайте взаимодействовать с другими. И получать в багаж опыта все прелести распределенных систем. Начиная от согласовывания межсервисного api, заканчивая реализацией паттернов проектирования таких систем, отладкой, обеспечиванием отказоустойчивой работы.
Вот для такой работы было бы неплохо иметь какое-то теоретическое начальное понимание. Можно закапаться в доку "50 оттенков шардирования в PostgreSQL" до конца не понимания сам принцип. И достать его оттуда. Или же, для начала прочитать такую вводную. Чтобы начать ориентироваться в этой новости плоскости своей деятельности.
Я бы рассматривал эту серию статей как ознакомительные, вводные по тематике System Design.