Logica en verbindingstolerantie opnieuw proberen met mssql-django

Verbindingen met SQL Server en Azure SQL kunnen tijdelijk mislukken om redenen die niets te maken hebben met uw code:

  • Een AlwaysOn-beschikbaarheidsgroep voert een failover uit.
  • Het netwerk laat een pakket vallen tijdens het instellen van de verbinding.
  • Resource Governor beperkt de databaseprestaties.
  • Een Azure SQL-replica wordt opnieuw geïnitialiseerd tijdens het schalen of bij een upgrade.

De meeste van deze fouten zijn binnen enkele seconden duidelijk. In dit artikel wordt beschreven hoe u in een Django-toepassing die gebruikmaakt van de mssql-django-backend, opnieuw kunt proberen na tijdelijke fouten, en hoe u Django en het ODBC-stuurprogramma configureert om automatisch te herstellen na verbindingsverbrekingen door inactiviteit.

Tijdelijke fouten

Tijdelijke fouten zijn tijdelijke fouten die zelfstandig worden opgelost. Het opnieuw proberen van de bewerking na een korte vertraging slaagt meestal.

De volgende fouten zijn tijdelijk wanneer ze optreden tijdens het tot stand komen van de verbinding of tijdens het verzenden van een aanvraag naar de server. Probeer het na een korte, begrensde wachttijd opnieuw. Fouten die zich na enkele nieuwe pogingen voordoen, geven meestal een configuratieprobleem aan (verkeerde server, ontbrekende machtigingen, uitgeput quotum) die opnieuw proberen niet worden opgelost.

Fout 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.) De TCP-verbinding valt halverwege de handshake weg. Geen referentiefout. Als dit zich blijft voordoen, controleert u op instabiliteit van het netwerk aan de clientzijde of een tussenliggend apparaat dat half tot stand gebrachte verbindingen afneemt.
233 The client was unable to establish a connection because of an error during connection initialization process before login. Transportfout vóór het aanmelden of TLS-fout. De server retourneert deze meestal wanneer deze de verbinding niet kan accepteren (resourceuitputting, max-connections bereikt of een niet-ondersteunde client). Geen referentiefout. Controleer de status van de server en controleer vervolgens de time-out voor clientaanmelding, de TLS-instellingen en de compatibiliteit van de TLS-versies van client en server.
4060 Cannot open database "%.*ls" requested by the login. The login failed. De aanmelding wordt geverifieerd, maar kan de aangevraagde database niet openen. Tijdelijke oorzaken zijn onder andere dat de database bezig is met een overgang (failover, herstel, schalen) of automatisch is gepauzeerd. Aanhoudende oorzaken (database bestaat niet, login heeft geen toegang) worden niet opgelost door opnieuw te proberen; controleer de databasenaam, de loginkoppeling en de status van de database.
4221 Login to read-secondary failed due to long wait on 'HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING'. De replica is niet beschikbaar voor aanmelden omdat voor transacties die nog in uitvoering waren toen de replica werd gerecycled, rijversies ontbreken. Draai de actieve transacties op de primaire server terug of leg ze vast om het probleem op te lossen. Beperk dit door lange schrijftransacties op de primaire schijf te voorkomen.
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.) De lokale kant breekt de verbinding af. Controleer de netwerkstatus aan de clientzijde en een lokale firewall of VPN-client.
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.) De externe zijde verzendt een TCP-reset. Veelvoorkomende oorzaken: het peerproces is vastgelopen, een firewall heeft een reset geïnjecteerd of de Azure SQL-gateway heeft een niet-actieve verbinding gesloten. Schakel voor patronen van resets door inactiviteit TCP-keepalive in op de client of verkort de inactiviteitstime-out van de connectionpool.
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. De database overschrijdt een Azure SQL limiet voor resourcebeheer. Resource-id 1 geeft de werkrollimiet aan; Resource-id 2 geeft de sessielimiet aan. Identificeer het limiettype van het bericht en verminder de gelijktijdigheid, schaal de database omhoog of verkort langlopende bewerkingen die de resource bevatten.
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. De database zit boven de minimumgarantie en de onderliggende server beperkt de prestaties. Het opnieuw proberen slaagt meestal wanneer de load van de buur afneemt. Aanhoudende incidenten geven aan dat u een hoger serviceniveau of een minder rumoerige omgeving nodig hebt.
40020,40143,40166,40540 Gerapporteerd in de Error code %d sleuf van fout 40197 tijdens de failover. Subcodes die zijn ingebed in een 40197-failoverbericht en die sommige paden weergeven als foutnummer op topniveau. Behandel ze hetzelfde als 40197.
40197 The service has encountered an error processing your request. Please try again. Error code %d. Een software-upgrade, een hardwarefout of een andere failovergebeurtenis in Azure SQL. Opnieuw verbinden leidt u naar een gezonde replica. De ingesloten foutcode identificeert het failovertype. Als de fout zich blijft voordoen, legt u de sessietracerings-id vast en neemt u contact op met de ondersteuning.
40501 The service is currently busy. Retry the request after 10 seconds. Incident ID: %ls. Code: %d. Azure SQL motorbeperking. De aanbevolen ondergrens is een wachttijd van 10 seconden. Aanhoudende throttling geeft aan dat de belasting de aan de database toegewezen resources heeft overschreden; schaal het serviceniveau op of verminder het aantal gelijktijdige aanvragen.
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'. De database is niet beschikbaar, meestal halverwege een failover of kort tijdens een schaalbewerking. Probeer het na enige wachttijd opnieuw; als het probleem na enkele minuten aanhoudt, leg dan de sessietracerings-id vast en maak een supportticket aan.
42108 Can not connect to the SQL pool since it is paused. Please resume the SQL pool and try again. De toegewezen SQL-pool (Synapse) is onderbroken. Opnieuw proberen lukt pas nadat de pool is hervat. Hervat de pool expliciet of plan de workload zodanig dat deze wordt uitgevoerd nadat de pool is hervat.
42109 The SQL pool is warming up. Please try again. De toegewezen SQL-pool wordt hervat. Blijf het opnieuw proberen met steeds langere tussenpozen totdat de pool online is; het opstarten duurt meestal een paar minuten.
49918 Cannot process request. Not enough resources to process request. The service is currently busy. Please retry the request later. De server kan momenteel onvoldoende resources toewijzen om aan de aanvraag te voldoen. Opnieuw proberen na een wachttijd. Als de fout zich blijft voordoen, schaalt u de database of elastische pool omhoog.
49919 Cannot process create or update request. Too many create or update operations in progress for subscription "%ld". Gelijktijdigheidslimiet op abonnementsniveau voor beheerbewerkingen. Verminder parallelle aanroepen voor maken of bijwerken, of spreid ze in de tijd.
49920 Cannot process request. Too many operations in progress for subscription "%ld". Gelijktijdigheidslimiet op abonnementsniveau voor bewerkingen in vlucht. Verminder het parallelisme of wacht tot de lopende bewerkingen zijn voltooid.

Fouten op instructieniveau staan niet in deze lijst, omdat ze optreden nadat de verbinding tot stand is gekomen en de sessie na de fout bruikbaar blijft. De meest voorkomende fouten in opnieuw uit te voeren instructies zijn 1205 (deadlockslachtoffer) en 1222 (time-out bij een vergrendelingsaanvraag). Voer de hele transactie opnieuw uit in plaats van de enkele mislukte instructie.

De tekst van het foutbericht is afkomstig van Azure SQL tijdelijke verbindingsfouten. Afzonderlijke stuurprogramma's onderhouden hun eigen ingebouwde lijsten voor nieuwe pogingen; in deze catalogus wordt beschreven welke fouten in aanmerking komen voor nieuwe pogingen in SQL Server, Azure SQL Database, Azure SQL Managed Instance, SQL-database in Microsoft Fabric en toegewezen SQL-pools in Azure Synapse Analytics.

Veerkracht van inactieve verbindingen van het ODBC-stuurprogramma

Het Microsoft ODBC-stuurprogramma voor SQL Server biedt ingebouwde niet-actieve verbindingstolerantie via de ConnectRetryCount trefwoorden en ConnectRetryInterval verbindingsreeks trefwoorden. Deze instellingen verwerken verbroken inactieve verbindingen op stuurprogrammaniveau voordat uw toepassingscode wordt betrokken.

Niet-actieve verbindingstolerantie inschakelen in 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 Aantal pogingen om automatisch opnieuw verbinding te maken voor niet-actieve verbindingen.
ConnectRetryInterval 10 Seconden tussen pogingen om opnieuw verbinding te maken.

Note

De tolerantie van niet-actieve verbindingen maakt opnieuw verbinding met verbindingen die zijn verbroken tijdens inactiviteit. Het probeert mislukte query's niet opnieuw uit te voeren en herstelt niet van fouten die optreden tijdens actieve transacties. Gebruik voor deze scenario's de logica voor opnieuw proberen op toepassingsniveau.

Django-database-middleware voor nieuwe pogingen

Maak een Django-middleware die tijdelijke fouten onderschept en de databasebewerking opnieuw probeert uit te voeren. Deze aanpak werkt voor het verwerken van aanvragen op weergaveniveau:

# 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

Registreer de middleware in settings.py:

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

Important

Plaats DatabaseRetryMiddleware vóór andere middleware die toegang hebben tot de database, zodat deze tijdelijke fouten uit de hele requestpijplijn kan onderscheppen en opnieuw kan proberen.

Decorator opnieuw proberen voor specifieke bewerkingen

Gebruik voor fijnmazige controle een decorator voor afzonderlijke functies:

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

Pas de decorator toe op database-intensieve functies:

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

Opnieuw proberen met transacties

Wanneer er een tijdelijke fout optreedt in een transactie, wordt de hele transactie teruggedraaid door de server. Voer de volledige transactie opnieuw uit, niet alleen de mislukte instructie:

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()

Waarschuwing

Probeer het niet opnieuw in transaction.atomic(). De retry-decorator moet het volledige atomic()-blok omvatten, zodat bij elke nieuwe poging een nieuwe transactie wordt gestart.

Fouten op instructieniveau

De foutenlijst in de vorige sectie bevat fouten op verbindingsniveau. Twee andere fouten worden vaak opnieuw uitgevoerd op statementniveau:

  • 1205: De sessie werd aangewezen als het deadlock-slachtoffer. Voer de transactie opnieuw uit.
  • 1222: Er is een time-out voor vergrendelingsaanvragen overschreden. Voer de transactie opnieuw uit of verhoog LOCK_TIMEOUT voor de sessie als de standaardwaarde te agressief is.

ConnectRetryCount probeert verbroken verbindingen opnieuw, dus dit is niet van toepassing op deze fouten op opdrachtniveau. Verwerk ze met hetzelfde decoratorpatroon door "1205" en "1222" toe te voegen aan TRANSIENT_ERROR_CODES voor transacties die veilig opnieuw kunnen worden uitgevoerd.

CONN_MAX_AGE en verouderde verbindingen

Django hergebruikt databaseverbindingen tussen aanvragen wanneer CONN_MAX_AGE deze is ingesteld. Een langlevende verbinding kan verouderen als de server deze sluit (bijvoorbeeld tijdens een Azure SQL-schaalactie of een firewalltime-out).

Ingesteld CONN_MAX_AGE op evenwicht tussen hergebruik en veroudering:

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 (standaard): Sluit de verbinding aan het einde van elke aanvraag. Veiligste maar langzaamste.
  • CONN_MAX_AGE=600: Verbindingen gedurende 10 minuten opnieuw gebruiken. Goede balans voor de meeste webtoepassingen.
  • CONN_MAX_AGE=None: Houd verbindingen voor onbepaalde tijd open. Alleen gebruiken met een mechanisme om verouderde verbindingen opnieuw te proberen.

CONN_HEALTH_CHECKS (Django 4.1 en hoger)

Django 4.1 geïntroduceerd CONN_HEALTH_CHECKS, waarmee een opnieuw gebruikte verbinding wordt gevalideerd voor elke aanvraag. Schakel dit samen met CONN_MAX_AGE in om verouderde verbindingen automatisch te detecteren:

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,
    },
}

Als statuscontroles zijn ingeschakeld, geeft Django een lichtgewicht validatiequery uit voordat een verbinding opnieuw wordt gebruikt. Als de verbinding verbroken is, opent Django transparant een nieuwe in plaats van een fout op te geven.

Beste praktijken

  • Gebruik exponentiële back-off met volledige jitter. Verdubbel bij elke poging de limiet en wacht vervolgens een willekeurige tijd binnen [0, cap]. Jitter voorkomt dat veel clients tijdens een regionale storing gelijktijdig opnieuw proberen, wat anders een kortdurende storing kan doen uitgroeien tot aanhoudende overbelasting. Beperk de slaapstand per poging (bijvoorbeeld 30 seconden), zodat de totale hersteltijd gebonden blijft.
  • Stel een plafond voor opnieuw proberen in. Drie herpogingen met exponentiële wachttijd is een redelijke standaardinstelling. Meer dan vijf nieuwe pogingen duiden meestal op een niet-tijdelijk probleem.
  • Sluit de verbinding voordat u het opnieuw probeert. Bel connection.close() zodat Django een nieuwe verbinding opent bij de volgende poging.
  • Registreer elke nieuwe poging. Herhaalde pogingen die ongemerkt slagen, kunnen prestatieproblemen verbergen. Meld u aan op WARNING niveau zodat u de frequentie kunt bijhouden.
  • Probeer niet opnieuw niet-tijdelijke fouten. Verificatiefouten, machtigingsfouten en syntaxisfouten profiteren niet van nieuwe pogingen.
  • Voer de hele transactie opnieuw uit. Plaats transaction.atomic() binnen de retrylogica, niet andersom.
  • Inschakelen CONN_HEALTH_CHECKS (Django 4.1 en hoger) voor webtoepassingen die gebruikmaken van CONN_MAX_AGE.