Zurück zur Startseite
Leistungen

Backend Engineering, Infrastruktur

Branche

B2B-Marktplatz

Jahr

2021-2024

Besuchertracking, das seinen eigenen Traffic überlebt.

Bei 50 Requests pro Sekunde war die Tracking-Middleware eines B2B-Marktplatzes unsichtbar. Bei 500 begann sie zu werfen. Zwei Requests desselben Besuchers kollidierten dutzende Male pro Minute in der Datenbank, einer stürzte ab, und ein Seitenaufruf, den niemand bemerken sollte, riss den ganzen Request mit sich. Nebenbei liefen Cookies über und Bots füllten die Tabellen.

DIE HERAUSFORDERUNG

Wenn get_or_create sich selbst begegnet

Djangos get_or_create sind zwei Queries, nicht eine. Zwei gleichzeitige Requests suchen den Besucher, beide finden nichts, beide fügen ein. Die Datenbank behält eine Zeile und antwortet dem anderen Request mit einem IntegrityError. Die unbehandelte Exception wird zur Fehlerseite, für etwas so Banales wie das Zählen eines Seitenaufrufs. Die naheliegenden Auswege waren gesperrt: keine Transaktion um jeden Request, keine Locks, und nie ein Fehler, der beim Nutzer ankommt.

DIE LÖSUNG

Das Double-Try-Pattern

Die Kandidaten waren SELECT FOR UPDATE, Advisory Locks oder eine Retry-Schleife mit Backoff. Alle drei zahlen bei jedem Request, um eine Kollision zu verhindern, die nur einen kleinen Teil davon trifft: Ein Row Lock serialisiert jeden Besucher-Lookup in eine Warteschlange, eine Backoff-Schleife wartet, wo nichts zu warten ist. Die Entscheidung: die Kollision billig machen, statt sie zu verhindern. Die Middleware ruft get_or_create auf wie bisher. Antwortet die Datenbank mit einem IntegrityError, existiert die Zeile, also fängt der Handler den Fehler und macht ein einfaches get(). Kein Lock, keine Transaktion, keine Schleife: ein zusätzlicher Read von etwa 2 ms, nur bei den Requests, die tatsächlich kollidiert sind. Der Preis: ein Unique Constraint auf dem Session-Key, der existieren muss, und ein Codepfad, der jedem falsch vorkommt, der gelernt hat, IntegrityErrors nie zu schlucken.

Der Lookup, mit der Cookie-Prüfung davor:

Python
def resolve_visitor(self, request):
    session_key = request.COOKIES.get('visitor_key')

    # Validate cookie format
    if session_key and len(session_key) > 64:
        session_key = None  # discard oversized cookie

    if not session_key:
        session_key = uuid4().hex

    try:
        visitor, created = Visitor.objects.get_or_create(
            session_key=session_key,
            defaults={
                'ip_address': self.get_client_ip(request),
                'user_agent': request.META.get('HTTP_USER_AGENT', '')[:512],
            },
        )
    except IntegrityError:
        # A concurrent request already created it, just fetch
        visitor = Visitor.objects.get(session_key=session_key)

    return visitor, session_key

Kollisionen beobachten

Requests treffen etwa einmal pro Sekunde ein; rund ein Drittel kommt paarweise für denselben neuen Besucher. Kollidiert ein Paar, wird der erste Versuch des unterlegenen Requests rot mit einem IntegrityError, und sein zweiter Versuch liest die Zeile, die der Gewinner gerade angelegt hat. Beide antworten 200. Behalte den Zähler für abgestürzte Requests im Auge: Er bleibt bei null.

Middleware-Trace
Besucher-KeyVersuch 1: get_or_createVersuch 2: get()Antwort
v-c5b3angelegtnicht nötig200 OK
v-f1a9gefundennicht nötig200 OK
v-d3e7IntegrityErrorgefunden200 OK
v-d3e7angelegtnicht nötig200 OK
v-b8c1gefundennicht nötig200 OK
v-a4f2angelegtnicht nötig200 OK
Requests
6
Kollisionen abgefangen
1
Requests abgestürzt
0
Kollisionsrate
17%

Was sonst noch unter Last brach

  • Übergroße Cookies

    Manche Browser und Proxys kürzen Cookies über 4 KB. Die Middleware prüft Länge und Format des Session-Keys vor jedem Lookup. Ein fehlerhafter Wert wird verworfen und ein neuer Key ausgegeben, damit keine Query mit einem Key läuft, der nie passen kann.

  • Bots und Monitoring

    Kompilierte Muster erkennen Googlebot, Bingbot, UptimeRobot, Pingdom sowie die Pfade /healthcheck und /status. Diese Requests umgehen die Middleware komplett: kein Write, kein Cookie, keine Besucherzeile. Die Regex wird einmal beim Import kompiliert, nicht pro Request.

  • Weitergeleitete IP-Adressen

    Hinter dem Load Balancer kommt X-Forwarded-For mit mehreren Adressen, IPv6 oder Müll an. Die Middleware nimmt die linkeste nicht-private Adresse, validiert sie, fällt auf die direkte Verbindung zurück und speichert 'unknown', statt eine Exception zu werfen.

Die Ausschlussliste, einmal beim Import kompiliert:

Python
BOT_PATTERNS = re.compile(
    r'bot|crawl|spider|slurp|Googlebot|Bingbot|'
    r'UptimeRobot|Pingdom|StatusCake|NewRelic|Datadog',
    re.IGNORECASE,
)

SKIP_PATHS = re.compile(
    r'^/(healthcheck|status|robots\.txt|favicon\.ico)',
)

def should_track(self, request):
    ua = request.META.get('HTTP_USER_AGENT', '')
    if BOT_PATTERNS.search(ua):
        return False
    if SKIP_PATHS.match(request.path):
        return False
    return True
DAS ERGEBNIS

Wieder unsichtbar

Die Middleware ist wieder etwas, das niemand bemerkt. Seit der Änderung hat kein IntegrityError einen Nutzer erreicht, und die Logs zeigen in 18 Monaten Produktion keinen einzigen Middleware-Fehler. Der Bot-Ausschluss senkte die Datenbank-Writes um 34 Prozent, die Cookie-Prüfung beseitigte jede Query mit fehlerhaftem Key. In dieser Zeit zählte die Middleware 47 Millionen Seitenaufrufe, und der Marktplatz behielt seine Analytics auf den eigenen Servern: kein Drittanbieter-Tool, kein Datenschutz-Banner, Zahlen, die das Team im Code nachlesen kann.

WICHTIGE KENNZAHLEN

500+Requests pro Sekunde, dauerhaft
2,8MEindeutige Besucher in 18 Monaten
0Middleware-Fehler in 18 Monaten

FÜR DEIN PROJEKT

  • Wann es passt

    Jedes get_or_create, Upsert oder Prüfen-dann-Einfügen auf einem heißen Pfad: Besucherzeilen, Session-Zeilen, Idempotency-Keys. Wenn zwei Requests gleichzeitig denselben Key tragen können, kollidieren sie irgendwann.

  • Was du prüfen solltest

    Such in deinem Error-Tracker nach IntegrityError auf einem Unique Constraint. Jeder Treffer ist ein Request, den ein Nutzer scheitern sah. Die Lösung ist ein try/except um den Aufruf, kein Lock um die Tabelle.

  • Was es braucht

    Ein Unique Constraint, den du vermutlich schon hast, ein paar Zeilen pro Aufrufstelle und eine Bot-Liste, die jemand pflegt. Kein Lock-Manager, keine Queue, keine Änderung an der Datenbank.

KUNDENSTIMME

Unsere Analytics laufen jetzt auf unseren eigenen Servern. Nichts verlässt das Haus, es gibt kein Datenschutz-Banner, und wenn eine Zahl komisch aussieht, öffnen wir die Middleware und lesen nach.

CTO

B2B-Marktplatz

FAQ

TECHNOLOGIE-STACK

Django
Python
PostgreSQL

Seit mittlerweile 10 Jahren arbeiten wir immer wieder gerne mit Daniel zusammen. Wir schätzen dabei sehr seine schnellen Reaktionszeiten rund um die Uhr und sein Allroundwissen. Egal ob Serverkonfigurationen oder Programmierungen, er hat immer die passende Lösung.

Manuel Kasbarian - Geschäftsführer, SophistiX

Tritt in die Fußstapfen von Manuel und erwecke deine Vision zum Leben.

Schreibe mirSchreibe mir