Эта статья не о «правильной» архитектуре. Здесь я постарался ответить на вопросы: почему микросервисы дороже, какие компромиссы неизбежны и по каким критериям выбирать архитектуру?

Мы поговорим о высоконагруженных распределённых системах и почему монолиты — это не плохо. Также обсудим необходимость рационального использования ресурсов и критерии выбора типа архитектуры.


Что такое высоконагруженные системы

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

Ключевые критерии:

  • Отказоустойчивость (надёжность): система должна оставаться работоспособной даже при отказе отдельных компонентов

  • Масштабируемость: возможность увеличивать производительность без значительных изменений

  • Эффективное использование ресурсов: баланс между вычислительной мощностью, хранением данных и сетевой инфраструктурой (процессоры, память, базы данных, хранилище, сеть)

  • Гибкость (удобство сопровождения): лёгкость поддержки и адаптации системы к меняющимся требованиям

Про гибкость, эффективное использование ресурсов и масштабируемость обычно всё понятно, но что насчёт отказоустойчивости?

Отказоустойчивость

Как пишет Мартин Клеппман:

«Всё ломается: оборудование, ПО, люди. Отказоустойчивость — это не про идеальность, а про минимизацию ущерба».

Отказоустойчивость — это способность системы продолжать работу, несмотря на сбои в отдельных компонентах. Неважно, насколько «умны» инженеры или сколько денег в облаке, сбои неизбежны. Вопрос в том, как система отреагирует на эти инциденты. Речь идёт не о том, чтобы «всё работало вс��гда», а о том, чтобы «отказ не превратился в катастрофу».

Ключевые критерии:

  • Аппаратные: серверы выходят из строя, перегреваются, теряют питание

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

  • Эффективное использование ресурсов: баланс между вычислительной мощностью, хранением данных и сетевой инфраструктурой (процессоры, память, базы данных, хранилище, сеть)

  • Человеческие: неправильно настроенные конфигурации — от этого никто не застрахован (даже у Amazon бывает, что инженеры «роняют продакшен»: например, в 2017 г. опечатка команды привела к удалению критичных узлов S3, что вызвало масштабный сбой в работе интернета — об этом можно прочитать в отчёте AWS и разборе инцидента)

Как проектировать отказоустойчивые системы

Для себя я выделил следующие ключевые принципы:

  1. Предполагаем, что всё сломается. Закон Мёрфи гласит: «Если что-то может пойти не так, это обязательно произойдёт». Ни один сервис не вечен. Наша задача — сделать так, чтобы отказ не касался пользователя (или касался минимально).

  2. Моделируем отказ заранее. Тестируем систему на устойчивость, применяя комплексный подход: от модульного и нагрузочного тестирования до практики «Chaos Engineering». Последняя методика (хаотичное тестирование отказов) стала популярной после того, как Netflix в 2011 г. запустил инструмент Chaos Monkey. Суть заключается в контролируемом имитировании сбоев (например, скачков трафика, обрывов сети, отказа узлов) для оценки реакции системы. Ещё похожие утилиты: https://github.com/gremlin, https://github.com/alexei-led/pumba.

  3. Изолируем. Отказ одного сервиса не должен приводить к сбою всей системы. Для этого используем шаблон Circuit Breaker (автоматическое отключение неисправного компонента), возвращаем заглушки (упрощённые ответы по умолчанию) и отключаем часть функциональности при нарушении SLA.

  4. Минимизируем критичные зависимости. Не делаем систему, в которой падение брокера кладёт веб.

  5. Настраиваем журналирование, мониторинг, оповещения. Грамотно настроенные метрики с оповещениями, трассировка и журналы дают возможность решить проблему или предотвратить её. Алекс Сюй в своей книге отметил: «Отказоустойчивость начинается с наблюдаемости. Если ты не видишь, что система падает — значит, она уже умерла».

Цена распределённости

СAP-теорема

Эрик Брюер представил эту теорему в 2000 г. на симпозиуме по принципам распределённых вычислений. Она гласит: в распределённых системах одновременно можно добиться только двух состояний из трёх: 

  • Согласованность (Consistency): все клиенты при чтении получают одни и те же актуальные данные, запись видна либо всем, либо никому

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

  • Устойчивость к разделению (Partition Tolerance): даже если между узлами отсутствует связь, они продолжают функционировать независимо друг от друга

Как это выглядит на практике

Жертвуем Availability ради Consistency (CP). Вы не можете показать клиенту баланс, который может быть неточным. Лучше ответить: «операция временно недоступна», чем отобразить некорректные данные.

Жертвуем Consistency ради Availability (AP). Можно позволить пользователю отправить комментарий, даже если реплика на другом узле получит его с небольшой задержкой.

А что насчёт Partition Tolerance? Разрыв сети между узлами приведёт к падению всей системы:
она не сможет гарантировать согласованность, а значит, перестанет функционировать.

В реальных системах всегда нужно обеспечивать Partition Tolerance, поскольку сетевые сбои — не гипотетическая угроза, а повседневная реальность. Это означает, что выбирать придётся между согласованностью и доступностью. Это не опциональный выбор, а обязательное требование к любой системе, работающей по сети. Даже если у вас два сервиса в одном ЦОДе, между ними может разорваться соединение, и тогда архитектура должна предусматривать, как с этим работать.

CAP-теорема слишком упрощает и не подходит для точного описания и критики распределённых систем, но становится отправной точкой для понимания проблем в этих системах. Для более глубокого понимания рекомендую изучить материалы тут.

ACID

Джим Грей — один из программистов, благодаря которым современные сервисы умеют «держать слово» перед пользователем. Он был исследователем и инженером, много лет работал над системами хранения данных и получил премию Тьюринга за вклад в теорию и практику обработки транзакций.

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

В конце 1970-х годов Джим Грей сформулировал требования к транзакционной системе:

  • Атомарность (Atomicity): либо будет выполнено всё, либо ничего

  • Согласованность (Consistency): невозможно завершить транзакцию, если данные несогласованны

  • Изолированность (Isolation): одна транзакция не должна влиять на результаты других транзакций, и наоборот

  • Устойчивость (Durability): тесно связана с консистентностью — если транзакция завершена, то она должна быть сохранена, независимо от того, что произошло в этот момент с оборудованием

ACID-мышление помогает определить, где требуется атомарность (например, при создании заказа и резервировании товара), а где — изолированность, чтобы избежать ситуации, когда два клиента одновременно «забирают» последний товар. Более подробно можно ознакомиться в этой статье.

Поддержка ACID применительно к транзакциям считается одним из главных преимуществ в реляционных СУБД. Но с появлением потоковой передачи и NoSQL-баз данных, где важны масштабируемость и скорость, классический ACID уступает место компромиссным моделям, таким как BASE.

BASE

Эту концепцию также сформулировал Эрик Брюер: в большинстве систем не требуется абсолютная немедленная согласованность, приоритет отдаётся доступности (AP из CAP-теоремы).

Критерии:

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

  • Мягкое состояние (Soft State): неопределённое состояние, при котором данные могут быть не согласованы в моменте (такие данные будут окончательно определены только после завершения всех транзакций)

  • Согласованность в конечном счёте (Eventually Consistent): согласованность будет достигнута не сразу, а после завершения всех одновременных обновлений

Если система не удовлетворяет ACID, это не означает, что она по умолчанию BASE. Это достаточно сильная гарантия, обеспечивающая согласованность данных в будущем.
Для более полного понимания можно почитать тут.

Согласованность

Так или иначе, с согласованностью мы сталкивались во всех разделах. Давайте рассмотрим это подробнее. У согласованности есть свои типы. Она конкурирует в системах с доступностью, но и здесь есть степени и компромиссы. В частности, многие распределённые системы приходят к Eventual Consistency (согласованность в конечном счёте, когда она достигается не сразу). Посмотрим на типы согласованности, начиная с более строгой (время ответа будет уменьшаться, а доступность — увеличиваться):

  • Сильная согласованность (Strong Consistency): после изменения записи, последующий доступ к данным покажет обновленное значение

  • Причинная согласованность (Causal Consistency): причина должна быть раньше следствия: например, мы пишем в чат сообщение и на него отвечают, что сообщения связываются причинной согласованностью, и первое сообщение никак не может оказаться после ответа на него; однако если пользователи пишут одновременно, то порядок уже не важен — такой случай подпадает под Eventual Consistency

  • Читай то, что записал (Read‑Your‑Writes Consistency): частный случай (Causal Consistency), когда клиент, обновив данные, всегда получает обновлённое значение и никогда не видит старое

  • Однообразное чтение (Monotonic Read Consistency): если пользователь увидел обновлённую запись, то при последующих обращениях к этим данным он никогда не получит более старое значение

  • Однообразная запись (Monotonic Write Consistency): система гарантирует упорядоченность записей пользователя

  • Сессионная согласованность (Session Consistency): пользователь получает доступ к хранилищу в контексте сессии: пока сессия существует, система гарантирует Read‑Your‑Writes, как может гарантировать и другие согласованности (Monotonic Read Consistency и Monotonic Write Consistency)

  • Согласованность в конечно�� счёте (Eventual Consistency): гарантирует, что в конечном счёте все запросы будут возвращать последнее обновлённое значение при временном отсутствии обновлений (самая популярная согласованность)

  • Слабая согласованность (Weak Consistency): система не гарантирует, что вернёт обновлённое значение; после изменения должны быть выполнены определённые условия и этот момент называется окном несогласованности (Inconsistency Window)

Нужно понимать, что система может комбинировать гарантии в зависимости от бизнес‑требований. Она может быть в целом Eventual Consistency, но также иметь гарантии для пользователя в рамках сессии Session Consistency, используя Read‑Your‑Writes Consistency, Monotonic Read Consistency и Monotonic Write Consistency. Такой подход позволяет сохранить масштабируемость и доступность системы, не усложняя пользователю жизнь.

Проблемы

Представим простую ситуацию: у нас есть монолитный сервис для оформления заказов. Используем одну базу данных (Postgres), которая де‑факто покрывает ACID. Пользователь добавляет в заказ товары и оплачивает. Получается следующая схема:

После подтверждения счёт пользователя обновляется и создаётся заказ. Дальше разбиваем сервис на прослойку Gateway для работы с фронтом и два (или более) сервиса, каждый со своей базой данных: один отвечает за данные пользователя, другой работает с заказами. 

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

  • Физическое разделение данных: каждый сервис имеет свою базу (паттерн Database per Service) 

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

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

  • Атомарность: запрос на обновление счёта пользователя может выполниться, а создание заказа — нет

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

И головной боли здесь больше:

  • Теперь сходить в другой сервис гораздо сложнее, чем просто вызвать метод: необходимы таймауты, повторы и другие спо��обы обхода ошибок сети

  • Больше сервисов — больше накладных расходов, документации, тестов, метрик и т.д.

  • Трафик — это деньги

  • Согласованность — это роскошь, теперь переходим к Eventual Consistency, что влечёт за собой все вытекающие саги, повторы, очереди и множество уловок, чтобы сделать наш флот согласованности непотопляемым

Микросервисы, монолиты и их границы

Давайте определим границы микросервисов и монолитов, и выделим их достоинства и недостатки.

Монолит — одно приложение, которое инкапсулирует в себе всю бизнес‑логику. Оно может одновременно управлять созданием заказов, обработкой данных и платёжными транзакциями. 

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

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

Микросервисная архитектура строится на принципе разделения системы по тематическим областям, зонам ответственности и бизнес‑логике. Одними из главных преимуществ являются масштабируемость и гибкость. Каждый микросервис может обладать своим стеком, заточенным под конкретные нужды, и отдельно масштабироваться. Сервисы не привязаны друг к другу, благодаря чему их можно разрабатывать, тестировать и развёртывать параллельно.

Основной недостаток заключается в сложности управления распределённой системой. С ростом числа сервисов возрастают трудности в поддержании их взаимодействия и согласованности данных.

Монолит гораздо проще и дешевле в разработке, тогда как микросервисная система лучше масштабируется. Так что же выбрать? 

Монолит будет оптимальным выбором, если:

  • Вы создаёте прототип

  • Ваш бизнес малый или средний

  • Процессы CI/CD не так хорошо развиты

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

Микросервисная архитектура лучше подходит для крупного бизнеса с множеством команд и большим объёмом данных, а также присутствует культура DevOps. Разбивка системы на более мелкие и независимые сервисы помогает в качественной организации, обслуживании и масштабируемости.

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

Важно понимать, что простое разделение монолита на отдельные сервисы не делает архитектуру автоматически микросервисной. Если несколько сервисов продолжают работать с общей базой, или сбой одного процесса приводит к неработоспособности другого, то это распределённый монолит. 

Для более глубокого изучения этой темы рекомендую ознакомиться с книгой Сэма Ньюмена «От монолита к микросервисам» (или «Создание микросервисов. 2-е издание»). В ней подробно рассматриваются паттерны декомпозиции как для монолита, так и базы данных. 

Перечислю паттерны, которые помогут эффективно декомпозировать монолит:

  • Фикус‑Удавка: выносим часть функциональности в новый сервис, тестируем, через прокси переводим трафик (например, nginx). Переключать удобно через Feature Toggle.

  • Ветвление по абстракции: случай, когда нам надо перенести не всю «ручку», а только часть функциональности. Создаём сервис, делаем в нём функциональность, под её интерфейс из монолита создаём реализацию вызова нового сервиса и ставим Feature Toggle (теперь переключаем либо использование новой функциональности, либо новый сервис), в конце выпиливаем из монолита.

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

  • Декоратор: он похож на Фикус‑Удавку. Представим ситуацию: у нас есть монолит с заказами, и мы хотим добавить начисление бонусов при оплате. Вместо того чтобы модифицировать существующий монолит, мы решили делать всё в новом сервисе. Этот паттерн предполагает создание прокси‑сервиса, который будет отправлять запросы в монолит и при необходимости взаимодействовать с сервисом бонусов. (Обычно мы можем себе позволить прикрутить события в монолите, тогда можно отсылать события, что создан заказ. Сервис бонусов будет их получать и, при необходимости, запрашивать информацию о заказе из монолита).

  • Захват изменений в данных: построен на триггерах в монолитной базе данных или чтении журнала. Отлавливаем изменения в данных и реагируем в новом сервисе (считаю, что это крайне сомнительный паттерн, скорее, безвыходный).

Выводы

Если поставлена задача продумать архитектуру, ответьте для начала на ключевые вопросы:

  • Что система должна делать, а что точно не должна? 

  • Какой ожидается трафик сегодня, через шесть месяцев и через год? 

  • Каков объём данных, каковы требования к их хранению и поиску? 

  • Какая требуется согласованность данных?

  • Какое время отклика допустимо, и какие сценарии критичны по SLA?

  • Допустимы ли сбои, и как будем с ними справляться?

  • Сколько средств (деньги = люди, время, серверы и т.д.) готовы выделить на разработку и поддержку?

После ответов на эти вопросов можно приступать к проектированию систему. Мы сознательно выбираем путь, и решение в пользу монолита — это не приговор и заведомо не будущий legacy-проект. Важно соблюдать модульность и чистоту кода, чтобы последующее разделение (при необходимости) прошло менее болезненно.

Сложные системы требуют больших значительный усилий как при разработке, так и при поддержке. Поэтому стоит задуматься: действительно ли нужно усложнять?

Мы, инженеры, стремимся найти ту самую «недостижимую» золотую середину. Проектируем, исходя из потребностей бизнеса, учитывая все граничные случаи, и стараемся сделать это максимально экономично с точки зрения ресурсов. Нет ни идеального монолита, ни распределённой системы, ни паттерна, ни инструмента. У всего есть свои достоинства и недостатки. 

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

Рекомендация к ознакомлению:

  • «Чистый код», Роберт Мартин

  • «A Critique of the CAP Theorem», Мартин Клеппман

  • «Чистая архитектура», Роберт Мартин

  • «От монолита к микросервисам», Сэм Ньюмен

  • «Создание микросервисов. 2-е издание», Сэм Ньюмен

  • «Высоконагруженные приложения. Программирование, масштабирование, поддержка», Мартин Клеппман

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
А что вы думаете?
15.38%Монолит всему голова!2
0%Конечно же за микросервисную архитектуру, у нас другого не бывает0
15.38%Нет монолитов и микросервисов – это все маркетинг, чтобы продавать книги и писать статьи на хабр, нужно просто решать задачи2
61.54%Пилим все по мере необходимости8
7.69%Что получится, то и будет1
Проголосовали 13 пользователей. Воздержался 1 пользователь.