Wiederholungslogik und Verbindungsresilienz mit mssql-django

Verbindungen mit SQL Server und Azure SQL können vorübergehend aus Gründen fehlschlagen, die nichts mit Ihrem Code zu tun haben:

  • Eine AlwaysOn-Verfügbarkeitsgruppe schlägt fehl.
  • Das Netzwerk legt während der Verbindungseinrichtung ein Paket ab.
  • Resource Governor drosselt die Datenbank.
  • Ein Azure SQL Replikat wird während einer Skalierung oder eines Upgrades wiederverwendet.

Die meisten dieser Fehler werden innerhalb von Sekunden gelöscht. In diesem Artikel wird gezeigt, wie Sie vorübergehende Fehler in einer Django-Anwendung wiederholen, die das mssql-django Back-End verwendet, und wie Sie Django und den ODBC-Treiber so konfigurieren, dass die Wiederherstellung von Leerlaufverbindungsabbrüchen automatisch erfolgt.

Vorübergehende Fehler

Vorübergehende Fehler sind temporäre Fehler, die eigenständig aufgelöst werden. Ein erneuter Versuch des Vorgangs nach einer kurzen Verzögerung ist in der Regel erfolgreich.

Die folgenden Fehler sind vorübergehend, wenn sie während der Verbindungseinrichtung oder beim Senden einer Anforderung an den Server auftreten. Versuchen Sie es mit einem kurzen, begrenzten Backoff. Fehler, die über einige Wiederholungen bestehen, weisen in der Regel auf ein Konfigurationsproblem hin (falscher Server, fehlende Berechtigungen, erschöpftes Kontingent), das nicht behoben werden kann.

Fehler Meldung Problembehandlung
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.) Die TCP-Verbindung bricht während des Handshakes ab. Kein Anmeldefehler. Wenn sie weiterhin besteht, suchen Sie nach clientseitiger Netzwerkinstabilität oder einem Zwischengerät, das halb etablierte Verbindungen abbricht.
233 The client was unable to establish a connection because of an error during connection initialization process before login. Transport- oder TLS-Fehler vor der Anmeldung. Der Server gibt sie häufig zurück, wenn sie die Verbindung nicht akzeptieren kann (Ressourcenausschöpfung, max. Verbindungen erreicht oder ein nicht unterstützter Client). Kein Anmeldefehler. Überprüfen Sie den Serverzustand und prüfen Sie dann die Anmeldezeitüberschreitung des Clients, die TLS-Einstellungen und die TLS-Version-Kompatibilität zwischen Client und Server.
4060 Cannot open database "%.*ls" requested by the login. The login failed. Die Anmeldung authentifiziert sich, kann die angeforderte Datenbank aber nicht öffnen. Vorübergehende Ursachen sind unter anderem, dass sich die Datenbank in einem Übergangszustand befindet (Failover, Wiederherstellung, Skalierung) oder automatisch angehalten wurde. Persistente Ursachen (Datenbank ist nicht vorhanden, Anmeldezugriff fehlt) werden nicht durch Wiederholung behoben. Überprüfen Sie den Datenbanknamen, die Anmeldezuordnung und den Datenbankstatus.
4221 Login to read-secondary failed due to long wait on 'HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING'. Das Replikat ist für die Anmeldung nicht verfügbar, da Zeilenversionen für Transaktionen fehlen, die im Test-Flight ausgeführt wurden, als das Replikat wiederverwendet wurde. Führen Sie ein Rollback aus, oder führen Sie einen Commit für die aktiven Transaktionen auf der primären Seite durch, um das Problem zu beheben. Abhilfe schaffen Sie, indem Sie lange Schreibtransaktionen auf der Primärinstanz vermeiden.
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.) Die lokale Seite bricht die Verbindung ab. Überprüfen Sie die clientseitige Netzwerkintegrität und alle lokalen Firewalls oder VPN-Clients.
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.) Die Gegenseite sendet einen TCP-Reset. Häufige Ursachen: der Peerprozess ist abgestürzt, eine Firewall hat ein Reset-Paket gesendet, oder das Azure SQL-Gateway hat eine inaktive Verbindung geschlossen. Aktivieren Sie für Idle-Reset-Muster tcp keepalive auf dem Client, oder kürzen Sie das Leerlauftimeout für den Verbindungspool.
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. Die Datenbank überschreitet einen Grenzwert der Azure SQL-Ressourcensteuerung. Ressourcen-ID 1 gibt den Arbeitsgrenzwert an; Die Ressourcen-ID 2 gibt den Sitzungsgrenzwert an. Ermitteln Sie anhand der Meldung die Art der Begrenzung und verringern Sie dann die Parallelität, skalieren Sie die Datenbank hoch oder verkürzen Sie lang andauernde Vorgänge, die die Ressource belegen.
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. Die Datenbank überschreitet ihre Mindestgarantie, und der zugrunde liegende Server drosselt die Leistung. Der Wiederholungsvorgang ist in der Regel erfolgreich, wenn die Benachbarte Last fällt. Dauerhafte Vorkommen deuten darauf hin, dass Sie eine höhere Dienstebene oder eine weniger laute Umgebung benötigen.
40020, 40143, 40166, 40540 Gemeldet im Error code %d-Slot von Fehler 40197 während des Failovers. In eine 40197-Failovermeldung eingebettete Untercodes, die auf einigen Pfaden als Fehlernummer auf oberster Ebene erscheinen. Behandeln Sie sie genauso wie 40197.
40197 The service has encountered an error processing your request. Please try again. Error code %d. Ein Softwareupgrade, Hardwarefehler oder ein anderes Failoverereignis in Azure SQL. Durch erneutes Verbinden werden Sie zu einem fehlerfreien Replikat weitergeleitet. Der eingebettete Fehlercode identifiziert den Failovertyp. Wenn der Fehler weiterhin besteht, erfassen Sie die Sitzungsablaufverfolgungs-ID, und wenden Sie sich an den Support.
40501 The service is currently busy. Retry the request after 10 seconds. Incident ID: %ls. Code: %d. Drosselung der Azure SQL-Datenbank-Engine Die empfohlene Untergrenze für die Wartezeit beträgt 10 Sekunden. Eine dauerhafte Drosselung weist darauf hin, dass die Arbeitsauslastung die Ressourcenzuordnung der Datenbank überschritten hat; Skalieren Sie die Dienstebene, oder reduzieren Sie die Parallelität.
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'. Die Datenbank ist nicht verfügbar, in der Regel mitten im Failover oder kurz während eines Skalierungsvorgangs. Versuchen Sie es nach einer Wartezeit erneut; wenn das Problem nach einigen Minuten weiterhin besteht, notieren Sie die Session-Trace-ID und eröffnen Sie einen Supportfall.
42108 Can not connect to the SQL pool since it is paused. Please resume the SQL pool and try again. Der dedizierte SQL-Pool (Synapse) befindet sich in einem angehaltenen Zustand. Der Wiederholungsversuch ist erst erfolgreich, nachdem der Pool wiederaufgenommen wurde. Nehmen Sie den Pool explizit wieder auf, oder planen Sie die Workload so, dass sie erst nach der Wiederaufnahme des Pools ausgeführt wird.
42109 The SQL pool is warming up. Please try again. Der dedizierte SQL-Pool wird wieder aufgenommen. Wiederholen Sie einen Backoff, bis der Pool online ist. Das Aufwärmen dauert in der Regel ein paar Minuten.
49918 Cannot process request. Not enough resources to process request. The service is currently busy. Please retry the request later. Der Server kann derzeit nicht genügend Ressourcen zuordnen, um die Anforderung zu erfüllen. Wiederholen Sie den Vorgang auf einem Backoff. Wenn der Fehler weiterhin besteht, skalieren Sie die Datenbank oder den elastischen Pool.
49919 Cannot process create or update request. Too many create or update operations in progress for subscription "%ld". Parallelitätsgrenze auf Abonnementebene bei Verwaltungsvorgängen. Reduzieren Sie parallele Erstellungs-/Aktualisierungsaufrufe oder führen Sie sie gestaffelt aus.
49920 Cannot process request. Too many operations in progress for subscription "%ld". Parallelitätsgrenzwert auf Abonnementebene für Vorgänge in Flight. Reduzieren Sie die Parallelität, oder warten Sie, bis die laufenden Vorgänge abgeschlossen sind.

Fehler auf Anweisungsebene befinden sich nicht in dieser Liste, da sie ausgelöst werden, nachdem die Verbindung hergestellt wurde und der Fehler die Sitzung nutzbar lässt. Die häufigsten Fehler bei Anweisungen, die erneut ausgeführt werden können, sind 1205 (Deadlock-Opfer) und 1222 (Zeitüberschreitung bei der Sperranforderung). Wiederholen Sie die gesamte Transaktion statt der einzelnen fehlerhaften Anweisung.

Fehlermeldungstext stammt aus Azure SQL vorübergehenden Verbindungsfehlern. Einzelne Treiber verwalten ihre eigenen integrierten Wiederholungslisten; in diesem Katalog wird beschrieben, welche Fehler in SQL Server, Azure SQL-Datenbank, Azure SQL Managed Instance, SQL-Datenbank in Microsoft Fabric und dedizierten SQL-Pools in Azure Synapse Analytics für Wiederholungen geeignet sind.

Resilienz von Leerlaufverbindungen des ODBC-Treibers

Der Microsoft ODBC-Treiber für SQL Server bietet integrierte Resilienz bei Leerlaufverbindungen durch die Schlüsselwörter ConnectRetryCount und ConnectRetryInterval der Verbindungszeichenfolge. Diese Einstellungen verwalten unterbrochene Leerlaufverbindungen auf Treiberebene, bevor Ihr Anwendungscode ins Spiel kommt.

Aktivieren sie die Ausfallsicherheit der Leerlaufverbindung 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",
        },
    },
}
Schlüsselwort Vorgabe Beschreibung
ConnectRetryCount 1 Anzahl der automatischen Wiederholungsversuche für Leerlaufverbindungen.
ConnectRetryInterval 10 Sekunden zwischen erneuten Verbindungsversuchen.

Note

Die Ausfallsicherheit der Leerlaufverbindung stellt verbindungen wieder her, die beim Leerlauf verworfen wurden. Es wiederholt keine fehlgeschlagenen Abfragen und erholt sich nicht von Fehlern, die während aktiver Transaktionen auftreten. Verwenden Sie für diese Szenarien die Wiederholungslogik auf Anwendungsebene.

Django-Datenbank-Middleware für Wiederholungsversuche

Erstellen Sie eine Django-Middleware, die vorübergehende Fehler abfangen und den Datenbankvorgang erneut überprüft. Dieser Ansatz funktioniert für die Verarbeitung von Anforderungen auf Ansichtsebene:

# 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

Registrieren Sie die Middleware in settings.py:

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

Important

Platzieren Sie DatabaseRetryMiddleware vor anderen Middleware, die auf die Datenbank zugreift, damit vorübergehende Fehler aus der gesamten Anforderungspipeline erfasst und wiederholt werden können.

Retry-Decorator für bestimmte Vorgänge

Verwenden Sie für feinkörnige Steuerung einen Dekorateur für einzelne Funktionen:

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

Wenden Sie den Dekorierer auf datenbankintensive Funktionen an:

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

Wiederholen von Transaktionen

Wenn innerhalb einer Transaktion ein vorübergehender Fehler auftritt, wird die gesamte Transaktion vom Server zurückgesetzt. Wiederholen Sie die vollständige Transaktion, nicht nur die fehlgeschlagene Anweisung:

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

Vorsicht

Wiederholen Sie den Vorgang nicht innerhalb transaction.atomic(). Der Wiederholungsdekorator muss den gesamten atomic()-Block umschließen, damit jeder Wiederholungsversuch eine neue Transaktion startet.

Fehler auf Aussageebene

Die Fehlerliste im vorherigen Abschnitt behandelt Fehler auf Verbindungsebene. Zwei weitere Fehler werden häufig auf Anweisungsebene wiederholt:

  • 1205: Die Sitzung wurde als Deadlock-Opfer ausgewählt. Führen Sie die Transaktion erneut aus.
  • 1222: Ein Timeout für Sperranforderungen wurde überschritten. Führen Sie die Transaktion erneut aus, oder erhöhen Sie LOCK_TIMEOUT für die Sitzung, wenn der Standardwert zu aggressiv ist.

ConnectRetryCount wiederholt Verbindungsversuche bei unterbrochenen Verbindungen und gilt daher nicht für diese Fehler auf Anweisungsebene. Behandeln Sie sie mit demselben Decorator-Muster, indem Sie "1205" und "1222" zu TRANSIENT_ERROR_CODES für Transaktionen hinzufügen, die sicher erneut ausgeführt werden können.

CONN_MAX_AGE und veraltete Verbindungen

Django verwendet Datenbankverbindungen über Anforderungen hinweg, wenn CONN_MAX_AGE festgelegt wird. Eine langlebige Verbindung kann veraltet werden, wenn der Server sie schließt (z. B. während eines Azure SQL Skalierungsvorgangs oder eines Firewalltimeouts).

Legen Sie CONN_MAX_AGE fest, dass die Wiederverwendung gegen Veraltetkeit ausgeglichen wird:

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 (Standard): Schließen Sie die Verbindung am Ende jeder Anforderung. Sicherste, aber langsamste.
  • CONN_MAX_AGE=600: Verbindungen 10 Minuten lang wiederverwenden. Gute Balance für die meisten Webanwendungen.
  • CONN_MAX_AGE=None: Lassen Sie Verbindungen unbegrenzt geöffnet. Wird nur mit einem Wiederholungsmechanismus für veraltete Verbindungen verwendet.

CONN_HEALTH_CHECKS (Django 4.1 und höher)

Django 4.1 führte CONN_HEALTH_CHECKS ein, das eine wiederverwendete Verbindung vor jeder Anfrage überprüft. Aktivieren Sie es zusammen mit CONN_MAX_AGE, um inaktive Verbindungen automatisch zu erkennen:

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

Mit aktivierten Integritätsprüfungen gibt Django eine einfache Überprüfungsabfrage aus, bevor eine Verbindung erneut verwendet wird. Wenn die Verbindung unterbrochen wird, öffnet Django transparent eine neue, anstatt einen Fehler zu auslösen.

Bewährte Methoden

  • Verwenden Sie exponentielles Backoff mit vollständigem Jitter. Verdoppeln Sie bei jedem Versuch die Obergrenze und warten Sie dann eine zufällige Zeitspanne innerhalb von [0, cap]. Jitter verhindert, dass viele Clients bei einem regionalen Ausfall gleichzeitig Wiederholungsversuche unternehmen, was andernfalls einen kurzzeitigen Ausfall in eine anhaltende Überlastung verwandeln kann. Begrenzen Sie die Wartezeit pro Versuch (z. B. 30 Sekunden), sodass die gesamte Wiederherstellungszeit begrenzt bleibt.
  • Legen Sie eine Wiederholungsgrenze fest. Drei Wiederholungen mit exponentiellem Backoff sind ein vernünftiger Standardwert. Mehr als fünf Wiederholungsversuche deuten in der Regel auf ein nichttransientes Problem hin.
  • Schließen Sie die Verbindung, bevor Sie den Vorgang wiederholen. Rufen Sie connection.close() auf, damit Django beim nächsten Versuch eine neue Verbindung öffnet.
  • Protokollieren Sie jeden Wiederholungsversuch. Wiederholungsversuche, die stillschweigend erfolgreich sind, können Leistungsprobleme verschleiern. Protokollieren Sie auf WARNING-Ebene, damit Sie die Häufigkeit verfolgen können.
  • Keine erneuten Versuche bei nicht vorübergehenden Fehlern. Authentifizierungsfehler, Berechtigungsfehler und Syntaxfehler profitieren nicht von Wiederholungen.
  • Wiederholen Sie die gesamte Transaktion. Schließen Sie transaction.atomic() in die Wiederholungslogik ein, nicht andersherum.
  • Aktivieren CONN_HEALTH_CHECKS (Django 4.1 und höher) für Webanwendungen, die CONN_MAX_AGE verwenden.