Logika opakování a odolnost připojení pomocí mssql-django

Připojení k SQL Server a Azure SQL můžou přechodně selhat z důvodů, které nemají nic společného s vaším kódem:

  • Převzetí služeb při selhání skupiny dostupnosti AlwaysOn
  • Síť během instalace připojení zahodí paket.
  • Resource Governor omezuje výkon databáze.
  • Replika Azure SQL se během škálování nebo upgradu recykluje.

Většina těchto selhání se během několika sekund vymaže. Tento článek ukazuje, jak v aplikaci Django, která používá backend mssql-django, opakovat pokusy při přechodných chybách a jak nakonfigurovat Django a ovladač ODBC tak, aby se automaticky zotavily z výpadků nečinného připojení.

Přechodné chyby

Přechodné chyby jsou dočasné chyby, které se samy vyřeší. Opakování operace po krátkém zpoždění obvykle proběhne úspěšně.

Následující chyby jsou přechodné, pokud k nim dojde během vytváření připojení nebo při odesílání požadavku na server. Opakujte pokus po krátké, omezené prodlevě. Chyby, které přetrvávají i po několika opakováních, obvykle ukazují na problém s konfigurací (nesprávný server, chybějící oprávnění, vyčerpaná kvóta), který opakování pokusu nevyřeší.

Error Message Troubleshooting
64 A connection was successfully established with the server, but then an error occurred during the login process. (provider: TCP Provider, error: 0 - The specified network name is no longer available.) Spojení TCP se přeruší uprostřed navazování spojení. Nejedná se o selhání přihlašovacích údajů. Pokud přetrvává, zkontrolujte nestabilitu sítě na straně klienta nebo přechodné zařízení, které ukončí částečně zřízená připojení.
233 The client was unable to establish a connection because of an error during connection initialization process before login. Přenos před přihlášením nebo selhání protokolu TLS Server ho obvykle vrací, když nemůže přijmout připojení (vyčerpání prostředků, dosažení maximálního počtu připojení nebo nepodporovaného klienta). Nejedná se o selhání přihlašovacích údajů. Ověřte stav serveru a zkontrolujte časový limit přihlášení klienta, nastavení protokolu TLS a kompatibilitu verzí protokolu TLS klienta/serveru.
4060 Cannot open database "%.*ls" requested by the login. The login failed. Přihlášení se ověří, ale požadovanou databázi nejde otevřít. Mezi přechodné příčiny patří stav, kdy se databáze nachází ve stavu přechodu (převzetí služeb při selhání, obnovení, škálování) nebo je automaticky pozastavená. Trvalé příčiny (databáze neexistuje, chybějící přístup k přihlášení) nebudou opraveny opakovaným pokusem; zkontrolujte název databáze, mapování přihlášení a stav databáze.
4221 Login to read-secondary failed due to long wait on 'HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING'. Replika není k dispozici pro přihlášení, protože verze řádků chybí pro transakce, které byly v testovacím prostředí při recyklaci repliky. Vrácením zpět nebo potvrzením aktivních transakcí na primárním serveru problém vyřešíte. Riziko zmírníte tím, že se vyhnete dlouhým zápisovým transakcím na primárním uzlu.
10053 A transport-level error has occurred when sending the request to the server. (provider: TCP Provider, error: 0 - An established connection was aborted by the software in your host machine.) Místní strana přeruší připojení. Zkontrolujte stav sítě na straně klienta a případnou místní bránu firewall nebo klienta VPN.
10054 A transport-level error has occurred when sending the request to the server. (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) Vzdálená strana odešle resetování protokolu TCP. Běžné příčiny: partnerský proces selhal, brána firewall vynutila reset spojení nebo brána Azure SQL ukončila nečinné připojení. U resetování při nečinnosti povolte na klientovi mechanismus TCP keepalive nebo zkraťte časový limit nečinnosti fondu připojení.
10928 Resource ID: %d. The %s limit for the database is %d and has been reached. See 'http://go.microsoft.com/fwlink/?LinkId=267637' for assistance. Databáze překračuje limit zásad správného řízení prostředků Azure SQL. ID prostředku 1 označuje limit pracovních procesů; ID prostředku 2 označuje limit relací. Určete typ limitu z hlášení a pak snižte souběžnost, navyšte kapacitu databáze nebo zkraťte dlouhotrvající operace, které blokují prostředek.
10929 Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d, and the current usage for the database is %d. However, the server is currently too busy to support requests greater than %d for this database. Databáze překračuje své garantované minimum a podkladový server omezuje výkon. Opakování obvykle proběhne úspěšně při poklesu zatížení souseda. Trvalé výskyty značí, že potřebujete vyšší úroveň služby nebo méně hlučné prostředí.
40020, 40143, , 4016640540 Hlášeno v slotu Error code %d chyby 40197 během převzetí služeb při selhání. Dílčí kódy obsažené ve zprávě o převzetí služeb při selhání 40197 se v některých případech zobrazují jako chybový kód nejvyšší úrovně. Zachází s nimi stejně jako s 40197.
40197 The service has encountered an error processing your request. Please try again. Error code %d. Aktualizace softwaru, selhání hardwaru nebo jiná událost převzetí služeb při selhání ve službě Azure SQL. Po opětovném připojení budete přesměrováni na zdravou repliku. Obsažený kód chyby určuje typ přepnutí při selhání. Pokud chyba přetrvává, poznamenejte si ID trasování relace a obraťte se na podporu.
40501 The service is currently busy. Retry the request after 10 seconds. Incident ID: %ls. Code: %d. Azure SQL omezování motoru. Doporučené minimum je prodleva 10 sekund. Dlouhodobé omezování výkonu znamená, že pracovní zátěž překročila přidělené prostředky databáze; přejděte na vyšší úroveň služby nebo snižte souběžnost.
40613 Database '%.*ls' on server '%.*ls' is not currently available. Please retry the connection later. If the problem persists, contact customer support, and provide them with the session tracing ID of '%.*ls'. Databáze není dostupná, obvykle během převzetí služeb při selhání nebo krátce během škálování. Opakujte pokus s postupně prodlužovaným intervalem; pokud problém přetrvává déle než několik minut, zaznamenejte ID trasování relace a vytvořte požadavek na podporu.
42108 Can not connect to the SQL pool since it is paused. Please resume the SQL pool and try again. Vyhrazený fond SQL (Synapse) je pozastavený. Opakovaný pokus bude úspěšný až po opětovném spuštění poolu. Fond obnovte explicitně nebo naplánujte úlohu tak, aby se spustila po obnovení fondu.
42109 The SQL pool is warming up. Please try again. Vyhrazený fond SQL se obnovuje. Opakujte pokus se zvyšujícími se prodlevami, dokud nebude pool online; inicializace obvykle trvá několik minut.
49918 Cannot process request. Not enough resources to process request. The service is currently busy. Please retry the request later. Server momentálně nemůže přidělit dostatek prostředků pro splnění požadavku. Zkuste to znovu po prodlevě. Pokud chyba přetrvává, vertikálně navyšte kapacitu databáze nebo elastického fondu.
49919 Cannot process create or update request. Too many create or update operations in progress for subscription "%ld". Limit souběžnosti operací správy na úrovni předplatného Omezte souběžná volání pro vytvoření nebo aktualizaci, nebo je rozložte v čase.
49920 Cannot process request. Too many operations in progress for subscription "%ld". Limit souběžnosti probíhajících operací na úrovni předplatného. Snižte míru paralelismu nebo počkejte, až probíhající operace doběhnou.

Chyby na úrovni příkazů SQL nejsou v tomto seznamu, protože nastávají až po navázání připojení a po selhání zůstane relace použitelná. Nejběžnější chyby příkazů, které lze opakovat, jsou 1205 (oběť uváznutí) a 1222 (časový limit požadavku na zámek vypršel). Zkuste zopakovat celou transakci, nikoli jen jednotlivý příkaz, který selhal.

Text chybové zprávy je převzat z přechodných chyb připojení Azure SQL. Jednotlivé ovladače udržují vlastní integrované seznamy opakování; tento katalog popisuje, u kterých chyb lze operaci opakovat v prostředích SQL Server, Azure SQL Database, Azure SQL Managed Instance, databáze SQL v Microsoft Fabric a vyhrazené fondy SQL v Azure Synapse Analytics.

Odolnost neaktivních připojení ovladače ODBC

Ovladač ODBC pro SQL Server od Microsoftu poskytuje integrovanou odolnost nečinných připojení prostřednictvím klíčových slov připojovacího řetězce ConnectRetryCount a ConnectRetryInterval. Tato nastavení řeší přerušená nečinná připojení na úrovni ovladače ještě předtím, než do toho vstoupí kód vaší aplikace.

Povolení odolnosti nečinných připojení v extra_params:

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "HOST": "<your-server>.database.windows.net",
        "PORT": "1433",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
            "extra_params": "ConnectRetryCount=3;ConnectRetryInterval=10",
        },
    },
}
Keyword Default Description
ConnectRetryCount 1 Počet pokusů o automatické opětovné připojení pro nečinná připojení
ConnectRetryInterval 10 Sekundy mezi opakovanými pokusy o připojení.

Note

Odolnost nečinných připojení znovu připojí připojení, která byla během nečinnosti ukončena. Neprovádí opakování neúspěšných dotazů ani zotavení z chyb, ke kterým dochází během aktivních transakcí. V těchto scénářích použijte logiku opakování na úrovni aplikace.

Databázový middleware Django pro opakované pokusy

Vytvořte middleware Django, který zachytává přechodné chyby a opakuje operaci databáze. Tento přístup funguje pro zpracování žádostí na úrovni zobrazení:

# myproject/middleware.py
import random
import re
import time
import logging
from django.db import OperationalError, connection

logger = logging.getLogger(__name__)

TRANSIENT_ERROR_CODES = {
    "64", "233", "4221",
    "10053", "10054", "10928", "10929",
    "40197", "40501", "40613",
    "49918", "49919", "49920",
    # Include "4060" only if targeting Azure SQL with geo-replication failover.
    # It is usually a permanent error (wrong database name or missing permissions).
}

# Microsoft ODBC driver formats native error codes as "(<number>)" in the
# message. Extracting parenthesized codes avoids false positives that a plain
# substring match would produce for short codes like "64".
_CODE_RE = re.compile(r"\((\d+)\)")


def is_transient(error):
    codes_in_message = set(_CODE_RE.findall(str(error)))
    return bool(codes_in_message & TRANSIENT_ERROR_CODES)


class DatabaseRetryMiddleware:
    """Retry database operations on transient errors."""

    def __init__(self, get_response):
        self.get_response = get_response
        self.max_retries = 3
        self.base_delay = 1   # seconds; doubled each attempt
        self.max_delay = 30   # cap on a single sleep, regardless of attempt

    def __call__(self, request):
        for attempt in range(self.max_retries + 1):
            try:
                return self.get_response(request)
            except OperationalError as e:
                if attempt < self.max_retries and is_transient(e):
                    # Exponential backoff with full jitter, capped at max_delay.
                    # Jitter spreads simultaneous retries so many clients
                    # don't hammer the server in lock-step during an outage.
                    capped = min(self.max_delay, self.base_delay * (2 ** attempt))
                    delay = random.uniform(0, capped)
                    logger.warning(
                        "Transient DB error (attempt %d/%d), retrying in %.2fs: %s",
                        attempt + 1, self.max_retries, delay, e
                    )
                    connection.close()
                    time.sleep(delay)
                    continue
                raise

Zaregistrujte middleware v settings.py:

MIDDLEWARE = [
    "myproject.middleware.DatabaseRetryMiddleware",
    "django.middleware.security.SecurityMiddleware",
    # ... other middleware
]

Important

Umístěte DatabaseRetryMiddleware před jiný middleware, který přistupuje k databázi, aby mohl zachytit přechodné chyby z celého kanálu požadavku a opakovat je.

Dekorátor opakování pro konkrétní operace

Pro jemně odstupňované ovládání použijte dekorátor u jednotlivých funkcí:

import random
import re
import time
import functools
import logging
from django.db import OperationalError, connection

logger = logging.getLogger(__name__)

TRANSIENT_ERROR_CODES = {
    "64", "233", "4221",
    "10053", "10054", "10928", "10929",
    "40197", "40501", "40613",
    "49918", "49919", "49920",
    # Include "4060" only if targeting Azure SQL with geo-replication failover.
}

_CODE_RE = re.compile(r"\((\d+)\)")


def is_transient(error):
    codes_in_message = set(_CODE_RE.findall(str(error)))
    return bool(codes_in_message & TRANSIENT_ERROR_CODES)


def retry_on_transient(max_retries=3, base_delay=1, max_delay=30):
    """Retry on transient database errors with exponential backoff and full jitter."""

    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries + 1):
                try:
                    return func(*args, **kwargs)
                except OperationalError as e:
                    if attempt < max_retries and is_transient(e):
                        # Exponential cap doubled per attempt, then jittered
                        # within [0, cap] and limited by max_delay.
                        capped = min(max_delay, base_delay * (2 ** attempt))
                        delay = random.uniform(0, capped)
                        logger.warning(
                            "Transient error in %s (attempt %d/%d), retrying in %.2fs: %s",
                            func.__name__, attempt + 1, max_retries, delay, e
                        )
                        connection.close()
                        time.sleep(delay)
                        continue
                    raise
        return wrapper
    return decorator

Použití dekorátoru u funkcí náročných na databázi:

from myproject.retry import retry_on_transient

@retry_on_transient(max_retries=3, base_delay=2)
def process_order(order_id):
    """Process an order with automatic retry on transient failures."""
    order = Order.objects.select_for_update().get(id=order_id)
    order.status = "processing"
    order.save()
    return order

Opakování pokusu s transakcemi

Pokud dojde k přechodné chybě uvnitř transakce, celá transakce se vrátí zpět serverem. Zopakujte celou transakci, nejen neúspěšný příkaz:

from django.db import transaction

@retry_on_transient(max_retries=3)
def transfer_funds(from_account_id, to_account_id, amount):
    """Transfer funds between accounts with retry."""
    with transaction.atomic():
        from_account = Account.objects.select_for_update().get(id=from_account_id)
        to_account = Account.objects.select_for_update().get(id=to_account_id)

        from_account.balance -= amount
        to_account.balance += amount

        from_account.save()
        to_account.save()

Caution

Nezkoušejte to znovu uvnitř transaction.atomic(). Dekorátor opakování musí obalit celý blok atomic(), aby každý opakovaný pokus začal novou transakci.

Chyby na úrovni příkazu

Seznam chyb v předchozí části se zabývá chybami na úrovni připojení. Na úrovni příkazu se běžně opakují ještě dvě další chyby:

  • 1205: Relace byla vybrána jako oběť deadlocku. Znovu spusťte transakci.
  • 1222: Byl překročen časový limit požadavku na uzamčení. Spusťte transakci znovu nebo pro relaci zvyšte hodnotu LOCK_TIMEOUT, pokud je výchozí hodnota nastavena příliš agresivně.

ConnectRetryCount opakuje přerušená připojení, takže se nevztahuje na tyto chyby na úrovni příkazů. Zpracujte je pomocí stejného vzoru dekorátoru přidáním "1205" a "1222" ke TRANSIENT_ERROR_CODES u transakcí, které lze bezpečně spustit znovu.

CONN_MAX_AGE a zastaralá připojení

Django opakovaně používá připojení k databázi napříč požadavky, když CONN_MAX_AGE je nastavená. Dlouhodobé připojení může přestat být platné, pokud ho server uzavře (například během operace škálování Azure SQL nebo při vypršení časového limitu na bráně firewall).

Nastavte CONN_MAX_AGE, aby se vyvážilo opětovné použití a zastaralost:

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "HOST": "<your-server>.database.windows.net",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
        },
        "CONN_MAX_AGE": 600,  # Close and reopen connections after 10 minutes
    },
}
  • CONN_MAX_AGE=0 (výchozí): Ukončete připojení na konci každého požadavku. Nejbezpečnější, ale nejpomalejší.
  • CONN_MAX_AGE=600: Znovu používat připojení po dobu 10 minut. Dobrá rovnováha pro většinu webových aplikací.
  • CONN_MAX_AGE=None: Nechte připojení otevřená po neomezenou dobu. Používejte pouze s mechanismem opakování pro zastaralá připojení.

CONN_HEALTH_CHECKS (Django 4.1 a novější)

Django 4.1 zavedl CONN_HEALTH_CHECKS, který ověřuje opakované připojení před každou žádostí. Povolte tuto možnost spolu s CONN_MAX_AGE, abyste mohli automaticky rozpoznat neaktivní připojení:

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "HOST": "<your-server>.database.windows.net",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
        },
        "CONN_MAX_AGE": 600,
        "CONN_HEALTH_CHECKS": True,
    },
}

Když jsou povolené kontroly stavu, Django před opětovným použitím připojení vydá jednoduchý ověřovací dotaz. Pokud je připojení přerušeno, Django transparentně otevře novou místo vyvolání chyby.

Osvědčené postupy

  • Použijte exponenciálně rostoucí prodlevu s plným jitterem. Při každém pokusu zdvojnásobte limit a poté počkejte náhodnou dobu v rámci intervalu [0, cap]. Jitter brání tomu, aby mnoho klientů během regionálního výpadku opakovalo požadavky synchronně, což by jinak mohlo proměnit krátkodobý výpadek v dlouhodobé přetížení. Omezte dobu spánku pro každý pokus (například na 30 sekund), aby celková doba zotavení zůstala omezená.
  • Nastavte maximální počet opakování. Tři opakované pokusy s exponenciální prodlevou jsou rozumným výchozím nastavením. Více než pět opakování obvykle značí netransientní problém.
  • Před opakováním připojení zavřete. Zavolejte connection.close() , aby Django otevřel nové připojení při dalším pokusu.
  • Protokolujte všechny opakování. Úspěšná opakování bez upozornění mohou skrývat problémy s výkonem. Protokolujte na úrovni WARNING, abyste mohli sledovat frekvenci.
  • Nezopakujte netransientní chyby. Selhání ověřování, chyby oprávnění a chyby syntaxe nemají prospěch z opakování.
  • Zopakujte celou transakci. Obalte transaction.atomic() logikou opakování, ne naopak.
  • Povolit CONN_HEALTH_CHECKS (Django 4.1 a novější) pro webové aplikace, které používají CONN_MAX_AGE.