Перед этим опусом стоит крайне сложная задача — описать принципы перехода от монолита к микросервисам, при этом не рассказывать читателю то, что он и так знает. Микросервисная архитектура настолько популярна, что автор свято уверен, что каждый второй читатель работал с такой архитектурой или же ненавидит её всеми фибрами своей души. Также бытует мнение, что большая часть разработчиков, читающих эти строки присоединились к проекту, построенному на микросервисах, уже после того, как он стал микросервисным. А та малая часть ребят, которые самостоятельно проделывали путь от монолита к микросервисам уже достаточно опытны, чтобы работать на должностях старших разработчиков, архитекторов или менеджерами проекта.
Если настало то самое время для приложения переходить от абсолютной монархии к раздробленному феодализму, то основная часть времени будет уходить на то, чтобы возвращать технический долг, накопленный годами. И долг этот нужно будет возвращать по чести — нужно вернуть то, что задолжал сам и то, что задолжали все предшественники. Для этого процесса нет понятия «моё» и «не моё», а есть только понятие рыцарской доблести разработчиков, которым в текущий момент не посчастливилось оказаться в проекте. Программист должен быть настоящим вассалом своего сюзерена и бросаться в бой с армией архитектурных ошибок, отсутствия тестов и кучи антипаттернов.
Известно очень много неудачных примеров перехода к микросервисам, в которых существующий код по возможности не изменялся. А вот новая функциональность писалась в отдельных репозиториях, которая использовала основной существующий код, как черный ящик. В итоге образовывалось огромное количество дублирования функциональности, дублирования данных и черт знает еще чего. Основной проект переставал поддерживаться, его зависимости безнадежно устаревали. Задача в таких проектах стояла в том, чтобы написать достаточное количество микросервисов вокруг основного проекта, чтобы полностью покрыть всю функциональность и работать снаружи только через сервисы. И только как этот момент достигался, по плану нужно было начать заменять функциональность черного ящика, вновь написанными микросервисами. Как правило, такие приложения никогда не выходили из зависимости от основного легаси-кода.
Конечно же, сказать «рефакторить» может каждый, для этого не нужно быть семи пядей во лбу. Чуть сложнее, но все еще банально, начать рассказывать про способы рефакторинга, перечисленных в сто и одной книжке, написанной Фаулером и фаулеровскими поклонниками и его ненавистниками. Вовсе бесполезно начать показывать огромные и усложненные примеры того, как делать не надо и идеально подобранные сферическовакуумные примеры, которые абсолютно неприменимы на практике. Также стоит учитывать, что те ребята-программисты, которые и создают проблемы давно уже переросли себя и больше так не делают. Либо же напротив — им никогда даже в голову прийти не может читать подобного рода статьи.
Так же подмечено, что рассматривать идеальный код и идеальный проект скучно и не интересно. В наших перфекционизированых аналитически устроенных программистских головах мы же всегда знаем как код должен выглядеть. Мы всегда с особой точностью и цинизмом сможем отличить плохой код от ужасного кода. Иногда, правда, попадаемся в логическую ловушку восклицая «какой идиот написал это?!», глядя на свой собственный код полугодичной давности. Кстати, это только лишь подтверждает высокий уровень профессионального роста разработчика за эти самые пол года.
Кроме того, автор не намерен попадаться в логическую ловушку, споря с читателем в вопросах, в которых читатель априори лучше разбирается. И поэтому, прежде чем приступить к громким утверждениям, договоримся, что в дальше рассматриваются гипотетические проекты с одной важной общей чертой — они написаны из ряда вон плохо. Ну прям ужасно. Нужно закрыть глаза и представить себе самый ужасный сферический проект в вакууме, который только можно себе представить. Само собой он, как любой другой уважающий себя энтерпрайз-проект, работает, и работает довольно стабильно и, что самое главное, приносит прибыль. У него есть все, что должно быть у такого проекта: просто клиенты, довольные клиенты, техподдержка и симпатичный вид снаружи. Вот только изнутри он ужасен и об этом знают только доблестные рыцари-программисты, готовые рефакторить, как никто никогда не рефакторил. Откуда-то у автора уверенность, что опытный разработчик представит такое без особого труда.
Но все-таки про некоторые стратегии и правила чистоты кода упомянуть нужно. Из этих правил и будут вытекать первые цели усовершенствования кода и подготовки его к микросервисной жизни.
Планирование микросервисов
Любимым делом всех неправильных менеджеров и старших разработчиков является планирование по поводу и без повода. Но автор не призывает отказаться от планирования вовсе — отсутствие грамотного планирования в проекте делает разработку похожим на лося, несущегося сквозь горящий лес. Но процесс долгосрочного планирования с точки зрения перехода к микросервисам, дело крайне неблагодарное. Можно более или менее точно сказать как нужно рефакторить отдельно взятый класс или библиотеку, но очень тяжело продумать целиком микросервисную архитектуру на раннем этапе и угадать. Задача такого раннего планирования — разделить проект на несколько логических частей без какой-либо привязки к коду или текущей архитектуре. Фактически — сделать ревизию того, что все-таки делает приложение и какие из этих функциональностей независимы. И возвращаться к этому списку каждое утро понедельника за чашкой горячего кофе, пересматривая написанный набор функций и сверяясь с тем, к чему нужно стремиться.
Глобальные переменные и синглтоны
Почему плохо использовать переменные с глобальной областью видимости знает каждый мальчишка. Еще с молоком матери каждый разработчик впитывает знание о том, что хранение и изменение таких вот данных приводит к проблемам с конкурентностью и масштабированием. Конечно же, это чистая правда. Но с точки зрения рефакторинга, глобальные переменные плохи тем, что становится невозможным выносить какую-то функциональность на отдельно сконфигурированный сервер без дублирования. Отдельно стоит упомянуть о библиотеках, имеющих глобально настроенные объекты. Например, руби-библиотека для работы с докером по-умолчанию предлагает настроить доступ к докер-окружению через глобально доступный синглтон:
Docker.url = 'tcp://example.com:5422'
Docker::Container.create({ 'Cmd' => ['ls'], 'Image' => 'base' })
Работа с глобально доступными переменными
Безусловно, такой вид настройки сэкономил первоначальное подключение этой библиотеки, но сделал невозможным горизонтально масштабироваться, работая с несколькими докер-хостами одновременно. В подобном проекте работа с докером проникла на все уровни абстракции и регулировать выбор докер-хоста для отдельно взятого запроса не представляется возможным. Благо, что создатели этой библиотеки предусмотрели возможность работы с несколькими хостами. Каждая команда имеет дополнительный необязательный параметр в котором можно определить хост:
connections = [
Docker::Connection.new('tcp://example.com:2375', {}),
Docker::Connection.new('tcp://example2.com:2375', {}),
Docker::Connection.new('tcp://example3.com:2375', {})
]
Docker::Container.create({ 'Cmd' => ['ls'], 'Image' => 'base' }, connections.sample)
утрированная простейшая реализация алгоритма Round-robin между несколькими серверами
Самым простым и очевидным способом отказа от глобальных объектов нужно переходить к простым формально-фактическим параметрам. Объект, используемый на нескольких слоях абстракции нужно объявить локальной переменной на самом верхнем из используемых слоев, и передавать через формально-фактические параметры во все места, где раньше использовалась глобальная переменная.
Но будьте осторожны. Обратной стороной медали этого подхода является случай в котором количество формальных параметров уже и так огромно. В таких случаях сто и одна книжка правильного рефакторинга рекомендует подходить к этому процессу прагматично — вынесение части функциональности в отдельные классы, работа с набором параметром, как с отдельно взятой структурой данных, выделение методов и прочие правильные вещи.
Прокси-классы и прокси-методы
Следующей вехой перехода к раздельным сервисам от монолитного приложения будет принцип разделения ответственности и принцип единственной обязанности. На этом этапе нужно постоянно держать в голове то, что совершенно непонятно какие сервисы получатся в результате этого долгого и кропотливого пути и не стремиться сразу сделать отдельные и независимые куски кода. Можно долго и со вкусом эксперта пересказывать книги Мартина и Демарко, выдавая прочитанное за собственные мысли, но авторы этих книг делают это значительно лаконичней. В итоге код, не соблюдающий принцип разделения ответственности, вынести в отдельный сервис крайне тяжело, даже если глобальные переменные и синглтоны уже отсутствуют.
Библиотеки
Хорошими считаются те библиотеки, которые хорошо спроектированы, не имеют существенных проблем и не требуют по каждому поводу перечитывать свой исходный код. И так уж сложилось, что работа с хорошо написанными сторонними библиотеками нам, программистам, приносит нескрываемое удовольствие. А библиотеки, которые требуют постоянного вмешательства, неочевидный предметно-ориентированный язык и кучу багов, вызывают желание выкинуть к чертям библиотеку целиком и написать свое собственное решение. Чаще всего это собственное решение оказывается почти таким же неудобным и неочевидным. И, как правило, рассматриваемый тип приложений и состоит из огромного количества собственных решений стандартных проблем.
В итоге следующей ступенью перехода от целого к раздробленному будет отказ от ручных решений, и переход к популярным хорошо написанным библиотекам с открытым исходным кодом. Конечно же, может быть такое, что требуемая функциональность просто отсутствует в виде библиотеки. В таком случае решением будет сделать такую библиотеку и открыть исходный код для сообщества. В будущем такого рода рефакторинг монолитного приложения позволит использовать общую функциональность в разных микросервисах.
Бытует мнение, что сделать библиотеку с открытым исходным кодом достаточно непросто, и требуется уделить этому процессу очень много времени. Документация, полное покрытие тестами, аннотации, поддержка служебных файлов, описание релизов и много-много не программистской работы. Это крайне сложно и утомительно. Кроме того, код, доступный для прочтения кем угодно заставляет разработчика трепетней относиться к тому, что пишется и большинство разработчиков предпочитают просто не показывать свой код окружающим, боясь быть осужденным за плохой код или некрасивые решения. Общего решения автор не имеет, а знает только лишь аналогию рыцаря и сюзереном. Рыцарей не спрашивают насколько интересно им делать ту или иную работу. Надо, значит надо.
Но вынесение кода в библиотеки общего назначения имеют еще несколько дополнительных очевидных преимуществ. Использование этих библиотек в сторонних приложениях позволит избавиться от багов, которые еще не были найдены, убедиться, что библиотека работает в разных окружениях и позволит развивать её руками сторонних разработчиков.
Также не стоит перегибать палку и выносить в библиотеки абсолютно все. Логика приложения должна оставаться внутри самого приложения. Хорошим критерием для вынесения в отдельную библиотеку какого-то кода является заданный вопрос о том, можно ли использовать эту библиотеку в приложениях с другой тематикой и бизнес-логикой.
Простые данные
На этом этапе должны уже сформироваться области приложения с явно выраженными точками входа и интерфейсом общения между соседними областями. Задача этого этапа рефакторинга — избавиться от сложных структур в формально-фактических параметрах. Конечно же, бездумный отказ от классов-структур в пользу хешей и массивов данных не спасет ситуацию, а лишь усугубит ее. Функция с шестнадцатью формальными простыми параметрами хоть и проста с точки зрения сериализации, но безумно сложна с точки зрения взаимодействия с ней. Формальный параметр сложносоставного типа легко может быть оформлен в виде отдельного сервиса, библиотеки или независимой подпрограммы.
Цель упрощения формально-фактических параметров — возможность вынесения отдельно взятого подприложения в независимый системный процесс. А общение с таким процессом нужно будет оформлять в виде стандартизированных независимых от языка протоколов.
Общедоступные стандарты
Протоколов придумали целое множество на любой вкус и цвет. Для популярных способов общения практически на всех языках существуют вполне приемлемые решения, причем реализуемые малой кровью. Выполнив все предыдущие инструкции, приложение наконец-то будет готово к вынесению отдельных его частей в отдельные подпрограммы или сервисы. Главным критерием выбора отдельного стандарта для общения между приложениями будет возможность с легкостью его заменить, обобщить, расширить или усовершенствовать.
Ошибочно полагать, что выбор одного единого общедоступного стандарта для общения между всеми микросервисами будет означать унификацию всего приложения. Наоборот, унификация общения между сервисами может заставить отдельно взятые микросервисы подстраивать архитектуру не в лучшую сторону и не с пользой для приложения. Стандарты общения между сервисами могут отличаться и зависеть от конкретных требований и условий. Выбирать стандарт нужно не спеша и с расстановкой и критично подходить к преимуществам и недостаткам того или иного стандарта при выборе оного для нового микросервиса. Конечно же тот факт, что той или иной стандарт уже используется в отдельных микросервисах дает ему преимущество перед остальными при прочих равных, но этот критерий не должен являться единственно учитываемым.
Вместо выводов
Переход на микросервисную архитектуру достаточно кропотливый процесс. И не стоит полагать, что после перехода на архитектуру, с обещаниями радужной жизни, в проекте действительно будет все радужно. Чудес не бывает. Кроме того, не нужно воспринимать эту статью, как призыв переводить совершенно любой проект на микросервисную архитектуру. Далеко не все проекты готовы к такому.
И не стоит путать причинно-следственную связь. Данная статья рассказывает о том, какие шаги нужно предпринять, если вы твердо решили связать будущее проекта с микросервисами. И ни в коем случае не наоборот. Эта статья не призыв использовать микросервисы в вашем проекте.
Комментарии (14)
Suvitruf
29.08.2016 15:36+1Я вот прочитал статью и не понял, а причём тут микросервисы?
1) Планирование? В любой проекте.
2) Глобальные переменные и синглтоны для любого проекта актуальны. И нет, они не всегда вредны.
и т.д.
Ну и в целом статья больше про код и архитектуру приложения как такового. В микросервисах самая большая, как по мне, проблема — это взаимодействие компонентов. В монолите у вас всё в куче, главного только обеспечить доступность, отказоустойчивость и т.п.
Когда вы монолит распилите, то надо настроить взаимодействие: REST? RabbitMQ или Kafka? Через базу? Или ещё как-то. Как наши сервисы найдут друг друга? consul? Или что-то другое для сервис дискавери.
В случае монолита, как правило, всего 2 состояния: работает и не работает. В случае с микросервисами надо думать и разруливать ситуации, когда какой-то сервис лежит.
Плюс, желательно, чтобы все эти отдельные сервисы были максимально стейтлес. А это тоже та ещё морока при переходе от монолита.
Возможно вообще лучше использовать что-то вроде kubernetes или mesos, тогда очень много чего переписать придётся.arvitaly
29.08.2016 16:25-3> В случае с микросервисами надо думать и разруливать ситуации, когда какой-то сервис лежит.
Это и есть одно из главных преимуществ, разработка микросервиса предполагает, что либо мы вообще ничего не знаем о других микросервисах (управление через посредника), либо считаем, что каждый из них, либо все могут перестать работать в любой момент времени.
Это не навязанная сложность, это то, с чего начинается определение микросервиса. А возможно это потому, что микросервисная архитектура предполагает композицию на основе бизнес-функций, а не техническое разделение.
Т.е. у нас не сервис для базы, сервис для шаблонизатора, сервис для генерации изображений. У нас сервис для оформления заказа, сервис для оформления доставки и т.д. И у каждого, в идеале, своя база, своя сеть, процесс или поток, или контейнер. И если у нас упала база сервиса по оформлению заказов, то сервису доставки все равно. В итоге > 95% бизнеса работает 100% времени (uptime), причем можно распределять надежность по критичности сервиса (например, сервису оплаты выделить 50 программистов, а сервису «Планирование корпоратива» — 1).Suvitruf
29.08.2016 16:38+2И если у нас упала база сервиса по оформлению заказов, то сервису доставки все равно.
Как сервис по доставке будет работать, если база с заказами недоступна?
А если упал сервис по оформлению доставки, то сервису оформления заказов тоже всё равно?alibertino
30.08.2016 11:37Как сервис по доставке будет работать, если база с заказами недоступна?
Потому, что у него своя база, никак не связанная с базой сервиса заказов. Архитектура строится так, что если сервису доставки нужны данные из заказа, то они загружаются в его собственную базу через обмен сообщениями, и ни в коем случае не через прямой обмен между базами или пользование одной. Это несет определенную нагрузку на систему сообщений, но полностью оправдывается слабой связанностью.
А если упал сервис по оформлению доставки, то сервису оформления заказов тоже всё равно?
Если бизнес-логикой они напрямую не связаны, то они поэтому и находятся в разных микросервисах. Принимаем спокойно заказы, а сервис доставки оперативно (или не очень) чиним.lair
30.08.2016 12:07Потому, что у него своя база, никак не связанная с базой сервиса заказов. Архитектура строится так, что если сервису доставки нужны данные из заказа, то они загружаются в его собственную базу через обмен сообщениями, и ни в коем случае не через прямой обмен между базами или пользование одной. Это несет определенную нагрузку на систему сообщений, но полностью оправдывается слабой связанностью.
Ага. Вот пользователь пришел и обновил в заказе данные об адресе доставки. В какой базе они обновились?
alibertino
30.08.2016 15:34Ага. Вот пользователь пришел и обновил в заказе данные об адресе доставки. В какой базе они обновились?
Нужно разделять вопросы UI и вопросы микросервисов. Пользователь не меняет ничего в заказе, он что-то сделал в UI, при этом возникло некое сообщение. Оно должно быть доставлено всем заинтересованым сервисам. Часто делают API-шлюз, выполняющий роль транспорта (не более). Но возможен вариант и работы из UI напрямую с нужным сервисом. Соответственно, в сервис заказа придут нужные ему данные, а в сервис доставки — нужные ему. Если изменился лишь адрес, и он никак не используется в сервисе заказов, то соответственно ему даже сообщение это не придет, а все изменения произойдут в базе сервиса доставки.
Понятия заказ в UI и сервис заказов не связаны в данном случае.lair
30.08.2016 23:24Пользователь не меняет ничего в заказе, он что-то сделал в UI, при этом возникло некое сообщение.
С точки зрения пользователя, он меняет именно в заказе, ему плевать на вашу декомпозицию и сообщения. И с точки зрения бизнеса тоже изменился именно адрес доставки заказа.
Соответственно, в сервис заказа придут нужные ему данные, а в сервис доставки — нужные ему.
Ага, так кто же отвечает за то, чтобы данные пришли и провалидировались (ну то есть чтобы мы не вбили адрес, который наша доставка не умеет)?
Если изменился лишь адрес, и он никак не используется в сервисе заказов, то соответственно ему даже сообщение это не придет, а все изменения произойдут в базе сервиса доставки.
Вот только "сервису заказов" адрес нужен, потому что история заказов должна содержать адреса доставки. Что делать в этом случае?
lair
29.08.2016 17:01А возможно это потому, что микросервисная архитектура предполагает композицию на основе бизнес-функций
Я стесняюсь спросить, а чем это отличается от старой доброй SOA с оркестровкой?
alibertino
30.08.2016 15:38Отличается размером сервисов, как следует даже из названия. А это повышает надежность всей системы, меньше единовременных точек отказа. Опять же, чем меньше кусок, чем проще повторное использование.
Ну и вокруг этого понятия уже сформировались некие принципы, почитайте любую литературу, ключевые слова, highly cohesive, loosely coupled.lair
30.08.2016 23:30Отличается размером сервисов, как следует даже из названия.
В названии-то что угодно можно написать, но "что конкретно"? Если мы и там, и там компонуем бизнес-функции, то размер сервиса определяется размером бизнес-функции, и больше ничем. Так за счет чего в микросервисах размер сервиса меньше?
Опять же, чем меньше кусок, чем проще повторное использование.
Это, кстати, неправда.
Ну и вокруг этого понятия уже сформировались некие принципы, почитайте любую литературу, ключевые слова, highly cohesive, loosely coupled.
Вы меня извините, но принципы "high cohesion, loose coupling" есть еще у МакКоннела — и это наверняка не единственное место, мне просто искать лень. Это, прямо скажем, вообще одни из базовых принципов в дизайне ПО.
maxru
Вода и капитанство, извините.
Как надо: microservices.io