Łukasz Szydło, Jakub Pilimon, Jakub Kubryński
- Architektura ma wiele definicji. Jedną z nich jest spektrum Wieża ≤—-> agile. Wieża to typowe korpo, wszystko narzucone z góry. Agile to totalny luz, “architektura zrobi się sama”. Agile może zrodzić “przypadkową architekturę”, czyli kiedy system wyraża w jakich okolicznościach został stworzony, a nie w jakim celu. Zazwyczaj dobre rozwiązania znajduje się gdzieś po środku
- Tworzenie oprogramowania jest kierowane driverami architektonicznymi. Występują w postaciach takich jak wymagania funkcyjne (Co ma robić, reguły biz. etc), ograniczenia (budżet, czas, wiedza)
- ^ Drivery architektoniczne są ogólne. Żeby sprowadzić je do konkretnych wartości, potrzebujemy konkretnych metryk. Dobre metryki powinny posiadać cechy: aktualna wartość, historyczna wartość (do policzenia trendu), limit, cel (może też być progiem bólu), ideał. Metryki mogą być jakościowe (im więcej tym lepiej, oddalamy się od czegoś, np. szybkość backupu) i dłużne (im mniejsza wartość tym lepiej, np. max koszt backupu),
- Im więcej heurystyk do odkrywania subdomen znamy i mamy w nich doświadczenie, tym lepiej. Bo jedne lepiej sprawdzają się dla istniejących rozwiązań, a inne dla greenfieldów. Odkrywać subdomeny można za pomocą:
- Struktury organizacji (zamówienia, magazyn, katalog, dostawy, fakturowanie itp.)
- Eksperci domenowi (magazynier, księgowa, obsługa klienta),
- Język ekspertów domenowych (książka w kontekście magazynu, katalogu, pozycji na fakturze) ORAZ kto jest kim w danym kontekście (kupujący nie musi być adresatem, ani płatnikiem (bo płatność to np. jakiś bon, ktoś kto odbiera))
- Wartość biznesowa (klienci korporacyjni, klienci indywidualni. Proces z pozoru może wyglądać tak samo, ale w praktyce i dla biznesu to całkiem inne podejścia, inne kroki)
- Kroki procesu biznesowego (katalog ⇒ koszyk ⇒ zamówienie ⇒ płatność ⇒ dostawa)
- Cechy dobrego Big Picture Event Stormingu:
- szybki
- prosty
- stopniowy
- uwidacznia braki
- synchronizuje wiedze
- Jak biznes zapyta się o nowy feature to zamiast wynajdywać problemy, lepiej powiedzieć ile może zająć znajdywanie odpowiedzi na te pytania (jakie spike trzeba będzie zrobić)
- Biznes dodaje nowe feature’y bo patrzy z perspektywy usera. Nie widzi tej złożoności pod spodem, bo jak ma ją widzieć?
- Metryki mogą być biznesowe (ilość userów) albo techniczne (ilość błędów na produkcji). Metrykami wyrażonymi w liczbach bardzo dobrze rozmawia się z biznesem. Budują zaufanie.
- Niektóre subdomeny są na tyle generyczne, że można je zakupić i dostać wartość z pudełka,
- Rozdzielanie subdomen i separowanie modeli opłaca się. Wtedy nie tworzymy niepotrzebnej złożoności w miejscach gdzie ona realnie nie występuje. Lepiej mieć trzy mniejsze modele, skrojone pod domenę, niż 1 duży, który będzie próbował ogarnąć wszystkie stany (przykład z książkami w kontekście magazynu, katalogu, faktury)
- Bounded contexty tutaj to te szare kwadraciki. One trzymają modele. Granicami bounded contextów jest zmiana znaczenia modelu.
Subdomeny mogą na siebie nachodzić (komunikować się przez określone API)
- Ubiquitous Language (wszechobecny język). Język używany w kontekście subdomeny, czyli żyjący w kodzie, używany przez biznes, w marketingu itp. Nie ma jednego ogólnego, jest ich N, per domena.
- W idealnym świecie powinniśmy mieć 1 subdomenę do 1 bounded contextu (czasami do 2-3 BC)
- Walidowanie granic bounded contextów:
- Autonomia Contextu - czy BC może sam podejmować decyzje, czy musi pytać o zgodę kilka innych?
- Liczba kontekstów w procesie biznesowym - przez im mniej kontekstów przechodzimy tym lepiej,
- Informacje zmieniające się razem,
- Informacje używane razem,
- Zadanie pytań:
- Jaka jest odpowiedzialność tego kontekstu (ile zdań?),
- Ile ma integracji zewnętrznych/wewnętrznych?
- Dlaczego ma tyle integracji?
- Czy ma jedno źródło prawdy?
- Czy nie ma schizofrenii (if/else w funkcji za każdym razem, żeby odnaleźć się w jakim kontekście jest wykonywana)
- Process Level Event Storming - Robiony w mniejszej grupie, z konkretnymi ekspertami. Tak samo jak w Big Picture ES, mamy aktory, eventy, systemy zewnętrzne. Zagłębiamy się w subdomenę i poznajemy jak ten kawałek działa. Mogą powstać nowe, nieoczywiste subdomeny. Walidujemy wyniki przy pomocy technik wymienionych wyżej :)
- Szyna ESB - wzorzec implementacji systemu rozproszonego. Wszystkie serwisy komunikują się ze sobą po jednej szynie. Szyna wyznacza kontrakt i sama decyduje gdzie jakie dane trafiają. Serwisy mają tylko pojęcie o kontrakcie. Cholernie droga, jak szyna jebnie to wszystko stoi,
- Autonomia może być:
- biznesowa - np. osobna obsługa B2B i B2C. Pozwala na inny cykl wydań, większa specjalizacja przekłada się na mniejszą złożoność (mniej if’ów)
- techniczna - różna skalowalność lub wymagania bezpieczeństwa, kwestie prawne
- technologiczna - różne języki, toole,
- Architektura warstwowa (CRUD):
- Plusy:
- powszechnie znany,
- mało złożony,
- separacja odpowiedzialności (np. to gdzie zapisujemy nie zależy od tego jak zostało to przetworzone)
- Minusy:
- Zmiany we wszystkich warstwach naraz,
- testowalność (jeśli chcemy przetestować tylko jedną warstwę)
Architektura heksagonalna:
- Architektura heksagonalna:
- Zalety:
- Testowalność,
- Rozwijalność & utrzymanie,
- Bardziej domenowe nazewnictwo (+ dostosowany poziom abstrakcji)
- wymiana technologii,
- I/O jest na końcach hexagonu - czyli czyste jądro, sama logika biznesowa.
- Wady:
- Ilość kodu,
- mniej znana od arch. warstwowej,
- złożoność w porównaniu z warstwową,
- trzeba wiedzieć w jakim środowisku jaki adapter jest używany + trochę cięższa nawigacja po kodzie
- Architektura Pipes and Filters, np. express.js
- Zalety:
- Prosty,
- Testowalny,
- Rozszerzalny,
- Możliwość zrównoleglenia,
- Możiwość rozproszenia (np. na poziomie systemów, ale to jest trudne)
- Wady:
- Trzeba dobrze podejść do obsługi błędów, bo można nieźle namieszać (najlepiej globalny error catcher, lub trudniejsze - podawanie errorów dalej - choć wtedy następne filtry muszą wiedzieć o tym)
- Architektura typu mikrojądro (np. VSC):
- Architektura typu mikrojądro:
- zalety:
- rozszerzalność - pluginów może być wiele,
- konfigurowalność,
- testowalność - osobno testowane jądro i osobno testowane pluginy
- wady:
- skalowalność - pluginów może być wiele, ale jądro jest jedno. Wszystko co w jądrze, działa w przestrzeni jednego modułu,
- Złożoność - API, konfig i core muszą być dobrze przemyślane
- Piramida testów aplikuje się praktycznie tylko do modułów głębokich. Dla CRUD’a to najczęściej kwadratowy kloc, dla jakiegoś Full Text Searcha to będzie diament (integracyjnych najwięcej), albo nawet odwrócona piramida testów.
- Design Level ES, ostatni, najniżej poziomowy z 3 innych
- Jeśli mapowanie wygląda tak:
To prawdopodobnie tutaj złożoności nie ma i jest to zwykły CRUD (często w domenach supportowych)
- Jeśli wygląda tak:
To tutaj jest dużo złożoności i pewnie lepiej użyć hexagona niż 3 warstwy (często w core domain)
- Implementacje modelu domenowego (czyli przełożenie karteczek na kod). Głownie realizowane na 3 sposoby:
- Transaction Script - wołamy praktycznie samego SQL’a (albo noSQL’a) i na nim operujemy,
- Anemic Domain Model - Praktycznie to samo co Transaction Script, ale na wyższym poziomie abstrakcji. Realizowane przez ORM, więcej operacji w danym języku programowania. Zmapowane 1:1 z tabelami/kolekcjami,
- Rich Domain Model - klasy, które enkapsulują jakieś dane i wystawiają odpowiednie metody
- Odkrywanie domen, używanie Ubiquitous Language - to wszystko mocno straci na wartości jeśli w kodzie, gdzie jest duża złożoność zastosujemy Anemic Domain Model (Czyli np. cały złożony proces pauzowania subskrypcji opiszemy za pomocą
.get()
,.set()
etc. i to ukryje miejsca gdzie będziesubscription.unblock()
) - Agregaty grupują bogate modele i robią z nich spójne całości. Zapewniają transakcyjność. Agregat nie powinien zawierać żadnych innych agregatów. Agregat jest pewną granicą w wykonaniu się funkcji.
- Encja w DDD to to reprezentacja czegoś np. użytkownika, czy auta w danym kontekście. Ale nie musi być mapowana 1:1 na tabelę w SQL.
- jest mutowalna,
- Dlatego trzeba zapewnić bezpieczną możliwość mutacji (czyli nie ma getterów, setterów)
- ma tożsamość (
ID
) - Niezmienniki kontekstowe
- Niezmienniki stałe (deffered validation, czyli czy ID jest ustawione (w Javie wszystko może być nullem), czy jakieś inwariancje itp. Zazwyczaj wykonują się na końcu wykonywania niezmienników kontekstowych)
- Jeśli encja zbytnio się rozrośnie (np. subskrypcja ma wiele stanów - typu z aktywnej może zostać zablokowana, albo “wygaśnięta”, ale nie może przejść w drugą stronę, zmieniają się akcje jakie mogą zostać dokonane - to można to rozbić na parę klas jak
PausedSubscription
,ActiveSubscription
,DisabledSubscription
) - ^To nawet zwiększy czytelność kodu i będzie się można pozbyć ifów. Np. Serwis który będzie robił coś na AktywnejSubskrypcji nie musi sprawdzać czy jest aktywna, bo jeśli nie jest to typ się nie będzie zgadzał i kompilator się wywali
- Kolekcje warto opakowywać w Klasy bo:
- Nie zawsze można dokonać wszystkich typowych operacji na danej kolekcji
- Stosujemy typowy język domenowy
- Value Object to wartość, atrybut. Czasami może być prosty jak np. opis, który będzie tylko stringiem, a czasami osobnym obiektem, gdy będzie bardziej złożony. Np. Jakaś cena w EUR - nie chcemy aby można ją było łatwo dodać do czegoś innego, to powinien być jakiś object który rozumie, że ma wartość X w walucie Y, i żeby dodać to do innej waluty to muszą być podjęte jakieś kroki, zaenkapsulowane w np.
money.add(…)
- najlepiej niemutowalne,
- nie ma tożsamości (5 to zawsze 5)
- Sprawdzają niezmienniki domenowe (np. Dodawanie EUR do PLN)
- Mogą walidować jakieś wewnętrzne reguły spójności
- Mogą sprawdzać proste zachowania (
.canAccept()
) - ^ przez to powinny być łatwe i zwięzłe do testowania
- Zamiast zawsze rzucać wyjątek i łapać go gdzieś wyżej, można zwrócić otypowany rezultat. Wtedy przepływ będzie liniowy (nie łapiemy wyjątku gdzieś 3 warstwy wyżej) i ktoś kto wywołuje daną metodę będzie sam mógł podjąć decyzję co dalej.
Rodzaje zdarzeń
Zewnętrzne:
- emitowane pomiędzy Bounded Contextami
- więc stają się kontraktem
- dlatego muszą być wersjonowane
- Proste, zawierające potrzebne innym minimum
- niemutowalne
- mają ID, mogą mieć przyczynę wygenerowania
Wewnętrzne
- Wewnątrz Bounded Contextu (danego serwisu, może to być w pamięci)
- kompilator pilnuje kontraktu
- więc nie muszą być wersjonowane, mogą być często zmieniane
- mogą zawierać jakieś bardziej zaawansowane koncepty
- niemutowalne
Publikowanie zdarzeń
Zazwyczaj serwis aplikacyjny (albo komenda) wyciąga agregat za pomocą repozytorium. Na agregacie sprawdza niezmienniki (polityki - żółte kartki), wywołuje jakąś akcję i zapisuje agregat. Jeśli się udało, publikuje zdarzenia:
- mogą być trzymane jako wewnętrzna kolekcja,
- zwrócone jako kolekcja z agregatu,
- wołane z statycznej klasy (uwaga - przy naiwnej implementacji to może publikować zdarzenia, a jak coś jebnie na koniec to trzeba będzie rollbackować te zdarzania)
- łatwa implementacja (+)
- niejawne efekty uboczne w jednej transakcji (-)
- wiązanie - jeśli serwis mailowy się wywali, transakcja się wywali (-)
- złożona implementacja (-)
- deduplikacjia
- ponawianie
- problem wielu instancji
- brak wiązania (+)
- gwarancja dostarczenia (+)
- niezawodność
Gwarancje dostarczania
- At most once delivery
- 0 albo 1 raz
- At least once delivery
- od 1 do N razy
- idempotentność trzeba zapewnić
Serwisy
Domenowe:
- Trzymają logikę biznesową, np. SubscriptionService
- Dobrze przetestowane
- Bezstanowe
Infrastrukturalne/aplikacyjne
- Nie zawiera logiki domenowej
- Do komunikacji z zewnętrznymi systemami
- Zajmuje się logowaniem, transakcjami, autoryzacją, błędami (tłumaczenie errorów domenowych na jakieś jeszcze wyższe
Komendy
Jak się taki serwis rozrasta to warto zrobić z tego Command. Commandy można rozbudować o Handlery. Handlery obsługują tylko jedną komendę na raz. Handlery trafiają na CommandBusa
Coupling
- powinien być jawny
- coupling jest nieunikniony
- powinien być luźny
- lepiej żeby się rzadko zmieniał
- lepiej żeby miał jak najmniej połączeń z innymi modułami
- przywiązany do abstrakcji, nie do szczegółów implementacyjnych
Cohesion
To spójność w module. Jeśli dwie funkcje wpadły do jednego pliku, ale nie są dosłownie niczym połączone, to to jest kohezja przypadkowa. Jeśli dane klasy wykorzystuje swoja pola między metodami, które mają jakieś wspólne znaczenie to dobry znak - kohezja wysoka :)
Low of Demeter - powinniśmy projektować API class w taki sposób, żeby nie można było z niego wyciągać szczegółów implementacyjnych. Czyli nie wspieramy wyciągania rzeczy typu Subscriptions[0].payer.bornDate.toISOString()
Anomalie współbieżnego dostępu:
- dirty writes - nadpisujemy czyjeś dane w jednej transakcji.
- Poziom izolacji: read uncommitted
- dirty reads - czytamy dane jeszcze przed commitem innej transakacji.
- Poziom izolacji: read committed zapobiega temu, domyślny dla PostgreSQL.
- non-repeatable reads - w jednej transakcji, pytając o to samo dostajemy dwa różne wyniki
- Poziom izolacji: snapshot isolation zapobiega temu, w PostgreSQL nazywany jest repeatable read
- lost update
UPDATE user SET score = score + 1 WHERE id = 123
- atomowy zapis zapobiega temuSELECT FOR UPDATE (id, score) FROM users WHERE id = 123
- blokowanie pesymistyczne- blokowanie optymistyczne po stronie aplikacji (odczytujemy z wersją i potem zapis z 2 wherami
WHERE id = 234 AND version = 1
) - Phantom reads - odczytujemy ILOŚĆ wierszy np.
COUNT(*)
na początku i na końcu transakcji i dostajemy różne wyniki. W non-repeatable czytaliśmy konkretne wartości z wierszy, a tutaj dane z grupy. - Snapshot isolation poradzi sobie z tym
- Write skew - czytamy dany rekord i na podstawie wyniku dodajemy jeszcze jeden. Jeśli dwie równoległe transakcje zrobią to w tym samym czasie na tym samym rekordzie to skończy się 3 rekordami. To może być nawet na przecięciu paru tabel.
- Możemy temu zapobiec blokując pesymistycznie;
SELECT FOR UPDATE * FROM asd WHERE status != 'active'
SELECT (status, version) FROM subs WHERE ID = 234;
-- zwrócono wiersz z version = 1
UPDATE subs SET version = version + 1, status = 'PAUSED'
WHERE ID = 234 AND version = 1;
-- 0 wierszy zaktualizowanych - ktos byt szybszy, zwracamy błqd
-- 1 wiersz zaktualiuzowany - OK
CAP
CAP - Consistency, Availability, Partition tolerance.
Przykładowe pary:
- C + A - MySQL, Postgres
- A + P - Cassandra, CouchDB
- C + P - Redis, MongoDB
BASE
Basically Available - wysoka dostępność
Soft state - wystarczająco świeże
Eventual consistency - odroczone osiągnięcie spójności. Często opisywane przez NWR
- N - liczba węzłów klastra
- W - węzły do zapisu
- R - węzły do odczytu
Jednym z lepszych sposobów na zwiększenie skalowalności BD jest modularyzacja na poziomie aplikacji. Zamiast budować potężny monolit, który ma wiele szerokich tabel i ogromny ruch, lepiej wydzielić mniejsze, niezależne moduły (w kontekście DDD) i to już zwiększy wydajność.
Event Sourcing
Dzięki event sourcingowi, testowanie jest łatwe i bardzo naturalne. Odtworzenie stanu (Given) jest po prostu załadowaniem sekwencji wydarzeń (strumień). When to operacja. Then to zapisane eventu.
Przykładowa implementacja:
eventType
i eventData
są związane z “biznesowymi danymi”. Reszta jest ustawiana na wejściu, podczas enrichmentuModel Odczytu
Klasyczny odczyt będzie trudniejszy do zrealizowania:
- Widoki z informacjami z różnych eventów,
- FTS,
- raporty przekrojowe (przechodzące przez wiele tabel)
Wiele problemów da się rozwiązać przez modularyzację (BD pod konkretne moduły). Również pobieranie do 100-200 eventów nie zawsze jest problemem.
Można też bardziej stworzyć bardziej kompozycyjny UI (microforntendy i każdy komponent pyta o swoje dane), albo zrobić Backend For Frontend
Do rozwiązywania problemów FTS czy sięgania po wiele danych, pomóc nam może publiczny read model (zewnętrzny).
Można stosować różne triki, jeśli musimy użytkownikowi pokazać spójność końcową:
- Wyświetlić jakąś stronę z podsumowaniem,
- Jakiś modal który będzie musiał potwierdzić wykonanie akcji,
- Jakaś ładna animacja ładowania
Wersjonowanie eventów
Dosyć kłopotliwe, bo eventy muszą być niemutowalne. Jest parę podejść.
- Silny schemat - Mapować stare eventy na nowe. Czyli jeśli mamy wersję v1 to jest mapper na v2. z v2 na v3 itp. Ale to się słabo skaluje,
- Słaby schemat - Wykorzystanie tego, że JSON zawiera strukturę & dane jako wartość. Na podstawie tego można wnioskować:
- Copy and replace - kopiujemy nową strukturę (czyli mamy 2 streamy reprezentujące to samo). Następnie usuwamy stream stary stream.
- copy and transform - kopiujemy cały event store, tylko że już z poprawionymi wartościami
Tipy dot. wesjonowania:
- Event store można robić na biznesach/modułach, które są wygrzane. To pozwoli uniknąć częstych breaking changes.
- Albo przeprowadzić dogłębny storming.
- Trzymać w odpowiednio małych agregatach
Wydajność
Wiele zdarzeń
Czasami jak mamy wiele eventów w jednym streamie (np. draft blogpostu, koszyk zakupowy), to ciężko się wyciąga te wszystkie eventy i składa ostatnią wersję.
Można wykorzystać snapshoty eventów do zwiększenia wydajności. Co N eventów (albo co jakiś czas) robimy specjalne zdarzenie, które jest pochodną poprzednich. Wtedy najpierw pytamy się o snapshot, potem o pozostałe eventy.
Duże zdarzenia
Np. przygotowanie jakiejś umowy kredytowej.
Rozbijanie:
- Task-based UI - zamiast myśleć w CRUDowy sposób (Edytowano umowę) to podzielić to na mniejsze taski, kroki.
- Weryfikacja niezmienników - czy aby na pewno wszystko musi znaleźć się w tym jednym agregacie (i w następstwie - eventcie)?
Operowanie na Diff-ach (jeśli na prawdę mamy tak duży event) - co konkretnie się zmieniło od ostatniego czasu
Dużo zapisów w krótkich odcinkach czasu
Np. Transakcje giełdowe, IOT
- Można trzymać agregat w pamięci (Redis)
- cache zdarzeń + sticky session
Dużo zdarzeń w bazie
np. po 5-10 latach, a mam pełno małych eventów.
- Cold storage - czy na pewno potrzebuje szybki dostęp do danych sprzed N lat?
- Kompaktowanie - jeśli ktoś edytował nazwę 3x razy to po jakimś czasie może nas to już nie obchodzić i można zostawić tylko ostatnie zdarzenie
Systemy rozproszone
Pierwszym kryterium przy rozpraszaniu powinna być autonomia rozwoju od strony biznesowej. To w tym będzie największa wartość.
Rozdzielenie na techniczne serwisy (wysyłanie maili, generowanie PDFów itp) ma swoje zalety:
- Skalowalność i zasoby
- odporność (jeśli będzie jakiś wyciek pamięci, to nie wywalimy 2 rzeczy tylko 1)
- Różne technologie (nie trzeba wielu libek w wielu językach)
Service-Level Agreement (SLA) - To na co klient się zgadza. Czyli podpisuje umowę, że nasz serwis będzie dostępny przez np. 99.9% czasu, a za złamanie tego, my ponosimy karę np. dajemy rabat klientowi,
Service-Level Objective (SLO) - To na co wewnętrznie się zgadzamy. Czyli np. 99.95% dostępności. Jest trochę bardziej restrykcyjne, ale za lekkie złamanie w granicach SLA nic nam nie grozi.
Service-Level Indicator (SLI) - Wskazanie SLO i SLI, zazwyczaj na jakimś dashboardzie. Np. Opóźnienie żądań do serwera
Z perspektywy użytkownika, dostępność liczona dla pojedynczego serwisu często nie ma sensu. Powinno być to liczone na procesie, np. Zalogowany użytkownik dokonuje zakupu nowej subskrypcji. Wtedy trzeba policzyć i rozłożyć dostępność między serwisami (niekoniecznie równomiernie).
temporal coupling - jedna aplikacja wymaga drugiej do działania. Występuje gdy komunikacja jest synchroniczna, np. microserwisy walą do siebie po koleji po HTTP/czymkolwiek innym. Można rozwiązać odpowiednią komunikacją asynchroniczną (eventy, przetwarzanie w kolejkach)
Komunikacja
Design for failure
Z perspektywy użytkownika, szybciej walnąć błędem niż przeciągać odpowiedź w oczekiwaniu, że N-ty retry, po jakimś czasie pomoże.
Sposoby na unikanie errorów wynikających z niedostępności serwisu:
- cache,
- wartość domyślna (np. ogólne rekomendacje zamiast spersonalizowane)
- retry (ale tylko szybki i prosty)
- komunikacja asynchroniczna
- testowanie za pomocą tzw chaos monkey - losowe serwisy są wyłączane i patrzymy jak zachowa się cały system
Style komunikacji
Request-reply
W reaktywnym programowaniu - message driven. Jak oczekujemy na odpowiedź.
Fire & Forget (One-way)
W reaktywnym Event driven. Jak emitujemy event i nie obchodzi nas odpowiedź.
Przykładem może być kolejka dla eventów (point-to-point), albo kolejka dla komend (wiele serwisów pcha do jednej kolejki komendy o wybranym kształcie. np. wyślij mi maila, tu masz JSONa z danymi, tu masz czas, do kogo itp. Daje to bufor.
Poison message - coś czego nie jesteśmy w stanie przerobić np. w Outbox patternie i ciągle to pobieramy, wywala się i zapisujemy jako nie wysłane. Najczęściej oznacza się taką wiadomość jako poisoned. Takie wiadomości trafiają na dead letter queue i muszą zostać ręcznie obsłużone przez kogoś (np. system wysyła maile że nie potrafi przetworzyć czegoś, albo jakiś dashboard)
Tracing
Logowanie powinno spływać do jednego miejsca. Prócz oczywistości (np. czasu) log powinien zawierać:
- Trace ID - pokazuje cały przepływ procesu
- Span ID - wszystkie wpisy dla żądania
- Parent Span ID - identyfikator tego co wywołało żądanie
Jest to B3 Propagation standard - headery X-B3-{element np. <traceId>}
Rozproszone sagi
Rozproszona saga - transakcja tylko przez wiele mikroserwisów. Podejście wszystko albo nic. Zamiast rollbacków, powinniśmy używać idempotentnych kompensacji (czyli nie “przelej 100zł” → error → “usuń wpis o wpłacie” tylko “przelej 100zł → error → “zwróć 100 zł” ). Są tego osobne narzędzia jak Netflix Conductor. Jeśli często musimy używać tego wzorca to prawdopodobnie coś źle zaprojektowaliśmy.
Jakość komunikacji
To wyżej to była stabilność transportu:
- sieć,
- topologia,
- klienty,
Teraz będzie o stabilności treści:
- URLe,
- treść dokumentów
Strong Typed w tym kontekście to wyciąganie i serializowanie wszystkiego z odpowiedzi. Np. przez współdzielenie typów.
- Plus: Jak sparsujemy to mamy pewność co gdzie jest,
- Minus: jak zmieni się coś czego nie potrzebujemy i nie będziemy mogli tego zdeserializować to możemy dostać rykoszetem
Be conservative in what you send, be liberal in what you accept - Jon Postel
Weak Typed - ciągniemy tylko to co potrzebujemy.
- Plus: Większa autonomia, nie wywala się to co nie potrzebujemy
- Minus: Tracimy spójność komunikacji
E2E
Jeśli mamy dobrze zaprojektowaną apkę - ma fallbacki, cache, moduły niezależnie funkcjonują - to paradoksalnie może to doprowadzić to do false-positive testów E2E, bo zadziałają jakieś fallbacki w miejscach gdzie tego nie chcieliśmy, albo dostanie dane z cache’a, albo screen będzie z jakimś błędem na boku, ale główna treść jakoś tam przejdzie.
Fail-safe vs Safe to fail
Fail-safe - robimy wszystko, żeby nie wydać błędu na produkcję. Restrykcyjne pipeline-y, bardziej dokładne testy E2E, więcej manualnych itp. Zakładamy, że wiemy gdzie coś może wybuchnąć.
- Plus: Ciężej wydać jakiś szit
- Minus: Rzadsze wydania
Safe-to-fail - jesteśmy mniej restrykcyjni niż w fail-save, ale mamy na tylko dobry monitoring i szybką możliwość fix-a (szybki proces wydawniczy + dostępni developerzy do naprawienia błędu). Odwrotność fail-safe.
W dzisiejszych czasach można mieć najlepsze z dwóch światów. Pipeline może dawać 80% wartości w 20% czasu, a dobry monitoring jest niezależny.
Testy wydajnościowe
To często pic na wodę. Bardzo ciężko realnie odwzorować obciążenie produkcyjne w realnych warunkach. Indexy na BD, optymalizacje JVM, zapełnianie cache-y … Bardziej opyla się sprawny monitoring i wdrażanie małych, łatwych do wychaczenia zmian.
Monitoring
Obserwowalność to cecha system, pozwalająca na określenie jego wewnętrznego stanu na podstawie udostępnianych wyników. Sam monitoring to kontrola zdrowia systemu oparta o obserwowalność. Na podstawie monitoring można robić alerting
Monitoring is the new testing.
Application Performance Management - ciągły monitoring aplikacji na produkcji. Są do tego toole (np. Datadog, New Relic). Potrafią odpowiedź z dokładnością do linijki kodu gdzie coś spowalania. Mogą zwolnić działanie systemo 1-3%, ale warto.
Logi
Zamiast robić coś w stylu:
logger.log(`Użytkownik ${email} dodał produkt ${productId} do koszyka`)
Lepiej zrobić
logger.log('Użytkownik dodał produkt do koszyka', { email, productId })
Lepiej się po tym szuka, nie trzeba żadnych dziwnych regexów.
HA
war gaming - można pobawić się na stage-ingu, że jedna osoba psuje jakąś rzecz, a zespół musi dojść do tego co się zepsuło. Np. zatrzymanie BD, drop części danych itp. Pozwala to na zweryfikowanie procesów awaryjnych w kontrolowanych warunkach.
Ważne w kontekście HA jest duplikacja pewnych mechanizmów bezpieczeństwa (fallback). Tak jak samolot ma parę mechanizmów zabezpieczeń, tak i apka powinna umieć się bronić. Np. jak jakiś zewnętrzny serwis często pada, to może spróbować z cache-u dane zaciągnąć, albo trzymać jakieś najpopularniejsze dane lokalnie, itp.
Disaster recovery
Mój luźny pomysł: Można puszczać testy E2E na produkcji, które sprawdzają najważniejsze funkcjonalności co np. N godzin. Pozwoli jeszcze szybciej powiadomić o jakimś problemie (+ wiadomo odpowiedni monitoring + alerting).
Metryki ciągłości działania
Recovery Point Objective (RPO)- dopuszczalna utrata danych,
Recovery Time Objective (RTO) - maks. oczekiwany czas przywrócenia
Maximum Allowable Outage (MAO) - maks. dozwolony czas niedostępności systemu
Refaktoryzacja
Fajny przykład użycia Anti-Corruption Layer (ACL):
Teraz środek modułu nie wie, że autor to jakiś user, mający uprawnienia i odpowiednie statusy
Refactor do serwisów
Strangler pattern
Stawiamy nowy serwis. Nowe featurey są tam dodawane, a stare systematycznie migrowane.
Strangler jest spoko jak mamy dużo wejść do serwisu (np. requesty trafiają do niego prosto z HTTP). Ale nie sprawdzi się najlepiej kiedy mamy dużo wywołań ze środka monolitu, pośrednio.
Branch by abstraction
Robimy sobie abstrakcje w monolicie przed wywołaniem. Następnie można stworzyć serwis poza monolitem (Katalog po prawej). Requesty przychodzące do starego monolitu, trafiają do abstrakcji. W abstrakcji może być feature swich który przepina ruch za pomocą jakiegoś klienta do nowego serwisu.
^ tutaj problem może być synchronizacja. Bo mamy dwie apki, które rozdzielają się na poziomie sieciowym, nie transakcji bazodanowej. Można to rozwiązać za pomocą Sagi albo Store and Forward. A jeśli nowy serwis ma korzystać z tej samej technologii BD to można dane w tej samej bazie pisać, tylko połączyć transakcją.
Security
Intrusion Detection System IDS- wykrywa sytuacje gdy jakiś user pobiera więcej danych niż zazwyczaj.
Intrusion Prevention System IPS - blokuje danego użytkownika (może być IP) jeśli IDS coś wykrył