
Микросервисный подход казался выбором по умолчанию в последнее десятилетие. «Делай микросервисы – и будет хорошо», – намекал опыт построения огромных программных систем. Вместе с тем в последнее время в сети всё чаще встречаются мнения, что «не нужны нам эти микросервисы», и тому есть причины.
Статья инспирирована переводом Распределенный монолит: тихий убийца мечты о микросервисах и некоторыми комментариями, отражающими, что тема описана не слишком подробно. Покажу видение данной темы немного с другой стороны.
Архитектура ПО – это компромиссы
Вначале хочется напомнить, повторяя мысль из книги Марка Ричардса и Нила Форда «Фундаментальный подход к программной архитектуре», что в архитектуре программного обеспечения почти не бывает идеальных решений. Это область компромиссов. Программная система должна обладать заданными характеристиками, по-хорошему они бывают прописаны в ТЗ как нефункциональные и иногда функциональные требования, но могут и «подразумеваться» стейкхолдерами. Например, отказоустойчивость, доступность, возможность быстрого внесения изменений в код и т.п. – все эти характеристики хороши, но не всегда обязательны, а также имеют свою цену. Практически всегда требуются только некоторые из них, и одна из задач архитектора – обеспечить наличие у системы требуемых характеристик, при этом уложившись в бюджет разработки.
Так как все хорошие характеристики реализовать в одной системе практически невозможно (они начинают противоречить друг другу), приходится выбирать, какие характеристики будут, а какие – нет. Также можно отметить, что система часто всё-таки будет функционировать при разных наборах её характеристик, но практически всегда в некоторых аспектах она будет работать плохо. Поэтому вышеупомянутую задачу архитектора можно переформулировать как «выбрать аспекты, в которых система будет работать хорошо, и в которых - плохо, и реализовать её».
Чем хороши микросервисы
Микросервисная архитектура – это архитектурный стиль, в котором программная система разделена на небольшие сервисы (именуемые «микросервисами»), которые взаимодействуют друг с другом. Среди её плюсов обычно выделяют:
Масштабируемость. Если какой-то сервис тормозит – просто увеличиваем число его экземпляров, и система начинает справляться с нагрузкой.
Независимый деплой. Команда может развернуть свой микросервис, не дожидаясь, пока все остальные команды вычистят баги из своих глючных модулей. Ну и быстро и незаметно передеплоить, если глючным оказался свой, а не чужой, модуль.
Скорость разработки. Микросервисы небольшие, их можно охватить небольшой командой без когнитивной перегрузки (как мы все слышали, команда должна быть максимум 10 человек), в сумме с возможностью независимого деплоя это повышает скорость разработки.
Есть конечно и минусы, о которых на старте вы, возможно, не подозреваете: сложность отладки, некоторый беспорядок в системе, но пока не будем об этом, ведь на старте они неочевидны.
Что получается вместо микросервисов? Распределённый монолит!
Вдохновившись микросервисной архитектурой по объективным причинам, или просто решив немного хайпануть (допустим, сейчас 2015-2020 год, ведь к ~2022 ажиотаж вокруг микросервисов утих) или уволить старого архитектора (ибо знатно утомил), вы по образу и подобию некой старой системы, теперь презрительно называемой «монолитом», задаётесь целью создать систему из разумного числа микросервисов, которые весело вызывают функции друг друга. И ожидаете, что через несколько месяцев реализации всё заработает лучше, чем старая система, т.е. отлично.
Так и вышло? Нет.
Как-то так получилось, что функции микросервисов, вызываемые в вашей системе по RPC (с сохранением структуры вызовов и модулей старой системы), иногда не вызываются, подвисают, на каждый вызов требуется маршалинг, в итоге система адово тормозит, иногда теряет данные, и скорее не работает, чем работает, а отладка каждого второго бага превращается в нетривиальные извращения, и ещё для обновления почти любого сервиса почему-то требуется передеплоить все остальные.
У вас «распределённый монолит» (диагноз такой). Монолит, потому что нельзя изменить микросервисы по отдельности. Распределённый, потому что планировалась масштабируемость.
Кто же знал, что нельзя использовать RPC?.. И вообще, сетевые вызовы – это долго...
Как правильно работать с микросервисами
Если у вас была возможность ознакомиться с книгой Сэма Ньюмена «Создание микросервисов», вы знаете, что микросервисы правильно готовить так:
Небольшие автономные команды. Команда, в которой число людей больше 10-15, становится неэффективной из-за излишнего количества коммуникаций, рекомендуется даже, чтобы число людей не превышало 10, поэтому, если ваша система настолько велика, что требует для разработки больше 10 человек, вы вынуждены разделиться на несколько команд
или страдать. Будет хорошо, если команды не будут мешать друг другу, т.е. работать каждая над своим кусочком системы. Эти кусочки и есть микросервисы. В полном соответствии с законом Конвея («организации обречены создавать системы, структура которых копирует структуру коммуникаций в организации»). Возможно даже, что команды для собственной эффективности будут применять различные стандарты кодирования, стеки технологий, языки программирования там, где это уместно, и вся система при этом будет бесшовно работать.1 микросервис – 1 домен. Удобно разделять систему на микросервисы по границам предметных областей (ограниченных контекстов из Domain-Driven Design) – каждая команда изучает одну предметную область и изучает её хорошо, появляется экспертиза в этой области, что добавляет адекватности принимаемым решениям. Часто большинство задач относятся к одному домену, и их реализация затрагивает 1 микросервис, что хорошо также в контексте следующего пункта.
Тесно сплочённая функциональность. Микросервис должен выполнять близкие по смыслу задачи, его функциональность не должна включать в себя независимые куски из разных мест системы. Это также соответствует букве S из SOLID Роберта Мартина – «У модуля должен быть только 1 источник изменений» – и минимизирует коллизии хотелок стейкхолдеров и конфликты слияний.
Контрактные интеграционные тесты. В большинстве систем полностью независимых микросервисов (как ни хотелось бы апологетам подхода) не бывает и быть не может, они вызывают функции друг друга, а если обобщить, осведомлены о других сервисах и зависят от них. Если не применять специальных техник, частым источником багов может быть ситуация, когда одна команда меняет поведение своего сервиса, и другие сервисы дружно падают. Или система требует одновременной правки и деплоя всех сервисов, и критически важно синхронизировать работу всех команд и ничего не упустить – это сложная и небыстрая задача. Эта неприятная организационная проблема относительно легко обходится специальными техниками, а именно поддержкой прямой и обратной совместимости API сервисов (упрощённо говоря, разрешено только добавление функций и необязательных полей, но не удаление и модификация – в результате и старая, и новая версия сервиса-поставщика могут одновременно работать и со старой, и с новой версией сервиса-потребителя, или по крайней мере не ломать всю систему) и контрактными (consumer-driven) интеграционными тестами (команда сервиса-потребителя формирует и закидывает сервису-поставщику контрактные тесты, проверяющие совместимость форматов данных в API, а команда сервиса-поставщика обязана соблюдать этот контракт и включает прогон этих тестов в тестирование своего сервиса). Нетривиально, да?
Ах да, сообщения вместо RPC. Возможно, вы и сами догадались. Если нет, но хотите попробовать догадаться, можете прочитать Заблуждения о распределённых вычислениях, датированные 1994 годом. Ну или вышеупомянутые книги, там всё подробно расписано.
Это идеальный случай, отклонения на самом деле могут присутствовать. Однако чем их больше – тем больше проблем. Тем больше вероятность получить тот самый «распределённый монолит» – систему, имеющую основные недостатки микросервисного и монолитного подходов – низкую скорость работы и низкую скорость разработки.
У вас соблюдены все пункты из списка? Тогда, наверно, вас можно поздравить.
Как правильно на самом деле
It depends!
Эту фразу Ричардс и Форд в своих книгах повторяют много раз. Суть идеи в том, что наилучшее решение зависит от конкретной ситуации. В каких-то случаях хорошо подойдут микросервисы, в каких-то – монолит. Нет единого «правильного» решения.
Есть предпосылки для выбора микросервисного стиля архитектуры:
Явные домены или ограниченные контексты. Если части вашей системы явно относятся к разным предметным областям, вероятно, что есть смысл реализовать их в виде отдельных микросервисов – так будет меньше коллизий при разработке. Ну, или просто сервисов.
Явная группировка функций. Аналогично предыдущему пункту, если функции удачно сгруппированы так, что в типичной задаче меняются только функции в пределах группы, может иметь смысл выделить их в микросервисы, или хотя бы в модули. Это достаточно спорный пункт: прежде, чем выделять микросервисы из групп, нужно обратить внимание и на логику использования функций, и на производительность взаимодействия получаемых микросервисов, иначе может получиться неудобный или медленный сервис.
Отсутствие зависимостей между группами функций. Независимые группы, не взаимодействующие (или редко взаимодействующие) между собой, можно разделить на микросервисы, чтобы развивать их разными командами. Коллизий после такого разделения возникать не должно.
Необходимость независимой разработки, наличие нескольких команд. Если у вас есть несколько несвязанных команд, находящихся в разных локациях, или какие-то части системы отдаются на аутсорс – микросервисная архитектура системы поможет сократить число взаимодействий между командами, что позволит уделить больше времени работе (в противовес «интересным» созвонам). Также, если ваша система достаточно велика, что одной команды из 10-15 человек не хватит для её разработки, вы просто вынуждены делать несколько команд, и удобнее выделить каждой команде кусочек системы. Так практичнее в плане наращивания экспертизы и комфортнее психологически для разработчиков из-за эффекта владения «своей» частью системы – в итоге получается более качественный софт.
Необходимость независимого деплоя. Микросер��исы можно беспроблемно разворачивать по отдельности, и если это требуется, то это ещё один довод выбрать микросервисную архитектуру. Сюда же относится и случай, когда разные модули разрабатываются с разной скоростью – можно один перезапускать часто, а другой примерно никогда, и интенсивная разработка первого модуля не скажется на доступности второго.
Необходимость горизонтального масштабирования. Тут всё просто – если один сервис тормозит, можно увеличить число его экземпляров. Часто это помогает (если, конечно, задачу можно распараллелить). С другой стороны, в некоторых случаях и монолит можно запускать в нескольких экземплярах, флагами отключая некоторые сервисы.
Необходимость отказоустойчивости. Когда возникает сбой и микросервис падает, работу на себя берёт другой инстанс. Даже если сбой повторяется, вызывая падение всех инстансов микросервиса, система может продолжать функционировать в ограниченном режиме.
БоромирМонолит упал бы полностью.Необходимость интеграции с другими системами. Обмен сообщениями естественен при взаимодействии с другими системами, особенно если они тоже основаны на микросервисных принципах.
Есть и специфичные минусы микросервисов, которые могут создавать предпосылки для выбора другого архитектурного стиля:
Низкая производительность сетевого обмена сообщениями. Маршаллинг никто не отменял, даже если сеть виртуальная и находится на одном хосте, сетевой вызов с маршаллингом всегда медленнее просто инструкции CALL.
Высокая сложность разработки. Поддержка API, контрактные тесты, взаимодействия команд, распределённая отладка, необходимость мониторинга и алертинга – это всё требует усилий и от разработчиков, и от организаторов разработки. А главное, нельзя пускать это на самотёк.
Необходимость описания микросервисов и ведения их реестра. В тысяче сервисов очень легко запутаться, что-то не вспомнить, поэтому необходимо уделять время документированию.
Довольно много нюансов, не правда ли?
В ряде случаев оказывается, что практичнее выбрать монолит, и, оправдываясь за этот немодный выбор, называть его «модульным монолитом» (как будто кто-то в здравом уме собирался разрабатывать немодульный монолит).
Есть различные архитектурные стили, не только монолит, микросервисы и event-driven architecture, и у них свои предпосылки для выбора и проблемные места.
Также вполне возможно, что с течением времени системе будет подходить сначала один архитектурный стиль, а затем (например, при масштабировании, расширении штата) – другой, и в какие-то моменты потребуется миграция, что, опять же, может быть приемлемым, а может и не быть. Архитектор должен иметь представление о том, какая архитектура подойдёт системе на каждом из этапов её развития, и выбирать архитектурные стили с учётом всего пути.
Если ваша система разрабатывается не с нуля, а по образу старой монолитной системы, или разработчики имели опыт преимущественно с монолитами, читали Мартина, но не читали Ньюмена, или же просто несколько закостенели, на новую систему будет велико влияние старых подходов, применявшихся, скажем, 15-20 лет назад. Шанс получить распределённый монолит возрастает, и сопротивление попыткам сделать «независимые сервисы, разрабатываемые независимыми командами» имеет здесь существенную роль. Вы заметили, что пункт «Контрактные интеграционные тесты» из раздела «Как правильно работать с микросервисами» находится в некотором противоречии с DRY? Принцип Don’t Repeat Yourself не допускает существования описания одних и тех же контрактов (описания API и типов данных) в репозиториях разных сервисов. Однако для интеграционного тестирования все команды должны видеть контракты сервисов, документацию и иметь возможность запустить тесты. Можно вынести общие описания в отдельные репозитории контрактов и репозитории схем, но это приближает архитектуру к монолитному стилю, заимствуя недостатки в виде общей точки синхронизации всех команд. Иногда практичнее скопировать код с описаниями типов «чужого» сервиса, и написать тесты на его основе. При небольшом размере микросервисов и их грамотном версионировании проблем обычно не возникает. Этот подход даже называется Write Everything Twice, как противоположность DRY. Некоторое нарушение правил, но не законов природы.
«Старые» разработчики и менеджеры, если они не открыты к новому, и особенно, если они читали только «Чистый код» Роберта Мартина, могут саботировать микросервисный подход, лихо вводя излишние зависимости команд, единые стандарты кодирования, неподходящие методологии разработки, неуместные для конкретных задач языки программирования, запрещая написание документации и дублирование кода контрактов, потому что «Мартин запретил писать документацию» и «это дублирование кода, а мы все вместе упоролись решили соблюдать DRY и SOLID!», «новый архитектор – не архитектор, мы 20 лет код писали и так и будем писать»... Отклонения от «правильного» микросервисного пути нарастают, и в итоге получаем ситуацию, когда ничего не работает, то, с чего началась эта статья.
Распределённый монолит.
Вместо заключения
Надеюсь, мне удалось подсветить проблему возникновения «распределённого монолита» вместо «правильных микросервисов», описать пути её решения и ограничения этих путей. Работа, искусство архитектора в т.ч. заключаются и в том, чтобы выбрать, как избежать подобных проблем, спланировав всю траекторию развития системы на протяжении её жизненного цикла. Но, «it depends!», бывают проблемы, которые не обязательно решать. Главное – решить те, которые решить необходимо. Все «хорошие» подходы имеют границы применимости. Как правило, система должна быть работоспособной и реализуемой – это минимальный набор характеристик. Остальное – опциональные (а в некоторых проектах – критически необходимые) улучшения, которые позволяют отличить от MVP действительно хорошую программную систему, выполняющую задачи пользователей, удовлетворяющую требованиям бизнеса, надёжную в эксплуатации, простую в доработке и поддержке.
Спасибо, что дочитали.
