Історія архітектури Dodo IS: ранній моноліт

Або кожна нещасна компанія з монолітом нещаслива по-своєму.

Розробка системи Dodo IS розпочалася одразу ж, як і бізнес Додо Піци – у 2011 році. В основі лежала ідея повного та тотального оцифрування бізнес-процесів, причому своїми силами, що ще тоді у 2011 році викликало багато питань та скептицизму. Але вже 9 років ми йдемо таким шляхом — з власною розробкою, яка починалася з моноліту.

Ця стаття — «відповідь» на запитання «Навіщо переписувати архітектуру та робити такі масштабні та довгі зміни?» до попередньої статті "Історія архітектури Dodo IS: шлях бекофісу". Почну з того, як починалася розробка Dodo IS, як виглядала початкова архітектура, як з'являлися нові модулі, і через які проблеми довелося проводити масштабні зміни.

Історія архітектури Dodo IS: ранній моноліт

Серія статей Що таке Dodo IS? розповість про:

  1. Ранній моноліт у Dodo IS (2011-2015 роки). (You are here)

  2. Шлях бекофісу: роздільні бази та шина.

  3. Шлях клієнтської частини: фасад над базою (2016–2017 роки). (In progress…)

  4. Історія реальних мікросервісів. (2018–2019 роки). (In progress…)

  5. Закінчений розпил моноліту та стабілізація архітектури. (In progress…)

Початкова архітектура

У 2011 році архітектура Dodo IS виглядала так:

Історія архітектури Dodo IS: ранній моноліт

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

  • клієнт дзвонить до піцерії;

  • трубку бере менеджер;

  • приймає по телефону замовлення;

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

Інтерфейс інформаційної системи виглядав приблизно так…

Перша версія від жовтня 2011:

Трохи покращена в січні 2012

Інформаційна система Додо Піца Delivery Pizza Restaurant

Ресурси розробки першого модуля прийому замовлення були обмежені. Потрібно було робити багато, швидко та малим складом. Малий склад - це 2 розробники, які і заклали фундамент усієї майбутньої системи.

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

  • Backend на ASP.NET MVC, мова C#. Розробники були дотнетчиками, цей стек був їм знайомий і приємний.

  • Фронтенд на Bootstrap та JQuery: інтерфейси користувача на самописних стилях та скриптах. 

  • База даних MySQL: без витрат на ліцензії, проста у використанні.

  • Сервери на Windows Server, тому що .NET тоді міг бути тільки під Windows (Mono обговорювати не будемо).

Фізично це все виражалося у «дедіку у хостера». 

Архітектура програми прийому замовлення

Тоді вже всі говорили про мікросервіси, а SOA років 5 використовувалося у великих проектах, наприклад, WCF вийшов у 2006 році. Але тоді обрали надійне та перевірене рішення.

Ось воно.

Історія архітектури Dodo IS: ранній моноліт

Asp.Net MVC - це Razor, який видає на запит з форми або від клієнта HTML-сторінку з рендерингом на сервері. На клієнті вже CSS та JS-скрипти відображають інформацію та, за потребою, виконують AJAX-запити через JQuery.

Запити на сервері потрапляють до класів *Controller, де у методі відбувається обробка та генерація підсумкової HTML-сторінки. Контролери роблять запити на шар логіки, що називається Services. Кожен із сервісів відповідав якомусь аспекту бізнесу:

  • Наприклад, DepartmentStructureService видавав інформацію з піцерій, департаментів. Департамент - це група піцерій під керуванням одного франчайзі.

  • ReceivingOrdersService приймав і розраховував склад замовлення.

  • А SmsService відправляв смс, викликаючи API-сервіси з відправки смс.

Сервіси обробляли дані із бази, зберігали бізнес-логіку. У кожному сервісі був один або кілька Repository з відповідною назвою. У них вже знаходилися запити до процедур, що зберігаються в базі і шар маперів. У сховищах була бізнес-логіка, особливо багато в тих, які видавали звітні дані. ОРМ не використовувався, всі покладалися на написаний руками SQL. 

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

Все це можна уявити такою моделлю:

Історія архітектури Dodo IS: ранній моноліт

Шлях замовлення

Розглянемо спрощений початковий шлях створення такого замовлення.

Історія архітектури Dodo IS: ранній моноліт

Спочатку сайт був статичний. На ньому були ціни, а зверху — номер телефону та напис «Хочеш піцу — дзвони за номером та замов». Для замовлення нам потрібно реалізувати простий flow: 

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

  • Клієнт називає продукти, які хоче додати на замовлення.

  • Називає свою адресу та ім'я.

  • Оператор приймає замовлення.

  • Замовлення відображається в інтерфейсі прийнятих замовлень.

Все починається з відображення меню. Залогінений користувач-оператор одночасно приймає лише одне замовлення. Тому draft-кошик може зберігатися у його сесії (сеанс користувача зберігається у пам'яті). Там об'єкт Cart, в якому продукти та інформація про клієнта.

Клієнт називає продукт, оператор натискає на + поряд із продуктом, і на сервер надсилається запит. По продукту витягується інформація з бази та додається інформація про продукт у кошик.

Історія архітектури Dodo IS: ранній моноліт

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

Далі вводимо адресу та ім'я клієнта. 

Історія архітектури Dodo IS: ранній моноліт

При натисканні «Створити замовлення»:

  • Запит відправляємо до OrderController.SaveOrder().

  • Отримуємо Cart із сесії, там лежать продукти у потрібній нам кількості.

  • Доповнюємо Cart інформацією про клієнта та передаємо у метод AddOrder класу ReceivingOrderService, де він зберігається до бази. 

  • У основі є таблиці із замовленням, складом замовлення, клієнтом і всі пов'язані.

  • Інтерфейс відображення замовлення йде та витягує останні замовлення та відображає їх.

Нові модулі

Прийом замовлення був важливий і необхідний. Не можна зробити бізнес із продажу піци, якщо немає прийому замовлення на продаж. Тому система почала обростати функціоналом — приблизно з 2012 до 2015 року. За цей час з'явилося багато різних блоків системи, які я називатиму модулями, на противагу поняттю сервісу чи продукту. 

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

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

Технічно модулі оформлялися як Area (ось така ідея навіть залишилася в ядро asp.net). Існували окремі файли для фронтенда, моделей, а також свої класи контролерів. У результаті система перетворилася з такої…

Історія архітектури Dodo IS: ранній моноліт

…у таку:

Історія архітектури Dodo IS: ранній моноліт

Деякі модулі реалізовані окремими сайтами (executable project), через зовсім окремого функціоналу і частково через кілька окремої, більш сфокусованої розробки. Це:

  • сайт - первая версія сайту dodopizza.ru.

  • Експорт: вивантаження звітів із Dodo IS для 1C. 

  • Особисте - Особистий кабінет співробітника. Окремо розроблявся та має свою точку входу та окремий дизайн.

  • fs - Проект для хостингу статики. Пізніше ми пішли від нього, переклавши всю статику на CDN Akamai. 

Інші блоки знаходилися в додатку BackOffice. 

Історія архітектури Dodo IS: ранній моноліт

Пояснення за назвами:

  • Cashier - Каса ресторану.

  • ShiftManager — інтерфейси для ролі Менеджер зміни: оперативна статистика з продажу піцерії, можливість поставити в стоп-лист продукти, змінити замовлення.

  • OfficeManager — інтерфейси для ролі «Керуючий піцерії» та «Франчайзі». Тут зібрані функції з налаштування піцерії, її бонусних акцій, прийом та робота зі співробітниками, звіти.

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

Вони використовували загальний прошарок сервісів, загальний блок доменних класів Dodo.Core, а також загальну базу. Іноді ще могли вести переходи один до одного. У тому числі до загальних сервісів ходили і окремі сайти, такі як dodopizza.ru або personal.dodopizza.ru.

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

Для кращого розуміння масштабу модулів, зроблених у системі, ось схема з 2012 року із планами розвитку:

Історія архітектури Dodo IS: ранній моноліт

До 2015 року все на схемі і навіть більше було у продакшні.

  • Прийом замовлення переріс окремий блок Контакт Центру, де замовлення приймається оператором.

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

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

  • Блок доставки став окремою Касою Доставки, де замовлення видавалося кур'єру, який попередньо став на зміну. Враховувався його робочий час для нарахування зарплати. 

Паралельно з 2012 по 2015 з'явилося понад 10 розробників, відкрилося 35 піцерій, розгорнули систему на Румунію та підготували до відкриття точок у США. Розробники вже не займалися всіма завданнями, а були поділені на команди. кожна спеціалізувалася на своїй частині системи. 

Проблеми

У тому числі через архітектуру (але не лише).

Хаос у базі

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

Але за 4 роки розробки в базі виявилося близько 600 таблиць, 1500 процедур, що зберігаються, у багатьох з яких була ще й логіка. На жаль, процедури, що зберігаються, не приносять особливої ​​переваги при роботі з MySQL. Вони не кешуються базою, а зберігання в них логіки ускладнює розробку та налагодження. Перевикористання коду теж утруднене.

На багатьох таблицях не було відповідних індексів, десь, навпаки, було дуже багато індексів, що ускладнювало вставку. Треба було модифікувати близько 20 таблиць - транзакція створення замовлення могла виконуватися близько 3-5 секунд. 

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

До тих самих таблиць проводилися дуже різноманітні запити. Особливо страждали популярні таблиці, на кшталт згадуваної таблиці замовлень або таблиці піцерія. Вони були використані для виведення оперативних інтерфейсів на кухні, аналітики. Ще до них звертався сайт(dodopizza.ru), куди будь-якої миті часу могло прийти раптово багато запитів. 

Дані не були агрегованими та багато розрахунків відбувалося на льоту засобами бази. Це створювало зайві обчислення та додаткове навантаження. 

Часто код ходив у основу тоді, коли міг цього не робити. Десь не вистачало bulk-операцій, десь треба було б рознести один запит на кілька через код, щоб прискорити та підвищити надійність. 

Зв'язність та заплутаність у коді

Модулі, які мали відповідати за свою ділянку бізнесу, не робили цього чесно. Деякі з них мали дублювання за функціями для ролей. Наприклад, локальному маркетологу, який відповідає за маркетингову активність мережі у своєму місті, доводилося користуватися як інтерфейсом «Адміна» (для закладу акцій), так і інтерфейсом «Менеджера Офісу» (для перегляду впливу акцій на бізнес). Звичайно, всередині обидва модулі використовували один сервіс, який працював з бонусними акціями.

Сервіси (класи у межах одного монолітного великого проекту) могли викликати одне одного збагачення своїх даних.

З самими класами-моделями, які зберігають дані, робота в коді велася по-різному. Десь були конструктори, якими можна було вказати обов'язкові поля. Десь це робилося через громадські характеристики. Звичайно, отримання та перетворення даних із бази було різноманітним. 

Логіка була або у контролерах, або у класах сервісів. 

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

Складність великої розробки

Труднощі виникли й у самій розробці. Потрібно було робити різні блоки системи, причому паралельно. Вмістити потреби кожного компонента в єдиний код ставало дедалі важче. Було не просто домовитися та догодити всім компонентам одночасно. До цього додавалися обмеження в технологіях, особливо щодо бази та фронтенду. Потрібно було відмовлятися від JQuery у бік високорівневих фреймворків, особливо щодо клієнтських сервісів (сайт).

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

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

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

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

Як блог Сила розуму поклав каси у ресторанах

Якщо зростання мережі піцерій (і навантаження) тривало б у тому ж темпі, то через деякий час падіння були б такими, що система і не підніметься. Добре ілюструє проблеми, з якими ми почали стикатися до 2015 року така історія. 

У блозі «Сила розуму» був віджет, який показував дані про виручку протягом року всієї мережі. Віджет звертався до публічного API Dodo, яке надає ці дані. Наразі ця статистика доступна на http://dodopizzastory.com/. Віджет відображався на кожній сторінці та робив запити по таймеру кожні 20 секунд. Запит йшов у api.dodopizza.ru і запитував:

  • кількість піцерій у мережі;

  • загальний виторг мережі з початку року;

  • виручку за сьогодні.

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

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

Схема виглядала так:

Історія архітектури Dodo IS: ранній моноліт

Якось восени, Федір Овчинников написав у свій блог довгу та популярну статтю. На блог прийшло дуже багато людей і почали уважно читати. Поки кожна людина читала статтю, віджет з виручкою справно працював і запитував API кожні 20 секунд.

API викликало процедуру, що зберігається, на розрахунок суми всіх замовлень з початку року по всіх піцеріях мережі. Агрегація йшла за таблицею orders, яка дуже популярна. У неї ходять всі каси всіх відкритих ресторанів на той момент. Каси перестали відповідати, замовлення не приймали. Ще вони не бралися з сайту, не з'являлися на трекері, менеджер зміни не міг побачити їх у своєму інтерфейсі. 

Це не єдина історія. До осені 2015 року щоп'ятниці навантаження на систему було критичним. Кілька разів ми вимикали публічне API, а одного разу нам довелося навіть відключити сайт, бо вже нічого не допомагало. Був навіть список сервісів із порядком відключення при серйозних навантаженнях.

З цього часу розпочинається наша боротьба із навантаженнями та за стабілізацію системи (з осені 2015 до осені 2018). Саме тоді сталося «Велике падіння». Далі також іноді відбувалися збої, деякі були дуже чутливими, але загальний період нестабільності зараз можна вважати пройденим.

Бурхливе зростання бізнесу

Чому не можна було «зробити одразу добре»? Достатньо подивитися на наступні графіки.

Історія архітектури Dodo IS: ранній моноліт

Також у 2014-2015 було відкриття у Румунії та готувалося відкриття у США.

Мережа росла дуже швидко, відкривалися нові країни, з'являлися нові формати піцерій, наприклад, піцерія відкрилася на фудкорті. Все це вимагало значної уваги до розширення функцій Dodo IS. Без усіх цих функцій, без трекінгу на кухні, обліку продуктів і втрат у системі, відображення видачі замовлення в залі фудкорту, навряд чи ми зараз міркували б про «правильну» архітектуру та «вірний» підхід до розробки.

Ще перешкодами для своєчасного перегляду архітектури та взагалі уваги до технічних проблем була криза 2014 року. Такі речі боляче б'ють на можливості для зростання команд, особливо для молодого бізнесу, яким була Додо Піца.

Швидкі рішення, які допомогли

Проблеми вимагали вирішення. Умовно, рішення можна поділити на 2 групи:

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

  • Системні і тому довгі. Реінжиніринг ряду модулів, поділ монолітної архітектури на окремі сервіси (більшість з них цілком не мікро, а швидше за макросервіси і про це є доповідь Андрія Моревського). 

Сухий список швидких змін такий:

Scale up майстер бази

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

З 2014 року ми перейшли в Azure, на цю тему ми теж писали ще на той час у статті «Як Додо Піца доставляє піцу за допомогою хмари Microsoft Azure». Але після низки збільшення сервера під базу вперлися за вартістю. 

Репліки бази для читання

Реплік для бази зробили дві:

ReadReplica для запитів на довідники. Застосовується для читання довідників, типу, міста, вулиці, піцерії, продуктів (slowly changed domain), і тих інтерфейсах, де допустима невелика затримка. Цих реплік було 2, ми забезпечували їхню доступність так само, як і майстри.

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

Кеші у коді

Кешей у коді ніде не було (взагалі). Це призводило до додаткових, не завжди потрібних запитів у навантажену базу. Кеші були спочатку як у пам'яті, так і на зовнішньому кеш-сервісі, це був Redis. Все було інвалідом за часом, налаштування вказувалися в коді.

Декілька серверів для бекенда

Бекенд програми теж треба було масштабувати, щоб витримувати підвищені навантаження. Потрібно було зробити з одного iis-сервера кластер. Ми перенесли сесію додатків з пам'яті на RedisCache, що дозволило зробити кілька серверів, які стоять за простим балансувальником навантаження з round robin. Спочатку використовувався той самий Redis, що і для кешів, потім рознесли на кілька. 

У результаті архітектура ускладнилася.

Історія архітектури Dodo IS: ранній моноліт

…але частину напруженості вдалося зняти.

А далі треба було переробляти навантажені компоненти, за що ми взялися. Про це ми розповімо у наступній частині.

Джерело: habr.com

Додати коментар або відгук