Implementowanie wzorca wyłącznika

Napiwek

Ta zawartość jest fragmentem książki eBook, architektury mikrousług platformy .NET dla konteneryzowanych aplikacji platformy .NET dostępnych na platformie .NET Docs lub jako bezpłatnego pliku PDF, który można odczytać w trybie offline.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

Jak wspomniano wcześniej, należy obsługiwać błędy, które mogą zająć zmienną ilość czasu do odzyskania, tak jak może się zdarzyć, gdy próbujesz nawiązać połączenie z zdalną usługą lub zasobem. Obsługa tego typu błędów może poprawić stabilność i odporność aplikacji.

W środowisku rozproszonym wywołania zasobów zdalnych i usług mogą zakończyć się niepowodzeniem z powodu przejściowych błędów, takich jak wolne połączenia sieciowe i przekroczenia limitu czasu, lub jeśli zasoby odpowiadają wolno lub są tymczasowo niedostępne. Te błędy zwykle naprawiają się po krótkim czasie, a niezawodna aplikacja w chmurze powinna być przygotowana do ich obsługi przy użyciu strategii takiej jak "Wzorzec ponawiania prób".

Mogą jednak wystąpić sytuacje, w których błędy są spowodowane nieprzewidzianymi zdarzeniami, które mogą trwać znacznie dłużej. Takie błędy mogą mieć różny stopień ważności — od częściowej utraty łączności do całkowitej awarii usługi. W takich sytuacjach może to być bezcelowe, aby aplikacja stale ponawiała próbę wykonania operacji, która prawdopodobnie nie powiedzie się.

Zamiast tego aplikacja powinna zostać zakodowana w celu zaakceptowania, że operacja nie powiodła się i odpowiednio obsłużyła błąd.

Użycie ponownych prób http nieostrożnie może spowodować utworzenie ataku typu "odmowa usługi" (DoS) w ramach własnego oprogramowania. Ponieważ mikrousługa kończy się niepowodzeniem lub działa wolno, wielu klientów może wielokrotnie ponawiać próby żądań, które zakończyły się niepowodzeniem. Stwarza to niebezpieczne ryzyko wykładniczo rosnącego ruchu ukierunkowanego na awarię usługi.

W związku z tym potrzebujesz jakiejś bariery obronnej, aby nadmierne żądania zatrzymały się, gdy nie warto próbować. Ta bariera obrony jest właśnie wyłącznik.

Wzorzec wyłącznika ma inny cel niż "Wzorzec ponawiania". Wzorzec ponawiania umożliwia aplikacji ponawianie próby wykonania operacji w oczekiwaniu, że operacja zakończy się pomyślnie. Wzorzec wyłącznika uniemożliwia aplikacji wykonywanie operacji, która prawdopodobnie zakończy się niepowodzeniem. Aplikacja może połączyć te dwa wzorce. Jednak logika ponawiania powinna być wrażliwa na każdy wyjątek zwrócony przez wyłącznik i powinna porzucić próby ponawiania próby, jeśli wyłącznik wskazuje, że błąd nie jest przejściowy.

Implementowanie wzorca wyłącznika za pomocą elementów IHttpClientFactory i Polly

Podobnie jak podczas implementowania ponownych prób, zalecane podejście dla wyłączników polega na wykorzystaniu sprawdzonych bibliotek platformy .NET, takich jak Polly i natywnej integracji z usługą IHttpClientFactory.

Dodanie zasad wyłącznika do IHttpClientFactory wychodzącego potoku oprogramowania pośredniczącego jest tak proste, jak dodanie pojedynczego fragmentu kodu przyrostowego do tego, co już masz w przypadku korzystania z programu IHttpClientFactory.

Jedynym dodatkiem do kodu używanego do ponawiania wywołań HTTP jest kod, w którym do listy zasad wyłącznika są używane zasady, jak pokazano w poniższym kodzie przyrostowym.

// Program.cs
var retryPolicy = GetRetryPolicy();
var circuitBreakerPolicy = GetCircuitBreakerPolicy();

builder.Services.AddHttpClient<IBasketService, BasketService>()
        .SetHandlerLifetime(TimeSpan.FromMinutes(5))  // Sample: default lifetime is 2 minutes
        .AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>()
        .AddPolicyHandler(retryPolicy)
        .AddPolicyHandler(circuitBreakerPolicy);

Metoda AddPolicyHandler() dodaje zasady do HttpClient obiektów, których będziesz używać. W tym przypadku dodaje się zasady Polly dla wyłącznika.

Aby zapewnić bardziej modułowe podejście, zasady wyłącznika są definiowane w oddzielnej metodzie o nazwie GetCircuitBreakerPolicy(), jak pokazano w poniższym kodzie:

// also in Program.cs
static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
}

W powyższym przykładzie kodu zasady wyłącznika są skonfigurowane tak, aby przerywały lub otwierały obwód, gdy wystąpiły pięć kolejnych błędów podczas ponawiania żądań HTTP. W takim przypadku obwód zostanie przerwany przez 30 sekund: w tym okresie wywołania zostaną natychmiast przerwane przez wyłącznik, a nie faktycznie umieszczone. Zasady automatycznie interpretują odpowiednie wyjątki i kody stanu HTTP jako błędy.

Wyłączniki powinny być również używane do przekierowywania żądań do infrastruktury rezerwowej, jeśli wystąpiły problemy w konkretnym zasobie wdrożonym w innym środowisku niż aplikacja kliencka lub usługa wykonująca wywołanie HTTP. W ten sposób, jeśli w centrum danych wystąpi awaria, która ma wpływ tylko na mikrousługi zaplecza, ale nie aplikacje klienckie, aplikacje klienckie mogą przekierowywać do usług rezerwowych. Polly planuje nowe zasady automatyzowania tego scenariusza zasad trybu failover.

Wszystkie te funkcje dotyczą przypadków, w których zarządzasz trybem failover z poziomu kodu platformy .NET, w przeciwieństwie do automatycznego zarządzania przez platformę Azure, z przezroczystością lokalizacji.

Z punktu widzenia użycia w przypadku korzystania z klienta HttpClient nie ma potrzeby dodawania żadnych nowych elementów, ponieważ kod jest taki sam, jak w przypadku używania z HttpClient elementem IHttpClientFactory, jak pokazano w poprzednich sekcjach.

Testowanie ponownych prób http i wyłączników w eShopOnContainers

Za każdym razem, gdy uruchamiasz rozwiązanie eShopOnContainers na hoście platformy Docker, należy uruchomić wiele kontenerów. Niektóre kontenery są wolniejsze do uruchamiania i inicjowania, na przykład kontenera programu SQL Server. Jest to szczególnie istotne przy pierwszym wdrożeniu aplikacji eShopOnContainers na platformie Docker, ponieważ wymaga skonfigurowania obrazów i bazy danych. Fakt, że niektóre kontenery zaczynają działać wolniej niż inne, mogą spowodować, że pozostałe usługi początkowo zgłaszają wyjątki HTTP, nawet jeśli ustawisz zależności między kontenerami na poziomie docker-compose, jak wyjaśniono w poprzednich sekcjach. Te zależności docker-compose między kontenerami są po prostu na poziomie procesu. Proces punktu wejścia kontenera może zostać uruchomiony, ale program SQL Server może nie być gotowy do obsługi zapytań. Wynikiem może być kaskada błędów, a aplikacja może uzyskać wyjątek podczas próby użycia tego konkretnego kontenera.

Ten typ błędu może być również wyświetlany podczas uruchamiania, gdy aplikacja jest wdrażana w chmurze. W takim przypadku koordynatorzy mogą przenosić kontenery z jednego węzła lub maszyny wirtualnej do innego (czyli uruchamianie nowych wystąpień) podczas równoważenia liczby kontenerów w węzłach klastra.

Sposób "eShopOnContainers" rozwiązuje te problemy podczas uruchamiania wszystkich kontenerów, jest użycie wzorca ponawiania zilustrowanego wcześniej.

Testowanie wyłącznika w eShopOnContainers

Istnieje kilka sposobów przerwania/otwarcia obwodu i przetestowania go za pomocą modułów eShopOnContainers.

Jedną z opcji jest obniżenie dozwolonej liczby ponownych prób do 1 w zasadach wyłącznika i ponowne wdrożenie całego rozwiązania na platformie Docker. W przypadku pojedynczej ponawiania istnieje duża szansa, że żądanie HTTP zakończy się niepowodzeniem podczas wdrażania, wyłącznik zostanie otwarty i zostanie wyświetlony błąd.

Inną opcją jest użycie niestandardowego oprogramowania pośredniczącego zaimplementowanego w mikrousłudze Koszyk . Po włączeniu tego oprogramowania pośredniczącego przechwytuje wszystkie żądania HTTP i zwraca kod stanu 500. Oprogramowanie pośredniczące można włączyć, wysyłając żądanie GET do nieprawidłowego identyfikatora URI, w następujący sposób:

  • GET http://localhost:5103/failing
    To żądanie zwraca bieżący stan oprogramowania pośredniczącego. Jeśli oprogramowanie pośredniczące jest włączone, żądanie zwraca kod stanu 500. Jeśli oprogramowanie pośredniczące jest wyłączone, nie ma odpowiedzi.

  • GET http://localhost:5103/failing?enable
    To żądanie umożliwia oprogramowanie pośredniczące.

  • GET http://localhost:5103/failing?disable
    To żądanie wyłącza oprogramowanie pośredniczące.

Na przykład po uruchomieniu aplikacji możesz włączyć oprogramowanie pośredniczące, wysyłając żądanie przy użyciu następującego identyfikatora URI w dowolnej przeglądarce. Pamiętaj, że mikrousługa porządkowania używa portu 5103.

http://localhost:5103/failing?enable

Następnie możesz sprawdzić stan przy użyciu identyfikatora URI http://localhost:5103/failing, jak pokazano na rysunku 8-5.

Screenshot of checking the status of failing middleware simulation.

Rysunek 8–5. Sprawdzanie stanu "Niepowodzenie" ASP.NET oprogramowania pośredniczącego — w tym przypadku wyłączone.

W tym momencie mikrousługa Koszyk odpowiada kodem stanu 500 za każdym razem, gdy wywołasz ją.

Po uruchomieniu oprogramowania pośredniczącego możesz spróbować wykonać zamówienie z aplikacji internetowej MVC. Ponieważ żądania kończą się niepowodzeniem, obwód zostanie otwarty.

W poniższym przykładzie widać, że aplikacja internetowa MVC ma blok catch w logice składania zamówienia. Jeśli kod przechwytuje wyjątek otwartego obwodu, zostanie wyświetlony przyjazny komunikat informujący o tym, że użytkownik czeka.

public class CartController : Controller
{
    //…
    public async Task<IActionResult> Index()
    {
        try
        {
            var user = _appUserParser.Parse(HttpContext.User);
            //Http requests using the Typed Client (Service Agent)
            var vm = await _basketSvc.GetBasket(user);
            return View(vm);
        }
        catch (BrokenCircuitException)
        {
            // Catches error when Basket.api is in circuit-opened mode
            HandleBrokenCircuitException();
        }
        return View();
    }

    private void HandleBrokenCircuitException()
    {
        TempData["BasketInoperativeMsg"] = "Basket Service is inoperative, please try later on. (Business message due to Circuit-Breaker)";
    }
}

Oto podsumowanie tych zmiennych. Zasady ponawiania próby kilka razy próbują wykonać żądanie HTTP i pobierają błędy HTTP. Gdy liczba ponownych prób osiągnie maksymalną liczbę ustawioną dla zasad wyłącznika (w tym przypadku 5), aplikacja zgłasza wyjątek BrokenCircuitException. Wynikiem jest przyjazny komunikat, jak pokazano na rysunku 8–6.

Screenshot of the MVC web app with basket service inoperative error.

Rysunek 8–6. Wyłącznik zwraca błąd w interfejsie użytkownika

Możesz zaimplementować inną logikę dla tego, kiedy należy otworzyć/przerwać obwód. Możesz też wypróbować żądanie HTTP względem innej mikrousługi zaplecza, jeśli istnieje rezerwowe centrum danych lub nadmiarowy system zaplecza.

Na koniec kolejną możliwością jest CircuitBreakerPolicy użycie Isolate (co wymusza otwarcie i trzyma otwarte obwód) i Reset (co ponownie go zamyka). Mogą one służyć do kompilowania punktu końcowego HTTP narzędzia, który wywołuje izolowanie i resetowanie bezpośrednio w zasadach. Taki punkt końcowy HTTP może być również używany, odpowiednio zabezpieczony w środowisku produkcyjnym w celu tymczasowego izolowania systemu podrzędnego, takiego jak w przypadku uaktualnienia. Może też ręcznie przejechać obwód w celu ochrony systemu podrzędnego, który podejrzewasz, że jest uszkodzony.

Dodatkowe zasoby