Główną rolą Modularnego Monolitu jest przygotowanie aplikacji do ewentualnego wydzielania serwisów z istniejących modułów. Nie zawsze jest to prosty proces, dlatego postanowiłem przyjrzeć się mu nieco bliżej na łamach bloga.
1. Rozglądnijmy się nieco szerzej…
Każdy moduł wewnątrz monolitu stanowi bardzo spójną wewnętrznie jednostkę kodu. Aby móc jednak wydzielić go na zewnątrz, powinniśmy spojrzeć na niego z perspektywy innej niż na co dzień – z perspektywy jego integracji z całą aplikacją. Dopiero, kiedy zlokalizujemy wszystkie elementy, z którymi współgra moduł, który chcemy wydzielić, będziemy mogli wziąć się za wydzielanie.
Na sam start pomyśl o Shared Kernelu
Pierwszą rzeczą, o której będziemy musieli pamiętać, jest to, że możemy korzystać z kodu należącego do części wspólnej, w DDD nazywanej Jądrem Współdzielonym (eng. Shared Kernel). Podczas wydzielania aplikacji musimy zadbać o to, aby zarówno aplikacja-matka oraz wydzielany serwis mogły dalej korzystać ze wspólnej bazy. Nie mam jednak na myśli wydzielania całego modułu Shared Kernel, tylko części wspólnej.
Spójrz na zależności 🙂
Ostatnim tematem, który chciałbym w tym miejscu poruszyć, jest zależność wydzielanego przez nas modułu od innych modułów znajdujących się w aplikacji. W tym przypadku bardzo dobrą praktyką jest utworzenie dodatkowego modułu łączącego obydwa światy – w świecie DDD nazywanego najczęściej mosetm. W sytuacji, kiedy most ma zależności do wydzielanego modułu, sytuacja jest zazwyczaj prosta. Zamieniamy wtedy komunikację z warstwą Aplikacji modułu na komunikację z warstwą Interfejsu Użytkownika. W sytuacji odwrotnej, kiedy to wydzielany serwis ma zależności do innego modułu (co w dobrze napisanej aplikacji nie powinno mieć miejsca), podczas wydzielania powinniśmy rozważyć wydzielenie obydwu modułów do poziomu osobnej aplikacji.
Pamiętaj o Hexagonie!
Drugą kwestią, o którą musimy zadbać, będzie obsługa warstwy Interfejsu Użytkownika, która korzysta z części aplikacyjnej wynoszonego na zewnątrz modułu. Mówimy tu przede wszystkim o komendach i zapytaniach, które są wykorzystywane w kontrolerach oraz skryptach konsolowych. W sytuacji, kiedy tworzymy aplikację w oparciu o klasyczne MVC, to istnieje szansa, że czas komunikacji synchronicznej z wydzielonym serwisem wpłynie na wydajność aplikacji. Jeżeli jednak tworzymy aplikację typu headless (backend wykorzystywany jedynie w formie API), to możemy spróbować zrobić redesign naszej aplikacji frontendowej. Zamiast komunikować się z aplikacją-matką pełniącą rolę proxy do wydzielonego serwisu, frontend może komunikować się z serwisem bezpośrednio.
Kolejnym będzie współpraca modułu z warstwą Infrastruktury, która najczęściej odbywa się poprzez porty pasywne, znane z Architektury Heksagonalnej. O ile same porty (w postaci interfejsów) należą zazwyczaj do modułu (czasami do Jądra Współdzielonego), to musimy zadbać, aby ich adaptery (implementacje tych interfejsów) znajdowały się również w aplikacji wydzielanego serwisu.
2. Zadbajmy o wybór odpowiedniej architektury
Przed konkretną pracą, powinniśmy przeanalizować wszystkie możliwości, które oferuje nam strona infrastruktury serwerowej.
Wydzielanie nowego API
Wydaje się, że najczęstszym wyborem jest wystawianie nowego serwisu w postaci udostępnienia API REST. Jeżeli decydujemy się na pozostanie na tym samym serwerze, to fizycznie oznacza to dodatkowe obciążenie dla serwera HTTP (standardowo NGINX lub Apache). Jeżeli wydzielany przez nas serwis będzie miał duże obciążenie, to lepszym rozwiązaniem będzie wydzielenie go na osobną jednostkę serwerową.
Niezależnie od tego, gdzie fizycznie będzie znajdował się nasz serwis, to musimy również zadbać o dostęp do zasobów z zewnątrz. Oznacza to, że musimy podpiąć go pod jakąś domenę. Możemy wybrać całkowicie nową domenę, aczkolwiek, zazwyczaj działa się w oparciu o subdomenę aplikacji, z której wydzieliliśmy serwis.
Następnym technicznym aspektem będzie kwestia autoryzacji do nowo opublikowanych zasobów. Do wyboru mamy pełen wachlarz możliwości. Jeżeli serwis nie wymaga autentykacji użytkownika, to możemy zrobić prostą autoryzację w ramach serwera HTTP (ang. Basic Authentication). W sytuacji, kiedy należy autentykować użytkownika (np. użytkownika aplikacji, sklep lub zewnętrzną aplikację), to będziemy potrzebowali sięgnąć do metod typu klucze API (ang. API Keys), OAuth2, JWT Tokens (ang. Bearer). Każdy sposób autoryzacji/autentykacji ma swoje wady i zalety, których powinniśmy być świadomi w momencie wyboru tej jedynej.
Ponieważ będziemy komunikować się przez protokół HTTP, to powinniśmy pamiętać również o certyfikatach, aby zminimalizować szansę na przechwycenie wrażliwych informacji.
Praca na kolejkach
Drugim sposobem na komunikację z wydzielonym serwisem będzie wykorzystywanie dobrodziejstwa systemów kolejkowych. Tego podejścia stosujemy, kiedy możemy pozwolić sobie na pracę asynchroniczną między serwisami. O ile komunikacja przez API najczęściej odbywa się w sposób synchroniczny (wysyłamy żądanie, po czym blokujemy wykonanie aplikacji do momentu odebrania odpowiedzi od serwera), to kolejki rządzą się prawem „Ok, przyjąłem. Za chwilę zrobię.”.
Praca na systemach kolejkowych polega na wysyłaniu zserializowanych wiadomości (bardzo często do formatu JSON) po protokole AMQP. Wiadomości zostają wysyłane do centralnego miejsca – instancji aplikacji systemu kolejkowego. Następnie jest ona analizowana pod kątem miejsc, w które powinna zostać rozdysponowana (kolejki). Po dyspozycji wiadomości do określonych kolejek, oczekuje ona na wykonanie „w swoim czasie” przez proces nazywany Consumer-em. Do jednej kolejki możemy mieć podłączonych wiele procesów nasłuchujących, dzięki czemu łatwiej możemy skalować aplikację w zależności od obecnego obciążenia.
Pracując na kolejkach, działamy na niepublicznym polu, zatem w przeciwieństwie do API, nie ma tutaj potrzeby autoryzacji dostępu. Pomimo prywatnego dostępu do wiadomości, pamiętajmy o skonfigurowaniu certyfikatów. Protokół AMQP, podobnie do protokołu HTTP, służy do przesyłania informacji między jednostkami serwerowymi. Jeżeli źle zabezpieczymy te dane (w tym wypadku nie skonfigurujemy SSL-a), to zostawiamy potencjalną furtkę niedobrym ludziom.
Miksowanie komunikacji po API z kolejkami
W obecnym wpisie rozważamy zupełne odseparowanie części aplikacji od jej całości. W takim razie wydzielony serwis będzie miał tylko tyle informacji o zasobach, ile zostanie mu wysłane w wiadomości. Dodatkowo, będziemy mieli informacje aktualne wyłącznie w chwili wysyłki tej wiadomości. Jest to dobra opcja, kiedy np. wydzielamy część realizującą notyfikacje do użytkowników. Jest to również super opcja, kiedy tworzymy wersjonowane zasoby.
Niestety, potrzeby biznesowe dalej są potrzebami biznesowymi i prędzej, czy później, zajdzie konieczność przetwarzania wiadomości z pracą na aktualnym zestawie danych. Przykładem tutaj może być dodatkowa weryfikacja, czy kontekst, w ramach którego działamy, w dalszym ciągu ma dostęp do funkcjonalności, w ramach której procesujemy wiadomość. Ponieważ systemy kolejkowe przyzwyczajają nas do pracy asynchronicznej, to nic nie stoi na przeszkodzie, aby móc wysyłać dodatkowe żądania HTTP wewnątrz procesu przetwarzającego wiadomość.
Niestety, miksowanie kolejek oraz API zazwyczaj wiąże się z podwojoną pracą po stronie warstw Interfejsu Użytkownika oraz tej typowo serwerowej.
Huston, mamy problem!
Wybór odpowiedniej architektury serwerowej nigdy nie oznacza pancerności na problemy w przyszłości. Nie jest tak, że kolejki są dobre, a API jest złe. Nie jest również na odwrót 🙂 Miksowanie API oraz kolejek również nie gwarantuje nam 100% pozbycia się problemów (a czasami jest wręcz dorzucanie nowych kłopotów do puli). Pamiętajmy, aby architekturę (w tym przypadku infrastrukturalną) dobierać do problemów, które planujemy rozwiązać. Zamiast rozpytywać „które rozwiązanie jest lepsze”, rozejrzymy się nieco za potencjalnymi problemami, z którymi możemy spotkać się podczas pracy w tym lub innym protokole.
I wracamy do SharedKernela 🙂
No tak, prawie zapomnielibyśmy o kodziku, który jest wspólny dla wydzielonego serwisu oraz aplikacji-matki. Jeżeli korzystamy z tych samych mechanizmów, tych samych klas itp., to najlepiej będzie tą bazę wydzielić do paczki Composera i wykorzystać w obydwu projektach.
Wydzielenie paczki wiąże się jednak z niebezpieczeństwem związanym z utrzymywaniem kompatybilności wstecznej dla wszystkich serwisów, które będą ją miały jako swoją zależność. Im więcej serwisów korzysta z paczki, tym większy problem będziemy mieli z pozbyciem się kodu, który generuje dług technologiczny. Dodatkowo sprawa może skomplikować się, jeżeli mamy kilka zespołów, np. jeden dla każdego serwisu. Wtedy dochodzi nam komunikacja międzyzespołowa, która do najłatwiejszych nie należy.
3. Wydzielamy, czas… start!
Kiedy rozglądnęliśmy się już wystarczająco szeroko, możemy przystąpić do finałowego etapu – wydzielania. Proces ten nie należy do najprostszych i za każdym razem wygląda inaczej, dlatego nie jestem w stanie go ustandaryzować oraz opisać na łamach bloga. Jest jednak jeden drobiazg, o którym chciałem wspomnieć, bo na pierwszy rzut oka nie wydaje się to jasne.
Wydzielenie serwisu to nie jest wycięcie kilku katalogów z jednego miejsca i wklejenie ich w inne miejsce. Zazwyczaj będziemy potrzebowali dodatkowych komponentów-mostów, nowych komend/zapytań CQRS-owych, dodatkowych adapterów i temu podobnych. Pamiętajmy, że wydzielamy nie dlatego, aby pozbyć się kilku katalogów w drzewie projektowym, lecz dlatego, że daje nam to konkretne korzyści (lepszy performance, bezpieczeństwo, separację pracy zespołów czy lepszą kontrolę nad procesami biznesowymi). Mniejsze i lepiej poukładane projekty są tylko przyjemnym efektem ubocznym tego bądź, co bądź – trudnego procesu.
Comments are closed.