Wzorzec ponawiania

Azure

Zapewnij możliwość obsługi błędów przejściowych w aplikacji podczas próby nawiązania połączenia z usługą lub zasobem sieciowym poprzez niewidoczne ponawianie operacji, która zakończyła się niepowodzeniem. Może to poprawić stabilność aplikacji.

Kontekst i problem

Aplikacja, która komunikuje się z elementami działającymi w chmurze, musi być wrażliwa na błędy przejściowe mogące wystąpić w tym środowisku. Błędy obejmują chwilową utratę łączności sieciowej ze składnikami i usługami, tymczasową niedostępność usługi lub przekroczenia limitu czasu, gdy usługa jest zajęta.

Te błędy zwykle są automatycznie usuwane, a jeśli akcja, która wyzwoliła taki błąd, zostanie powtórzona z odpowiednim opóźnieniem, prawdopodobnie zakończy się pomyślnie. Na przykład usługa bazy danych, która przetwarza dużą liczbę współbieżnych żądań, może zaimplementować strategię ograniczania przepustowości, która tymczasowo odrzuca wszelkie dalsze żądania, dopóki obciążenie nie zostanie złagodne. Próba uzyskania dostępu do bazy danych przez aplikację może zakończyć się niepowodzeniem, ale ponowna próba po określonym opóźnieniu może się powieść.

Rozwiązanie

W chmurze błędy przejściowe nie są niczym niezwykłym, a aplikacja powinna zostać tak zaprojektowana, aby obsługiwać je w sposób sprawny i przezroczysty. Pozwoli to zmniejszyć wpływ błędów na zadania biznesowe wykonywane przez aplikację.

Jeśli aplikacja wykryje błąd podczas próby wysłania żądania do usługi zdalnej, może go obsłużyć za pomocą następujących strategii:

  • Anulowanie. Jeśli błąd wskazuje na to, że awaria nie jest przejściowa, lub jest mało prawdopodobne, że nie wystąpi on podczas ponownej próby, aplikacja powinna anulować operację i zgłosić wyjątek. Na przykład wykonanie ponownej próby w przypadku błędu uwierzytelniania spowodowanego podaniem nieprawidłowych poświadczeń prawdopodobnie nie powiedzie się, niezależnie od tego, ile razy zostanie ona podjęta.

  • Ponowienie próby. Jeśli zgłoszony błąd jest nietypowy lub występuje bardzo rzadko, mógł on zostać spowodowany wystąpieniem nietypowych okoliczności, takich jak uszkodzenie pakietu sieciowego podczas transmisji. W takim przypadku aplikacja może natychmiast ponowić próbę wykonania żądania zakończonego niepowodzeniem, ponieważ wystąpienie takiego samego błędu jest mało prawdopodobne i żądanie powinno zakończyć się pomyślnie.

  • Ponowienie próby po opóźnieniu. Jeśli błąd został spowodowany przez co najmniej jedną typową awarię związaną z łącznością lub zajętością, konieczne może być odczekanie jakiegoś czasu, aż w ramach sieci lub usługi problemy z łącznością zostaną naprawione albo lista prac zostanie wyczyszczona. Aplikacja powinna oczekiwać przez odpowiedni czas przed ponowieniem próby żądania.

W przypadku bardziej typowych błędów przejściowych okres między ponownymi próbami powinien zostać dobrany w taki sposób, aby możliwie najbardziej równomiernie rozłożyć żądania z wielu wystąpień aplikacji. Zmniejsza to ryzyko, że zajęta usługa będzie nadal przeciążona. Jeśli wiele wystąpień aplikacji stale przeciąża usługę za pomocą żądań ponownych prób, odzyskanie sprawności przez usługę może dłużej potrwać.

Jeśli żądanie nadal kończy się niepowodzeniem, aplikacja może poczekać i dopiero wtedy podjąć kolejną próbę. Jeśli to konieczne, ten proces można powtarzać z rosnącymi opóźnieniami między ponownymi próbami, dopóki nie zostanie podjęta określona maksymalna liczba żądań. Opóźnienie można zwiększyć przyrostowo lub wykładniczo, w zależności od typu błędu i prawdopodobieństwa, że zostanie on w tym czasie usunięty.

Na poniższym diagramie przedstawiono wywołanie operacji w hostowanej usłudze przy użyciu tego wzorca. Jeśli żądanie nie zakończy się pomyślnie po wykonaniu wstępnie zdefiniowanej liczby prób, aplikacja powinna potraktować błąd jako wyjątek i odpowiednio go obsłużyć.

Rysunek 1. Wywoływanie operacji w hostowanej usłudze przy użyciu wzorca ponawiania

Aplikacja powinna opakować wszystkie próby uzyskania dostępu do zdalnej usługi w kodzie, który implementuje zasady ponawiania dopasowane do jednej z wymienionych powyżej strategii. Żądania wysyłane do różnych usług mogą podlegać różnym zasadom. Niektórzy dostawcy udostępniają biblioteki implementujące zasady ponawiania, w ramach których aplikacja może określić maksymalną liczbę ponownych prób, czas między ponownymi próbami i inne parametry.

Aplikacja powinna rejestrować szczegóły błędów i operacji kończących się niepowodzeniem. Te informacje są przydatne dla operatorów. Oznacza to, że w celu uniknięcia operatorów powodzi z alertami dotyczącymi operacji, w których następnie ponowione próby zakończyły się powodzeniem, najlepiej jest rejestrować wczesne błędy jako wpisy informacyjne i tylko niepowodzenie ostatniej próby ponawiania jako rzeczywisty błąd. Oto przykład sposobu, w jaki wygląda ten model rejestrowania.

Jeśli usługa jest regularnie niedostępna lub zajęta, często spowodowane jest to wyczerpanymi zasobami. Częstotliwość takich błędów można zmniejszyć, skalując usługę w poziomie. Jeśli na przykład usługa bazy danych jest stale przeciążona, korzystne może być podzielenie bazy danych na partycje i rozłożenie obciążenia na wiele serwerów.

Aplikacja Microsoft Entity Framework udostępnia funkcje służące do ponawiania operacji bazy danych. Ponadto większość usług platformy Azure i zestawów SDK klienta zawiera mechanizm ponawiania prób. Aby uzyskać więcej informacji, zobacz Wskazówki dotyczące ponawiania prób dla określonych usług.

Problemy i kwestie do rozważenia

Podczas podejmowania decyzji o sposobie implementacji tego wzorca należy rozważyć następujące kwestie.

Zasady ponawiania powinny być dostosowane do wymagań biznesowych aplikacji i charakteru niepowodzenia. W przypadku pewnych niekrytycznych operacji korzystniejsze jest szybsze zakończenie ich działania niż kilkukrotne ponowienie próby mające wpływ na przepływność aplikacji. Na przykład w interaktywnej aplikacji internetowej, która uzyskuje dostęp do usługi zdalnej, lepiej jest zakończyć się niepowodzeniem po mniejszej liczbie ponownych prób tylko z krótkim opóźnieniem między próbami ponawiania prób i wyświetlenie odpowiedniego komunikatu dla użytkownika (na przykład "spróbuj ponownie później"). W przypadku aplikacji wsadowych bardziej odpowiednie może być zwiększenie liczby ponownych prób z wykładniczo rosnącym opóźnieniem między nimi.

Agresywne zasady ponawiania z minimalnym opóźnieniem między próbami i dużą liczbą ponownych prób mogą jeszcze bardziej obniżyć wydajność zajętej usługi, która jest bliska wykorzystania dostępnych zasobów lub je wykorzystuje. Te zasady ponawiania mogą mieć również wpływ na czas odpowiedzi aplikacji, jeśli stale podejmowana jest w niej próba wykonania operacji kończącej się niepowodzeniem.

Jeśli żądanie nadal kończy się niepowodzeniem po znacznej liczbie ponownych prób, lepszym rozwiązaniem dla aplikacji jest unikanie dalszych żądań do tego samego zasobu i po prostu natychmiastowe zgłoszenie błędu. Po wygaśnięciu danego okresu aplikacja może wstępnie zezwolić na niewielką liczbę żądań w celu sprawdzenia, czy zakończą się one pomyślnie. Aby uzyskać więcej informacji o tej strategii, zobacz Wzorzec wyłącznika.

Rozważ, czy operacja jest idempotentna. Jeśli tak, ponawianie próby jest z założenia bezpieczne. Jeśli nie, ponowne próby mogą spowodować wykonanie operacji więcej niż jeden raz i wystąpienie niezamierzonych skutków ubocznych. Na przykład usługa może odebrać żądanie, pomyślnie je przetworzyć, ale wysłanie przez nią odpowiedzi może zakończyć się niepowodzeniem. W takie sytuacji logika ponawiania może ponownie wysłać żądanie, zakładając, że pierwsze żądanie nie zostało odebrane.

Żądanie względem usługi może kończyć się niepowodzeniem z różnych powodów i powodować występowanie różnych wyjątków w zależności od charakteru błędu. Niektóre wyjątki wskazują błędy, które można szybko usunąć, a niektóre wskazują dłużej trwające błędy. W przypadku zasad ponawiania przydatne jest dostosowanie czasu między ponownymi próbami na podstawie typu wyjątku.

Należy wziąć pod uwagę, w jaki sposób operacja będąca częścią transakcji wpłynie na ogólną spójność transakcji. Dostosuj zasady ponawiania na potrzeby operacji transakcyjnych, aby zmaksymalizować prawdopodobieństwo powodzenia i ograniczyć prawdopodobieństwo konieczności wycofania wszystkich kroków transakcji.

Upewnij się, że cały kod dotyczący ponawiania został w pełni przetestowany w różnych warunkach błędu. Sprawdź, czy w znaczny sposób nie wpływa on na wydajność lub niezawodność aplikacji, nie powoduje nadmiernego obciążenia usług lub zasobów ani nie generuje sytuacji wyścigu lub wąskich gardeł.

Logikę ponawiania należy implementować tylko w tych miejscach w kodzie, w których zrozumiały jest pełny kontekst operacji kończącej się niepowodzeniem. Jeśli na przykład zadanie zawierające zasady ponawiania wywołuje inne zadanie, które również zawiera zasady ponawiania, ta dodatkowa warstwa ponownych prób może spowodować duże opóźnienia przetwarzania. Lepszym rozwiązaniem może być skonfigurowanie zadania niższego poziomu w celu szybkiego kończenia działania i raportowania przyczyny błędu z powrotem do zadania, które je wywołało. To zadanie wyższego poziomu może następnie obsłużyć błąd na podstawie własnych zasad.

Ważne jest, aby rejestrować wszystkie błędy łączności powodujące ponawianie próby, dzięki czemu możliwe będzie identyfikowanie podstawowych problemów z aplikacją, usługami lub zasobami.

Sprawdź błędy, które z największym prawdopodobieństwem mogą wystąpić w przypadku usługi lub zasobu, aby dowiedzieć się, czy mogą być one długotrwałe lub powodujące zakończenie działania. Jeśli tak, to lepiej obsłużyć takie błędy jako wyjątek. Aplikacja może zarejestrować lub zgłosić wyjątek, a następnie podjąć próbę kontynuowania działania, wywołując alternatywną usługę (jeśli taka jest dostępna) lub oferując funkcjonalność o obniżonym poziomie. Aby uzyskać więcej informacji na temat sposobu wykrywania i obsługi długotrwałych błędów, zobacz Wzorzec wyłącznika.

Kiedy używać tego wzorca

Użyj tego wzorca, gdy w aplikacji mogą występować błędy przejściowe podczas współdziałania z usługą zdalną lub uzyskiwania dostępu do zasobu zdalnego. Takie błędy są zwykle krótkotrwałe, a powtórzenie żądania, które wcześniej zakończyło się niepowodzeniem, może zakończyć się pomyślnie w kolejnej próbie.

Ten wzorzec może nie być przydatny w następujących sytuacjach:

  • Gdy błąd jest prawdopodobnie długotrwały, ponieważ może to mieć wpływ na czas odpowiedzi aplikacji. Aplikacja może marnować czas i zasoby, podejmując próbę powtórzenia żądania, które prawdopodobnie zakończy się niepowodzeniem.
  • Na potrzeby obsługi awarii, które nie zostały spowodowane błędami przejściowymi, czyli na przykład wyjątków wewnętrznych spowodowanych przez błędy logiki biznesowej aplikacji.
  • Jako alternatywa dla rozwiązywania problemów ze skalowalnością w systemie. Jeśli w aplikacji występują częste błędy związane z zajętością, zwykle świadczy to o tym, że konieczne jest skalowanie w górę usługi lub zasobu, do którego uzyskiwany jest dostęp.

Projekt obciążenia

Architekt powinien ocenić, w jaki sposób wzorzec ponawiania prób może być używany w projekcie obciążenia, aby sprostać celom i zasadom opisanym w filarach platformy Azure Well-Architected Framework. Na przykład:

Filar Jak ten wzorzec obsługuje cele filaru
Decyzje projektowe dotyczące niezawodności pomagają obciążeniu stać się odporne na awarię i zapewnić, że zostanie przywrócony do w pełni funkcjonalnego stanu po wystąpieniu awarii. Łagodzenie błędów przejściowych w systemie rozproszonym jest podstawową techniką poprawy odporności obciążenia.

- RE:07 Self-preservation
- RE:07 Błędy przejściowe

Podobnie jak w przypadku każdej decyzji projektowej, należy rozważyć wszelkie kompromisy w stosunku do celów innych filarów, które mogą zostać wprowadzone przy użyciu tego wzorca.

Przykład

Ten przykład w języku C# ilustruje implementację wzorca ponawiania. Przedstawiona poniżej metoda OperationWithBasicRetryAsync wywołuje asynchronicznie usługę zewnętrzną za pomocą metody TransientOperationAsync. Szczegóły metody TransientOperationAsync będą specyficzne dla usługi i pominięto je w przykładowym kodzie.

private int retryCount = 3;
private readonly TimeSpan delay = TimeSpan.FromSeconds(5);

public async Task OperationWithBasicRetryAsync()
{
  int currentRetry = 0;

  for (;;)
  {
    try
    {
      // Call external service.
      await TransientOperationAsync();

      // Return or break.
      break;
    }
    catch (Exception ex)
    {
      Trace.TraceError("Operation Exception");

      currentRetry++;

      // Check if the exception thrown was a transient exception
      // based on the logic in the error detection strategy.
      // Determine whether to retry the operation, as well as how
      // long to wait, based on the retry strategy.
      if (currentRetry > this.retryCount || !IsTransient(ex))
      {
        // If this isn't a transient error or we shouldn't retry,
        // rethrow the exception.
        throw;
      }
    }

    // Wait to retry the operation.
    // Consider calculating an exponential delay here and
    // using a strategy best suited for the operation and fault.
    await Task.Delay(delay);
  }
}

// Async method that wraps a call to a remote service (details not shown).
private async Task TransientOperationAsync()
{
  ...
}

Instrukcja wywołująca tę metodę znajduje się w bloku try/catch w pętli for. Pętla for zostanie opuszczona, jeśli wywołanie metody TransientOperationAsync zakończy się pomyślnie bez zgłoszenia wyjątku. Jeśli metoda TransientOperationAsync zakończy się niepowodzeniem, w bloku catch zostanie sprawdzona przyczyna błędu. Jeśli błąd zostanie uznany za przejściowy, po krótkiej chwili będzie miało miejsce ponowienie próby wykonania operacji.

W pętli for śledzona jest również liczba prób wykonania operacji, a jeśli kod nie powiedzie się trzy razy, przyjmowane jest założenie, że wyjątek jest bardziej długotrwały. Jeśli wyjątek nie jest przejściowy lub jest długotrwały, procedura obsługi catch zgłasza wyjątek. Ten wyjątek powoduje opuszczenie pętli for i powinien zostać przechwycony przez kod, który wywołuje metodę OperationWithBasicRetryAsync.

Przedstawiona poniżej metoda IsTransient sprawdza, czy istnieje określony zestaw wyjątków mających zastosowanie względem środowiska, w którym uruchamiany jest kod. Definicja wyjątku przejściowego będzie się różnić w zależności od zasobów, do których uzyskiwany jest dostęp, i środowiska, w którym wykonywana jest operacja.

private bool IsTransient(Exception ex)
{
  // Determine if the exception is transient.
  // In some cases this is as simple as checking the exception type, in other
  // cases it might be necessary to inspect other properties of the exception.
  if (ex is OperationTransientException)
    return true;

  var webException = ex as WebException;
  if (webException != null)
  {
    // If the web exception contains one of the following status values
    // it might be transient.
    return new[] {WebExceptionStatus.ConnectionClosed,
                  WebExceptionStatus.Timeout,
                  WebExceptionStatus.RequestCanceled }.
            Contains(webException.Status);
  }

  // Additional exception checking logic goes here.
  return false;
}

Następne kroki

  • Przed napisaniem niestandardowej logiki ponawiania rozważ użycie ogólnej struktury, takiej jak Polly for .NET lub Resilience4j for Java.

  • Podczas przetwarzania poleceń, które zmieniają dane biznesowe, należy pamiętać, że ponowne próby mogą spowodować dwukrotne wykonanie akcji, co może być problematyczne, jeśli ta akcja jest podobna do ładowania karty kredytowej klienta. Użycie wzorca idempotencji opisanego w tym wpisie w blogu może pomóc w radzeniu sobie z tymi sytuacjami.

  • Wzorzec niezawodnej aplikacji internetowej pokazuje, jak zastosować wzorzec ponawiania prób do aplikacji internetowych zbieżnych w chmurze.

  • W przypadku większości usług platformy Azure zestawy SDK klienta obejmują wbudowaną logikę ponawiania prób. Aby uzyskać więcej informacji, zobacz Wskazówki dotyczące ponawiania prób dla usług platformy Azure.

  • Wzorzec wyłącznika. Jeśli prawdopodobne jest, że błąd będzie trwał dłużej, bardziej odpowiednie może być zaimplementowanie wzorca wyłącznika. Połączenie wzorców ponawiania prób i wyłącznika zapewnia kompleksowe podejście do obsługi błędów.