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

0. Преамбула

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

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

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

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

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

Что отличает настоящую архитектуру микросервисов от экстравагантного франкенштейновского монолита, использующего сетевой трафик вместо внутренних API?

1. Универсальные языки и протоколы связи

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

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

Вы можете подумать, что это нормально, но выслушайте меня. Одно дело придерживаться модели, например, HTTP/REST, другое — принять структуру уровня приложения.

Мое эмпирическое правило:

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

Базовыми компонентами являются, например, HTTP-клиенты и парсеры. Обобщенными компонентами являются, например, клиенты протокола AMQP.

Кроме того, важна часть «разумно». Например, вы можете разумно использовать GraphQL API без библиотеки GraphQL, и, хотя это ужасно, при необходимости этого можно легко добиться.
Но все меняется, когда вы имеете дело с библиотеками RPC, Например. Да, для большинства библиотек можно использовать свой HTTP-клиент, но там нет ничего приемлемого, кроме контента для крутого эксперимента.

Почему это так важно?

  1. Не упускайте прекрасные возможности. Что делать, если определенная платформа/язык, невероятно подходящий для микросервиса, не поддерживает эту платформу?
  2. Не все реализации одинаковы по качеству или зрелости. Вы можете найти реализацию Java исключительно хорошей, а реализацию Rust — ерундой. Но оба они очень хорошо поддерживают HTTP и JSON, не так ли?
  3. Фреймворки развиваются. Что происходит в большой системе, когда необходимо внедрить новую основную версию коммуникационной структуры, потому что она нужна определенному подразделению? Будет ли он совместим? А если нет? Встречайте ужасные волны обновлений.

Исключения
В каждом правиле есть исключения, и это правило неизбежно применяется к микросервисам, которые вы разрабатываете. Базы данных, очереди сообщений… они вам неподконтрольны, и хотя некоторые из них могут соответствовать некоторым стандартам, другие — нет.
Некоторые инструменты и платформы являются шедеврами в своем жанре, и было бы глупо от них отказываться. потому что они общаются по-своему.
Одним из примеров — среди многих — является Apache Kafka.
Совершенно нормально использовать библиотеки Kafka, потому что это требование для использования Kafka в первую очередь. И вы хотите использовать Кафку, поверьте мне.

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

2. Общий код должен оставаться товаром, а не необходимостью

Я был очень виноват в этом, поэтому я не могу никого винить.
Допустим, вам повезло, и все ваши микросервисы используют одну и ту же платформу/язык.

Когда им нужно общаться, вы по праву следовали моему совету из главы 1, и вы делаете это весьма агностически.
Но насколько это агностически?
Видите ли, протоколы — не единственная принадлежность вы можете иметь. Есть и другие скрытые требования, которые вы можете внедрить, даже не заметив.

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

Затем тарабарщина увеличивается и увеличивается, и очень скоро ни один человек не может понять ни черта о том, о чем говорят API, но вам все равно, потому что у вас есть общие библиотеки.

В качестве побочного эффекта все общие библиотеки будут поставляться со своим списком зависимостей. И по мере роста общих частей будут расти и зависимости. В конечном итоге каждому новому микросервису потребуется 200 МБ зависимостей только для того, чтобы существовать.

Затем происходит одно или все из следующих событий:

  1. Команде крайне необходимо обновить одну библиотеку, но это также зависит от общих библиотек. Обновление возможно, но нет никакой гарантии. Возьмите попкорн.
  2. Команда, отвечающая за набор распределенных bean-компонентов, вносит изменения, но не распределяет их должным образом, что может привести к необъяснимому поведению. Если повезет, все сломается. Если нет, проблема будет прерывистой. Больше попкорна, пожалуйста.
  3. Архитектор решает, что Node.js — правильный инструмент для нового микросервиса, а остальные — на Java. В то время как связь происходит в простом HTTP/REST, обмениваемые данные настолько сложны, что для их десериализации требуются специализированные bean-компоненты. Поэтому вам нужно будет реплицировать bean-компоненты и их поведение в Node.js, вызывая все побочные эффекты репликации кода.

Следовательно:

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

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

3. Базы данных также страдают от совместного использования

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

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

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

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

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

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

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

Не без цены.

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

А как насчет жирных транзакций?
Хорошая новость: это можно сделать. Плохая новость: будет немного больно. Наиболее популярный шаблон для решения этой проблемы называется Saga.

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

Я не буду вдаваться в подробности реализации, здесь вам поможет Google. Мой главный совет, когда речь идет о Saga:

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

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

4. Выводы

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

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