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.
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, aInvocationThrottles(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
- Bedrock InvokeModel ThrottlingException 429 - ścieżka kwotowa, gdy zamiast 503 widzisz 429
- Inferencja LLM na AWS: Bedrock vs SageMaker vs self-hosted na EKS - porównanie architektur i kiedy dedykowana pojemność bije on-demand
- Dokumentacja inferencji międzyregionalnej AWS Bedrock