Pojawienie się architektur mikroserwisowych wymusiło zastosowanie wzoru Sagi do zarządzania rozproszonymi transakcjami w granicach usług, gdzie tradycyjne gwarancje ACID są niemożliwe. Historycznie testy polegały na monolitycznych bazach danych z natychmiastową spójnością, ale nowoczesne systemy poliglotyczne wymagają walidacji asynchronicznych przepływów pracy i logiki kompensacyjnej. Głównym problemem jest to, że konwencjonalne testy integracyjne zakładają synchronizowane odpowiedzi, co nieuchronnie prowadzi do przeoczenia warunków wyścigu, podziałów sieci i niejednoznacznych stanów, które występują, gdy niektórzy uczestnicy sagi zatwierdzają, podczas gdy inni zawodzą.
Rozwiązanie wymaga podejścia Chaos Engineering zintegrowanego z ramą testową. Zaprojektuj framework z wykorzystaniem Testcontainers, aby orchestrationować rzeczywiste instancje PostgreSQL, MongoDB i Redis w izolowanych sieciach Docker. Wprowadź Toxiproxy jako programowalny proxy TCP między usługami, aby wprowadzać opóźnienia, ograniczenia pasma i podziały sieci w precyzyjnych krokach sagi. Użyj Awaitility do asynchronicznych asercji na podstawie polling, zamiast statycznych opóźnień, i zintegrować Jaeger do rozproszonego śledzenia, aby odtworzyć dokładne ścieżki wykonania. Wdrażaj śledzenie identyfikatorów kluczy idempotencyjnych opartych na UUID, aby weryfikować semantykę dokładnie raz dla kompensacji, i buduj GlobalConsistencyValidator, który wykonuje migawki stanów we wszystkich warstwach trwałych, aby zweryfikować zachowanie invariantu.
Kontekst: Międzynarodowa platforma e-commerce przetwarzała zamówienia przez event-driven sagę obejmującą Usługę Zapasów (PostgreSQL), Usługę Płatności (MongoDB dla logów transakcji) i Usługę Wysyłki (Elasticsearch). Architektura wykorzystywała Apache Kafka do choreografii między mikroserwisami opartymi na Java.
Opis problemu: W czasie szczytowego ruchu, przerywanie w sieci spowodowało, że przetwarzanie płatności się powiodło, podczas gdy rezerwacja zapasów nie powiodła się, co wyzwoliło kompensację. Jednak logika kompensacyjna zawierała krytyczny warunek wyścigu, w którym wydano podwójne żądania zwrotu, jeśli początkowe żądanie zwrotu czasowo się zakończyło, naruszając umowy o idempotencji. Dodatkowo opóźnienia w ostatecznej spójności między warsztatami poliglotycznymi spowodowały fałszywe pozytywy w istniejących testach, które twierdziły, że zapasy zostały natychmiast przywrócone, co prowadziło do niestabilnych pipeliny CI/CD i ujawnionych defektów, w których klienci zostali obciążeni za niedostępne przedmioty.
Podejście 1: Testowanie końcowe z interfejsem użytkownika z ustalonymi opóźnieniami
Początkowo rozważaliśmy użycie Selenium WebDriver do symulacji procesów zakupu przez użytkowników, wprowadzając Thread.sleep(5000) w celu oczekiwania na asynchroniczne przetwarzanie.
Zalety: Proste do wdrożenia, obejmujące pełną ścieżkę użytkownika i nie wymagające zmian w kodzie usługi.
Wady: Ekstremalnie krucha; pięć sekund było niewystarczające podczas przeciążenia i nadmierne w okresach bezczynnych. Nie można było wprowadzać awarii sieci na precyzyjnych etapach sagi, co uniemożliwiło reprodukcję konkretnego warunku wyścigu. To podejście nie dostarczało widoczności w zakresie wzorców komunikacji międzyserwisowej HTTP ani przejściowych stanów baz danych.
Podejście 2: Testowanie jednostkowe z mockowanymi bazami danych w pamięci Drugą opcją było mockowanie wszystkich zewnętrznych wywołań serwisów przy użyciu Mockito i korzystanie z bazy danych w pamięci H2 dla jednostkowych testów każdej usługi. Zalety: Czas wykonania poniżej 10 sekund, brak zależności infrastrukturalnych i deterministyczne wyniki w izolacji. Wady: Nie wykryto rzeczywistych problemów z serializacją, zachowań timeout sieci TCP ani mechanizmów blokowania specyficznych dla baz danych występujących w PostgreSQL, ale nie w H2. Warunek wyścigu idempotencyjnego ujawnił się tylko przy rzeczywistym zachowaniu pakietów sieciowych i wyczerpaniu puli połączeń, czego mocki nie mogą zreplikować.
Podejście 3: Orkiestracja Chaosu z Rzeczywistą Infrastrukturą (Wybrane) Zrealizowaliśmy dedykowaną ramę testową dzięki JUnit 5 i Testcontainers. Każda usługa działała w izolowanych kontenerach Docker z Toxiproxy zarządzającym wszystkimi połączeniami sieciowymi między nimi. Użyliśmy RestAssured do punktów wejścia API i WireMock do symulacji zachowania idempotencyjnego zewnętrznego procesora płatności. Zalety: Umożliwiło precyzyjne wstrzykiwanie błędów na konkretnych etapach sagi (np. przerwanie połączenia po zatwierdzeniu płatności, ale przed sprawdzeniem zapasów). Awaitility pozwolił na dynamiczne oczekiwanie na ostateczną spójność bez ustalonych opóźnień. Ślady Jaeger dostarczyły analizy forensic ścieżek wykonania w celu weryfikacji ścieżek kompensacyjnych. Wady: Wyższa początkowa złożoność konfiguracji i wymagania zasobów (minimum 8 GB RAM dla lokalnego wykonania), plus dłuższy czas inicjalizacji w porównaniu do testów jednostkowych.
Rezultat: Ramka wykryła błąd idempotencyjny, w którym ponowne próby kompensacji nie miały właściwego przetwarzania HTTP 409 Conflict dla podwójnych kluczy. Po naprawieniu logiki w celu sprawdzenia kluczy idempotencyjnych Redis przed wysłaniem żądań zwrotu, produkcyjne podwójne obciążenia spadły do zera. Czas wykonania testów zmniejszył się z 8 minut (niestabilne testy UI) do 45 sekund (skierowane testy integracyjne), zwiększając pokrycie scenariuszy awarii o 300%.
Jak weryfikujesz, że transakcje kompensacyjne zachowują idempotencję, gdy awarie sieci powodują niejednoznaczne wyniki żądań?
Kandydaci zazwyczaj potwierdzają tylko końcowe salda kont, pomijając istotną weryfikację, że systemy downstream otrzymały dokładnie jedno żądanie. Właściwa implementacja polega na uchwyceniu klucza idempotencyjnego UUID przed wstrzyknięciem chaosu, a następnie użyciu metody verify(exactly(1), postRequestedFor()) WireMock, aby potwierdzić, że dokładnie jedno pasujące żądanie dotarło do bramki płatności. Dodatkowo sprawdź dzienniki stanu Saga Orchestratora, aby upewnić się, że przejścia odbywają się zgodnie z COMPENSATING -> COMPENSATED bez pośrednich stanów FAILED, które mogą wywołać niepotrzebne alerty. Wymaga to kontroli na poziomie proxy TCP, aby odrzucać połączenia po przesłaniu bajtów żądania, ale przed przybyciem bajtów odpowiedzi, tworząc dokładny niejednoznaczny warunek czasowy, który testuje obsługę idempotencyjności.
Jaka strategia zapobiega niestabilności testów podczas asercji ostatecznej spójności w różnych magazynach danych o różnych opóźnieniach replikacji?
Większość kandydatów sugeruje polling z ustalonym timeoutem. Solidne rozwiązanie wykorzystuje Awaitility z eksponentialnym opóźnieniem początkowym 100 ms, zakończonym na 99. percentylu latencji produkcyjnej (np. 3 sekundy). Kluczowe jest wdrożenie mechanizmu Global Clock lub Vector Clock w testach, aby wykonać migawki logicznych znaczników czasowych we PostgreSQL, MongoDB i Redis przed rozpoczęciem sagi. Assercje następnie weryfikują, że operacje odczytu zwracają dane z znacznikami czasowymi większymi lub równymi czasowi rozpoczęcia sagi. W scenariuszach CQRS subskrybuj zdarzenia CDC przy użyciu Debezium osadzonego w testach, zamiast przeprowadzać polling baz danych, co skraca czasy oczekiwania z sekund do milisekund i eliminuje warunki wyścigu między asercją testu a replikacją danych.
Jak wykrywasz częściowe stany wykonania, gdzie niektórzy uczestnicy sagi zatwierdzili, podczas gdy inni pozostają w stanie oczekiwania, bez dostępu do narzędzi do obserwacji produkcji?
Kandydaci często przeoczają potrzebę śledzenia Sagi w procesie lub Dzienników Audytu Sagi, które są dostępne dla ram testowych. Rozwiązanie wymaga wstrzyknięcia wzoru Sidecar w kontenerach testowych, które przechwytują wywołania gRPC lub HTTP do usług uczestników przy użyciu Envoy lub własnych proxy. Utrzymuj Matrycę Stanu Sagi w ramie testowej, która śledzi status każdego uczestnika (OCZEKUJĄCE, ZATWIERDZONE, ANULOWANE). Gdy Toxiproxy wstrzykuje podział, zapytaj tę matrycę, aby zweryfikować, że zatwierdzeni uczestnicy odpowiadają oczekiwanemu stanowi sprzed awarii, podczas gdy anulowani uczestnicy nie pokazują żadnych efektów ubocznych. Użyj asercji JSONPath na znacznikach spanów Jaeger, aby potwierdzić, że ścieżki kompensacyjne wykonują się tylko dla zatwierdzonych uczestników, zapewniając, że zasoby nie zostaną zwolnione dla transakcji, które nigdy ich rzeczywiście nie zarezerwowały.