Pull to refresh

Это вопрос должен решать архитектор. Или нет?

Reading time 15 min
Views 26K
У меня есть некоторый опыт в реализации систем на базе микросервисной архитектуры и я хотел бы поделится вопросами (и ответами), которые возникают при реализации подобных проектов. К сожалению, я не имею права распространяться о проектах в которых я участвовал, поэтому я выдумал собственный сферический проект в вакууме. В этом проекте нам встретится множество стандартных проблем.

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

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

image

Основная идея проекта


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

Я предлагаю вам другой подход. Смотрите, вы идете ко мне на сайт, регистрируетесь, заполняете анкету. В анкете будет примерно следующее: хочу каждый день с понедельника по пятницу в 8:15 выйти из дома (пункт А) сесть в мерседес и доехать до работы (пункт Б). Потом вечером в 18:00 выйти из офиса (пункт Б) сесть в ауди и доехать до дома (пункт А). Там же еще можете пометить галочкой пункт: хочу иметь возможность уехать с работы раньше, машину готов ждать не более 8 минут.

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

За все удовольствие с вас попросят 150 у.е. в месяц. Звучит неплохо, не правда ли?

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

Наш сервис будет давать нам множество данных из которых мы можем добыть много интересной информации:

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

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

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

В общем и целом речь идет о большом и сложном проекте, который можно назвать модным словом «cognitive solution» для бизнеса с элементами из мира Internet of Things.

Методология разработки и анализ требований


Естественно, сначала необходимо проанализировать все требования к проекту, решить какую методологию мы применим (waterfall, rup, scrum, …). Но в данном случае мы все эти этапы пропустим, т.к. практически все поднятые в этой статье вопросы возникнут в любом случае, независимо от выбранной методологии.

Язык, фреймворк, архитектура


Изначально я Java Developer, и потому имплементация будет на java. Не обессудьте.

Кстати вот сразу вопрос, является ли выбор языка программирования задачей архитектора или он «выше этого»?

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

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

А какой фреймворк возьмем? Если немного погуглить, то станет понятно, что особо вариантов нет, мы будем делать на Spring Framework. Причина проста, в Spring Cloud есть все, что нам потребуется.

Еще у нас будут всякие API Gateway’и, Config Service’ы, Message Broker’ы, Docker, Workflow, Rule Engine и много всяких других заумных слов по мелочи.

Есть два основных подхода к проектированию микросервиса.
  • Domain Driven Design
  • Functional Driven


Domain Driven Design означает определение доменных объектов и имплементирование всех необходимых действий, которые требует ваш заказчик. Например заказчик какой нибудь аптечной системы говорит: мне нужно иметь возможность вносить новое лекарство в систему, удалять старые, а вот редактирование уже внесенного лекарства надо запретить. Вы делаете класс «Medicine» со всеми нужными полями и имплементируете названный функционал. Так у вас появляется MedicineService. Т.е. при таком подходе начальной точкой является доменные объекты.

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

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

Сервисов с одним доменным объектом весьма не много, можно сразу начинать с функционала, но мне так проще.

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

Начнем с транспорта. Например с Car. Хотя просто Car не пойдет. Поясню. Например вдруг человеку будет удобно доехать на машине до вокзала, там пересесть на поезд, доехать до города, а потом последние два километра проехать на велосипеде? Мы ведь хотим с этого милого человека еще и за велосипед денег попросить? А вдруг кто то захочет эту самую последнюю милю на моноколесе проехать? Не обижать же человека, тем более, если у него на это деньги есть? Давайте сдадим ему в аренду моноколесо! Таким образом в будущем нам может потребоваться множество классов, описывающих разные транспортные средства.

Возьмем за стартовую точку некоторый абстрактный класс Vehicle

 public abstract class Vehicle {
….
   protected String model;
   protected int wheelNumber;
   protected Date manufactureYear;
   protected EngineType engineType; 
   protected Producer producer;
}

Как мы выяснили у нас будут разные Vehicle, давайте сделаем парочку:

public class Car extends Vehicle {
   public Car() {
       wheelNumber = 4;
   }
}

и еще один для бедных, но спортивных:

public class Bicycle extends Vehicle {
 
   public Bicycle() {
       wheelNumber = 2;
   }
}

Замечательно. Теперь нам нужен тот, ради кого мы все это делаем: наш клиент, он же источник нашего дохода. Назовем эту сущность Customer’ом

public class Customer {
   private String firstName;
   private String lastName;
   private Date birthDay;
}

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

public class Contract {
   private long customerId;
   private long vehicleId;
}

Итого мы имеем одну иерархию классов с Vehicle на вершине, Customer и Contract. Предлагаю сделать из них VehicleService, ContractService и CustomerService.

Что означает 'микро' в слове 'микросервис'?
Раньше меня мучал вопрос, а что означает «микро» в слове «микросервис»? По идее это означает «маленький». Но что значит маленький?

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

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

Если ваш ответ «да», то это микросервис. А если нет, то и нет. И что важно, задавайте себе этот вопрос регулярно по мере реализации. Если вдруг у вас проскочил ответ «нет», начинайте рефакторить/дробить/переосмысливать этого монстра. Хотя обычно к этому времени все полимеры уже <..censored..>

Рудиментарную имплементацию сервисов вы можете посмотреть тут.

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

Что бы засунуть наши сервисы в докер нам потребуется плагин для мавена (смотрите в pom.xml docker-maven-plugin) и dockerfile.

Все сервисы мы будем запускать через docker-compose, для этого в корне проекта лежит docker-compose.yml.

Обратите так же внимание на файл .env и его содержимое. Больше об этом файле найдете в документации . Без этого файла у меня на машине с Windows 7 не получилось инициализировать MySQL.

Что у меня получилось?


Начнем с плюсов:

  • эта штука работает.

На этом плюсы к сожалению закончились

Минусы:

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

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

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

Добавление нового типа транспорта в систему


Диаграмма классов это не архитектура
Часто слышу такое высказывание: диаграмма классов это не архитектура. Объясняется это примерно так: что и как там в модуле сделано меня не интересует, мне важно что модули делают и как они между собой коммуницируют. Как правило, говорят это люди, которые программистом никогда не работали, но каким то образом сразу же стали архитекторами. Что я могу на это сказать? Может они действительно правы, но мой опыт говорит об обратном. И сейчас мы рассмотрим как раз такой случай.


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

Сейчас VehicleService выглядит вот так.

image

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

image

Мы напишем новый класс Scooter в VehicleService, протестим, скомпилим, задеплоим. А если у нас будут десятки типов транспортных средств? Будем каждый раз писать новый класс, тестить, компилить и т.д.? Есть ли другой способ?

Можно, например, сделать так. Сделаем класс VehicleType.

public class VehicleType {
    private String name;
    private List<VehicleProperty> properties;
    ….
}

Как видите у VehicleType есть VehicleProperty:

public class VehicleProperty<T> {
   private String name;
   private T value;
   private String description;
…..
}

Сделаем еще класс Vehicle:

public class Vehicle {
  private VehicleType vehicleType ;
  private List<VehicleProperty> customProperties;
   …...
}

Теперь, если хотим добавить скутер в систему, то мы создадим сначала VehicleType «Scooter»:

VehicleProperty wheelNumberProperty = new VehicleProperty<Integer>("wheelNumber", 2, "number of wheels");
…….
VehicleType scooterType = new VehicleType("Scooter");
 scooterType.addProperty( wheelNumberProperty);
……..

А если нам нужно создать экземпляр скутера:

Vehicle scooter1 = new Vehicle(scooterType);
….. 

Таким образом мы можем добавлять в систему сколько угодно новых типов не создавая новые классы в VehicleService.

image

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

Скрытый business case
Вообще вопрос изменения чего либо в сервисе требует отдельного внимания. Поясню на примере.

У меня был такой случай. На митинге презентирую клиенту нашу будущую архитектуру. Ответил на вопросы. Вроде все хорошо, всем все нравится, все довольны. И тут главный айтишник клиента задает такой простой вопрос: как быстро вы можете добавить новое поле к доменному объекту «АBC»? Простой ведь вопрос, правда? Я и ответил просто: добавить поле — 2 минуты, написать тесты еще от нескольких минут до пары часов, потом прогон всех тестов (может длится часами), и т.д. В общем назвать какую то конкретную цифру я не смог и думаю что никто не сможет, пока это хотя бы раз не было сделано. Вроде как я правильно ответил, но ощущение, что ответ неверен меня не покидало. И вот однажды я таки понял, как я должен был ответить.

На сегодняшний день мой ответ звучит так: «А как часто это должно происходить?» Если это исключительная ситуация, то в принципе не важно сколько длится добавление поля, лишь бы этот срок был адекватным с точки зрения бизнеса. Если же это происходит часто, то надо бы задать вопрос: а не является ли это Business case? И если да, то этот функционал нужно изначально закладывать в систему и тогда ответ должен быть: 20-30 минут (это вранье, конечно, но звучит хорошо), может дольше, если случай тяжелый.

Также возникает другой вопрос: как так получилось, что этот business case всплыл только сейчас?
И еще более важный вопрос, а нет ли других подобных business case’ов, которые мы упустили?

Следующий момент. Смотрите, если наш VehicleService падает, то мы не можем ни создать новый экземпляр велосипеда в системе (например мы закупили новую партию велосипедов и хотим добавить их в систему), ни арендовать велосипед. Т.е. ни наши клиенты, ни наши сотрудники в офисе ничего сделать не могут. Было бы гораздо лучше, если бы даже в случае проблем в офисе наши клиенты могли бы делать заказы и приносить нам деньги. Как это можно сделать? Похоже надо делить наш VehicleService на два, один для клиентов и один для наших сотрудников.

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

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

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

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

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

  • Car
  • Bicycle
  • Scooter
  • Traktor
  • ……

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

А можно и по другому. При старте каждый сервис должен постучаться к VehicleTypesService сообщить ему о своем существовании. Это решение вроде хорошо выглядит, но не отвечает на вопрос что делать, если нам надо не добавить, а удалить вид транспортного средства. Например, через полгода мы поняли, что моноколесами никто не пользуется, мы хотим удалить его из системы. Как мы это сделаем?

А вот еще интересный вопрос. Как видите у Vehicle есть поле EngineType.

public abstract class Vehicle {
   …...
   protected EngineType engineType;
 

В моей рудиментарной имплементации EngineType имеет enum’ы:

  • Gas
  • Diesel
  • Elektro

А теперь собственно сам вопрос: как мы создадим автомобиль с гибридной силовой установкой?

Сделаем вместо EngineType список EngineType’ов (и тогда у 99% машин вдруг появится список с одним элементом)?

public abstract class Vehicle {
   …...
   protected List<EngineType> engineTypes;
 

А может добавим новый тип в enum, что то вроде Gibrid?

Кто в этом случае принимает решение и, соответственно, несет за него ответственность?

Можно ли сказать, что здесь идет речь об архитектуре или это слишком «мелкий» вопрос?

На примере поля EngineType хочу задать еще один вопрос: а нужно ли нам вообще знать какой двигатель у машины? Ведь нашему клиенту с большой долей вероятности абсолютно все равно, заливаем мы в бак солярку или бензин. А вот, например, возможность захватить с собой велосипед (т.е. большой ли багажник или есть ли специальный крепеж для велосипеда), может оказаться очень даже важным.

На самом деле тип двигателя конечно же важен, но не для клиента, а для нас, компании, которая этот сервис предоставляет. Одна из причин — статистика (например по затратам на топливо или необходимому ремонту). Она будет сильно различаться для каждого типа двигателя. Отсюда возникает еще один вопрос: а не должны ли быть доменные объекты (или их представления) различными для нашего backend’a и frontend’a?

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

Сложные запросы


Предположим, я хочу посмотреть всех клиентов с их договорами, которые живут на улице Апельсиновой в славном городе Берлине. Т.е. я хочу увидеть что то вроде такой таблички:
Фамилия, Имя Номер договора Дата подписания договора Машина
Пупкин, Вася 12345 01.01.2017 Audi Q4
...... ...... ...... ......

Как вы видите в таблице содержатся данные из трех микросервисов: CustomerService, VehicleService и ContractService. Как мы будем их собирать вместе? В случае монолита вопрос решается одним запросом в базу, а что делать когда у нас три базы?

image

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

Спасибо, Кэп!
А теперь минуточку внимания. Когда надо было задаваться этими вопросами? Ответ: конечно же перед тем как писать код. И отвечать на эти вопросы должен в том числе и архитектор.

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

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

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

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

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

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

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

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

На последок еще две мысли.

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

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

Однажды я прошляпил этот момент и только на Sprint Review абсолютно случайно узнал, что программист сделал совсем не так, как было ему сказано. На вопрос: написано же черным по английски, посылай «event», почему ты шлешь http request? был получен ответ: ну я подумал, что так будет лучше. Естественно все проконтролировать невозможно, надо доверять своей команде. Но, как говорят немцы, доверие хорошо, а контроль лучше.

История изменений


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

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

Как это можно сделать мы обсудим в следующей статье.

Проблемы с инфраструктурой


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

  • единая точка входа, он же API Gateway
  • поиск сервисов, он же Service Discovery
  • одна точка конфигурации, он же Config Service
  • центральная точка сбора и анализа логов
  • мониторинг
  • ……

Эти и другие вопросы в следующей части.
Tags:
Hubs:
+31
Comments 67
Comments Comments 67

Articles