Albo każda nieszczęśliwa firma z monolitem jest nieszczęśliwa na swój sposób.
Rozwój systemu Dodo IS rozpoczął się od razu po założeniu biznesu Dodo Pizzy — w 2011 roku. W jego podstawie leżała idea pełnej i całkowitej cyfryzacji procesów biznesowych, gdzie , co już wtedy w 2011 roku budziło wiele pytań i sceptycyzmu. Ale oto już od 9 lat idziemy tą drogą — z własnym rozwiązaniem, które zaczynało się od monolitu.
Ten artykuł jest «odpowiedzią» na pytania «Po co przepisywać architekturę i wprowadzać tak duże i długotrwałe zmiany?» do poprzedniego artykułu . Zacznę od tego, jak rozpoczęła się praca nad Dodo IS, jak wyglądała pierwotna architektura, jak powstawały nowe moduły i z jakimi problemami musieliśmy się zmierzyć, aby wprowadzić istotne zmiany.

Seria artykułów „Czym jest Dodo IS?” opowie o:
Wczesny monolit w Dodo IS (2011-2015). (Jesteś tutaj)
.
Droga części klienckiej: fasada nad bazą (2016-2017). (W trakcie…)
Historia prawdziwych mikroserwisów. (2018-2019). (W trakcie…)
Zakończony podział monolitu i stabilizacja architektury. (W trakcie…)
Pierwotna architektura
W 2011 roku architektura Dodo IS wyglądała tak:

Pierwszym modułem w architekturze była obsługa zamówień. Proces biznesowy był następujący:
klient dzwoni do pizzerii;
słuchawkę podnosi menedżer;
przyjmuje zamówienie przez telefon;
równocześnie wpisuje je w interfejsie przyjęcia zamówienia: uwzględnia informacje o kliencie, szczegóły zamówienia i adres dostawy.
Interfejs systemu informacyjnego wyglądał mniej więcej tak…
Pierwsza wersja z października 2011:
Zasoby przeznaczone do opracowania pierwszego modułu obsługi zamówień były ograniczone. Trzeba było zrobić dużo, szybko i w małym składzie. Mały skład — to 2 deweloperów, którzy położyli fundament całego przyszłego systemu.
Ich pierwsze rozwiązanie zdefiniowało dalszą przyszłość technologicznego stosu:
Backend na ASP.NET MVC, język C#. Programiści byli specjalistami .NET, ten stos był im znany i przyjemny.
Frontend na Bootstrap i JQuery: interfejsy użytkownika na własnych stylach i skryptach.
Baza danych MySQL: bez kosztów licencji, łatwa w użyciu.
Serwery na Windows Server, ponieważ .NET wówczas działał tylko na Windows (Mono nie wchodzi w grę).
Fizycznie wszystko to wyrażało się w „dedyku u hostera”.
Architektura aplikacji do obsługi zamówień
Wtedy już wszyscy mówili o mikroserwisach, a SOA było stosowane przez około 5 lat w dużych projektach, na przykład WCF zadebiutowało w 2006 roku. Ale wtedy wybrano niezawodne i sprawdzone rozwiązanie.
I oto jest.

Asp.Net MVC to Razor, który na żądanie z formularza lub od klienta generuje stronę HTML z renderowaniem na serwerze. Na kliencie CSS i skrypty JS wyświetlają informacje i, w razie potrzeby, wykonują zapytania AJAX za pomocą JQuery.
Żądania na serwerze trafiają do klas *Controller, gdzie w metodzie przebiega przetwarzanie i generowanie końcowej strony HTML. Kontrolery wykonują zapytania do warstwy logiki, zwanej *Services. Każdy z serwisów odpowiadał za jakiś aspekt biznesu:
Na przykład, DepartmentStructureService dostarczał informacji o pizzeriach i departamentach. Departament to zbiór pizzerii zarządzanych przez jednego franczyzobiorcę.
ReceivingOrdersService przyjmował i kalkulował skład zamówienia.
A SmsService wysyłał SMS, wywołując API do wysyłania SMS.
Serwisy przetwarzały dane z bazy, przechowywały logikę biznesową. W każdym serwisie znajdował się jeden lub więcej *Repository o odpowiednich nazwach. Zawierały one zapytania do procedur składowanych w bazie oraz warstwę mapperów. W procedurach składowych była zawarta logika biznesowa, szczególnie dużo w tych, które generowały dane raportowe. ORM nie był używany, wszyscy polegali na ręcznie napisanym SQL.
Była również warstwa modelu domenowego i wspólnych klas-helperów, na przykład klasa Order, która przechowywała zamówienie. Tam także znajdował się helper do przekształcania wyświetlanego tekstu według wybranej waluty.
Wszystko to można przedstawić w takiej modelu:

Ścieżka zamówienia
Rozważmy uproszczoną początkową ścieżkę stworzenia takiego zamówienia.

Początkowo strona była statyczna. Zawierała ceny, a na górze znajdował się numer telefonu oraz napis "Chcesz pizzę - dzwoń pod numer i zamów". Aby złożyć zamówienie, musimy wdrożyć prosty flow:
Klient wchodzi na statyczną stronę z cenami, wybiera produkty i dzwoni pod numer podany na stronie.
Klient wymienia produkty, które chce dodać do zamówienia.
Podaje swój adres i imię.
Operator przyjmuje zamówienie.
Zamówienie wyświetla się w interfejsie przyjętych zamówień.
Wszystko zaczyna się od wyświetlenia menu. Zalogowany użytkownik-operator w danym momencie przyjmuje tylko jedno zamówienie. Dlatego koszyk roboczy może być przechowywany w jego sesji (sesja użytkownika jest przechowywana w pamięci). Tam znajduje się obiekt Cart, w którym są produkty i informacje o kliencie.
Klient wymienia produkt, operator naciska na + obok produktu, a na serwer wysyłane jest zapytanie. Informacje o produkcie są pobierane z bazy danych i dodawane do koszyka.

Uwaga. Tak, można nie pobierać produktu z bazy, a przekazać z frontendu. Ale dla przejrzystości pokazałem dokładnie ścieżkę z bazy.
Następnie wprowadzamy adres i imię klienta.

Po naciśnięciu „Utwórz zamówienie”:
Wysyłamy zapytanie do OrderController.SaveOrder().
Pobieramy Cart z sesji, tam znajdują się produkty w potrzebnej nam ilości.
Uzupełniamy Cart informacjami o kliencie i przekazujemy do metody AddOrder klasy ReceivingOrderService, gdzie zostaje zapisane w bazie.
W bazie znajdują się tabele z zamówieniem, składem zamówienia, klientem i są one ze sobą powiązane.
Interfejs wyświetlania zamówienia przechwytuje i pobiera ostatnie zamówienia oraz je wyświetla.
Nowe moduły
Przyjmowanie zamówienia było ważne i niezbędne. Nie można prowadzić biznesu związanego ze sprzedażą pizzy bez systemu przyjmowania zamówień. Dlatego system zaczynał się rozwijać — od około 2012 do 2015 roku. W tym czasie powstało wiele różnych modułów systemu, które będę nazywać modułami, w przeciwieństwie do pojęcia usługi lub produktu.
Moduł to zestaw funkcji, które są powiązane jakąś wspólną celą biznesową. Fizycznie wszystkie znajdują się w jednej aplikacji.
Moduły można nazwać blokami systemu. Przykładowo, to moduł raportów, interfejsy administracyjne, , autoryzacja. To wszystko różne interfejsy dla użytkownika, niektóre mają nawet różne style wizualne. Wszystko w ramach jednej aplikacji, jednego działającego procesu.
Technicznie moduły były realizowane jako Area (taki pomysł pozostał w ). Istniały oddzielne pliki dla frontendu, modeli, a także własne klasy kontrolerów. W rezultacie system przekształcił się z takiego…

…w taki:

Niektóre moduły zostały zaimplementowane jako oddzielne strony (projekt wykonywalny), z powodu całkowicie odrębnych funkcjonalności oraz częściowo z powodu bardziej ukierunkowanego rozwoju. Są to:
Strona — strony dodopizza.ru.
Eksport: eksport raportów z Dodo IS do 1C.
Personal — osobisty panel pracownika. Opracowany oddzielnie i ma własny punkt wejścia oraz oddzielny design.
fs — projekt do hostowania statyki. Później zrezygnowaliśmy z niego, przenosząc całą statykę na CDN Akamai.
Pozostałe bloki znajdowały się w aplikacji BackOffice.

Wyjaśnienie nazw:
Cashier — Kasa restauracji.
ShiftManager — interfejsy dla roli „Menedżer zmiany”: bieżąca statystyka sprzedaży pizzerii, możliwość dodania produktów do czarnej listy, zmiana zamówienia.
OfficeManager — interfejsy dla ról „Zarządzający pizzerią” i „Franczyzobiorca”. Tutaj zbierane są funkcje do konfiguracji pizzerii, jej promocji, zarządzania pracownikami oraz raportów.
PublicScreens — interfejsy dla telewizorów i tabletów wiszących w pizzeriach. Na telewizorach wyświetlane jest menu, informacje reklamowe oraz status zamówienia przy odbiorze.
Wykorzystano wspólną warstwę usług, wspólny blok klas domenowych Dodo.Core, a także wspólną bazę. Czasami można było się przemieszczać pomiędzy nimi. W tym również z wspólnych usług korzystały oddzielne strony, takie jak dodopizza.ru czy personal.dodopizza.ru.
Przy wprowadzaniu nowych modułów starano się jak najwięcej wykorzystywać wcześniej stworzony kod usług, procedur składowych i tabel w bazie.
Aby lepiej zrozumieć zakres modułów w systemie, oto schemat z 2012 roku z planami rozwoju:

Do 2015 roku wszystko, co na schemacie i jeszcze więcej, było wdrożone w produkcji.
Przyjmowanie zamówień przerodziło się w oddzielny blok Centrum Kontaktowego, gdzie zamówienia przyjmowane są przez operatora.
Pojawiły się ogólnodostępne ekrany z menu i informacjami, wiszące w pizzeriach.
W kuchni znajduje się moduł, który automatycznie odtwarza komunikat głosowy „Nowa pizza” po przyjęciu nowego zamówienia oraz drukuje dokument dla kuriera. To znacznie upraszcza procesy w kuchni, pozwalając pracownikom skupić się na bardziej złożonych operacjach.
Blok dostaw stał się oddzielną Kasą Dostaw, gdzie zamówienie wydawane było kurierowi, który wcześniej zgłosił się do pracy. Jego czas pracy był uwzględniany przy naliczaniu wynagrodzenia.
Równolegle, od 2012 do 2015 roku, pojawiło się ponad 10 programistów, otwarto 35 pizzerii, rozwinięto system w Rumunii i przygotowano do otwarcia punktów w USA. Programiści przestali zajmować się wszystkimi zadaniami, zostali podzieleni na zespoły, z których każdy specjalizował się w swojej części systemu.
Problemy
W tym także z powodu architektury (ale nie tylko).
Chaos w bazie
Jedna baza — to wygodne. Można osiągnąć spójność, a przy tym korzystać z wbudowanych w relacyjne bazy narzędzi. Praca z nią jest naturalna i wygodna, szczególnie, jeśli jest mało tabel i danych.
Jednak w ciągu 4 lat rozwoju w bazie znalazło się około 600 tabel, 1500 procedur składowych, z których w wielu była także logika. Niestety, procedury składowe nie przynoszą szczególnych korzyści w pracy z MySQL. Nie są one buforowane przez bazę, a przechowywanie logiki w nich utrudnia rozwój i debugowanie. Ponowne wykorzystanie kodu także jest trudne.
Na wielu tabelach brakowało odpowiednich indeksów., gdzieś z kolei było ich zbyt wiele, co utrudniało wstawianie. Należało zmodyfikować około 20 tabel — transakcja na utworzenie zamówienia mogła trwać około 3-5 sekund.
Dane w tabelach nie zawsze były w najbardziej odpowiedniej formie.. Gdzieś konieczna była denormalizacja. Część regularnie otrzymywanych danych znajdowała się w kolumnie w postaci struktury XML, co wydłużało czas wykonania, komplikowało zapytania i utrudniało rozwój.
Do tych samych tabel wykonywano bardzo zróżnicowane zapytania.. Szczególnie ucierpiały popularne tabele, jak wspomniana tabela orders lub tabela pizzeria.Były one wykorzystywane do wyświetlania operacyjnych interfejsów w kuchni, analizy. Strona również sięgała po nie (), gdzie w każdej chwili mogło napłynąć nagle wiele zapytań.
Dane nie były zgrupowane i wiele obliczeń odbywało się na bieżąco w bazie. To powodowało zbędne obliczenia i dodatkowe obciążenie.
Często kod łączył się z bazą wtedy, gdy nie powinien tego robić. Gdzieś brakowało operacji zbiorczych, gdzieś inne zapytanie należałoby rozdzielić na kilka przez kod, aby przyspieszyć i zwiększyć niezawodność.
Powiązania i skomplikowanie w kodzie
Moduły, które powinny odpowiadać za swój obszar biznesowy, nie robiły tego rzetelnie.Niektóre z nich miały powielanie funkcji dla ról. Na przykład lokalny marketer, odpowiedzialny za aktywność marketingową sieci w swoim mieście, musiał korzystać zarówno z interfejsu 'Administratora' (do wprowadzania akcji), jak i z interfejsu 'Menadżera Biura' (do przeglądania wpływu akcji na biznes). Oczywiście, wewnętrznie oba moduły korzystały z jednego serwisu, który obsługiwał bonusowe promocje.
Serwisy (klasy w ramach jednego monolitycznego dużego projektu) mogły wywoływać się nawzajem, aby wzbogacać swoje dane.
Z samymi klasami-modelami, które przechowują dane, praca w kodzie przebiegała różnie.. Gdzieś były konstruktory, przez które można było wskazać obowiązkowe pola. Gdzie indziej odbywało się to przez publiczne właściwości. Oczywiście pobieranie i przekształcanie danych z bazy było zróżnicowane.
Logika była albo w kontrolerach, albo w klasach serwisów.
To mogły być drobne problemy, ale mocno spowalniały rozwój i obniżały jakość, co prowadziło do niestabilności i błędów.
Złożoność dużego projektu
Problemy pojawiły się również w samej implementacji. Należało tworzyć różne bloki systemu, a do tego równolegle. Coraz trudniej było pomieścić potrzeby każdego komponentu w jednym kodzie. Nie było łatwo się dogadać i zadowolić wszystkie komponenty jednocześnie. Do tego dochodziły ograniczenia technologiczne, szczególnie dotyczące bazy danych i front-endu. Należało zrezygnować z JQuery na rzecz frameworków wyższego poziomu, szczególnie w odniesieniu do usług klienta (strona internetowa).
W niektórych częściach systemu mogłyby być używane bazy bardziej odpowiednie do tego celu. Na przykład, później mieliśmy przypadek przejścia z Redis na CosmosDB do przechowywania koszyka zamówienia.
Zespoły i deweloperzy, zajmujący się swoją dziedziną wyraźnie chcieli większej samodzielności dla swoich usług, zarówno w zakresie rozwoju, jak i wdrożeń. Konflikty przy scalaniu, problemy podczas wydania. Jeśli dla 5 deweloperów ten problem nie był istotny, to przy 10, a tym bardziej przy planowanym wzroście, wszystko stałoby się poważniejsze. A przed nami miała być druga wersja aplikacji mobilnej (zaczęła się w 2017, a w 2018 miał miejsce) ).
Różne części systemu wymagały różnych wskaźników stabilności, ale z powodu silnego powiązania systemu, nie mogliśmy tego zapewnić. Błąd przy opracowywaniu nowej funkcji w panelu admina, mógłby wpłynąć na odbiór zamówienia na stronie, ponieważ kod jest wspólny i wielokrotnie używany, baza i dane także są jedne.
Prawdopodobnie można byłoby i w ramach takiej monolityczno-modułowej architektury unikać tych błędów i problemów: wprowadzić podział odpowiedzialności, przeprowadzać refaktoryzację zarówno kodu, jak i bazy danych, wyraźnie oddzielać warstwy od siebie, dbać o jakość każdego dnia. Ale wybrane rozwiązania architektoniczne i skupienie na szybkim rozszerzaniu funkcjonalności systemu doprowadziły do problemów ze stabilnością.
Jak blog Siła umysłu wpłynął na kasy w restauracjach
Gdyby wzrost sieci pizzerii (i obciążenia) trwał w tym samym tempie, to po pewnym czasie spadki byłyby już tak duże, że system by się nie podniósł. Dobrze ilustruje to problemy, z którymi zaczęliśmy się zmagać w 2015 roku, taka oto historia.
Na blogu „” był widget, który pokazywał dane o przychodach za rok całej sieci. Widget korzystał z publicznego API Dodo, które dostarcza te dane. Teraz te statystyki są dostępne na . Widget był wyświetlany na każdej stronie i co 20 sekund wysyłał zapytania do api.dodopizza.ru i pytał:
liczbę pizzerii w sieci;
całkowite przychody sieci od początku roku;
przychody za dzisiaj.
Zapytanie o statystyki przychodów szło bezpośrednio do bazy i zaczynało zbierać dane o zamówieniach, agregując je na bieżąco i podając sumę.
Do tej samej tabeli zamówień trafiały kasy w restauracjach, które zgrywały listę przyjętych zamówień z dzisiaj, a nowe zamówienia były też dodawane do tej tabeli. Kasy wysyłały swoje zapytania co 5 sekund lub przy odświeżeniu strony.
Schemat wyglądał tak:

Pewnego jesiennego dnia, Fiodor Owczynnikow napisał na swoim blogu długi i popularny artykuł. Na bloga przyszło bardzo dużo ludzi, którzy zaczęli uważnie wszystko czytać. Podczas gdy każdy z przybyłych czytał artykuł, widget z przychodami działał poprawnie i co 20 sekund wysyłał zapytania do API.
API wywoływało procedurę składowaną do obliczenia sumy wszystkich zamówień od początku roku we wszystkich pizzeriach sieci. Agregacja odbywała się na tabeli zamówień, która jest bardzo popularna. Do niej sięgały wszystkie kasy wszystkich otwartych restauracji w danym czasie. Kasy przestały odpowiadać, zamówienia nie były przyjmowane. Nie były też przyjmowane z strony internetowej, nie pojawiały się w trackerze, menedżer zmiany nie mógł ich zobaczyć w swoim interfejsie.
To nie jedyna historia. Na jesieni 2015 roku w każdą piątkę obciążenie systemu było krytyczne. Kilka razy wyłączaliśmy publiczne API, a pewnego razu musieliśmy nawet wyłączyć stronę, ponieważ nic już nie pomagało. Była nawet lista usług z kolejnością wyłączania przy poważnych obciążeniach.
Od tego momentu zaczyna się nasza walka z obciążeniami i stabilizacją systemu (od jesieni 2015 do jesieni 2018). Właśnie wtedy zdarzyło się „Czasami zdarzały się inne awarie, niektóre były dość dotkliwe, ale ogólny okres niestabilności można już uznać za za nami.
Nagły wzrost biznesu
Dlaczego nie można było "od razu dobrze zrobić"? Wystarczy spojrzeć na następujące wykresy.

W latach 2014-2015 otworzyliśmy oddział w Rumunii, a także przygotowywaliśmy otwarcie w Stanach Zjednoczonych.
Sieć rosła w bardzo szybkim tempie, otwierano nowe kraje, pojawiały się nowe formaty pizzerii, na przykład pizzeria na food courcie. Wszystko to wymagało znacznej uwagi skierowanej na rozwój funkcji Dodo IS. Bez tych wszystkich funkcji, bez śledzenia w kuchni, bez uwzględniania produktów i strat w systemie, bez wyświetlania realizacji zamówienia na food courcie, mało prawdopodobne jest, że teraz dyskutowalibyśmy o "poprawnej" architekturze i "właściwym" podejściu do rozwoju.
Innym przeszkodą w terminowym przeglądzie architektury i w ogóle zwróceniu uwagi na problemy techniczne był kryzys z 2014 roku. Tego typu rzeczy mocno uderzają w możliwości wzrostu zespołów, szczególnie w przypadku młodego biznesu, jakim była Dodo Pizza.
Szybkie rozwiązania, które pomogły
Problemy wymagały rozwiązania. Można je zasadniczo podzielić na dwie grupy:
Szybkie, które gaszą pożar i dają niewielki zapas wytrzymałości oraz pozwalają na zmiany.
Systemowe i, w związku z tym, czasochłonne. Reengineering szeregu modułów, podział monolitycznej architektury na odrębne usługi (większość z nich to raczej nie mikro, a bardziej makro usługi i o tym jest ).
Sucha lista szybkich zmian jest następująca:
Skalowanie bazy master
Oczywiście pierwszym krokiem w walce z obciążeniem jest zwiększenie mocy serwera. To zostało zrobione dla bazy master oraz serwerów webowych. Niestety, możliwe jest to jedynie do pewnego momentu, później staje się to zbyt kosztowne.
Od 2014 roku przeszliśmy na Azure, na ten temat pisaliśmy również w tamtym czasie w artykule "". Ale po serii zwiększeń serwera pod bazę utknęliśmy w kwestii kosztów.
Repliki bazy do odczytu
Stworzyliśmy dwie repliki bazy:
ReadReplica do zapytań baz danych. Stosowane są do odczytu słowników, takich jak miasta, ulice, pizzerie, produkty (powoli zmieniająca się domena), oraz w tych interfejsach, gdzie dozwolona jest niewielka zwłoka. Było ich 2, zapewnialiśmy ich dostępność tak samo jak bazie master.
ReadReplica dla zapytań o raporty. Ta baza miała niższą dostępność, ale wszystkie raporty korzystały z niej. Niech mają trudne zapytania dotyczące ogromnych przeliczeń danych, ale nie wpływają na główną bazę i interfejsy operacyjne.
Cache w kodzie
Nie było żadnych cache'ów w kodzie (w ogóle). Prowadziło to do dodatkowych, nie zawsze potrzebnych, zapytań do obciążonej bazy. Cache były początkowo zarówno w pamięci, jak i na zewnętrznej usłudze cache, był to Redis. Wszystko było unieważniane w określonym czasie, a ustawienia były podane w kodzie.
Kilka serwerów dla backendu
Backend aplikacji również należało skalować, aby wytrzymać zwiększone obciążenia. Trzeba było zrobić klaster z jednego serwera iis. Przenieśliśmy z pamięci na RedisCache, co pozwoliło stworzyć kilka serwerów, stojących za prostym load balancerem z round robin. Na początku używano tego samego Redisa, co dla cache'y, potem rozdzielono na kilka.
W efekcie architektura stała się bardziej skomplikowana…

…ale udało się złagodzić część napięcia.
A dalej trzeba było przeprojektować obciążone komponenty, co także zrobiliśmy. O tym opowiemy w następnej części.
Źródło: habr.com
