company_banner

Макропроблема микросервисов

Автор оригинала: Ryland Goldstein
  • Перевод

Всего за 20 лет разработка ПО перешла от архитектурных монолитов с единой базой данных и централизованным состоянием к микросервисам, где всё распределено по многочисленным контейнерам, серверам, ЦОДам и даже континентам. Распределённость упрощает масштабирование, но привносит и совершенно новые проблемы, многие из которых раньше решались с помощью монолитов.

Давайте с помощью краткого экскурса по истории сетевых приложений разберёмся, как мы пришли к сегодняшней ситуации. А затем поговорим о модели исполнения с сохранением состояния (stateful execution model), используемую в Temporal, и о том, как она решает проблемы сервис-ориентированных архитектур (service-oriented architectures, SOA). Я могу быть предвзятым, потому что руковожу продуктовым отделом в Temporal, но считаю, что за этим подходом будущее.

Короткий исторический урок


Двадцать лет назад разработчики почти всегда создавали монолитные приложения. Это простая и согласованная модель, аналогичная тому, как вы программируете в локальном окружении. По своей природе монолиты зависят от единой базы данных, то есть все состояния централизованы. В рамках одной транзакции монолит может менять любое своё состояние, то есть это даёт двоичный результат: сработало или нет. Для несогласованности нет места. То есть монолит прекрасен тем, что из-за сбойной транзакции не возникнет несогласованного состояния. И это означает, что разработчикам не нужно писать код, всё время гадая о состоянии разных элементов.

Долгое время монолиты имели смысл. Ещё не было множества подключённых пользователей, поэтому требования к масштабированию ПО были минимальны. Даже крупнейшие программные гиганты оперировали ничтожными по современным меркам системами. Лишь горстка компаний вроде Amazon и Google использовала «масштабные» решения, но то были исключения из правила.

Люди как ПО




В последние 20 лет требования к ПО постоянно растут. Сегодня приложения должны с первого же дня работать на глобальном рынке. Компании вроде Twitter и Facebook превратили онлайн-режим 24/7 в необходимое условие. Приложения больше не обеспечивают работу чего-либо, они сами превратились в пользовательский опыт. Сегодня у каждой компании должны быть программные продукты. «Надёжность» и «доступность» уже не свойства, а требования.

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

Ответом стали микросервисы (ну, сервис-ориентированные архитектуры). Изначально они представлялись прекрасным решением, потому что позволяли дробить приложения на относительно самодостаточные модули, которые можно независимо масштабировать. И поскольку каждый микросервис поддерживал своё собственное состояние, приложения больше не ограничивались вместимостью одной машины! Наконец-то разработчики могли создавать программы, удовлетворяющие требованиям по масштабированию в условиях растущего количества подключений. Также микросервисы дали командам и компаниям гибкость в работе за счёт прозрачности в ответственности и разделении архитектур.


Бесплатного сыра не бывает


Хотя микросервисы решили проблемы масштабирования и доступности, мешавшие росту ПО, не всё было безоблачно. Разработчики начали понимать, что у микросервисов есть серьёзные недостатки.

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

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

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

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

Однако на спрос возникает предложение. Чем шире распространялись микросервисы, тем больше росла мотивация разработчиков к решению инфраструктурных проблем. Медленно, но уверенно начали появляться инструменты, и пробел заполнили технологии вроде Docker, Kubernetes и AWS Lambda. Они очень сильно облегчили эксплуатацию микросервисной архитектуры. Вместо того, чтобы писать свой код для оркестрирования контейнерами и ресурсами, разработчики могут опираться на уже готовые инструменты. В 2020-м мы наконец-то достигли рубежа, когда доступность нашей инфраструктуры больше не мешает надёжности наших приложений. Прекрасно!


Конечно, мы ещё не живём в утопии идеально стабильного ПО. Инфраструктура больше не является источником ненадёжности приложений, это место занял код приложений.

Другая проблема микросервисов


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


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

public void transferWithoutTemporal(
  String fromId, 
  String toId, 
  String referenceId, 
  double amount,
) {
  boolean withdrawDonePreviously = myDB.getWithdrawState(referenceId);
  if (!withdrawDonePreviously) {
      account.withdraw(fromAccountId, referenceId, amount);      
      myDB.setWithdrawn(referenceId);
  }
  boolean depositDonePreviously = myDB.getDepositState(referenceId);
  if (!depositDonePreviously) {
      account.deposit(toAccountId, referenceId, amount);                
      myDB.setDeposited(referenceId);
  }
}

Увы, невозможно написать код без ошибок. И чем сложнее код, тем вероятнее появление багов. Как вы могли ожидать, код работающий с «промежуточным», не только сложный, но и запутанный. Хоть какая-то надёжность лучше её отсутствия, так что разработчикам пришлось писать такой изначально забагованный код, чтобы поддерживать пользовательский опыт. Это стоит нам времени и сил, а работодателям — кучу денег. Хотя микросервисы прекрасно масштабируются, за это приходится платить удовольствием и продуктивностью разработчиков, а также надёжностью приложений.

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


Temporal


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

До сегодняшнего дня не было решения, позволяющего использовать микросервисы без расхлёбывания вышеописанных проблем. Вы можете тестировать и эмулировать сбойные состояния, писать код с учётом падений, но эти проблемы всё-равно возникают. Мы считаем, что Temporal их решает. Это open-source (MIT, без дураков) stateful-среда для оркестрации микросервисов.

У Temporal два основных компонента: stateful-бэкенд, работающий на выбранной вами БД, и клиентский фреймворк на одном из поддерживаемых языков. Приложения создаются с помощью клиентского фреймворка и обычного старого кода, который автоматически сохраняет изменения состояния в бэкенде по мере исполнения. Вы можете использовать те же зависимости, библиотеки и цепочки сборки, что и при создании любого другого приложения. Честно говоря, бэкенд сильно распределён, так что это не как с J2EE 2.0. По сути, именно распределённость бэкенда обеспечивает почти бесконечное горизонтальное масштабирование. Temporal обеспечивает для уровня приложений согласованность, простоту и надёжность, как это сделали для инфраструктуры Docker, Kubernetes и бессерверная архитектура.

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

Приведу аналогию. Как разработчик, наверняка сегодня вы полагаетесь на версионирование SVN (это OG Git) для отслеживания сделанных вами в коде изменений. SVN просто сохраняет новые файлы, а затем ссылается на существующие файлы, чтобы избежать дублирования. Temporal — что-то вроде SVN (грубая аналогия) для stateful-истории работающих приложений. Когда ваш код меняет состояние приложения, Temporal автоматически безошибочно сохраняет это изменение (не результат). То есть Temporal не только восстанавливает упавшее приложение, он ещё и откатывает его назад, форкает и делает многое другое. Так что разработчикам больше не нужно создавать приложения с оглядкой на то, что сервер может упасть.

Это похоже на переход от ручного сохранения документов (Ctrl+S) после каждого введённого символа к автоматическому облачному сохранению Google Docs. Не в том смысле, что вы больше ничего не сохраняете вручную, просто больше нет какой-то одной машины, связанной с этим документом. Сохранение состояния означает, что разработчики могут писать намного меньше скучного шаблонного кода, который приходилось писать из-за микросервисов. Кроме того, больше не нужна специальная инфраструктура — отдельные очереди, кеши и базы данных. Это упрощает эксплуатацию и добавление новых фич. А также сильно облегчает ввод новичков в курс дела, потому что им не нужно разбираться в запутанном и специфичном коде управления состояниями.

Сохранение состояния реализуется и в виде «устойчивых таймеров». Это отказоустойчивый механизм, которым можно пользоваться с помощью команды Workflow.sleep. Она работает точно так же, как нативная языковая команда sleep. Однако с Workflow.sleep можно безопасно усыплять на любой промежуток времени. Многие пользователи Temporal используют усыпление на недели, и даже годы. Это достигается с помощью хранения длительных таймеров в хранилище Temporal и отслеживания кода, который нужно разбудить. Повторюсь, даже если сервер упадёт (или вы его просто выключили), код перейдёт на доступную машину по срабатыванию таймера. Процессы сна не потребляют ресурсов, вы можете иметь их миллионы при ничтожных накладных расходах. Возможно, звучит слишком абстрактно, так что вот пример рабочего Temporal-кода:

public class SubscriptionWorkflowImpl implements SubscriptionWorkflow {
  private final SubscriptionActivities activities =
      Workflow.newActivityStub(SubscriptionActivities.class);
  public void execute(String customerId) {
    activities.onboardToFreeTrial(customerId);
    try {
      Workflow.sleep(Duration.ofDays(180));
      activities.upgradeFromTrialToPaid(customerId);
      while (true) {
        Workflow.sleep(Duration.ofDays(30));
        activities.chargeMonthlyFee(customerId);
      }
    } catch (CancellationException e) {
      activities.processSubscriptionCancellation(customerId);
    }
  }
}

Помимо сохранения состояния Temporal предлагает набор механизмов для создания надёжных приложений. Функции активностей вызываются из рабочих процессов, но код, работающий внутри активности, не является stateful. Хотя они и не сохраняют своё состояние, активности содержат автоматические повторы, таймауты и heartbeat’ы. Активности очень полезны для инкапсулирования кода, который может сбоить. Допустим, ваше приложение использует банковский API, который часто недоступен. В случае с традиционным ПО вам понадобится обернуть весь код, вызывающий этот API, выражениями try/catch, логикой повторов и таймаутами. Но если вызывать банковский API из активности, то все эти функции предоставляются из коробки: если вызов сбоит, активность будет автоматически повторена. Всё это прекрасно, но иногда вы сами являетесь владельцем ненадёжного сервиса и хотите защитить его от DDoS. Поэтому вызовы активностей также поддерживают таймауты, подкреплённые длительными таймерами. То есть паузы между повторами активностей могут достигать часов, дней или недель. Это особенно удобно для кода, который должен отработать успешно, но вы не уверены, насколько быстро это должно произойти.

В этом видео за две минуты объясняется модель программирования в Temporal:


Другой сильной стороной Temporal является наблюдаемость работающего приложения. API наблюдения предоставляет SQL-подобный интерфейс для запроса метаданных из любого рабочего процесса (исполняемого или нет). Также можно определять и обновлять свои значения метаданных прямо внутри процесса. API наблюдения очень удобен для Temporal-операторов и разработчиков, особенно при отладке в ходе разработки. Наблюдение поддерживает даже пакетные действия с результатами запросов. Например, можно отправить kill-сигнал во все рабочие процессы, соответствующие запросу со временем создания > вчера. Temporal поддерживает функцию синхронного извлечения, позволяющую вытаскивать значения локальных переменных из работающих экземпляров. Это словно отладчик из вашего IDE поработал с production-приложениями. Например, так можно получить значение greeting в работающем экземпляре:

public static class GreetingWorkflowImpl implements GreetingWorkflow {

    private String greeting;

    @Override
    public void createGreeting(String name) {
      greeting = "Hello " + name + "!";
      Workflow.sleep(Duration.ofSeconds(2));
      greeting = "Bye " + name + "!";
    }

    @Override
    public String queryGreeting() {
      return greeting;
    }
  }

Заключение


Микросервисы — вещь замечательная, это покупается ценой продуктивности и надёжности, которую платят разработчики и бизнес. Temporal создан для решения этой проблемы за счёт предоставления окружения, которое платит микросервисам за разработчиков. Предоставляемые из коробки сохранение состояния, автоматические повторы при сбоях и наблюдение — лишь часть возможностей Temporal, которые делают разумной разработку микросервисов.
Mail.ru Group
Строим Интернет

Похожие публикации

Комментарии 17

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

      Есть поддежка Golang и скоро появится PHP.

      +5
      >Горизонтально: создавая копии монолита, каждая из которых обслуживает определённую группу пользователей или запросов. Такое масштабирование приводит к недоиспользованию ресурсов, а при достаточно больших размерах вообще перестаёт работать.

      А почему ведет «недоиспользованию ресурсов» и «перестает работать»?

      Вот есть, условно, очень большое приложение. Что мешает поднять 100500 его инстансов? Да, вероятно придется разным его инстансам завести разные типы запросов — чтобы разные подсистемы в нем включались, и разумнее использовалась бы память. Если используется RDBMS — придется продумать как в него не упереться (шардинг, разбить на разные БД, read-реплики, кеши), но это и с микросервисами оно так.

      Какие будут там проблемы, что этот подход перестанет работать?
        +6
        Когда хайп прошел, а он прошел, примерно так и пишут множество софта.
        Монолит подготовили для горизонтального маштабирования и научили расти линейно от нагрузки. С такой архитектурой живется отлично.
        И самое главное никаких распреденных транзакций. Все лежит вот в этой БД и меняется атомарно под обычной локальной транзакцией.
        Все падения обрабатываются обычным http балансером. Один ретрай на балансере дает и возможность упасть и даст не более х2 нагрузки в самом невероятно плохом случае.

        Просто и надежно.
        +10

        Против микросервисов.


        Микросервисы — это сплошные недостатки. Усложняется архитектура, увеличивается суммарный объем кода, снижается производительность, усложняется логгирование (нужно агреггировать логи), отладка. Сложно развернуть все это дело — нужен мощный компьютер с гигабайтами памяти, нужен докер, который требуется запускать от рута, который рискует разломать систему, качает гигабайты образов, глючит и тормозит на Маке и Windows. Нужно запускать кучу скриптов и демонов.


        В то время как в Монолите мы вызываем функции напрямую, и переходим между ними одним кликом мышки в IDE, в микросервисах передаются недокументированные куски JSON через недокументированное API. Ищи, разбирайся, что тут пошло не так.


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


        Отдельная проблема — общий код. Что, если сервису A и B нужно использовать общий код? Приходится либо копипастить (и вносить изменения в несколько копий кода), либо выносить в библиотеки и мучаться с версионированием и оркестрацией кучи микросервисов и бибилиотек. В то время как в Монолите этой проблемы просто нет изначально.


        Микросервисы — это усложнение на ровном месте. Не понимаю, как их вообще можно хвалить и одобрять.


        Монолит может все то же, что и микросервисы, только лучше и без Докера. Хочется масштабирования — запускаем несколько копий Монолита на нескольких серверах. Нужна очередь? Делаем очередь и запускаем две копии Монолита — писатель и читатель. Уперлись в производительность сервера БД? — переделываем Монолит на работу с несколькими разными серверами БД. Получается то же масштабирование как в микросервисах, но без боли, разных языков и без докеров.


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


        Некоторые жалуются на запутанный код в Монолите. Это не аргумент. Если разработчики не способны написать чистый код в Монолите, они и микросервисы такие же нечитаемые напишут. Некоторые думают, что микросервисы будут "маленькие" и "простые". Это не так. Если подумать логически, то суммарный объем кода в микросервисах = объем кода в Монолите + накладные расходы на передачу данных. То есть в сумме микросервисы по объему будут больше Монолита.

          +1
          Зато микросервисная архитектура идеально ложится на закон Мёрфи Конвея.
            0

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

              +2
              > где надо и (очень часто) где не надо.
              скорее даже так — очень редко где надо и почти всегда где не надо

              +3

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

                +4
                Шансы, что проект вырастет до размеров, когда будут актуальны микросервисы, откровенно говоря не велики. А до этого момента микросервисы только мешают.
                  +1
                  Вы правы, но средних и мелких проектов намного больше чем больших. Для большинства проектов никогда не понадобиться горизонтальное масштабирование. Проблема микросервисов не в том что они плохие, а в том что их применяют там где они не нужны, не только не приносят, пользу но и создают проблемы.

                  Хотя я вижу, что это происходит с каждой модной технологией.

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

                  +4
                  в микросервисах передаются недокументированные куски JSON через недокументированное API

                  так документируйте, OpenAPI и прочее для этого и придумывали. как будто в монолите нельзя недокументированные интерфейсы написать :)

                  Хуже того, микросервисы позволяют писать код на разных языках и фреймворках

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

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

                  так а что, в монолите разбираться не надо, если не знаком с кодовой базой? вообще странный аргумент, потому что если ты участвовал в разработке проекта, то много чего «изучать» не надо будет, а если не участвовал — так и так разбираться (и чем больше проект — тем дольше разбираться, независимо от его структуры).

                  Простая задача, в монолите решаемая за несколько часов, займет несколько дней, а то и недель

                  какие-то цифры с потолка, непонятно откуда взятые

                  Отдельная проблема — общий код. Что, если сервису A и B нужно использовать общий код?

                  монорепозиторий

                  Таким образом, с помощью Монолита можно добиться той же производительности и масштабируемости, только без головной боли и присущих микросервисам недостатков

                  да вот только головная боль будет. большой проект сложно поддерживать, особенно при разрастании команды. большой проект сложнее контролировать на пример дедлоков и stateful-вещей, мешающих масштабированию.
                  помимо уменьшения связанности кода, у микросервисов есть еще одно, может быть даже более важное, преимущество — независимые деплои. какой бы чистый код разработчик не писал, с разрастанием кодовой базы сложнее следить за зависимостями, в конечном счете любой код может зависеть от любого (разработчики тоже люди и тоже могут писать неидеальный код, да-да), поэтому и требования к тестированию релизов соответствующие. это уже не говоря о том что код внутри монолита могут писать разные команды с разными потребностями и разным релизным циклом. микросервисы уменьшают эту проблему и, как следствие, time to market

                  Хочется масштабирования — запускаем несколько копий Монолита на нескольких серверах

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

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

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

                  самое смешное, что микросервисы — не серебряная пуля, это правда. писать их без соответствующей инфраструктуры, CI/CD и прочего ведет к боли и проблемам. на маленьких проектах в них действительно может не быть смысла (особенно если приложение состоит из CRUD'ов), но они были придуманы не глупыми людьми и не просто так
                    0

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

                    0

                    Что произойдёт, если во время выполнения Workflow.sleep(Duration.ofDays(30)); изменить последующий шаг и передеплоить? А если добавить внешний блок? Или вовсе убрать тот шаг, на котором в данный момент происходит ожидание?

                      0

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

                      0
                      Кто-нибудь может объяснить чем микросервисы отличаются от SOA (сервис ориентированная архитектура)?

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

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

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

                      Самое читаемое