С советами по их решению.

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

Массив или HashSet? Глубокий поиск или поиск вдохновения? Нормализованная или денормализованная таблица? Разработчики должны каждый день отвечать на эти и другие вопросы, анализируя бизнес и технические требования, ограничения…

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

Общий пакет или копипаст?

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

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

Повторное использование кода в одном и том же проекте буквально просто и выглядит так:

//Shared code
public static class Utility
{
    public static string Trim(string str)
        => str.Trim();
}
public class Consumer
{
   public void DoSomething()
   {
       var str = Utility.Trim("string ");
       //...
   }
}
public class AnotherConsumer
{
   public void DoSomething()
   {
       var str = Utility.Trim(" string ");
       //...
   }
}

Рефакторинг интерфейса общего кода (переименование метода Trim или добавление к нему другого параметра) и обновление метода во всех потребителях можно выполнить всего несколькими щелчками мыши в среде IDE. Если что-то было сделано не так, компилятор покажет, где проблема.

В архитектуре микрослужб повторное использование кода достигается за счет создания автономных пакетов с общим кодом, которые могут устанавливать и использовать другие микрослужбы. Эти пакеты могут размещаться в отдельных репозиториях и иметь собственные пайплайны CI/CD.

Совместное использование пакетов между микросервисами позволяет разработчикам избежать копирования кода между микросервисами, но приводит к другим проблемам:

  • Ключевые изменения. Критические изменения легко внести, изменив интерфейсы, определенные в общем пакете. Проблема станет заметной только после того, как микросервисы будут обновлены до версии пакета, содержащей критическое изменение.
  • Управление версиями. Критических изменений можно избежать, используя методы управления версиями API. Однако частая модификация интерфейсов, определенных в общем пакете, приведет к беспорядку в коде. Многие API и модели с окончаниями V1, V2…N появятся в общем коде пакета.
  • Тестируемость. Изменение логики, определенной в общих пакетах, может нарушить работу всех потребительских микросервисов. Чтобы обнаружить эти проблемы как можно раньше, разработчикам потребуется внедрить автоматизированные сквозные тесты, охватывающие все микросервисы.
  • Последовательность. Разные микросервисы могут использовать разные версии одного и того же общего пакета (если разработчики забыли обновить версию одного из микросервисов), вызывая несоответствия в поведении микросервисов или баги.
  • Документация. Каждый общий пакет должен иметь документацию, которую необходимо поддерживать по мере изменения кода.

Разработчики могут рассмотреть возможность копирования и вставки кода между микросервисами в тех случаях, когда код имеет крайне нестабильный интерфейс (но извлекает его в общий пакет после этапа стабилизации) или когда это небольшой служебный код (чтобы уменьшить количество общих пакетов до минимум).

Связь между службами или шина сообщений?

В то время как некоторые микросервисы могут быть полностью изолированы, другие микросервисы должны обязательно взаимодействовать друг с другом: оповещать о произошедшем событии, обмениваться данными и т.д.

Двумя распространенными подходами к обмену данными для микрослужб являются связь между службами и обмен сообщениями. Сложные микросервисные приложения обычно реализуют оба подхода: некоторые наборы микросервисов напрямую взаимодействуют с другими сервисами с помощью HTTP или gRPC, а другие используют служебную шину.

Разработчики должны выбирать шаблон связи в каждом конкретном случае. Связь служебной шины часто считается разработчиками выбором по умолчанию из-за ряда преимуществ, которые она предоставляет:

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

Обмен данными между службами делает микрослужбы тесно связанными, приводит к проблемам с версиями API, усложняет микрослужбы из-за необходимости реализации логики отработки отказа, такой как повторные попытки, шаблон прерывателя цепи и т. д.

Однако подход между службами может быть предпочтительнее обмена сообщениями, когда одна микрослужба должна вызывать другую и получать ответ, чтобы продолжить выполнение. Кроме того, когда производительность критична, связь между службами через gRPC или даже HTTP может быть значительно быстрее, чем обмен сообщениями.

Больше микросервисов или меньше?

Разработчики могут принять решение о внедрении нового микросервиса по разным причинам, например:

  • Новый поддомен. Развитие бизнес-доменов привело к появлению новых поддоменов, которые можно реализовать в виде отдельных микросервисов. Каждый из микросервисов может быть назначен разным командам.
  • Масштабирование. На некоторые функции может потребоваться гораздо более высокая нагрузка, чем на остальную часть приложения. В этом случае этот функционал может быть выделен в отдельный микросервис, который можно масштабировать самостоятельно.
  • Отказоустойчивость. Выполнение некоторых функций может занять очень много времени и использовать много ресурсов ЦП и памяти. Этот функционал можно выделить в отдельный микросервис, чтобы он не влиял на другие микросервисы в случае сбоев или высокого потребления ресурсов.

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

Как вы можете видеть на диаграмме выше, добавление еще одного микросервиса приводит к необходимости реализации многих дополнительных вещей.

Как правило, для каждого нового микросервиса разработчикам может потребоваться создать отдельный репозиторий, внедрить конвейеры CI/CD, предоставить новый веб-сервис, хранилища и т. д. Новый микросервис должен будет взаимодействовать с другими микросервисами, что приводит к дополнительным циклам обмена и задержкам. Возможно, потребуется реализовать общий пакет для совместного использования логики между микросервисами. Кроме того, может быть важно поддерживать согласованность данных между микросервисами, которые требуют реализации шаблона Saga.

Разработчики должны тщательно взвесить все за и против внедрения нового микросервиса в систему и убедиться, что новый микросервис принесет больше пользы, чем сложностей.

Монорепо или мультирепо?

Размещение каждого нового микросервиса в отдельном репозитории может показаться более естественным подходом, чем размещение всех микросервисов в одном или нескольких репозиториях.

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

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

Размещение всех микросервисов в одном репозитории или разделение микросервисов на несколько групп по какому-либо признаку и хранение каждой группы в отдельном репозитории может полностью или частично решить вышеуказанные проблемы. Подробнее об этом можно прочитать в другой моей статье:



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

Спасибо за прочтение. Если вам понравилось то, что вы прочитали, ознакомьтесь с этой историей ниже: