Na początek trochę teorii. Osoby znające zarówno twierdzenie CAP jak i wzorzec Post/Redirect/Get mogą spokojnie przejść od razu do akapitów poniżej.
Twierdzenie CAP
Twierdzenie to sformułował Eric Brewer i dotyczy ono rozproszonych systemów składowania danych. Zgodnie z nim w takich systemach niemożliwe jest zapewnienie jednocześnie:
- spójności (ang. Consistency) – odczyt, niezależnie z którego węzła następuje, zwróci zawsze najświeższe dane
- dostępności (ang. Availability) – każde żądanie doczeka się odpowiedzi
- odporności na partycjonowane (ang. Partition tolerance) – system potrafi działać pomimo utraty części komunikatów lub uszkodzenia niektórych węzłów
Post/Redirect/Get
Jest to wzorzec projektowy stosowany w aplikacjach web’owych. Jego zadaniem jest umożliwienie odświeżenia strony zaprezentowanej po zatwierdzeniu formularza, bez ryzyka ponownego wysłania/zapisania danych.
Przebieg obsługi żądania jest następujący:
- Wyświetlenie formularza (HTTP GET)
- Wypełnienie danych i zatwierdzenie (HTTP POST)
- Obsłużenie żądania i zapis danych
- Przekierowanie (HTTP Status 3xx)
- Wyświetlenie strony (HTTP GET)
Po wykonaniu takiego przebiegu odświeżenie strony w przeglądarce owocuje odświeżeniem strony zaprezentowanej w wyniku żądania GET (punkt 5). Nie następuje ponownie wysłanie formularza (POST).
Houston mamy problem
Jak to często bywa, życie weryfikuje nasze rewelacyjne pomysły. Jednym z rewelacyjnych pomysłów w tym przypadku, była zmiana architektury systemowej. Na początek diagram stanu przed.
Komponenty systemu:
- Load balancer z użyciem Apache + mod_jk
- Dwa węzły aplikacji Frontend. Aplikacja zaimplementowana w Spring MVC. Podczas zapisu formularza zastosowany został wzorzec POST/REDIREC/GET.
- Dwa węzły aplikacji Backend realizującej zapis i odczyt z relacyjnej bazy danych PostgreSQL
- Na backend’ach zapięty cache (EHcache 2) w topologii z replikacją przez RMI
- Cache na frontend’ach również jest, ale pomijam go, gdyż nie ma on wpływu na opisywany problem
Usprawnienie architektury polegało na wprowadzaniu dodatkowego load balancer’a pomiędzy frontend’ami a backend’ami.
Nowe komponenty:
- HAProxy – load balancer przykrywający backend’y i wystawiający je frontendom pod jednym adresem
- Zastosowany algorytm: Round Robin
Zmiana miała na celu poprawienie dostępności aplikacji tak, aby awaria jednego z węzłów backendu nie powodowała błędów w obsłudze wszystkich requestów podpiętego do niego węzła frontend’u.
Niedługo po wprowadzeniu modyfikacji dostaliśmy zgłoszenie mówiące o tym, że zmiany po zapisaniu nie są widoczne w systemie.
Szybka analiza pokazała, że zmiany faktycznie nie są widoczne, jednak problem ustępuje po ponownym wyświetleniu strony.
Podejrzenie padło oczywiście na cache, a dokładnie na problem z jego replikacją. Spodziewaliśmy się, że po prostu ktoś nieumyślnie zepsuł konfigurację replikacji. Niestety nie okazało się to takie proste. Konfiguracja był w porządku.
Przyczyna
Część z Was już napewno się domyśla, o co chodzi. Zastosowanie algorytmu Round Robin na HAProxy spowodowało, że przebieg w przypadku POST/REDIRECT/GET był następujący:
- POST – zapis poprzez Backend węzeł 1
- Redirect
- GET – dane pobrane z Backend węzeł 2
Niestety topologia opierająca się na replikacji przez RMI nie dała odpowiedniego poziomu spójności. Analiza wykonana przy użyciu Dynatrace pokazała, że żądania replikacji wykonywały się za późno. Dane z węzła 1 backend’u nie znajdowały się w cache węzła 2 w momencie obsługiwania żądania GET.
Rozwiązanie
Modyfikacja do wdrożenia krótkoterminowo polegać miała na zmianie algorytmu load balancer’a na HAProxy na coś bardziej „sticky”. Chodzi o to, aby requesty wykonywane w ramach tej samej sesji na froncie przeskakiwały na drugi węzeł backend’u tylko w momencie niedostępności pierwszego.
Docelowo planujemy zmianę topologii cache połączoną z wymianą silnika cache. Taka modernizacja i tak jest wskazana ze względu na przestarzałego już Ehcache’a w wersji drugiej. Wybranym rozwiązaniem, na moment pisania tego tekstu, jest Hazelcast z centralnym serwerem cache. Upatrujemy się w tym rozwiązaniu poprawy spójności danych. Obawę mamy co do potencjalnego spadku czasów odpowiedzi takiego rozwiązania.