Архитектурные паттерны для высокой масштабируемости. Часть 2

Итак, в прошлой статье я показал, что 90% систем не имеют такого масштаба чтобы оправдать микросервисы. И большинству архитекторов когда они обосновывают микросервисы только целями масштабирования я хочу сказать

Ваша система не доросла до необходимости использовать микросервисов

Ваша система не доросла до необходимости использовать микросервисов

Я не рассматриваю здесь другие доводы за микросервисы: упрощение доработок легаси и deploy fast (CI/CD). Об этом в другой статье.

И все же рассмотрим как распределенные архитектуры (и микросервисы в частности) помогают реализовать high load системы

Data-bounded шаблоны масштабирования — окончание

Чтобы не усложнять статью я не рассматриваю здесь остальные паттерны распределенной архитектуры, кроме микросервисов:

  • SOA (Service-Oriented Architecture, Сервисно-ориентированная архитектура), сервисы связаны напрямую или через ESB

  • SBA (Service-Based Architecture), сервисы обычно знают только о своих соседях и взаимодействуют напрямую, что делает архитектуру менее централизованной

  • Space-Based Architecture, единая БД, локальные синхронизируемые кеши чтения и записи либо in memory data grid

Они все основаны на единой БД и с точки зрения распределения данных одинаковы. Из-за отсутствия распределенности данных нет и проблемы их консистентности. Впрочем, кэширование записываемых данных уже приводит к Eventual Consistency.

Более полное описание можно найти в книге «Фундаментальный подход к программной архитектуре: паттерны, свойства, проверенные методы» Марк Ричардс и Нил Форд

Распределенные системы. Микросервисы

Для упрощения я ставлю знак равенства, что конечно же не так.

Заблуждения распределенных вычислений:

  • Сеть надежна;

  • Задержка равна нулю;

  • Пропускная способность бесконечна;

  • Сеть безопасна;

  • Топология не меняется;

  • Есть один администратор;

  • Стоимость транспортировки равна нулю;

  • Сеть однородна;

  • Сторона, с которой вы общаетесь, заслуживает доверия

  • Управление версиями — это просто

  • Компенсирующие обновления всегда работают

  • Наблюдаемость необязательна

Микросервисы основаны на разделении данных по доменам и позволяют разделить систему на независимые сервисы, каждый из которых отвечает за строго определенную область (bounded context) и работает со своим набором данных. Это ограничивает распространение данных только внутри одной области, что упрощает масштабирование и изоляцию изменений.

Преимущества

Преимущества не только в повышении масштабируемости, но и в других факторах, которые также надо учитывать при выборе архитектурного решения

  1. Радикальная изоляция данных.

    • Каждый микросервис имеет свою базу данных, что снижает зависимость от других сервисов и минимизирует риски влияния ошибок в одном компоненте на всю систему.

    • Это позволяет масштабировать каждый сервис независимо, в зависимости от его нагрузки.

  2. Гибкость разработки и развертывания. Мультитехнологичность

    • Микросервисы можно разрабатывать на разных языках программирования и разворачивать независимо друг от друга.

    • Изменения в одном сервисе не требуют перезапуска всей системы.

  3. Ускорение Time-to-Market. CI/CD

Частично эти свойства можно получить и при использовании монолита.

  • Вместо CI/CD будет blue-green deploy и часто этого достаточно. Также можно использовать плагин архитектуру для отключения компонента в виде dll, его замены и запуска. Прямые вызовы кода и единая БД при этом остаются.

  • Мультитехнологичность не всегда полезна, а в небольших компаниях и нежелательна. Исключение, пожалуй, это древний легаси. Но и тут по моему опыту очень неохотно переходят на другие языки и фреймворки

Trade-offs

  1. Жертва консистентностью.

    • Данные между микросервисами синхронизируются через асинхронные сообщения или события, что приводит к eventual consistency и необходимости увязывания данных, например чрезе паттерн Сага (разбить общую логику на цепочку локальных транзакций, где каждая выполняется в одном сервисе, а в случае ошибки вызываются «компенсирующие транзакции» — отмена).

    • Это требует дополнительных усилий для обеспечения согласованности данных и обработки ошибок.

  2. Сложность взаимодействия.

    • Микросервисы требуют хорошо продуманной архитектуры взаимодействия (API Gateway, REST, gRPC и т. д.).

    • Появляется необходимость в управлении зависимостями между сервисами и обработке отказов.

  3. Сложность инфраструктуры.

    • Для работы микросервисов нужна развитая инфраструктура: оркестрация контейнеров (Kubernetes), системы мониторинга (Prometheus, Grafana), централизованное логирование и трассировка (Jaeger, Elastic Stack).

    • Увеличивается нагрузка на DevOps-команду.

  4. Рост накладных расходов.

    • Сетевые взаимодействия между микросервисами создают дополнительные задержки и увеличивают потребление ресурсов.

    • Трассировка и отладка распределенных систем становятся сложнее.

  5. Высокие требования к команде.

    • Разработка и поддержка микросервисов требуют знаний в области DevOps, управления распределенными системами и асинхронного взаимодействия.

Микросервисы могут использовать множество приемов из других архитектурных паттернов:

  • CQRS и Event Sourcing: Для разделения чтения и записи, а также хранения истории событий.

  • Кэширование: Для ускорения работы и снижения сетевой нагрузки.

  • Шардирование и репликация: Для масштабирования внутри микросервиса.

Эта интеграция делает микросервисы мощным инструментом, но одновременно увеличивает сложность разработки.

Микросервисы обычно дают наибольшую в сравнении с остальными паттернами изоляцию данных и возможность масштабировать систему поэтапно, но за это приходится платить сложностью инфраструктуры, взаимодействия и поддержания консистентности данных. Их использование оправдано только в системах с высокой нагрузкой, сложной бизнес-логикой или потребностью в независимом развитии разных частей системы. Для менее сложных систем стоит начинать с более простых паттернов, таких как шардирование, репликация или CQRS.

Даже если вы не собираетесь разделять приложение на микросервисы разбить его на домены практически необходимо чтобы ограничить рост зависимостей и необходимых изменений в разных частях приложения.

Вот краткое описание для каждого типа согласованности, включая 2–3 проблемы, последствия, а также примеры приложений и объяснение, почему эти проблемы не критичны для них.

Обновлённые последствия с примерами:

1. Strong Consistency (Сильная согласованность)

  • Проблемы:

  • Последствия:

2. Eventual Consistency (Сходимость к согласованности)

  • Проблемы:

  • Последствия:

    • Пользователи могут видеть устаревшие данные (от секунд до минут).

    • Конфликты данных могут привести к потере или дублированию информации.

      • Пример: два пользователя могут одновременно изменить поле профиля в системе, и один из вариантов будет перезаписан (last write wins).

    • Некорректные результаты аналитических запросов.

      • Пример: в онлайн-магазине количество товара может отображаться как 10 единиц, хотя фактически их уже осталось 5.

3. Linearizability (Линейная согласованность)

  • Проблемы:

  • Последствия:

    • Увеличение задержек из-за необходимости глобальной синхронизации.

    • Рост времени отклика в критических разделах системы.

      • Пример: при изменении конфигурации кластера в Zookeeper обновление может занять несколько секунд, чтобы обеспечить строгий порядок выполнения операций.

4. Snapshot Isolation (Изоляция снимков)

  • Проблемы:

  • Последствия:

    • Фантомные записи могут привести к некорректным аналитическим данным.

      • Пример: аналитический запрос в базе данных может показать 100 заказов, хотя после начала транзакции добавилось ещё 5, и они не учитываются.

    • Рост потребления памяти для хранения снимков данных.

      • Пример: в PostgreSQL или Snowflake длительная транзакция увеличивает объём временных данных, что может замедлить выполнение других запросов.

    • Ошибки при выборке данных.

      • Пример: при анализе продаж за день аналитик может не увидеть новые заказы, которые были добавлены параллельно с выполнением запроса.

5. Causal Consistency (Каузальная согласованность)

  • Проблемы:

  • Последствия:

    • Сообщения и изменения могут отображаться в неверном порядке.

      • Пример: в мессенджере ответ на сообщение может появиться раньше оригинального сообщения, если узлы реплицируются асинхронно.

    • Некорректное отображение изменений в системах совместной работы.

      • Пример: в Google Docs изменения одного пользователя могут быть видны с задержкой, что приводит к временным расхождениям в документе.

    • Нарушение логики работы приложения.

      • Пример: в Trello пользователь может переместить карточку в колонку, но другой пользователь увидит её в старом состоянии в течение нескольких секунд.

6. Client-Centric Consistency Models (Клиентоцентричные модели)

  • Проблемы:

  • Последствия:

    • Один клиент видит изменения, а другой — нет.

    • Разные версии данных на разных устройствах.

      • Пример: в Gmail письмо может быть помечено как прочитанное на телефоне, но на ноутбуке оно ещё отображается как непрочитанное.

    • Отложенная репликация может привести к несоответствиям.

      • Пример: в CDN обновлённая версия сайта может быть доступна в одном регионе, но старая версия продолжает кэшироваться в другом регионе в течение часа.

Обобщённые примеры для всех моделей:

  • Strong Consistency: финансовые транзакции и биржевые операции требуют мгновенной синхронизации данных.

  • Eventual Consistency: лайки, комментарии и уведомления могут обновляться с задержкой.

  • Linearizability: распределённые базы выполняют сложные транзакции с глобальной координацией.

  • Snapshot Isolation: аналитические запросы могут работать с устаревшими данными, но сохраняют изоляцию от текущих операций записи.

  • Causal Consistency: порядок сообщений в чатах и документах важен для корректной работы.

  • Client-Centric Models: мобильные приложения и кэш-системы обеспечивают согласованность только для отдельных пользователей, но не для всей системы.

Сводная таблица моделей согласованности

Тип согласованности

Проблемы

Последствия

Примеры приложений

Почему проблемы не критичны

Strong Consistency

— Высокая задержка — Низкая доступность

— Долгие задержки (банковские переводы до 1 мин) — Недоступность при сбое (блокировка транзакций)

Банковские системы, торговые биржи

Точность данных критична. Ошибка может привести к финансовым потерям.

Eventual Consistency

— Временная несогласованность — Конфликты данных

— Устаревшие данные (лайки до 30 сек) — Конфликты записей (профили перезаписываются)

Социальные сети, NoSQL базы данных

Небольшие задержки в обновлении данных не влияют на UX.

Linearizability

— Высокая стоимость — Сложность реализации

— Задержки (200 мс в Spanner) — Замедление работы кластера (секунды в Zookeeper)

Google Spanner, Zookeeper

Строгий порядок операций важен для предотвращения ошибок в критических системах.

Snapshot Isolation

— Фантомные чтения — Высокие ресурсы

— Пропуск новых данных (до 5 записей) — Рост потребления памяти (PostgreSQL, Snowflake)

Snowflake, PostgreSQL, MySQL

В аналитике важнее изоляция, чем актуальность данных в реальном времени.

Causal Consistency

— Сложность отслеживания — Увеличение метаданных

— Неверный порядок сообщений (ответы раньше вопросов) — Задержка обновлений (Trello, Google Docs)

WhatsApp, Google Docs, Trello

Пользователям важен правильный порядок операций, небольшие задержки допустимы.

Client-Centric Models

— Сложная синхронизация — Несогласованность системы

— Разные данные на устройствах (письма в Gmail) — Отложенная репликация (CDN обновляется до 1 часа)

Instagram, Gmail, CDN

Важно, чтобы клиент видел свои изменения сразу, даже если другие пользователи их видят позже.

Объяснение таблицы:

  • Проблемы — основные технические трудности, возникающие при использовании модели.

  • Последствия — реальные примеры задержек, устаревших данных и конфликтов.

  • Примеры приложений — системы и сервисы, где используется данная модель согласованности.

  • Почему проблемы не критичны — объяснение, почему эти проблемы допустимы или незначительны для конкретных приложений.

Таблица позволяет быстро сравнить модели согласованности и понять, какую из них лучше использовать в зависимости от специфики приложения.

здесь были указаны только основные модели, подробней здесь

https://jepsen.io/consistency

https://www.researchgate.net/figure/Consistency-Models-based-on-Reference-14_fig2_331104869

4. Философские мысли о природе ограничений data-bounded масштабируемости. Осторожно, матан!

Когда мы сталкиваемся с ограничениями data-bounded масштабируемости, то неизбежно упираемся в фундаментальные законы распределенных систем. Эти законы определяют, какой ценой достигается производительность и масштабируемость, и на что нужно закрыть глаза ради достижения желаемого результата.

Конечная скорость распространения данных

Скорость распространения данных в распределенной системе ограничена физически — скоростью передачи данных по сети, задержками из-за обработки данных узлами, а также синхронизацией между ними. Это фундаментальное ограничение заставляет нас жертвовать консистентностью, чтобы добиться большей скорости и доступности системы.

Сюда отлично вписывается посыл, сформулированный мной в 1й части статьи:

Распространение данных / состояния по системе требует времени. Если требуется консистентность данных во всей системе то нужно ждать пока данные распространятся по всей системе. Очевидные способы борьбы за скорость в распределенной архитектуре

  • не требовать одновременной консистентности во всей системе (event sourcing, cqrs, no acid)

  • требовать консистентности данных, но лишь в ограниченных областях системы (bounded context, например в микросервисах) и не распространять данных за их пределы

  • не ждать пока данные распространятся и проверять консистентность уже после изменений (паттерн сага)

  • отказ от распределенной архитектуры или объединение неудачно разделенных микросервисов

Очевидные способы борьбы за скорость в монолитной архитектуре

Свойства ACID

ACID — это гарантии транзакций в базах данных:

  • Atomicity (атомарность): Все операции внутри транзакции выполняются как единое целое (или не выполняются вовсе).

  • Consistency (консистентность): Данные всегда остаются в согласованном состоянии.

  • Isolation (изоляция): Одновременные транзакции изолированы друг от друга.

  • Durability (устойчивость): Записанные данные сохраняются даже в случае сбоев.

Когда мы говорим об оптимизации, то ослабляем одно или несколько свойств ACID:

  • Ослабление Consistency приводит к eventual consistency (согласованность наступает со временем).

  • Ослабление Isolation позволяет выполнять больше транзакций параллельно за счет возможных конфликтов.

  • Ослабление Durability может ускорить операции записи, но приводит к риску потери данных.

Теперь матан про ограничения в распределенной арзитектуре

CAP-теорема

CAP-теорема утверждает, что в распределенной системе невозможно одновременно обеспечить:

  • Consistency (согласованность): Все узлы видят одни и те же данные в одно и то же время.

  • Availability (доступность): Каждый запрос получает успешный ответ (без гарантии согласованности).

  • Partition tolerance (устойчивость к разделению): Система продолжает работать, несмотря на сбои связи между узлами.

В реальных системах всегда приходится выбирать между согласованностью и доступностью в условиях сетевых разделений.

Пример: Системы типа NoSQL (например, Cassandra) чаще выбирают доступность и устойчивость к разделению, жертвуя сильной согласованностью данных.

Теорема PACELC

PACELC расширяет CAP-теорему, добавляя еще одну дихотомию:

IF P THEN (C or A), ELSE (C or L).

Если система сталкивается с сетевым разделением, т.е. временной недоступностью узла (P), то она должна выбрать между C (согласованностью) и A (доступностью).
Если же разделения нет (ELSE), то она должна выбрать между C (согласованностью) и L (задержкой, латентностью).

Пример: DynamoDB от AWS, в зависимости от конфигурации, позволяет выбирать между строгой согласованностью (strict consistency) или более низкой задержкой (low latency).

Практическое применение CAP и PACELC

CAP и PACELC чаще всего используются для выбора подходящей базы данных. Например:

  • Если вам нужна максимальная доступность и низкая задержка (например, для системы рекомендаций), вы можете выбрать систему типа Cassandra или DynamoDB.

  • Если же важна строгая консистентность (например, для банковских операций), лучше подойдет реляционная база данных с поддержкой ACID.

5. Шаблоны CPU-bounded масштабирования

Когда узким местом системы становится не обработка данных, а доступные вычислительные ресурсы (CPU), приходится искать другие подходы к масштабированию. В таких случаях ключевыми инструментами становятся:

Горизонтальное масштабирование

Добавление новых серверов или узлов в систему позволяет распределить нагрузку между ними. При этом важно, чтобы архитектура приложения поддерживала горизонтальное масштабирование (например, через stateless-сервисы).

Пример: Kubernetes и Docker позволяют легко управлять контейнерами, которые можно масштабировать в зависимости от нагрузки.

Параллельная обработка

Разделение задач на более мелкие независимые части, которые могут выполняться параллельно, позволяет значительно снизить время обработки.

Пример: MapReduce и его аналоги (Hadoop, Apache Spark) используются для обработки больших объемов данных параллельно на множестве узлов.

Стоит не забывать и о параллельной обработке на одном узле, чтобы эффективно использовать вертикальное масштабирование на сотни потоков в пределах одного сервера. Тут помогают такие методы как функциональное программирование.

Асинхронная обработка

Не все операции необходимо выполнять синхронно. Асинхронные вызовы позволяют разгрузить основное приложение и обрабатывать задачи в фоновом режиме.

Пример: Использование очередей сообщений (RabbitMQ, Kafka) для выполнения длительных операций.

Оптимизация алгоритмов

Иногда простая оптимизация алгоритмов может значительно снизить нагрузку на CPU. Например, использование более эффективных структур данных или методов сортировки.

6. Практические выводы

Интуитивно понятные эвристики для определения времени обеспечения консистентности данных

Определение времени, необходимого для обеспечения консистентности данных в распределенной системе, — это сложная задача, требующая понимания как технических ограничений, так и бизнес-требований. Ниже приведены интуитивно понятные эвристики, которые помогут вам оценить время достижения консистентности и принять меры для его оптимизации.

Оценка времени распространения данных

  • Максимальное время сети (Network Latency): Определите максимальное время, за которое данные могут быть переданы между самыми удаленными узлами в вашей системе. Это включает время на сетевую передачу, обработку на каждом узле и возможные задержки из-за загрузки сети.

  • Время на обработку (Processing Time): Оцените время, необходимое узлам для обработки входящих данных. Это может включать время на валидацию, запись в базу данных, и другие бизнес-операции.

  • Время на репликацию (Replication Time): Если вы используете репликацию, добавьте время, необходимое для синхронизации данных между репликами. Обратите внимание на стратегию репликации (синхронная или асинхронная) и её влияние на время.

Определение допустимой задержки (Tolerable Latency)

  • Бизнес-требования: Спросите у бизнес-стейкхолдеров, какая максимальная задержка в достижении консистентности данных допустима. Например, в некоторых системах задержка в несколько секунд может быть приемлема, в то время как в других (например, в финансовых системах) требуется почти мгновенная консистентность.

  • Пользовательский опыт: Оцените, как задержка влияет на пользовательский опыт. Если задержка приводит к значительным неудобствам, это может стать ключевым фактором в принятии решений.

Выбор подходящего уровня консистентности

  • Strong Consistency: Если требуется немедленная консистентность, используйте синхронные операции и строгие гарантии ACID. Однако это может привести к снижению производительности и доступности.

  • Bounded Staleness, Session, Consistent Prefix и другие промежуточные варианты, где данные становятся консистентными в пределах определенного времени или количества операций. Подходит для сценариев, где допустима небольшая задержка, но важна предсказуемость.

  • Eventual Consistency: Если допустима задержка, рассмотрите асинхронные операции и eventual consistency. Это улучшит производительность и масштабируемость, но требует дополнительных механизмов для обработки конфликтов и восстановления состояния.

В конечном итоге выбор подхода к масштабированию зависит от конкретных требований системы. Вот несколько ключевых рекомендаций:

  1. Начинайте с простого. Прежде чем переходить к сложным архитектурным решениям, попробуйте оптимизировать текущую систему (например, за счет кэширования, репликации или шардирования).

  2. Понимайте trade-offs. Каждый архитектурный паттерн имеет свои компромиссы. Например, микросервисы увеличивают изоляцию, но усложняют управление системой.

  3. Не изобретайте велосипед. Используйте готовые решения и инструменты (например, Kubernetes для оркестрации, Redis для кэширования, Kafka для обработки событий).

  4. Масштабируйте постепенно. Если текущие методы оптимизации перестают работать, переходите к более сложным подходам, таким как CQRS или микросервисы.

  5. Моделируйте ограничения. Используйте теоремы CAP и PACELC, чтобы понять, какие компромиссы приемлемы для вашей системы.

  6. Не забывайте о стоимости. Более сложные архитектуры, такие как микросервисы, могут быть дорогими в разработке и поддержке. Убедитесь, что они действительно необходимы.

В конечном итоге цель любой системы — найти баланс между производительностью, масштабируемостью и сложностью, который будет соответствовать требованиям бизнеса.

В третьей, заключительной части я напишу про инструменты для реализации высокой масштабируемости приложений: кеширования, шардирования, репликации, про различные типы СУБД и в какой мере они обеспечивают баланс между консистентностью и скоростью, про инструментарий требуемый для работоспособности микросервисов

А также немножко про OAuth / OpenID / WebAuth, jwt, шифрование, kafka, SSO, web socket, REST

© Habrahabr.ru