AWS Bedrock LLM GenAI

AWS Bedrock ServiceUnavailableException (503): obsługa błędów przy dużym obciążeniu bez mylenia ich z limitem kwoty

Napraw błędy InvokeModel w Bedrock z ServiceUnavailableException (HTTP 503) wynikające z przejściowego braku pojemności przy dużym obciążeniu: dostrojony backoff, circuit breaker, inferencja międzyregionalna i provisioned throughput.

Jerzy Kopaczewski ·
Wywołania Bedrock InvokeModel kończą się błędem ServiceUnavailableException (HTTP 503) i komunikatem "service temporarily unavailable"? W odróżnieniu od 429 to nie jest limit Twojego konta. To przejściowy brak pojemności modelu on-demand w szczycie obciążenia. Rozwiązanie: ponawiaj rozsądnie, poszerz pulę pojemności inferencją międzyregionalną i zarezerwuj dedykowaną pojemność dla ruchu, który tego nie toleruje.

Ten runbook dotyczy ścieżki 503 (pojemność). Jeśli widzisz ThrottlingException z HTTP 429, to limit konta, a nie ściana pojemności. Zobacz powiązany runbook Bedrock InvokeModel ThrottlingException 429. W sprawie projektu odpornej warstwy modeli w produkcji umów konsultację.

Objawy

W logach aplikacji pojawia się 503, a nie 429:

botocore.errorfactory.ServiceUnavailableException: An error occurred (ServiceUnavailableException) when calling the InvokeModel operation: Service temporarily unavailable, please try again.

Albo na API strumieniowym:

botocore.exceptions.ClientError: An error occurred (ServiceUnavailableException) when calling the InvokeModelWithResponseStream operation: Service temporarily unavailable, please try again.

Obserwowalny wpływ:

  • Metryka CloudWatch AWS/Bedrock > InvocationServerErrors (5xx) rośnie, a InvocationThrottles (429) pozostaje płaska
  • Błędy grupują się w serie zbieżne ze szczytami ruchu, a nie ze stałym pułapem na minutę
  • Prymitywna pętla natychmiastowego retry przepycha część zapytań, ale pod obciążeniem utrzymuje widoczny poziom błędów
  • To samo zapytanie przechodzi kilka sekund później bez żadnej zmiany kwoty

Przyczyna

503 ServiceUnavailableException oznacza, że Bedrock przejściowo nie jest w stanie obsłużyć zapytania. Sygnał dotyczy pojemności modelu w danej chwili, a nie limitów konta:

  • Szczyty pojemności on-demand - modele on-demand współdzielą regionalną pulę pojemności. Przy szerokim wzroście zapotrzebowania usługa odrzuca nadmiar ruchu kodem 503, zamiast kolejkować go bez końca.
  • Koncentracja w jednym regionie - przypięcie całego ruchu do jednego regionu i jednego ID modelu sprawia, że wszystkie zapytania konkurują o jedną pulę, bez miejsca na przelanie nadmiaru.
  • Skorelowane serie po stronie klienta - zadania typu fan-out, zaplanowane przebiegi wsadowe czy burza retry ściskają zapytania w wąskie okna, które pokrywają się z napięciem po stronie usługi.
  • Traktowanie 503 jak 429 - odczekiwanie 60-sekundowego cyklu kwoty to zła kuracja. 503 zwykle ustępuje szybciej, więc czekanie w stylu kwotowym marnuje budżet opóźnienia.

Praktyczne rozróżnienie:

Aspekt 503 ServiceUnavailableException 429 ThrottlingException
Główna przyczyna Przejściowy brak pojemności usługi Przekroczony limit RPM/TPM konta
Związek z kwotą Nie Tak
Odzyskiwanie Szybsze, często sekundy Zależne od 60-sekundowego cyklu kwoty
Właściwa reakcja Retry z backoff, przełączenie między regionami Backoff plus wniosek o zwiększenie kwoty

Rozwiązanie

Krok 1: Potwierdź, że to 503, a nie 429

Zanim cokolwiek zmienisz, rozdziel w CloudWatch oba sygnały po stronie serwera:

# 5xx (zawiera 503 ServiceUnavailableException)
aws cloudwatch get-metric-statistics \
  --namespace AWS/Bedrock \
  --metric-name InvocationServerErrors \
  --dimensions Name=ModelId,Value=eu.anthropic.claude-haiku-4-5-20251001-v1:0 \
  --start-time $(date -u -v-1H +%Y-%m-%dT%H:%M:%S) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 60 --statistics Sum --region eu-central-1

# 429 throttles, dla porównania
aws cloudwatch get-metric-statistics \
  --namespace AWS/Bedrock \
  --metric-name InvocationThrottles \
  --dimensions Name=ModelId,Value=eu.anthropic.claude-haiku-4-5-20251001-v1:0 \
  --start-time $(date -u -v-1H +%Y-%m-%dT%H:%M:%S) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 60 --statistics Sum --region eu-central-1

Jeśli InvocationServerErrors jest podwyższona, a InvocationThrottles bliska zeru, jesteś na ścieżce 503 (pojemność). Nie składaj wniosku o zwiększenie kwoty - nie pomoże.

Krok 2: Retry z backoff dostrojonym do wolniejszego odzyskiwania

Zachowaj kształt exponential backoff z jitterem, ale pracę zostaw adaptacyjnemu trybowi retry w boto3 i większej puli połączeń, zamiast ręcznie pisanej pętli:

import boto3
from botocore.config import Config

config = Config(
    region_name="eu-central-1",
    retries={"mode": "adaptive", "max_attempts": 5},
    max_pool_connections=50,  # domyślne 10 jest za małe przy wielowątkowości
)

bedrock = boto3.client("bedrock-runtime", config=config)

Tryb adaptive dokłada po stronie klienta ograniczanie tempa i backoff zarówno dla 429, jak i 503. Domyślna pula 10 połączeń głodzi klientów o dużej współbieżności i sama fabrykuje błędy.

Krok 3: Dodaj circuit breaker dla ścieżek krytycznych

Przy utrzymujących się 503 mocniejsze ponawianie tylko pogarsza sytuację. Circuit breaker szybko odcina ruch w oknie napięcia i automatycznie testuje odzyskanie. Użyj utrzymywanej biblioteki, a nie własnej implementacji:

import pybreaker
from botocore.exceptions import ClientError

# Otwórz po 5 błędach, sprawdź ponownie po 30 s
breaker = pybreaker.CircuitBreaker(fail_max=5, reset_timeout=30)

@breaker
def invoke(model_id, body):
    return bedrock.invoke_model(modelId=model_id, body=body)

try:
    resp = invoke(model_id, body)
except pybreaker.CircuitBreakerError:
    # obwód otwarty: podaj odpowiedź z cache / fallback zamiast dokładać do kolejki
    resp = fallback_response()
except ClientError:
    raise

pybreaker i tenacity są sprawdzone w boju. Własny breaker ma sens tylko przy nietypowej logice wykrywania awarii.

Krok 4: Poszerz pulę pojemności inferencją międzyregionalną

Inferencja międzyregionalna (CRIS) kieruje ruch przez profil inferencji obejmujący wiele regionów, więc zapytania lądują tam, gdzie jest pojemność, zamiast walczyć o jedną pulę:

# Geograficzny profil inferencji (EU) zamiast ID modelu związanego z jednym regionem
resp = bedrock.invoke_model(
    modelId="eu.anthropic.claude-haiku-4-5-20251001-v1:0",
    body=body,
)
  • Profile geograficzne (US, EU, APAC) trzymają dane w jednej geografii, czerpiąc z pojemności wielu regionów. Zanim włączysz, potwierdź, że reguły rezydencji danych (RODO, HIPAA, FedRAMP) dopuszczają każdy region w tej geografii - skierowanie ruchu do niedozwolonego regionu, choćby na chwilę, może być naruszeniem zgodności.
  • Profile globalne rozkładają ruch na wszystkie regiony, w których działa model, wybierając cel według pojemności w czasie rzeczywistym.

CRIS to mechanizm rozkładania pojemności, a nie system disaster recovery ani automatycznego przełączania awaryjnego. Traktuj go jako wygładzanie przepustowości i łącz z wzorcami retry oraz circuit breaker powyżej.

Krok 5: Zarezerwuj dedykowaną pojemność dla ruchu, który nie zniesie 503

Dla przewidywalnych, dużych i wrażliwych na opóźnienie ścieżek zdejmij je całkowicie ze współdzielonej puli on-demand:

  • Provisioned Throughput - zarezerwuj dedykowane jednostki modelu, aby krytyczny ruch był odizolowany od szczytów zapotrzebowania innych odbiorców.
  • Poziomy usługi (service tiers) - kieruj najbardziej krytyczny ruch przez poziom oferujący bardziej przewidywalną pojemność.
  • Oddzielne konta AWS - niezależna pojemność i kwoty per aplikacja, co przy okazji eliminuje konkurencję między zespołami i porządkuje przypisanie kosztów.

Walidacja

Po zmianie potwierdź, że poziom błędów 5xx spadł:

aws cloudwatch get-metric-statistics \
  --namespace AWS/Bedrock \
  --metric-name InvocationServerErrors \
  --dimensions Name=ModelId,Value=eu.anthropic.claude-haiku-4-5-20251001-v1:0 \
  --start-time $(date -u -v-30M +%Y-%m-%dT%H:%M:%S) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 60 --statistics Sum --region eu-central-1

Oczekiwane: Sum zmierza do 0.0, a ewentualne resztkowe 503 wyłapuje warstwa retry, zamiast wychodzić do użytkownika.

Śledź wskaźnik sukcesu względem SLO, a nie surową liczbę błędów, bo 503 są seryjne. Alarmuj, gdy wskaźnik sukcesu spadnie poniżej celu - na przykład poniżej 95% w oknie 10 minut:

aws cloudwatch put-metric-alarm \
  --alarm-name bedrock-503-success-rate \
  --alarm-description "Wskaznik sukcesu Bedrock ponizej SLO (brak pojemnosci 503)" \
  --metrics '[
    {"Id":"errors","MetricStat":{"Metric":{"Namespace":"AWS/Bedrock","MetricName":"InvocationServerErrors"},"Period":600,"Stat":"Sum"},"ReturnData":false},
    {"Id":"calls","MetricStat":{"Metric":{"Namespace":"AWS/Bedrock","MetricName":"Invocations"},"Period":600,"Stat":"Sum"},"ReturnData":false},
    {"Id":"success_rate","Expression":"(calls-errors)/calls*100","Label":"SuccessRate"}
  ]' \
  --threshold 95 --comparison-operator LessThanThreshold \
  --evaluation-periods 1 \
  --alarm-actions arn:aws:sns:eu-central-1:ACCOUNT:ops-alerts \
  --region eu-central-1

Rozdziel 429 i 503 na osobne widgety i alarmy. Sklejenie ich zaciera, czy skok to ściana kwoty (złóż wniosek o zwiększenie), czy brak pojemności (przełącz region i zarezerwuj pojemność).

Powiązane