Verteilung von verteilten Aufgaben mit PostgreSQL Advisory Locks orchestrieren
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Einleitung
In der Welt verteilter Systeme ist die Koordinierung von Aufgaben über mehrere unabhängige Instanzen hinweg ein häufiges, aber dennoch herausforderndes Problem. Sicherzustellen, dass nur eine Instanz zu einem bestimmten Zeitpunkt eine kritische Operation durchführt, oder die Verwaltung gemeinsam genutzter Ressourcen ohne Konflikte erfordert einen robusten Synchronisationsmechanismus. Traditionelle Ansätze beinhalten oft dedizierte Warteschlangendienste oder komplexe Konsensalgorithmen, die zusätzliche Infrastruktur-Overhead und Komplexität einführen können. Für viele Szenarien kann eine einfachere, datenbankzentrierte Lösung jedoch bemerkenswert effektiv sein. Dieser Artikel befasst sich damit, wie PostgreSQLs Advisory Locks ein leistungsfähiges, aber oft übersehenes Primitiv zur Erzielung verteilter Aufgabenkoordinierung bieten, eine leichtgewichtige, effiziente und sofort verfügbare Lösung für Anwendungen, die bereits PostgreSQL nutzen.
Verständnis von Advisory Locks und ihrer Anwendung
Bevor wir uns mit der Implementierung befassen, wollen wir die beteiligten Kernkonzepte klar verstehen:
Advisory Locks sind anwendungsebene Sperren, die von PostgreSQL verwaltet werden. Im Gegensatz zu Tabellen- oder Zeilensperren, die automatisch vom Transaktionsmanager der Datenbank erworben und freigegeben werden, sind Advisory Locks explizit. Sie werden vom Anwendungscode erworben und freigegeben, was Entwicklern eine feingranulare Kontrolle über ihren Lebenszyklus gibt. Wichtig ist, dass Advisory Locks nicht an spezifische Datenbankobjekte gebunden sind und reguläre SQL-Operationen auf Tabellen oder Zeilen nicht blockieren. Sie existieren als willkürliche Ganzzahlschlüssel, die der Anwendung bekannt sind, was sie perfekt für die Definition anwendungsspezifischer Synchronisationspunkte macht. PostgreSQL bietet sowohl sitzungs- als auch transaktionsbasierte Advisory Locks. Sitzungsbasierte Sperren bleiben für die Dauer der Datenbanksitzung oder bis zur expliziten Freigabe bestehen, während transaktionsbasierte Sperren am Ende der Transaktion automatisch freigegeben werden. Für die Koordination verteilter Aufgaben sind sitzungsbasierte Sperren normalerweise nützlicher, da sie einer einzelnen Instanz ermöglichen, eine Sperre über mehrere Operationen oder sogar über längere Zeiträume zu halten.
Distributed Task Coordination bezieht sich auf die Herausforderung, Operationen zu verwalten und zu synchronisieren, die von mehreren unabhängigen Prozessen oder Diensten in einer verteilten Umgebung durchgeführt werden. Dies umfasst Szenarien wie die Sicherstellung, dass ein Batch-Job nur einmal ausgeführt wird, die Wahl eines Leaders unter mehreren Instanzen oder den Schutz eines kritischen Codeabschnitts, der mit einer externen Ressource interagiert.
Funktionsprinzip
Das Kernprinzip der Verwendung von Advisory Locks zur Koordination verteilter Aufgaben ist einfach: Jeder verteilten Aufgabe oder kritischem Abschnitt wird eine eindeutige Kennung (typischerweise eine Ganzzahl) zugewiesen. Wenn eine Instanz diese Aufgabe ausführen möchte, versucht sie, die Sperre zu erwerben, die mit dieser Kennung verbunden ist.
- Wenn die Sperre verfügbar ist, erwirbt die Instanz sie und fährt mit der Aufgabe fort. Dies bedeutet, dass sie nun der designierte "Besitzer" oder "Executor" dieser Aufgabe ist.
- Wenn die Sperre bereits von einer anderen Instanz gehalten wird, kann die aktuelle Instanz entweder auf die Freigabe der Sperre warten (blockierendes Verhalten) oder sofort mit einem alternativen Pfad fortfahren (nicht-blockierendes Verhalten). Die Wahl zwischen blockierend und nicht-blockierend hängt von der spezifischen Koordinationsanforderung ab.
Sobald die Aufgabe abgeschlossen ist, gibt die Instanz die Sperre explizit frei und macht sie für andere Instanzen verfügbar. Dieser Mechanismus stellt sicher, dass zu einem bestimmten Zeitpunkt immer nur eine Instanz eine bestimmte Sperre erfolgreich erwerben und halten kann, wodurch der Zugriff auf gemeinsam genutzte Ressourcen koordiniert oder die exklusive Ausführung einer Aufgabe sichergestellt wird.
Implementierungsbeispiel: Sicherstellen, dass ein Batch-Job einmal läuft
Betrachten wir ein häufiges Szenario: Ein täglicher Batch-Job, der nur von einer einzigen Instanz ausgeführt werden sollte, auch wenn mehrere Anwendungsinstanzen ihn ausführen sollen.
Wir verwenden PHP mit der pdo_pgsql-Erweiterung, aber das Konzept ist leicht auf andere Sprachen und ORMs übertragbar.
<?php // Datenbank-Verbindungsdetails $dsn = 'pgsql:host=localhost;port=5432;dbname=mydatabase'; $user = 'myuser'; $password = 'mypassword'; // Eindeutige Kennung für die Advisory Lock unseres Batch-Jobs // Es ist gute Praxis, diese von einer Konstante oder einer aussagekräftigen Zeichenkette abzuleiten const BATCH_JOB_LOCK_ID = 123456; try { $pdo = new PDO($dsn, $user, $password, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_CASE => PDO::CASE_NATURAL, ]); echo "Versuche, die Sperre für den Batch-Job zu erwerben...\n"; // Versuche, eine sitzungsbasierte Advisory Lock zu erwerben (nicht-blockierend) // pg_try_advisory_lock(key) gibt true zurück, wenn erfolgreich, sonst false $stmt = $pdo->prepare("SELECT pg_try_advisory_lock(:lock_id)"); $stmt->execute([':lock_id' => BATCH_JOB_LOCK_ID]); $result = $stmt->fetchColumn(); if ($result) { echo "Sperre erfolgreich erworben! Führe Batch-Job aus...\n"; // Simulieren der Ausführung des Batch-Jobs sleep(5); // Die eigentliche Logik des Batch-Jobs würde hier stehen echo "Batch-Job abgeschlossen.\n"; // Sperre freigeben // pg_advisory_unlock(key) $stmt = $pdo->prepare("SELECT pg_advisory_unlock(:lock_id)"); $stmt->execute([':lock_id' => BATCH_JOB_LOCK_ID]); echo "Sperre freigegeben.\n"; } else { echo "Sperre konnte nicht erworben werden. Eine andere Instanz führt wahrscheinlich den Batch-Job aus. Überspringe.\n"; } } catch (PDOException $e) { echo "Datenbankfehler: " . $e->getMessage() . "\n"; exit(1); } ?>
Erklärung:
- Eindeutige Sperr-ID:
BATCH_JOB_LOCK_IDist eine willkürliche Ganzzahl, die diesen spezifischen Batch-Job eindeutig über alle Instanzen hinweg identifiziert. pg_try_advisory_lock(): Dies ist die Schlüsselfunktion. Sie versucht, eine sitzungsbasierte Advisory Lock zu erwerben. Wenn die Sperre bereits von einer anderen Sitzung (auch von einer anderen Anwendungsinstanz) gehalten wird, gibt sie sofortfalsezurück, ohne zu blockieren. Dies ist entscheidend, um Deadlocks zu vermeiden und anderen Instanzen zu ermöglichen, die Ausführung ordnungsgemäß zu überspringen.- Kritischer Abschnitt: Der Code innerhalb des
if ($result)-Blocks ist der kritische Abschnitt – der Teil des Codes, den nur eine Instanz ausführen sollte. pg_advisory_unlock(): Sobald der kritische Abschnitt abgeschlossen ist (oder wenn ein Fehler auftritt und Sie die Sperre explizit freigeben möchten), wirdpg_advisory_unlock()aufgerufen, um die Sperre freizugeben. Es ist wichtig sicherzustellen, dass Sperren immer freigegeben werden, sei es durch explizite Aufrufe oder indem die Sitzung endet.
Anwendungsfälle
- Leader Election: Eine Reihe von Instanzen kann um den Erwerb einer bestimmten Advisory Lock "rennen". Diejenige, die erfolgreich ist, wird zum Leader. Wenn der Leader ausfällt, endet seine Sitzung schließlich, gibt die Sperre frei, und eine andere Instanz kann sie erwerben.
- Ratenbegrenzung gegenüber externen APIs: Bevor ein Aufruf an einen externen Dienst mit Ratenbegrenzungen erfolgt, kann eine Instanz eine Advisory Lock erwerben, die einen "Ratenbegrenzungs-Slot" darstellt. Dies begrenzt die Anzahl gleichzeitiger Aufrufe.
- Verteilte Cache-Invalidierung: Wenn ein Cache invalidiert werden muss, kann eine Instanz eine Sperre erwerben, um die Invalidierung durchzuführen und andere Instanzen daran zu hindern, gleichzeitig denselben kostspieligen Vorgang zu versuchen.
- Ressourcenreservierung: Reservieren Sie eine gemeinsam genutzte, begrenzte Ressource für einen bestimmten Zeitraum, indem Sie eine Advisory Lock halten.
Überlegungen und Best Practices
- Lock-IDs: Verwenden Sie große, eindeutige Ganzzahlwerte für Lock-IDs, um versehentliche Kollisionen zu vermeiden. Erwägen Sie das Hashing von Zeichenketten oder das Kombinieren von Bezeichnern, um eindeutige Lock-IDs programmatisch zu generieren. PostgreSQL bietet
pg_advisory_lock(bigint key1, bigint key2)für 128-Bit-Schlüssel, was noch mehr Namensraum bietet. - Fehlerbehandlung: Behandeln Sie immer Datenbankverbindungsfehler und stellen Sie sicher, dass Sperren freigegeben werden, auch wenn der kritische Abschnitt fehlschlägt.
- Sitzungsbeendigung: Sitzungsbasierte Advisory Locks werden automatisch freigegeben, wenn die Datenbanksitzung endet (z. B. Anwendungsabsturz, Verbindung verloren). Dies ist ein wichtiger Sicherungsmechanismus.
- Blockierend vs. Nicht-blockierend: Wählen Sie
pg_try_advisory_lock()für nicht-blockierendes Verhalten (geeignet für "einmal auszuführende" Aufgaben, bei denen andere überspringen sollen) undpg_advisory_lock()für blockierendes Verhalten (wenn Instanzen darauf warten sollen, dass sie an der Reihe sind). Seien Sie vorsichtig mit blockierenden Sperren in verteilten Systemen, um Deadlocks oder lange Wartezeiten zu vermeiden. - Überwachung: Obwohl Advisory Locks nicht direkt in standardmäßigen PostgreSQL-Überwachungsansichten sichtbar sind, können Sie
pg_locksabfragen, um aktive Advisory Locks anzuzeigen und potenzielle Probleme zu identifizieren. Suchen Sie nachlocktype = 'advisory'. - Granularität: Achten Sie auf die Granularität Ihrer Sperren. Zu breite Sperren können die Parallelität verringern, während zu feingranulare Sperren die Komplexität erhöhen können.
Fazit
PostgreSQL Advisory Locks bieten einen leistungsfähigen, leichtgewichtigen und oft unterschätzten Mechanismus zur Koordination verteilter Aufgaben. Durch die Nutzung dieser anwendungsebene Sperren können Entwickler robuste Synchronisationsmuster direkt in ihrer Datenbank implementieren und für viele gängige Szenarien den Overhead und die Komplexität separater Koordinierungsdienste vermeiden. Dieser Ansatz bietet Einfachheit, Zuverlässigkeit und Effizienz und macht ihn zu einem hervorragenden Werkzeug für die Orchestrierung von Aufgaben und die Verwaltung gemeinsam genutzter Ressourcen in PostgreSQL-basierten verteilten Systemen.

