Сегодня трудно себе представить более переоцененный подход к созданию архитектуры программных решений, чем микросервисы. В сети доступно огромное количество статей и видеолекций, в которых авторы рассказывают о том, что такое микросервисы и зачем они нужны. С чем, в общем, справляются довольно успешно, раскрывая их преимущества и недостатки. Так что эта статья рискует стать очередным пересказыванием очевидных вещей. Поэтому я сосредоточусь на том, чтобы сформулировать и донести до вас то, как я понимаю эту тему, основываясь на опыте тех проектов, в которых имел честь принимать участие. И том совокупном коллективном опыте, что медленно, но верно накапливается в IT индустрии.
Эта статья является моим авторским текстом и не является переводом. Если вы со мной не согласны, желаете дополнить или поделиться своим мнением, я буду рад обсудить вашу взвешенную и аргументированную позицию в комментариях.
Человеку, который только начинает заниматься разработкой или поддержкой программных решений, придется столкнуться с тем, что микросервисная архитектура является очень размытым и плохо определенным понятием:
Одни говорят о том, что микросервис - это просто маленький сервис, не уточняя при этом, насколько маленьким он должен быть. Возникает классическая проблема, знакомая нам по старому мультфильму, - сколько орехов нужно собрать, чтобы получилась целая куча.
Некоторые вводят понятие человеко-пицца-часов, за рамки которых не должно выходить время разработки одного такого сервиса. Ходят слухи, что для написания одного микросервиса необходимо затратить порядка двух недель, при этом над ним должно работать столько разработчиков, сколько можно накормить двумя пиццами.
Другие говорят, что сперва необходимо разбить проект, над которыми вы работаете, на слабо связанные бизнес-задачи и поместить каждую такую задачу в свой микросервис. Непонятно тогда, почему выделение этой же бизнес логики в отдельную изолированную библиотеку является недостаточным. Почему логические рамки, задаваемые на уровне программного кода в виде отдельных библиотек, модулей и файлов, нужно заменять другими рамками - отдельными процессами, существующими уже на уровне операционной системы.
Все эти расплывчатые формы приводят к тому, что микросервисами иногда называют и огромные монструозные программы, и отдельные функции, которые по какому-то странному стечению обстоятельств были оформлены как самостоятельные программы - сервисы. Причем часто эти два подхода в одном проекте сосуществуют рядом друг с другом как дань накопившемуся со временем техническому долгу.
Однако несомненным остается следующее: в наше время практически все новые системы, за некоторым исключением, начинают создаваться как микросервисы, причем никто даже не понимает почему. Это воспринимается как современный стандарт, как будто по-другому сегодня делать невозможно. Начинание нового проекта в формате так называемого “монолита” может восприниматься как признак отсутствия профессионализма, как проблема архитектуры, которую впоследствии невозможно будет устранить.
Микросервисы и SOA
Если мы начнем копать еще глубже, то выяснится, что микросервисы как будто ничего нового и не привносят. Задолго до того, как появилось это название, похожий подход уже вовсю эксплуатировался. Даже термин был вполне себе сложившийся - сервис ориентированная архитектура (SOA). Многие идеи и схемы уже существовали и почти в неизменном виде были изобретены заново: общая шина сообщений, использование реляционных баз данных, использование сериализованного формата HTTP запросов для межсервисного взаимодействия. Все это имеет настолько похожую форму, что мы имеем право задаться вопросом о том, насколько правомочно выделять микросервисы в нечто самостоятельное, революционное с точки зрения развития технологии.
Я смею предположить, что изначально сервис-ориентированную архитектуру воспринимали скорее как недостаток, чем преимущество. Крупные компании на определенном уровне своего развития сталкивались с необходимостью реализации нового функционала, который использует уже хранящиеся где-то в другом месте данные. Чтобы расширить предоставляемый сервис и получить при этом конкурентное преимущество, им было необходимо настроить интеграцию с системами бизнес партнеров или одной из дочерних компаний.
Они, конечно, предпочли бы то положение вещей, когда все данные лежат в одном месте и доступны без задержек и любого рода ограничений. Но сама реальность диктовала условия, когда необходимо реализовывать множественные связи между полностью или частично изолированными системами. А потому эти разные сервисы должны каким-то образом обмениваться информацией, обеспечивая при этом высокую скорость и надежность работы. Необходимость при разработке учитывать нестабильность сетевых соединений скорее могла раздражать и воспринималась как временный недостаток, который впоследствии, с развитием технологий, будет устранен. На первых этапах мало кто мог разглядеть в этом принципиально новую реальность, под которую необходимо переосмыслять весь подход к разработке программного обеспечения. Если раньше мы просто запускали функцию и ждали результата ее работы, то теперь мы вынуждены жить с тем, что вызовы удаленных процедур по сети принципиально не позволяют обеспечить достаточно высокие гарантии надежности. Какие технологии мы бы не использовали, всегда будут задержки, обрывы и прочие неприятные эффекты, связанные с тем, что часть нашей системы находится вне нашей зоны контроля. И ее разные части не ограничиваются рамками одного сервера. Это подталкивало к тому, чтобы разрабатывать новые распределенные и асинхронные системы для обмена сообщениями с гарантией доставки. К тому же, в какой-то момент объёмы информации, с которой необходимо работать увеличились настолько, что хранить их на одном сервере, даже самом мощном, стало физически невозможно. Именно тогда межсервисное взаимодействие и вошло окончательно и бесповоротно в нашу жизнь и весь вопрос заключался в том как именно оно будет осуществляться.
С этой стороны SOA выступает перед нами некой стихийно сложившейся действительностью, более или менее осознанной разработчиками, чем сознательно продуманным и цельным подходом. И хотя, много позже, ее довольно неплохо стандартизировали и привели в некое подобие стандарта, она так до конца и не смогла укрепиться и в результате была вытеснена микросервисной архитектурой. Которая во многом выступала скорее как осознанный выбор, позволяющий получить вполне конкретные преимущества. Многие тенденции, наметившиеся ещё в сервис ориентированной архитектуре были доведены до своих логических крайностей и получили более выраженную и законченную форму.
Микросервисы и монолит
Это еще раз подводит нас к вопросу - что же такое микросервисы, насколько маленькими они должны быть и как должны взаимодействовать между собой?
Статьи о микросервисах обычно начинают с рассказа о том, что такое "монолит". Читателя плавно подводят к осознанию того, насколько же он плох в современной разработке, подкрепляя этот вывод длинным списком сопутствующих проблем. Под монолитом имеется в виду любой достаточно большой сервис, возможно даже включенный в более сложную сервис ориентированную систему как один из ее компонентов. Ближе к концу статьи вы обязательно прочитаете о том, что начинать разработку системы легче и быстрее с монолита, но со временем стоимость поддержки этого монолита станет дороже, чем стоимость поддержки системы, построенной на микросервисах. Это подаётся как очевидный факт. В заключение, авторы выведут правило, что на этапе прототипа все же стоит начать с монолитной архитектуры и со временем, по необходимости, начать разбивать его функционал на микросервисы.
Несмотря на то, что рациональное зерно в этом утверждении есть, его все же нужно понимать очень условно. Так как любую систему можно переусложнить до такой степени, что с ней уже нечего будет делать, кроме как переписать заново. Так или иначе, но в моей практике чаще попадались примеры, когда проект стартовал с микросервисной архитектуры и разработчики с самого первого дня начинали страдать от всех болезней, связанных с этим выбором, таких, как избыточность кода, управление зависимостями и поддержкой распределенных систем. Тогда как преимуществ, которые, как было заявлено, следуют вместе с микросервисами, было либо совсем мало, либо их полезность можно было поставить под сомнение. Возможно, это происходит из-за завышенных ожиданий, когда сходу и на чистом энтузиазме пытаются построить идеальную систему, которая сможет на равных конкурировать с ведущими лидерами рынка.
Таким образом, под монолитом имеют в виду единый сервис, который содержит в себе большой пласт логики для автоматизации работы если не целого предприятия, то как минимум одного из его подразделений. Сервис ориентированная архитектура обычно все еще имеет в своей основе монолиты, но такие, что обмениваются между собой информацией. Такие сервисы могут быть как исторически сложившимися, так и написанными вполне осознанно, отвечающими за вполне конкретный и обособленный функционал. Как пример можем привести сервис авторизации пользователей. Если же мы говорим про микросервисную архитектуру, то даже один такой сервис авторизации может разбиваться на десятки других, более мелких сервисов, каждый из которых делает что-то свое в зависимости от конкретных особенностей бизнеса.
Микросервисы и боль
Как же будет выглядеть система, в которой неукоснительно следуют за теми лучшими и общепринятыми практиками, что обычно приходят на ум, когда говорят о микросервисной архитектуре? Что предстанет перед взглядом условного инженера, который попадет в команду разработчиков, разрабатывающих такую систему?
Он, вероятнее всего, увидит огромное количество разрозненных Git репозиториев. Десятки, а может быть сотни проектов, созданных разными людьми в разное время. Даже если все написано на одном языке программирования, каждый проект будет довольно сильно отличаться от остальных: разные фреймворки, разные межсервисные зависимости, разные версии одних и тех же библиотек, разный код для инициализации и развертывания и многое другое. Он обнаружит, что вместо того, чтобы выделить функционал в общие библиотеки, часть логики будет дублирована между сервисами, а та, что удостоится чести быть общей, будет требовать повторной cборки всех проектов при своем изменении. Он увидит, что команда разработки очень много усилий уделяет обратной совместимости сервисов и версионированию API. В общем, каждое изменение, которое требует изменений сразу в нескольких микросервисах, будет требовать очень серьезных усилий со стороны широкого круга людей.
К примеру, для обеспечения обратной совместимости работы сервисов необходимо поддерживать старые API, что используются другими, ещё не обновленными частями системы. Эти сервисы, к слову сказать, могут принадлежать совсем другой команде, и вы никак не можете повлиять на то, когда именно они внесут то или иное изменение. Это создает проблему координации изменений и удаления кода, который уже не используется, на уровне каждого отдельного сервиса.
Да что там отдельный API интерфейс! Часть сервисов будет устаревшими, но никто не будет способен гарантировать, что ни один из сотен остальных сервисов их более не использует. Даже если все согласны, что они в целом и общем более не нужны, эти интерфейсы все еще могут использоваться во второстепенных и не критичных для функционирования механизмах. К примеру, в каких-нибудь переодических заданиях по составлению отчётности. Может так случиться, что вы все же захотите найти эти зависимости во внешних системах и удалить их. В этом случае препятствием может выступить то, что люди, которые ответственны за эти системы, могут быть весьма загружены и не иметь времени вам содействовать. А может быть это будет один из тех сирот-сервисов, который уже никому не принадлежит, но все еще активно используется. Поэтому, только если это не является абсолютно критичным, программисты обычно предпочитают не трогать чужие проекты, надеясь на то, что в будущем кто-то другой их починит и удалит ненужный или неработающий код.
Даже если команды тратят огромные усилия на поддержку всего этого “зоопарка” в актуальном состоянии, это само по себе ничего не гарантирует. Ведь пишущие бизнес логику разработчики, а так же инженеры, которые занимаются развитием и поддержанием всей системы в рабочем и стабильном состоянии, зачастую имеют противоречащие задачи и приоритеты, слабо увязывающиеся вместе.
Если сервис больше не нужен - разработчик про него забывает и идет дальше, но он и та инфраструктура, что им использовалась, все еще остается.
Сервис не стабилен - разработчик выделяет ему больше ресурсов, не предполагая, что причина кроется совсем в другом и его действия ни на что не повлияли.
У него нет времени, он спешит сдать задачу и переключится на другие проекты, а команда эксплуатации продолжает поддерживать такие кривые конфигурации, которые к тому же стоят компании огромных денег. Удалять инфраструктуру становится очень сложно, так как никогда не знаешь, что используется, а что нет. А спросить об этом в больших и сложных системах порой бывает некого, так как нужные компетенции могут попросту отсутствовать, а искать разбросанную по сотням репозиториев информацию может быть крайне не просто.
Тогда бизнес говорит - давайте равняться на международный опыт лучших компаний и руководствоваться принципом “You own it - you run it”, подразумевающий то, что разработчики будут сами отвечать за те сервисы, что создают и будут управлять всем их жизненным циклом. Вплоть до поддержки той инфраструктуры, с которой непосредственно взаимодействуют клиенты. В этом случае ещё сильнее, чем раньше, начинает размываться технологический стёк и каждая команда начинает писать что-то совершенно свое, без оглядки на опыт других. Именно так в системе появляется зоопарк решений, где PostgreSQL соседствует с MariaDB, Kafka соседствует с Pulsar, Redis соседствует с Memcached, а так же порождается много других забавных и нелепых ситуаций. Было бы неправильно думать, что операционная сложность системы в результате таких действий уменьшится, скорее наоборот. В какой-то момент каждая команда может принять решение использовать разные системы сборки, разные инструменты для тестирования и выката изменений. Понятно, что для каждой такой миграции могут понадобиться месяцы, в течении которых необходимо будет поддерживать обе системы, а возможно ее рудименты будут сохраняться годами в качестве ненужных файлов в репозитории, не использующихся классов и библиотек. А также запущенных сервисов, которые все еще кем то используются.
Мы, конечно, имеем право спросить - куда смотрят архитекторы и менеджеры? Куда смотрит бизнес? Где найти тот баланс, когда можно и успевать за быстро изменяющимися потребностями, и находить время постоянно рефакторить уже написанный код? Парадокс заключается в том, что долгосрочные интересы бизнеса неявно продвигают инженеры, которые хотят снизить сложность своей ежедневной работы, тогда как бизнес часто руководствуется краткосрочными интересами. Несмотря на то, что на словах он как раз и выступает за долгосрочное развитие, на деле же оно часто подчиняется краткосрочным квартальным и годовым планам. Если у вас не будет хорошо мотивированных инженеров, которым будет предоставлено достаточное количество свободы действий, любая претерпевающая изменения система со временем будет только захламляться и обрастать техническим долгом. Это будет происходить вне зависимости от профессиональных навыков отдельных разработчиков и вне зависимости от того, какое количество стандартов и процессов будет в компании создано вокруг механизмов принятия решений. Необходимо регулярно проводить чистку кодовой базы, адаптировать архитектуру системы под новые условия, удалять ненужные ресурсы, оптимизировать нужные, а также проводить другие профилактические работы. Если последовательно и достаточно долго игнорировать эти, казалось бы мелкие и некритичные в масштабе целой компании проблемы, в конце концов единственным возможным решением будет переписать все заново, каким бы трудным это ни являлось. Вы неизбежно придете к ситуации, когда любое даже самое мелкое изменение будет требовать таких больших и фундаментальных усилий, что любое движение вперед будет стоить нечеловеческих усилий. Либо, как альтернатива, будет приводить к созданию новых уровней абстракции, что еще больше будут усугублять ситуацию, откладывая решение проблемы на будущее.
В такой системе наш инженер также увидит огромное количество сервисов, часть из которых содержат всего несколько сотен строк кода. При попытке разобраться, выяснится, что они все реализуют один и тот же адаптер для разных типов данных. Ведь все же знают, что при написании микросервисов нужно делать именно так. Вместо того, чтобы реализовать один асинхронный сервис-воркер, где в одном месте реализована обработка разных типов данных, его дробят на части, создавая новые репозитории с огромным процентом дублирующего кода и отдельным CI/CD процессом. Для каждого такого сервиса-обработчика должна быть своя отдельная база данных, а также отдельно и независимо настроено его автомаштабирование в зависимости от нагрузки на систему.
Разве где-то написано, как правильно делать? Создавать ли сервис адаптер для каждого типа данных, либо один такой сервис для всего многообразия использующихся типов данных. Разве где-то написано, какие базы данных нужно использовать? Разве где-то написано о том, можно ли переиспользовать одну базу данных для разных сервисов или правильно создавать отдельную базу данных для каждого из них? Разве где-то написано, как правильно управлять зависимостями между элементами одной системы? В каждой конкретной ситуации должно исходить из конкретных особенностей каждого сервиса и проекта в целом, учитывать разные интересы, возможно, конфликтующие между собой. Однако, вместо этого разработчики чаще исходят из своих субъективных представлений о том, как должна выглядеть микросервисная архитектура, и без оглядки следуют этим представлениям.
Монолиты и монолитность
Так что же такое микросервисная архитектура? Чем она отличается от других архитектур, и каковы ее особенности? Обычно, когда отвечают на этот вопрос, то пытаются противопоставить ее не только уже упоминаемой нами ранее сервисо-ориентированной архитектуре, но и тому подходу, что условно называется монолитом.
Как мы уже говорили, исторически многие даже очень крупные системы разрабатывались как единый программный код, который компилировался в один довольно раздутый бинарный файл и непосредственно запускался на продакшене как системный сервис. В нем находилась вся бизнес-логика организации, были реализованы все возможные интерфейсы на все случаи жизни. И до какого-то момента это всех устраивало. Единство всей кодовой базы позволяло хранить все модули и библиотеки внутри проекта и эффективно их переиспользовать. А с другой стороны, внутри такой программы все функции и данные доступны в едином адресном пространстве, что позволяло при необходимости создавать неявные связи между модулями, создавая так называемый “спагетти код”. Некоторый уровень изоляции мог быть в какой-то степени реализован средствами самого языка, однако, в общем случае, всегда существовала масса способов получить доступ к внутренним ресурсам других модулей, не получив на это разрешения у команд, которые их разрабатывают. Это могло приводить и постоянно приводило к разным трудно уловимым проблемам. Разобраться в ситуации, найти источник проблемы и исправить ее было крайне трудно. Эту ситуацию можно до какой-то степени смягчить, введя очень подробные стандарты и жесткие правила аудита, как это сделано, например, в ядре Linux, но все же в полной мере эта проблема решена быть не может.
К примеру, любой модуль драйвера устройств, подключенный в пространство ядра, имеет прямой доступ ко всем его подсистемам, что в некоторых случаях при недобросовестном поведении может обрушить всю систему.
В попытках исправить эту ситуацию появилась идея разбивать эти самые модули на отдельные процессы. В этом случае программа будет лучше изолирована от других таких же сервисов и все взаимодействие между ними сможет происходить по заранее прописанным интерфейсам и никак иначе. Может показаться, что это решает проблему, что железные межпроцессорные барьеры помогут создавать лучший и более безопасный код. Но на самом деле весь тот ужас, что мы описывали ранее, переместился на уровень выше, но от этого не перестал существовать, но только усугубился дополнительными уровнями абстракций. По сути, проблема заметается под ковер, когда ее как бы не существует на уровне программного кода, где ее не видят разработчики. Но она же возникает на уровне инфраструктуры, создавая и запутывая связи между сервисами, усложняя конфигурацию и без того непростой системы. К примеру, если вы используете Kubernetes, это может выразиться в том, что часть бизнес логики переносится в манифесты сервисов (Deployments, StatefulSets), одноразовых заданий (Jobs, CronJobs), каждый из которых необходимо настроить, в том числе передать ему секреты для защищенного подключения к другим системам, базам данных и многое другое. То, что раньше было одной программой и ее отдельными модулями, теперь выражено манифестами, которые повторяют логику ее разбиения на модули, но уже в инфраструктуре.
Однако, тем не менее, несмотря на все это, при появлении таких инструментов оркестрации как Kubernetes, огромное количество компаний бросилось переписывать свои монолиты на микросервисы, усмотрев в этой технологии огромный шаг вперед. И, как выяснилось, микросервисная архитектура позволила решить огромное количество проблем, которые ранее либо не решались вовсе, либо решались из рук вон плохо. Значит необходимо признать за микросервисами и их прогрессивную роль, и ни в коем случае не принижать ее значение.
Но в чем именно заключается эта роль?
Тут мы можем пуститься в долгое пересказывание того, что обычно делается в любой статье и в любой лекции по этой теме, а именно рассказать про масштабируемость, отказоустойчивость, гибкость в выборе языка программирования, библиотек и многое другое. Со всеми этими доводами тяжело спорить, так как они бесспорно верны. Однако, вместо этого я попробую указать на то, что обычно опускают при перечислении преимуществ микросервисной архитектуры и что уходит на второй план.
Даже самая хорошая технология сама по себе ничего не значит. Легко дать примеры, когда замечательный с первого взгляда инструмент оказывается не востребован и заменяется менее удобным, а более удачный язык программирования остается нишевым продуктом и не приобретает массового успеха. Та легкость, с которой огромная IT индустрия приняла эту технологию и в кратчайшие сроки адаптировала в своих системах, позволяет предположить, что причины успеха микросервисной архитектуры нужно искать не только в технических, а скорее в корпоративных ее особенностях. Дело в том, что микросервисы позволяют легко создать условия, при которых инженерные границы повторяют границы административные. То есть такие границы, которые копируют отношения внутри компании, повторяют деление между разными командами и даже разными отдельными инженерами. Границы микросервисов с их отдельными репозиториями, отдельными базами данных, независимым процессом CI/CD позволяет очень гибко очерчивать зоны ответственности и гибко перераспределять людские ресурсы между проектами. Создание новой бизнес логики легко отслеживать по созданию новых микросервисов и даже технически неподкованным людям легко визуализировать процесс разработки. И, наконец, знать кто конкретно отвечает за конкретный микросервис и конкретный функционал, чтобы быстро реагировать на инциденты и распределять новые задания. В целом, микросервисная архитектура позволяет снизить сложность управления людьми, позволив одному и тому же количеству инженеров строить более сложные и масштабные системы. А значит, быть более конкурентноспособными на рынке, что обычно выражается в большей прибыли.
Однако сама по себе микросервисная архитектура - это не лекарство от всех проблем и не серебряная пуля. Просто разбить старую систему - монолит на микросервисы само по себе ничего не гарантирует и не сделает вашу систему менее сложной. Люди, которые утверждают обратное, как правило, путают монолит как способ разработки программного обеспечения и монолитность как свойство вообще любых систем. Противопоставляя монолитам микросервисы, они забывают, что это всего лишь способ доставки программного кода конечным пользователям. Но это ничего не говорит о внутреннем строении системы и ее свойствах. Можно представить себе микросервисы, которые являют собой монолитную систему, а можно написать монолит, который не обладает свойствами монолитности. Монолитность - это такое свойство системы, не позволяющее изучать ее по частям. Такую систему можно изучать только целиком. Сама попытка ее декомпозировать, то есть разделить на отдельные части и изучить помодульно, бессмысленна, потому что каждый отдельный модуль сам по себе ничего не означает. Он имеет смысл только в связке с другими модулями, и сам с ними связан. Монолитность будет всячески мешать инженерам работать с системой и в целом ничего хорошего не несет.
Золотая середина
Имея в виду все сказанное выше, имеем ли мы право утверждать, что монолит устарел окончательно и бесповоротно, а начинать разработку любой системы следует начинать сразу с микросервисов? Можно ли на ранних этапах разработки объединить преимущества обеих архитектур, чтобы погасить негативные эффекты друг друга, пусть и ценой отхода от общепринятой практики? Я склонен положительно отвечать на последний вопрос.
Вне всякого сомнения, что микросервисная архитектура плотно вошла в нашу жизнь и любая достаточно большая система получит огромные преимущества от грамотного ее использования. Существует так же огромное количество удобных инструментов, вроде того же Kubernetes, которые созданы облегчать разработку и поддержку таких систем. В противовес этому несомненным является также то, что на ранних этапах разработки все же стоит избежать чрезмерного дробления проекта на мелкие части, чтобы разработка и поддержка проекта была максимально простой. В этом отношении более крупные сервисы будут иметь очевидные преимущества. Если вы сохраните модульность на всем протяжении процесса разработки такого сервиса и заранее продумаете те границы, по которым он впоследствии будет разбит на микросервисы, то в случае необходимости не составит труда произвести такое разбиение относительно быстро и без особого риска для системы.
Конкретно это может выразиться в следующем:
Вы можете работать в одном Git репозитории, который содержит код нескольких сервисов. Каждый сервис будет располагаться в отдельной директории, названной в честь этого сервиса. В добавок к этому отдельная специальная директория будет содержать код всех общих библиотек.
Мы, конечно, хотим запускать наши сервисы как контейнеры, поэтому для каждого сервиса будет храниться Dockerfile, на основе которого мы будем создавать отдельный образ. В каждый образ будем включать только код директории сервиса и код общих библиотек из общей директории. Так что в образе каждого сервиса будет содержаться только то, что ему необходимо для своей работы, но не код смежных сервисов. Для тех, кто может наблюдать только запущенные контейнеры, будет вовсе не очевидным, что весь код системы фактически находится в одном месте.
Все сервисы, для простоты могут использовать одну и ту же реляционную базу данных, причем все таблицы можно поместить рядом, визуально отделив их общим префиксом. Разные сервисы будут использовать только свои таблицы, поэтому при необходимости в будущем вы сможете без последствий разделить их по разным базам. Понятно, что создание внешних ключей между таблицами разных сервисов должно быть полностью исключено. Также исключены запросы, что объединяют принадлежащие разным сервисам данные. Такой подход значительно упростит резервное копирование вашей системы, так как обслуживать одну базу данных гораздо проще, чем несколько.
При добавлении новых коммитов в репозитории проекта будет автоматически запускаться процесс, который соберет для каждого из сервисов их исполняемые файлы, затем построит для каждого свой образ и сохранит эти образы в специальном репозитории. Все это можно делать параллельно, ведь зависимостей между сервисами быть не должно. Вы также можете создать для каждого сервиса свой Helm Chart, поместить его в отдельной директории репозитория проекта, а затем специальной утилитой, вроде Helmfile или ее аналога, разворачивать новую версию ваших сервисов в кластере Kubernetes.
Самый важный совет, который только можно дать на первом этапе разработки, это избегать плодить лишние сервисы. На этом этапе весь API Backend можно разместить в одном сервисе, логически разделив префиксами те API, что в последствии будут выделены вами в отдельные микросервисы. Весь код такого сервиса должен быть модульно организован, а межмодульные связи должны быть реализованы через тот же самый HTTP API, даже если фактически достаточно локально вызвать метод.
Таким же образом можно реализовать еще один сервис для асинхронной обработки данных через общую для всего проекта шину сообщений. Один и тот же процесс, вероятно, запущенный в нескольких репликах, будет подписываться и обслуживать события разных типов. Все взаимодействие между модулями внутри одного такого асинхронного сервиса так же должно реализовываться асинхронно через шину сообщений, даже если технически достаточно просто вызвать локальную функцию. Внутри такого сервиса обработчик каждого типа реализован как отдельный модуль, никак не связанный с модулями других обработчиков, а поэтому, несмотря на то, что формально вы все еще имеете один сервис, практически это никак не будет себя проявлять.
После всех этих манипуляций вся ваша система сведется до трех - четырех сервисов, которые не являются микросервисами в полном смысле этого слова, но все же недалеко от них уходят. Их легко поддерживать, так как для расширения функционала нам достаточно работать только с одним репозиторием, и все логические связи проекта и весь его код находятся в одном месте. Подобный подход убирает с повестки дня вопрос управления зависимостями, так как их просто нет - всегда существует только одна актуальная версия сервисов. Одну версию мы строим, ее же мы тестируем. Затем мы разворачиваем ее в одной из рабочих сред, одновременно обновляя образы всех сервисов. По сути мы имеем тут монорепозиторий со всеми вытекающими из него преимуществами и недостатками. Последние, к слову, не будут проявлять себя до поры до времени, пока ваш проект не станет чрезмерно большим.
С технической стороны вы все еще сможете использовать Kubernetes и многие другие современные инструменты, разработанные для микросервисной архитектуры. Да, возможно, вы не получите максимальной гибкости, но все еще будете находится в одном и том же стеке технологий, набирая опыт и оттачивая навыки.
Архитектура типичного проекта может выглядеть следующим образом:
Сервис для API Backend, с которым непосредственно взаимодействуют клиенты. Взаимодействует с шиной сообщений и с базой данных.
Сервис для асинхронных заданий, который получает из шины сообщений задания разных типов и паралельно их исполняет. Взаимодействует с сервисом API Backend, а так же может самостоятельно посылать задания в шину сообщений. В особых случаях может работать напрямую с базой данных связанного с обработчиком сервиса.
Сервис для статичного контента, построеный как обертка поверх nginx. Хранит html, JavaScript и любой другой редко изменяющийся контент для функционирования сервиса.
Сервис-контроллер, который будет генерировать переодичные события и посылать их через шину сообщений на асинхронную обработку. Не обязателен, так как не все системы нуждаются в таком функционале.
Реляционная база данных. Хранит все состояние системы.
Брокер, реализующий шину сообщений нашей системы.
Да, это не идеальная схема, но она позволит вам силами небольшой команды продвинуться достаточно далеко и довольно быстро, а затем, когда (и если) ситуация будет этого требовать, быстро разделить ваши сервисы на множество мелких микросервисов и построить систему, не уступающую лучшим мировым стандартам.
Какой же вывод можно сделать из всего нами сказанного?
Микросервисная архитектура начинает проявлять свои главные преимущества только на большом масштабе. Верным признаком этого является ситуация, когда над проектом трудятся много команд, каждая из которых испытывает острую необходимость в автономности. В случае, если ваша система относительно невелика, либо поддерживается одной небольшой командой, вы, конечно, можете получить некоторые преимущества и технические возможности при использовании чистого микросервисного подхода, но не более того. К этому масштабу нужно подбираться постепенно, начав с относительно крупных, но модульных сервисов, и всю свою работу организовать таким образом, чтобы последующее дробление их на микросервисы было легко осуществимым.
Микросервисная архитектура - это не более, чем способ организации программного кода для его удобного запуска и поддержки в рамках достаточно большой системы. Система, которая следует такому подходу, должна восприниматься как единое целое, начиная от начального дизайна, написания кода, и вплоть до его последующей поддержки. Рассматривать микросервисы как потребность одной группы инженеров эксплуатации в стабильности работы системы - это заранее обречь свой проект на неудачу. Для успеха нужна слаженная и тесная работа огромного коллектива людей и все должны быть заинтересованы в конечном результате.
Не пишите микросервисы, пишите модульно. Дробление кодовой базы не всегда хорошо, тогда как модульность хороша всегда. Она позволит вам писать код, который легко читается, легко поддерживается и будет вам стоить меньше. Рефакторинг в долгой перспективе важнее, чем функционал. Не бойтесь переписывать. Игнорируемый сегодня технический долг - это огромные деньги, которые необходимо будет потратить завтра.
В целом и общем, я могу лишь пожелать вам успехов в создании интересных проектов.
А на сегодня это все.