Особливості event-driven architecture на прикладах з практики

Всім привіт. Я Сергій Моренець, розробник, викладач, спікер та технічний письменник. Хочу поділитися своїм досвідом роботи з такою цікавою темою як event-driven architecture та розкрити ті теми, які з нею пов’язані. Спочатку для комунікації між сервісами і системами використовувалися синхронні механізми request/response, які і зараз досить поширені. Проте з появою розподілених систем та мікросервісної архітектури, у них почала застосовуватися event-driven architecture. Ми розглядаємо цей матеріал на деяких тренінгах. Постійно зіштовхуючись із цією темою, у мене набралося достатньо матеріалу для окремої статті. Тому я наведу як переваги та особливості цієї архітектури, так і практичні приклади її використання. Оскільки це дуже велика тема і всю її неможливо розглянути в одній статті (тим більше, з практичними прикладами), то мені будете цікаво в коментарях почути ваші історії її використання (як вдалі, так і тi, що не вдалися).

Комунікація між мікросервісами

Для зручності розберемо такий практичний приклад. Уявімо, що ми розробляємо мікросервісний проєкт для покупки квитків. Ми так розділили доменну модель проєкту, що намітили у тому числі 4 мікросервіси:

1) Квитковий — відповідає за квитки та замовлення (бронювання).

2) Сервіс поїздок — керує маршрутами та поїздками.

3) Сервіс нотифікацій — надсилає нотифікації (email, СМС, месенджери) користувачеві.

4) Платіжний — здійснює інтеграцію з платіжними системами та оплату.

У нашому UI покупка квитка організована таким чином (спрощено):

Кожен крок — це окрема сторінка (екран) у клієнтському додатку. На сторінці оплати користувач вибирає платіжну систему та її атрибути, підтверджує оплату або зберігає замовлення (бронює місце) з можливістю оплатити пізніше.

І тут постає цікаве питання. Оплата замовлення, як будь-яка фінансова операція може тривати тривалий час. Вона може залучати інші фінансові (банківські) сайти та системи, включаючи авторизацію в них і т. д. Як ми дізнаємось у нашій системі, що оплата пройшла успішно?

Зрозуміло, коли ми надсилаємо запит до платіжної системи, ми передаємо callback з нашого API, і це буде API платіжного сервісу. Але як інші послуги дізнаються про це? Квитковому сервісу, наприклад, потрібно буде оновити стан замовлення, сервісу нотифікацій повідомити клієнта і відправити квиток разом з нотифікацією. Сервіс поїздок також буде залучений у цей ланцюжок, тому що йому потрібно буде помітити одне місце як продане (зайняте).

Дуже важливий нюанс — вся ця операція, що залучає кілька мікросервісів, повинна бути атомарною і відповідати ACID принципам. Уявимо, наприклад, що якийсь користувач купив квиток на автобусний рейс. Ми після цього маємо зменшити кількість вільних місць на рейсі на одну одиницю в сервісі поїздок. І при цьому з’ясовується, що вільні місця закінчилися. Можливо, відбулася колізія при одночасному придбанні квитка кількома пасажирами або програмна чи конфігураційна помилка. Нам потрібно відразу повідомити про це покупця, повернути гроші за квитки або запропонувати поміняти квиток на інший.

Таким чином, операція купівлі квитка, по суті, стає транзакцією, але не звичайною локальною транзакцією, керованої СУБД, як ми звикли, а розподіленою (distributed) транзакцією. Такій транзакції притаманні всі стандартні ACID атрибути, але вона виконується на різних JVM та різних серверах.

Повернемося до комунікації між мікросервісами. Найбільш простим способом комунікації є REST API, але чи підходить він для цього випадку? Нам доведеться з платіжного сервісу надіслати три запити на залучені мікросервіси (сервіс поїздок, квитковий та сервіс нотифікацій). А це в якомусь сенсі порушує автономність та ізольованість наших мікросервісів, оскільки вони мають добре знати про функціональність та API один одного. Є ще один мінус у використанні REST API для такого завдання. Уявимо, що платіжний сервіс не зміг відправити запит на сервіс поїздок (навіть з використанням retry logic), оскільки останній був недоступний. Ми не можемо точно знати, коли відновиться доступ до сервера (або мережевих ресурсів). Як бути у такому разі? Можна скасувати транзакцію, що буде песимістичним результатом. Але можна зберегти цей запит у певній черзі, щоб виконати його пізніше за розкладом. При цьому ця черга повинна бути персистентною (durable) і бажано розподіленою, щоб ми не втратили її вміст в результаті збою/перезавантаження.

У REST API підходу є й інші мінуси для нотифікації сервісів про успішний платіж:

1) Сервіс-відправник повинен знати всіх одержувачів нотифікації та їх API. Більше того, по суті, він має надіслати не нотифікацію, а команду-запит, наприклад, змінити стан замовлення або зменшити кількість вільних місць на рейсі. Виходить, що функціональність, пов’язана з конкретним мікросервісом, розмивається між рештою, порушуючи принцип SRP. Далі, якщо нам потрібно поміняти одержувачів нотифікації, нам довелося б міняти і сервіс-відправник, порушуючи його автономність і ізольованість.

2) Використовуючи REST API, складно реалізувати гарантовану доставку нотифікації в разі недоступності одного з одержувачів.

3) Оскільки загальна операція має бути атомарною, нам необхідний API як для відправки нотифікації (команди), так і для відправки rollback-нотифікації, що загалом ускладнює кожен мікросервіс і робить їх ще більш пов’язаними.

Як же вийти з цього становища? Нам потрібен підхід, який би дозволив:

  • Зробити наші мікросервіси ізольованими та прибрати тісний зв’язок між ними.
  • Надати засіб для гарантованої та максимально швидкої відправки/отримання/зберігання нотифікацій про зміну стану моделі даних — подій (або повідомлень).

Messaging systems and patterns

По суті, нам потрібна реалізація структури даних «черга» для наших нотифікацій. Але не та черга (Queue), яка існує в JDK, а більш досконала, яка зберігає дані поза JVM на дисковій підсистемі, підтримує high-availability (failover), динамічне підключення та масштабування роботи підписувачів. Такі технології (або платформи) існують давно і в залежності від своєї функціональності є:

  • Messaging queuing service (StormMQ, Amazon SQS).
  • Message-oriented middleware или message broker (RabbitMQ).
  • Stream processing или event streaming (Apache Kafka).

Однак ці системи об’єднує одне — вони призначені, щоб пересилати повідомлення (messages) між відправником і одержувачем (чи споживачем). Тому ми не помилимося, якщо використовуватимемо для них такий загальний термін як messaging systems.

Такі системи мають досить складну архітектуру, використовують різні математичні алгоритми для обробки та відправлення даних, тому їх пишуть не з нуля, а використовуючи готові напрацювання або ті ж патерни. Ці патерни відносяться до групи Enterprise Integration Patterns, тобто патернів, які допомагають будувати (інтегрувати) розподілені системи. Усього таких патернів 65, і вони добре описані в однойменній книзі. На жаль, ми не зможемо торкнутися всіх патернів, та й це не ціль цієї статті. Але найцікавіші ми розберемо, оскільки вони допоможуть зрозуміти особливості використання messaging systems:

  • Перший та найпростіший патерн — Message (повідомлення). Ми використовуємо саме це слово, а не нотифікацію, тому що в повідомленні можна передати все, що завгодно, а не тільки повідомлення про якусь подію. Повідомлення — це абстрактна структура даних (value object), яку можна передати через Message Channel.
  • Message Channel — спосіб організації доставки повідомлень, коли відправник посилає повідомлення в певний канал повідомлень, а одержувач підключається до цього каналу для отримання повідомлень. Цей термін має альтернативні назви, наприклад, топік в Apache Kafka.
  • При цьому одержувач та відправник знають ідентифікатор (назву) каналу і виходячи з цього, знають які дані та якого формату там мають бути. У самих повідомленнях немає потреби додавати метадані про структуру. Це передбачає патерн Datatype channel.
  • Message — це упаковка для наших повідомлень. Залежно від того, що ми хочемо надіслати — інформацію про подію, команду або дані, ми можемо використовувати патерни Event Message, Command Message або Document Message.
  • Пересилання повідомлень по одному вкрай неефективне, тому їх зазвичай поєднують у групи (batches) як при пересиланні, так і при отриманні. Для цього використовують пат-терн Message Sequence.
  • Повідомлення — це набір байт, у яких ми серіалізували наш вихідний Java об’єкт. Але формат даних при серіалізації може бути різним, що ускладнить інтеграцію наших додатків (сервісів). Тому патерн Canonical Data Model пропонує використовувати один загальний формат даних для всіх учасників обміну повідомленнями, який, по суті, інкапсулює той формат, який використовує конкретний сервіс.
  • Для того, щоб перетворювати дані з Java об’єктів у бінарні повідомлення та навпаки, потрібен патерн Messaging Mapper
  • Паттерн Message Bus (шина повідомлень) пропонує обмін повідомленнями в розподіленій системі, коли учасники цієї схеми нічого не знають один про одного, і будь-який сервіс може як приєднатися, так і відключитися від цієї шини.
  • Якщо у вашого повідомлення повинен бути тільки один одержувач, то рекомендується використовувати патерн Point-To-Point. При цьому навіть якщо у вас до каналу повідомлень підключено кілька одержувачів, то тільки один з них отримає доступ до цього повідомлення.
  • Якщо ж ви хочете, щоб ваше повідомлення могло бути доставлене всім споживачам (consumers), то рекомендується використовувати патерн Publish-Subscribe.
  • Якщо при обробці повідомлення виникла помилка, пов’язана з некоректним вмістом цього повідомлення, то патерн Invalid Message Channel пропонує відправити це повідомлення в окремий канал.
  • Дуже важливо, щоб при відправці повідомлення воно не загубилося у великій розподіленій системі. Паттерн Guaranteed Delivery гарантує, що повідомлення буде збережено в системі і не видалено, поки його не опрацюють усі споживачі. Це в свою чергу вимагає від messaging system наявності надійного сховища даних для повідомлень.
  • Що якщо повідомлення все ж таки не вдалося відправити? Тоді згідно з патерном Dead letter channel його потрібно відправити в спеціальний канал для подальшого аналізу.
  • Для надсилання та отримання повідомлень потрібен API клієнт (схожий на HTTP клієнт у REST API), який дозволить інтегруватися з messaging system. Такий клієнт є реалізацією патерну Channel Adapter.
  • Якщо API клієнт є низькорівневим способом доступу до messaging system, то патерн Messaging Gateway пропонує інкапсулювати такий API, щоб спростити додатком роботу з повідомленнями.
  • Якщо одержувач готовий до прийому повідомлень, він може використовувати найпростіший — синхронний блокуючий підхід, коли він підключається до messaging channel і чекає на їх надходження. За це відповідає патерн Polling Consumer.
  • У той самий час найефективніше асинхронне отримання повідомлень пов’язані з паттер-ном Event-Driven Consumer. Одержувач підписується на прийом повідомлень, вказуючи метод-callback, який і буде викликаний у цьому випадку.
  • Послідовне отримання повідомлень одним споживачем — найпростіший, але з найшвидший варіант. Для прискорення роботи можна обробляти повідомлення паралельно, що вимагає наявності кількох одержувачів в одного каналу та підтримки такої функціональності на рівні messaging channel. Такий патерн називається Competing Consumers.
  • Необхідно зберігати стан кожного споживача — які повідомлення він уже прочитав, а які ще немає. Це дозволить уникнути пропущених повідомлень, якщо споживач, наприклад, перевантажується чи недоступний. Для цього існує патерн Durable subscriber. При цьому кожен споживач повинен мати унікальний ідентифікатор, щоб його можна було відрізняти від інших споживачів.
  • Що, якщо при обробці повідомлення виникне помилка, і одержувачеві потрібно прочитати та обробити повідомлення ще раз? Якщо обробка зводиться до додавання даних, необхідно передбачити, щоб повторна обробка не призвела до дублювання даних. Це передбачає патерн Idempotent Receiver.

Загалом структура взаємодії одержувача та підписувачів виглядає так:

Особливості event-driven architecture

Тепер поговоримо про особливості самої архітектури, які важливі для майбутньої розробки та відмінності від REST API.

Перша особливість така. Якщо в REST API ми надсилаємо запит і знаємо за відповіддю або його відсутністю, наскільки успішно він виконаний, то в новому варіанті дещо складніше. Якщо ми відправили в message broker подію «Платіж оплачений» і отримали повідомлення, що подія доставлена, то не означає 100% гарантії, що вона не буде втрачена. Наприклад, подія була доставлена ​​в primary broker, але ще не було синхронізовано з репліками. Якщо primary broker впаде (або стане недоступним) і одну з реплік виберуть лідером, повідомлення вже не буде доставлене споживачам. Наш сервіс буде думати, що операція доставки пройшла успішно. Але навіть якщо подія була успішно доставлена, ми ніяк не дізнаємося, що хтось цю подію отримав, а якщо отримав, то обробив. Більше того, ми не можемо заздалегідь знати, в який момент ця подія буде отримана. Це ускладнює логіку роботи додатку загалом. Якщо квитковий мікросервіс не зможе (з різних причин) отримати або обробити цю подію, то ми отримаємо не консистентний стан замовлення. Гроші користувач сплатить, а квиток виписаний не буде. Така неконсистетність — наслідок розподіленої роботи наших сервісів, і вона потребує контролю над такими ситуаціями.

Друга особливість полягає в тому, а що взагалі являє собою подію? Якщо ми використовуємо REST API, найчастіше вибираємо популярний формат JSON і HTTP як транспортний протокол для комунікації. Це дуже зручно, тому що в будь-якій мові програмування є свій HTTP клієнт і можливість створення/парсингу JSON документів. А це дозволяє створювати нові мікросервіси будь-якою мовою програмування і на будь-яких платформах. У messaging systems кожна подія — це лише масив байт. В Apache Kafka він зберігається на диску як байтовий масив. Тому нам потрібно спочатку серіалізувати нашу об’єкт-подію, а потім десеріалізувати. А це призводить до думки, що алгоритм для серіалізації має бути платформенно-незалежним. Наприклад, ми перетворимо об’єкт на JSON, а потім в масив байт. І все б добре, але JSON — не найкомпактніший, і не найшвидший у плані парсингу. Набагато ефективніше було використовувати бінарний формат, який добре зарекомендував себе (Protobuf, FlatBuffers, Kryo, MsgPack), але потрібно бути впевненим, що для нього є підтримка у використовуваних платформах/мовах програмування.

Третя особливість стосується збереження коду подій. Уявімо, що ми маємо мікросервіс, написаний на Java, який відправляє подію в message broker. Ця подія буде прийнята та оброблена іншими мікросервісами. Де зберігатиметься код класу-події? Він не може зберігатися в самому мікросервісі, значить нам знову потрібно його винести в якусь загальну бібліотеку. Тоді сервіси-споживачі при отриманні події дізнаються за метаінформацією його тип, завантажать його сигнатуру із загальної бібліотеки і виконувати десеріалізацію. Але що якщо вони написані не на Java (Python, Ruby)? Адже вони не зможуть завантажити Java байт-код. А це означає, що в такому випадку потрібно використовувати якусь кросплатформну мову (Interface definition language, IDL) для опису моделі (схеми) подій (Apache Thrift, Avro, Pro-tobuf).

Такий підхід був свого часу революційним для IT, тому що раніше ми завжди вручну писали код. Якщо ми використовуємо Apache Thrift, наприклад, то можемо описати не лише наші класи, але й винятки та навіть описи сервісів:

exception IllegalOperationException {
    1: i32 erroCode,
    2: string description
}
 
struct PaymentEvent {
    1: i32 paymentId,
    2: string customer,
    3: double amount
}

Схоже виглядає схема для Google Protocol Buffers:

message PaymentEvent {


  optional int32 paymentId = 1;

  optional string customer = 2;


  optional double amount = 3;
}

З цієї схеми за допомогою спеціальних утиліт (компіляторів) можна згенерувати код як клієнтської, так серверної частини). Це ускладнює роботу, так як ми обмежені можливостями такої мови. Ми не можемо використовувати звичні типи даних із Java SDK, тільки примітиви та колекції. Але це дозволяє бути більш незалежним у виборі платформи для розробки.

Ще один плюс використання таких схем (або контрактів) в тому, що вони є частиною так званого contract-first design, коли всі зацікавлені сторони можуть обговорити контракт на визначення моделі даних і сервісів і тільки потім почати їх реалізацію. Використання схем (а не коду) дозволяє говорити однією, платформо-незалежною мовою.

Четверта особливість стосується зміни нашого API. Як і у випадку з REST API, всі використовуються класи-події є частиною публічного API. І це означає, що ми можемо змінювати їх, не повідомивши всіх споживачів (клієнтів). Ситуація ускладнює тим, що Kafka зберігає події на своїх серверах. І якщо ми змінили вміст події у відправнику, то сервіс-споживач зі старою схемою не зможе його розпізнати. Якщо така ситуація можлива, нам доведеться вводити версіонування, яке підтримується не всіма IDL реалізаціями (наприклад, Apache Avro).

П’ята особливість стосується конвенцій та правил. У REST API було набагато простіше, тому що ми використовували RESTful веб-сервіси, де вже сформувалися загальні правила та best practices з проєктування такого API. У messaging systems немає жодних обмежень (constraints) на опис подій. Візьмемо для прикладу оплату платежу. Яку подію (або події) потрібно згенерувати? Як будуть називатися класи подій і яку інформацію вони містять? Звідки ми знатимемо, що наявної інформації достатньо інших послуг для обробки цієї події? У разі великого корпоративного проєкту легко створити ситуацію, коли в кожній команді (мікросервісі) будуть власні конвенції та практики з використання подій.

Шоста особливість стосується асинхронної обробки подій. Така обробка є великим плюсом з погляду продуктивності. Але з іншого боку, вона вимагає більш складного керування всім процесом комунікації між сервісами, особливо обробки помилок. Це насамперед стосується атомарності наших операцій. Якщо платіж успішно оплачений, а потім виявилося, що вільних місць на рейсі немає або він скасований, необхідно скасувати і оплату. Чим більше послуг задіяно в одній операції, тим складніше забезпечити її атомарність та обробку помилок.

Сьома особливість стосується масштабування. Якщо ми використовуємо REST сервіси, то з збільшенням навантаження стикаємося з тим, що сервери не встигають обробляти вхідні запити. І як вихід — горизонтальне масштабування, коли ми створюємо кластер із серверів, а перед ним ставимо load balancer. Він перенаправлятиме запити по закладених у ньому алгоритмах, балансуючи навантаження. Якщо ми використовуємо messaging systems, то тут у нас більше можливостей. Ми можемо збільшувати кількість споживачів. Але при цьому виникають дві проблеми. Нам потрібно гарантувати, що кожну подію буде оброблено лише один раз (одним споживачем). Друга проблема полягає в тому, що споживачі будуть конкурувати за вхідні події, то це знизить загальну продуктивність. Тому є ще одна опція — зберігати події (повідомлення) у кількох місцях (partitions) в брокері, відповідно призначаючи кожному споживачеві свій partition. Це прискорить обробку за рахунок паралелізму, але тут можлива ще одна складність. Потрібно гарантувати, що події будуть оброблені саме в тому порядку, в якому вони відправлені. Інакше може вийде, що подія «Платіж оплачений» прийде раніше, ніж «Платіж створено». І четверта проблема полягає в тому, що у споживача може статися збій (програмний або апаратний) при обробці події. Якщо така подія буде позначена в брокері як прочитана, то вона буде просто втрачена і ніколи не буде оброблена. Тому як варіант тут можна включати ручне підтвердження прочитання споживачем

Восьма особливість полягає в тому, що подія зберігає в собі будь-яку операцію над сутністю (або root aggregate), успішну або невдалу. А коли ми маємо справу з критично важливою інформацією, то необхідно робити її аудит. У найпростішому випадку це можна реалізувати за допомогою AFTER INSERT/UPDATE тригера, який просто копіює новий стан запису. Але в такому разі ми змушені зберігати всю інформацію, а не тільки ту, що змінилася, і ми не зберігаємо контекст, тобто в рамках якоїсь бізнес-операції це виконувалося і для чого. Наші події є immutable даними, які можна зберігати в базі даних (event store) як для аудиту, так і щоб можна було переглянути стан сутності на будь-яку дату. В такому випадку можна взагалі не зберігати в БД поточний стан сутності, тому що його можна отримати, застосувавши всі зміни, що стосуються його подій. Такий патерн називається event sourcing, і вiн досить популярний зараз для фiнансових проєктiв.

Як ви бачите, у event-driven architecture є велика кількість особливостей і підводних каменів, тому завжди перед початком розробки варто продумати, що краще використовувати для комунікації між сервісами: її, REST API або якісь інші підходи.

Domain and integration events

Думаю, що всі добре знають підхід Domain-Driven Design (DDD) і часто його використовують, створюючи сутності і пов’язуючи їх один з одним. Події також є частиною DDD, оскільки є частиною бізнес-правил та реакцією на зміну стану сутності у цій моделі. Тому проєктування подій має бути нерозривно пов’язане з нашою існуючою моделлю даних. Давайте торкнемося особливості проєктування самих подій.

Отже, перша особливість у тому, що події не однорідні і діляться на доменні (domain) і integration (інтеграційні). Доменні події є частиною доменної моделі мікросервісу, в якому вони визначені (його Bounded Context). Вони не повинні залишати свій мікросервіс, і вони не застосовні поза своєю моделлю. По суті, такі події зручні для того, щоб обмінюватись нотифікаціями між компонентами однієї програми. Їх ключова риса — їх можна відправляти в рамках локальної транзакції, при цьому вони будуть підкоряються відомим принципам ACID. Нас більш цікавлять інтеграційні події. Їхня відмінна риса — вони є частиною доменної моделі декількох мікросервісів (Shared Bounded Context). Наприклад, подію «Платіж оплачено» або «Рейс скасовано». Тому ми можемо спокійно пересилати їх між сервісами, які можуть обробити таку подію. Тому їх необхідно зберігати у загальних бібліотеках, до яких мають доступ всi сервiси. Ще одна їх особливість — вони відправляються тільки після завершення локальної транзакції (успішного або неуспішного), коли локальний стан сервісу став стабільним. Проєктування таких подій є більш складним процесом, тому що включає роботу відразу кількох зацікавлених команд. А це певною мірою порушує автономність кожного мікросервісу. Наприклад, ми спочатку відправляли подію «Платіж оплачений», але потім сторонньому сервісу знадобилася знати також про не минулу оплату, і ми створюємо і відправляємо нову подію (Збій при оплаті платежу).

Друга особливість — події є immutable типами. Дуже важливо, щоб ні в кого не було можливості модифікувати вміст події після відправки. Інакше буде неможливо дізнатися, що в оригіналі було відправлено. Крім того, immutability випливає з того факту, що подія — щось, що сталося у минулому. А якщо так, то ми не можемо його змінювати на основі інформації з сьогодення. DTO — найпопулярніший приклад immutable types. Але події, хоч і переносять інформацію між сервісами, як і DTO, мають важливу відмінність. Якщо виникла помилка при відправленні події в інший сервіс, ми маємо повторити відправку того самого об’єкта-події. Але якщо, наприклад, якась подія сталася ще раз (збій при оплаті платежу), то нам необхідно і надіслати нову подію

Третя особливість полягає в тому, що хоча події можуть містити найрізноманітнішу інформацію, але в них обов’язково повинні бути три властивості:

  • Аудит — час створення (надсилання), ідентифікатор відправника (користувач або сервіс).
  • Унікальний ідентифікатор самої події для того, щоб ми не обробили його двічі.
  • Тип події (наприклад, PaymentSuccess). Ми могли б використовувати замість типу назва класу в нашому Java коді, але це ускладнило б рефакторинг. Якби ми хотіли перейменувати клас, то нам потрібно ці зміни відправити всім споживачам. Крім того, потрібно подбати про те, щоб випадково не створити дублікат типу з такою самою назвою. Імовірність такої колізії невелика, адже кожен мікросервіс має свою доменну модель і свою сутність. Але можуть бути сутності, які належать до різних Bounded Context (Користувач), і тоді такий конфлікт може статися.
  • Ідентифікатор тієї сутності, до якої належить подія (наприклад, платежу, якщо йдеться про неї).

Також опціонально можуть бути дані про платіж. Але ж які? Звідки наш платіжний сервіс може заздалегідь знати про це? Він і не зобов’язаний це знати, тому що інакше це порушує його ізольованість та автономність. Тому при створенні події можна використовувати два патерни. Перший патерн Event notification передбачає, що у події буде лише ключова інформація — наприклад, ідентифікатор платежу. Якщо якомусь сервісу потрібно більше, він використовує REST API, щоб запросити ці дані. Якщо ми використовуємо патерн Event-carried state transfer, то поміщаємо всю можливу інформацію про платеж. У кожного патерну є свої плюси та мінуси. Перший іноді вимагає робити додаткові запити, що збільшує навантаження послуги, але робить їх незалежнішими. Другий паттерн збільшує навантаження на мережу, оскільки ми постійно надсилаємо надмірну інформацію. Ми в кожному конкретному випадку вирішуватимемо, який із них використовувати.

Четверта особливість стосується того, як ми називатимемо наші події. У REST API прийнято дуже зручну CRUD-модель, де на кожну операцію є свій HTTP-метод:

  • GET — отримання ресурсу;
  • POST — створення ресурсу;
  • PUT — оновлення ресурсу;
  • DELETE — видалення ресурсу.

І є спокуса створити універсальні класи PaymentCreatedEvent або PaymentUpdatedEvent, які ми генерували б на будь-яку зміну в стані платежу. Так можна зробити, але буде принципово неправильно відправляти їх у кожному випадку. Справа в тому, що запити в REST API — це команди від зовнішнього клієнта, які визначають однозначну дію над нашим ресурсом (створити платіж, змінити платіж). А події, як уже говорилося, є частиною DDD. І генерація події — реакція на ті бізнес-процеси, що відбуваються у нашій доменній моделі. Якщо операція оплати платежу завершилася успішно, то ми повинні відправити PaymentSuccessEvent

Якщо ж ми відправимо PaymentUpdatedEvent, то одержувачі цієї події не знатимуть, що спричинило його створення. Єдине, в чому вони можуть бути впевнені — що даний платіж був збережений. І така нотифікація може бути використана хіба що для аудиту (або надсилання повідомлення заінтересованим особам). Одержувачі навіть не знатимуть, які поля були змінені чи додані. Тому в нашому випадку ми використовуватимемо події, прив’язані до доменної моделі.

Створюємо події

Для того щоб краще зрозуміти, як виглядають події, створимо їх у нашому коді. Як уже говорилося, більш продуманим підходом є той, за якого ми створюємо схеми (data definitions), використовуючи Apache Avro або Google Protocol Buffers. Але оскільки не всі з ними знайомі, то для простоти можна створити стандартні Java класи.

Де зберігатимуться ці події? Доменні події як частина доменної моделі зберігаються у своєму мікросервісі. А інтеграційні доведеться зберігати в окремій клієнтській бібліотеці, до якої мають доступ (імпортують) усі зацікавлені мікросервіси.

Щоб спростити обробку подій і зробити її універсальною, добре додати клас, який буде базовим для всіх наших інтеграційних подій. Такий підхід дозволить уникнути можливих проблем з несумісністю API різних сервісів і спростить створення нових подій. Він не вплине на автономність мікросервісів, оскільки зачіпає інфраструктурне питання, а не бізнес-орієнтоване.

Отже, базовий клас BaseEvent буде виглядати так:

@Getter
public abstract class BaseEvent<T> {
      
       private final String id;
      
       private final String entityId;
      
       private final String type;
      
       private final String source;
      
       private final LocalDateTime createdAt;
      
       private final T payload;
 
       public BaseEvent(String entityId, String type, String source, T payload) {
              this.entityId = entityId;
              this.type = type;
              this.source = source;
              this.payload = payload;
              createdAt = LocalDateTime.now();
              id = UUID.randomUUID().toString();
       }
}

Цей клас оголошений як immutable, тому що його зміна повинна бути неможлива. Розберемо у деталях його вміст:

  • id — унікальний (на основі UUID) ідентифікатор події, який генерується відправником;
  • entityId — ідентифікатор сутності, прив’язаної до цієї події. Він може і числовим, і рядковим, тому ми вибрали рядковий тип як більш універсальний;
  • type — тип події, який його однозначно характеризує (наприклад, PaymentSuccess). Він зроблений рядковим, а не перерахуванням, щоб нам не доводилося щоразу змінювати загальну бібліотеку при додаванні/видаленні/зміні подій;
  • source — ідентифікатор модуля-відправника (наприклад, payment-service);
  • createdAt — час створення події;
  • payload — опціональне поле, в яке можна записати будь-які додаткові дані, специфічні для цієї події (параметризувавши його типом T).

Цей тип можна було б оголосити як Java record, але тоді ми не змогли б його успадкувати. Тепер у платіжному сервісі вже можна додати перерахування PaymentEventType, оскільки воно стосується лише тих подій, що генеруються одним сервісом

public enum PaymentEventType {
       PAYMENT_SUCCESS, PAYMENT_FAILURE
}

де ми вкажемо всі інтеграційні події, які можуть відбуватися у платіжному сервісі. Потім додамо два класи-спадкоємці для BaseEvent: PaymentSuccessEvent:

public class PaymentSuccessEvent extends BaseEvent<PaymentDTO> {
 
       public PaymentSuccessEvent(String entityId, String source,
                     PaymentDTO payload) {
              super(entityId, PaymentEventType.PAYMENT_SUCCESS.name(), source, payload);
       }
}

та PaymentFailureEvent:

public class PaymentFailureEvent extends BaseEvent<PaymentDTO> {
 
       public PaymentFailureEvent(String entityId, String source,
                     PaymentDTO payload) {
              super(entityId, PaymentEventType.PAYMENT_FAILURE.name(), source, payload);
       }
 }

Якщо у необхідності додавання події PaymentSuccessEvent немає жодних сумнівів, то наскільки виправданою є поява PaymentFailureEvent? Спочатку він виглядає внутрішньою подією для платіжного сервісу, але він може стати в нагоді для повідомлення квтикового сервісу, що поточне замовлення має проблеми з оплатою і необхідно встановити його стан в ERROR (або FAILED).

Як payload ми вибрали найбільш загальний та повний варіант — PaymentDTO з усіма даними платежу, таким чином ми використовуємо патерн Event-carried state transfer:

@Getter
@Setter
public class PaymentDTO {
 
       @NotEmpty
       private String id;
      
       @NotEmpty
       private String userId;
      
       private double amount;
      
       private String currency;
      
       private int ticketId;
      
       private int orderId;
      
       private String paymentType;
      
       private boolean success;
      
       private String errorDescription;
      
       private LocalDateTime createdAt;
}

Як уже говорилося, його мінус у тому, що подія тут містить надлишкові дані, але оскільки сам PaymentDTO дуже простий і містить трохи полів, то такий вибір є виправданим. Як альтернативний варіант — створити DTO, де будуть лише ті дані, які специфічні для невдалого платежу: errorDescription, createdAt та orderId.

Висновки

У цій статті ми розглянули основні особливості event-driven architecture, головні патерни з групи Enterprise Integration Patterns, виділили два типи подій і навели приклади Java-коду для цих подій.

Такий підхід дозволяє зробити послуги слабозв’язаними (на відміну від REST API), підвищити їхню надійність (за рахунок зберігання повідомлень у messaging systems) і продуктивність (оскільки ми не чекаємо відповіді від сервера). Але при цьому підвищуються інфраструктурні витрати (потрібно купувати сервери або послуги для messaging) і ускладнюється загальна обробка помилок та забезпечення атомарності операцій (робота з розподіленими транзакціями).

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось22
До обраногоВ обраному23
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Популярность событийных архитектур объясняется тем, что императивная парадигма (выполнение команд) не отражает природу реального физического мира. Программисты, обычно не помнят «ломки» мозга когда начинали делать первые шаги в программировании. Потому очень тяжело вернуть мозги назад в более естественный образ мышления. Современные event-driven системы представляют собой гибрид из привычного программирования и требований природы вещей. Я не буду критиковать уважая труд людей. Я просто попробую дать определения фундаментальных применяемых вещей и понятных интуитивно и не всегда правильных. Тестовым примером может служить вопрос «Буква это объект или значение? » . Опытные программисты обычно задумываются. И не знают что отвечать.
Итак, начнем с краеугольного и широко используемого понятия «Событие». Сначала обратимся к википедии ru.wikipedia.org/...​ризонте и одновременности.
Цитируем «В информатике и программировании событие — это сообщение программного обеспечения либо его части, которое указывает, что произошло»
Это определение мягко говоря ни о чем. Потому как событие не может быть подвешено в воздухе. В определении этот вопрос решается ссылкой на некое программное обеспечение или его часть. Настаиваю на том, что б эта часть как-то назвать. Например, не уменьшая общности назовем объектом. Единственно чем обладает объект так это — состоянием. Так вот событие — это изменение состояния объекта. В банальном случае событие так и называют по имени нового состояния в которое перешел объект. Событие «Равно 0» Но сам факт изменения состояния — это тоже событие! — это подмножество изменение состояний объекта! Но, такое изменение состояний, которое как-то характеризует объект с точки зрения взаимодействия с другими объектами. На этом можно было бы и остановиться, но мы имеем дело с программированием и потому продолжим. Очень любопытным фактом является то, что анализ на изменение состояния (выражение для определения произошло ли данное событие) не изменяет самого состояния объекта! И потому может выполняться параллельно. Но, есть и нюанс. Это выражение надо запустить для выполнения. Иначе откуда мы узнаем произошло ли ИНТЕРЕСУЮЩЕЕ нас событие? Ибо может получиться что событие произошло, а мы о нем никак не узнаем. Таким образом, надо разделить определение события (коим является выражение определяющее событие) и сам факт события. Что из этих двух сущностей мы называем событием? Обычно из контекста понятно, что именно имеется в виду. Но, я б зафиксировал разницу. Осталось разобраться с вопросом, когда надо запускать анализ на выполнение выражения. Ответ, то очевидный, когда оно нас интересует. И здесь мы приходим к понятию подписка. Процедура обработки событий выполняет эту же роль. Говорит о том, что данное событие интересует.

Так вот событие — это изменение состояния объекта.

А если событие — это ещё и вызов метода объекта? Callback.

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

www.youtube.com/watch?v=W9RM92eumDM
P.S. Только не

Callback

. Возвращаться никуда не надо

Не, ну если сам факт события обернуть в объект и говорить, что изменение состояния этого объекта — это событие... тогда да. Типа шёл я по лесу, а тут навстречу — медведь. И у меня в объекте количества встреченных медведей инкремент произошёл))
Пойду смотреть видео.

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

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

Объект здесь уровень напряжения на ноге микроконтроллера. Прерывание это результат анализа этого значения на то, что нас интересует. Переход по вектору прерывания это подписка на событие. В контроллере оно (событие в смысле анализ) жестко задано установками пина. 0, 1, изменение значения и реакция по переднему фронту или по заднему. В принципе это должно быть дело пользователя организация подписки и на какое событие. Но, тут уж как есть)

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

Дякую за статтю , цікава інформація.
В мене склалось враження, що описані підходи найбільше підходять до сервісів на Джаві. Чи є якісь приклади подібних патернів/ архітектур іншими мовами програмування.

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

Свого часу робили подібне на .NET (навіть, не .NET Core — його ще тоді і не було). Тому, запропоновані патерни та архітектура аж ніяк не специфічні саме для Java. Хіба що, для Java існує досить непоганий фреймворк Axon, а от для .NET, last time I checked, так і не спромоглися зробити достойний аналог.

Знайшов отаке: github.com/...​ycz/EventSourcing.NetCore
але не можу сказати, наскільки воно завершене та «канонічне»

Навіть для PHP є якась реалізація: getprooph.org

Корисна інформація. Дякую.

---

нотифікації

Не сприйміть за занудство, але вже визначтесь: чи то англійською ви пишете, чи українською. А то виходять англійські слова, але українськими літерами. Є слово «сповіщення».

Дякую за зауваження. Можна говорити і повідомлення i сповіщення. З іншого боку, не думаю, що слово «нотифікація» прямо-таки ріже око.

Це слово є в словнику. Не пишіть дурниць.

Т. І. Шинкаренко. Нотифікація // Українська дипломатична енциклопедія: У 2-х т./Редкол.:Л. В. Губерський (голова) та ін. — К.:Знання України, 2004 — Т.2 — 812с. ISBN 966-316-045-4

масштабування роботи передплатників

Мабуть тут мається на увазі насправді «підписники», а не «передплатники», так же?

Разом із мікросервісною архітектурою приходить складність взаємодії цих сервісів, обробки помилок, і т.д. Цікаво чи оцінював хтось з якого моменту переваги мікросервісної архітектури перевищують її недоліки, у порівнянні з монолітом. Які критерії є важливими для такого порівняння? Кількість запитів повинна бути як у сервісів Розетки до війни? (Насправді я не знаю скільки, але думаю що дуже багато... можливо десятки тисяч за секунду)

Питання із ряду вічних-холіварних. Насправді Microservice architecture != Event driven architecture.

У Марка Річардса в його курсі на O’reilly є досить крута порівняльна таблиця архітектурних паттернів по декільком важливим параметрам: Agility, Deployment, Testability, Performance, Scalability, Development.

www.oreilly.com/...​re-architecture-patterns

І який особисто ви робите висновок з цієї таблиці, в контексті мого запитання?

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

Наприклад — якщо це MVP де в процесі розробки залучено до 5 людей і немає супер великих вимог по перфомансу на початку — модульний моноліт виглядає більш пріорітетним ніж та ж мікросервісна архітектура, і навпаки — якщо є вимоги що велика кількість людей повинная бути якимось чином поділена на зони відповідальності і чітко відповідати за певний функціональний компонент з перспективою легкого масштабування — тут вже це аргументи в сторону мікросервісного підходу.

Треба більш чітко розуміти реальні вимоги і не намагатись вирішувати потенційних проблем яких взгалі може не бути.

Зону відповідальності можна чітко поділити і в моноліті, достатньо щоб він був просто модульним. Мені здається, що більшості проектів мікросервіси не потрібні.

Зону відповідальності можна чітко поділити і в моноліті, достатньо щоб він був просто модульним. Мені здається, що більшості проектів мікросервіси не потрібні.

на справді суть мікросервісів в тому щоб для розробки однієї фічі можно було залучити як мінімум команд — що досягається завдяки делегуваню роботи над одним bounded context одній команді, що дозволяє скейлити девелопмент легше ніж у модульному моноліті — де розподіл іде за фукнціональним, а не доменним принципом і відповідо разробка переважной більшості фіч може вимагати постійного залучення велокої кількості команд. Аналогічно доменний принцип розроділу роботи дозволяє ії ізолювати без необхідності залучення складним інтеграційних патернів і переважну більшість розробки фіч end-to-end сконцертувати в одному проекті и команді без необхідності постійно відслідковувати складні інтеграційні ланцюги. А перформанс тут взагалі часто нідочого, так як аналогічні техніки можно використовувати як в моноліті так і в мікросервісах — єдина перевага, ізоляція що дозволяє більш локально застосувувати підходи без імпакту для всієї системи.

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

Насправді модулі можуть мати різне призначення, в тому числі є варіант і доменних модулів.

— Domain Is organized around a feature, business domain, or user experience.
— Routed Is the top component of the NgModule. Acts as the destination of a router navigation route.
— Routing Provides the routing configuration for another NgModule.
— Service Provides utility services such as data access and messaging.
— Widget Makes a component, directive, or pipe available to other NgModules.
— Shared Makes a set of components, directives, and pipes available to other NgModules.

то що ви пишите якраз функціональний розроділ — роутинг, сервіса, віджети, а не доменний — блок юзер менеджменту(наприклад) на UI котрий не буде працювати поки не залучиш все з вище зазначенного.

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

Схоже що ви не зрозуміли, що я зрозумів =).

на справді суть мікросервісів в тому щоб для розробки однієї фічі можно було залучити як мінімум команд

Це суть сферичного коня у вакуумі.

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

В результаті елементарні фічі тонуть під багатошаровим кодом непотрібної насправді інфраструктури.

Єдині, хто справді мають зиск від засилля мікросервісів в сучасному ІТ — це девопси. Тому що без 8 годин 5 днів підтримки все це титанічне гівно з кубернетісами, хелмами, дженкінсами тераформами просто не працює. Хоча насправді вся ця багатогігабайтна срань просто перекидає жалюгідні коротенькі пласкі жсони з кількох баз на фронт.

Хоча насправді вся ця багатогігабайтна срань просто перекидає жалюгідні коротенькі пласкі жсони з кількох баз на фронт.

О, знатно пригоріло, одразу видно що не зміг. Не підійшов мікроскоп забивати цвяхи. Поганий мікроскоп. Співчуваю.

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

Мікроскрвісами скейлять розробку в першу чергу, а не навантаження, ваші уявлення про мікросервіси сугубо теоретичні, я писав вище — розуміння практичного 0.

Ну якщо треба скейлити навіть розробку, то це очевидно стосується великих проектів. А якщо проект великий, то навряд чи він прийматиме невелике навантаження. Ви, як практик, не бачите такого зв’язку?

Бачу зв‘язок як у м‘якого з теплим. Очевидно, що це дві різні речі. І проект не обов‘язково має велике навантаження, якщо є необхідність просто робити розробку паралельно 2-3-5 і більше командами, за коротший термін. Окрім сайтів-візитівок і умовного netflix, amazon є величезна кількість проектів де потрібно скейлити розробку не маючи high-load навантажень.

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

www.terrasoft.ua
www.folderit.com
sword-grc.com
www.finally.com
www.grapecity.com
Відкрив перші лінки по запитам crm, back-office, document management, project management, reporting/analytics etc — де очевидно що буде купа складних різнородних фіч та інтеграцій і доволі обмеженні навантаження в рамках користувача, взагалі там сотні такого тільки внутрішньокорпоративного добра.

Якщо ви праві, і бізнес використовує мікросервіси для швидкого створення масштабних проектів, навіть якщо вони не будуть мати великого навантаження, я це сприймаю як марнотратство. Не бачу причини чому це саме не можна зробити з модульним монолітом. Вийде і простіше, і дешевше. Точно так само як мікросервіси, можна роздати модулі між командами. Точно так само весь проект на мікросервісах не працюватиме без ключових компонентів, навіть якщо одна команда зарелізила свій мікросервіс раніше. Єдине що не зможе дати моноліт — це таке ж легке масштабування навантаження, ну і написання різних сервісів на різних мовах. Усе решта — понти для приезжих.

Зрозуміло. Доречі, багато зробив/брав участь у проектах з scaled agile? SAFe, LeSS, SaS і тому подібне?

Ні. Про мікросервіси читав лише теорію, практики справді — нуль. Тим не менше, але я таки намагаюсь об’єктивно оцінити ситуацію. Якщо б я побачив серйозні переваги, я б створював відповідні проекти. Поки що бачу у 99% понти від людей, які мають практику, але не можуть пояснити на конкретних прикладах в чому саме полягають переваги мікросервісів в порівнянні із модульним монолітом.

Перевага в деплойменті? Можу сказати лише за свій досвід — моноліти, над якими я працював, деплоїлись ну дуже довго, зміна одного рядку коду могла деплоїтись в сумі ( з прогоном тестів і т.д) більше години, коли деплой мікросервісу займає лічені хвилини.

+ single failure point, якщо в мікросервісі щось не так, впаде частина функціоналу, якщо в монолітному модулі щось не так — може впасти весь функціонал.

зміна одного рядку коду могла деплоїтись в сумі ( з прогоном тестів і т.д) більше години, коли деплой мікросервісу займає лічені хвилини.

Моноліт і мікросервіси писались на одному стеку технологій? Чи моноліт на Java, а мікросервіси на JavaScript? =)

+ single failure point, якщо в мікросервісі щось не так, впаде частина функціоналу, якщо в монолітному модулі щось не так — може впасти весь функціонал.

Тут треба більш детально пояснити що означає «якщо щось не так». Мабуть тут можливий єдиний варіант, який має відмінність між мікросервісом і монолітом — упав застосунок на бекенді. На скільки це часто може статись? Якщо розробники подбали про тести, дуже мала ймовірність такого сценарію. До речі, якщо бекенд написано на Node.js, то через пів секунди застосунок знову підніметься.

Ні, це були різні стеки технологій, але в першому кейсу проганялись тисячі тестів, включаючи тривалі e2e тести, а в другому сотні юніт тестів :)

Мабуть тут можливий єдиний варіант, який має відмінність між мікросервісом і монолітом — упав застосунок на бекенді

 — Так, це мав і на увазі. Звісно, якщо тестами все ок покрито, то шанс близький такого кейсу до нуля.

+ single failure point, якщо в мікросервісі щось не так, впаде частина функціоналу

Казки євангелістів. В 99% випадків ви не пишете амазон, де сервісів справді дуууууже багато, і падіння одного домену не накриє всю компанію загалом.
В 99% випадків ви пишете якийсь один продукт, в якому немає зайвих або не дуже важливих компонентів (а інакше нащо вони взагалі) і падіння одного із них викликає непрацездатність всього продукту.

якщо в монолітному модулі щось не так — може впасти весь функціонал.

І навпаки, якщо він ізольований, то так само весь моноліт може пережити.

А сенс такий що життєздатність моноліту і мікросервісів однакова. Хіба що мікросервіси тянуть за собою купу інфраструктурного брухту, який сам по собі додає points of failure.

scaled agile? SAFe, LeSS, SaS

Так це таке ж саме порівняння теплого з м’яким. Яким саме чином модульний моноліт заважає впроваджувати scaled agile?

Костя виразив дуже слушну думку:

Єдине що не зможе дати моноліт — це таке ж легке масштабування навантаження, ну і написання різних сервісів на різних мовах

І, якщо перший чинник ми виключаємо за вашим власним визначенням:

crm, back-office, document management, project management, reporting/analytics etc — де очевидно що буде купа складних різнородних фіч та інтеграцій і доволі обмеженні навантаження в рамках користувача

то, виходить, єдиною перевагою мікросервісів залишається можливість використання різних мов програмування кожною командою, залученою до проекту?

До речі, я не знецінюю цю перевагу, інколи вона може стати критичною з точки зору бізнесу. Треба тільки завжди пам’ятати про long-term maintenance costs такого рішення, та приймати його цілком свідомо.

Яким саме чином модульний моноліт заважає впроваджувати scaled agile?

Це інше питання. Але відвовідь очевидна. Якщо не вдаватися в розподіл компонентів, очевидно що більше ніж одній команді легше спринт делівирити якщо ти білдиш мікросервіс , налаштовуєш на це білд гейти, тести і тд а не коли ти білдиш і релізиш моноліт в якому деліверять ще декілька команд і імпактят один одного і страждають від втрати часу на зайві комунікації та імпакт шаредного простору розробки. про розподіл компонентів — у модульних монолітах теж намагаються виділяти по доменному принципу(копіюючи ідеологію мікросервісного дизайну) і при цьому інші варіанти розподілу між командами декларують не іделогічно вірними чи зручними для скейлу девелопменту.

імпактят один одного і страждають від втрати часу на зайві комунікації

Нібито модульний моноліт саме для того й проектується модульним, щоб максимально ізолювати один модуль від іншого?

А коли раптово змінюється контракт мікросервісу, від якого залежать інші мікросервіси — це не «втрата часу на зайві комунікації»?

імпакт шаредного простору розробки

Я не розумію, що це.

при цьому інші варіанти розподілу між командами декларують не іделогічно вірними чи зручними для скейлу девелопменту.

Хто саме декларує? Та які саме конкретно варіанти розподілу?
Чим саме ці варіанти зручніші для масштабування розробки?

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

Але відвовідь очевидна.

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

Нібито модульний моноліт саме для того й проектується модульним, щоб максимально ізолювати один модуль від іншого?
імпакт шаредного простору розробки

Ок, зробив припущеня, що ви знаєте базово чим відрізняється моноліт від мікросервісів.
Базова різниця між мікросервісом і монолітом буде у тому що, одиницею розробки і диплойменту є ізольований доменний сервіс у випадку мікросервісу, у випадку моноліту (компонентного чи ні — не важливо) додаток в цілому.
Очевидно, що у випадку мікросервісів кожна команда може мати ізольований цикл розробки і поставки від інших команд, у випадку моноліту це просто не можливо. Як результат ви будете мати купу імпакту від інших команд і обмежень навіть якщо ви налаштуєте все ідеально — у вигляді складнішого/довшого процесу білдів, dependency management policy на рівні додатку та залежний цикл оновлень(якщо хтось оновився з лібою, оновлюватися мают всі примусово) складнішим процессом перевірки, дебагу через необходність піклуватись про запуск всього додатку, імпактом можливих code issues що можуть заблокувати розробку іншої команди.. Ви цього всього не знали? так на базі чого ж ви робите свої висновки?

А коли раптово змінюється контракт мікросервісу, від якого залежать інші мікросервіси — це не «втрата часу на зайві комунікації»?

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

зробив припущеня, що ви знаєте базово чим відрізняється моноліт від мікросервісів.

Я знаю теорію. А також я знаю схильність євангелістів прихильників мікросервісів «демонизувати» моноліт. Тому, цікавить саме реальний практичний досвід, а не перекази статей або книжок.

у вигляді складнішого/довшого процесу білдів

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

dependency management policy на рівні додатку та залежний цикл оновлень(якщо хтось оновився з лібою, оновлюватися мают всі примусово)

А от і не треба допускати неконтрольованих оновлень ліб. Тут моноліт навіть кращий, бо інакше будемо швидко мати «зоопарк»

складнішим процессом перевірки, дебагу через необходність піклуватись про запуск всього додатку

Наскільки це складніше у реальності, а не в байках євангелистів? Запуск всього додатку — це, типу, рілі щось страшне?

імпактом можливих code issues що можуть заблокувати розробку іншої команди..

Ну це хіба що у репозиторій можна комітити код що не компілюється та не проходить тести. Це, по-перше, а, по-друге — модульний моноліт на те й модульний, що залежності між модулями мінімізовано та вони регулюються API контрактами (власне, як і у мікросервісах, але без фізичного розподілення на одиниці розгортування)

зовнішні інтеграції в добре розподілених доменах не будуть критично імпактити розробку іншої команди, хоча можуть мати залежатносі від допоміжних доменів и мати дублікати по данним.

Усе те ж саме у модульному моноліті.

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

Наскільки це складніше у реальності, а не в байках євангелистів? Запуск всього додатку — це, типу, рілі щось страшне?

Якщо у вас всі компоненти незалежні і мають передумови старту(сетап), умовний seed то з мікросервісом мене це мало обходить. Якщо мені взагалі пофігу інші компоненти для старту — мені все одно треба щоб інші команди ментейнили це і не зламали, інакше може не стартанути взагалі моноліт.

Ну це хіба що у репозиторій можна комітити код що не компілюється та не проходить тести. Це, по-перше, а, по-друге — модульний моноліт на те й модульний, що залежності між модулями мінімізовано та вони регулюються API контрактами (власне, як і у мікросервісах, але без фізичного розподілення на одиниці розгортування)

Та блін, у вас у стартапі щось залили що падає в рантаймі(не знали що таке буває?). І валить весь стартап. Эдин вихід це зловитим- ранити end-to-end тести з конфігураєю енву як білд гейт на пул риквесті, що на практиці ніхто не робить.

А от і не треба допускати неконтрольованих оновлень ліб. Тут моноліт навіть кращий, бо інакше будемо швидко мати «зоопарк»

Ви розумієте, що у вас якщо компоненти залежні між собою в рамках моноліту — це окремі ліби теж(залежніть з єдиною версією).
Якщо це мікросервіс, оновлення мікросервісу не форсить оновлювати код всіх залежних компонентів — я спокійно можу мати різні версій і не підганяти свій цикл релізу фічи під потребу щоб оновилися всі хто від мене залежить.
В моноліті відповідно фічу можна релізити тільки тоді коли всі консьюмери оновилися. Або створювати нову імплементацію кожен раз і тримати їх всі на рівні поточного білду у коді(версіоновуння в коді єдино доступне). Ви не можете підрубити компоненти різніх версій зовньшньої залежності в моноліт. Я якщо там граф залежностей — це взагалі буде ад відслітковувати будь яке оновлення. Це day-to-day морока і колосальний maintanence і ризик поламати функціональність, яка просто відсутня за умови наявності ізоляції що полегшує code sharing, і реліз фіч робить незалежним від інших.

Чесно кажучи — мені трохи смішно читати все що ви пишите як контраргументацію. Це базовий біль з яким стакаєшся при розробці такого классу додатків, і у мене враження що це намагання просто сказати аби шо у відповідь з повним запереченням і нерозумінням ситуації\відсутністю тої самої практики.

у вас у стартапі щось залили що падає в рантаймі(не знали що таке буває?). І валить весь стартап.

Сарказм вам не личить. З адекватною командою розробки таке буває ну раз на кілька місяців. Та виправляється дуже швидко.

Це day-to-day морока і колосальний maintanence

Ви так кажете, ніби ви версії ліб оновлюєте кожного дня. Взагалі-то там, де у наявності є зрілий configuration management, намагаються дотримуватися принципу «працює — не чіпай». Оновлються залежності контрольовано або тоді, коли треба позбутися security дірки, або тоді, коли вкрай потрібна функціональність, яка є тільки у новій версії.

за умови наявності ізоляції що полегшує code sharing

Як це відноситься до оновлення версій ліб? Це вже не кажучи про те, що ізоляція та code sharing — нібито, речі несумісні.

Як ваші незалежні компоненти інтегровані у моноліті будуть та будуть взаэмодіяти?

Через API контракти, тобто інтерфейси. При цьому, це буде in-process, тобто ніякої network latency, та самі виклики не можуть стати point of failure, коли раптом зникне мережеве з’єднання, на відміну від REST / GRPC / ...

Як варіант, якась in-memory queue, можливо з додатковим backing store.

Якщо у вас shared memory компоненти. Це можуть бути або залежності по коду або підключені пакети. Кожен компонент інтегрований з іншими і виконує певну фукнціональнасть від якої можуть залежати інші компоненти.

Візьмемо банальний приклад — логіка перевірки валідності ІНН для користувача у системі що використовується для низки паралельних соціальних автоматизованих процессів — оформлення соціальних виплат
оформленя податкового обліку
оформлення страхування
оформлення соціальних програм
реєстрація фіз особи підприємця
отримання персональних подсвідчень
і т.д.

уявімо що таких компонентів у вас в системі штук 50.. і всі вони дозволяють вводити ІНН і створювати\лікнувати користувача з відповідного специфічного UI і потребують валідації коду.

Припустим ми залежали тількі від дати нарождення і все було відлагоджено V1. І тут я отримаю на спринт feature request:
1. Той модуль що працює з фіз особами підприємцями хоче пітримувати наприклад інший код для осіб зареестрованих в іншій конкретній країні. Інші модуля такого не потребують. Це версія валідації скажімо V2
2. Мені потрібно змінити існуючий алгоритм, тому що змінилися/розширилися стандарти і паралельно діють старі на перехідний період (рік-два), можливо контракти навіть змінилися. V3

З inter-process компонентами\мікросервісами я можу цю логіку хоч в нову версію пакету поставити, хоч як окрему версію сервісу задіплоїти, а стару залишити для сумісності. Зберегти один паблік метод — все інше деталі імплементації. Вартість моє розробки — оновленя могу коду + оновлення залежного 1 компоненту що потребує змін версії v2. Все інше буде працювати поки інші команди не запланують цю роботу в рамках якихось своїх майбутніх змін у зручний момент.

Яка вартість будет оновлення у випадку з shared memory монолітом? новий контракт окремий і вимога вибирати потрібну імплементацію клієта? breaking change з оновленям 50 компонентів і регресією додатку в рамках поточного спринта щоб я міг свої вимоги виконати? 3 версії паблік методів? інші варінти?

Це впринципі стандартна штука з якою стикаєшся постійно, оновлюватись может не тількі security patch фреймворку, а підходи для авторизації в додатку, протоколи взаємодії сервісів, якісь загальні речі повязані з хендлінгом помилок, локализацією, шедулінгом джоб, логуванням, документуванням контрактів, серіалізацією, доступом до данних іще мільйоном різних речей які міняють їх потрібно розвивати і якщо їх постійно потрібно оновлювати глобально — це завжди будет стопером у випадку моноліту для розвитку додатку і обмежень у виконанні цілей спринта у випадку кодингу прямих функціональних вимог.

Яка вартість будет оновлення у випадку з shared memory монолітом? новий контракт окремий і вимога вибирати потрібну імплементацію клієта?

Вартість така сама:

оновленя могу коду + оновлення залежного 1 компоненту що потребує змін версії v2

Ніяких breaking changes. Завдяки різним API contracts через dependency injection так само можна розрулити, куди інжектити стару та нову реалізації.

підходи для авторизації в додатку, протоколи взаємодії сервісів, якісь загальні речі повязані з хендлінгом помилок, локализацією, шедулінгом джоб, логуванням, документуванням контрактів, серіалізацією, доступом до данних іще мільйоном різних речей які міняють їх потрібно розвивати і якщо їх постійно потрібно оновлювати глобально — це завжди будет стопером

Це ваш власний досвід, або знову переказ якоїсь статті або книжки?

Якщо власний, то цікаво почути конкретику, реальні кейси з вашого особистого досвіду. Але, нажаль, поки що я у відповідь отримую тільки купи buzzwords...

До того ж, if you ask me

підходи для авторизації в додатку, протоколи взаємодії сервісів, якісь загальні речі повязані з хендлінгом помилок, локализацією, шедулінгом джоб, логуванням, документуванням контрактів, серіалізацією, доступом до данних

це те, що взагалі повинно змінюватися тільки за апрувом архітектурного борду проекту та суворо централізовано.

Бо інакше будуть анархія та зоопарк. Особливо цікаво з цієї точки зору виглядають протоколи взаємодії сервісів, серіалізація, підходи до авторизації, та логування.

Ніяких breaking changes. Завдяки різним API contracts через dependency injection так само можна розрулити, куди інжектити стару та нову реалізації.

Тобто у моноліті якщо бізнесс вимога потребує breaking changes (як у прикладі вище) у паблік контракті компоненту вы відмовите product owner у реалізації? :) Якісь проблеми з цим?

це те, що взагалі повинно змінюватися тільки за апрувом архітектурного борду проекту та суворо централізовано.
Бо інакше будуть анархія та зоопарк. Особливо цікаво з цієї точки зору виглядають протоколи взаємодії сервісів, серіалізація, підходи до авторизації, та логування.

Ну, по-перше, яке відношення має погодження підходів, до складнощів процессу їх імплементації у різних архітектурах про котрі ми говоримо?

По-друге, суворо централізовано це як?
Комусь треба почати апгрейдити у рамках свої вимог, наприклад юзати якусь специфічний атрибут з токену і задеприкейтити існуючий. А ти такий приходиш і кажеш — всі в наступному спринті зупинємо розробку, бо треба переробити авторизацію данних і задепрекейтити підходи старі і перейти на нові, або нічого не робимо зі змін чекаємо наступного технічного спринта для всіх команд? Бо інакше анархія\зоопарк , я вірно розумію?

Це ваш власний досвід, або знову переказ якоїсь статті або книжки?

Знов перекази статі? Які саме книжки чи статі я десь переказував тут, ви нічого не плутаєте?
Віповідаючи на питання. Всі описани кейси — мій власний абстрагований від конкретної ситуації досвід.

Якщо власний, то цікаво почути конкретику, реальні кейси з вашого особистого досвіду. Але, нажаль, поки що я у відповідь отримую тільки купи buzzwords...

Наведіть мені ці buzzwords з попереднього коменту з описом ситуації практичної — я спеціально перечитав, не знайшов там ніодного. Якщо вони там дійсно є, я можу пояснити спробувати.

Вартість така сама:
Ніяких breaking changes. Завдяки різним API contracts через dependency injection так само можна розрулити, куди інжектити стару та нову реалізації.

Про зворотню сумісніть і breaking changes у моноліті і створення нової вeрсії інтерфейсу не міняючи старої імплементації(те що по вашому буде мати таку саму вартість і зворотню сумісність):

1. Потрібно думати як поєднати новую і стару версію коду — наприклад якщо там є спільна логіка, треба обрати серед наступних варіантів:
— зробити повний дублікат коду(для ізоляції як ви сказали потенційних breaking changes та уникнути регрессії на залежних компонентах)
— не робити дублікати і спільний код підключити в обидві версії і при їх зміні розуміти, що насправді ви комітите зміни не в одну версії, а у всі.
З мікросервісами цієї проблеми немає — ви просто змінюєте код і диплоїте новую версію поруч зі старою з чистим кодом(нова версія) і повністю гарантуючи незмінність попередньої версії(що у випадку сервісу, що у випадку пакету).
Я б сказав як мінімум з цього уже очевидно, що вартість буде різною.

2. Потрібно розуміти ваш компонент у моноліті не може ізольовано оновлювати свої залежності у разі потреби(будь-яка third-party ліба або пакет) — допустим ви пройшли этап архітектурного погодження з усіма оновлення залежностей(напишу на всяк випадок, щоб ми знову не зупинялись на цьому).
Тепер реальність буде такою, що треба оновити залежності для всього проекту одномоментно.
У вас не може бути дві версії залежності у коді на весь моноліт. Тобто ви навіть технічно не можете зберегти попередню версію без змін, оскільки оновленя залежностей потягне їх у всі версії.
Ви не можете цього зробити не домовившись про оновлення іншого комопоненту(якщо ліба і там викростовується) і не узгодивши таймлайни різних команд на розробку, котра дуже може бути для інших команд не буде нести ніякої business value, просто maintenance тех боргу.

Автор, код лінукс має майже 30 млн рядків коду. Все це написано в опенсорсі (що я вважаю безумовно мінусом для складних проектів, де мотивація розробників частково тримається на ентузіазмі) і велика частина написана навіть без використання ООП (обджектив Сі). Ти дійсно думаєшь що мікросервіси, або навіть ООП, це якись сільвер буліт для написання великих проектів ? Мабуть треба почати, якщо б код подібного проекта нарізали на мікросервіси, то його кодова база роздулась би мінімум в три рази ...

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

Всё придумано до нас: en.wikipedia.org/wiki/Microkernel

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

Автор, код лінукс має майже 30 млн рядків коду. Все це написано в опенсорсі (що я вважаю безумовно мінусом для складних проектів, де мотивація розробників частково тримається на ентузіазмі) і велика частина написана навіть без використання ООП (обджектив Сі). Ти дійсно думаєшь що мікросервіси, або навіть ООП, це якись сільвер буліт для написання великих проектів ?

Ну тут залізобетонная аргументація проти мікросервісного підходу(напевно всіх популярних сучасного паттернів і стандартів розробки побудованих навколо agile) я проти такого беззбройний, нічого не можу протиставити — все треба викинути) Сюди можна ще додати ПО для великого адроінного колаідеру я думаю та microsoft office з загальновідомої картинки як гарні приклади для наслідування % )

стандартів розробки побудованих навколо agile

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

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

:D
Ну хоч б той що на fixed price проектах(про який ви пишите) , бюджет фіксованний завжди.
А там де він не фіксований (time and material) методологію вам оберуть за вас, або як мінімум треба буде довести її еффективність для конкретного випадку:)

Зазвичвй

fixed price

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

Дуже влучна аргументація на користь моноліту :D

нічого не можу протиставити — все треба викинути)

Твій сарказм насправді не такий вже і сарказм. Якщо увірватися в будь-який рандомний проект на мікросервісах, то 8 із 10 девелоперів тобі скажуть що так, все працює хйово і все треба викинуть.
Інші двоє — то місцевий архітект-фанат мікросервісів і скрам-мастер який начитавсь казок про те що мікросервіси це шлях до вселенського щастя.

Твій сарказм насправді не такий вже і сарказм. Якщо увірватися в будь-який рандомний проект на мікросервісах, то 8 із 10 девелоперів тобі скажуть що так, все працює хйово і все треба викинуть.
Інші двоє — то місцевий архітект-фанат мікросервісів і скрам-мастер який начитавсь казок про те що мікросервіси це шлях до вселенського щастя.

так-так, все кляті мікросервіси — залишилось тільки дочекати коли девелопери почнут массово звілнятися з FAANG і подібних технологічних компаній через мікросервіси і вони перероблять свої архитектури на монолітні — і почнется закат эпохи, як те прогнозують освідчені спеціалісти на ДОУ)

так-так, все кляті мікросервіси — залишилось тільки дочекати коли девелопери почнут массово звілнятися з FAANG і подібних технологічних компаній через мікросервіси і вони перероблять свої архитектури на монолітні — і почнется закат эпохи, як те прогнозують освідчені спеціалісти на ДОУ)

Насправді ні в ФААНГ ні в іншій компанії нема технологій, які тобі гарантуючь роботу хочаб найближчі 5-10 років. Єдина можливість працювати з одними й тими же технологіями, це присісти на якісь довго граючий легасі і сподіватись що його за 10 років не викинуть чи не перепишуть.
Технології мінливі і судячи з кількістю скептиків мікросервісів, дуже вірогідно что ми вже маємо спадаючий тренд.

Технології мінливі і судячи з кількістю скептиків мікросервісів, дуже вірогідно что ми вже маємо спадаючий тренд.

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

відмовилися від мікросервісів на користь моноліту

Так

Я бачив проекти, де замовник відмовився далі продовжувати, та сказав переробояти мікросервісну архітектуру назад в моноліт, лишивши лише дуже грубі сервіси

Далі.

що більше схожі на купку неосиляторів, ніж на тих хто задає якісь тренди.

Нічого нового. Колись так С++ занепав, а до того Асемблер, бо «неасілятори» перейшли на Джави та Дот нети.

Так
Я бачив проекти, де замовник відмовився далі продовжувати, та сказав переробояти мікросервісну архітектуру назад в моноліт, лишивши лише дуже грубі сервіси

Дуже авторитетно:D

Йой, красивий аргумент на майбутню могилу мікросервісів.

зробити повний дублікат коду(для ізоляції як ви сказали потенційних breaking changes та уникнути регрессії на залежних компонентах)

Хто заважає у монолиті зробити так само, та через конфіг dependency injection нову реалізацію прокидувати тільки туди, де вона очікується (маркером чого буде новий API контракт у анотації @inject споживача)?

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

Ну от я тому й питаю про реальний досвід, шо у моєму досвіді це ніколи не було великою проблемою.

Хто заважає у монолиті зробити так само, та через конфіг dependency injection нову реалізацію прокидувати тільки туди, де вона очікується (маркером чого буде новий API контракт у анотації @inject споживача)?

Так само це як? у мікросервісах код будет лише одної версій білдидится(останньої), не буде дублікату коду автоматом.

У вас же дві імплементації у коді моноліту. Поки що я не бачу як ви робите це так само як у мікросервісах. Натомість якщо зостувати ваш підхід ви одразу отримаєте оце:

1. Потрібно думати як поєднати новую і стару версію коду — наприклад якщо там є спільна логіка, треба обрати серед наступних варіантів:
— зробити повний дублікат коду(для ізоляції як ви сказали потенційних breaking changes та уникнути регрессії на залежних компонентах)
— не робити дублікати і спільний код підключити в обидві версії і при їх зміні розуміти, що насправді ви комітите зміни не в одну версії, а у всі.
З мікросервісами цієї проблеми немає — ви просто змінюєте код і диплоїте новую версію поруч зі старою з чистим кодом(нова версія) і повністю гарантуючи незмінність попередньої версії(що у випадку сервісу, що у випадку пакету).
Я б сказав як мінімум з цього уже очевидно, що вартість буде різною.

2. Потрібно розуміти ваш компонент у моноліті не може ізольовано оновлювати свої залежності у разі потреби(будь-яка third-party ліба або пакет) — допустим ви пройшли этап архітектурного погодження з усіма оновлення залежностей(напишу на всяк випадок, щоб ми знову не зупинялись на цьому).
Тепер реальність буде такою, що треба оновити залежності для всього проекту одномоментно.
У вас не може бути дві версії залежності у коді на весь моноліт. Тобто ви навіть технічно не можете зберегти попередню версію без змін, оскільки оновленя залежностей потягне їх у всі версії.
Ви не можете цього зробити не домовившись про оновлення іншого комопоненту(якщо ліба і там викростовується) і не узгодивши таймлайни різних команд на розробку, котра дуже може бути для інших команд не буде нести ніякої business value, просто maintenance тех боргу.

Ну от я тому й питаю про реальний досвід, шо у моєму досвіді це ніколи не було великою проблемою.

ну у мене працює 6-7 scrum команд зі своїми пріорітитеми на спринт, коли технічні оновленя виконуються в рамках імплементації конкретної бізнесс фічі і не зачіпають тих, кому це не потрібно зараз — це доволі зручно скажу я вам :)
upd: особливо в контексті релізів — коли команди релізять незалежно як бекенд так і UI одне від одного. додаткові можливості, наприклад — кількість залучених команд скейлиться під розмір бюджету конкретного релізу, натомість замість мати їх константною і планувати додатку зайнятість через нефункціональну розробку. пріорітезувати релізні активності незалежно від інших інших команд, мати короткий цикл поставки, безпечно параллельно релізити hot-fixes.
монолітна розробка не може тут нічого конкуретного запропонувати взагалі по всим цим пунктам(ми власне з цих причин з перорієнтувались на мікросервісну розробку).

у мікросервісах код будет лише одної версій білдидится(останньої)
З мікросервісами цієї проблеми немає — ви просто змінюєте код і диплоїте новую версію поруч зі старою з чистим кодом(нова версія)

А ота стара імплементація, якою ще хтось користується — звідки буде деплоїтися у випадку, коли впав под (чи як там в вас це називається у Кубернетісі) з нею? Ну ок, припустимо, що вона зберігається у вигляді вже зібраного container image. Але, якщо там бага знайшлася у тій старий версії, як фіксити?

Ми ж ще про існування двух версій одного мікросервісу одночасно?

технічні оновленя виконуються в рамках імплементації конкретної бізнесс фічі і не зачіпають тих, кому це не потрібно зараз

Я навмисне виділив жирним шрифтом, бо мені важко уявити, як імплементація бізнес-фічі потребує оновлення системних архитектурних (!) залежностей, таких, як сериалізація, security, логування, та навіть й ORM (ми ж саме про такі залежності говорили вище). Бо якщо це якась дуже специалізована залежність для конкретної бізнес-фічі (ну, наприклад, платежі через Stripe), то її оновлення навряд чи буде чіпати когось навіть у моноліті.

А ота стара імплементація, якою ще хтось користується — звідки буде деплоїтися у випадку, коли впав под (чи як там в вас це називається у Кубернетісі) з нею? Ну ок, припустимо, що вона зберігається у вигляді вже зібраного container image. Але, якщо там бага знайшлася у тій старий версії, як фіксити?

Взяти потрібну версію коду із source control по бранчі/тегу, зробити хотфікс і продиплоїти з тими yml конфігами що лежать поруч з кодом і потрібні для відповідної версії ресурсів, коду , спеки контейнеру і відповідною версією ci/cd пайплану.

Я навмисне виділив жирним шрифтом, бо мені важко уявити, як імплементація бізнес-фічі потребує оновлення системних архитектурних (!) залежностей, таких, як сериалізація, security, логування, та навіть й ORM (ми ж саме про такі залежності говорили вище). Бо якщо це якась дуже специалізована залежність для конкретної бізнес-фічі (ну, наприклад, платежі через Stripe), то її оновлення навряд чи буде чіпати когось навіть у моноліті.

Ну элементарно — я ж навіть приклад вам вище давав, якщо ви уважно читали

Комусь треба почати апгрейдити у рамках свої вимог, наприклад юзати якусь специфічний атрибут з токену і задеприкейтити існуючий. А ти такий приходиш і кажеш — всі в наступному спринті зупинємо розробку, бо треба переробити авторизацію данних і задепрекейтити підходи старі і перейти на нові, або нічого не робимо зі змін чекаємо наступного технічного спринта для всіх команд? Бо інакше анархія\зоопарк , я вірно розумію?

Серіалізація — наприклад потрібно засеріалізувати специфічним чином якусь коллекцію складну — map[string, map] І треба кастомний конвертер писати на рівні двох компонентів залежних.
Orm — кастомний маппінг того ж типу на реляційне представлення.
Зазвичай це у моноліті буде в якісь обгортці на відповідних бібліотеках, або окремому компоненті, що треба оновлювати після таких змін. Якщо справа за оновлення прямої залежності вона може оновлюватися рідше, залежності від потреб динаміки розвитку, lts політики вендора, але всі ці фактори будут форсити оновлюватися — і навіть оновленя раз на рік такі массовані може бути нетривіально робити для усіх команд одномоментно, якщо це розвиваючийся проект, а не support.

у випадку моноліту (компонентного чи ні — не важливо) додаток в цілому.

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

В принципе, по итогу разработки на микросервисах — получается всё тот же монолит, но разбитый на модули не для одного компа, а на модули для разных компов. C соответствующими проблемами 1) деплоймента 2) обеспечения отказоустойчивости 3) отладки 4) быстродействия ( в сравнении с монолитом).
С другой стороны, меньше проблем с маштабированием — возможно, именно ради этого, жертвуют всеми преимуществами традиционного монолита.

P.S. Отдельно нужно добавить, что в связи с нынешними «зелёными веяниями» — микросервисные архитектуры маст дай. Возможно, довольно скоро.

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

pbs.twimg.com/...​E8gagyWWEAcYzUR.jpg:large

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

Копонент моноліту який незалежно розробляється, дебажиться, іще й білдиться виходить.
Чим же він тоді від мікросервісу відрізняєтсья? :)

Чим же він тоді від мікросервісу відрізняєтсья?

Микросервис — это «single-responsibility» модуль/сервис. И это касается всего, вплоть до работы с БД (например, когда отделъный микросервис делается под работу с отдельной таблицей).

Но сборище микросервисов — это «распределённый монилит».

Что касается отдельного компонента монолита — его вполне можно рассматривать, как микросервис тоже. Разница лишь в том, что все компоненты монолита крутятся на одном компе, в одном адресном пространстве, зачастую являются не сервисами (т.е. не самостоятельными процессами), а библиотеками. Это делает все вещи легче и проще, кроме масштабирования.

C соответствующими проблемами 1) деплоймента 2) обеспечения отказоустойчивости 3) отладки 4) быстродействия ( в сравнении с монолитом).

Именно эту мысль я пытаюсь донести до оппонента. Что за преимущества микросервисной архитектуры приходится платить, что она повышает total cost of ownership.

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

Последнее преимущество, кстати, тоже не без коварства — допустим, «свой» микросервис собрать и запустить проще, а поднять все зависимости к нему? Или, предполагается, что на машине разработчика микросервис отлаживается и тестируется строго в изоляции, а его интеграция с остальной системой происходит где-то ещё? А как, тогда, отлаживать интеграционные баги?

А зоопарк версий внезапно обрубается сканером уязвимостей, когда деплой блокируется.

А как, тогда, отлаживать интеграционные баги?

каждый микрос пишет в логи — что пришло, что ушло
бродим по логам, или сводим их в единый.
был недавно в одном проекте где почти все в AWS — там в логах и живешь, больше чем в IDE :)

интеграция в единую систему — точно больней получается чем в монолите. по коду уже ничего не видно.

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

Єдине що не зможе дати моноліт

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

Микросервисы это GoTo нашего времени. В 70х годах людям было скучно, писали много гоуту в коде, строили стейт машины которые роутили свое выполнение запутаной лапшой. Потом от гоуту избавились, пришло же одному умному человеку в голову здравая мысль . Но потом опять разработчикам стало скучно. Опять пуляют управление в асинхронной лапше через отправку сообщений. Это технологический тупик. Со временем скейлинг и нагрузки останутся. А вот микросервисы, именно как стиль написания апликейшенов, займет свое почетное место возле GoTo.

Абсолютно підтримую. Більшості інформаційних систем мікросервіси так само корисні як коню 5-6 додаткових ніг, і на кожній нозі різна версія копита. І кілька ніг заскейлені в 3 поди просто тому що так можна.

Цілком вірно, можна побудувати мікросервісний додаток без подій, за допомогою REST API, наприклад. Але в такому випадку сервicb будуть сильніше пов’язані між собою, а це погано вплине на їхню ізольованість і як наслідок можливість автономної розробки.

Насправді Microservice architecture != Event driven architecture.

 100%!!!

Дякую за питання. Тут немає однозначної відповіді, все залежить від багатьох факторів.Я думаю, що зрештою все упирається у фінанси. Що таке фінанси у сучасному ІТ?
1) Вартість інфраструктури (наприклад, у хмарі)
2) Вартість оплати послуг айтішників
Тут просто можна порахувати, що коштує дешевше — розробка проекту як моноліту або мікросервісів і зробити відповідні висновки.

Цікаво чи оцінював хтось з якого моменту переваги мікросервісної архітектури перевищують її недоліки

Коли ти стаєш амазоном. Якщо ти не амазон — мікросервіси це оверінжинірінг, карго-культ і їх недоліки просто космічно перважують їх ілюзорні переваги.

Цікаво чи оцінював хтось з якого моменту переваги мікросервісної архітектури перевищують її недоліки, у порівнянні з монолітом.

Якщо під конкретну задачу уже хтось інший завчасно заімлементував усе що необхідно

Ви не описали як все ж таки досягти атомарності при розподіленій транзакції в event-driven підході. Взяти ваш приклад; платіжний сервіс отримав коллбек що платіж отримано і кинув івент PaymentSuccessEvent. Його Успішно обробили підписані на нього сервіс нотифікацій (надіслав повідомлення) і квитків (обновив статус замовлення), але в сервісі поїздок сталася колізія і він не може успішно обобити повідомлення. Виходить транзакція не атомарна і система в неконсистентному стані. Як правильно в такому випадку ревертнути транзакцію?

Можно сделать «предварительно успешный» и потом уже «таки да — успешный». Тогда откат — равно снятие «предварительно успешного».

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

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

Дивіться saga pattern + compensating events. Розподілені транзакціі взагалі цікава тема. Також повязано з хореографією та оркестрацію мікросервісів microservices.io/patterns/data/saga.html

saga pattern + compensating events

все вірно, от тільки на практиці реалізація саги перетворюється на побудову підводно-підземного танка-літака з вбудованою функцією виходу в космос.

Дякую за питання. Розподілені транзакції — технологічно складне питання, котрому потрібна окрема стаття.
Але на базовому рівні, якщо у нас у сервісі поїздок трапилася колізія, то, очевидно, потрібно відкотити всю транзакцію.
Для цього добре підходить патерн Сага (у якого є дві реалізація — оркестрація та хореографія).
Але в обох випадках генерується нова подія (щось типу TripReservationFailureEvent), яка може бути оброблена різними способами.
У найпростішому випадку — просто повернути гроші. Або, наприклад, запропонувати користувачеві перебронювати квиток на інший день (рейс).

як все ж таки досягти атомарності при розподіленій транзакції в event-driven підході.

Ніяк.
В розподіленому середовищі атомарність доволі ілюзорна річ.

Як правильно в такому випадку ревертнути транзакцію?

3-5 синьорів, 2 девопси, 2 тестери, півроку. Якщо на мікросервісах і івент-драйвен.

1 мідл і півдня якщо моноліт.

Ніяк.
В розподіленому середовищі атомарність доволі ілюзорна річ.

Це твердження справедливе тільки у мікро-сервісному середовищі

3-5 синьорів, 2 девопси, 2 тестери, півроку. Якщо на мікросервісах і івент-драйвен.

Якщо івент-драйвен без мікросервісів то тверждення не справедливе.

Яка різниця? Amqp і подібне, на відміну від http, stateful протокол — він завжди пам‘ятає стейт операції, навіть якщо вирубили світло в дц, просто месседж теж комітится має ttl, дедлеттериться, має транзакційний send messages, exactly-once гарантію і відкачується окремим хендлером. Ця штука з точки зори атомарності розподіленої операції безвідмовна у будь якій конфігурації.

Ніяк.
В розподіленому середовищі атомарність доволі ілюзорна річ.

Одразу видно глибоке розуміння питання. Як там 2PC/ACP до речі, який описанний і успішно використовується з 80х років минулого сторіччя, теж всьо?

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

Для чого тоді Isolation в ACID — якщо атомарність покраває і обмеження на доступ до проміжного/незакоміченого стейту?

Это два взаимосвязанных механизма. Стандалон СУБД задизайнены так, что выполняют операции атомарно (транзакционно) при этом излолируя свое промежуточное состояние от других на время выполнения такой операции. Распределенные системы, в большинстве своем, так себя не ведут. Операции на нескольких узлах делают не атомарно и свое промежуточное состояние не пытаются скрывать от внешних клиентов. Хотя у внешних клиентов все же есть средства узнать, операция в состоянии комита или отката. Поэтому возвращаясь к изначальному посту, таки

В розподіленому середовищі атомарність доволі ілюзорна річ.
взаимосвязанных механизма

Ок, а чим відрізняються?

Atomicity — нікто не відіт промежуточное состояние?
Isolation — ???

Там написано не

Atomicity

а

Atomicity Operation (атомарная операция)

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

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

а не лінгвістику.

Так о этом и речь. Сначала понимание, а потом уже все эти аббревиатуры. А то некоторые сыпят аббревиатурами, а понимание както запаздывает. Кстате с тойже оперы и Consistency. Там где нет Atomicity и Isolation нет и Consistency поскольку систему могут увидеть в промежуточном неконсистентном состоянии.

Так ви поясните в чому різниця між
Atomicity
Isolation
В аббревіатурі ACID?

Так ви поясните в чому різниця між

В лингвистике
Атомарная операция = Atomicity + Isolation + Consistency

Не встречал СУБД где из этой тройки реализовано чтото одно

Atomicity — это понятно, операция либо выполняется как единое целое, либо не выполняется полностью
Isolation — как могут видеть состояние базы в процессе выполнения транзакции. Если как описывает DNA Exp — это полная изоляция и никто не может видеть систему в неконсистемном состоянии. Достигается это read-write блокировками на базе. На практике в высоконкурентных сценариях это приводит к тормозам, поэтому понятие Isolation ослабляют. Вводят isolation levels

Підписатись на коментарі