Andriy Shyrokoryadov

.Net developer, data scientist

Микроуслуги - вопрос №1 на собеседование по программированию

Текст к видео "Что такое микроуслуги (микросервисы)," на канале YouTube

Под одним из видео, опубликованном на моем канале, в комментариях я получил несколько просьб подготовить видео на тему микросервисов или микросервисной архитектуры. В данном видео я выполняю эту просьбу, так как помимо вопросов сугубо технических, на собеседовании могут появиться вопросы архитектурные, кроме всего прочего также и на тему микросервисов. Микросервисы в последние несколько лет стали популярной темой в программировании современных приложений. Большинство новых приложений создается с использованием этого архитектурного подхода. Старые приложения также не отстают – их переписывают на микросервисы. Поскольку микросервисы это популярная и модная тема, решение об их использовании в приложении иногда принимается эмоционально, без чёткого понимания плюсов и минусов данного решения. Все сейчас так делают, и мы будем делать также. Чтобы вы не оказались в таком положении, и чтобы решение об использовании, или не использовании, микросервисов было рациональным, а не эмоциональным, данное видео будет выстроено следующим образом:

  • описание концепции микроуслуг с использованием примера из жизни – вы удивитесь, но примеров «микрослуг» в повседневной жизни достаточно много;
  • поговорим о преимуществах и недостатках микроуслуг;
  • в конце видео будут приведены несколько примеров использования данного архитектурного подхода в программировании.

Представим себе супермаркет, в котором, кроме продовольственных отделов, есть структурные подразделения с определенными функциями. Например:

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

Естественно, в реальном супермаркете может быть иное число подразделений, больше или меньше, а также они могут иметь другие функции. Однако для нашего примера наличие этих 5 структурных подразделений со своими функциями будет достаточно. Как можно заметить данные структурные подразделения выделены на основании своих функций и нет в нашем списке подразделений, которые свои функции дублируют.

Для нормального функционирования супермаркета данные подразделения должны взаимодействовать между собой. Однако самого взаимодействия будет недостаточно. Если взаимодействие никак не регламентировано, то возникнет хаос. Например, может быть требование из зала касс к торговому залу отменить покупку, сделанную по ошибке, однако оно не будет обслужено. Например может быть просьба «Маша, отмени мне покупку алкоголя!» и ответ на эту просьбу «Не могу Катя, мне на склад надо накладную занести, вернусь через полчаса.» Чтобы избежать такого хаоса, данные взаимодействия между отделами необходимо регламентировать.

Регламент взаимодействия между отделами легче всего опирать на том, что каждый из отделов может сделать для других отделов. То есть каждый из отделов может подготовить список, который будет начинаться словами «Я могу…»

Например, такой список для структурного подразделения «Склад» мог бы выглядеть следующим образом:

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

Имея такой список по каждому структурному подразделению, подразделения могут составить список, что они могут получить от других отделов. Например, торговый зал, имея список функций склада составить список взаимодействия со складом:

  • склад может выдать нам товары;
  • мы можем запросить остатки по всем товарам или по одному товару
  • склад может известить нас если какие-либо товары заканчиваются на складе.

Давайте подведем итог, что мы имеем на данный момент:

  • список структурных подразделений супермаркета;
  • список функций каждого структурного подразделения супермаркета;
  • список требований каждого подразделения по отношению к другим подразделениям.

Изначально, когда я готовился к данному видео я планировал в этом месте сообщить следующее. Каждое подразделение супермаркета можно назвать микросервисом, который выполняет определенные функции (предоставляет услуги). Сам по себе микросервис является отдельным приложением. В нашем списке 5 подразделений значит у нас будет 5 приложений, то есть 5 микросервисов. Если кто-то спросит нас как выглядит наше приложение, то мы ответим, что наше приложение состоит из 5 микросервисов. Что из себя представляет приложение, которое является микросервисом? Это приложение, которое способно обрабатывать определенные запросы. Какие именно запросы данное приложение способно обрабатывать определяется API данного приложения. Что такое API? Это спецификация того, как вы можете взаимодействовать с данным приложением на программном уровне. То есть, по сути, список того, что может делать наш склад, который был показан несколько минут ранее, это и есть API микроуслуги «склад». Это был мой изначальный план для этого видео. Но я подумал, что будет лучше если мы распишем вместе создание приложения «Супермаркет» используя не только «микроуслуги», но и их противоположность, так называемый, «монолит» или монолитная архитектура. Таким образом лучше будет видно преимущества и недостатки этих двух архитектурных решений. Но прежде, чем мы начнем это делать я хотел бы более детально рассмотреть несколько понятий, который уже упоминались:

  • определить, как фактически может выглядеть приложение – микроуслуга.
  • более подробно рассмотреть, что такое API и дать примеры из реальной жизни.

Каждая микроуслуга — это специализированное приложение, работа которого сосредоточена на выполнение определенных операций. Несколько взаимодействующих между собой микроуслуг формируют одно большое приложение. Поскольку наши приложения являются микроуслугами, то они должны позволить каким-то образом другим приложениями взаимодействовать с собой. В связи с этим каждая микроуслуга является мини-сервером, который обрабатывает запросы. Это может быть сервер услуг WCF (Windows Communication Foundation) или сервер HTTP. Возможно позже, в другом видео я расскажу, что представляют из себя эти технологии, поэтому не забываем подписываться на канал, чтобы не пропустить это обновление. Если это сервер, то он должен иметь адрес и так называемые endpoint’ы (конечная точка). Адрес сервера можно сравнить с адресом веб-страницы, а endpoint – с конкретным адресом какой-то конкретной вебстраницы. Например, адрес – это https://www.avito.ru/, а endpoint – это https://www.avito.ru/rossiya/nedvizhimost. Концовка второго адреса определяет endpoint. Это был пример на основании адресов веб-страниц, а пример реальных адресов и endpoint’ов микроуслуг может выглядеть следующим образом:

  • http://storage:12345/ - адрес микроуслуги «Склад».
  • http://storage:12345/Product/{product_id}/Quantity - это endpoint получения количества товара на складе по идентификатору товара {product_id}. Скорей всего под этим endpoint ом будет скрываться метод с сигнатурой int GetProductQuantity(Guid productId). Для себя вы можете это запомнить так – endpoint это адрес, по которому используя протоколы http / https / tcp / или другие протоколы, вы можете вызвать определенный метод с определенными параметрами и получить результат.

Итак, на данном этапе мы уяснили, что приложение – микроуслуга это сервер, который может обрабатывать запросы. Запросы приходят под определенные endpoint (адреса) и в рамках работы приложения – микроуслуги выполняются определенные операции и возвращаются результаты. А как физически выглядит это приложение? В зависимости от того, какой тип сервера Вы выбрали WCF или HTTP у вас будет несколько возможностей. Если это сервер WCF, то микроуслуга может быть обычным консольным приложение, услугой Windows, приложением Windows Forms… Преимуществом услуг WCF является то, что они могут быть размещены буквально в каждом приложении. Если вы выбрали сервер HTTP, то ваше приложение будет запускаться либо как приложение в рамках сервера IIS (Internet Information Services) либо как консольное приложение ASP.Net Core. Какой вывод можно сделать из этой информации? Чтобы создать приложение, которое может использоваться в качестве микроуслуги, вам не надо создавать какое-то специальное приложение с шаблоном «Микроуслуга» или обладать специальными знаниями и умениями, чтобы написать приложение «Микроуслуга». Будет достаточно того, что вы в состоянии написать приложение WCF или ASP.Net Core.

А теперь вернемся к вопросу API. Как уже было сказано, API это спецификация того, как мы можем взаимодействовать с определенным приложением с уровня нашего кода. То есть если некоторое приложение имеет графический интерфейс с кнопкой А, нажатие которой вызывает метод Б, то использование API данного приложения позволяет вызвать метод Б с уровня нашего кода, не используя кнопку А и графический интерфейс. Мы уже знаем, что с приложением можно взаимодействовать не только через его графический интерфейс, но и через endpoint’ы. Соответственно API определенного приложения – это будет список всех endpoint ов, которое поддерживает наше приложение.

Крупные веб-сервисы предоставляют публичные API, которые мы можем использовать для написания собственных приложений, которые будут взаимодействовать с этими веб-сервисами. Например, сервис avito.ru имеет API для программистов. К примеру, по данной ссылке описывается endpoint для получения информации по объявлениям авторизированного пользователя. В описании мы можем найти следующую информацию:

  • с каким методом HTTP можно вызвать данный endpoint – в данном случае это GET;
  • какие параметры принимает данный endpoint – раздел QUERY PARAMETERS;
  • с какими заголовками должен быть отправлен запрос – раздел HEADER PARAMETERS;
  • пример успешного ответа на запрос – ответ со статусом 200;
  • пример ответа с ошибкой на запрос – ответ с информацией об ошибке;

То есть используя данный endpoint с уровня кода нашего приложение мы можем запросить avito информацию по объявлениям авторизированного пользователя. Нет необходимости заходить на веб страницу avito.ru и проверять что-либо в ручную.

Подведём некоторый промежуточный итог:

  • микроуслуга – это приложение-сервер способный обрабатывать определенные запросы;
  • тип приложения – микроуслуги будет зависеть от выбранного типа сервера;
  • список поддерживаемых запросов называется API услуги;
  • микроуслуги взаимодействуют между собой посредством вызовов определенных endpoint ов API.

alt text

Как видно из диаграммы, зная API наших услуг, взаимодействия между микросервисами нашего приложения может быть самым разнообразным. Часто, а даже можно сказать, очень часто, взаимодействие двух микроуслуг может изменять состояние некоторых объектов. Данное состояние может быть важно для функционирования всего приложения. Например, для приложения «Супермаркет» событие «Добавлен новый сотрудник» в услуге «Отдел кадров» может иметь значение для услуги «Бухгалтерия», а также для услуги, в которой будет использоваться объект данного сотрудника, например в услуге «Склад». В связи с этим возникает необходимость известить заинтересованные услуги об изменении состояния определённых объектов. При работе с микроуслугами для извещения об изменении состояния различных объектов и элементов системы используется несколько концепций:

  • концепция проектирования «Шина сообщений»;
  • шаблон проектирования «Агрегатор событий» (Event Aggregator);
  • шаблон проектирования «Издатель / подписчик» (Publsiher / Subscriber);

alt text

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

**Шиной сообщений **можно назвать общий канал между всеми микроуслугами. Как правило если что-то опубликовано в шине сообщений, так называемое сообщение, то каждая из услуг, которая имеет доступ к шине сообщений, может получить доступ к этому сообщению. Таким образом мы можем известить все заинтересованные стороны о каком-либо событии отправляя определенное сообщение на шину. Как правило в развитых приложениях с микросервсиной архитектурой используются шины или брокеры сообщений, которые разрабатываются сторонними разработчиками. Например, брокер сообщений RabbitMQ или платформа потоков событий Apache Kafka. То есть нет необходимости писать данные механизмы с нуля.

Шаблоны проектирования «Агрегатор событий» и «Издатель / подписчик» основываются на том, что есть некоторая группа объектов, которые называются «Издателями» - данная группа генерирует сообщения, и есть группа объектов – подписчиков. Объекты – подписчики могут подписаться на определенные сообщения через шину сообщений. Когда объект – издатель генерирует сообщение, все подписавшиеся на это сообщение объекты – подписчики получат это сообщение.

alt text

Теперь, когда нам известно, как взаимодействуют микроуслуги, давайте вернемся к вопросу преимуществ и недостатков архитектуры микроуслуг и архитектуры монолита. У нас есть приложение «Супермаркет», которое мы можем написать, используя эти 2 архитектурные решения. Давайте договоримся, что каждое их эти приложений будет иметь свой одинаковый пользовательский интерфейс. В какой технологии будет написан пользовательский интерфейс не имеет значения, потому что в данном видео нам важен выбор архитектуры приложения, а не его внешний вид.

Форма приложения.

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

Если речь идет об архитектуре микроуслуг, то у нас будет 5 небольших взаимодействующих между собой приложений. Каждой приложение будет отвечать за отдельное подразделение.

Изменения в коде и публикация приложения.

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

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

Другой вопрос - использование микроуслуг. Если изменение касается только бухгалтерии, то мы работаем только с приложением – микроуслугой, которое отвечает за бухгалтерию. Другие микроуслуги нас не интересуют. Мы изменяем приложение бухгалтерии, тестируем его и только его и устанавливаем его отдельно на производственном сервере. Остальные микроуслуги не затронуты нашими изменениями и в апргрейде на производственном сервере не нуждаются.

В случае микроуслуг, благодаря их модулярности и независимости их друг от друга, нам легче их улучшать и вносить в них изменения. Код микроуслуг изолирован друг от друга. Например, изменения в модуле бухгалтерии не влияют на модуль отдела кадров. Иногда в монолитной архитектуре также появляется это ошибочное чувство спокойствия – я изменил модуль А, в модуле Б наверняка ничего не произойдет, но это утверждение ошибочно. В монолитах модули как правило очень тесно между собой связанны и опрометчивое изменение в одном месте может что-то испортить в совершенно другом, непредсказуемом месте.

Поиск ошибок.

Предположим, что от пользователей нашего приложение поступило сообщение что, что-то не работает в модуле отдела кадров. В случае монолитной структуры источником ошибки, которое влияет на модуль отдела кадров, может быть, любое место в коде нашего большого приложения. С другой стороны, если бы вы использовали микросервисную архитектуру, то место поиска ошибки сократилось бы только лишь до одного небольшого модуля, связанного с отделом кадров. Легче искать ошибки в маленьком специализированном приложении, чем в одном большом.

Работа команды над приложением.

Предположим, что над приложением «Супермаркет» работает одна команда, состоящая из нескольких программистов. В случае монолитной архитектуры приложения при активной работе с кодом и частым внесением изменений весьма вероятно появление множества мердж-конфликтов, т.е. конфликтов при слиянии кода, измененного несколькими программистами. Мердж-конфликт, пусть даже и разрешенный, является источником потенциальных ошибок и лучше было бы свести их количество к минимуму. В случае микроуслуг можно договориться в команде, что каждый программист работает с определенной микроуслугой или определенными микроуслугами. Таким образом каждый из программистов работает со своей частью кода, никто из программистов не наступает друг другу на пятки, мердж конфликты хоть и появляются, но уже не так часто, как в случае с монолитной архитектурой.

Устойчивость работы приложения.

Предположим, что в приложении «Супермаркет» появилась ошибка, которая проявилась в необработанном исключении. В случае если в приложении появляется необработанное исключение, как правило оно вводит приложении в такое состояние, когда такое приложение должно быть закрыто и перезапущено. Если речь идёт о монолите, то независимо от того, где появилось необработанное исключение, будь то «бухгалтерия» или «торговый зал», монолитное приложение будет полностью закрыто. Иначе обстоят дела с микроуслугами. Если ошибка затронула только «бухгалтерию», то микроуслуга «бухгалтерии» будет закрыта, а остальные услуги продолжат работу. Некоторые пользователи, которые не работают с бухгалтерией, даже могут не заметить, что что-то не так. Конечно, если мы попытаемся что-то получить от микроуслуги бухгалтерии, то у нас на экране появится сообщение об ошибке, но это будет выглядеть не так фатально, как закрытие всего приложения.

Картина была бы не полной если бы не сказали несколько слов о недостатках микоуслуг. Если быть более точным, речь идет не о недостатках как таковых, а о том, что микроуслуги не являются решением всех проблем. Не для каждой ситуации подходит архитектура микроуслуг. Давайте рассмотрим конкретные ситуации.

Микроуслуги являются решением для сложных проблем. Здесь хочется процитировать слова Мартина Фоулера, программиста и мыслителя: «Даже не рассматривайте микроуслуги в качестве архитектурного решения, если ваша система в виде монолита может быть обслужена без излишних проблем.» Ссылка на стьтью Мартина Фоулера о микрослугах Если приложение является монолитом, но его обслуживание не создает дополнительных проблем команде программистов, то рефакторинг монолита в сторону микроуслуг является избыточным и не нужным.

Второй аспект является следствием первого замечания о микроуслугах. Иногда монолит является слишком маленьким, чтобы его можно было разделить на несколько частей. Поэтому, пусть даже микроуслуги это сейчас модно, но не стоит переписывать каждый «монолит» на микроуслуги.

Если у нас есть старое приложение, так называемый legacy код, то это должно быть некоторым предупреждением нам, что возможно микроуслуги это не то решение, которое мы ищем. Естественно, первым порывом может быть желание переписать этот старый говно код на микроуслуги, но у опытных программистов перед глазами появляется вопрос – знаем ли мы специфику данного кода и бизнес-логику, на основании которой был написан этот код. Если ответ на этот вопрос «Нет», то попытка переписать данный код на микроуслуги может быть болезненной.

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

Еще одним аспектом, который необходимо принимать во внимание при рассмотрении возможности перехода на микроуслуги является скорость работы приложения. В случае монолита вся коммуникация между модулями приложения происходит в оперативной памяти компьютера, на котором установлено приложение – монолит. В случае микроуслуг – коммуникация может проходить в оперативной памяти компьютера, если услуги находятся на одной хост-машине, однако если микроуслуги находятся на разных компьютерах коммуникация будет происходить по сетевым каналам. Что в итоге – в итоге скорость обмена информации может упасть. Пусть один из запросов будет выполнятся на несколько сот милисекунд секунд дольше. Для одних приложений это будет не заметно и даже не важно, а для других это будет недопустимо. Поэтому данный аспект имеет важно значение при принятии решения об использовании микроуслуг.

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

alt text

Второе приложение, по моему, является хорошим примером микроуслуг.

alt text

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

alt text