Sporo ostatnio o SOA i mikroserwisach. Jednym z wyzwań podczas rozłupywania monolitu na serwisy jest wydajność. Wywołania in-memory są zastępowane np. HTTP lub innym zdalnym protokołem. Niesie to ze sobą kilka niedogodności m.in.:
1. Wydajność jest dużo mniejsza – serializacja, deserializacja, nawiązanie połączenia, transmisja danych.
2. Serwis może być nieaktywny.
3. Może wystąpić timeout.
Powyższe punkty mogą być wyjątkowo niebezpieczne, gdy wiele usług próbuje wywołać serwisy, które aktualnie nie odpowiadają. Każde wywołanie niesie ze sobą zużycie zasobów. Jeśli zbyt długa kolejka nieudanych wywołan zostanie skumulowana, kolejna usługa może przestać odpowiadać. Przypomina to efekt domina, gdzie jedna usługa próbuje nieustannie połączyć się z drugą, która aktualnie nie odpowiada. Przez zbyt dużą liczbę nieudanych połączeń, taka usługa sama przestaje odpowiadać, powodując takie same efekty na innych usługach.
Wzorzec Circuit Breaker, ma na celu zminimalizowanie efektu, gdy część usług nie odpowiada, a wiele klientów chce do nich uzyskać dostęp. Nie ma sensu stosować wzorca, gdy wiemy, że nie mamy dużej ilości wywołań na sekundę. W takiej sytuacji, timeout nie zaszkodzi i nie spowoduje wyczerpania zasobów.
W przypadku, gdy mamy setki wywołań, wtedy lepiej, gdy np. pierwszy klient ostrzeże innych o tym, że i tak nie ma sensu łączyć się z daną usługą, ponieważ ona nie odpowiada. I to jest właśnie cała istota wzorca. Stanowi on punkt między konsumentami, a usługą. Jeśli wszystko jest OK to obwód jest zamknięty (circuit). W momencie, gdy nastąpi timeout lub inna zdefiniowana awaria, wtedy obwód jest otwierany tak, że inni klienci dostaną natychmiastową odpowiedź, że nie można połączyć się z daną usługą. Nie będziemy musieli czekać na timeout, co pochłania ogromne ilości zasobów (w przypadku akumulacji połączeń).
Kolejny problemy jaki należy rozwiązać to ponowne zamknięcie obwodu po naprawie awarii. W przypadku programów, bardziej racjonalne jest automatyczne zamknięcie niż czekanie na manualną interwencję. Sposobów jest wiele, ale najprostszym może być okresowe zamykanie obwodu. Jeśli po automatycznym zamknięciu, znów okaże się, że zewnętrzne zasoby są niedostępne, wtedy możemy go natychmiast otworzyć i czekać na kolejny moment, kiedy możemy spróbować i przetestować połączenie poprzez zamykanie obwodu.
Wzorzec zatem, to nic innego jak proxy, który ma następujące stany:
1. Obwód zamknięty – normalny tryb pracy. Wszystkie zapytania są przesyłane do danych usług\dostawców. W tym stanie, proxy powinien również śledzić liczbę nieudanych połączeń. Jeśli przekroczy ona z definiowany próg w danym okresie (np. 5 nieudanych połączeń w ciągu 10 sekund), wtedy obwód jest otwierany. Warto podkreślić, że chcemy resetować licznik niepowodzeń okresowo. Mam na myśli, że nie powinniśmy po prostu śledzić całkowitej liczby błędów. Chcemy uniknąć sytuacji, gdzie tymczasowe awarie po paru dniach pracy mogą doprowadzić do otwarcia obwodu. Dzięki prostej zasadzie, że awarie muszą nastąpić z określoną częstotliwością, unikniemy otwarcia obwodu w przypadku krótkotrwałych awarii albo po prostu chwilowego przeciążenia sieci.
2. Obwód otwarty – jeśli proxy jest w tym stanie, to znaczy, że dopuszczalna liczba nieudanych połączeń została przekroczona i ze względu bezpieczeństwa, obwód został otwarty. Wszystkie wywołania kończą się natychmiastowym wyjątkiem, bez pochłaniania zasobów (jeszcze raz podkreślam, w celu uniknięcia kaskadowego wyczerpania zasobów).
3. Powyższe dwa stany są obowiązkowe w każdej implementacji. Zwykle jednak dodaje się kolejny, który jest pomiędzy nimi (half-open, półotwarty). Wspomniałem wyżej, że nie chcemy manualnie zamykać obwodu po awarii. Prostym rozwiązaniem jest dopuszczanie ograniczonej liczby zapytań, które będą stanowić tak naprawdę test usług. Jeśli zakończą się powodzeniem, wtedy możemy przypuszczać, że awaria została naprawiona i możemy dopuścić już wszystkie zapytania poprzez całkowite zamknięcie obwodu.
W przyszłym wpisie pokażę prostą implementację wzorca w C#. Wszystko zależy od potrzeb i w zależności od tego, implementacja wzorca może być bardziej lub mniej skomplikowana. Zdecydowanie należy wziąć pod uwagę wielowątkowość i wynikające z tego zagrożenia typu stampede czy lock convoy.
Kolejnym ważnym aspektem jest wykonywanie logów. W końcu jest to doskonały punkt na wszelkiego typu monitoring, audyt, logging itp. Zdecydowanie stanowi to dobry punkt do czerpania informacji o sposobie wykorzystania naszego systemu.
Innym wyzwaniem jest łapanie konkretnych wyjątków. Nie chcemy wszystkich traktować jednakowo bo choćby po statusie HTTP, możemy domyślić się czy awaria potrwa kilka minut czy godzin. Bardzo możliwe, że same wysłanie zapytania zakończyło się błędem i w tym przypadku, jakakolwiek próba wysłania kolejnego zapytania nie ma sensu.
Istnieje wiele strategii zamykania obwodu po awarii. Może być to np. okresowe zamykanie, ping’owanie usługi w celu sprawdzenia jej stanu czy wspomniany mechanizm half-open.
Troszkę odrywając się od głównego tematu posta, zastanawiam się w jaki sposób zarządzasz transakcjami w mikroserwisach. Załóżmy scenariusz że mamy serwis, który w metodzie X wykonuje po kolei 3 mikro serwisy, gdzie każdy używa innej bazy danych znajdującej się na innym serwerze bazodanowym, w jaki sposób najlepiej to rozwiązać?
Bardzo ciekawe. Piotrze, skąd zaopatrujesz się w wiedzę w tej dziedzinie?
@Pawel:
Internet szeroko pojety – zwykle cos w pracy uslysze, ktos zasugeruje, wtedy zglebiam temat. Czesto czytajac o rzeczy A, dowiaduje sie przy okazji o innych podejsciach itp.
@Jan:
To sa rozproszone transkacje, ale nie chce nic doradzac bo po prostu zawsze wszystkie mikroserwisy byly u mnie calkowicie niezalezne. Trzeba tez uwazac, aby na sile ich nie tworzyc. Moze ma sens wykonac calosc operacji w tej samej usludze?
Tak przestrzegam bo widze, ze czasami ludzie ida za mikroserwisami a zdecydowanie tego nie potrzebuja. Mikroserwisy to ogromny overhead i nic na sile. Jesli appka dziala OK jako “monolit” nie ma w tym nic zlego, chyba ze to monolit skladajacy sie z setek projektow – wtedy zdecydowanie warto pomyslec o mikroserwisach. Generalnie mam wrazenie, ze ludzie zbyt bardzo rozlumuja monolit i powstaje potem cholernie trudna w utrzymaniu aplikacja.