Що таке архітектура додатка?
Архітектура додатка – це спосіб організації компонентів програмного продукту, який визначає, як частини взаємодіють між собою і з користувачем.
Правильна архітектура дозволяє створювати додатки, які легко масштабуються, тестуються та підтримуються. Вона допомагає уникати технічного боргу і забезпечує стабільну роботу проєкту навіть при зростанні навантаження або змінах у команді. Покроковий посібник для тімлідів-початківців та архітекторів.
Як спроектувати архітектуру додатка з нуля
Досвідчені розробники знають, що будь-яку систему – застосунок чи сервіс – спершу необхідно спроєктувати й описати. І тільки потім можна відкривати редактор коду і переходити до реалізації.
Але далеко не всі роблять саме так. Навпаки, багато команд одразу розпочинають розробку на звичному технологічному стеку, маючи лише слабкий начерк того, який вигляд має мати продукт.
Навіщо проєктувати застосунок
Може виникнути цілком резонне запитання: а навіщо взагалі витрачати час на продумування і докладний опис архітектури?
Якщо пропустити цей крок, то ви ризикуєте зіткнутися з двома вкрай неприємними наслідками: результат вашої роботи не відповідатиме очікуванням замовника і, цілком імовірно, в майбутньому вам доведеться витратити багато сил на рефакторинг.
Образ застосунку в голові розробника може не збігатися з уявленнями продуктового менеджера або власника продукту. Може з’ясуватися, що ваше рішення не закриває ключові потреби користувачів, або закриває не так, як треба, або не відповідає інфраструктурі компанії. У результаті доведеться вносити безліч змін, а може, і зовсім переробляти проєкт.
Коли людина будує складні системи в голові, то часто не бере до уваги суттєві деталі та зв’язки між компонентами. Вони можуть не витримувати навантажень, погано масштабуватися. Найімовірніше, проблеми розкриються ще на етапі тестування і код доведеться рефакторити до або одразу після здачі проєкту.
Тому завжди описуйте структуру застосунку, його компоненти та взаємодію між ними в окремому документі. Такий опис потрібно узгоджувати з колегами і замовниками – це дає змогу синхронізувати бачення проєкту і переконатися в тому, що нічого важливого не було упущено.
У цій статті ми складемо й опишемо логіку роботи застосунку, який працюватиме всередині наявної мікросервісної архітектури. Пройдемо всі етапи проєктування: від збору вимог до інтеграції нового проєкту з іншими продуктами в компанії.
З чого почати?
Насамперед необхідно зібрати повну інформацію про продукт, що розробляється. Для цього потрібна людина, яка більше за інших у нього занурена. Зазвичай це продуктовий менеджер або власник продукту.
Збір функціональних вимог
Збираємо детальний опис продукту з точки зору бізнесу:
- які бізнес-цілі ми ставимо перед сервісом. Наприклад, збільшення частки компанії на наявному ринку;
- які потреби бізнесу він закриває. Наприклад, продукт може бути спрямований на максимізацію прибутку, а може – на підвищення лояльності клієнтів;
- які вимоги до продукту мають користувачі;
- які функціональні вимоги та обмеження існують з боку компанії;
- які залежності від інших систем варто врахувати.
Будьте готові до того, що замовник не відразу відповість на всі запитання, а відповіді, найімовірніше, будуть поверхневими. Тому не соромтеся ставити уточнювальні запитання, а за необхідності домовляйтеся на нову зустріч, давши співрозмовнику час на підготовку.
Ваше завдання на цьому етапі – отримати максимально можливу кількість інформації про продукт і зібрати її в один структурований документ. Він нам ще знадобиться на наступних етапах.
Визначення та опис нефункціональних вимог
Після того як усю доступну інформацію буде зібрано, необхідно продумати нефункціональні вимоги. До них відносять аспекти роботи застосунку, які безпосередньо не впливають на бізнес-показники, але стосуються користувачів і тих, хто підтримуватиме систему.
Нам потрібно буде подумати про такі фактори:
- доступність;
- змінюваність;
- продуктивність;
- безпека;
- тестованість;
- зручність використання;
- розширюваність;
- підтримуваність;
- взаємодія з іншими системами.
Розберемо детально кожен пункт.
Доступність
Це період часу, протягом якого система функціонує без збоїв. Ідеальне значення – 99,99%. Усі прагнуть до цієї цифри. Адже якщо компанія не хоче втрачати користувачів, застосунок має працювати в режимі 24/7.
Змінність
Наскільки легко ми зможемо вносити коригування в продукт або його частину, не впливаючи при цьому на решту функціональності. Інакше кажучи, систему треба проєктувати так, щоб модифікація тієї чи іншої частини коду не торкнулася інших частин системи.
Продуктивність
Одна з найзрозуміліших характеристик, яка показує, яке навантаження може витримати система і не піти у відмову. Орієнтуйтеся не на середньостатистичні, а на граничні показники навантаження.
Щоб отримати значення, яке здатна витримувати ваша інфраструктура, потрібно до цих екстремальних показників додати ще приблизно 20%.
Безпека
У системи не повинно бути вразливостей. Орієнтуйтеся на список OWASP Top 10 і консультуйтеся з фахівцями з інформаційної безпеки, якщо такі є в компанії.
Тестованість
Система вважається тестованою, якщо вона легко «віддає» свої помилки. Тобто їх легко виявити стандартними способами, наприклад відладчиком або за допомогою автоматизованого тестування.
Чим складніше тестувати продукт, тим дорожче його підтримка обходитиметься бізнесу.
Зручність використання
Наскільки легко користувачеві виконувати свої завдання і як швидко він може знайти розв’язання проблеми, що виникла, в документації або завдяки службі підтримки.
Очевидно, що чим вищий рівень зручності, тим краще.
Розширюваність
Замовник може вирішити через рік додати в застосунок нові фічі для користувача або для команди підтримки.
Під час їхнього розроблення та впровадження старий код не повинен заважати цьому.
Підтримуваність
Робота над продуктом не закінчується після приймання – його необхідно підтримувати, причому не тільки код, а й серверну архітектуру. Тому система має бути зручною для тих, хто буде її використовувати надалі. Наприклад, якщо розробники в компанії працюють на певному стеку технологій, то бажано, щоб продукт був написаний на ньому.
Взаємодія з іншими системами
Оскільки сервіс розробляється всередині наявної мікросервісної архітектури, то важливо забезпечити його взаємодію з іншими компонентами системи. Це необхідно заздалегідь продумати, оскільки наявні системи ніхто не буде підлаштовувати під ті, що розробляються.
У будь-якій компанії паралельно йде розробка десятків сервісів, тому спроби вносити зміни в наявний код для їхнього запуску призведуть до хаосу. Тому компанії проєктують мікросервіси, підлаштовуючи нові під ті, що вже працюють, а не навпаки.
Усі перераховані фактори необхідно розписати для створюваної системи й оформити в тому самому документі, що й функціональні вимоги, зібрані від замовника на попередньому кроці. Тепер загальний опис можна передати на узгодження замовнику.
Технічні деталі реалізації застосунку
Після того як усі вимоги узгодять, архітектор або розробник може бути впевнений, що його уявлення про продукт збігається з баченням замовника. Тепер можна занурюватися в технічні деталі.
Створення архітектури застосунку розберемо на прикладі сервісу для великого забудовника, який зводить елітні житлові комплекси по всій Україні та за кордоном. Усі вони об’єднані в загальну IT-інфраструктуру.
Для нього ми будемо створювати сервіс для оплати послуг. Може здатися, що це буде легко. Але це не так – чим більше запитань ставитиме архітектор, тим більше важливих нюансів він дізнається.
На першому етапі спілкування із замовником розробник з’ясував, що у компанії тисячі житлових комплексів і котеджних селищ. Їхні мешканці оплачують послуги ЖКХ, телебачення, інтернет, вивезення сміття тощо. Важливо, що це не разові платежі, а постійні, що здійснюються в різний час доби.
Керуюча компанія кожного комплексу – окрема юридична особа, не пов’язана з організацією, де працює команда розробника.
Частина платежів від КК приходить в інший мікросервіс компанії, який обробляє їх і складає дані про проведені транзакції в DWH. Це перше джерело інформації.
Друге джерело даних – звіти від керуючих компаній. Важливий нюанс у тому, що керуючі компанії надають дані в різних форматах. У когось це CSV-файли, що надсилаються на поштову скриньку, у когось це JSON, що передається через API тощо.
Завдання розроблюваного мікросервісу в тому, щоб звіряти дані з DWH з даними, які надають керуючі компанії. Це потрібно для того, щоб контролювати керуючі компанії та внутрішні системи на предмет збоїв і помилок.
Розробник припускає, що користувачами системи будуть співробітники його організації, яких ми далі називатимемо саппортами, і їм знадобиться адмінка для відстеження даних. Вона буде знаходитися в окремому мікросервісі і зможе взаємодіяти зі створюваним додатком за API.
У логіці застосунку з’явилися нові дійові особи – саппорти. Розробник, який виконує роль архітектора, має обговорити з ними продукт і зібрати інформацію про те, які функції та аспекти роботи застосунку вони вважають необхідними.
Паралельно можна дізнатися у саппортів, як вони взаємодіють з керуючими компаніями зараз. Наприклад, може з’ясуватися, що вони в ручному режимі звіряють Excel-файли із самописними макросами, які перейшли до них у спадок від попередніх співробітників.
Підсумуймо всі вимоги та умови, які зміг з’ясувати розробник до цього моменту:
- мікросервіс має обмінюватися даними з DWH. Для цього можна використовувати інтеграцію через API;
- звіти надаються в різних форматах і різним «транспортом»;
- керуючі компанії запускають звірку в різний час доби;
- розрахункові навантаження на систему на одну звірку становлять від 1000 до 500 000 операцій. Цифри отримано після обговорення сервісу із саппортами;
- поточна кількість звірок – до 40 на день;
- у деяких керуючих компаній є окремий підвид фінансових операцій, які повинні оброблятися разом з іншими операціями з однієї транзакції;
- звіти надаються подобово з даними за попередній день;
- деякі керуючі компанії не надсилають звіти у вихідні та святкові дні. Тому їхні звіти в понеділок містять дані за кілька днів;
- дані у звітах розподіляються не рівно подобово, а приблизно подобово. Тобто не вийде робити вибірки рівно з 00:00:00 до 23:59:59. Це пов’язано з тим, що керуючі компанії працюють у різних часових зонах і мають різний час закриття бізнес-дня;
- часто керуючі компанії не надсилають звіти вчасно.
Також вдалося сформувати низку технічних вимог до продукту:
- код має бути покритий автотестами не менше ніж на 98%;
- потрібна система моніторингу;
- потрібна система логування.
Від цих даних уже можна відштовхуватися і переходити до проєктування.
Стек технологій і начерк архітектури
Найкращий спосіб візуалізувати внутрішній устрій продукту – представити його у вигляді діаграми. Але, перш ніж це зробити, потрібно визначити основний технологічний стек.
Припустимо, що в команді є два PHP-розробники, один фронтенд-розробник і сам сеньйор-розробник (архітектор або тімлід), який займається проектуванням. Оскільки він не хоче збирати нову команду розробки під новий технологічний стек, то використовуватиме звичний для команди з актуальною версією PHP 8.2.
Що далі? Обмежень щодо використання фреймворків немає, а отже, можна вибрати між Symfony і Laravel. Архітектору варто порадитися з командою, щоб краще зрозуміти їхні відмінності, переваги та недоліки. Додатково можна почитати аналітичні статті з порівнянням у профільних виданнях. Припустимо, що вирішили зупинитися на актуальній на поточний момент версії Symfony 6.4.
У проекті не обійтися без бази даних. Найпростіше використовувати реляційну БД, наприклад MySQL або Postgres. Перш ніж вибрати конкретне рішення, архітектор порівнює їх між собою і визначає, чи є в команді розробки фахівець із роботи з конкретною базою даних.
Для нового сервісу було обрано Percona Server for MySQL 8.0, оскільки з нею мають досвід роботи всі члени команди, а в адмінів є готові налаштовані пресети, які вони зможуть швидко розгорнути на стенд.
У нас сформувався базовий технологічний стек:
- PHP 8.2;
- Symfony 6.4;
- Percona Server for MySQL 8.0.
Тепер можна зробити перший начерк архітектури застосунку. Для цього підійде онлайн-сервіс draw.io або будь-який аналог.
Що ж є на діаграмі? Зліва зверху розташований DWH, що зберігає дані від компанії.
Праворуч на діаграмі вказано кілька інтеграцій з керуючими компаніями, які для стислості називатимемо просто інтеграціями. Також відзначена адмінка для управління сервісом, база даних і сам сервіс у центрі зображення.
Зараз це загальна діаграма, яка показує тільки те, як сервіс взаємодіє з іншими елементами. Але поки що незрозуміло, що собою являє він сам.
Сутності у роботі сервісу
Завдання сервісу, що розробляється, – звіряти пачки (чанки) операцій між DWH і даними від керуючих компаній щодоби або близько того. Якщо узагальнити, то за якийсь не повністю нормований інтервал, який частіше дорівнює добі і не перевищує трьох днів. Нагадаю, що деякі керуючі компанії не працюють у вихідні, тому їхні дані накопичуються протягом суботи, неділі та понеділка.
З огляду на завдання сервісу можна визначити, які ключові сутності він міститиме.
Відразу запишемо найочевидніші з них:
- DwhOperation – операція з боку DWH, якій має знайтися пара на стороні вендора – керуючої компанії через інтеграції;
- VendorOperation – операція з боку вендора;
- ReconciliationResult – результат звіряння конкретних операцій;
- ReconciliationTask – сутність, що відображає процес звіряння;
- ReconciliationCalendar – календар звіряння, що регламентуватиме святкові та вихідні дні різних компаній.
Назви сутностей мають бути зрозумілими і відображати те, що ця сутність робить. Поки що може бути складно зрозуміти їхнє призначення, але ми детально розберемо це далі.
Далі я наведу атрибути кожної сутності.
DwhOperation
- operationId – основний ідентифікатор операції на боці нашої компанії;
- transactionId – транзакція, до якої входить кілька операцій;
- amount – сума операції в заданих одиницях;
- currency – валюта проведення операції;
- createdAt – дата і час створення операції за UTC;
- vendorPaymentId – ідентифікатор операції на боці вендора;
- operationTypeId – тип операції: оплата, повернення, кешбек, комісія, списання за передплатою і так далі;
- operationStatusId – статус операції: success, decline,
VendorOperation
- vendorPaymentId – ідентифікатор операції на стороні вендора;
- amount – сума операції в заданих одиницях;
- currency – валюта проведення операції;
- createdAt – дата і час створення операції, наведений до UTC;
- operationTypeId – тип операції за версією вендора, який, найімовірніше, не збігатиметься з нашими типами;
- operationStatusId – статус операції за версією вендора, який, найімовірніше, не збігатиметься з нашими статусами.
ReconciliationResult
- dwhOperationId – ідентифікатор операції DWH;
- vendorOperationId – ідентифікатор операції вендора;
- reconciliationTaskId – ідентифікатор задачі звірки, до якого належить результат звірки;
- status – статус звірки.
ReconciliationTask
- id – ідентифікатор завдання звірки;
- reportDate – дата звіту;
- status – статус завдання звірки;
- vendor – назва керуючої компанії;
- createdAt – дата та час створення завдання звірки;
- updatedAt – дата та час оновлення завдання звірки;
- error – текст помилки, якщо така є.
ReconciliationCalendar
- id – ідентифікатор запису в календарі;
- date – дата;
- process – запускати процес чи ні (boolean-поле);
- vendor – назва вендора;
- createdAt – дата та час створення запису в календарі;
- updatedAt – дата та час оновлення запису в календарі.
Звідки взялися ці сутності й атрибути? Їх визначив розробник на основі всієї зібраної раніше інформації. На цьому етапі проектування потрібно увімкнути уяву і подумати про ті сутності, які описуватимуть бізнес-процеси продукту і події з реального світу.
Спробуємо узагальнити завдання. У компанії є операція на її стороні, яку необхідно звірити з операцією на стороні вендора і в результаті отримати якийсь результат звірки.
Це може бути інформація про те, що дані і результат операції у компанії і вендора повністю збігаються. Усе це відбуватиметься в процесі, який ми назвали завданням (task) звіряння, а для управління запуском цього процесу використовуватимемо календар звірок.
Поки що картина далека від готового продукту, оскільки розробник позначив тільки основні поняття навскидку. Тепер настав час додати деталізації та продумати, як відбуватиметься звірка.
Аналіз даних та стадії роботи
Наступний крок – подумати, як відбуватиметься процес звірки з технічного погляду. Зараз відома тільки відправна і кінцева точка, а що і як відбувається між ними – неясно. Щоб сервіс працював, це потрібно прописати.
Аналіз даних
Розібратися в тому, як відбувається процес звірки, допоможуть правильні запитання:
- Як він буде запускатися?
- За яким алгоритмом буде проходити процес звірки?
- Як потрібно робити вибірки даних для звірки?
- Який обсяг цих даних?
- Чи можна запустити паралельне опрацювання даних або їх потрібно обробляти в один потік?
- Як технічно відбуватиметься передача даних?
- Як працюватиме схема для вендорів, які не надіслали звіт вчасно?
- Як виділити інтерфейс для вендорів так, щоб реалізувати прості інтеграції з кожним із них?
- Як реалізувати систему розкладу звірок?
- Як реалізувати кастомізовані процеси звірок для компаній, у яких є транзакційна звірка?
Почати варто з глибокого вивчення даних. Можна спробувати звірити невеликий шматок вручну або написати найпростіший скрипт, який зробить це автоматично. Такий підхід допоможе виявити досить велику кількість цікавих деталей, які потрібно врахувати для побудови правильної архітектури нового застосунку.
Після аналізу архітектору стало зрозуміло, що потрібно зіставляти операції одна з одною за якоюсь властивістю. У поточній версії подій є лише одна властивість, за якою це можна зробити, – vendorPaymentId, оскільки цей ключ є і в операції DWH, і у звітах вендора. Тому вона буде ключем для зіставлення операцій.
Але відомо, що у багатьох вендорів це поле не є унікальним і може містити під собою кілька різних операцій, наприклад продаж і повернення. Тому ключ зіставлення слід зробити складовим – він міститиме в собі vendorPaymentId і тип операції.
Визначення варіантів сценаріїв звірки
Питання з ключами вирішено. Тепер можна розглянути сценарії звірки, які виникатимуть у процесі зіставлення операцій.
Які сценарії можуть бути:
- ми змогли підібрати операції, і всі їхні параметри повністю зійшлися;
- ми змогли підібрати операції, але розійшлася сума;
- ми змогли підібрати операції, але розійшлася валюта;
- ми змогли підібрати операції, але розійшлася як сума, так і валюта;
- ми знайшли операцію на боці DWH, але не знайшли на боці вендора даних;
- ми знайшли операцію на боці вендора даних, але не знайшли на боці DWH.
Відправною точкою звірки є звіт, оскільки в ньому дані вже якимось чином прив’язані до часового інтервалу. Оскільки даних може бути дуже багато, то проводити пошук поштучно не найкращий варіант. Це призведе до підвищеного навантаження на DWH і всю мережу загалом.
Отже, необхідно спочатку отримати дані від вендора і тільки потім з DWH, вибравши ті, які присутні у звіті керуючої компанії. Це можна зробити запитом за ключами зіставлення. Але якщо обмежитися тільки такою вибіркою, то не вийде закрити сценарій, коли операція знайшлася на стороні DWH, але не знайшлася на стороні вендора.
Виникає питання: як це врахувати? Найочевидніший спосіб – робити вибірку за часом, наприклад з дати і часу створення операції, звіреної останньою в попередньому таску звірки, і до дати і часу створення операції, звіреної останньою в поточному таску звірки. Таким чином ми зможемо вибрати всі операції з DWH, які підпадають під інтервал операцій у звіті вендора даних.
Але як це реалізувати? Необхідна стадійність звірки, оскільки деякі процеси всередині неї не можуть йти паралельно, а отже, їх потрібно запускати послідовно один за одним. При цьому кожна стадія спиратиметься на дані, отримані на попередній стадії.
Таким чином, у розробника-архітектора з’являється розуміння того, що у звірки повинні бути стадії, і результат роботи виходить тільки за підсумком проходження через кожну з них.
Відразу виникають нові запитання:
- Які це будуть стадії?
- Скільки їх буде?
- Як вони будуть взаємодіяти одна з одною?
- Який транспорт даних буде між стадіями?
Без відповідей на ці запитання йти далі не варто. Треба знову подумати і уявити логіку всього процесу.
Список стадій звірки
- initiated – стартова стадія. Запускається, коли стає зрозуміло, що необхідно щось звірити. На цій стадії створюється таск звірки, в який передаються необхідні для цього параметри: назва вендора, дата звірки тощо.
- get_vendor_operations – на цій стадії сервіс отримує і зберігає відповідь від вендора. Тут з’являється гарне запитання – куди ми все це зберігаємо? Повернемося до нього пізніше.
- get_dwh_operations – стадія отримання даних із DWH для звірки за ключами зіставлення. Стадії 2 і 3 можна було б об’єднати. Але в цьому разі стадія буде перевантажена через виконання двох логічних дій – одночасного отримання даних від вендора і з DWH. Оскільки варто дотримуватися принципів SOLID, то її краще розділити на дві.
- reconciliation_by_keys – звірка за ключами зіставлення. У цей момент сервіс отримує список перших результатів звірки, а також дату і час останньої, звіреної в поточному таску звірки, операції.
- get_dwh_operations_by_interval – отримання даних із DWH за інтервалом. Інтервал, як ми пам’ятаємо, визначається датою і часом операції, звіреної останньою в попередньому таску звірки, і датою і часом операції, звіреної останньою в поточному таску звірки. Цю дату сервіс виявив на попередній стадії.
- reconciliation_by_interval – звірка операцій за інтервалом.
- reconciliation_results_saving – збереження результатів звірки. Для спрощення вважатимемо, що результат звірки зберігається в DWH, а не в окреме сховище.
- finish_reconciliation – закриваємо всі процеси, формуємо дані для адмінки і завершуємо процес звірки.
Оскільки нам точно знадобиться API для роботи з адмінкою, то відразу заплануємо його наявність.
Схема стала більш повною, але поки залишаються відкриті питання. Наприклад, незрозуміло, як звірку запускатимуть, як здійснюватиметься транспорт даних, взаємодія класів тощо.
Побудова системи звірки
Перше питання, яке має поставити архітектор: як буде запускатися кожна звірка? Поки що невідомо, що буде для неї тригером і як це працюватиме в деталях.
Перш ніж знайти рішення, варто подумати про те, яким чином буде побудована робота застосунку загалом. Прояснити це допоможуть такі питання:
- Запуск відбуватиметься командою з інтерфейсу командного рядка?
- Чи можна буде керувати запуском з інтерфейсу?
- Кожна стадія має запускатися автоматично?
- Що буде перемикати стадії?
- На чому працюватиме стадійність – на кронах чи на чергах або, можливо, якимось іншим способом?
Припустимо, у нашому прикладі розробник відмовився від ручного запуску і хоче, щоб усе відбувалося автоматично. Тобто можливості керувати стадіями через інтерфейс не буде.
Отже, залишається вибрати, на якому підході буде побудована система стадій і вся звірочна лінія загалом. Архітектор вирішує розглянути три можливі варіанти реалізації.
Варіант 1
Зробити застосунок лінійним і таким, що запускається по крону, в одному процесі
Переваги:
- проста реалізація;
- найкоротші терміни розробки.
Недоліки:
- низька продуктивність;
- якщо в системі щось впаде, то впаде і весь процес без можливості відновлення;
- важка підтримка – довгі процеси складніше моніторити;
- відсутність масштабованості;
- відсутність можливості перезапуску конкретної стадії;
- розклад запусків доведеться зберігати в операційній системі. Це погано, оскільки розклад належить до бізнес-
- логіки самого застосунку;
- алокація великої кількості ресурсів сервера на довгий термін.
Варіант 2
Зробити додаток на чергах із запуском з-під супервізора
Переваги:
- алокація ресурсів сервера на короткі проміжки часу;
- масштабованість;
- можливість перезапуску стадій;
- організувати моніторинг простіше, ніж у попередньому варіанті.
Недоліки:
- складніша реалізація порівняно з варіантом із кронами;
- більший термін розробки.
Варіант 3
Зробити застосунок на чергах із стадіями, що запускаються паралельно
Перевага:
- Можемо потенційно виграти час і підвищити швидкість обробки даних. Але це не точно!
Недоліки:
- дуже складна реалізація;
- довга розробка;
- є питання до доцільності рішення для цього завдання.
Виходячи із запропонованих варіантів, архітектор робить висновок, що найкращий варіант другий – черги з супервізором.
Порядок запуску застосунку
Припустимо, що час запуску не потрібно вказувати з точністю до секунди, а отже, у налаштуваннях кожної інтеграції можна використовувати cron-подібний розклад, тобто такий, який виконує завдання у визначений час.
Тому в застосунку стане в пригоді якийсь клас, консольна команда, яка запускатиметься раз на хвилину, звірятиме годинник і аналізуватиме, виходячи з налаштувань інтеграцій і календаря ReconciliationCalendar, які звірки варто запустити в конкретну хвилину.
Найпростіше це зробити консольною командою, всередині якої буде логіка запуску з подальшим викликом функції sleep, яку згодом поставимо під супервізор. Назвемо цей клас ReconciliationStarter. Він створюватиме таски в чергу на обробку і таски для сутності ReconciliationTask.
Як буде влаштовано управління процесами стадій? Для цього створюється клас ReconciliationManager. Це демон, який обробляє свою власну чергу і перемикає стадії звірок після їхнього завершення.
Якщо архітектор подивиться на схему, то зрозуміє, що передбачив не всі поля сутностей. Необхідно додати нові:
ReconciliationResult.additionalInfo – поле для зберігання деталізованої інформації про знайдені розбіжності між операціями.
ReconciliationTask.attempts – лічильник кількості спроб звіритися. Він необхідний, оскільки низка вендорів може надсилати звіти не за розкладом, а із запізненням. У цьому разі потрібна механіка перезапуску окремої стадії за відкладеним таймером. Це якраз дає змогу реалізувати обраний варіант реалізації стадійності.
ReconciliationTask.duration – зберігатиме середнє значення часу роботи однієї звірки.
ReconciliationTask.lastOperationDt – поле для збереження дати та часу останньої звіреної операції в таску звірки в таймзоні UTC.
ReconciliationTask.comment – необов’язкове поле з коментарем від саппорта до тасків звірки.
Деталізація процесів
Залишається кілька важливих технічних та архітектурних запитань:
- Як відбуватиметься передача даних між стадіями?
- На чому мають бути реалізовані черги?
- Як обробляти різні формати звітів?
- Як обробляти специфічні випадки?
Розберемо питання по порядку, почавши з транспорту даних. Які технології доступні команді розробки? Що прийнято використовувати в компанії? З чим розробники з команди мають досвід роботи?
Уявімо, що після відповіді на ці запитання залишається два кандидати – це Memcached і Redis. Обидві технології доступні та використовуються в компанії, і з обома з них у команди є досвід роботи. Так яку ж обрати, якщо обидві такі хороші?
У таких випадках корисно дивитися на початкове призначення інструментів і їхні відмінності.
Memcached – високопродуктивний сервіс, працює швидко незалежно від кількості збережених даних. Швидше, ніж Redis. Можна сказати, що це сховище «ключ – значення», що підтримує атомарні операції. Довжина ключів може досягати 250 байт, а обсяг даних під одним ключем – 1 МБ.
Redis підтримує велику кількість типів даних. Він вміє періодично зберігати інформацію на жорсткий диск і дає змогу зберігати до 512 МБ даних у значеннях. Redis підтримує master-slave-реплікацію, і його можна використовувати як постійне сховище даних. Він однопотоковий.
Можливо, вибір тут буде не зовсім очевидний, але для цього застосунку Redis надлишковий, оскільки дані потрібно не зберігати на постійній основі, а тільки передавати між стадіями звірки. Тому архітектор обирає Memcached.
Наступне питання – черги. Тут теж є кілька варіантів – Kafka, Gearman і RabbitMQ.
Припустимо, що з Gearman у команді ніхто не працював і під нього потрібно писати клієнт з нуля, а це займе багато часу – тому він відпадає.
У Kafka велика пропускна здатність порівняно з RabbitMQ, і він легко масштабується. RabbitMQ вміє в складну маршрутизацію і підтримку різних протоколів – це його основні переваги, але в нашому застосунку вони не потрібні. Тому обираємо Kafka.
Завершення роботи
Необхідно розв’язати питання настроюваного опрацювання звітів і роботи з різними форматами даних, що надходять від вендорів. Нагадаємо, що ці дані надали саппорти на першому етапі збору інформації.
У пошуку відповідей допоможуть патерни проєктування та ООП.
Насамперед належить вирішити питання з різними форматами та іншими відмінностями між вендорами. При цьому процес звірки буде однаковим майже для всіх них і для більшості операцій. Розрізняються лише формати даних і деякі аспекти реалізації інтеграцій з вендорами.
Тому можна виділити базову та інтеграційну частини. Для інтеграційної частини потрібно оформити якийсь інтерфейс, з яким працюватиме базова частина. А в ній будуть розташовані операції звірки DwhOperation і VendorOperation.
Давайте спробуємо визначити інтерфейс для інтеграції – ReconciliationInterface:
- public function getVendorData(): VendorOperationCollection – метод передбачає, що інтеграція з вендором поверне колекцію об’єктів VendorOperation. Він буде викликатися на стадії get_vendor_operations.
- public function getVendorReconciliationKey(VendorOperation): string – цей метод дасть змогу діставати ключ зіставлення з будь-якого поля VendorOperation.
- public function canOperationsBeMatched(DwhOperation, VendorOperation): bool – метод визначає, чи можемо ми порівнювати операції, наприклад, за типом, якщо вони збіглися за ключем зіставлення.
- public function compareOperations(DwhOperation, VendorOperation): ReconciliationResult – метод, який звірятиме дані з двох операцій за їхніми властивостями.
Такий інтерфейс дає змогу парсити звіти в різних форматах і навіть звертатися за даними в API. Крім цього, можна використовувати різні ключі зіставлення для операцій і проводити додаткові перевірки для визначення фактичної можливості звірки двох операцій.
Окремо варто згадати про можливість звіряти операції в різних інтеграціях. Наприклад, в одній звірці ми звіряємо в операцій суму, номер гаманця і прізвище користувача, а в іншій – номер картки і валюту. Усе це реалізується за рахунок архітектури в одному контурі з підміною тільки частини коду.
Отже, на схемі з’явився ReconciliationInterface і одна нова стадія, яку архітектор забув відобразити раніше – це reconciliation_by_interval. Забути щось додати на діаграму дуже просто, тому корисно повторно проходити нею, повторюючи логіку застосунку.
Залишається одне невирішене питання – як дозволити проведення кастомізованих транзакційних звірок за умови необхідності звіряння декількох операцій?
Тут нам допоможе додатковий інтерфейс, який буде опціональний для реалізації, на відміну від ReconciliationInterface, під назвою CustomReconciliationInterface. У нього буде всього один метод:
public function reconcileOperations(DwhOperations, VendorOperations): ReconciliationResultCollection. Цей метод дасть змогу звіряти ті операції, які мають повністю кастомну функціональність звірки. У нього передаватимуться незвірені операції вендора і DWH, що залишилися, а на виході буде очікуватися колекція об’єктів ReconciliationResult.
Тепер потрібно визначитися з місцем його виклику. Найкраще передбачити для цього окрему стадію custom_reconciliation.
Перевіряємо повноцінність архітектури
Тепер схему завершено, вона відображає всю архітектуру застосунку. Але потрібно перевірити вихідний список завдань, які сформулював архітектор.
Необхідно налаштувати взаємозв’язок із DWH за допомогою інтеграції за API. Усередині програми буде створено клієнт або сервіс для зв’язку з DWH за API. Нюанси його реалізації в цій статті вказувати не будемо, оскільки без конкретних параметрів продукту та інфраструктури це неможливо зробити.
Необхідно вміти обробляти різні звіти в різних форматах. Реалізовано за рахунок ReconciliationInterface.
- Звірки повинні запускатися в різний час протягом дня. Реалізовано за рахунок ReconciliationStarter і ReconciliationCalendar.
- Розрахункові навантаження на систему на одну звірку від 1000 до 500 000 операцій. Варто провести навантажувальне тестування, але загалом такі цифри не виглядають проблемою в поточній архітектурі.
- Кількість звірок за поточними даними – до 40 в один день. Варто провести навантажувальне тестування, але це невеликі цифри, щоб переживати через них.
- Обробка звітів від керуючих компаній, які не надсилають їх у вихідні та святкові дні. Для цього передбачено ReconciliationCalendar, який дає змогу керувати днями звірки з адмінки або примусово вмикати чи вимикати їх у певні дні.
- Обробка спеціальних фінансових операцій, які існують у низки керуючих компаній, що вимагають обробки разом з іншими операціями з однієї транзакції. Для цього передбачено CustomReconciliationInterface.
- Обробка звітів, які постачаються подобово з даними за попередній день. Це враховано в архітектурі, оскільки вона будувалася на основі цієї інформації.
- Врахувати обробку даних у звітах, які формуються не строго подобово в інтервалі з 00:00:00 до 23:59:59. Це враховано у звірці за інтервалом дати і часу, який розраховується в процесі самої звірки, а не строго заданий у конфігурації.
- Обробляти дані від керуючих компаній, які не надсилають звіти у вихідні та святкові дні. Звіт у понеділок може містити дані за кілька днів. Цей варіант обробляється процесом інтервального звіряння, оскільки інтервал формується в процесі самого звіряння.
- Обробляти дані від керуючих компаній, які не надсилають звіт вчасно. Для цього в процесі звірки передбачено стадії, які можна перезапускати за необхідності. Варіантів технічної реалізації тут досить багато, і це можна залишити на розсуд розробника.
- Автотестами має бути покрито не менше 98% коду. Оскільки в технологічному стеку є Symfony, можна використовувати чудовий фреймворк для автотестів Codeception з докладною документацією. У ньому також передбачені інструменти для виміру відсотка покриття з підсвічуванням не закритого автотестами коду, що зручно для розробників.
- Створення системи моніторингу. Як інструменти моніторингу можна використовувати Grafana або будь-який аналогічний інструмент, прийнятий всередині компанії. Зазвичай це питання уточнюють у групи експлуатації або адміністраторів.
- Створення системи логування. Як і в будь-якому фреймворку, в Symfony є логгер, який можна використовувати в розробці. Головне – визначити ключові місця коду, де необхідне логування, і додати його туди. Для перегляду логів підійде Kibana або її аналоги.
Рекомендації
Отже, ми розглянули приклад створення архітектури нового застосунку з нуля. Пройшли шлях від ідеї до готового концепту, який можна описати в документі, а потім на основі цього опису скласти технічне завдання і нарізати завдання.
На завершення хотів би дати рекомендації архітекторам-початківцям:
Не соромтеся питати думки колег і не ігноруйте їхні поради. Обмін ідеями дає змогу по-новому поглянути на логіку роботи застосунку і не упустити важливі деталі на старті роботи.
Після завершення проєктування проводьте презентацію архітектури саппортам, розробникам та іншим колегам, які працюватимуть із застосунком. Вони можуть поставити важливі запитання, які з якоїсь причини не виникли на перших етапах роботи.
Деталізуйте схему системи і детально описуйте призначення кожного компонента. Якщо у нас є клас, то пропишіть його в деталях: як працюватиме кожен метод, які будуть властивості тощо. Це дає змогу побачити систему загалом і підготувати опис для технічних завдань розробникам.
Узгодьте список використовуваного технологічного стека з відділом інформаційної безпеки. Деякі ваші рішення можуть суперечити внутрішнім правилам ІБ. Фахівці з інформаційної безпеки зможуть запропонувати аналоги, дозволені в компанії.
Під час порівняння технологій і вибору інструментів розглядайте мінімум три їхні різновиди, намагаючись дотримуватися об’єктивності. Не варто робити вибір тільки на основі звичок.
Намагайтеся не проєктувати вдалий досвід попередніх проєктів на поточний, тому що він може бути нерелевантним у цій ситуації. Попередні рішення можна розглянути, але не варто їх пріоритизувати.
Висновок
Добре спроєктована архітектура дає змогу розвивати додаток без хаосу і збоїв.
Вона спрощує командну роботу, покращує якість коду і скорочує витрати на підтримку та розвиток системи.








