Периодически общаемся с разработчиками о микросервисах, монолитах и прочих мифических существах. Порой такая эзотерика живёт в головах у людей и слышишь порой такое, что ёжики в тумане нервно курят в сторонке.
Когда спрашиваю у людей на собесах, или когда в команде решаем, как клепать очередной проект, такое порой слышу, что становится страшновато. Мне кажется, лет через 5 все компании будут обитать в мультивселенной безумия из “микросервисов”, которую они себе радостно построили, уходя от этих ваших страшных “монолитов”.
С такой кашей в голове можно напринимать таких решений, что мы с вами запаримся потом разгребать последствия. Так вот, чтобы страдать нам всем пришлось поменьше, решил поделиться инфой, может мне повезёт и это прочитают мои будущие коллеги.
Что представляют себе разработчики в среднем по больнице
Если спросить разработчика на интервью, кто такие эти ваши микросервисы, и нафига козе баян, то ответы будут однообразные и грустненькие.
Сразу вспоминают про монолиты, что микросервисы это шаг вперёд и вообще гораздо более лучше. На вопросы, а что есть ещё кроме микросервисов и монолитов, отвечают обычно что не знают.
Прошу обозначить плюсы и минусы каждого подхода - тут всё обычно сложно, но со скрипом называют такие плюсы микросервисов:
-
Можно фигачить каждый сервис на своём фреймворке и языке
-
Проще поддерживать - сервис же маленький, кода мало
-
Проще тестировать - то же самое, сервис маленький же
-
Лучше скейлятся - почему так, и почему эти самые “монолиты” скейлятся хуже, обычно непонятно
Про минусы вообще всё грустно, часто говорят что их нет, или могут сказать, что микросервисы сложнее писать, но почему, опять не могут раскрыть.
Как выглядят эти ответы для меня:
Мало того, что они упускают всю суть, и следовательно начинают лепить “микросервисы” где надо и где не надо, так ещё и делают это плоско, не видя, что никаких “микросервисы vs монолиты” в принципе то и нет, а есть многомерная ситуация в нескольких плоскостях сразу, с кучей выбора.
Отсюда получаем довольно унылые решения, которые негибко ложатся на потребности технической команды и заставляют всех в ней страдать, при этом неясно, кому это вообще было нужно.
Что важно на самом деле
-
Во-первых, “микросервисы” - это сложно
-
Во-вторых, “монолиты” - это не всегда что-то плохое.
-
В-третьих, индустрия давно уже перешагнула через эту возню “микросервисы vs монолиты”, и воюют уже совсем про другое, но вы ещё не в курсе
С кем воюем
У нас сейчас такой обывательский консенсус, что монолит = плохое зло.
– Чем микросервисы лучше?
– Чем монолиты конечно
(цитаты королей разработки)
Пошла такая охота на ведьм, где всё, что вышло плохо, называется монолитным. Монолит стал таким ругательным словом, которое говоришь - и ничего дальше объяснять не нужно.
“Переписываем монолит на микросервисы”, - говорят и пишут из каждого утюга. Объяснять зачем, уже не модно, и так же понятно - за всё хорошее, против всего плохого, за себя и за Сашку.
Если всё-таки покопаться в сортах монолитов, то на поверхности будет такое:
Есть легаси, лапшекод и плохая архитектура. Это там, где код - это лютая каша и поделка без нормальной архитектуры, и там почти весь код - легаси. Разобраться в таком коде трудно, правки вносить ещё труднее, тесты писать тем более - там целое непаханое поле, да и код нетестируемый. Это называется плохой код, и он может быть хоть где, если не уметь писать что-то лучше.
И есть монолиты (про это чуть дальше).
По какой-то совершенно неизвестной мне причине первое часто приравнивается ко второму, хотя это как сладкое и мокрое, вообще разное.
При этом количество синьоров, которые видели эти самые монолиты в глаза, стремительно уменьшается со временем. Сейчас почти каждый разработчик с 5 годами опыта говорит на интервью, что он синьор, и у многих в опыте одни микросервисы.
Индустрия уже давно впереди
Так вот, сейчас умные люди уже подразобрались, что к чему, и уже давно нет битвы микросервисов с монолитами, в этой плоскости теперь думать уже не модно. Вместо этого думают сразу в трёх:
-
Модули. Думают, как код разложить на относительно самостоятельные и осмысленные модули / компоненты. Смотрят, где у нас в кодовой базе разные зоны ответственности, и где между этими зонами провести границы.
-
Репы. То есть репозитории и толкание разработчиков в них, чтобы вместе писать там те самые модули, делать в них ПРы, гонять пайплайны и релизить что-то. Можно сделать монорепу и разрабатывать единицы, десятки или тысячи модулей вместе. Можно каждый модуль вынести в свою отдельную репу. Можно сгруппировать по несколько близких модулей в свои репозитории.
-
Артефакты. Артефакты бывают разные, но в целом это те штуки, которые мы релизим. Сейчас это часто докер-образы, или вообще любая штука, которая релизится или работает более-менее независимо. Думаем, в сколько и в какие артефакты мы хотим деплоить наши модули. Какой-то модуль может быть выгодно задеплоить отдельно, какие-то другие наоборот лучше деплоить вместе.
Про модули
Как и когда разбивать на модули?
Модули нужны чтобы обозначать границы. Границы между чем и чем? Между разными частями нашей предметной области, или как сейчас модно говорить между поддоменами.
При этом если у вас новая разработка с нуля, или ещё круче - новый бизнес, вы не будете сразу понимать, где эти границы проходят, какие штуки связаны между собой и сбиваются в кучи, а какие наоборот не особо связаны.
Поэтому разбивать на модули лучше всего там, где вы понимаете, как разбить, где предметная область вам уже неплохо известна и не будет сильно меняться. Или если вы бьёте на модули, то не проводить между ними сильных границ, то есть не надо вкорячивать между ними общение по HTTP и разносить эти модули в разные микросервисы, пока вы не поняли, что всё устаканилось. Иначе дальше будет много страданий с тем самым распределённым монолитом - штукой, когда границы модулей выбраны неверно, и каждый модуль живёт в своей репе и деплоится отдельно, но они очень зависят друг от друга. Исправить такое будет куда сложнее, чем если все модули лежат в одной репе и вызывают друг друга через методы, а не через сеть.
Получается так. Когда бить на модули:
-
Когда без разбиения на части становится сложно разбираться в коде, большая когнитивная сложность
-
Когда сложно тестировать - например тестов слишком много и они долго выполняются. А гонять надо все тесты при любом изменении. Разбиваем на модули, и если модуль не менялся, то и тесты для него не надо запускать.
-
Когда несколько команд будут работать над разными частями кода. Лучше каждой команде раздать по модулю, потому что очень фигово получается работать, когда много людей толкаются в одних и те же файлах. Постоянно потом конфликты, которые приходится мержить, это долго и больно. И менеджерам сложно трекать, из-за какой команды постоянно баги и откладываются релизы (сарказм, ну или не совсем сарказм ;)). Гораздо проще провести границу между модулями и через эту границу взаимодействовать командам.
-
Когда в коде явно делаются какие-то совсем разные вещи - в одном модуле солим огурцы, в другом управляем грузовиком.
Когда бить ещё рано:
-
Когда вы плохо понимаете, в чём будет ответственность каждого независимого модуля, и где сейчас проходят границы этой ответственности. Можно сначала посмотреть, что там вообще происходит, а потом уже бить на части, когда разберётесь.
-
Когда границы ваших модулей постоянно и довольно сильно меняются. Иначе придётся постоянно эти модули рефачить, двигая границы туда-сюда. Ну и нужны эти границы тогда?
Я мог бы сказать проще - бить надо только тогда, когда уже пора. А если ещё не пора и не видно модулей, то бить не надо. Но бывают сложные ситуации, и там просто сидеть и ждать, когда станет понятно, не сработает. А бывает, что опыта не хватает, и само понимание не придёт. Так что лучше пользуйтесь подсказкой абзацем выше.
Важно: модули - не равно микросервисы. Они не обязаны общаться между собой по HTTP/Кафке, хоть это и можно. Они не обязаны деплоиться отдельно, хоть и можно.
Про репы
У нас 2 крайности - можно или совсем всё держать в одной репе, и будет у нас монорепа, или каждый модуль раскидать в свой репозиторий.
Монорепы
Говорят, сейчас волна хайпа по монорепам прошла, и их уже не форсят так как раньше. Плюс чтобы сделать хорошую монорепу, недостаточно всё сложить в один репозиторий. Нужно ещё классно обмазать всё это пайплайнами, так чтобы у вас был не 1 мега-билд всего, а чтобы артефакты билдились независимо друг от друга и чтобы тесты гонялись отдельно и независимо. И ещё чтобы всё это не толкалось между собой и билды не занимали несколько часов из-за простоя в очереди.
Чего хочет разработчик
Разработчик хочет, чтобы у него всё было в одном окошке IDE, а не в 10, чтобы ему не нужен был для разработки ноут на 64 гига оперативки, и чтоб не надо было переключаться между 5 проектами, чтобы поправить одну багу, а потом создавать и протаскивать 5 ПРов. И ревьюер тоже скажет спасибо, что его не мучают и не заставляют в голове склеивать этот паззл. А сверху на это всё можно написать красивый интеграционный тест и положить его в проект, чтобы можно было его гонять локально, а не ставить задачу автоматизаторам, чтобы подождать несколько дней и потом получать только ночные прогоны этого теста раз в сутки.
Некоторые разработчики считают, что чем больше проектов мы создадим и чем больше окошек IDE у нас будет открыто, тем выше наша крутость, и что это реально зачем-то нужно, для какой-нибудь “поддерживаемости” и “масштабируемости”. Чаще всего ситуация обстоит просто - они не понимают что делают, и это не нужно никому, вот вообще никому.
Когда можно разносить модули по разным репам?
-
Если эти модули редко будут меняться вместе в рамках одной задачи
-
Если эти модули будут пилить разные команды, которые не хотят толкаться и путаться в ПРах и страдать от поломанных другой командой билдов, красных тестов и прочего
-
Когда репа начинает превращаться в монорепу и требовать нетривиальных усилий на поддержку инфраструктуры билдов
Когда не нужно разносить?
-
Когда мы стартуем новый проект и просто решили, что каждый модуль будет в своей репе
-
Когда мы думаем, что нужно всё сделать “масштабируемо” без понимания сути
Просто помните - бездумно растаскивая модули в разные репы, вы делаете больно в первую очередь себе.
Про артефакты
Мы можем захотеть деплоить каждый модуль в свой артефакт, но это сразу приносит кучу проблем.
Нашим модулям надо общаться между собой. Ну или не надо, тогда вообще всё отлично, деплоим как хотим. Или мы делаем библиотеку, которую потом подключаем где-то ещё в коде - тоже без проблем (для библиотек правда совсем другие правила, но статья не о них).
Так вот, если общаться модулям всё-таки надо, а мы их задеплоили по-отдельности, то теперь нельзя просто из одного модуля вызвать метод другого, между ними теперь сеть. И хоба - наша система превращается в распределённую! Со всеми её минусами:
-
Вызвать метод в другом модуле - это теперь долго. Очень долго. Нужно настраивать таймауты, готовить наших клиентов к тому, что наш сервис может отвечать долго из-за сети и доступа к другим сервисам
-
Очень долго - это возможно и никогда. У нас может потеряться сетевая связность между нашими сервисами, или какие-то сервисы могут быть доступны, а какие-то - нет. Часть приложения работает, а часть - нет :) и как нам с этим жить, придётся теперь думать.
-
Всё, что мы хотим передать по сети, надо сначало превратить в текст, типа в JSON например, а потом обратно. Это вообще-то жжёт проц, придётся платить.
-
За траффик тоже придётся платить, так что готовьтесь к разным хакам по его сокращению:
-
сжатие: о нет, оно тоже жрёт проц!
-
кэширование, которое как известно приносит нам самую сложную проблему программирования, плюс вообще-то жрёт память и даёт сервису стейт, с которым потом придётся прыгать - делать его распределённым и прочее (привет ещё траффик!)
-
-
А напоследок самое весёлое - теперь ваша система всегда неконсистентна! Вот этот вот eventual consistency - теперь ваш лучший друг. А ещё:
-
Невозможность атомарно совершать операции. Например, вместо того чтобы в транзакции вызвать метод другого модуля, теперь мы делаем REST-вызов, который не откатится, если наша транзакция упадёт. Транзакции как раньше нам теперь нельзя.
-
Здравствуйте, компенсирующие транзакции!
-
Здравствуй, Saga!
-
Здравствуй, retry!
-
Здравствуй, circuit breaker!
-
Привет, асинхронное общение через очереди!
-
Хай, Dead Letter Queue!
-
Привет, идемпотентность и дедупликация!
-
Здравствуй, exactly once!
-
Здравствуй, transactional outbox!
-
Идемпотентность - наш хлеб и соль!
-
CDC и дебезиум - наше всё!
-
Распределённый сбор логов!
-
Распределенный трейсинг!
-
Распределённый дебаг! (ой, такого у нас нет)
-
И всё это лишь бы не вызывать один метод из другого.
Кстати, некоторые товарищи пытаются сделать вид, что мы всё ещё типа вызываем метод, просто как бы по сети, но проблемы-то уже обратно не закопать, и абстракции у такого подхода текут будь здоров.
Наговнокодить то, что будет тормозить, как жопа тигра, и при этом жрать кучу проца, памяти и гнать кучу траффика, а потом ещё и не мочь разобраться, почему оно тормозит и как это починить - вот классика современной разработки.
Это прямо как с каким-нибудь hibernate ORM, который в фоне заваливает базу миллионом ненужных вам запросов, и никто об этом не знает, пока это не начинает тормозить.
У вас между микросервисами может течь дурацкий паразитный траффик, который никто специально не порождал, и никто и не знает, что он там есть. Просто в результате бага шлётся не 1, а 2 запроса. И ещё вон там лишний запрос, который можно было и не делать. Это в добавок к тому траффику, который все знают, что он там есть, но тоже нифига не понятно, зачем это было гнать по сети.
Нет, конечно для интеграции между системами это всё тоже нужно, но зачем усложнять себе жизнь внутри нашей же системы, которую пилят 2 с половиной землекопа?
Умные дядьки говорят - деплоить модули в отдельные артефакты, которые общаются по сети вместо простого вызова метода, нужно только там, где по-другому не получится, потому что есть такие требования. То есть вы подумали, попытались, точно поняли, что оно не взлетает, и только тогда начинаете между модулями общаться по рестам или кафке.
Когда может иметь смысл деплоить вещи в разные артефакты?
-
Когда один модуль может грохнуть процесс и утащить за собой все остальные, а нам так не хочется. Например, есть система приёма заказов, и система обработки заказов. Если наша система обработки заказов нестабильна и часто падает по OOM, то это не значит что с ней должна падать и система приёма заказов, это может стоить много деняк. А может и нет. Кто знает. Надо узнавать.
-
Когда мы понимаем, что какой-то модуль нужно будет скейлить в совершенно другом темпе, чем все остальные. Тогда можем подеплоить и скейлить его отдельно. Например, какой-то наш API очень любят другие системы и он очень нагружается, а другие нет.
-
Когда мы хотим вообще другой релизный цикл для модуля - хотим деплоить его много, часто, и версионировать своими версиями, и не хотим, чтобы пришлось в релизы тащить все 50 модулей вместе с ним.
-
Когда нам нужно использовать другой стек. Внимание: это бывает не так часто, чаще всего компания старается использовать одинаковый стек везде где можно. Например, есть у нас нейросетка, она что-то считает на видяхе, у неё там вообще свой стек, свои либы, своя жизнь. Мы её можем захотеть запускать отдельно, в том месте, где есть мощные видяхи.
-
Ну либо у нас большая система, и какие-то модули поддерживает отдельная команда, Тогда может быть они сами хотят решать всё что описано выше, и пусть решают себе, и дайте им контроль над тем, как деплоится их код, а не мучайте в общем релизе общего артефакта. Или мучайте если уж так хотите, кто я такой чтобы вам тут указывать.
И помните, что всё ещё существуют библиотеки. Это как сервисы, только их можно подключить себе в проект и вызывать их методы напрямую. Разделение есть, а распределённости нет. Хотя это в наши дни почти забытая техника, потому что в микросервисной библиотеки делать особо не принято.
Дак вот, монолиты и микросервисы
Давайте теперь применим наши новые многомерные знания обратно к изначальной теме.
Получается, наш “монолит” - это система, которая разбита на N модулей, которые разрабатываются в 1 репозитории и деплоятся в 1 артефакт - например один docker-образ или war-файл.
А наши “микросервисы” - это по сути те же N модулей, которые разрабатываются в N репозиториях и деплоятся в N артефактов.
И есть же ещё куча вариантов где-то между, этими двумя, где N модулей могут лежать в M репах и деплоиться в P образов. Если вы видите только монолиты и микросервисы, вы не видите кучу вариантов между ними.
Едем дальше. Все эти модули, сервисы - это не статичная херня. Никогда не бывает, что сели архитекторы, придумали как всё будет, потом пришли прогеры, и всё так и написали. А потом ещё 10 лет система жила и всё так и осталось, как было придумано.
Сервисы живут и развиваются, вечно всё меняется, и не надо думать, что монолит - это на всю жизнь. Или что микросервисы всегда будут микросервисами. Всё со временем изменяется под новые потребности.
Например, начинать разработку часто профитно в монолите - 1 репа, N модулей (часто канает по началу даже 1), 1 артефакт. Так быстрее разрабатываем, легче деплоим, нас всё равно в начале мало.
А дальше никто не запрещает эти N модулей начать деплоить в M артефактов вместо 1. Если вы не нарушали границы между модулями, то просто пошли, обмазали это всё контроллерами или кафкой, поменяли конфиг сборки - и готово.
Если хочется, можно ещё и на P реп это всё разнести. Например когда у нас 3 команды и каждая пилит свои модули, не пересекаясь с другой, и им не хочется видеть ПРы других команд и хочется всё своё и отдельно.
Итого
Вооружившись таким пониманием, теперь вы сможете видеть целых 3 измерения там, где раньше было видно только монолиты и микросервисы. И когда к вам придут и попросят спроектировать архитектуру, вы может быть справитесь чуть лучше и сделаете скорее так, как вам реально нужно, а не так, как выдумает ваше испуганное такой задачей сознание, и принесёте чуть меньше боли и себе, и своей команде, ведь вам же потом с этим жить.
Нам пора уходить от устаревшей и абсолютно непродуктивной дихотомии “монолит – микросервисы” и смотреть на наши конкретные потребности, при этом не стоя сложные распределённые системы начиная с первого дня, особенно там где они изначально не нужны.
И не смотрите на своё решение как на финальное, всё течёт и всё меняется. У вас всё равно не получится сделать хорошо сразу и навсегда. Будьте готовы завтра усложнить там, где будет надо, а не сразу делать сложно, ведь “через год нам это понадобится, когда нагрузка вырастет”. Вы за этот год можете и не закончить ваш мега-микросервисный проект.
Дисклеймеры
Дисклеймер 1: да, язык у статьи простоватый и даже упрощённый, а правила русского языка выборочно игнорируются. Это сделано сознательно, хочу чтобы не звучало душно и люди прочитали, а не заигнорили. Возможно это прочитают и те, с кем я потом буду работать.
Дисклеймер 2: естественно, много всего было упрощено, иначе получилась бы книга. А в чём-то я мог ошибиться. Если это что-то важное, черканите в комменты плз, буду рад узнать что-то новое с вашей помощью.
Дисклеймер 3: как всё на самом деле, я конечно же не знаю, и вы не знаете, и никто не знает. Но все вместе можем предпринять хорошую попытку это выяснить.
Автор: Captain_Jack