Вопросы по проектированию распределенных систем, масштабированию, базам данных, кэшированию и очередям сообщений.
CAP-теорема утверждает, что распределенная система хранения данных может гарантировать одновременно не более двух из трех свойств: * **C (Consistency)**: Согласованность. Все узлы видят одинаковые данные в один и тот же момент времени. * **A (Availability)**: Доступность. Каждый неисправный узел возвращает успешный ответ (без гарантии свежести данных). * **P (Partition Tolerance)**: Устойчивость к разделению. Система продолжает работу, даже если связь между узлами потеряна. * **Выбор**: В реальном мире сети всегда могут давать сбои, поэтому выбор P обязателен. Мы выбираем между: * **CP**: Жертвуем доступностью ради согласованности (система возвращает ошибку, если данные нельзя синхронизировать, например, в СУБД с транзакциями). * **AP**: Жертвуем согласованностью ради доступности (система возвращает старые данные, но работает, например, в Cassandra, DynamoDB).
* **Вертикальное масштабирование (Scale-up)**: Увеличение мощности одного сервера (добавление ядер процессора, оперативной памяти, быстрых дисков). Имеет аппаратный предел и ведет к единой точке отказа. * **Горизонтальное масштабирование (Scale-out)**: Добавление дополнительных серверов (нод) в пул системы. Практически не имеет ограничений по масштабу, требует балансировки нагрузки и усложняет архитектуру приложений.
* **Вертикальное партиционирование**: Разделение таблиц по колонкам (например, вынос редко используемых текстовых описаний в отдельную таблицу). * **Шардирование (Горизонтальное партиционирование)**: Разделение строк таблицы по разным серверам БД на основе ключа шардирования (Shard Key). * **Стратегии шардирования**: 1. **Range-based**: По диапазонам значений (например, пользователи с ID от 1 до 1,000,000 на Shard 1). 2. **Hash-based**: По хэшу ключа (например, `hash(userID) % number_of_shards`). 3. **Directory-based**: Через сервис-реестр, хранящий маппинг ключей на адреса серверов.
Согласованное хэширование — это алгоритм хэширования, который минимизирует количество перераспределяемых ключей при добавлении или удалении серверов из пула. * **Принцип**: Ключи и сервера хэшируются на воображаемое логическое кольцо (диапазон значений от `0` до `2^32-1`). Ключ направляется на ближайший сервер по часовой стрелке. * **Преимущество**: При добавлении нового сервера перераспределяется в среднем лишь `1/N` часть ключей (где N — число серверов). При обычном хэшировании по остатку (`hash(key) % N`) изменились бы позиции почти всех ключей. * **Применение**: Распределенные кэши (Memcached), NoSQL хранилища (Cassandra, DynamoDB), балансировщики нагрузки.
* **SQL (Реляционные БД)**: Строгая схема данных, поддержка транзакций (ACID), сложные джоины (JOIN). Хорошо масштабируются вертикально. Примеры: PostgreSQL, MySQL. * **NoSQL**: Гибкая схема (документы, ключ-значение, графы, колонки), горизонтальная масштабируемость «из коробки», компромиссы в согласованности (BASE). Примеры: MongoDB, Cassandra, Redis. * **Выбор**: SQL выбирают для финансовых систем, транзакционных данных, систем со сложными связями. NoSQL — для больших объемов неструктурированных данных, кэшей, каталогов товаров, чатов, логов.
Кэширование хранит горячие данные в быстрой памяти (RAM) для снижения нагрузки на БД и уменьшения задержек. **Стратегии записи**: * **Write-through (Сквозная запись)**: Данные пишутся одновременно в кэш и в БД. Гарантирует согласованность, но замедляет операции записи. * **Write-around (Запись в обход)**: Данные пишутся напрямую в БД. В кэш они попадают только при первом чтении. Разгружает кэш от редко используемых данных. * **Write-back (Запись с задержкой)**: Данные пишутся только в кэш, а в БД сбрасываются асинхронно батчами. Самая быстрая запись, но есть риск потери данных при сбое кэш-сервера.
Инвалидация — это процесс удаления или обновления устаревших данных в кэше. * **TTL (Time to Live)**: Автоматическое удаление записи по истечении заданного времени. * **LRU (Least Recently Used)**: Удаление объектов, которые не запрашивались дольше всего, при заполнении памяти кэша. * **LFU (Least Frequently Used)**: Удаление объектов с наименьшей частотой запросов. * **Явная инвалидация**: Принудительное удаление записи из кэша кодом приложения при обновлении данных в БД.
* **Point-to-Point (Очередь сообщений)**: Одно сообщение обрабатывается ровно одним консьюмером. Сообщение удаляется из очереди после успешного подтверждения обработки. Пример: RabbitMQ (стандартная очередь). * **Publish-Subscribe (Публикация/Подписка)**: Сообщение доставляется всем активным подписчикам темы (Topic). Пример: Apache Kafka, RabbitMQ Fanout exchange.
Apache Kafka — это распределенный распределенный лог коммитов. * **Partition (Партиция)**: Каждая тема разделена на партиции, которые представляют собой упорядоченный append-only файл лога на диске. Это обеспечивает высокую скорость последовательной записи. * **Offset**: Позиция сообщения в партиции. Консьюмеры сами хранят и управляют своим текущим offset'ом. * **Масштабируемость**: Партиции темы могут распределяться по разным серверам (брокерам), что позволяет масштабировать чтение и запись горизонтально.
* **Push модель**: Брокер сам отправляет сообщения консьюмерам по мере их поступления. Плюс — низкая задержка. Минус — риск перегрузить (overflow) консьюмер, если он не справляется с потоком данных (требуется backpressure). Пример: RabbitMQ. * **Pull модель**: Консьюмер сам запрашивает сообщения у брокера по мере своей готовности. Плюс — консьюмер контролирует свою нагрузку и может забирать сообщения батчами. Минус — возможные задержки при пустых запросах. Пример: Apache Kafka.
Балансировщик нагрузки распределяет входящий сетевой трафик по пулу серверов приложений. **Алгоритмы**: * **Round Robin**: Последовательное распределение запросов по кругу. * **Least Connections**: Направление запроса на сервер с наименьшим количеством активных соединений. * **IP Hash**: Направление запросов от одного IP-адреса на один и тот же сервер (для сохранения сессии). * **Weighted Round Robin**: С учетом весов (мощности) серверов.
CDN — это географически распределенная сеть серверов (Edge-серверов), кэширующая статический контент (картинки, видео, скрипты) ближе к конечным пользователям. Запрос пользователя перенаправляется на ближайшую ноду CDN, что минимизирует задержки (RTT) и разгружает основные сервера приложений.
* **Монолит**: Единая база кода, разворачиваемая как один процесс. Прост в разработке, тестировании и деплое на начальных этапах. Сложно масштабировать отдельные компоненты, при падении одной части падает все приложение. * **Микросервисы**: Система состоит из независимых сервисов, общающихся по сети (HTTP, gRPC, очереди). Каждый микросервис имеет свою БД. Позволяет масштабировать компоненты независимо и использовать разный стек технологий, но усложняет инфраструктуру, деплой и обеспечение согласованности данных.
API Gateway — это единая точка входа для клиентов в микросервисную архитектуру. **Функции**: * Маршрутизация запросов к конкретным микросервисам. * Аутентификация и авторизация пользователей. * Ограничение частоты запросов (Rate Limiting). * Сбор метрик и логирование. * Трансляция протоколов (например, HTTP в gRPC).
Так как у каждого микросервиса своя БД, стандартные двухфазные транзакции (2PC) работают неэффективно и блокируют систему. **Паттерн Saga**: Распределенная транзакция разбивается на цепочку локальных транзакций в каждом микросервисе. * **Оркестрация**: Единый сервис-оркестратор управляет шагами транзакции, вызывая методы сервисов по очереди. * **Хореография**: Сервисы общаются через события (Pub/Sub). * **Компенсация**: Если на одном из шагов произошел сбой, Saga запускает цепочку компенсирующих (откатывающих) транзакций в обратном порядке.
Репликация — это копирование данных с одного сервера БД на другие для повышения отказоустойчивости и производительности. * **Master-Slave (Primary-Secondary)**: Все операции записи идут на Master. Master транслирует изменения на Slave-сервера. Чтение происходит со Slave. Разгружает систему при высокой доле чтений. * **Master-Master (Multi-Leader)**: Запись может происходить на любой из серверов. Требует сложных механизмов разрешения конфликтов синхронизации данных.
* **Синхронная**: Master подтверждает клиенту успешную запись только после того, как все реплики подтвердили получение и запись этих данных. Гарантирует отсутствие потерь данных, но увеличивает задержку (latency) записи. * **Асинхронная**: Master подтверждает запись сразу после сохранения у себя, а реплики догоняют его асинхронно. Запись происходит быстро, но при падении Master до синхронизации возможна потеря данных.
CQRS — это разделение операций изменения данных (Commands) и операций чтения данных (Queries) на уровне архитектуры приложения. Они могут использовать разные модели данных и даже разные базы данных (например, запись идет в реляционную PostgreSQL, а чтение оптимизировано в Elasticsearch).
Rate Limiting — это ограничение количества запросов от клиента за единицу времени для защиты системы от перегрузки и DDOS-атак. **Алгоритмы**: * **Token Bucket**: Ведро наполняется токенами с фиксированной скоростью. Запрос тратит токен. Если ведро пусто — запрос отклоняется. * **Leaky Bucket**: Запросы попадают в очередь (ведро) и обрабатываются с фиксированной скоростью. Излишек запросов переливается (отклоняется). * **Sliding Window Log**: Логируются таймштампы всех запросов клиента, и вычисляется их количество за скользящее окно времени.
Паттерн Circuit Breaker защищает систему от каскадных сбоев. Если один из зависимых микросервисов начинает сбоить (таймауты, 5xx ошибки), предохранитель разрывает цепь (состояние **Open**), и все последующие запросы к нему сразу возвращают ошибку или дефолтные данные в обход сети, не дожидаясь таймаутов, давая сбоящему сервису время на восстановление.
* **REST**: Использует HTTP/1.1, данные передаются в текстовом формате JSON. Прост, человекочитаем, поддерживается всеми браузерами из коробки. * **gRPC**: Использует HTTP/2, данные сериализуются в бинарный формат Protocol Buffers. Поддерживает двунаправленный стриминг, строгую генерацию контрактов кода, работает значительно быстрее и потребляет меньше сетевого трафика.
В облачных динамических инфраструктурах адреса (IP-адреса и порты) инстансов микросервисов постоянно меняются при масштабировании или перезапусках. Service Discovery — это реестр (например, Consul, Eureka), в котором регистрируются все запущенные сервисы, позволяя им находить сетевые адреса друг друга.
Heartbeat (пульс) — это периодический сигнал (пинг), отправляемый узлом распределенной системы управляющему серверу или другим узлам для подтверждения того, что он все еще активен и доступен по сети.
* **Cache-Aside**: Приложение само обращается к кэшу. Если данных нет (cache miss), приложение идет в БД, получает данные, записывает их в кэш и возвращает пользователю. * **Read-Through**: Приложение всегда обращается к единому сервису кэша. Если данных в кэше нет, кэш-провайдер сам идет в БД, считывает данные, обновляет себя и возвращает их приложению прозрачно для него.
Колончатые БД хранят данные на диске сгруппированными по колонкам, а не по строкам. Это обеспечивает колоссальную скорость при выполнении аналитических запросов (OLAP), когда нужно агрегировать несколько колонок по миллионам строк, но делает точечную запись строк дорогой. Примеры: ClickHouse, Cassandra.
Репликационный лаг — это задержка по времени между моментом записи данных на Master-сервер и моментом, когда эта запись появляется на Slave-реплике при асинхронной репликации.
Идемпотентность гарантирует, что повторные одинаковые запросы к API вернут тот же результат, что и первый запрос, не вызывая повторных изменений состояния системы (например, двойного списания денег). Обеспечивается передачей уникального ключа идемпотентности (Idempotency Key) в заголовках, который кэшируется на сервере вместе с результатом ответа.
* **Memcached**: Простой, многопоточный, горизонтально масштабируется на стороне клиента с помощью согласованного хэширования (клиент сам решает, на какую ноду положить ключ). * **Redis**: Однопоточный (на ядро), поддерживает сложные структуры данных. Масштабируется с помощью встроенного механизма Redis Cluster с разделением на 16384 хэш-слота и автоматическим перенаправлением запросов.
Распределенный замок гарантирует, что только один процесс в распределенной системе выполняет критическую операцию в данный момент. Часто реализуется в Redis с помощью операции `SET key value NX PX milliseconds` (атомарная запись с проверкой отсутствия ключа и автопродлением TTL) или через алгоритм Redlock.
DNS Anycast — это сетевая технология маршрутизации, при которой несколько физических DNS-серверов в разных частях мира используют один и тот же IP-адрес. Маршрутизаторы сети Интернет автоматически направляют запрос пользователя на физически ближайший к нему сервер, снижая задержку ответов.
* **CAP теорема**: В распределенной системе при наличии разделения сети (Partition tolerance) можно обеспечить только один из двух параметров: либо согласованность данных (Consistency), либо доступность (Availability). * **PACELC теорема**: Расширяет CAP. Если есть разделение сети (P), как система балансирует между доступностью (A) и согласованностью (C)? Иначе (E — Else), когда разделения нет, как система балансирует между задержкой (L — Latency) и согласованностью (C)? * Пример: MongoDB выбирает согласованность (PC/EC), а DynamoDB — доступность и низкую задержку (PA/EL).
* **Партиционирование (Partitioning)**: Разделение таблицы базы данных на логические части в рамках **одного физического сервера** (например, разбиение таблицы логов по месяцам в PostgreSQL). Прозрачно для приложения. * **Шардирование (Sharding)**: Распределение строк таблицы по **разным физическим серверам** (шардам) на основе ключа шардирования. Требует логики маршрутизации запросов на уровне приложения или прокси-слоя баз данных.
Последовательное хэширование решает проблему распределения ключей по динамическому набору серверов (например, в распределенном кэше). * **Принцип**: Хэш-функция отображает как сервера, так и ключи на воображаемый замкнутый круг (кольцо хэшей). Ключ сохраняется на ближайшем сервере при движении по часовой стрелке. * **Плюс**: При добавлении или удалении сервера нужно перераспределить лишь малую часть ключей (`1/N`), в то время как обычное хэширование `hash(key) % N` потребовало бы перезаписи почти всех ключей.
* **B-Tree**: Сбалансированное дерево поиска. Данные обновляются на месте (in-place update) в блоках диска. Обеспечивает быстрое чтение за `O(log N)` и эффективные интервальные запросы. Оптимально для реляционных БД (PostgreSQL, MySQL). * **LSM-Tree**: Дерево слияния логов. Данные пишутся только в конец лога (append-only) в памяти (MemTable), а затем сбрасываются на диск в виде неизменяемых файлов (SSTables), которые периодически сливаются (Compaction). Обеспечивает экстремально быструю запись. Используется в NoSQL (Cassandra, RocksDB).
Split-brain («раздвоение мозга») возникает при разделении сети, когда кластер делится на две изолированные части, которые не видят друг друга. Каждая часть считает, что противоположная сторона упала. Если обе части объявят себя лидерами и начнут принимать запись данных, возникнет рассинхронизация и порча базы. Для решения проблемы используют механизмы **кворума** (Quorum: `N/2 + 1` узлов должны подтвердить лидера) и алгоритмы консенсуса (Raft, Paxos).
* **Row-oriented (строковые, OLTP)**: Данные одной строки хранятся на диске последовательно. Отлично подходит для транзакционных систем, где нужно быстро читать и обновлять конкретные сущности целиком (например, данные профиля пользователя). Пример: PostgreSQL. * **Column-oriented (колоночные, OLAP)**: Данные одной колонки хранятся вместе. Позволяет читать только нужные колонки при аналитических запросах (например, посчитать сумму продаж за год), данные отлично сжимаются. Пример: ClickHouse.
* **Cache-Aside**: Приложение сначала читает кэш. Если данных нет, читает БД, записывает в кэш и возвращает. Обновление идет напрямую в БД, кэш инвалидируется. * **Write-Through**: Приложение пишет в кэш, а кэш-система сама синхронно записывает данные в БД. Низкий риск потери данных, но выше задержка записи. * **Write-Back (Write-Behind)**: Данные пишутся только в кэш, а в БД сбрасываются асинхронно батчами. Быстрая запись, но риск потери данных при сбое кэша.
Когда кэш переполняется, нужно удалить старые данные: * **LRU (Least Recently Used)**: Удаляет элементы, к которым дольше всего не обращались. Реализуется через хэш-карту и двусвязный список (Doubly Linked List). * **LFU (Least Frequently Used)**: Удаляет элементы, к которым обращались реже всего (подсчитывается частота вызовов). Требует больше памяти для хранения счетчиков частоты.
* **At-most-once (максимум один раз)**: Сообщение отправляется без подтверждения. Возможна утеря сообщений, но дубликаты исключены. * **At-least-once (минимум один раз)**: Отправитель требует подтверждения (`ACK`). Если подтверждение потерялось, сообщение отправляется повторно. Утери нет, но возможны дубликаты. * **Exactly-once (ровно один раз)**: Гарантирует доставку без потерь и дублей. Требует поддержки транзакций со стороны брокера сообщений или дедупликации на стороне потребителя (идемпотентность).
Kafka масштабируется за счет **партиционирования (Partitions)** топиков: * Топик делится на несколько партиций, распределенных по разным брокерам. * Запись в партицию идет строго последовательно (append-only log). * Каждая партиция может читаться только одним консьюмером из конкретной группы потребителей (Consumer Group) в один момент времени. Это гарантирует порядок обработки сообщений в рамках партиции и горизонтальное масштабирование чтения.
В облачных инфраструктурах IP-адреса сервисов постоянно меняются при перезапусках. Service Discovery — это реестр доступных сервисов: * **Регистрация**: Сервис при старте сообщает свой адрес в реестр (Consul, Eureka) и периодически присылает сигналы здоровья (Heartbeats). * **Поиск**: Клиенты обращаются к реестру или локальному балансировщику, чтобы получить актуальный список IP-адресов целевого сервиса для отправки запросов.
Circuit Breaker («предохранитель») оборачивает вызовы к внешним сервисам. Он имеет три состояния: 1. **Closed (Закрыт)**: Нормальная работа, запросы проходят. 2. **Open (Открыт)**: Если процент ошибок превышает порог, предохранитель размыкается. Запросы не отправляются наружу, а сразу возвращают ошибку (или дефолтный ответ), снижая нагрузку на упавший сервис. 3. **Half-Open (Полуоткрыт)**: По истечении таймаута пропускается тестовая группа запросов. Если они успешны, предохранитель закрывается, иначе — снова открывается.
* **Token Bucket**: В бакет с фиксированной скоростью добавляются токены. Запрос забирает токен. Если токенов нет, запрос отклоняется. Позволяет обрабатывать кратковременные пиковые всплески трафика (bursts), пока в бакете есть накопленные токены. * **Leaky Bucket**: Запросы попадают в бакет (очередь) и «вытекают» (обрабатываются) строго с постоянной скоростью. Сглаживает любой трафик до постоянной величины, задерживая всплески.
Переход оправдан, когда: * Команда разработки выросла (более 20-30 человек) и разработка в одном репозитории приводит к постоянным конфликтам слияния и блокировкам релизов. * Разным частям системы требуются разные масштабы ресурсов (например, один модуль требует много CPU, другой — GPU, третий — памяти). * Требуется независимый цикл деплоя критических модулей. * **Внимание**: Микросервисы усложняют мониторинг, транзакции и отладку.
Трейсинг позволяет отследить путь запроса через цепочку микросервисов: 1. На входе запроса (API Gateway) генерируется уникальный идентификатор `Trace ID` и `Span ID` первого шага. 2. Эти идентификаторы передаются в HTTP-заголовках (например, стандарт W3C Trace Context) при всех последующих вызовах других микросервисов. 3. Каждый микросервис логирует свои шаги (Spans) с привязкой к единому Trace ID и отправляет метрики в систему сбора трейсов (Jaeger, Zipkin).
Event Sourcing — паттерн, при котором состояние сущности не перезаписывается в БД, а хранится в виде неизменяемой последовательности событий (например, «создан заказ», «добавлен товар», «оплачен»). * **Плюсы**: Идеальный аудит-лог событий, возможность восстановить состояние сущности на любой момент времени в прошлом, отсутствие блокировок при записи (только вставка в лог). * **Минусы**: Сложно читать текущее состояние (требуется проиграть все события с начала или использовать снимки-snapshots).
Saga заменяет классическую распределенную транзакцию (2PC) цепочкой локальных транзакций в микросервисах: * Каждый сервис выполняет свою локальную транзакцию и публикует событие. * Следующий сервис слушает событие и запускает свою транзакцию. * **Компенсирующие транзакции**: Если на каком-то шаге произошел сбой, Saga запускает обратные транзакции в выполненных ранее сервисах для отката изменений (например, возврат денег на баланс, если склад не подтвердил наличие товара).
* **SQL (реляционные)**: Нужна строгая транзакционность (ACID), сложные джоины (JOIN) и гибкие аналитические запросы. Подходит для финансовых данных. Пример: PostgreSQL. * **NoSQL (нереляционные)**: Нужна простая структура данных (Key-Value, Document), горизонтальное масштабирование «из коробки» под огромный трафик на запись, высокая доступность при слабой согласованности. Пример: Cassandra, MongoDB.
CDN — это географически распределенная сеть серверов (Edge-нод). Когда пользователь запрашивает статический файл (картинку, JS-скрипт, видео), DNS-сервер CDN перенаправляет запрос на сервер, который физически находится ближе всего к пользователю. Если на Edge-ноде файла нет, она скачивает его с оригинального сервера приложения (Origin), кэширует у себя и отдает пользователю.
API Gateway служит единой точкой входа для внешних клиентов: * **Маршрутизация (Routing)**: Перенаправляет запросы на нужные внутренние микросервисы. * **Безопасность**: Валидация токенов авторизации (JWT), SSL-termination. * **Защита**: Ограничение частоты запросов (Rate Limiting), Circuit Breaking. * **Трансформация**: Форматирование заголовков, сборка ответов из нескольких сервисов.