„Życie to sztuka wyboru”. Nie wiem dokładnie, kto jest autorem tego powiedzenia, ale jest w tym cholernie dużo racji. Wytwarzanie oprogramowania to też sztuka wyboru. To sztuka znajdowania kompromisów. W klasycznym powiedzeniu „Dobrze, szybko, tanio – wybierz dowolne dwa atrybuty” jest naprawdę dużo racji.
Aby podjąć decyzję, często wypisujemy sobie argumenty za i przeciw. Jest to dobra metoda, gdy mamy tylko dwie opcje wyboru. W przypadku decyzji o kształcie wytwarzanego systemu opcji, jak również samych decyzji, jest wiele.
Drivery architektoniczne
W naszym świecie z pomocą przychodzą nam drivery architektoniczne. Wspomina o nich Simon Brown w swojej książce „Software architecture for developers”, możemy usłyszeć o nich na konferencjach czy szkoleniach takich jak DNA – Droga Nowoczesnego Architekta.
Drivery architektoniczne to zestaw czynników, które musimy wziąć pod uwagę projektując i implementując system.
Zgodnie z literaturą i materiałami, jakie miałem okazję studiować, istnieje następujący podział driverów na pięć klas:
- wymagania funkcjonalne
- atrybuty jakościowe
- ograniczenia projektowe
- konwencje
- cele projektowe
Niestety poza tym, że wyszczególnione zostały klasy driverów na próżno szukać gotowych list driverów każdej kategorii. Drivery niestety bywają specyficzne dla problemu z jakim się mierzymy.
Case study
Chciałbym Wam w tym artykule przedstawić, na prawdziwym produkcyjnym przykładzie, proces myślowy, który doprowadził do zidentyfikowania najważniejszych driverów architektonicznych. Pokażę też, co się stało, kiedy część driverów została odkryta zbyt późno.
Opis systemu
Na potrzeby tego artykuły przyjmijmy, że jest to portal informacyjny o wydarzeniach sportowych. W rzeczywistości była to inna branża, jednak chciałem zachować tu poufność. System ten:
- Pełnił funkcję portalu informacyjnego wystawionego w Internecie
- Umożliwiał posiadaczom biletów oraz wszystkim innym odwiedzającym sprawdzenie podstawowych informacji o wydarzeniu sportowym
- Wspierał trzy rodzaje użytkowników: gość, gość z numerem biletu oraz posiadacz konta
- W zależności od rodzaju użytkownika serwował informacje na innym poziomie szczegółowości
- W przypadku zalogowanych użytkowników posiadał moduł notyfikacji
- W celu pobrania informacji o wydarzeniach komunikował się z kilkoma innymi systemami (innymi w zależności od rodzaju użytkownika)
- Musiał być SEO friendly
- Pełnił również rolę serwera autoryzacyjnego dla innych systemów dla tego samego klienta
Proces identyfikacji Driverów architektonicznych
Wymagania funkcjonalne
Zbieranie wymagań funkcjonalnych jest czymś oczywistym na wczesnym etapie projektu. Istnieją różne techniki jak chociażby Event storming. W mojej organizacji akurat wymagania zbierane są przez osobny działa analizy. Specyfikacja wymagań została przekazana zespołowi w postaci gotowego dokumentu. Sytuacja zastana, zero dyskusji.
Analizując wymagania funkcjonalne wyróżnione zostały następujące drivery:
- Krytyczność, mierzona dostępnością, różnych funkcji systemu nie jest jednakowa dla każdej funkcji
- Obciążenie różnych funkcji systemu nie będzie jednakowe
- Możliwość skalowania per moduł, a nie per cały system
- System musi być podzielony na wiele niezależnych jednostek wdrożeniowych
Zapadła decyzja, że najlepiej sprawdzi się architektura mikroserwisowa. W skład systemu miały wejść:
- Aplikacja frontend’owa. Ponieważ system miał być SEO friendly odpadała Single page. Zdecydowano, że wystarczy standardowy wzorzec MVC
- Trzy niezależne aplikacje backendowe wystawiające frontendowi API REST
- Serwer autoryzacyjny oAuth
Dostęp do serwisów backend’owych miał być zrealizowany za pośrednictwem service discovery.
Na początek dwie instancje frontend’u (load balancing z Apache + mod_jk i sticky session).
Atrybuty jakościowe
Ciekawiej zrobiło się gdy przyszło do identyfikacji driverów grupy drugiej „atrybuty jakościowe„. Niestety stwierdzenia „system ma być wysoko dostępny”, „system ma być bezawaryjny” brzmią bardzo fajnie w ustach managerów ale są kompletnie bezużyteczne, gdy próbujemy sporządzić SLA (Service Level Agreement). Niezbędne były metryki jakościowe. Wypracowane zostało następujące rozwiązanie. Zidentyfikowane zostały najczęstsze i najbardziej kluczowe scenariusze użycia. Każdy z nich składał się z 1 do n kroków. Kosztowność wykonania każdego z kroków była różna. Zaplanowane zostały testy wydajnościowe, które wyliczały średnią z czasu wykonania każdego kroku scenariusza. Przyjęto, że akceptowalne są wartości mniejsze niż 2 sek na średnią każdego kroku. Był to nasz atrybut jakościowy. Oczywiście kolejnymi atrybutami jakościowymi były: brak wyjątków czasu wykonania powodujących fail scenariuszy testowych oraz poprawność biznesowa wykonywanych scenariuszy.
Ograniczenia projektowe
W przypadku ograniczeń projektowych były one podzielone na dwie grupy:
- Ograniczenia w naszej firmie
- Ograniczenia narzucone przez klienta
Nasza firma specjalizuje się w Java. Naturalnie wybrano ten język programowania. Postanowiono użyć Springa, gdyż developerzy najlepiej go znali i potencjalnie najłatwiej znaleźć na rynku ludzi z doświadczeniem w tym framework’u. Wybór PostgreSQL na silnik bazodanowy również wynikał z faktu, że nasz dział DBA posiadał największe kompetencje w pracy z tą bazą.
Także stos technologiczny miał wyglądać (w dużym skrócie) następująco:
- SpringBoot
- Thymeleaf
- Feign
- Ribbon
- Eureka
- PostgreSQL
Mikroserwisy uruchamiane z jarów. Typowy stos oparty o Springa. Co tu mogło pójść nie tak, pomyślicie :D.
Ze swojej strony klient zastrzegł, że infrastruktura produkcyjna będzie zarządzana przez ich administratorów. Oznaczało to dla zespołu wykonawczego tyle, że należy przywiązać szczególną uwagę do dokumentacji wdrożeniowej oraz przygotować rekomendacje potrzebnych komponentów najbardziej szczegółowo, jak to możliwe.
Konwencje i Cele projektowe
Drivery z tych grup pominę na potrzeby tego artykułu, gdyż odegrały one najmniejszą rolę.
Co się dzieje, gdy odkrywasz nowy driver, gdy system jest już gotowy
Zespół zabrał się ochoczo do pracy. W pewnym momencie uznano, że należy poprosić klienta o te ich maszynki.
Jakie było zaskoczenie, gdy okazało się, że klient ma w swoich szeregach architekta, który mentalnie został głęboko w latach dziewięćdziesiątych poprzedniego stulecia.
Pojawiły się nowe drivery!!!
Okazało się, że aplikacje nie mogą być odpalane z jara ale muszą być zdeployowane na Tomcat’ach.
Żaden problem. Dajcie po prostu osobne wirtualki z osobnym Tomcat’em na każdy komponent aplikacji. Żeby zapewnić HA potrzeba po dwie instancje każdego komponentu.
Okazało się niestety, że maks co można otrzymać to cztery wirtualki, a na każdej jeden Tomcat. Dwa węzły frontend’u i dwa węzły backend’u, żeby zapewnić HA.
Zaczęły się schody. Trzeba było upchnąć 3 aplikacje backend’owe na jednego Tomcata. Oznaczało to, że należy je udostępnić w różnych kontekstach innych niż ROOT. Eureka niestety tego nie wspiera. W zasadzie, to jeśli infrastrukturę mamy i tak zalaną betonem i nie ma mowy o łatwym dodawaniu kolejnych węzłów, to czy potrzeba tak naprawdę service discovery?
Dostarczanie paczki wdrożeniowej
Gwoździem do trumny okazał się fakt, że klient chce dostawać jedną paczkę wdrożeniową z nadaną wspólną wersją. Continous delivery? Nie, nie panowie. No to ma być taki zip opatrzony wersją, którego będziecie wrzucać na ten oto serwer FTP, a tutaj jest szablon dokumentu, który musicie wypełnić za każdym razem, gdy wychodzi nowa wersja.
Tak oto umiera architektura mikroserwisowa. A przecież doradzali: monolith first!!!