Еволюція мікросервісної архітектури в Conversations API

Мене звуть Олександр Маліновський, працюю техлідом в компанії Twilio в продукті Conversations API. Двома словами про продукт: це SaaS, який дає змогу використовувати різні типи повідомлень в одному каналі — тобто чат-користувачі та смс-користувачі можуть обмінюватися повідомленнями так, ніби це один і той самий канал.

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

Причини зміни архітектури

В далекому 2021, коли COVID був у самому розпалі, Twilio був одним з бенефіціарів локдауну, тому що багато бізнесів перейшли в онлайн і їм треба було спілкуватися з кастомерами. Conversations-api також отримав буст по навантаженню і менеджмент почав думати як «to keep momentum going»... Треба було додавати більше нових фіч, тому треба було більше людей.

На початок пандемії над продуктом працювало 4 команди: 2 команди по 4-5 осіб працювали над бекендом, ще одна команда — над сервісами синхронізації даних на SDK та самим протоколом, і ще одна — над SDK для різних платформ (javascript/android/ios). Оскільки це продуктова компанія, то доволі багато часу йде на KTLO (keep the lights on) тому для всіх нових фіч людей не вистачало, і було прийняте рішення додати нову бекенд-команду.

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

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

Нова архітектура і як ми на неї переходили

Перш ніж ми перейдемо до архітектури, хочу додати трохи контексту:

  1. Twilio використовує «single pizza team» підхід. Тобто всі команди не великі за розміром — десь 5-6 розробників та 2 менеджери (з розробки та продуктовий) і також кожна команда підтримує якийсь набір сервісів, які виконують конкретні функції. Як таких великих сервісів не повинно бути, тому що тоді декілька команд будуть відповідати за одну кодову базу, що суперечить company policy.
  2. Коли цей продукт тільки створювався, замість 2 бекенд-команд була одна, яка потім розділилася на дві. Оскільки команда була одна треба було додавати функціональність «на вчора» і самим ефективним і простим підходом було використання якомога меншої кількості сервісів. Одним з основних сервісів був «chat service», який як раз містив у собі всю основну бізнес-функціональність продукту. В процесі еволюції команда розділилася на дві, і нова команда для своїх функцій почала створювати нові сервіси (там, де це мало сенс), а от стара оперувала chat service. Позаяк chat service мав у собі доволі багато функціональності, і в процесі майже кожній команді треба було робити в ньому зміни, то і розбивати вирішили в першу чергу його.

Так виглядають сервіси, які наша команда підтримує:

За SDK synchronisation layer, а також SDK network layer (та код самих SDK) відповідають дві інші команди з мого продукту. Twilio Communication Services це частково внутрішні сервіси Twilio, які ми використовуємо, і частково сервіси, які розробляє інша бекенд-команда (в основному для інтеграції з різними messaging каналами (GBM, Whatsapp, Email etc)).

Окрім Chat Service, в якому сконцентрована основна бізнес-логіка та Media Service, який відповідає за все, що повʼязано з медіаконтентом, в нас ще є Data Cleanup Service який відповідає за видалення даних, яке можна зробити асинхронно, щоб мінімізувати час затримки.

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

  1. Використання хмарних технологій. Це доволі очевидний пункт — коли ресурси в хмарі, то доволі легко додати ще. Крім цього, хмарні провайдери надають додаткові сервіси (різні типи черг, моніторингу, бази даних та багато іншого), а також гнучкість у налаштуваннях.
  2. Paved path при створенні нових java (та не тільки) сервісів. Це свого роду spring boot starter, але на максималках — окрім того, що за допомогою декількох команд створюється вся структура проєкту, також додаються базові графи в DataDog, налаштування моніторингу, алертів (наприклад, коли CPU вище допустимих значень), Bigquery для логів, Rollbar для помилок у логах, CI/CD пайплайни та багато іншого, що зазвичай забирає час на налаштування, але дуже легко автоматизується.

Архітектурні дискусії

Якщо подивитися різні доповіді на тему міграції на мікросервісну архітектуру, то зазвичай перше, що роблять спікери — розбивають сервіси за принципом доменних сутностей, тобто роблять User service, Message Service, Channel Service і так далі.

Ми вирішили спробувати цю тактику для нашого сервісу, як найочевиднішу й просту у виконанні. Ось що вийшло:

Тобто ми створюємо новий сервіс SDK gateway, який грає роль backend for frontend для SDK і по факту він оркеструє запити, якщо треба зробити декілька на різні сервіси. Також для оновлення між сервісами ми планували використовувати kafka, наприклад, при видаленні каналу Message Service та Participant Service отримують івент і роблять необхідні видалення для своїх баз.

В процесі дискусій ми виявили деякі проблеми з цим підходом. По-перше, все одно в деяких ситуаціях схема запитів виглядатиме так:

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

Така ситуація виникає і для інших сервісів, а коли після розбивки на мікросервіси у вас з’являються тісні звʼязки між сервісами, то це своєрідний дзвіночок, що щось не так. По факту це «distributed monolith», що в плані мінусів набагато гірше.

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

Після цього ми подумали: «А чому б не спробувати розбити на сервіси на основі функцій?».

Функції які були реалізовані в Chat Service:

  1. Менеджмент та відправка пуш-повідомлень.
  2. Менеджмент та виклик вебхуків.
  3. Підрахунок та зберігання інформації, необхідної для платежів.
  4. Протоколи для SDK та перевірка авторизації для функцій.
  5. Логіка, повʼязана с підтримкою різних каналів звʼязку і виклик необхідних внутрішніх Twilio API.
  6. CRUD операції для основних доменних сутностей, таких як канал, користувач, повідомлення і синхронізація їх на SDK.

Якщо розбивати на сервіси за принципом функцій, то також має сенс торкнутися типів комунікацій між цими сервісами. З самих функцій можна зрозуміти, що деякі з них не потребують обробки у реальному часі, тому має сенс використати чергу повідомлень, яку б слухали ці сервіси. Є дуже цікава стаття від Мартина Фаулера про те, які розрізняють типи event driven architecture.

З того, що підходило нам, — це event carried state transfer. Фактично при цьому підході відправляється не просто повідомлення, що сталась подія, наприклад, хтось надіслав повідомлення у якийсь канал, а надсилається, так би мовити, «знімок» стану системи на момент відсилки повідомлення.

Це дає змогу сервісам мати всю необхідну інформацію та не робити додаткові виклики на сервіс, який ці події генерує. Важливий момент тут не забувати про ліміти ваших брокерів повідомлень, наприклад, для kafka це 1 MB «з коробки».

Декілька основних моментів з цієї діаграми:

  1. Колір сервісу — це фактично команда, яка за сервіс відповідає.
  2. Як видно — між conversations-webhook-service та core-data-service є як асинхронна комунікація, так і синхронна. Це тому, що є два типи вебхуків — PRE та POST, для POST неважлива синхронність виконання і тому вони виконуються на основі черги повідомлень. Для PRE це важливо тому, що фактично це валідація виконання функції на стороні кастомера.
  3. Окремо варто зазначити створення SDK gateway сервісу. Було вирішено, що якраз SDK команда буде відповідати за нього і він містить в собі логіку різних протоколів, а також валідацію і авторизацію.

Які ми виявили плюси та мінуси цієї переробки:

Плюси

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

Мінуси

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

На завершення

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

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

👍ПодобаєтьсяСподобалось14
До обраногоВ обраному9
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

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

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

Напишіть! Цікаво звичайно, ми ж усі з системами в продакшені працюємо, які не можна зупинити на кілька місяців, доки все переписується(:.

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

Чи є CQRS, та відповідно що є write store’м ?
Кост на динамі жахливий, тому часті зїжають на ScyllaDB +/- PostgreSQL тощо.

Що думаєте за Scylla ?
Чи розглядаєте CDC баз данних як джерело подій для ES ?

мабуть правильний топік для запитання: як обрати між event sourcing та event carried state transfer?
зараз ми в івенті шлемо увесь об’єкт та додаємо інфу про операцію, але у нас фактично мінімальна інфа про операцію — таки собі CRUD event
а вже консьюмер може порахувати diff коли отримає updated івент, якщо йому то потрібно і зробити якусь дію.

з плюсів:
1. просто дизайнити івент
2. не треба морочитися зі сховищем івентів, часто вам завжди потрібен актуальній стейт об’єкту

з мінусів:
1. консьюмеру треба вираховувати diff
2. може бути потенційна проблема, що не можна однозначно трактувати зміни.
3. більший об’єм івенту
4. не підходить для об’ектів у котрих є зв’язок на інші великі об’єкти або списки об’єктів

дякую

З нашого досвіду чому ми обрали саме event carried state transfer:
1. Не треба репліціювати багато данних на самих консьюмерах що робить їх менш вразливими до потенційних інцідентів повʼязаних з кафкою або їх власним стораджем.
2. Більший обʼєм івенту або списки дійсно є проблемою але ми в таких випадках кладемо їх на s3 і в пейлоал кладемо лінку на s3.

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

так, ви правильно все зрозуміли. Також є цікава стаття де такі компанії як Airbnb чи Twitter також розмірковують над своїми мікросервісами — thenewstack.io/...​icroservice-complexities.

івенти дозволяють послабити зв’язність системи, прискорити час відповіді — частину роботи можна «відкласти» і зробити в фоні.
+ асинхронна обробка без івентів ніяк, черги дозволяють регулювати навантаження на сервіси.

ще дужа важлива характеристика івентів в нашій ситуації це backpressure. Наприклад кастмер відсилає 10к смс в секунду але телефонний оператор може обробити лише 1к і якщо його затротлити то він просто перестане відповідати взагалі.

Кажется у Фаулера есть объяснение на все, что может придумать девелопер.
Теперь буду всем рассказывать, что мы используем — event carried state transfer

це точно, в нашому випадку також спочатку була імплеменція, а потім ми зрозуміли яким терміном це можна назвати:)

Так, але майже ніхто не пояснює релевантність CDC та примітивів транзакцій, разом з CRDT.
т.к. часто Event-driven State Transfer може, наприклад, відбутись у модель Persistent Actors.

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